Repository navigation
[AMD][gfx950] DSV4: fuse wo_b's GEMM with its all-reduce via mori cco - #40757
Closed
yangyuhuiling wants to merge 3 commits into
Closed
yangyuhuiling wants to merge 3 commits into
yangyuhuiling wants to merge 3 commits into
Conversation
yangyuhuiling
requested review from
BBuf,
Edwardf0t1,
Fridge003,
HaiShaw,
Ying1123,
ch-wan,
ishandhanani,
ispobock,
merrymercy,
sogalin and
yctseng0211
as code owners
September 22, 2026 15:10
This was referenced Sep 30, 2026
`wo_b` is a RowParallelLinear: a block-scale fp8 GEMM followed by an NCCL
all-reduce of the [M, hidden] result. mori's fused kernel writes each
destination's row band straight into a symmetric window and hands it to the
SDMA copy engines as soon as that band's tiles are done, so the transfer runs
inside the GEMM instead of after it.
Opt-in and ROCm-only. `SGLANG_OPT_FUSED_WO_B_AR=1` turns it on;
`fused_wo_b()` returns None for everything it does not serve -- decode, small
or ragged M, a weight that is not aiter-preshuffled, no SDMA queues -- and
disables itself for the process on any failure, so the ordinary path is always
the fallback rather than an error.
Measured on 8x MI355X (gfx950), DeepSeek-V4-Pro, TP8, in 4096 / out 512:
bs base fused fp8 gather
8 1136.1ms -3.36% -6.04%
16 2232.0ms -3.03% -6.29%
64 8890.9ms -3.26% -6.44%
(TTFT; bs=1 is at the fusing threshold and is noise either way.) The layer
itself goes 1460.3us -> 1145.1 at M=16384 N=7168 K=2048, i.e. -21.6%; the
end-to-end share is smaller because wo_b is about 12% of the forward. The
fused and unfused paths agree to relL2 2.72e-05 -- both quantise identically,
so what is left is bf16 accumulation order.
The optional fp8 all-gather leg (`..._FP8_GATHER=1`, also off by default)
halves that leg's bytes for a further ~3%, consistently across batch sizes
because the leg grows linearly with M. It costs accuracy: relL2 on the gather
goes 2.35e-3 -> 2.49e-2, which is e4m3's 3-bit mantissa rather than a
granularity that can be tuned, and the window grows 700 -> 812 MiB.
Two call sites of this layer own the all-reduce themselves and the fused path
stands aside for both: mHC post-fusion passes `skip_all_reduce=True`, and a
`Mxfp8SwizzledInput` arrives when wo_a fuses an MXFP8 quantization into its
epilogue. Neither is reachable on ROCm today, so this checks rather than
relies on the platform.
`BUILD_CCO_SDMA=ON` is set where the image and CI build MORI. Without it cco
has no SDMA queues, every put is a silent no-op, and
`GemmAllReduceOp.self_test` refuses to serve a collective that moves no bytes
-- so the flag would be accepted and then disable itself.
The window is VMM memory outside torch's allocator, sized from
`get_schedule().chunked_prefill_size` and unable to grow afterwards, so a run
that enables this needs a correspondingly lower --mem-fraction-static.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….1-Flash PR sgl-project#40757 fuses wo_b's GEMM with its all-reduce for V4-Pro, whose wo_b is a 128x128 block-scale fp8 linear. V4.1-Flash quantises at 32-wide ue8m0 and takes the gfx950 native mxfp8 route instead, so `aiter_bpreshuffled` is never set and the fused path declined every layer -- after allocating its 560 MiB window. Ported from xiangch/mori-on-dsv41-amd-main: - mori_gemm_ar: quantise the way V4.1's wo_b does, and accept the fp8-grid operand the gfx950 wo_a fork emits instead of requiring a plain tensor. - mori_mxfp8_gemm / mori_mxfp8_common: the same multiply with no collective attached, behind SGLANG_OPT_MORI_MXFP8_GEMM. It serves wq_b, which is column-parallel and has nothing to fuse with, and any wo_b call the fused path declines. Gated on the GEMM's grid rather than on M. - Both switches are independent on purpose: they are separate optimisations and have to be A/B-able separately. The hooks are re-pointed at where upstream now puts these routes: the mHC branch moved out of _project_wo_b into its caller's defer_all_reduce, and the native mxfp8 linear is _apply_native in fp8_hip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The harnesses were written against a tree where the parallel topology was an argument, prepare_mxfp8_native_weight also returned a dequantised bf16 copy, and the native linear took it as a fallback. Upstream has since moved the topology into the published context and split the bf16-dequant route out into bf16_dequant_blockscaled_linear, so every one of these runs failed to start. - initialize_model_parallel() reads the published widths: publish them first. - ServerArgs carries tp_size, or publishing it contradicts the groups already built and the topology validator rejects it. - prepare_mxfp8_native_weight returns two tensors, and the native linear has no weight_bf16 parameter. - native_route_plan is gone; the row loses that annotation rather than the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yangyuhuiling
force-pushed
the
xiangch/fused-wo-b-gemm-ar-main
branch
from
October 8, 2026 17:04
bdc87de to
24ab24d
Compare
yangyuhuiling
requested review from
Alisehen,
AniZpZ,
FlamingoPg,
OrangeRedeng,
b8zhong and
mmangkad
as code owners
October 8, 2026 17:04
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.
What
wo_bin DeepSeek-V4 is aRowParallelLinear: a block-scale fp8 GEMM followedby an NCCL all-reduce of the
[M, hidden]result. The two can overlap.mori.ops.gemm_ar's fused kernel writes each destination's row band straightinto a symmetric window and hands it to the SDMA copy engines as soon as that
band's tiles are done, so the transfer runs inside the GEMM instead of after
it.
Opt-in and ROCm gfx950 only.
SGLANG_OPT_FUSED_WO_B_AR=1turns it on.fused_wo_b()returnsNonefor everything it does not serve — decode, smallor ragged
M, a weight that is not aiter-preshuffled, no SDMA queues — anddisables itself for the process on any failure, so the ordinary path is the
fallback rather than an error.
Numbers
8x MI355X (gfx950), DeepSeek-V4-Pro, TP8, input 4096 / output 512,
bench_one_batch_server,last_ttft:bs=1sits at the fusing threshold (one row band per destination, nothing tooverlap) and is noise in either direction.
The layer itself goes 1460.3us → 1145.1 (−21.6%) at
M=16384 N=7168 K=2048on 8 ranks. End-to-end is smaller because
wo_bis about 12% of the forward —TTFT is the metric that moves, since the fused path only engages in prefill;
decode runs at
M=batch_sizeand never fuses.Fused and unfused agree to relL2 2.72e-05. Both quantise identically, so
what is left is bf16 accumulation order.
The optional fp8 gather, and what it costs
SGLANG_OPT_FUSED_WO_B_AR_FP8_GATHER=1(also off by default) sends theall-gather leg as e4m3 with a per-row scale. That leg grows linearly with
M,which is why the extra gain is near-constant across batch sizes (−2.8 / −3.4 /
−3.3% on top of the bf16 wire).
It is not free: relL2 on that leg goes 2.35e-3 → 2.49e-2, which is e4m3's
3-bit mantissa rather than a granularity that can be tuned away, and the
symmetric window grows 700 → 812 MiB. Off by default for that reason.
Worth a reviewer's attention
aside for both: mHC post-fusion passes
skip_all_reduce=True, and aMxfp8SwizzledInputarrives whenwo_afuses an MXFP8 quantization into itsepilogue. Neither is reachable on ROCm today (mHC needs Blackwell and
tp_size == attn_tp_size == 4), so this checks rather than relying on theplatform.
BUILD_CCO_SDMA=ONis set where the image and CI build MORI. Without itcco has no SDMA queues and every put is a silent no-op;
GemmAllReduceOp.self_test()then refuses to serve a collective that movesno bytes, so the flag would be accepted and immediately disable itself. The
Dockerfile and
amd_ci_install_dependency.shmust change together — the CIstep rebuilds MORI over the image's copy and would otherwise undo it.
get_schedule().chunked_prefill_sizeand unable to grow afterwards, so a runthat enables this needs a correspondingly lower
--mem-fraction-static. Itis taken from the chunked-prefill limit rather than the first request seen,
so a short prompt arriving first cannot fix a window too small for a later
full chunk.
_MIN_FUSED_Mand_MIN_PAD_FILLanswer "is fusing worth it against what the model runstoday", which is not a property of the kernel;
mori.ops.gemm_ar.supports()answers the separate question of whether the shape is expressible.
Not in this PR
A three-arm nightly perf suite (base / fused / fp8 + GSM8K) exists and runs,
but it is a 6-hour 8-GPU job for a feature that is off by default, so it is
held back rather than added to
nightly-test-amd.ymlhere. Happy to bring itin if maintainers would rather have the regression coverage up front.
Dependency
Needs
moriwithmori.ops.gemm_ar(ROCm/mori#662) built withBUILD_CCO_SDMA=ON, andMORI_ENABLE_SDMA=1at process start. SGLang alreadydepends on mori for
moriepand the disaggregation transport; this is thefirst use of it inside the model forward.
🤖 Generated with Claude Code
CI States
Latest PR Test (Base): ❌ Run #37813744419
Latest PR Test (Extra): ❌ Run #37813743891
Latest PR Test (AMD ROCm 10): ❌ Run #37813744414