fix(custom_all_reduce): use SYSTEM scope + ACQUIRE ordering for cross-device signal loads - #4786
Open
hekhong-png wants to merge 1 commit into
Open
fix(custom_all_reduce): use SYSTEM scope + ACQUIRE ordering for cross-device signal loads#4786hekhong-png wants to merge 1 commit into
hekhong-png wants to merge 1 commit into
Conversation
…-device signal loads The start_sync and end_sync spin-wait loops load synchronization flags that are written by remote GPUs via P2P, but used __MEMORY_SCOPE_DEVICE for the loads. DEVICE scope only guarantees visibility within the same device; it does not order loads against cross-device stores. This caused the receiving CU to spin on stale data for an extended tail, and in pathological cases could lead to kernel hangs on TP8 configurations. Three fixes to csrc/include/custom_all_reduce.cuh (4 insertions, 3 deletions): 1. start_sync load: __ATOMIC_RELAXED -> __ATOMIC_ACQUIRE ACQUIRE ordering flushes the invalidate queue so cross-CU stores become visible promptly. 2. start_sync + end_sync load: __MEMORY_SCOPE_DEVICE -> __MEMORY_SCOPE_SYSTEM Matches the store side (already SYSTEM) and the CUDA reference path. The signal is written by a remote GPU via P2P, so SYSTEM scope is required. 3. __threadfence_system() after __builtin_nontemporal_store nontemporal_store bypasses L2 and writes directly to memory. Without a fence, the data may not be visible to remote CUs when end_sync signals completion. The fence is placed inside the if(is_broadcast_reg_outptr) block so it only runs on the nontemporal_store path. Validated on TP8 (8x MI308X, gfx942, ROCm 7.2.0) with GLM-5.2-FP8 + EAGLE. Instrumented profiling across ~196M all-reduce invocations showed: - Max spin-wait reduced 48% (144,279 -> 76,809) - All >=16384-spin events eliminated (7,890 -> 0) - Safety net (10M spin limit) never triggered - Production regression test (v7 final, ~17 min, ~680 req): no throughput or latency regression (avg ~228 tok/s, p50 ITL 25ms, p99 ITL 40ms), no errors or hangs Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix cross-device memory ordering in custom_all_reduce on multi-GPU HIP
Summary
The
custom_all_reducekernel's synchronization primitives use__MEMORY_SCOPE_DEVICEfor loads of signals that are written by remote GPUs via P2P. On HIP,DEVICEscope only guarantees visibility within the same device — it does not order loads against cross-device stores. This causes the receiving CU to spin on stale data for an extended tail before the store becomes visible, and in pathological cases can lead to a kernel hang on TP8 configurations.This patch aligns the load scopes with the CUDA reference implementation (which already uses
SYSTEMscope) and adds a__threadfence_system()after__builtin_nontemporal_storeto guarantee the data payload is visible to remote CUs before the completion signal is sent.Root cause
start_syncandend_synceach contain a spin-wait loop that polls a flag written by peer GPUs:The flag is written by a remote GPU using
__MEMORY_SCOPE_SYSTEM(the store side is already correct), but the load side uses__MEMORY_SCOPE_DEVICE. On HIP:DEVICEscope only orders memory operations within the same device. It does not guarantee that a store from another GPU (arriving via P2P / XGMI) is visible to the load.SYSTEMscope orders operations across all devices in the system, matching the CUDA reference.The
__ATOMIC_RELAXEDordering onstart_sync's load is also too weak — it provides no acquire semantics, so the invalidate queue is not flushed and cross-CU stores may take a long time to become visible.end_syncalready used__ATOMIC_ACQUIRE(conditionally), so only the scope was wrong there.Additionally, the
cross_device_reduce_2stage_write_modepath uses__builtin_nontemporal_storeto write the reduction result directly to memory (bypassing L2 cache). Without a fence, the data may not have propagated through the memory controller whenend_syncsignals completion, causing the receiver to spin waiting for data that hasn't become visible yet.Changes
Three minimal fixes to
csrc/include/custom_all_reduce.cuh(4 insertions, 3 deletions):1.
start_syncload:RELAXED→ACQUIRE+DEVICE→SYSTEMACQUIREordering flushes the invalidate queue so cross-CU stores become visible promptly.SYSTEMscope matches the store side and the CUDA reference.2.
end_syncload:DEVICE→SYSTEMwhile(__scoped_atomic_load_n(&self_sg->end[blockIdx.x][threadIdx.x], final_sync ? __ATOMIC_RELAXED : __ATOMIC_ACQUIRE, - __MEMORY_SCOPE_DEVICE) < flag) + __MEMORY_SCOPE_SYSTEM) < flag)end_syncalready hadACQUIRE(conditional onfinal_sync); only the scope was wrong.3.
__threadfence_system()after__builtin_nontemporal_store__builtin_nontemporal_store(*(src_addr + 3), dst_addr + 3); + __threadfence_system(); }Placed inside the
if(is_broadcast_reg_outptr)block so it only executes on the nontemporal_store path (theelsebranch uses shared memory and doesn't need it). The fence forces all prior stores — including nontemporal stores that bypass L2 — to be visible to all devices beforeend_syncsignals completion.Why these are safe and correct
#elsebranch guarded by__CUDA_ARCH__) already uses__threadfence_system()and system-scope atomics. The HIP path was inconsistent: stores usedSYSTEMscope but loads usedDEVICEscope. This patch makes both sides consistent.Validation
Tested on a TP8 configuration (8× AMD MI308X, gfx942, ROCm 7.2.0) running GLM-5.2-FP8 with EAGLE speculative decoding (
speculative-num-steps 5,speculative-eagle-topk 1,speculative-num-draft-tokens 6).Instrumented profiling (subsequently removed) measured the spin-wait tail length across ~196 million all-reduce invocations:
Key findings:
v5) eliminated all ≥16384-spin events (7,890 → 0) and reduced max spin by 41%.v6) reduced max spin a further 10% and maintained 0 high-spin events.Production regression test (v7, final patch)
The final patch (3 fixes only, no instrumentation, no safety net) was deployed and load-tested for ~17 minutes with 6 concurrent workers (~680 requests, ~125K generated tokens):
Throughput fluctuated 203–275 tok/s across samples (batch composition variance), averaging ~228 tok/s — within noise of the v6 baseline (218.9). Latency percentiles were bit-identical (p50=25ms, p99=40ms). No errors, no hangs, no crashes across the run.
Scope of impact
#ifndef __CUDA_ARCH__).custom_all_reduce(i.e.,disable_custom_all_reduce=false, the default on HIP).use_write_modepath (where the fence is added) only triggers whenworld_size_ == 8 && bytes > 4,194,304 && arch.find("gfx942") != std::string::npos.