Add inverse_rope_group_quant op for DeepSeek-V4 wo_a input - #4428
Merged
Merged
Conversation
Contributor
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
|
yzhou103
force-pushed
the
feat/inverse-rope-group-quant
branch
3 times, most recently
from
July 28, 2026 15:27
b7b0436 to
c28a5c2
Compare
yzhou103
marked this pull request as ready for review
July 29, 2026 02:15
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a new DeepSeek-V4 output-path operator that fuses inverse (GPT-J style) RoPE on the attention output with FP8 (e8m0 scale) per-group quantization to produce the wo_a GEMM input, along with a correctness/perf test and the JIT/pybind plumbing to build and expose the HIP kernel.
Changes:
- Added a new HIP kernel (
inverse_rope_group_quant) + C++ interface and pybind module for fused inverse-RoPE + FP8 group quant. - Added a Python op wrapper that allocates outputs (including optional column-major scale layout via strides) and calls the compiled extension.
- Added an op test that validates fused vs reference (and includes an unfused baseline + optional HIP graph capture/replay check), plus JIT module registration and public export.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| op_tests/test_inverse_rope_group_quant.py | New correctness + benchmark + HIP-graph test harness for the fused op and an unfused baseline. |
| csrc/pybind/inverse_rope_group_quant_pybind.cu | New pybind translation unit to expose the C++ entrypoint and set stream handling. |
| csrc/kernels/inverse_rope_group_quant.cu | New HIP kernel + host launcher/validation for fused inverse RoPE and FP8 group quant. |
| csrc/include/rocm_ops.hpp | Adds the INVERSE_ROPE_GROUP_QUANT_PYBIND binding macro. |
| csrc/include/inverse_rope_group_quant.h | Declares the C++ API for the new op. |
| aiter/ops/inverse_rope_group_quant.py | New Python wrapper and @compile_ops entry for the new JIT module. |
| aiter/jit/optCompilerConfig.json | Registers the new JIT module sources/includes/flags. |
| aiter/jit/core.py | Excludes the new module from CK exclude-module logic (JIT plumbing). |
| aiter/init.py | Exports the new op at the package top level. |
Comments suppressed due to low confidence (1)
csrc/kernels/inverse_rope_group_quant.cu:300
positionsis indexed assuming unit-stride contiguous storage (positions[s]), but there is no contiguity/stride check for it. If a caller passes a strided view (e.g. slicing a larger positions buffer), the kernel will read the wrong positions and silently produce wrong output.
CHECK_CONTIGUOUS(o);
CHECK_CONTIGUOUS(x_fp8);
CHECK_CONTIGUOUS(cos_cache);
CHECK_CONTIGUOUS(sin_cache);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
yzhou103
force-pushed
the
feat/inverse-rope-group-quant
branch
9 times, most recently
from
July 29, 2026 05:41
56c7f24 to
aaeb216
Compare
Fuses the inverse RoPE on the V4 attention output with 1xN e8m0 FP8 group quant that produces the wo_a GEMM input, saving a round trip through memory. Kept as its own translation unit and its own JIT module: the op sits on the attention output path and shares no kernel or helper with the QK-norm/RoPE input-path ops, so folding it into their 6k-line file would only make every edit there rebuild it. transpose_scale stores the scale column-major, which is what the preshuffled-B blockscale GEMM reads; the plain gemm_a8w8_blockscale wants row-major instead, so the flag tracks whether B is preshuffled rather than "blockscale" in general. Both directions were checked numerically against the two GEMMs. Note this is a plain transpose, not the MX mx_scale_shuffle_idx swizzle, which belongs to the group-32 MX/MoE GEMMs. The test pins the op bit-for-bit against a torch reference -- both the dequantized values and the exact e8m0 scale bytes -- and times it against the unfused pair it replaces, triton's inverse RoPE followed by dynamic_per_group_scaled_quant. Fusing wins 1.2-2.1x across the sweep: ~2x while launch-bound at low token counts, settling to ~1.2x once bandwidth-bound, where it saves the bf16 round trip the two-kernel path needs between its halves. That round trip is also why the unfused baseline is checked against a reference that models it, and to one quantization step rather than exactly. Verified on gfx950 across quant group sizes 32/64/128, both scale layouts, and token counts 1..16384, plus HIP-graph capture/replay equivalence over all four kernel tiers. Co-authored-by: Cursor <cursoragent@cursor.com>
yzhou103
force-pushed
the
feat/inverse-rope-group-quant
branch
from
July 29, 2026 05:45
aaeb216 to
ca3d538
Compare
junhaha666
previously approved these changes
Jul 30, 2026
Fuses the inverse RoPE on the V4 attention output with 1xN e8m0 FP8 group quant that produces the wo_a GEMM input, saving a round trip through memory. Kept as its own translation unit and its own JIT module: the op sits on the attention output path and shares no kernel or helper with the QK-norm/RoPE input-path ops, so folding it into their 6k-line file would only make every edit there rebuild it. transpose_scale stores the scale column-major, which is what the preshuffled-B blockscale GEMM reads; the plain gemm_a8w8_blockscale wants row-major instead, so the flag tracks whether B is preshuffled rather than "blockscale" in general. Both directions were checked numerically against the two GEMMs. Note this is a plain transpose, not the MX mx_scale_shuffle_idx swizzle, which belongs to the group-32 MX/MoE GEMMs. The test pins the op bit-for-bit against a torch reference -- both the dequantized values and the exact e8m0 scale bytes -- and times it against the unfused pair it replaces, triton's inverse RoPE followed by dynamic_per_group_scaled_quant. Fusing wins 1.2-2.1x across the sweep: ~2x while launch-bound at low token counts, settling to ~1.2x once bandwidth-bound, where it saves the bf16 round trip the two-kernel path needs between its halves. That round trip is also why the unfused baseline is checked against a reference that models it, and to one quantization step rather than exactly. Verified on gfx950 across quant group sizes 32/64/128, both scale layouts, and token counts 1..16384, plus HIP-graph capture/replay equivalence over all four kernel tiers. Co-authored-by: Cursor <cursoragent@cursor.com>
Replace transpose_scale (bool) with scale_shuffle (bool): - False: row-major [S, G, Ks] (unchanged behavior) - True: MFMA tile-shuffled [G, S_pad, Ks_pad] for gfx950 batch GEMM The shuffle layout matches the permute pattern: view(M//32, 2, 16, K//8, 2, 4).permute(0,3,5,2,4,1) which packs scale bytes into 256-byte tiles of [32_M, 8_K] as the V_MFMA_SCALE instruction's op_sel expects. Co-authored-by: Cursor <cursoragent@cursor.com>
junhaha666
approved these changes
Jul 31, 2026
1 task
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
Fuses the inverse RoPE on the V4 attention output with 1xN e8m0 FP8 group quant that produces the wo_a GEMM input, saving a round trip through memory. Ships a HIP kernel plus a Triton implementation used as the test reference.
Kept as its own translation unit and its own JIT module: the op sits on the attention output path and shares no kernel or helper with the QK-norm/RoPE input-path ops, so folding it into their 6k-line file would only make every edit there rebuild it.
transpose_scale stores the scale column-major, which is what the preshuffled-B blockscale GEMM reads; the plain gemm_a8w8_blockscale wants row-major instead, so the flag tracks whether B is preshuffled in general. Both directions were checked numerically against the two GEMMs. .
Technical Details
Test Plan
Verified on gfx950 across quant group sizes 32/64/128, both scale layouts, and token counts 1..8192, matching the Triton reference on output and on the exact e8m0 scale bytes, plus HIP-graph capture/replay equivalence over all four kernel tiers.
Test Result
Measured on MI355X (gfx950, 256 CU), idle GPU,
run_perftesthip_us over 101 iters.Shape is the V4 attention output:
o[S, H, 512] bf16→x_fp8[S, G, 4096]+scale[S, G, 32]e8m0,rope_dim=64,quant_group_size=128.Baseline is the unfused two-kernel path this op replaces — triton inverse RoPE
(
_rope_cached_bwd, rotating the rope tail in place) followed bydynamic_per_group_scaled_quantwith an fp8_e8m0 scale buffer, i.e. the samepair atom's
_V4RoPE.inversepath runs today.hg = (n_local_heads, n_local_groups) = (16, 2) — V4-Pro TP8
Peak 6.13 TB/s at S=8192, or 77% of the 8 TB/s HBM roofline. Fusing wins
~2x while launch-bound at low token counts and settles near 1.2x once
bandwidth-bound, where the win is the saved bf16 round trip rather than the
saved launch.
hg = (8, 1) — V4-Flash TP8, single group
Quant group size axis — group 32 regresses at large S
hg=(16,2), row-major scale:
At S=8192 with
group_size=32the fused kernel loses (42.48 µs vs 41.13 µs) —the only point in the sweep where it does. The unfused baseline is flat across
group size, since its quant kernel reads the same bytes either way; the fused
kernel is not, because group 32 does 4x as many amax reductions per row as
group 128 and the cross-lane reduction path does not amortize them at high
token counts. The
wo_apath uses group 128, so this does not affect theshipped configuration. Correctness is unaffected at every group size.
Byte count
TB/s= bytes / measured µs, where bytes = reado, plus one cos and one sinrow per token (all heads of a token share the row), plus written fp8 data at
1 B/elem plus one e8m0 scale byte per group. Both columns use the fused op's
ideal byte count, so the unfused figure is an effective bandwidth over the
same logical work — it really moves more, round-tripping the bf16 rows between
its two kernels. Bandwidth is not meaningful below S≈128, where both paths are
launch-bound.
Submission Checklist