Skip to content

Add bulk asynchronous copy primitives - #853

Open
Changho Hwang (chhwang) wants to merge 5 commits into
mainfrom
feature/tma-bulk-memory-channel
Open

Add bulk asynchronous copy primitives#853
Changho Hwang (chhwang) wants to merge 5 commits into
mainfrom
feature/tma-bulk-memory-channel

Conversation

@chhwang

@chhwang Changho Hwang (chhwang) commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds pointer-based 1-D bulk-copy primitives for NVIDIA sm_90+ in mscclpp/bulk_device.hpp:

  • BulkBarrier for reusable async-load completion
  • bulkLoad and bulkStore for global ↔ shared copies
  • bulkReduceStore<T> for copy-engine reduction into global or peer memory
  • commit, completion-wait, source-reuse-wait, and proxy-fence helpers
  • isBulkSupported() / is_bulk_supported capability queries

The API operates on global pointers, including peer-mapped pointers. It does not add MemoryChannel methods: EP kernels use raw peer-pointer arrays for data movement and channels only for synchronization. The API covers all TMA instructions used by #852 and qinghuazhou/unified_ep_bench_ht_python.

Unsupported device targets fail at compile time instead of silently doing nothing. BulkBarrier storage remains host-visible so launch code can size dynamic shared memory. The store API separates destination completion from source-tile reuse for pipelined kernels.

bulkReduceStore currently supports Add for float, __nv_bfloat16, and uint32_t; unsupported combinations fail a static_assert.

Tests

  • Single-GPU tests: load, multi-source gather, barrier reuse, pipelined store, and reduction
  • Multi-rank EP-shaped tests:
    • staged dispatch to peer memory
    • double-buffered multi-source combine
    • push combine with remote reduction
  • Doxygen/Sphinx coverage for the new API
  • Python binding build and import
Platform Result
H200, sm_90 unit_tests 40/40; mp_unit_tests 60/60; patterns 3/3 at 6 ranks
GB200, sm_100 unit_tests 40/40; non-IB mp_unit_tests 32/32; patterns 3/3 at 4 ranks
MI300X, ROCm Results match the merge-base: 23 passed / 9 skipped; non-IB mp tests 24 passed / 3 pre-existing failures

Limitation

Concurrent reduction from several GPUs into the same peer address produced exact results across repeated H200 and GB200 runs, but cross-peer atomicity was not found in the PTX documentation. The header documents this as empirical rather than guaranteed.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Comment thread include/mscclpp/copy_device.hpp Outdated
Comment thread include/mscclpp/memory_channel_device.hpp Outdated
Comment thread include/mscclpp/memory_channel_device.hpp Outdated
Comment thread include/mscclpp/memory_channel_device.hpp Outdated
Expose the sm_90+ bulk copy engine (cp.async.bulk / cp.reduce.async.bulk) as a
pointer-based device API in a new header, mscclpp/bulk_device.hpp.

The primitives are channel-free by design. Bulk copies move bytes between global
and shared memory and do not care whether the global side is local or peer
mapped; channels answer where a peer's memory is and how to synchronize with it.
Those concerns compose through a plain pointer. The expert-parallel kernels that
motivate this feature gather from arrays of raw peer pointers and use channels
only for signal/wait, so binding the primitives to a channel would not serve
them. This also matches how SwitchChannel exposes multimem: a public,
pointer-based primitive is the foundation.

Surface:
  BulkBarrier            load completion, with caller-held phase parity
  bulkLoad               global -> shared, tracked by a barrier
  bulkStore              shared -> global
  bulkReduceStore<T,Op>  shared -> global, accumulating at the destination
  bulkStoreCommit        close the current bulk group
  bulkStoreWait<N>       stores have landed
  bulkStoreWaitSource<N> source tiles are reusable, stores may be in flight
  bulkFence              order generic shared accesses against the async proxy
  isBulkSupported()      host capability query, alongside isNvlsSupported()

Notes on specific choices:

- MSCCLPP_BULK_AVAILABLE gates the declarations, so unguarded use on an
  unsupported target is a compile error rather than a silent no-op. This follows
  the existing __CUDA_ARCH__ >= 900 call-site guards used for NVLS.
- BulkBarrier storage is declared on every target, including host compilation,
  because host code must size the dynamic shared memory that holds barriers.
  Only the operations are gated.
- expect, arrive and arriveAndExpect are separate so a multi-source gather can
  accumulate N loads against one barrier and wait once. Every expect for a batch
  must precede the arrival that completes the arrival count; arriveAndExpect
  carrying the batch total is documented as the recommended form.
- init() includes the proxy fence that publishes the barrier to the async proxy.
  relaxedInit() omits it so an array of barriers can be set up under one fence,
  mirroring signal() and relaxedSignal().
- wait() advances the phase, so a barrier is initialized once and reused rather
  than reinitialized per batch.
- bulkStoreWait and bulkStoreWaitSource are distinct because a double-buffered
  store pipeline needs to refill a tile without draining the store.

Tests. test/unit/bulk_tests.cu covers the primitives on a single GPU. The
reduction tests seed the destination so they distinguish accumulate from
overwrite. test/mp_unit/bulk_pattern_tests.cu adds BulkPatternTest, three
multi-rank kernels shaped after expert-parallel dispatch and combine: a staged
push, a multi-source pull and reduce that is double buffered across chunks, and
the same reduction expressed as a push using bulkReduceStore.

Verified on H200 (sm_90): unit_tests 40/40, mp_unit_tests 60/60 at 2 ranks,
BulkPatternTest 3/3 at 6 ranks. Guard behavior confirmed: guarded code builds at
sm_80 and for multi-arch sm_80+sm_90, unguarded code fails to compile at sm_80,
and an unsupported reduction type fails its static_assert.
@chhwang
Changho Hwang (chhwang) force-pushed the feature/tma-bulk-memory-channel branch from c407c26 to bfb4d24 Compare August 4, 2026 17:17
<cuda_bf16.h> was included only where MSCCLPP_BULK_AVAILABLE is 1, so
__nv_bfloat16 could not be named in a host translation unit and
bulkReduceStore<__nv_bfloat16> could not be instantiated from one. The unit test
only compiled because gpu_utils.hpp happened to pull the type in.

Gate the include on MSCCLPP_DEVICE_CUDA instead, matching
switch_channel_device.hpp. Plain host builds without the CUDA toolkit are
unaffected.

Found while validating on GB200 (sm_100), where the header is used without that
incidental include.

Verified on GB200 (sm_100, CUDA 13.0, aarch64): 11/11 standalone checks
including peer-memory load, store and reduce, and 4-GPU concurrent accumulate
into one buffer over 50 iterations. Guard behavior holds under CUDA 13: guarded
code builds at sm_80 and multi-arch sm_80+sm_100a, unguarded code fails to
compile at sm_80, plain g++ reports sizeof(BulkBarrier)=8. Re-verified on H200
(sm_90): unit_tests 40/40, mp_unit_tests 60/60 at 2 ranks.
Three cleanups found by running things that had not been run.

Doxygen strips everything guarded by MSCCLPP_BULK_AVAILABLE, because the macro
derives from __CUDA_ARCH__ and doxygen does not define it. Every directive added
to cpp_api.rst therefore failed:

  WARNING: doxygenfunction: Cannot find function "mscclpp::bulkLoad" in doxygen
  xml output for project "mscclpp" from directory: ./doxygen/xml

Add MSCCLPP_BULK_AVAILABLE=1 to PREDEFINED in the Doxyfile, next to the existing
MSCCLPP_DEVICE_COMPILE and MSCCLPP_DEVICE_CUDA entries that exist for the same
reason. Sphinx now emits no bulk warnings and every symbol renders.

Bind isBulkSupported() as is_bulk_supported and export it, matching
is_nvls_supported.

Replace the unit test's homegrown compute-capability check with
isBulkSupported(). The mp_unit tests already used it; having two ways to ask the
same question is what the host query exists to avoid.

Also document that the cross-device atomicity of bulkReduceStore() is
established by measurement rather than by the PTX documentation, so callers do
not take it as guaranteed.

Verified on H200: docs build clean of bulk warnings and all ten symbols present
in the generated HTML; unit_tests 40/40; mp_unit_tests 60/60 at 2 ranks; Python
bindings build and mscclpp.is_bulk_supported() returns True.
@chhwang Changho Hwang (chhwang) changed the title Add TMA bulk-load support to MemoryChannel Add bulk asynchronous copy primitives Aug 7, 2026
Where MSCCLPP_BULK_AVAILABLE is 0, BulkBarrier has no operations, only the
storage declared so host code can size shared memory holding barriers. Clang
then warns on every ROCm translation unit that includes the header:

  include/mscclpp/bulk_device.hpp:139:23: warning: private field 'mbar_' is not
  used [-Wunused-private-field]

Mark the member maybe_unused. No effect where the operations exist.

Verified on MI300X (ROCm 7.2, gfx942): warning count for this field 0, and both
suites match the merge-base exactly -- unit_tests 32 ran / 23 passed / 9 skipped,
mp_unit_tests --filter=-Ib 29 ran / 24 passed / 3 failed. The three failures are
CommunicatorTest.BasicWrite, .WriteWithDeviceSemaphores and
.WriteWithHostSemaphores, which fail identically on the merge-base; this node has
no IB device. Re-verified on H200: unit_tests 40/40, mp_unit_tests 60/60.
Mark kernel parameters [[maybe_unused]] at their declarations instead of adding
#else blocks with (void)param casts when bulk copy is unavailable. This keeps
the unsupported-target path declarative and removes 25 lines of warning-only
code.

Verified with nvcc at sm_80, the H200 bulk tests (6/6 unit, 3/3 multi-rank), and
the MI300X ROCm build (no unused-parameter warnings; unit tests 23 passed / 9
skipped).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants