Repository navigation
[Perf] batch-invariant: Fused residual add + RMSNorm kernel - #46159
Merged
HaiShaw merged 1 commit intoOct 28, 2026
Merged
Conversation
In batch-invariant mode, every RMSNorm call with a residual fell back to forward_native, an eager chain of float32 ops (add, pow, mean, rsqrt, mul) of 8-10 kernels per call. Decoder layers pass a residual to both of their norms, so this ran twice per layer per step and took about a third of deterministic decode GPU time on MI350X. Add a Triton kernel that adds the residual (and the optional post-residual addend) in float32, stores the new residual and RMS-normalizes the sum, one row per program, so a row's result does not depend on the batch. RMSNorm uses it in batch-invariant mode where forward_native's math applies (no cast_x_before_out_mul, no variance size override, no padded output, not the FSDP RL target); other cases keep falling back to forward_native.
aarnetalman
force-pushed
the
batch-invariant-fused-add-rmsnorm
branch
from
October 23, 2026 21:15
e32a416 to
e706ba2
Compare
aarnetalman
marked this pull request as ready for review
October 23, 2026 21:16
aarnetalman
requested review from
BBuf,
Edwardf0t1,
Fridge003,
HaiShaw,
Ying1123,
ch-wan,
hebiao064,
ispobock and
merrymercy
as code owners
October 23, 2026 21:16
Collaborator
|
/tag-and-rerun-ci |
Collaborator
|
All pr-test-base passed. |
Collaborator
|
Hi @aarnetalman |
Contributor
Author
|
Thanks @1am9trash . I'd keep it platform-neutral, for these reasons:
I haven't measured it end to end on NVIDIA. If you'd rather limit it to ROCm until someone has, I can add the guard and leave CUDA for a follow-up. |
Collaborator
|
Okay, that works for me. |
HaiShaw
approved these changes
Oct 28, 2026
This was referenced Oct 28, 2026
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.
Motivation
In deterministic mode, RMSNorm with a residual add (both norms in every decoder layer) falls back to eager PyTorch, which takes about ten kernel launches instead of a single fused kernel. This PR adds a fused, batch-invariant add+RMSNorm kernel for that path. It applies to any model whose decoder layers use
RMSNormwith a residual (Llama, Mistral, Qwen2/3, Qwen3-MoE, Phi, DeepSeek, gpt-oss and most others). Tested on MI350X with Qwen3-8B, Qwen3-30B-A3B and Phi-4: single-request decode throughput with the fused kernel is 56-73% higher and 12-22% higher at concurrency 16, and every determinism check passes on Qwen3-8B (TP1, TP2) and Qwen3-30B-A3B.The fallback is a large share of deterministic decode time. Profile of a single-request decode (1k-token prompt, 256 output tokens, CUDA graphs off) in deterministic mode on MI350X:
In normal mode the same norms take 12.6 ms on Qwen3-8B as one fused kernel.
Scope: deterministic mode only, on every platform that runs the batch-invariant Triton kernels. The new kernel uses the same float32 math as
forward_native. Models that useGemmaRMSNorm(Gemma, Qwen3-Next/3.5/3.8) take a different path and are not affected. Normal mode is unchanged.Modifications
batch_invariant_ops.py: new_fused_add_rms_norm_kernelandfused_add_rms_norm_batch_invariant, next to the existingrms_normkernel and in the same style. One program per row:fp32_residual);The reduction runs over one row with a fixed block size, so a row's result does not depend on the batch.
layernorm.py: in the batch-invariant branches ofRMSNorm.forward_cuda,forward_aiter,forward_hipandforward_xpu, calls with a residual use the new kernel whereforward_native's math applies. These cases keep falling back toforward_nativeas before:cast_x_before_out_mul;New
test/registered/unit/batch_invariant_ops/test_fused_add_rms_norm_batch_invariant.py(per-PR, CUDA and MI35x):torch.equal) when computed in batches of 1, 3, 17 and 64;fp32_residualon and off.Unchanged: normal mode; batch-invariant calls without a residual; the fallback cases above.
Accuracy Tests
MI350X, ROCm 7.2.4,
rocm/sgl-dev:v0.5.22-rocm724-mi35x-20261021.Unit tests:
End to end, Qwen3-8B TP1, deterministic mode with AITER prefill and decode attention. These runs use the open MI350X deterministic-mode PRs (#43788, #44045, #44438, #44489, #45599, #45601, #34543), which is the configuration that deterministic mode runs in on MI350X today; "before" is the same tree without this change:
python3 -m sglang.launch_server --model-path Qwen/Qwen3-8B --enable-deterministic-inference \ --attention-backend triton --prefill-attention-backend aiter --decode-attention-backend aiter python3 -m sglang.test.test_deterministic --test-mode {single,prefix,radix_cache} --n-trials 50With this change, Qwen3-8B:
single1 output,prefix1/1/1/1 (twice),prefix --return-logprobidentical,radix_cachepass, and a bitwise batch-size check (the same prompt in batches of 1 to 50, 1,275 responses, token IDs and logprobs compared exactly with the batch-size-1 response) 1,275 / 1,275. Qwen3-30B-A3B (same setup):single1,prefix1/1/1/1, bitwise 1,275 / 1,275. Qwen3-8B at TP2:single1,prefix1/1/1/1, bitwise 1,275 / 1,275.GSM8K (sgl-eval 0.1.2,
--temperature 0 --max-tokens 2048, all 1,319 questions, Qwen3-8B, deterministic): 93.18% with this change (before: 93.25% deterministic, 93.48% normal mode, same image).Speed Tests and Profiling
Deterministic mode (setup above, CUDA graphs on), before vs after on the same node and image,
python3 -m sglang.benchmark.serving --backend sglang --dataset-name random --random-range-ratio 1.0 --request-rate inf --max-concurrency C(output throughput, median inter-token latency):A second alternating round for Qwen3-8B gives the same numbers within 1% (before 105 tok/s / 9.40 ms and 1,655 tok/s / 7.75 ms; after 174 tok/s / 5.64 ms and 2,036 tok/s / 6.11 ms).
For reference, normal mode on the same setup has an inter-token latency of 4.8 ms (Qwen3-8B) and 4.4 ms (Qwen3-30B-A3B) at concurrency 1, so this closes most of the deterministic-mode gap for single-request decode. No change outside deterministic mode.
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ✅ Run #40025448735
Latest PR Test (Extra): ❌ Run #40025448730
Latest PR Test (AMD ROCm 10): ❌ Run #40025448697