[KMCompiler][Hygon] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert - #5449
Closed
cheersluvs wants to merge 9 commits into
Closed
[KMCompiler][Hygon] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert#5449cheersluvs wants to merge 9 commits into
cheersluvs wants to merge 9 commits into
Conversation
…or op discovery Three bugs in the test and benchmark for fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert, each of which makes them report something untrue. Scoped to this operator's own files. 1. is_support_fp8e4nv() gated on get_device_capability() >= (8, 9). That threshold means "Ada or newer" on NVIDIA only; other vendors report their own major/minor on a different scale, so it is a false negative that skips this whole file on hardware that supports the dtype. MetaX C550 reports (8, 0) and converts fp8e4nv bit-identically to torch. The check now consults an explicit vendor list before falling back to the NVIDIA capability rule. Four other files carry the same local copy of this check and have the same bug; they are out of scope here and left untouched. 2. The memory guard excluded shapes via a list hardcoded for an 80GB H800 -- which still OOMs on 64GB cards -- and skipped them with a bare return, so they were counted as passed. Replaced with a budget measured against free device memory, skipping explicitly. It collects before measuring and counts the allocator's cached blocks as available: releasing them with empty_cache() instead makes a marginal allocation fail that otherwise succeeds. 3. The benchmark probed torch.ops._C for its reference without importing the library that registers it, and torch.ops._C gives no hint that nothing did. Where the reference IS installed the benchmark reported it missing and skipped, so no comparison ran. The provider is not always vLLM's own: on MetaX it is mcoplib._C, while the vllm wheel there fails to load at all. Importing the top-level package is not enough; the compiled submodule must be imported before the schemas register.
…quant_insert
The generic kernel runs one program per (token, head) slot at num_warps=1 --
64 threads for 512 elements on a 64-lane warp. At 131072 tokens x 64 heads
that is 8.5M programs of 64 threads. Two costs follow: the KV slot does seven
quantisation blocks against a Q slot's normalise-and-rotate, so one slot per
program makes it a straggler, and 64-thread blocks under-fill the SM. The
straggler is 1-in-65 at 64 heads but 1-in-129 at 128, which matches the
measured shortfall being worse at the lower head count.
Raising num_warps alone does not help and measurably hurts (1134 -> 691 ->
398 -> 199 GB/s at 8192x64): work per program stays at 512 elements, so each
lane gets less to do while the program occupies more of the machine. Work and
width have to rise together. Each program here handles TPP=8 tokens of ONE
slot, keeping every program uniformly all-Q or all-KV -- no divergence -- at
num_warps=4, which is 16 elements per lane instead of 8. The (TPP, num_warps)
optimum runs along a diagonal; TPP=2/w1, 4/w2 and 8/w4 are all near the top,
so what matters is elements per lane rather than warps.
Below 512 tokens the generic kernel is faster and is dispatched to: TPP=8
masks off 7/8 of every program there, and the wider blocks raise the launch
floor from ~26us to ~32us. The crossover was measured at 512 on both head
counts (256 tokens: 0.96x/0.99x, 512 tokens: 1.03x/1.04x). TPP=8/w4 is best at
every size above it, so one configuration suffices.
Scoped to the MetaX backend rather than changing the generic op: all tuning is
C550-only and the tiling is matched to 64-lane warps, where num_warps=4 is 256
threads. On NVIDIA's 32-lane warps it is 128, a different trade-off entirely.
Measured on MetaX C550 against 1388 GB/s of achievable bandwidth (512 MiB
device-to-device copy):
64 heads 1164 -> 1339 GB/s (83.9% -> 96.5%)
128 heads 1224 -> 1354 GB/s (88.2% -> 97.5%)
Output is bit-identical to the generic kernel -- the FP8 cache matches byte for
byte and q matches exactly -- including at token counts that are not multiples
of TPP. The reduction structure is unchanged: reducing [TPP, 512] along axis 1
is the same tree per row as reducing [512], so the arithmetic order does not
move. test_backend_override_matches_generic asserts this for any backend
override, and skips where none is registered.
cheersluvs
requested review from
0x45f,
103yiran,
Caeruleann,
bin913,
douxetpur,
hellojack163,
huangyiqun,
tengqm and
w1120029931-bit
as code owners
August 13, 2026 09:13
cheersluvs
marked this pull request as draft
August 13, 2026 09:14
Contributor
Author
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.
A token-tiled Hygon override for
fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert. Two files, both undersrc/flag_gems/runtime/backend/_hygon/fused/— no shared module and no genericoperator changed.
Performance
Measured on Hygon BW1000 against 1340.3 GB/s of achievable bandwidth
(512 MiB device-to-device copy — measured, not a spec figure):
The generic kernel gives each program one
(token, head)slot atnum_warps=1,which on a 64-lane warp is 8 elements per lane, and that leaves more than half the
bandwidth unused here.
Through this repo's benchmark,
--mode kernel --level core, all 22 shapes, againstvLLM's own portable Triton implementation of this operator as the baseline:
Two caveats on that table, because it is easy to read more into it than it says.
The figures at 128 heads are larger mostly because the baseline degrades there:
this operator holds 89.6% of achievable at both head counts while the baseline
falls from 45% to 29.6%, so the honest summary is "about 2x, more at 128 heads
because the baseline drops". And the 1-17 token rows dispatch to the generic
kernel, so that gain is one fused launch beating the baseline's two and has nothing
to do with tiling.
The baseline is not a vendor kernel: there is no vendor implementation of this
operator for this card.
vllm._custom_opsregisters 49 ops and none is this one,and a scan of 2505 shared objects for the symbol found nothing. The vendor's
lightopdoes cover this stage —fused_rms_norm_rope_contiguousandfuse_rmsnorm_rope_quant_qkvboth takeslot_mappingandkv_cache— but onlyfor a scalar-scale FP8 convention:
kv_cache_scaleis a singlefloat, whichcannot carry seven per-block exponents per token, so that interface cannot express
this operator's UE8M0 blockwise layout whatever the kernel does internally.
What the optimisation does
Each program handles
TPP=8tokens of one slot, so every program is uniformlyall-Q or all-KV with no divergence, at
num_warps=4: 16 elements per lane insteadof 8, in a 256-thread block instead of 64.
Two axes matter and neither is visible on its own, which is worth recording
because tuning them separately gives the wrong answer twice over. A full
TPP x num_warpssweep puts every optimum atTPP / num_warps = 2— two tokensper warp, so 16 elements per lane. Bandwidth by elements per lane is
237 / 408 / 604 / 906 / 1183 / 1077 / 1050 GB/s for 1 / 2 / 4 / 8 / 16 / 32 /
64. But elements per lane does not explain everything:
TPP=1/warps=1andTPP=2/warps=2are both 8 elements per lane and differ by 50% (604 vs 906 GB/s),because the second has a wider program. Meanwhile sweeping
num_warpsalone atTPP=1shows 8 and 4 elements per lane tied at ~605, which invites the conclusionthat access width does not matter — at
TPP=1the block is only 512 elements andthere is nothing to widen into.
TPP=8 / num_warps=4is one of the optimal points and is also what the MetaX C550override in #5321 uses. Both parts have 64-lane warps, so the tuning transfers
directly.
Dispatch threshold
Below a threshold the generic kernel wins: a 256-thread block costs about 10 us
more to launch here (measured at one token: 80.5 us versus 90.2 us) and
TPP=8masks off most of every program when there are fewer than 8 tokens to fill it.
The threshold is a program count, not a token count. The crossover measured at
256 tokens for 64 heads and 128 tokens for 128 heads — 16640 and 16512 programs
respectively — so
num_tokens * (num_heads + 1)is what the two agree on, and thethreshold is 16384. A flat 256-token threshold would forfeit the 1.16x-1.32x
available between 128 and 256 tokens at 128 heads.
Correctness
This override is verified against the torch reference, not against the generic
kernel, and the distinction turned out to matter. Under the stack on the machine
used for development (torch 2.10.0 / FlagTree 0.6.1a1+hcu3.6) the tiled and
generic kernels are bit-identical at every shape tried — 32 shapes, 10 repetitions
each, zero differing bytes and zero differing q elements. Under the combination
backends.yamlpins for this card, which is what CI installs (torch 2.9.0 /FlagTree 0.5.1+hcu3.1), they are not:
qdiffers on 5 of 16744448 elements by upto 2 bf16 ULP at 511 tokens, and one FP8 cache byte differs at 1000 tokens.
That is expected rather than alarming, once stated precisely: a 2-D
[TPP, 512]tile assigns lanes differently than a 1-D 512 block, so the RMSNorm
variancerounds differently, and how that lowers is the compiler's choice. It is why
#5321's override test compares each implementation against the reference at the
tolerances the accuracy tests use, rather than requiring two Triton
implementations to agree more closely than either agrees with the oracle.
The accuracy suite is 60 passed / 0 failed / 0 skipped with the override
active.
That suite was also run with the threshold temporarily set to 0, forcing every
shape through the tiled path. This is the only way to exercise the
tok_okmaskingat 1, 4 and 17 tokens, where 7 or more of each program's 8 slots are empty — at the
production threshold those shapes dispatch to the generic kernel and the masking is
never tested. It passes 60/60 there too.
One thing for reviewers to decide
The ~140-line tiled kernel is now duplicated between
_metax/fused/and_hygon/fused/, identical in both. Both parts are 64-lane, so the two copies willstay in sync by accident rather than by construction. The alternatives are a shared
module or one backend importing the other's, and neither looked obviously right in
this layout, so raising it rather than deciding it unilaterally. Happy to factor it
out whichever way you prefer.