[ROCm] Fuse per-token activation quant into RMSNorm for per-channel quantized attention - #34502
Emmanuel0612 wants to merge 51 commits into
Conversation
Conflicts: python/sglang/srt/distributed/parallel_state.py
Conflicts: python/sglang/srt/layers/layernorm.py
Conflicts: python/sglang/srt/layers/layernorm.py
…el fp8 attn For per-channel fp8 attention projections (q_b_proj / kv_b_proj / fused_qkv_a_proj_with_mqa) on gfx95, the per-token fp8 activation quant was a standalone _per_token_group_quant_8bit launch before each projection. Fold it into the preceding (fused) RMSNorm so the projection receives a (fp8, x_scale[m,1]) tuple consumed directly by gemm_a8w8_bpreshuffle. Adds a new quant_format "fp8_per_token" (per-channel fp8) alongside the existing block-scale "fp8": - apply_fp8_linear: tuple fast path -> gemm_a8w8_bpreshuffle (fp8_utils.py) - Fused AR+RMSNorm+per-token-quant plumbing: layernorm.py, parallel_state.py (custom_fused_ar_rms_quant), communication_op.py, communicator.py - deepseek_v2.py: _detect_gfx95_quant_format returns "fp8_per_token" for per-channel fp8; prepare_qkv_latent / q_b_proj_forward route the tuple through the standard proj (fused-a-gemm cannot consume a tuple) - forward_mla_rocm.py: fused_qk_rmsnorm_q_pertoken_fp8 (decode q_b_proj) - forward_mha_rocm.py: _fused_rmsnorm_pertoken_fp8 (prefill q_b_proj/kv_b_proj) All new paths gate on aiter kernel availability and fall back to the existing bf16 + standalone-quant path when unavailable. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The predicate now gates on the explicit _fp8_weight_preshuffled marker and an exact output-channel match (output_size_per_partition | output_size). Update the mock proj builder to set those attrs (preshuffled marker + output_size), so the per-channel eligible cases pass, and add coverage for a missing marker and for a scale sized to the input dim K (which must be rejected). Co-Authored-By: Claude <noreply@anthropic.com>
…n fp8 fold Drives the fold's (fp8, scale[M,1], dtype) tuple through a real ColumnParallelLinear whose weight is quantized and preshuffled by the actual Quark W8A8Fp8 scheme (create_weights -> load -> process_weights_after_loading), then calls layer.forward() -- exercising the production routing (quant_method.apply -> scheme.apply_weights -> apply_fp8_linear -> gemm_a8w8_bpreshuffle) that the kernel-level parity test bypasses. Asserts the preshuffle marker landed, the folded-tuple forward matches the plain-activation forward, and dtype is preserved (q_b/kv_b widths, M=1 and M>1, BF16/FP16). Validated on MI355X (gfx950). Co-Authored-By: Claude <noreply@anthropic.com>
kkHuang-amd
left a comment
There was a problem hiding this comment.
Reviewed current head 21d2f5ab. The earlier tuple-routing, exact [M,1] scale, output-dtype, preshuffle-marker, and real ColumnParallelLinear coverage findings are resolved. Three P0 issues remain in the newly added fused collective wrapper, so I cannot approve yet. The current-head CPU and MI35X jobs were also gate-skipped; rerun them after the fixes. One P1 test-hardening item is non-blocking and may be handled in a follow-up PR.
| use_1stage_ar = envs.SGLANG_USE_1STAGE_ALLREDUCE.get() | ||
| else: | ||
| total_bytes = input_.numel() * input_.element_size() | ||
| use_1stage_ar = total_bytes <= 128 * 1024 |
There was a problem hiding this comment.
[P1] The generic default can select AITER's 1-stage kernel beyond its documented 80-token limit, but the advertised Kimi/DeepSeek path is safe. For BF16 hidden size 7168, the 128 KiB threshold permits at most nine tokens, so this is not reachable in the supported checkpoint. A future/custom hidden width such as [128,512] would exceed kMaxBlocks=80 and can silently truncate work. In a follow-up PR, add token_num <= 80 to the default decision and cover the 80/81-token boundary. This does not block approval of the current Kimi path.
There was a problem hiding this comment.
We can separate the issue into 3 situations and discuss.
- the batch_size is small (<= 128KB)
it will run "1-stage fused AR+RMSNorm+Quant", 9 tokens on Kimi, smaller than 80. - bigger batch_size (>128KB and AR-fusion still on)
it will run "2-stage fused AR+RMSNorm+Quant", no token cap. - AR-fusion off
it will run "plain AR + fused RMSNorm+Quant", still does not encounter 80-token risk.
So, it will not cause any problem.
| use_1stage_ar = total_bytes <= 128 * 1024 | ||
|
|
||
| try: | ||
| return ca_comm.custom_fused_ar_rms_quant( |
There was a problem hiding this comment.
[P0] The quantized wrapper omits the TC-piecewise CUDA-graph capture-state handling used by fused_allreduce_rmsnorm. When AITER's global _IS_CAPTURING state is active but the current stream is not capturing, the custom wrapper can return dummy zero outputs; this non-None tuple is then consumed as real activations/residuals/scales. Mirror the existing piecewise guard (or use the communicator's real registered path) before this call, and add capture/replay correctness coverage for the per-token format.
There was a problem hiding this comment.
This has been solved.
| eps, | ||
| use_1stage_ar, | ||
| ) | ||
| except Exception: |
There was a problem hiding this comment.
[P1] The broad catch exposes a real collective-safety robustness gap, but no normal supported Kimi trigger is demonstrated. Shape/width preflight is rank-symmetric and most HIP kernel failures surface asynchronously, so the current supported path is not known to diverge here. A rank-local synchronous launch fault could still make one rank return None and enter a second all-reduce while peers proceed in the fused collective. In a follow-up PR, return None only from deterministic checks before backend entry and propagate exceptions after launch. This does not block approval once the TC-piecewise P0 is fixed.
There was a problem hiding this comment.
If some errors go to the exception.
Then it still can't run since the asynchronous GPU error and shape error.
That means the launching will stop without sending any incorrect result.
| _HAS_PATH = ( | ||
| _use_aiter and _use_aiter_bpreshuffle_gfx95 and torch.cuda.is_available() | ||
| ) | ||
| except Exception: |
There was a problem hiding this comment.
[P1] This broad import catch can silently turn the required MI35X layer-routing regression into a green skip when AITER/Quark initialization is broken. In a follow-up PR, keep normal skips for unsupported platforms, but let unexpected import or initialization errors fail on the gfx95 runner so coverage cannot disappear silently. This item does not block approval once the P0s and current MI35X run are resolved.
There was a problem hiding this comment.
- The Quark/linear imports now run unguarded when _HAS_PATH=True. For gfx95 runners, if AITER/Quark initialization is broken, the import fails (the test errors red) instead of degrading into a green skip — that's the coverage this guards.
- The try/except wraps only the lightweight, aiter-independent hardware probe. Its sole purpose is to make sure the test skips cleanly on non-ROCm/non-gfx95 environments — i.e. "keep normal skips for unsupported platforms."
kkHuang-amd
left a comment
There was a problem hiding this comment.
Severity correction after checking the supported Kimi/DeepSeek shapes and AITER contract at head 21d2f5ab:
- P0 (blocking): missing TC-piecewise CUDA-graph handling is directly reachable under the default ROCm prefill graph path. AITER returns non-
Nonedummy zero activations/residuals/scales when_IS_CAPTURINGis set but the current stream is outside capture; this wrapper accepts them as real output. Mirror the existing unquantized guard and add capture/replay parity coverage. - P1 (follow-up): the generic one-stage rule lacks the 80-token cap, but Kimi's BF16 hidden size 7168 limits the byte-threshold path to at most nine tokens, so this is not reachable in the supported checkpoint.
- P1 (follow-up): broad exception-to-
Nonefallback is a collective-safety robustness gap, but no normal supported Kimi trigger for rank-local synchronous divergence is demonstrated. - P1 (follow-up): the MI35X layer-routing test can silently skip on unexpected AITER/Quark initialization errors.
The request-changes state remains because one reachable P0 is still open. Once it is fixed and current-head CPU/MI35X CI executes successfully, the remaining P1s do not block approval.
fused_allreduce_rmsnorm_quant called custom_fused_ar_rms_quant without the TC-piecewise CUDA-graph capture-state handling its unquantized sibling fused_allreduce_rmsnorm already has. Under the default ROCm prefill graph path, when AITER's _IS_CAPTURING is set but the current stream is outside capture, that kernel returns non-None dummy zero outputs which were then consumed as real (fp8, residual, per-token scale) activations. Mirror the existing guard and return None in that state so the caller falls back to the plain fused AR+RMSNorm registered path plus a separate per-token quant, which is correct under capture/replay. Add a deterministic CPU test that reproduces the exact capture state and pins each leg of the three-way predicate. Co-Authored-By: Claude <noreply@anthropic.com>
…port The broad import catch turned any AITER/Quark initialization failure into _HAS_PATH=False, silently skipping the required MI35x layer-routing regression on the exact runner where it must run. Split it: detect the target hardware defensively (non-gfx95 still skips), but leave the Quark / linear imports unguarded on gfx95 so an API move or a broken pinned build fails loudly instead of losing coverage. Co-Authored-By: Claude <noreply@anthropic.com>
kkHuang-amd
left a comment
There was a problem hiding this comment.
Re-reviewed head d31e005a79c702368756ff3a326968b0ba1b6d3c against latest main dad6fd0f04556a9a2c09fc08388ecee45ed5a33f.
The previous blocking findings are resolved: tuple eligibility and exact [M,1] routing are tied to the real preshuffled per-channel layout, output dtype is preserved, production ColumnParallelLinear coverage is present, and the fused per-token collective now avoids AITER dummy outputs in the TC-piecewise replay state. The MI35X import guard also fails loudly on the target platform. No current PR hunk is redundant with main.
The remaining 80-token one-stage cap and post-collective broad exception fallback are non-blocking P1 follow-ups already noted in review comments.
Approving from a code-review perspective. Before merge, integrate current main and rerun the required CPU and MI35X checks; the relevant MI35X targeted job passed, but aggregate CI is currently red.
|
@amd-bot ci-status |
Co-authored-by: mqhc2020 <62472426+mqhc2020@users.noreply.github.com>
|
@amd-bot ci-status |
Co-Authored-By: Claude <noreply@anthropic.com>
a59df27 to
4709d9a
Compare
…R capture guard test Co-Authored-By: Claude <noreply@anthropic.com>
|
@Emmanuel0612 Please fix lint test. |
…i35x The three test_pertoken_fp8_* tests lived under test/registered/kernels/, which fails the taxonomy check (kind must be one of unit/kernel/e2e/accuracy/ perf/stress under <kind>/<subsystem>/). They register the AMD mi35x suite, so place them in the vendor-exempt test/registered/amd/kernel/mi35x/ tree, matching the existing amd/<kind>/<arch> layout; suite registration unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
Thank you. It is done. |
…used RMSNorm+quant The fully-fused AR+RMSNorm+MXFP4 kernel has only a 1-stage tier; past its envelope (>56 tokens at n=7168) the 2-stage kernel rejects the shape and raises, crashing test_aiter_allreduce_fusion_amd at (64,7168). Gate fused_allreduce_rmsnorm_mxfp4_quant to return None past the 1-stage envelope, and route the corner case in LayerCommunicator to plain all-reduce + fused_rms_mxfp4_quant so RMSNorm+quant stay fused and the consumer still receives the (fp4, scale) tuple. Applies to both Kimi-K2.7 and Qwen3.5 (both set fuse_quant=True, quant_format=mxfp4). Co-Authored-By: Claude <noreply@anthropic.com>
This PR fuses the per-token FP8 activation quantization into the preceding RMSNorm for per-channel dynamic FP8 attention projections on AMD gfx95 (validated on MI355X with Kimi-K2.7-Code-MXFP4). By emitting a pre-quantized
(fp8, scale, orig_dtype)tuple straight from the norm, it removes the standalone per-token quant kernel before each attention projection, improving throughput by up to ~3.5% with no accuracy change.Motivation
For per-channel fp8 attention projections on gfx95, the per-token activation quant runs as a standalone
_per_token_group_quant_8bitlaunch immediately before each projection (fused_qkv_a_proj_with_mqa,q_b_proj,kv_b_proj). Profiling shows this as a separate kernel that can be folded into the preceding RMSNorm.Change
Fold the per-token fp8 quant into the preceding RMSNorm, so the projection receives a pre-quantized
(fp8, x_scale[m, 1], orig_dtype)tuple consumed directly bygemm_a8w8_bpreshuffleinapply_fp8_linear. The standalone quant kernel is eliminated. The tuple carries the original activation dtype so the GEMM output preserves it (FP16 stays FP16; not promoted to BF16).Fusion sites, each emitting a per-token whole-row scale
[m, 1]:fused_qkv_a_proj_with_mqa): fold intoinput_layernorm, covering the fused all-reduce + RMSNorm path (custom_fused_ar_rms_quant) and the post-all-reduce path used by large-batch prefill.q_b_proj: fold into the fused q/kv RMSNorm (fused_qk_rmsnorm,quant_type=per_Token).q_b_proj/kv_b_proj: fold into the single-tensor RMSNorm (rmsnorm2d_fwd_with_dynamicquant,group_size=0).Testing
test/registered/unit/models/test_pertoken_fp8_eligibility.py(CPU)— unit tests for the fold-eligibility predicate
_is_per_channel_dynamic_fp8. Asserts the layout contract is read from the layer's own state: the explicit_fp8_weight_preshuffledmarker (not the global gfx95 flag),input_scale is None(dynamic), andweight_scale.numel()equal to the projection's real output-channel count (output_size_per_partitionelseoutput_size), 1-D[N]or 2-D[N,1]. Covers eligible per-channel (1-D/2-D) vs. rejected per-tensor, static-input-scale, block-scale, non-fp8, missing-marker, and a scale sized to the input dimK(must be rejected).test/registered/kernels/test_pertoken_fp8_fold_parity.py(MI35x)—GEMM-level parity: the pre-quantized
(fp8, scale)tuple path vs. the standardapply_fp8_linearpath produce identical output, and the carried dtype is preserved (FP16 stays FP16; a 2-tuple falls back topre_quant_output_dtype/ BF16).test/registered/kernels/test_pertoken_fp8_quant_method_routing.py(MI35x)— drives the folded tuple through the real quant method rather than
apply_fp8_lineardirectly:test_quark_scheme_routes_tupleruns the actual QuarkW8A8Fp8apply_weightsfor the entry (2112), q_b (12288), and kv_b (16384) widths at M=1 (decode) and M>1 (prefill), BF16/FP16, asserting parity and dtype preservation;test_unwrapped_per_token_scale_does_not_assertguards the nativeFp8LinearMethod/ compressed-tensors unwrap path (apply_fp8_linear(input=qx, input_scale=scale[M,1])) that previously assertednumel == 1and failed for M>1.test/registered/kernels/test_pertoken_fp8_layer_routing.py(MI35x)—genuine layer-path regression test: builds a real
ColumnParallelLinear, quantizes and preshuffles its weight via the real Quark scheme (create_weights→ load →process_weights_after_loading, which sets the marker), then callslayer.forward()— exercising the full production routing (ColumnParallelLinear.forward → quant_method.apply → scheme.apply_weights → apply_fp8_linear → gemm_a8w8_bpreshuffle). Asserts the preshuffle marker landed, the folded-tuple forward matches the plain-activation forward, and dtype is preserved (q_b/kv_b, M=1 and M>1, BF16/FP16).test_pertoken_fp8_ar_capture_guard.py(CPU)— guards the P0 in
fused_allreduce_rmsnorm_quant. Under AITER's TC-piecewise replay state (_IS_CAPTURINGset, stream not capturing, in a piecewise graph),custom_fused_ar_rms_quantreturns non-Nonedummy zeros that would be consumed as real(fp8, residual, scale). Using a fake communicator with the three capture predicates patched, it asserts:— the wrapper returns
Nonewithout calling the kernel in that capture state, and— it still calls the kernel in the three non-capture cases (so the fold isn't silently disabled).
Pure control-flow logic, runs on CPU CI.
All four pass on MI355X (gfx950); the CPU eligibility test also runs on CPU CI.
Accuracy test
Accuracy (GSM8K) — neutral, within run-to-run noise:
Results (MI355X / gfx950, Kimi-K2.7-Code-MXFP4)
Speed Tests and Profiling
Throughput / GPU (tok/s) — consistent improvement:
Results (MI355X / gfx950, Kimi-K2.7-Code-MXFP4 / 8K-1K)
original profiling:


fold_quant profiling:
Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ⏳ Run #34840567183
Latest PR Test (Extra): ❌ Run #34840566707
Latest PR Test (AMD ROCm 10): ❌ Run #34840567029