-
Notifications
You must be signed in to change notification settings - Fork 84
[Multidevice] Handwritten distributed matmul kernels -- reference implementations for fused comm/compute kernels - #6002
[Multidevice] Handwritten distributed matmul kernels -- reference implementations for fused comm/compute kernels #6002samnordmann wants to merge 23 commits into
Conversation
Description
|
| Relevant files | |
|---|---|
| Enhancement |
test_multidevice_fused_remote_matmul.h
Header with distributed matmul types and declarations tests/cpp/test_multidevice_fused_remote_matmul.h test_multidevice_fused_remote_matmul.cpp
Test harness and benchmark implementation tests/cpp/test_multidevice_fused_remote_matmul.cpp measurement test_multidevice_fused_remote_matmul_kernel.cu
CUDA kernels for distributed matmul implementations tests/cpp/test_multidevice_fused_remote_matmul_kernel.cu baseline scalar compute scalar compute coordination symmetric_tensor.cpp
Add device peer pointer support in SymmetricTensor csrc/multidevice/symmetric_tensor.cpp pointers symmetric_tensor.h
SymmetricTensor header updates for device peer pointers csrc/multidevice/symmetric_tensor.h table |
| Configuration changes |
CMakeLists.txt
Build system updates for CUTLASS integration and new testsCMakeLists.txt |
PR Reviewer Guide
Here are some key observations to aid the review process:
Memory Management
The devicePeerPointers() method performs CUDA memory allocation and memcpy without comprehensive error handling. Consider adding try-catch blocks or NVF_CHECK for all CUDA operations to ensure proper cleanup on failure paths.
void** SymmetricTensor::devicePeerPointers() const { NVF_CHECK(are_remote_tensors_setup_ == true, "Remote tensors not setup"); if (device_peer_ptrs_ == nullptr) { std::vector<void*> host_peer_ptrs(world_size_); for (int64_t rank = 0; rank < world_size_; ++rank) { host_peer_ptrs[rank] = reinterpret_cast<void*>(remote_ptrs_[rank]); } NVFUSER_CUDA_RT_SAFE_CALL( cudaMalloc(&device_peer_ptrs_, world_size_ * sizeof(void*))); NVFUSER_CUDA_RT_SAFE_CALL(cudaMemcpy( device_peer_ptrs_, host_peer_ptrs.data(), world_size_ * sizeof(void*), cudaMemcpyHostToDevice)); } return device_peer_ptrs_; }
Synchronization Robustness
The waitOne() and waitAll() functions use atomic operations with a hardcoded kMaxPoll limit. While this prevents infinite loops, the trap instruction may be too aggressive for production use. Consider adding more graceful error handling or logging for timeout scenarios.
__device__ inline void waitOne( int32_t* local, int64_t row, int64_t m, int64_t writer, int32_t epoch) { auto* p = reinterpret_cast<unsigned int*>(local + (writer * m + row) * kVecW); int64_t s = 0; while (atomicAdd(p, 0U) < (unsigned)epoch) if (++s > kMaxPoll) asm volatile("trap;"); } __device__ inline void waitAll( int32_t* local, int64_t row, int64_t m, int64_t ws, int32_t epoch) { for (int64_t r = 0; r < ws; ++r) { auto* p = reinterpret_cast<unsigned int*>(local + (r * m + row) * kVecW); int64_t s = 0; while (atomicAdd(p, 0U) < (unsigned)epoch) if (++s > kMaxPoll) asm volatile("trap;"); } }
Architecture-Specific Code
The multimemGatherKernel contains architecture-specific inline assembly for SM90+. While appropriate for experimental code, ensure proper feature detection and fallback paths for non-Hopper architectures to prevent runtime failures.
asm volatile( "multimem.st.global.v4.f32 [%0]," " {%1, %2, %3, %4};" : : "l"((void*)(arow + vi * kVec)), "f"(__int_as_float((int)val.x)), "f"(__int_as_float((int)val.y)), "f"(__int_as_float((int)val.z)), "f"(__int_as_float((int)val.w)) : "memory"); #else (void)val; asm volatile("trap;"); #endif } for (int64_t kk = nvec * kVec + threadIdx.x; kk < k; kk += blockDim.x) arow[kk] = a[lr * k + kk]; } __syncthreads(); // --- Semaphore barrier --- #if __CUDA_ARCH__ >= 900 const int32_t epoch = epoch_base + 1; if (threadIdx.x == 0 && rank == owner) publishToAll(sem_r, sem_l, rank, row, m, ws, epoch); __syncthreads(); if (threadIdx.x == 0 && rank != owner) waitOne(sem_l, row, m, owner, epoch); __syncthreads(); #else (void)sem_r; (void)sem_l; (void)rank; (void)ws; (void)epoch_base; asm volatile("trap;"); #endif
Uh oh!
There was an error while loading. Please reload this page.
Linked with Issue
Motivation
nvFuser currently achieves multi-GPU overlap by scheduling separate communication and compute kernels through Host IR, using stream parallelism. This works, but the overlap granularity is coarse, and with this approach communications necessarily represent kernel fusion boundaries.
This PR explores a different approach: GPU-initiated communication inside compute kernels. Instead of the host orchestrating separate comm and compute phases, a single CUDA kernel reads/writes remote GPU memory directly via symmetric memory pointers, interleaving data movement and computation at the thread level.
This is an experimental reference PR -- not intended for merge as-is, but as a self-contained, readable codebase for the team to study, benchmark, reproduce, and iterate on. The fused scalar kernels demonstrate the comm patterns and synchronization model. The two-kernel CUTLASS variants establish a performance ceiling. Closing the gap -- achieving CUTLASS-level compute inside a truly fused single kernel -- is the central open problem where we need the compute team's expertise.
For simplicity, we focus on "Allgather+Matmul" problem, on single H100 node NVLink.
How to run
Requires Hopper (SM90) for CUTLASS and multimem variants. Build with flag like
TORCH_CUDA_ARCH_LIST="9.0a"What this PR contains
A self-contained benchmark comparing 7 distributed matmul implementations for
C[M,N] = A[M,K] x B[K,N]whereAis row-sharded across ranks on axisM,Bis replicated. All code lives in 3 test files:test_multidevice_fused_remote_matmul.h: Shared types, enum, context struct, perf summarytest_multidevice_fused_remote_matmul_kernel.cu: CUDA kernels, CUTLASS wrapper, launcherstest_multidevice_fused_remote_matmul.cpp: Test harness, resource setup, timing, baselinesSmall infrastructure change:
SymmetricTensor::devicePeerPointers()added tosymmetric_tensor.{h,cpp}-- lazily allocates a device-side pointer table for convenient kernel access to peer buffers.Implementations
Baselines (separate allgather + matmul, no fusion):
baselineNcclAllgatherMatmul-- NCCL allgather to rebuild full A, thenat::matmul. The standard-library reference.baselineCudaAllgatherMatmul-- Same pattern using nvFuser's native backend for the allgather, using multicast NVLSTruly fused kernels (comm + compute in a single kernel launch):
naiveRemoteRead-- Simplest possible fusion. Each thread computes oneC[row,col]by reading A elements directly from the owner rank's shard via remote pointers. No staging, no explicit gather. Every A element traverses NVLink on every access -- no reuse.threadloadGatherScalarCompute-- Two-stage fused kernel. Stage 1: cooperative thread loads gather one full A row from the owner's remote shard into a local staging buffer. Stage 2: scalar matmul from the staged row. Inter-rank synchronization via device-side ready/done semaphores (owner signals readiness; non-owners poll; readers ack completion). SeethreadloadGatherKernel(tests/cpp/test_multidevice_fused_remote_matmul_kernel.cu, line 292).multimemGatherScalarCompute-- Same two-stage structure, but Stage 1 uses Hoppermultimem.st.global.v4.f32instructions to write A rows to an NVLS multicast buffer, delivering data to all peers in hardware. Requires SM90+ and multicast-capable symmetric memory. SeemultimemGatherKernel(tests/cpp/test_multidevice_fused_remote_matmul_kernel.cu, line 35).Two-kernel path (separate comm kernel, then CUTLASS GEMM -- NOT truly fused):
threadloadGatherThenCutlass-- The threadload gather kernel (with semaphores) materializes full A into a staging buffer, then a separate host-launched CUTLASS 3.x SM90 TMA GEMM consumes that buffer. These are two distinct<<<...>>>launches on the same stream. The gather kernel runs withn=0to skip its in-kernel compute stage.multimemGatherThenCutlass-- Same as above but using multimem gather instead of threadload gather.These two-kernel variants establish a performance ceiling: they show what throughput is achievable when the comm pattern is correct and the compute is Hopper-native WGMMA. True single-kernel fusion with equivalent compute quality is the first goal -- see "Where I need the team's input" below.
Performance (8xH100 DGX, M=N=K=1024, half precision)
Key observations:
Synchronization model
Fused kernels require device-side inter-rank synchronization since there is no host between the comm and compute stages. This PR implements epoch-based remote semaphores:
__threadfence_system()+ remote writesatomicAdd(..., 0)until the expected epoch appearsWhere I need the team's input
The central challenge: true single-kernel fusion with Hopper-native compute.
The fused scalar kernels prove that the comm and sync model works. The two-kernel CUTLASS path proves the perf ceiling is high. But achieving both in a single kernel is challenging for me because:
CUTLASS 3.x mainloops are designed to own the entire kernel -- they manage shared memory layout, warpgroup roles (TMA producer vs MMA consumer), and async pipeline barriers. They cannot be called from within another kernel.
TMA descriptors are created on the host via
cuTensorMapEncodeTiled. They cannot be created from device code.WGMMA requires careful warpgroup scheduling -- which warps do MMA, which do data movement, and how shared memory is partitioned between operand staging and communication buffers.
The right approach is likely a custom kernel using CUTE primitives (
MMA_Atom,TiledMMA,TMA_LOAD) at the building-block level, weaving P2P comm into the producer/consumer pipeline. This is where I need your expertise:cp.async.bulkinstead of thread loads, freeing SMs entirely.What else is NOT in this PR