Mori 库提供了一套灵活的内存管理方案,通过三种模式(StaticHeap、VMHeap、Isolation)满足不同场景的需求。本文分析每种模式在 初始化 和 P2P 内存映射 过程中的技术细节。
cpp
void MemoryStatesInit(ShmemStates* states) {
application::Context* context = states->rdmaStates->commContext;
// Create memory management objects
states->memoryStates = new MemoryStates();
states->memoryStates->symmMemMgr =
new application::SymmMemManager(*states->bootStates->bootNet, *context);
states->memoryStates->mrMgr =
new application::RdmaMemoryRegionManager(*context->GetRdmaDeviceContext());
// Handle Isolation mode (no heap allocation needed)
if (states->mode == ShmemMode::Isolation) {
MORI_SHMEM_INFO("Running in isolation mode (no heap allocation)");
return;
}
// Configure heap type (applies to both VMM and static heap)
application::HeapType heapType = ConfigureHeapType();
states->memoryStates->heapType = heapType;
// Initialize heap based on mode
switch (states->mode) {
case ShmemMode::VMHeap: {
// Try to initialize VMM heap
bool vmmSuccess = TryInitializeVMMHeap(states, heapType);
if (vmmSuccess) {
return; // VMM heap initialized successfully
}
// Fallback to static heap if VMM initialization failed
MORI_SHMEM_INFO("Falling back to static heap mode");
states->mode = ShmemMode::StaticHeap;
// Fall through to StaticHeap case
[[fallthrough]];
}
case ShmemMode::StaticHeap: {
InitializeStaticHeap(states, heapType);
break;
}
default: {
MORI_SHMEM_ERROR("Unknown heap mode: {}", static_cast<int>(states->mode));
throw std::runtime_error("Unknown heap mode");
}
}
}
cpp
void* ShmemMalloc(size_t size) {
ShmemStates* states = ShmemStatesSingleton::GetInstance();
states->CheckStatusValid();
if (size == 0) {
return nullptr;
}
// Dispatch to appropriate allocator based on mode
switch (states->mode) {
case ShmemMode::StaticHeap:
return AllocateStaticHeap(states, size);
case ShmemMode::VMHeap:
return AllocateVMMHeap(states, size);
case ShmemMode::Isolation:
return AllocateIsolation(states, size);
default:
MORI_SHMEM_ERROR("Unknown ShmemMode: {}", static_cast<int>(states->mode));
return nullptr;
}
}
对称内存与 P2P 访问基础
Mori 的对称内存模型要求:所有 PE(Processing Element,即进程)在同一虚拟地址偏移处看到相同的内容。这意味着一个指针值在任何 PE 上解引用时,都能正确访问对应 PE 的本地物理内存(或远程物理内存的映射)。
实现这一目标的核心数据结构是 SymmMemObj,它记录了:
- localPtr:本地虚拟地址。
- peerPtrspe:每个 PE 上该内存块的虚拟地址(用于 RDMA 或直接访问)。
- p2pPeerPtrspe:每个 PE 上该内存块在当前进程地址空间中的可直接解引用的映射地址(用于同节点 P2P)。
P2P 映射建立(RegisterSymmMemObj 中):
- 调用 hipIpcGetMemHandle 获取本地物理内存的 IPC 句柄,并通过全局通信(bootNet.Allgather)交换所有 PE 的句柄。
- 对每个同节点且非同进程的 PE,调用 hipIpcOpenMemHandle 将其物理内存映射到当前进程的虚拟地址空间,得到可直接访问的指针,存入 p2pPeerPtrspe。
- 对同进程的 PE,直接使用原始指针,并调用 hipDeviceEnablePeerAccess 使能 GPU 间 P2P 路由。
经过这一步,p2pPeerPtrs 数组就成为了"远程本地内存"的快捷访问表,GPU 内核或 CPU 均可通过它直接读写同节点其他 GPU 的内存。
三种模式概览
| 模式 | 物理分配方式 | P2P 映射建立时机 | 地址复用 | 典型场景 |
|---|---|---|---|---|
| StaticHeap | 初始化时一次性 hipMalloc 整块 | 初始化时一次 IPC | 是 | 固定内存,频繁分配/释放 |
| VMHeap | 按需分配物理页(chunk) | 首次分配 chunk 时 | 是 | 动态大内存,多节点通信 |
| Isolation | 每次 ShmemMalloc 独立分配 | 每次分配时 IPC | 否 | 少量特殊分配,需 flags |
StaticHeap模式: 预分配大堆,零拷贝子区域
初始化阶段
cpp
static void InitializeStaticHeap(ShmemStates* states, application::HeapType heapType) {
MORI_SHMEM_TRACE("Initializing static symmetric heap");
// Parse heap size from environment variable
const char* heapSizeEnv = std::getenv("MORI_SHMEM_HEAP_SIZE");
size_t heapSize = DEFAULT_STATIC_SYMMETRIC_HEAP_SIZE;
if (heapSizeEnv) {
heapSize = ParseSizeString(heapSizeEnv);
}
MORI_SHMEM_TRACE("Static heap size: {} bytes ({} MB)", heapSize, heapSize / (1024 * 1024));
// Allocate GPU memory based on heap type
void* staticHeapPtr = nullptr;
if (heapType == application::HeapType::Uncached) {
HIP_RUNTIME_CHECK(hipExtMallocWithFlags(&staticHeapPtr, heapSize, hipDeviceMallocUncached));
} else {
HIP_RUNTIME_CHECK(hipMalloc(&staticHeapPtr, heapSize));
}
// Initialize memory
HIP_RUNTIME_CHECK(hipMemset(staticHeapPtr, 0, heapSize));
// Register with symmetric memory manager
application::SymmMemObjPtr heapObj =
states->memoryStates->symmMemMgr->RegisterSymmMemObj(staticHeapPtr, heapSize, true);
if (!heapObj.IsValid()) {
MORI_SHMEM_ERROR("Failed to allocate static symmetric heap!");
throw std::runtime_error("Failed to allocate static symmetric heap");
}
// Store heap metadata
states->memoryStates->staticHeapBasePtr = heapObj.cpu->localPtr;
states->memoryStates->staticHeapSize = heapSize;
states->memoryStates->staticHeapObj = heapObj;
// IMPORTANT: Start with a small offset to avoid collision between heap base address
// and first ShmemMalloc allocation. Without this, when staticHeapUsed == 0,
// the first ShmemMalloc would return staticHeapBasePtr, which is the same address
// as the heap itself in memObjPool, causing the heap's SymmMemObj to be overwritten.
constexpr size_t HEAP_INITIAL_OFFSET = 256;
states->memoryStates->staticHeapUsed = HEAP_INITIAL_OFFSET;
// Initialize VA manager for static heap to enable memory reuse
states->memoryStates->symmMemMgr->InitHeapVAManager(
reinterpret_cast<uintptr_t>(states->memoryStates->staticHeapBasePtr), heapSize);
MORI_SHMEM_TRACE("Static heap allocated at {} (local), size {} bytes, initial offset {} bytes",
states->memoryStates->staticHeapBasePtr, heapSize, HEAP_INITIAL_OFFSET);
MORI_SHMEM_INFO("Static heap initialized successfully");
}
物理内存:通过 hipMalloc(或 hipExtMallocWithFlags 若 heapType=Uncached)分配一块固定大小的连续 GPU 内存(默认 4GB, DEFAULT_STATIC_SYMMETRIC_HEAP_SIZE)。
父堆对象:调用 RegisterSymmMemObj(staticHeapPtr, heapSize, true) 注册整个堆地址。
此调用会执行完整的 P2P 映射流程:交换 IPC 句柄、打开同节点其他 PE 的映射,填充 staticHeapObj.cpu->p2pPeerPtrs。
VA 管理器:初始化 HeapVAManager,基址为堆起始地址,大小为堆总大小。该管理器使用 First‑Fit 算法管理堆内的虚拟地址子区域,支持释放后合并相邻空闲块,实现地址复用。
预留偏移:为防止第一个 ShmemMalloc 返回的地址与父堆基址重合(导致 memObjPool 冲突),设置 staticHeapUsed = 256,使首次分配从堆基址 + 256 处开始。
分配子区域(ShmemMalloc → AllocateStaticHeap)
cpp
static void* AllocateStaticHeap(ShmemStates* states, size_t size) {
// Align to 256 bytes for better performance
constexpr size_t ALIGNMENT = 256;
// Use VA manager to allocate address (thread-safe, handles reuse)
uintptr_t allocAddr =
states->memoryStates->symmMemMgr->GetHeapVAManager()->Allocate(size, ALIGNMENT);
if (allocAddr == 0) {
MORI_SHMEM_ERROR(
"Out of static heap memory! Requested: {} bytes. Hint: Increase via MORI_SHMEM_HEAP_SIZE "
"(default: 2GB)",
size);
return nullptr;
}
void* ptr = reinterpret_cast<void*>(allocAddr);
// Register the allocated region as a sub-region of the static heap
states->memoryStates->symmMemMgr->RegisterStaticHeapSubRegion(
ptr, size, &states->memoryStates->staticHeapObj);
uintptr_t baseAddr = reinterpret_cast<uintptr_t>(states->memoryStates->staticHeapBasePtr);
MORI_SHMEM_TRACE("Allocated {} bytes at ptr={:#x} (offset={}, aligned to 256={})", size,
allocAddr, allocAddr - baseAddr, (allocAddr % 256 == 0 ? "yes" : "no"));
return ptr;
}
- 调用 heapVAManager->Allocate(size, 256) 获得一段虚拟地址 allocAddr(对齐到 256 字节)。
- 调用 RegisterStaticHeapSubRegion(allocAddr, size, &staticHeapObj):
- 计算相对于堆基址的偏移 offset = allocAddr - heapBase。
- 浅拷贝(Shallow Copy) 父堆的元数据:
- peerPtrspe = staticHeapObj.peerPtrspe + offset
- p2pPeerPtrspe = staticHeapObj.p2pPeerPtrspe + offset
- ipcMemHandles、peerRkeys、lkey 直接指向父堆的数组(不重新分配/注册)。
将新对象插入 memObjPool,键为 allocAddr。
关键点:
无额外 IPC 或 RDMA 注册:所有 P2P 映射已在父堆初始化时完成,子区域仅通过偏移量复用,分配开销极低(几乎只是 O(1) 的地址簿操作)。
释放:FreeStaticHeap 调用 DeregisterStaticHeapSubRegion 释放 CPU/GPU 端的元数据,并将地址归还给 VA 管理器(heapVAManager->Free),物理内存始终保留。
VMHeap 模式:按需物理页,动态 P2P 映射
初始化阶段
cpp
static bool TryInitializeVMMHeap(ShmemStates* states, application::HeapType heapType) {
MORI_SHMEM_TRACE("Initializing VMM-based dynamic heap");
// Check ROCm and hardware VMM support
bool rocmSupportsVMM = IsROCmVersionGreaterThan7();
bool hardwareSupportsVMM = states->memoryStates->symmMemMgr->IsVMMSupported();
MORI_SHMEM_INFO("VMM support: ROCm >= 7.0: {}, Hardware: {}", rocmSupportsVMM,
hardwareSupportsVMM);
if (!rocmSupportsVMM || !hardwareSupportsVMM) {
MORI_SHMEM_INFO("VMM not supported, will fallback to static heap");
return false;
}
// Parse VMM configuration from environment variables
const char* chunkSizeEnv = std::getenv("MORI_SHMEM_VMM_CHUNK_SIZE");
const char* vmmHeapSizeEnv = std::getenv("MORI_SHMEM_HEAP_SIZE");
size_t chunkSize = 0; // 0 means auto-detect
size_t vmmHeapSize = DEFAULT_VMM_SYMMETRIC_HEAP_SIZE;
if (chunkSizeEnv) {
chunkSize = std::max(ParseSizeString(chunkSizeEnv), DEFAULT_VMM_MIN_CHUNK_SIZE);
}
if (vmmHeapSizeEnv) {
vmmHeapSize = ParseSizeString(vmmHeapSizeEnv);
}
MORI_SHMEM_TRACE("VMM heap config: virtual size {} bytes ({} MB), chunk size {} bytes ({} KB)",
vmmHeapSize, vmmHeapSize / (1024 * 1024), chunkSize, chunkSize / 1024);
// Try to initialize VMM heap
bool success =
states->memoryStates->symmMemMgr->InitializeVMMHeap(vmmHeapSize, chunkSize, heapType);
if (success) {
// Store VMM heap metadata
states->memoryStates->useVMMHeap = true;
states->memoryStates->vmmHeapInitialized = true;
states->memoryStates->vmmHeapVirtualSize = vmmHeapSize;
states->memoryStates->vmmHeapChunkSize = states->memoryStates->symmMemMgr->GetVMMChunkSize();
states->memoryStates->vmmHeapObj = states->memoryStates->symmMemMgr->GetVMMHeapObj();
states->memoryStates->vmmHeapBaseAddr = states->memoryStates->vmmHeapObj.cpu->localPtr;
// Initialize VA Manager for VMM heap to enable memory reuse
// Pass granularity (chunkSize) to ensure VA blocks don't cross physical memory boundaries
states->memoryStates->symmMemMgr->InitHeapVAManager(
reinterpret_cast<uintptr_t>(states->memoryStates->vmmHeapBaseAddr), vmmHeapSize,
states->memoryStates->vmmHeapChunkSize);
MORI_SHMEM_TRACE(
"VMM heap VA Manager initialized: base={}, size={} bytes, granularity={} bytes",
states->memoryStates->vmmHeapBaseAddr, vmmHeapSize, states->memoryStates->vmmHeapChunkSize);
MORI_SHMEM_INFO("VMM heap initialized successfully");
return true;
} else {
MORI_SHMEM_INFO("Failed to initialize VMM heap, will fallback to static heap");
return false;
}
}
- 虚拟地址预留:symmMemMgr->InitializeVMMHeap调用 hipMemAddressReserve 为整个 VMM 堆预留虚拟地址空间(默认 16GB)。为每个 P2P 对等体分配独立的虚拟区(vmmPeerBasePtrspe),确保对称偏移。
- 堆对象:InitializeVMMHeap中调用CreateVMMHeapObject 创建 vmmHeapObj,其 peerPtrs 填充为各 PE 的虚拟基址(通过 bootNet.Allgather交换),但 p2pPeerPtrs 初始为 vmmPeerBasePtrs 的地址(尚未映射物理内存)。
- VA 管理器:初始化时传入 chunkSize(默认 64MB)作为粒度,确保 VA 分配不会跨越 chunk 边界,避免后续物理页映射不对齐。
分配过程(AllocateVMMHeap → VMMAllocChunk)
cpp
SymmMemObjPtr SymmMemManager::VMMAllocChunk(size_t size, HeapType heapType) {
std::lock_guard<std::mutex> lock(vmmLock);
if (!vmmInitialized || !heapVAManager) {
MORI_APP_WARN("VMMAllocChunk failed: VMM heap not initialized");
return SymmMemObjPtr{nullptr, nullptr};
}
int worldSize = bootNet.GetWorldSize();
int rank = bootNet.GetLocalRank();
// Step 1: Allocate virtual address from VA manager
uintptr_t allocAddr = AllocateVirtualAddress(size);
if (allocAddr == 0) {
return SymmMemObjPtr{nullptr, nullptr};
}
void* startPtr = reinterpret_cast<void*>(allocAddr);
uintptr_t baseAddr = reinterpret_cast<uintptr_t>(vmmPeerBasePtrs[rank]);
size_t offset = allocAddr - baseAddr;
// Step 2: Verify VA allocation consistency across all PEs
if (!VerifyVAConsistency(allocAddr, size, offset, rank, worldSize)) {
return SymmMemObjPtr{nullptr, nullptr};
}
// Step 3: Calculate chunk information
size_t startChunk = offset / vmmChunkSize;
size_t endOffset = offset + size;
size_t endChunk = (endOffset + vmmChunkSize - 1) / vmmChunkSize;
size_t chunksNeeded = endChunk - startChunk;
MORI_APP_TRACE("VMMAlloc: rank={} VA={:p} size={} chunks=[{},{})", rank, startPtr, size,
startChunk, endChunk);
// Step 4: Check if these chunks already have physical memory allocated
bool needPhysicalAlloc = false;
for (size_t i = 0; i < chunksNeeded; ++i) {
size_t chunkIdx = startChunk + i;
if (chunkIdx >= vmmMaxChunks || !vmmChunks[chunkIdx].isAllocated) {
needPhysicalAlloc = true;
break;
}
}
// Step 5: Allocate physical memory if needed, otherwise reuse existing
if (needPhysicalAlloc) {
MORI_APP_TRACE("VMMAlloc: rank={} allocating {} NEW chunks", rank, chunksNeeded);
int currentDev = 0;
hipError_t result = hipGetDevice(¤tDev);
if (result != hipSuccess) {
MORI_APP_WARN("VMMAllocChunk failed: Cannot get current device, hipError: {}", result);
heapVAManager->Free(allocAddr);
return SymmMemObjPtr{nullptr, nullptr};
}
// Configure allocation properties based on heap type
hipMemAllocationProp allocProp = ConfigureAllocationProp(heapType, currentDev);
// Initialize local shareable handles (for P2P exchange)
std::vector<int> localShareableHandles(chunksNeeded);
for (size_t i = 0; i < chunksNeeded; ++i) {
size_t chunkIdx = startChunk + i;
localShareableHandles[i] = (chunkIdx < vmmMaxChunks && vmmChunks[chunkIdx].isAllocated)
? vmmChunks[chunkIdx].shareableHandle
: -1;
}
// Allocate physical memory for each chunk
for (size_t i = 0; i < chunksNeeded; ++i) {
size_t chunkIdx = startChunk + i;
// Reuse chunks that already have physical memory allocated
if (chunkIdx < vmmMaxChunks && vmmChunks[chunkIdx].isAllocated) {
vmmChunks[chunkIdx].refCount++;
MORI_APP_TRACE("VMMAlloc: rank={} reusing chunk {} (refCount={}, fd={})", rank, chunkIdx,
vmmChunks[chunkIdx].refCount, vmmChunks[chunkIdx].shareableHandle);
continue;
}
// Allocate new physical chunk
void* localChunkPtr =
static_cast<void*>(static_cast<char*>(vmmPeerBasePtrs[rank]) + chunkIdx * vmmChunkSize);
bool success = AllocateSingleChunk(vmmChunks[chunkIdx], chunkIdx, localChunkPtr, vmmChunkSize,
allocProp, rank, currentDev);
if (!success) {
CleanupAllocatedChunks(startChunk, i);
heapVAManager->Free(allocAddr);
return SymmMemObjPtr{nullptr, nullptr};
}
localShareableHandles[i] = vmmChunks[chunkIdx].shareableHandle;
}
// Step 6: Register P2P peer memory mapping
if (!RegisterP2PPeerMemory(localShareableHandles, startChunk, chunksNeeded, rank, worldSize,
currentDev)) {
return SymmMemObjPtr();
}
// Step 7: RDMA registration for RDMA transport
RegisterRdmaChunks(startChunk, chunksNeeded, rank, worldSize);
} else {
MORI_APP_TRACE("VMMAlloc: rank={} REUSE {} chunks at VA=0x{:x}", rank, chunksNeeded, allocAddr);
}
// Step 8: Register and return SymmMemObj
MORI_APP_TRACE("VMMAlloc: done VA={:p} size={}", startPtr, size);
return VMMRegisterSymmMemObj(startPtr, size, startChunk, chunksNeeded);
}
- 虚拟地址分配:通过 VA 管理器获得一段连续虚拟地址,计算所需 chunk 范围(startChunk 到 endChunk)。
- 检查 chunk 是否已分配:若目标 chunk 已存在物理内存,则增加引用计数,直接复用。
- 首次分配新 chunk:
- 物理内存创建:AllocateSingleChunk调用 hipMemCreate 为当前 GPU 分配一个 chunk 的物理内存,得到 handle。
- 本地映射:hipMemMap 将物理页映射到本地虚拟地址(vmmPeerBasePtrsrank + chunkIdx * chunkSize)。
- 导出 dmabuf FD:hipMemExportToShareableHandle 获得 POSIX 文件描述符,用于跨进程共享。
- P2P 映射建立,RegisterP2PPeerMemory:
- 通过 本地 Bootstrap 网络(基于 Unix Socket)与同节点其他 PE 交换 dmabuf FD。
- 对每个对等体,hipMemImportFromShareableHandleCompat 导入 FD,hipMemMap 将其映射到该对等体的虚拟区(vmmPeerBasePtrspe + offset),并设置访问权限。
- 记录映射关系(mappedPeers 和 importedHandles),支持引用计数。
- RegisterRdmaChunks, RDMA 注册(若启用):通过 dmabuf FD 注册 RDMA 内存区域,交换 rkeys,更新 vmmHeapObj 的 vmmRkeyInfo。
- VMMRegisterSymmMemObj 构建子对象的 SymmMemObj,其 peerPtrs 和 p2pPeerPtrs 直接指向 vmmHeapObj 的全局 chunk key 数组(vmmLkeyInfo/vmmRkeyInfo),实现轻量级视图。
RegisterP2PPeerMemory的作用是在同一个节点内,让当前GPU能够直接通过内存语义(Load/Store指令)访问同一节点内其他GPU的物理内存。
为什么要把对方内存映射到 vmmPeerBasePtrspe 而不是自己的地址?这是VMM对称内存设计的精髓:
为了确保所有PE看到相同的虚拟地址偏移(对称性),每个PE的虚拟地址空间被划分成多个区域(vmmPeerBasePtrs数组)。
当前PE(假设Rank 0)访问 Rank 1的内存 时,使用的虚拟地址是 vmmPeerBasePtrs1 + offset。
与此同时,Rank 1 访问自己的同一块内存时,使用的是 vmmPeerBasePtrs1 + offset(在Rank 1的地址空间中,这块区域恰好映射的是自己的本地物理内存)。
这样,同一个指针值,在Rank 0上触发的是跨PCIe去读Rank 1的物理内存,在Rank 1上触发的是读自己本地的物理内存,完美实现了对称内存语义(Symmetric Memory)。
释放与回收
VMMFreeChunk:递减 chunk 引用计数。若归零,则:
解除所有对等体的映射(hipMemUnmap 并释放导入句柄)。
释放本地映射和物理内存(hipMemUnmap + hipMemRelease)。
关闭 dmabuf FD。
清除对应的 chunk key(置 0),同步到 GPU。
关键点
P2P 映射建立发生在 首次分配 chunk 时,通过 dmabuf 交换实现,比 IPC 句柄方式更灵活(支持动态分配)。
VA 管理器与 chunk 粒度对齐,避免物理页浪费。
引用计数机制支持多个分配共享同一 chunk,减少物理内存使用。
Isolation 模式:独立分配,完整映射
初始化阶段
无堆:MemoryStatesInit 直接返回,不分配任何物理内存。
无 VA 管理器:每次分配都调用驱动 API。
分配过程(AllocateIsolation)
调用 SymmMemMgr->Malloc(size),内部执行 hipMalloc(或 hipExtMallocWithFlags)分配独立物理内存。
调用 RegisterSymmMemObj(ptr, size, false),完整执行 IPC 句柄交换和 hipIpcOpenMemHandle,为每个分配建立独立的 P2P 映射,并填充 p2pPeerPtrs。
将对象插入 memObjPool
静态堆 和 VMM堆的不同
物理内存的存在性不同
静态堆(RegisterSymmMemObj):初始化时通过 hipMalloc 一次性提交了全部物理内存。因此,调用 hipIpcGetMemHandle 时,物理页已经真实存在,可以直接打开映射。
VMM 堆(需要 RegisterP2PPeerMemory):初始化时仅调用 hipMemAddressReserve 预留了虚拟地址空间,并未分配任何物理页。CreateVMMHeapObject 中填写的 p2pPeerPtrs 指向的是 vmmPeerBasePtrs------这些地址在此时没有任何物理内存 backing。
只有当 ShmemMalloc 触发 VMMAllocChunk 后,才通过 hipMemCreate 真正分配物理页。因此,P2P 映射必须推迟到物理页实际存在的那一刻才能建立。
HIP API 的强制性差异(IPC vs dmabuf)
hipIpcGetMemHandle / hipIpcOpenMemHandle:专用于 hipMalloc(或 hipExtMalloc)分配的常规 GPU 内存。它适用于静态堆。
VMM 分配的物理内存:通过 hipMemCreate 创建,返回的是 hipMemGenericAllocationHandle_t。HIP 规范禁止 对此类句柄使用 hipIpcGetMemHandle(会报错)。跨进程共享 VMM 物理内存的官方途径是:
- hipMemExportToShareableHandle → 导出为 POSIX 文件描述符(dmabuf)。
- 通过 LocalBootstrapNetwork 交换 FD。
- hipMemImportFromShareableHandleCompat + hipMemMap 导入并映射。
虚拟地址布局的对称性要求(映射到特定槽位)
在 VMM 模式中,为了保证所有 PE 看到相同的虚拟地址偏移,Mori 为每个 PE 预留了独立的虚拟地址区间(vmmPeerBasePtrspe):
- 访问 PE 1 的内存时,本地使用的虚拟地址必须是 vmmPeerBasePtrs1 + offset。
- RegisterP2PPeerMemory 在处理导入的 dmabuf 时,明确调用 ImportPeerChunk:
cpp
void* peerChunkPtr = (char*)vmmPeerBasePtrs[pe] + chunkIdx * vmmChunkSize;
hipMemMap(peerChunkPtr, vmmChunkSize, 0, importedHandle, 0);
将远端物理页精确映射到预留给该 PE 的虚拟槽位。
数据传输使用p2p 还是 RDMA?
在多机集群场景中,本节点数据传输走p2p路径,跨节点数据传输走RDMA。如何区分?通过目标 PE(处理单元/进程)和预先协商好的传输类型(TransportType)进行动态路由。整个决策流程在初始化时建表,在运行时查表分支。
cpp
#define DISPATCH_TRANSPORT_TYPE(func, pe, ...) \
GpuStates* globalGpuStates = GetGlobalGpuStatesPtr(); \
application::TransportType transportType = globalGpuStates->transportTypes[pe]; \
if (transportType == application::TransportType::RDMA) { \
func<application::TransportType::RDMA>(__VA_ARGS__); \
} else if (transportType == application::TransportType::P2P) { \
func<application::TransportType::P2P>(__VA_ARGS__); \
} else if (transportType == application::TransportType::SDMA) { \
func<application::TransportType::SDMA>(__VA_ARGS__); \
} else { \
assert(false); \
}
在 Context::InitializePossibleTransports,Mori 会探测当前节点与所有其他 PE 之间的物理连接能力,并为每个 PE 标记一个 TransportType:
- TransportType::P2P:同节点、共享 PCIe 或 Infinity Fabric,支持 GPU 直接 Load/Store 访问。
- TransportType::RDMA:跨节点(不同物理服务器),必须通过 InfiniBand/RoCE 网卡通信。
- TransportType::SDMA:同节点但使用 SDMA 引擎(某些特殊场景)。