Skip to content

[RFC] torch SymmetricMemory allocator over CCO: staged design (pointer array -> LSA window -> scale-out/SDMA) #557

Description

@carlushuang

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:

  1. ccoGetPeerPtr has no Cython binding — cco.pxd exposes ccoMemAlloc / ccoMemImport / all three ccoWindowRegister overloads, but not this one.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions