Skip to content

[HIP] fix(custom_all_reduce): uniform trip count for the block barriers - #5694

Merged
zufayu merged 3 commits into
ROCm:mainfrom
Arist12:fix/custom-allreduce-2stage-block-barrier
Sep 23, 2026
Merged

zufayu merged 3 commits into
ROCm:mainfrom
Arist12:fix/custom-allreduce-2stage-block-barrier

Conversation

@Arist12

@Arist12 Arist12 commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong

Three kernels in csrc/include/custom_all_reduce.cuh stage their operands through shared memory
and bracket that with two block-wide __syncthreads() — inside a grid-stride loop whose trip
count is not uniform across the block:

kernel loop
cross_device_reduce_2stage stage 1, reduce-scatter
cross_device_reduce_2stage_write_mode stage 2
reduce_scatter_cross_device_store fused allreduce+rmsnorm, first step

Taking the first as the example:

for(int idx = start + tid; idx < end; idx += stride)
{
    *(reinterpret_cast<P*>(&tmp_smem[0]) + threadIdx.x) = ptrs[warp_id][idx];
    __syncthreads();
    if(warp_id == 0) { /* sum the ngpus operands out of smem */ }
    __syncthreads();
}

end - start is this rank's partition, size / ngpus, which is not generally a multiple of
stride = gridDim.x * (THREAD_NUM / ngpus). The lanes that cover the tail therefore run one
iteration more than the rest, so the barriers pair up across different iterations — and against
the ones in end_sync. Two consequences:

  • the warp_id == 0 reduction can read tmp_smem before the other wavefronts have written
    their operands into it;
  • a wavefront can reach end_sync and publish "stage 1 done" while another is still producing
    tmp_out, so the stage-2 all-gather can read a partition that is not finished.

Either way the output is wrong at a fixed relative position inside each rank's partition.

How it shows up

size is flat, so each rank owns a contiguous size / ngpus slice — one quarter of the rows of
a [bs, hidden] tensor at ngpus == 4. The damage lands at the same offset in every quarter:

bs = 120  ->  bad rows {1, 31, 61, 91}      (stride 30 = 120/4)
bs = 160  ->  bad rows {11, 51, 91, 131}    (stride 40 = 160/4)

Found in an RL workload; reproduced standalone below.

Standalone reproduction

Four ranks on MI355X, bf16 [bs, 2880], 32 shapes × 600 reductions each. Every reduction is run
twice and compared bitwise (no tolerance needed), and also compared against RCCL's all_reduce
(bf16 rounding on a 4-way sum is ~0.05, so anything above that is a wrong answer rather than a
different summation order):

before after
wrong vs RCCL, and non-deterministic 64 of 19200 0 of 19200
shapes affected bs = 120, 121, 127, 157, 160, 161 none
worst |diff| 12.4 0.0625 (pure bf16 rounding)
48 reductions back to back, bs=160 300 of 11520 wrong, worst 13.1 0 of 11520
world_size=2 (1-stage path) 0 of 5120 0 of 5120 — no regression

It is a race, so the counts move between runs; an earlier sweep over a slightly different set of
shapes gave 43/19200 and 173/11520. Most shapes pass either way — the race needs the partition
remainder to land mid-block, which is why this has survived.

The numbers above were measured on a build based on d9e5ef7ce that also carried a second
commit, since dropped, which widened two sync scopes. @TennyWang1223 re-measured 6110f00f
on its own and reproduced the fix, which attributes it to the barrier change alone. The
three patched hunks are unchanged on main, and the rebased diff is byte-identical to the
one that was tested.

The fix

Round the loop bound up to a multiple of the stride, which makes the trip count exactly uniform
(tid < stride by construction), and predicate the work. No added synchronisation and no change
to the reduction order.

reduce_scatter_cross_device_store is fixed by inspection — same defect, same file — but the
reproducer above does not exercise the fused-allreduce path.

Timing, 7 reps × 500 calls, median us/call taken as the max over ranks, whole measurement
repeated twice: within run-to-run noise at bs=120..161, and 3-6% slower at bs=256
(19.2 → 20.3) and bs=512 (32.2 → 33.1).

Three kernels in this header stage their operands through shared memory and
bracket that with two block-wide __syncthreads(), inside a grid-stride loop
whose trip count is not uniform across the block:

  cross_device_reduce_2stage             stage 1
  cross_device_reduce_2stage_write_mode  stage 2
  reduce_scatter_cross_device_store      (fused allreduce+rmsnorm, first step)

The span each loop walks -- the rank's own partition, size/ngpus or
m*valid_pack_count/ngpus -- is not generally a multiple of the stride, so the
lanes covering the tail run one iteration more than the rest. The barriers then
pair up across different iterations, and against the ones in end_sync. Two
things follow: the reduction can read tmp_smem before the other wavefronts have
written their operands into it, and a wavefront can publish "stage 1 done" in
end_sync while another is still producing its output. The result is a wrong
reduction at a fixed relative position inside each rank partition -- observed as
four bad rows per batch, spaced by size/ngpus.

Rounding the loop bound up to a multiple of the stride makes the trip count
exactly uniform (tid < stride by construction); the work is predicated instead.
No added synchronisation and no change to the reduction order.

Repro on 4x MI355X, bf16 [bs, 2880], 32 shapes x 600 reductions each, against
RCCL all_reduce (bf16 rounding on a 4-way sum is ~0.05). Each reduction is also
run twice and compared bitwise, which needs no tolerance:

  before   64 of 19200 wrong and non-deterministic, at bs = 120, 121, 127, 157,
           160, 161; worst |diff| = 12.4
  after    0 of 19200, worst |diff| = 0.0625
  chained, 48 reductions back to back at bs=160:
           before 300 of 11520 wrong, worst 13.1; after 0 of 11520
  world_size=2 (1-stage path): 0 of 5120 either way, i.e. no regression

It is a race, so the counts move between runs -- an earlier sweep gave 43/19200
and 173/11520, over a slightly different set of shapes. Most shapes pass either
way: the race needs the partition remainder to land mid-block, which is why this
has survived.

reduce_scatter_cross_device_store is fixed by inspection -- same defect, same
file -- but the reproducer above does not exercise the fused-allreduce path.

Timing, 7 reps x 500 calls, median us/call taken as the max over ranks, whole
measurement repeated twice: within run-to-run noise at bs=120..161, and 3-6%
slower at bs=256 (19.2 -> 20.3) and bs=512 (32.2 -> 33.1).
@Arist12
Arist12 requested a review from a team September 20, 2026 05:48
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
multigpu Aiter multi-GPU tests on the 8-GPU runner
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: GPT-OSS-120B, DeepSeek-R1-0528, Kimi-K2.5
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 5694 --add-label <label>

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to opt this PR out.

@github-actions github-actions Bot changed the title fix(custom_all_reduce): uniform trip count for the block barriers [HIP] fix(custom_all_reduce): uniform trip count for the block barriers Sep 20, 2026
@github-actions github-actions Bot added the HIP label Sep 20, 2026
@Arist12
Arist12 force-pushed the fix/custom-allreduce-2stage-block-barrier branch from 6f55afa to 6fb1e0e Compare September 20, 2026 05:50
@zufayu
zufayu requested a review from yzhou103 September 21, 2026 01:11

@TennyWang1223 TennyWang1223 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The change of device function start_sync and end_sync are unnecessary. detail see #4294 (comment)
  2. The change on kernels are truely useful, I have tested on main and this pr without change start_sync and end_sync(commit 6110f00), results as bellow. this can also prove origin start_sync and end_sync have no influence on accuracy

main
Image

commit 6110f00
Image

@Arist12
Arist12 force-pushed the fix/custom-allreduce-2stage-block-barrier branch from 3368bc0 to bf8bc55 Compare September 22, 2026 07:08
@Arist12

Arist12 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author
  1. The change of device function start_sync and end_sync are unnecessary. detail see #4294 (comment)
  2. The change on kernels are truely useful, I have tested on main and this pr without change start_sync and end_sync(commit 6110f00), results as bellow. this can also prove origin start_sync and end_sync have no influence on accuracy

main Image

commit 6110f00 Image

  1. The change of device function start_sync and end_sync are unnecessary. detail see #4294 (comment)
  2. The change on kernels are truely useful, I have tested on main and this pr without change start_sync and end_sync(commit 6110f00), results as bellow. this can also prove origin start_sync and end_sync have no influence on accuracy

main Image

commit 6110f00 Image

hi @TennyWang1223 , thanks for the careful review, I've trimmed the corresponding section per requested.

TennyWang1223
TennyWang1223 previously approved these changes Sep 22, 2026

@TennyWang1223 TennyWang1223 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comments only; every changed line starts with `//`, so the generated code is
identical to 6110f00.
@Arist12

Arist12 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

The new commit is syntax-only, only trim the comments and the code should function well; should be good to merge @TennyWang1223

@zufayu
zufayu merged commit 5e77166 into ROCm:main Sep 23, 2026
55 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants