mori通信库分析(二)——对称内存初始化与 P2P 映射

 Mori 库提供了一套灵活的内存管理方案,通过三种模式(StaticHeap、VMHeap、Isolation)满足不同场景的需求。本文分析每种模式在 初始化 和 P2P 内存映射 过程中的技术细节。

MemoryStatesInit

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");
    }
  }
}

ShmemMalloc

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 中):

  1. 调用 hipIpcGetMemHandle 获取本地物理内存的 IPC 句柄,并通过全局通信(bootNet.Allgather)交换所有 PE 的句柄。
  2. 对每个同节点且非同进程的 PE,调用 hipIpcOpenMemHandle 将其物理内存映射到当前进程的虚拟地址空间,得到可直接访问的指针,存入 p2pPeerPtrspe
  3. 对同进程的 PE,直接使用原始指针,并调用 hipDeviceEnablePeerAccess 使能 GPU 间 P2P 路由。

 经过这一步,p2pPeerPtrs 数组就成为了"远程本地内存"的快捷访问表,GPU 内核或 CPU 均可通过它直接读写同节点其他 GPU 的内存。

三种模式概览

模式 物理分配方式 P2P 映射建立时机 地址复用 典型场景
StaticHeap 初始化时一次性 hipMalloc 整块 初始化时一次 IPC 固定内存,频繁分配/释放
VMHeap 按需分配物理页(chunk) 首次分配 chunk 时 动态大内存,多节点通信
Isolation 每次 ShmemMalloc 独立分配 每次分配时 IPC 少量特殊分配,需 flags

StaticHeap模式: 预分配大堆,零拷贝子区域

初始化阶段

InitializeStaticHeap

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)

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;
}
  1. 调用 heapVAManager->Allocate(size, 256) 获得一段虚拟地址 allocAddr(对齐到 256 字节)。
  2. 调用 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 映射

初始化阶段

TryInitializeVMMHeap

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;
  }
}
  1. 虚拟地址预留:symmMemMgr->InitializeVMMHeap调用 hipMemAddressReserve 为整个 VMM 堆预留虚拟地址空间(默认 16GB)。为每个 P2P 对等体分配独立的虚拟区(vmmPeerBasePtrspe),确保对称偏移。
  2. 堆对象:InitializeVMMHeap中调用CreateVMMHeapObject 创建 vmmHeapObj,其 peerPtrs 填充为各 PE 的虚拟基址(通过 bootNet.Allgather交换),但 p2pPeerPtrs 初始为 vmmPeerBasePtrs 的地址(尚未映射物理内存)。
  3. VA 管理器:初始化时传入 chunkSize(默认 64MB)作为粒度,确保 VA 分配不会跨越 chunk 边界,避免后续物理页映射不对齐。

分配过程(AllocateVMMHeap → VMMAllocChunk)

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(&currentDev);
    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);
}
  1. 虚拟地址分配:通过 VA 管理器获得一段连续虚拟地址,计算所需 chunk 范围(startChunk 到 endChunk)。
  2. 检查 chunk 是否已分配:若目标 chunk 已存在物理内存,则增加引用计数,直接复用。
  3. 首次分配新 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),支持引用计数。
  1. RegisterRdmaChunks, RDMA 注册(若启用):通过 dmabuf FD 注册 RDMA 内存区域,交换 rkeys,更新 vmmHeapObj 的 vmmRkeyInfo。
  2. 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 物理内存的官方途径是:

  1. hipMemExportToShareableHandle → 导出为 POSIX 文件描述符(dmabuf)。
  2. 通过 LocalBootstrapNetwork 交换 FD。
  3. 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)进行动态路由。整个决策流程在初始化时建表,在运行时查表分支。

DISPATCH_TRANSPORT_TYPE

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 引擎(某些特殊场景)。

Reference

rocm mori