Goal
A host-side, torch-native way to allocate buffers that mori CCO can consume. torch owns the lifecycle (device, stream, caching, rendezvous, tensor); CCO owns the window semantics (layout, peer addressing, MR, signals). The allocator is the seam, not a third memory subsystem.
This is the design #544 is being refactored around. Packaging of the resulting module is #549; this issue is only about the model.
Stage 1 — pointer-array model (what torch symm-mem actually is)
SymmetricMemory exposes peers as buffer_ptrs / buffer_ptrs_dev: an array of per-peer base addresses, one entry per rank. Every shipped backend does this, including the ones whose heap is not scattered at all:
| backend |
peer VA derivation |
flat reservation underneath? |
| CUDA / IPC |
buffer_ptrs[r] from imported handles |
no |
| NVSHMEM |
nvshmem_ptr(p) → pe_bases[pe] + off |
yes — cuMemAddressReserve(npes * heap_size), peers at globalBase + slot*heapSize, but slots are rotated from mype+1, so pe is not the slot index and the array is unavoidable |
| NCCL/RCCL |
buffer_ptrs is empty today; pytorch#192524 is adding LSA peer pointers |
yes — ncclGetLsaPointer(w, off, peer) = add4G(lsaFlatBase, peer*stride4G) + off |
So stage 1 is the plain torch contract and nothing else: alloc / rendezvous / buffer_ptrs(+_dev) / rank / world_size, no flat view in the public API, signal pad compiled out while its ops are unimplemented. That makes mori a drop-in backend for any existing torch symm-mem kernel or op.
This costs nothing measurable. A/B of the two addressing modes in the all2all example, same kernel, same window:
|
flat (base + rank*stride) |
pointer array |
| gfx950, 8 ranks, 4 MiB |
1912.0 GB/s |
1909.4 GB/s |
| gfx1250, 4 ranks, 4 MiB |
1714.7 GB/s |
1702.9 GB/s |
Stage 2 — LSA window model (base + rank·stride), reusing CCO's scheme
Rather than invent a third layout, adopt CCO's window verbatim — include/mori/cco/cco.hpp, ccoWindowDevice:
winBase = flatBase + slotOffset
peer_va = winBase + ((uint64_t)peerLsaRank * stride4G << 32) + offset
Same 4 GiB-quantised stride as NCCL's add4G, and indexing by LSA rank, not world rank.
CCO already has the host hooks for precisely this handoff:
ccoMemImport(comm, externalPtr, size, &ptr) — aliases an external HIP VMM allocation into the flat-VA slot; the header's own example is "e.g. a torch.symm_mem buffer"
ccoWindowRegister(comm, externalPtr, size, &win, &localPtr) (overload C) — import + register, returns the flat-VA alias
ccoGetPeerPtr(comm, localPtr, pe) — host-computable peer VA, documented as the way to pre-fill a peer-pointer table that a kernel dereferences
So stage 2 does not add a parallel implementation: the allocator allocates/exports/maps under torch's lifecycle, hands the allocation to CCO, and CCO owns the window. The flat view returned to Python becomes a literal ccoWindow view — (winBase, stride4G, lsaRank), named after CCO rather than the ad-hoc flat_layout() in the current draft — and stage 1's buffer_ptrs become ccoGetPeerPtr results, so there is one source of truth for peer addressing instead of two.
Open items for stage 2:
ccoGetPeerPtr has no Cython binding — cco.pxd exposes ccoMemAlloc / ccoMemImport / all three ccoWindowRegister overloads, but not this one.
- Bootstrap mapping: torch calls
rendezvous(group_name) per allocation, whereas ccoCommCreate takes perRankVmmSize up front and reserves the flat VA once per communicator. Needs a policy for sizing and for caching a comm per process group.
- Handle type must agree between the two paths (fabric vs POSIX fd), including the granularity shift that
requestedHandleType causes.
Stage 3 (future, needs CCO co-design) — scale-out and SDMA
Both axes are already visible in ccoWindowDevice, which is why they belong to CCO and not to the allocator:
- Scale-out.
ibgdaWin carries the per-window MR (peerRkeys[worldSize], lkey, iova=0 + offset), so a peer with no LSA slot is representable. torch has a precedent for the mixed case: NVSHMEM's nvshmem_ptr returns null for network peers and the backend flips world_within_cuda_p2p_ off. Same shape here — null entries in buffer_ptrs, plus a window handle the kernel uses for RDMA.
- SDMA. Signals live on
ccoDevComm::sdma, per-DevComm rather than per-window. This is where the compiled-out signal pad should come back — as CCO's signal pool, not a private 9216-byte pad — but it needs a DevComm, which the allocator does not own today.
Neither is in scope for #544.
Consequences for #544
Reorganise it down to stage 1: the torch contract, pointer array only, flat view dropped from the public surface until it is a CCO window. The example keeps both addressing modes, as the benchmark that will justify stage 2. Remaining known gap: releasing a rendezvous'd window segfaults at world_size >= 4 (teardown is off by default, MORI_SYMM_TEARDOWN=1 re-enables) — stage 2 may dissolve it outright, since CCO would own unmap ordering.
Goal
A host-side, torch-native way to allocate buffers that mori CCO can consume. torch owns the lifecycle (device, stream, caching,
rendezvous, tensor); CCO owns the window semantics (layout, peer addressing, MR, signals). The allocator is the seam, not a third memory subsystem.This is the design #544 is being refactored around. Packaging of the resulting module is #549; this issue is only about the model.
Stage 1 — pointer-array model (what torch symm-mem actually is)
SymmetricMemoryexposes peers asbuffer_ptrs/buffer_ptrs_dev: an array of per-peer base addresses, one entry per rank. Every shipped backend does this, including the ones whose heap is not scattered at all:buffer_ptrs[r]from imported handlesnvshmem_ptr(p)→pe_bases[pe] + offcuMemAddressReserve(npes * heap_size), peers atglobalBase + slot*heapSize, but slots are rotated frommype+1, sopeis not the slot index and the array is unavoidablebuffer_ptrsis empty today; pytorch#192524 is adding LSA peer pointersncclGetLsaPointer(w, off, peer) = add4G(lsaFlatBase, peer*stride4G) + offSo stage 1 is the plain torch contract and nothing else:
alloc/rendezvous/buffer_ptrs(+_dev)/rank/world_size, no flat view in the public API, signal pad compiled out while its ops are unimplemented. That makes mori a drop-in backend for any existing torch symm-mem kernel or op.This costs nothing measurable. A/B of the two addressing modes in the all2all example, same kernel, same window:
base + rank*stride)Stage 2 — LSA window model (
base + rank·stride), reusing CCO's schemeRather than invent a third layout, adopt CCO's window verbatim —
include/mori/cco/cco.hpp,ccoWindowDevice:Same 4 GiB-quantised stride as NCCL's
add4G, and indexing by LSA rank, not world rank.CCO already has the host hooks for precisely this handoff:
ccoMemImport(comm, externalPtr, size, &ptr)— aliases an external HIP VMM allocation into the flat-VA slot; the header's own example is "e.g. a torch.symm_mem buffer"ccoWindowRegister(comm, externalPtr, size, &win, &localPtr)(overload C) — import + register, returns the flat-VA aliasccoGetPeerPtr(comm, localPtr, pe)— host-computable peer VA, documented as the way to pre-fill a peer-pointer table that a kernel dereferencesSo stage 2 does not add a parallel implementation: the allocator allocates/exports/maps under torch's lifecycle, hands the allocation to CCO, and CCO owns the window. The flat view returned to Python becomes a literal
ccoWindowview —(winBase, stride4G, lsaRank), named after CCO rather than the ad-hocflat_layout()in the current draft — and stage 1'sbuffer_ptrsbecomeccoGetPeerPtrresults, so there is one source of truth for peer addressing instead of two.Open items for stage 2:
ccoGetPeerPtrhas no Cython binding —cco.pxdexposesccoMemAlloc/ccoMemImport/ all threeccoWindowRegisteroverloads, but not this one.rendezvous(group_name)per allocation, whereasccoCommCreatetakesperRankVmmSizeup front and reserves the flat VA once per communicator. Needs a policy for sizing and for caching a comm per process group.requestedHandleTypecauses.Stage 3 (future, needs CCO co-design) — scale-out and SDMA
Both axes are already visible in
ccoWindowDevice, which is why they belong to CCO and not to the allocator:ibgdaWincarries the per-window MR (peerRkeys[worldSize],lkey, iova=0 + offset), so a peer with no LSA slot is representable. torch has a precedent for the mixed case: NVSHMEM'snvshmem_ptrreturns null for network peers and the backend flipsworld_within_cuda_p2p_off. Same shape here — null entries inbuffer_ptrs, plus a window handle the kernel uses for RDMA.ccoDevComm::sdma, per-DevComm rather than per-window. This is where the compiled-out signal pad should come back — as CCO's signal pool, not a private 9216-byte pad — but it needs a DevComm, which the allocator does not own today.Neither is in scope for #544.
Consequences for #544
Reorganise it down to stage 1: the torch contract, pointer array only, flat view dropped from the public surface until it is a CCO window. The example keeps both addressing modes, as the benchmark that will justify stage 2. Remaining known gap: releasing a rendezvous'd window segfaults at
world_size >= 4(teardown is off by default,MORI_SYMM_TEARDOWN=1re-enables) — stage 2 may dissolve it outright, since CCO would own unmap ordering.