Skip to content

[KMCompiler][MetaX][Hygon][Mthreads][T-Head][Ascend] fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert backends, plus test and benchmark fixes - #684

Draft
cheersluvs wants to merge 38 commits into
flagos-ai:mainfrom
cheersluvs:deepseek-v4-quant-insert-metax-hygon
Draft

[KMCompiler][MetaX][Hygon][Mthreads][T-Head][Ascend] fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert backends, plus test and benchmark fixes#684
cheersluvs wants to merge 38 commits into
flagos-ai:mainfrom
cheersluvs:deepseek-v4-quant-insert-metax-hygon

Conversation

@cheersluvs

@cheersluvs cheersluvs commented Aug 14, 2026

Copy link
Copy Markdown

Backend overrides of fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert for
MetaX C550, Hygon BW1000, Moore Threads S5000, T-Head PPU-ZW810E and Ascend
910B
, plus seven fixes to this operator's test and benchmark that a card other
than an 80 GiB H800 exposes. Twelve files, all belonging to this operator — no
shared module added or changed.

The five overrides are not the same change. Three are token tiling (C550,
BW1000, S5000) with per-card launch constants and dispatch rules; one is an
enablement, not a tuning (ZW810E, where the operator could not be built at all);
and one is both (910B, which needed eight compiler workarounds before it ran and
then eight tuning steps).

One of the seven fixes is a prerequisite for all of it: as the file stands, a
vendor override of this operator can never be reached from its own test or
benchmark
. Details in the last section.

Performance against achievable bandwidth

The ceiling is taken warm, which matters more than it sounds. On C550 a
512 MiB device-to-device copy reads 1209.7 GB/s with three warmup iterations and
1396.6 / 1397.7 / 1397.9 once the clocks have ramped; measuring it cold at the
top of a script inflates every percentage computed against it — enough, in one
draft of this table, to put the operator above 100% of its own ceiling. Only
MetaX ramps
: the same probe on BW1000 gives 1343.9 cold against 1350.1 / 1349.9
/ 1349.9 warm, and on S5000 the ceiling taken after a full load is within 0.3% of
the one taken before it. Every card here uses the warm figure.

ceiling generic override
MetaX C550, 131072 x 64 1397 GB/s 1162.8 (83.2%) 1337.6 (95.7%)
MetaX C550, 32768 x 128 1397 GB/s 1224.2 (87.6%) 1355.5 (97.0%)
Hygon BW1000, 32768 x 64 1350 GB/s 609.9 (45.2%) 1197.8 (88.7%)
Hygon BW1000, 32768 x 128 1350 GB/s 611.6 (45.3%) 1209.6 (89.6%)
Hygon BW1000, 131072 x 128 1350 GB/s 612.3 (45.4%) 1213.8 (89.9%)
MTT S5000, 32768 x 64 1332 GB/s 1162.5 (87.4%) 1220.3 (91.6%)
MTT S5000, 32768 x 128 1332 GB/s 1245.0 (93.6%) no config wins, falls through
T-Head ZW810E, 32768 x 64 2097 GB/s cannot be built 2013.3 (96.0%)
T-Head ZW810E, 131072 x 128 2097 GB/s cannot be built 2014.1 (96.0%)
Ascend 910B, 32768 x 64 1229 GB/s cannot be built 179.2 (14.6%)
Ascend 910B, 32768 x 128 1229 GB/s cannot be built 265.1 (21.6%)

Ascend is the outlier and the percentage is the honest number, not a typo. That
card has no tl.float8e4nv, no pointer bitcast, no scalar float-to-int and a
65535-program launch cap, so there is no generic build to compare against; what
the override buys is measured below as a 6.7x / 9.9x improvement over the
first version that ran at all. The 1229 GB/s is the low end of a 1229-1433 GB/s
copy ceiling. Do not read the 14.6% as headroom that a day of tuning would take
— the closing ledger below lists what was tried.

Performance against a baseline

Through this repo's benchmark, --mode kernel --level core, 22 shapes, except
where noted. Which baseline is available differs per card and each one's
provenance is stated after the table.

64 heads 128 heads
C550 vs mcoplib, 8192-131072 0.870-0.888 → 1.004-1.015 0.945-0.951 → 1.047-1.052
C550 vs mcoplib, 1-64 tokens 1.23-1.61x, generic path both ways 1.27-1.52x, generic path both ways
BW1000 vs vLLM XPU Triton, 8192-131072 2.02-2.04x 2.99-3.22x
BW1000 vs vLLM XPU Triton, 1024-2048 1.94-1.98x 1.96-2.30x
BW1000 vs vLLM XPU Triton, 1-17 tokens 2.99-3.63x 2.47-3.62x
S5000 vs vLLM XPU Triton, 8192-131072 1.144-1.208 → 1.205-1.264 1.087-1.150, generic path
S5000 vs vLLM XPU Triton, 1-64 tokens 1.38-1.73x, generic path both ways 1.29-1.88x, generic path both ways
ZW810E vs T-Head's own vLLM, 32768-131072 1.24-1.25x 1.21-1.22x
ZW810E vs T-Head's own vLLM, 1024-8192 1.25-1.32x 1.23-1.26x
ZW810E vs T-Head's own vLLM, 1-64 tokens 1.34-1.41x 1.32-1.41x
910B vs an eager torch_npu composition, 32768-131072 11.3-11.8x 10.8-11.0x (to 65536)
910B vs an eager torch_npu composition, 1-64 tokens 7.6-8.8x 8.5-9.9x

The C550 rows need a disclosure, because the baseline cannot run unaided.
MetaX's kernel returns mcErrorInvalidValue from every launch of this op on C550
— every shape, both dtypes, with a valid configuration — so with a stock
environment this benchmark reports 1 skipped, which is what the wrapper
described further down is for. The numbers above were taken with an LD_PRELOAD
shim that forwards wcudaLaunchKernelExC to the classic mcLaunchKernel. That
forwarding is semantically exact here: numAttrs == 0 on these launches, so the
extended call carries nothing the classic one lacks — grid, block, shared memory
and stream. And the forwarded baseline was checked rather than assumed: running
test_matches_vllm_reference under the same shim on the same card passes 16/16 at
the accuracy suite's own tolerances, which rules out the specific failure mode
worth worrying about (MetaX's launcher computes kWarpsPerBlock = kBlockSize / 32
while C550 warps are 64 wide, so a half-covered grid would have looked
impressively fast). Reported as
MetaX-MACA/mcoplib#59. Both
halves of the C550 row come from the same card, branch, shim and harness — the
only difference is whether the override is registered.

The BW1000 and S5000 baselines need their provenance stated: they are the same
code, patched, and it is local-only.
Neither card has a vendor kernel for this
operator. On BW1000 the installed vLLM (0.18.1+das.fa71803.dtk2604) does not
ship the operator in any form — searching its tree for deepseek_v4 or
xpu_qnorm_rope returns nothing; on S5000 the installed vLLM is an .empty
build registering 0 deepseek ops, and vllm-musa 0.1.1 is a platform plugin,
not an operator library. Run as-is this benchmark reports 1 skipped on both.
The baseline above is vLLM's own portable XPU Triton implementation taken from
v0.27.1, checked function by function against upstream rather than trusted:
quantize_and_insert_k_kernel is byte-identical, and _xpu_qnorm_rope_kernel
differs by exactly six lines, which are two one-line fixes — tl.program_id cast
to int64, and a mask=offs < NOPE_DIM on the KV bulk store. The host wrappers
additionally drop vLLM-internal imports and the FNUZ branch (it needs
vllm.platforms) and gain a num_warps argument. None of this is in the
diff
— it is wired in by hand for the measurement and reverted afterwards.

Both fixes are needed to quote anything at these sizes: without the cast the
address computation wraps past 2^31 q elements, which 65536 x 64 already reaches;
without the mask the bulk store writes the unrotated RoPE region and a later store
overwrites it with no ordering, corrupting ~0.7% of tokens nondeterministically.
Both also make the baseline faster — the mask cuts total stores per token from
576 to 512 elements — so patching it makes this comparison harder on us, not
easier. Reported upstream as
vllm-project/vllm#52415 and
#52416.

The baseline runs at its own measured optimum on each card, not at a value
carried over. On BW1000 that is num_warps=1 (590.6 versus 572.9 / 461.8 / 241.6
GB/s at 2 / 4 / 8); on S5000 it is also num_warps=1, at 982.5 GB/s against
445.6 at num_warps=4
. That second figure is worth stating because an earlier
S5000 table quoted the num_warps=4 number: the baseline was crippled 2.2x —
worse than this operator was under the same compiler defect — so the old table
understated this operator, and the apparent 64-head "parity" in it was the
coincidence of both implementations landing near 446 GB/s. A benchmark harness
will not tell you a baseline is handicapped; back-compute GB/s from its own
reported latency and compare against a known bandwidth before believing any ratio.

What "as shipped" would give, and why this PR does not quote it

Upstream passes no num_warps — v0.27.1 and main both, at
vllm/models/deepseek_v4/xpu/xpu_qnorm_rope_kv_fp8_insert.py:135, with no
@triton.autotune — so running the baseline verbatim takes Triton's default of
4. Same harness, same 22 shapes, baseline rebuilt from upstream with both file
sha256 verified and only the two portability patches above applied:

8192-131072 as shipped (num_warps absent → 4) at the baseline's own optimum
BW1000, 64 heads 2.57-2.62x 2.02-2.04x
BW1000, 128 heads 2.89-3.03x 2.99-3.22x
S5000, 64 heads 2.68-2.81x 1.205-1.264x
S5000, 128 heads 2.81-2.92x 1.087-1.150x

On BW1000 the launch configuration only matters at 64 heads. There the
default takes the baseline from 45% to 34.5% of achievable and the ratio from
2.02 to 2.62. At 128 heads the two columns overlap and the baseline sits at
~30% of achievable either way: it is already degraded there for some reason that
is not the launch configuration, and this PR does not explain that — it is the
same unexplained 128-head effect noted in the decomposition above, now with one
more property rather than a mechanism.

On S5000 the default costs the baseline 982.5 → 445.6 GB/s and inflates the
ratio 2.3x across both head counts.

Two checks on the as-shipped run itself. It reproduces the num_warps=4 point
of the BW1000 sweep quoted above independently — 461.4 GB/s measured at
32768 x 64 against 461.8 in that sweep — and on both cards the as-shipped
baseline lands at 33-34% of a ceiling it otherwise reaches 45% of, which is the
signature of a handicapped launch rather than a slow kernel.

This PR quotes the at-optimum column. A gap that one launch argument closes
is not this operator's contribution. Rows below ~192 tokens are omitted from the
as-shipped table: that region spreads 12-34% across repetitions on these parts,
and the as-shipped run was single-pass.

The BW1000 45% → 90% figures were measured separately on this branch by comparing
flaggems_vllm.ops.<op> against flaggems_vllm.<op> in one process — the generic
implementation and whatever the vendor registrar bound — so there is no file
editing between the two halves and no chance of drift.

On BW1000, that there is no vendor kernel was established by symbol scan rather
than by absence of an import: vllm._custom_ops registers 49 ops and none is this
one, and 2505 shared objects export no matching symbol. The vendor's lightop
does cover this stage — fused_rms_norm_rope_contiguous and
fuse_rmsnorm_rope_quant_qkv both take slot_mapping and kv_cache — but only
for a scalar-scale FP8 convention: kv_cache_scale is a single float, which
cannot carry seven per-block exponents per token, so that interface cannot express
this operator's UE8M0 blockwise layout whatever the kernel does internally.

The 910B row is not from this benchmark. The harness gates on
VLLM_REF_AVAILABLE and self-skips there, because no baseline of any kind exists
on that card — not a vendor kernel, and not even vLLM's portable Triton fallback,
which does not compile (details in the Ascend section). The eager torch_npu
composition it is measured against is described there too. Those figures come
from a standalone script, and saying so is the point: they are not the same
measurement as the rows above.

What the token tiling does

This part applies to C550, BW1000 and S5000. The generic kernel runs one program
per (token, head) slot at num_warps=1 — 64 threads for 512 elements on a
64-lane warp, which is 8.5M programs of 64 threads at 131072 x 64. 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, matching the measured shortfall being worse at the lower head count.

Each program here handles TPP tokens of one slot, so every program is
uniformly all-Q or all-KV with no divergence. On C550 and BW1000 that is
TPP=8/num_warps=4: 16 elements per lane instead of 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_warps sweep on BW1000 puts every optimum at TPP / num_warps = 2
two tokens per 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=1 and
TPP=2/warps=2 are both 8 elements per lane and differ by 50% (604 vs 906),
because the second has a wider program. Meanwhile raising num_warps alone at
TPP=1 measurably hurts on C550 (1134 → 691 → 398 → 199 GB/s) and shows 8 and 4
elements per lane tied at ~605 on BW1000 — which invites the conclusion that
access width does not matter. At TPP=1 the block is only 512 elements and there
is nothing to widen into.

Do not read a (TPP, num_warps) pair across cards. S5000 has a 32-lane
warp where the other two have 64, so its stock num_warps=1 launch already
delivers 16 elements per lane — the value C550 and BW1000 need TPP=8/warps=4 to
reach. Read elements per lane and program width, not the constants.

Moore Threads S5000 is a two-case rule, and a toolchain floor

Tiling pays at 64 heads only. Against a 1332 GB/s ceiling at 32768 tokens the
generic kernel reaches 87.4% at 64 heads but 93.6% at 128, and a full
TPP x num_warps sweep at 128 heads finds nothing above 0.94x — there is no
headroom left there. The tests and benchmark use 64 and 128 heads only, so this
is an exhaustive two-case rule rather than a fitted threshold. At 64 heads the
best cell is TPP=4/num_warps=4, 91.6% of ceiling.

The 64-head dip is real and was checked before being built on: 48 heads 90.8%,
64 heads 87.4%, 96 heads 90.8%, 128 heads 93.6%, with 0.1-0.6% spread over
three round-robin rounds and -0.3% ceiling drift. It is not monotonic, so "the KV
straggler is amortised as head count rises" is wrong, and it is not a
power-of-two stride effect either — 64 is the worst and 128 the best. The
mechanism is unexplained; it is also not load-bearing, since only 64 and 128
occur.

The token threshold is 512, chosen from where the measurement stops being noise:
below 192 tokens three round-robin repetitions spread 12-34%, so the apparent
0.82x at 64 tokens and 1.02x at 96 are both unusable. From 192 up the spread falls
to <= 3%, and the crossover sits between 256 (1.00x) and 512 (1.03x).

This override requires flagtree >= 0.6.1+mthreads3.6, and that is a real
floor, not a precaution.
On older builds the tiled kernel does not compile in
any configuration — five were tried, including TPP=1/num_warps=1, whose 16
elements per lane match the generic launch that works fine, and all five abort
with llc code -6. The defect is an instruction-selection failure in the llc
shipped with MUSA toolkit 4.3.5 (MTGPU DAG->DAG Pattern Instruction Selection,
SelectionDAGISel::CannotYetSelect on a v8bf16 LSU_LD_CACHE_HINT), reached
because the older FlagTree wheels ship no bin/ and Triton falls back to the
system binary. 0.6.1+mthreads3.6 bundles a working llc at
triton/backends/mthreads/bin/llc, md5 cec9ff66714e311670b9412ec760e4aa
byte-identical to the build the kernel was validated against, which is how the
version floor was confirmed rather than assumed. The two binaries both
self-report LLVM 14.0.0, so a version string cannot tell them apart; compare md5.
On a build without it the dispatch simply routes to the generic kernel, so
nothing here is reached and nothing breaks.

This repository pins no compiler — there is no backends.yaml and no flagtree
dependency — so which Triton runs is the installation's choice. That is also why
mthreads is added to the fp8 vendor whitelist here: I argued against it twice on
the grounds that opening the gate would turn a clean skip into compile failures
on the pinned FlagTree, which was importing a constraint from FlagGems that this
repo does not have.

Measured on the card with 0.6.1+mthreads3.6, two independent benchmark runs
agreeing within 0.5% on every shape:

tokens 64 heads (generic → tiled) 128 heads (control)
1024 0.965 → 1.040 1.074 → 1.064
2048 1.115 → 1.088 1.077 → 1.078
8192 1.144 → 1.205 1.088 → 1.087
32768 1.197 → 1.252 1.124 → 1.122
65536 1.208 → 1.253 1.151 → 1.150
98304 1.182 → 1.222 1.114 → 1.121
131072 1.200 → 1.264 1.138 → 1.135

The 128-head column is a built-in control and it holds: it takes the generic
path either way and is unchanged cell by cell, so the 64-head movement is the
tiling and not environment drift. Large shapes gain 3.4-5.3%, consistent with the
sweep's TPP=4/warps=4 figure of 1.06x against generic. The 2048 cell goes the
other way by 2.5%, which is inside this card's noise band but is not hidden here.

mthreads is added to the fp8 vendor whitelist, so the suite now runs on S5000
instead of skipping 108 tests. That gate is what hid the hardcoded device = "cuda" underneath it: nothing on that card ever reached the operator, so neither
defect was visible.

Accuracy on the card, excluding the four largest token counts: 76 passed, 16
skipped, 0 failed
— every 64-head case at 512 tokens and above, which is the
set that takes the tiled path, so the new llc produces a kernel that is not only
buildable but numerically right. What happens at the four excluded shapes is in
the last section, under the memory-guard fix; it is not a property of this
override.

T-Head PPU-ZW810E: the operator could not be built at all

Not a tuning problem. tl.float8e4nv is refused for this card, so the generic
kernel does not compile — there is no "before" to compare against.

The card reports capability (8, 0) and its Triton target is ppu:80, and the
fp8 policy is inherited from the NVIDIA backend at two levels. FlagTree's
third_party/ppu/backend/compiler.py adds fp8e4nv to supported_fp8_dtypes
only when capability >= 89; that option is overridable, and overriding it moves
the failure into the PPU's own lowering pass, which fails with
`LLVM ERROR: Conversion from/to f8e4m3nv is only supported on compute capability

= 89— an NVIDIA threshold, and an NVIDIA concept, enforced insideConvertTritonGPUToLLVMPPU`. So the format is refused by inherited policy rather
than shown to be unsupported.

The override changes one thing: the seven unrolled quantisation groups call
_f32_to_e4m3_bits instead of .to(tl.float8e4nv). Everything else is the
generic kernel. The encoder is integer arithmetic — which is what a software fp8
conversion does anyway below sm_89; torch's own cast on this card takes such a
path. It is bit-identical to torch.float8_e4m3fn over 65536 values, half of
them drawn from the subnormal range below 2^-6, which is the part a naive encoder
gets wrong: a first version flushed subnormals to zero and also mis-saturated
exponent 15 with mantissa 0, which is the legal value 256 rather than an overflow.

Accuracy suite on the card: 92 passed, 16 skipped (the 16 are the vLLM
cross-check; the vLLM installed there is 0.19.0, which predates this operator).
That run predates the memory-guard removal described in the last section, so on
this card as on the others, whether the four largest shapes fit is a separate
question from whether the override is correct.

Performance is not measurably hurt by the software conversion — unsurprising for a
memory-bound operator, but measured rather than assumed: 96.0% of a warm
2097 GB/s ceiling
at 32768 and 131072 tokens, ramping from 63.9% at 1024. Small
shapes need a cache flush between iterations to measure at all: this part has
roughly 32-64 MiB of cache (a 512 MiB copy reads 2097 GB/s, a 32 MiB copy 7140),
and the operator rewrites q in place, so at 1024 tokens its 67 MB footprint sits
right at the boundary and an unflushed measurement reads high.

The ZW810E baseline is T-Head's own vLLM port, used unmodified. That card's
pip index serves only +ppu* builds, and the newest,
vllm==0.20.1+v0.1.0.ppu2.1.0, registers
_C::fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert — a vendor C++
implementation, which does its FP8 conversion outside Triton and is therefore
untouched by the fp8e4nv gate that blocks the Triton path on the same card. It
launches, and it agrees with this operator under the accuracy suite's own
k_cache_compare at 64x64, 777x64 and 4096x128.

This is the only one of the baselines that needs no local patch at all: with
the vendor wheel on PYTHONPATH the benchmark resolves torch.ops._C
unmodified. Figures are from two independent runs agreeing within ~1% on every one
of the 22 shapes, with a known-answer device check run clean before and after each
— that check is there because this card intermittently enters a state where it
returns wrong answers
, cause unidentified, so every measurement on it is
repeated rather than trusted once.

The box also carries a stock vllm 0.19.0+cu130, which predates the operator; an
earlier revision of this description reported ZW810E against vLLM's portable XPU
Triton implementation carrying our own E4M3 encoder, because that stock build was
mistaken for the only vLLM available. Those numbers are withdrawn in favour of
these.

Delete _thead/fused/ once the PPU backend either lowers fp8e4nv or reports a
target that permits it. Nothing else in it differs from the generic
implementation.

Ascend 910B: eight compiler limits, then 6.7x / 9.9x

Also an enablement before it is an optimisation, and for more reasons than one.
Four separate constructs the generic kernel relies on are unavailable here, and
they are the same four that stop vLLM's portable Triton fallback from
compiling on this card — taken verbatim from upstream and tried at this
operator's own shapes:

x_clamped.to(tl.float8e4nv)
    -> 'hivm.hir.vcast' op currently don't support cast
       float_to_UNKNOWN_rintmode / unrecognized float type: 'f8E4M3FN'
token_bf16_ptr.to(tl.pointer_type(tl.bfloat16))
    -> Casting pointers with unmatched bitwidth!

with encoded_scale.to(tl.uint8) from a float and an inexact tl.exp2 waiting
behind them. So 910B has no baseline of any kind — no vendor kernel and no
portable fallback — which is a stronger statement than the "no vendor op" that
applies to BW1000 and S5000, and is exactly why this file exists.

The override therefore carries: an integer E4M3 encoder (same reason as ZW810E),
the UE8M0 scale built from exponent bits rather than a float-to-int, a 2-D offset
for the RoPE pairs instead of a pointer cast, and a launch issued in chunks of
65535 programs because the runtime refuses anything wider.

Tuned from 26.8 GB/s — the first version that ran — in four steps, each one a
measurement correcting a guess:

step what 64 heads 128 heads
one (token, head) unit per program 26.8 26.8
1 32-unit flat Q tile 56.9 70.5
2 E4M3 subnormals rounded arithmetically 81.6 86.8
3 Q tiled over heads within a token 114.6 186.4
4 KV quantised as one [8,64] tile, 3 dead branches cut 179.2 265.1

6.7x and 9.9x, confirmed two ways: a private loop timing both versions
alternately on the same tensors, and the repo's own Benchmark.run() with the
old operator bound into the empty torch_op slot. The two agree at every shape
from 1024 tokens up.

Two of those steps are worth stating because the mechanism was not the obvious
one. Step 2 looked like an encoder rewrite; the actual cost was three shifts by
a per-lane amount
in the subnormal path, which this vector unit has no
instruction for and so scalarises — an E4M3 subnormal is m * 2^-9, so adding
2^23 and subtracting it again forces the same rounding with no shift at all, and
quantisation went 1.276 → 0.627 us/token. Step 3 looked like a tiling win and was
really a gather removed: every head of a token shares one position, but a flat
(token, head) tile makes position_id a vector, so each unit re-gathered the same
256 bytes of cos/sin once per head. Tiling heads of one token makes it scalar,
11.43 → 4.15 ns/unit at 64 heads and 16.81 → 3.99 at 128 — which also explains
what had looked unexplainable, the same kernel being 48% slower at 128 heads than
at 64.

Then the Q and KV halves were merged into one launch branching on the program
id, rather than two. A launch costs ~450 us on this card, so this is 1.94x below
~256 tokens
and 1.02x at 1024 and above, bit-exact including mixed negative
slots. It also stops num_tokens being a constexpr, so shapes no longer force
recompiles — the suite runs in 5.15s against 13.53s.

That merge exists because of a lesson about A/B measurement, not because of a
kernel insight: the first before/after sampled only the shapes the tiling
targeted, all ≥1024 tokens. Run against the benchmark's own shape list, the tuned
version was ~1.9x slower below 64 tokens — two launches where the untuned one
had issued a single fused launch. Those are the decode-side shapes. A before/after
that samples only the shapes the optimisation targeted will not show what it cost
elsewhere.

The baseline here is an eager torch_npu composition, since neither of the
usual two exists: npu_rms_norm for the weightless RMSNorm (not a naive five-op
version), elementwise RoPE, the same integer FP8 encoder the kernel uses
(torch_npu cannot cast to float8_e4m3fn at all, so there is no alternative),
and an advanced-indexing scatter into the paged cache. It is validated against
the test file's own oracle with the operator taken out of the middle, so the
argument is not circular — k_cache bit-identical on every shape checked, mixed
negative slots included. It settles at ~11x on large shapes; the 7.6-9.9x at small
shapes is dozens of framework launches against one.

It cannot run 98304x128 or 131072x128, and not because of any single
allocation: the composition holds q.float(), npu_rms_norm's fp32 output, the
fp32 rope intermediate and the bf16 write-back live at the same time, about six
times q's own size, so the peak is 75 and 100 GiB against 60.96 available. Those
two are skipped by predicting the footprint, not by catching the failure — on
this backend an allocation error does not reliably land on the call that caused
it, and one left in flight poisons the rest of the process, which is how a stray
aicpu timeout corrupted an earlier sweep of this same baseline.

Two of 22 shapes, and not the large end. 131072 tokens at 64 heads runs and
is reported; only its 128-head partner is missing, which is why the 128-head
column above is labelled to 65536 rather than averaging over the survivors and
calling the range 32768-131072. The operator does both missing shapes in 97 and
129 ms, so the card is not the limit here — the composition is.

Two ways to make them fit were considered. Chunking over tokens multiplies the
framework launch count by the chunk count; the overhead looks small at these
sizes but it moves the ratio in our favour, and it was not measured, so it is not
relied on. Feeding npu_rms_norm bfloat16 would fit in 36 GiB and moves the
ratio against us, which is the safe direction, but it rounds the normalised
value before the rotation and stops matching the kernel's arithmetic. Neither is
needed to support a claim made here, so the two cells stay empty and say why.

ascend is in the fp8 vendor whitelist for the same reason as mthreads and
metax: the card reports capability (0, 0), so the NVIDIA rule skips the whole
file.

Two things this card taught that are worth a reviewer's time. First, torch_npu
cannot cast to float8_e4m3fn
, so the test's torch oracle needs a CPU fallback
for that one step — one of the seven fixes below. Second, twice during this work
the operator was right and the oracle was wrong (exp2, rsqrt); the only
thing that ever settled such a case was dumping intermediates and diffing
bit-for-bit against an exact reference until the first divergence appeared.

Everything else tried on this card, and rejected on measurement: packing four
FP8 bytes into an int32 store (0.19x — five times slower, and bit-exact, so
the packing was right); num_warps 2 or 4 (identical to three decimals, here as
on every kernel in this PR); tiling KV over tokens as well (1.05-1.06x, ~3% of the
operator, against having to solve the scattered cache store); a 16-bit encoder
(0.99x, after a synthetic microbenchmark promised 1.33x — its op mix was not
the encoder's); capping the grid at the vector core count and looping inside
(nothing, and 20/32/40/48/64/128/256 were all measured); an exact reciprocal
multiply instead of the divide (nothing, within 1%); and TLE
dsa.alloc/dsa.copy staging the Q tile through UB (1.00x / 0.99x,
bit-exact — on this card the vector unit can only reach UB, so tl.load already
stages through it and the explicit copy restates what the compiler does anyway).
Nothing on that list is untried.

Dispatch thresholds

Below a threshold the generic kernel wins, because a wider block costs more to
launch and TPP masks off most of every program when there are fewer tokens than
that to fill it. The three tiling cards want three different rules and all three
were measured:

  • C550: num_tokens < 512 falls through, on both head counts (256 tokens:
    0.96x/0.99x; 512: 1.03x/1.04x). The launch floor moves ~26 us → ~32 us.
  • BW1000: crossover at 256 tokens for 64 heads and 128 tokens for 128 heads —
    16640 and 16512 programs respectively, so the quantity the two agree on is
    num_tokens * (num_heads + 1), and the threshold is a program count, 16384.
    A flat token threshold would forfeit the 1.16x-1.32x available between 128 and
    256 tokens at 128 heads. The launch floor here is much higher in absolute terms:
    80.5 us → 90.2 us at one token.
  • S5000: num_heads > 64 or num_tokens < 512 falls through — the two-case
    head rule above, plus the token threshold from where the measurement stops being
    noise.

Correctness

Each override is verified against the torch reference, not against the generic
kernel, and that distinction turned out to matter. On C550 the tiled and generic
kernels are bit-identical. On BW1000 they are bit-identical under torch 2.10.0 /
FlagTree 0.6.1a1 — 32 shapes, 10 repetitions each, zero differing bytes — but
under the older combination that card's pinned stack installs (torch 2.9.0 /
FlagTree 0.5.1), q differs on 5 of 16744448 elements by up to 2 bf16 ULP at 511
tokens and one FP8 cache byte differs at 1000 tokens. The same appears on 910B
between the pre- and post-tiling versions: 66 of 33.5M elements at 2-3 bf16 ULP,
at magnitudes near 1e-6.

That is expected once stated precisely: a 2-D [TPP, 512] tile assigns lanes
differently than a 1-D 512 block, so the RMSNorm variance rounds differently, and
how that lowers is the compiler's choice. Near zero, bf16 is sparse enough that
"3 ULP" is 1.2% of a value whose absolute error is 3e-8. So
test_backend_override_matches_reference compares each implementation against the
reference at the tolerances the other tests use, rather than requiring two Triton
implementations to agree more closely than either agrees with the oracle. It runs
on shapes the other tests do not reach — 511, 512, 777, 1000, 4096 tokens, chosen
for not being multiples of a plausible tiling — and skips where no override is
registered.

On BW1000 both suites are green: 60 passed / 0 failed / 0 skipped with the
override active, and the same with the dispatch threshold temporarily set to 0,
which is the only way to exercise the tok_ok masking at 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.

On S5000, each of the four dispatch branches was checked directly: 777x64 and
32768x64 take the tiled path and match the generic kernel byte-for-byte in the FP8
cache with q within one bf16 ULP, while 511x64 (below the token threshold) and
777x128 (above the head threshold) are bit-identical, which is what confirms
the fallback really calls the generic implementation.

Verification

Every figure and pass/fail above was produced by running this branch's code on the
physical cards, not inferred. Where a measurement is not from this repo's
benchmark — the 910B rows, the BW1000 generic-vs-override bandwidth pair — that is
said at the point it appears.

select_backends.py picks non-NVIDIA backends from the changed paths under
src/flaggems_vllm/runtime/backend/_<vendor>/, so this PR should select metax,
hygon, mthreads, thead and ascend on its own, provided each is marked enabled in
the CI registry — I cannot see .ci/flaggems-registry/.github/backends.json from
here to confirm which are. The run is currently action_required because this
comes from a fork, so it needs a maintainer to approve it before anything
executes. Happy to attach raw logs from the cards in the meantime.

The seven test and benchmark fixes

A vendor override of this operator was unreachable from its own test and
benchmark.
SpecOpRegistrar writes vendor implementations into
flaggems_vllm's module globals, but this operator's test called
flaggems_vllm.ops.<op> and its benchmark did set_gems(flaggems_vllm.ops.<op>)
attributes of the ops submodule, which the registrar never touches. So both
always ran the generic kernel, silently, and no override for this operator could
ever have been measured or validated. Both now use the top-level binding
(flaggems_vllm.ops_experts_impl, flaggems_vllm.weight_norm,
flaggems_vllm.sparse_attn_triton, flaggems_vllm.w8a8_block_fp8_matmul are the
existing examples). Of the 29 set_gems call sites in benchmark/, 28 use the
top-level binding; the only other .ops. one is
test_flash_attn_varlen_opt_init_func.py, whose operator has no vendor override,
so it is unaffected. This is a prerequisite for the five overrides above to have
any effect.

device = "cuda" was hardcoded in four places. MACA aliases cuda, so this
is invisible on MetaX; on MUSA every case dies at its first allocation with
NotImplementedError: Could not run 'aten::empty.memory_format' with arguments from the 'CUDA' backend, before reaching the operator at all — 60 of 92 tests,
none of them for a reason related to the operator. Now flaggems_vllm.device,
which 60 of the 69 test files already use. Same family as the capability gate
below: both silently exclude non-NVIDIA backends, and neither can be noticed from
an NVIDIA machine.

The correctness oracle depended on test collection order. ref_impl ran
vLLM's C++ kernel instead of this file's torch reference whenever
torch.ops._C happened to carry the op. Nothing in this module imports vLLM — but
test_cp_gather_indexer_k_quant_cache.py and test_cutlass_scaled_mm.py do so at
module level and both sort earlier, and pytest imports every collected module
before running any test. So the oracle was vLLM's kernel when the suite ran and
the torch reference when the file ran alone, with nothing reporting the switch.
Worse, the vLLM path cannot execute on MetaX C550, so simply adding the missing
import — the obvious fix — would have broken the suite there. ref_impl is now
always the torch reference, and the vLLM cross-check is its own skipif-gated
test, which is how test_persistent_topk and test_top_k_per_row_decode are
already structured.

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 — C550 reports (8, 0), S5000 (3, 1), ZW810E
(8, 0) and 910B (0, 0), and all four convert fp8e4nv correctly (two of them via
the integer encoder in this PR). The check now consults an explicit vendor list
before falling back to the NVIDIA capability rule, and stays local to these two
files.

The torch oracle's FP8 cast cannot run on every backend. torch_npu has no
cast to float8_e4m3fn at all, so on 910B the reference itself raises before the
operator is reached. That one step now falls back to CPU, and the oracle's
power-of-two scale is built exactly rather than through exp2, which is off by one
ULP for integer arguments there — in both tl and torch. Both are oracle fixes
and neither touches the operator.

The shape exclusion was hardcoded and silent. if (num_tokens == 98304 or num_tokens == 131072) and n_heads == 128: return — a bare return, so those four
of 60 cases were reported as passed, and having been added in the commit that
introduced the operator, they had never actually run on any card. The list is also
wrong in both directions on a 64 GiB part: it excludes the 128-head shapes and does
not exclude 64 heads x 131072, which OOMs there. Removed outright, with nothing
in its place: those cases now run, and on a card that cannot hold the reference
they fail visibly rather than reporting success.

Two softer treatments were tried on this branch and both are reverted. Evaluating
the reference and the comparison in token slices let the large shapes fit, but it
changed what the oracle computes — and an oracle is the wrong place to economise:
if the whole-tensor path is right and the kernel has a size-dependent fault, a
sliced reference agrees with it and hides the one bug worth finding. A memory
budget check that skipped shapes instead was closer to honest, but a shape that
cannot be verified on a given card should say so rather than disappear.

What that costs, stated plainly, because it is more than "those shapes go red".
On 910B it is 11 OOM cases out of 92, with the operator not on the stack — the
oracle's xf.pow(2).mean(...) builds a 32 GiB float32 copy of a 16 GiB q.
On S5000 it is worse than a per-case failure: MUSA errors are sticky, so the
first failure poisons the context and every later GPU call in the same process
raises too. The same command run twice gave 12 failed / 80 passed and 55 failed /
37 passed — the difference being only where in the collection order the first
failure landed. Exclude the four largest token counts and it is 76 passed / 16
skipped / 0 failed. The first failure there is itself a known vendor defect
(torch_musa#146): boolean
masked assignment fails past 2^31 elements, and it sits directly on
assert_close's diff-reporting path, so the failure surfaces as
RuntimeError: Comparing with the actual difference never printed. Neither of
those is caused by this change; both are made visible by it, which was the point.

The benchmark probed torch.ops._C without importing the library that
registers the op
, 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 at all. It now does exactly what the three sibling benchmarks that
use torch.ops._C do — top_k_per_row_decode, persistent_topk and
cutlass_scaled_mm — a single import vllm._custom_ops under try/except, then
hasattr. On MetaX C550 that entry point is also the one that works: import vllm._C fails on libcudart.so.13, while vllm._custom_ops registers all 57 ops
through mcoplib.

That guard catches RuntimeError as well as ImportError/AttributeError, which
is a deviation from the three siblings and is deliberate: on a box with two vLLM
platform plugins registered, the import raises RuntimeError: Only one platform plugin can be activated, but got: ['fl', 'musa'], and with the narrower guard that
aborts collection of the entire file rather than degrading to "no baseline".
A misconfigured optional dependency should not be able to do that. It is not
widened to bare Exception, which would swallow real problems.

A reference can also be registered and still not run, so the benchmark
distinguishes the two. MetaX's build of this op returns mcErrorInvalidValue from
every launch on C550, and finding it that way would turn the benchmark red on the
vendor's defect, while skipping as "not installed" hides it. The reference is
wrapped so the first call reports the launch error as a skip reason. Making that
work took measuring how the failure propagates, and the result is worth recording:
on that backend a failed launch is not surfaced by synchronize(), nor by a
device-to-host copy, nor by a small allocation — it is surfaced by the next kernel
launch, which is why it otherwise lands on do_bench's 256 MB L2-flush
allocation, several frames from the cause.

Two things reviewers should know

Vendor discovery is a no-op where mcoplib is absent, so the benchmark skips
exactly as before on such machines. For anyone running it on a machine that does
have it: on C550 that kernel cannot launch (reported as
MetaX-MACA/mcoplib#59), and the
benchmark will now say so rather than silently comparing against something else.

The ~140-line tiled kernel now exists in three copies_metax/fused/,
_hygon/fused/ and _mthreads/fused/ — with identical kernel bodies; only the
launch constants and the dispatch rule differ per card. (_thead and _ascend
are not copies of it: one is the generic kernel with a different FP8 conversion,
the other a differently-shaped kernel built around this card's compiler limits.)
Three copies will stay in sync by accident rather than by construction, and the
case for factoring it out is stronger than it was with two. The alternatives are a
shared module or one backend importing another'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.

…v_rope_quant_insert, plus test and benchmark fixes

Token-tiled backend overrides for MetaX C550 and Hygon BW1000, and five fixes to
this operator's test and benchmark that any card other than an 80GiB H800
exposes -- including one that made every vendor override for this operator
unreachable from its own test and benchmark.

MetaX C550   131072x64  1164 -> 1339 GB/s (83.9% -> 96.5% of achievable)
             32768x128  1224 -> 1354 GB/s (88.2% -> 97.5%)
Hygon BW1000 32768x64   604.8 -> 1182.9 GB/s (45.1% -> 88.3%)
             32768x128  609.2 -> 1201.4 GB/s (45.5% -> 89.6%)
@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…acle depending on collection order

The benchmark now resolves torch.ops._C the way the three sibling benchmarks
that use it already do -- a single 'import vllm._custom_ops' guarded by
try/except, then hasattr -- instead of the multi-library loader with its own
logging. The mcoplib fallback was unnecessary: vllm._custom_ops is what works on
MetaX (vllm._C is the one that fails on libcudart). _skip_if_unrunnable stays,
now documented as the one deliberate deviation, because MetaX C550 registers the
op and cannot launch it, which no sibling has hit.

In the test, ref_impl silently ran vLLM's C++ kernel instead of the torch
reference whenever torch.ops._C carried the op. Nothing in this module imports
vLLM, but test_cp_gather_indexer_k_quant_cache and test_cutlass_scaled_mm do so
at module level and both sort earlier, so the oracle differed between running
this file alone and running the suite. ref_impl is now always the torch
reference; the vLLM cross-check is its own skipif-gated test, as in
test_persistent_topk.
…pe_quant_insert at 64 heads

Tiling pays on S5000 only at 64 heads and only above 512 tokens, and the file
says so rather than reusing the MetaX or Hygon rules -- those parts have 64-lane
warps, so their tuning does not transfer to a 32-lane one where the generic
launch already gives 16 elements per lane.

At 32768 tokens against a 1332 GB/s ceiling the generic kernel reaches 87.4% at
64 heads and 93.6% at 128; a full TPP x num_warps sweep at 128 heads finds
nothing above 0.94x. Tests and benchmark use 64 and 128 heads only, so this is an
exhaustive two-case rule. Below 192 tokens three round-robin repetitions spread
12-34%, so the crossover was taken from the stable region: 256 is 1.00x, 512 is
1.03x, and the threshold is 512.

The gain is ~1.04-1.05x at 64 heads on large shapes and nothing at 128 -- far
from Hygon's 2x, because there the generic kernel starts at 45% of achievable
and here at 87%.

Inert on a stock install: the tiled kernel does not compile on the FlagTree that
backends.yaml pins, in any configuration, so the dispatch routes to the generic
kernel until a release bundles a working llc.
…d "cuda"

Four sites hardcoded device = "cuda". MACA aliases cuda so this is invisible on
MetaX, but on MUSA every case dies at the first allocation with
NotImplementedError: Could not run 'aten::empty.memory_format' with arguments
from the 'CUDA' backend -- before reaching the operator at all. Same family as
the fp8 capability gate: both silently exclude non-NVIDIA backends.

flaggems_vllm.device is what 60 of the 69 test files already use.
@cheersluvs cheersluvs changed the title [KMCompiler][MetaX][Hygon] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert, plus test and benchmark fixes [KMCompiler][MetaX][Hygon][Mthreads] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert, plus test and benchmark fixes Aug 15, 2026
S5000 reports capability (3, 1), so the NVIDIA-scale >= 89 threshold skipped this
operator's entire test file and benchmark there -- 108 skipped, which is how the
hardcoded device = "cuda" underneath went unnoticed for so long. tl.float8e4nv is
bit-identical to torch.float8_e4m3fn on that card.

With that and the device fix in place the suite is 92 passed / 16 skipped on
S5000 (the 16 are the vLLM cross-check; no vLLM is installed on that box).
… build

tl.float8e4nv cannot be compiled for PPU-ZW810E. The card reports capability
(8, 0) and its Triton target is ppu:80, and the fp8 policy is inherited from the
NVIDIA backend twice over: FlagTree's PPU compiler.py adds fp8e4nv only when
capability >= 89, and overriding that option to clear the frontend then fails
inside ConvertTritonGPUToLLVMPPU with 'Conversion from/to f8e4m3nv is only
supported on compute capability >= 89' -- an NVIDIA threshold enforced in a
PPU-specific pass.

The kernel body is the generic one; the seven unrolled quantisation groups call
_f32_to_e4m3_bits instead. Verified bit-identical to torch.float8_e4m3fn over
65536 values, half drawn from the subnormal range, which is where a naive
encoder goes wrong.
PPU-ZW810E reports capability (8, 0), so the NVIDIA-scale >= 89 threshold skipped
this operator's test file and benchmark there. With the integer E4M3 encoder in
_thead/fused/ the operator builds and is correct on that card: 92 passed,
16 skipped (the 16 are the vLLM cross-check; the vLLM installed there is 0.19.0,
which predates this operator).
@cheersluvs cheersluvs changed the title [KMCompiler][MetaX][Hygon][Mthreads] Token-tiled fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert, plus test and benchmark fixes [KMCompiler][MetaX][Hygon][Mthreads][T-Head] fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert backends, plus test and benchmark fixes Aug 17, 2026
…it instead

The reference and the assert_close comparison were evaluated in token slices so
that large shapes would fit in 64 GiB. That bounded the temporaries, but it also
changed what the oracle computes, and an oracle is the wrong place to economise:
if the whole-tensor path is correct and the kernel has a size-dependent fault, a
sliced reference agrees with it and hides exactly the bug worth finding.

The second justification for slicing was that torch returns wrong results past
~3e9 elements on one backend. That claim is not established -- five minimal
per-row reductions agree exactly whole versus sliced at that size, and the
original observation has not been narrowed to an operation. It should not be
load-bearing for a change to the verification logic.

_skip_if_reference_wont_fit stays: a shape that does not fit is now skipped
rather than evaluated differently. That still fixes the hardcoded H800 list it
replaced, which was a bare return reporting those cases as passed.
A shape that cannot be verified on a given card should say so, not disappear.
The guard replaced a hardcoded H800 exclusion list -- a bare return that reported
four cases as passed without running them -- and removing the guard does not
bring that back: those cases now run, and fail visibly if the card cannot hold
the reference.

Removes _free_device_memory, REF_BYTES_PER_ELEM and the gc import with it.
… build

tl.float8e4nv cannot be compiled for Ascend 910B4. Unlike T-Head there is no
capability gate to override: BiShengIR does not recognise the type at all, with
'hivm.hir.vcast op currently don't support cast float_to_UNKNOWN_rintmode' and
'unrecognized float type: f8E4M3FN'.

The kernel body is the generic one; the seven unrolled quantisation groups call
_f32_to_e4m3_bits instead. Verified bit-identical to torch.float8_e4m3fn on the
card at block widths 256 through 2048 (the operator uses 512).

The encoder keeps ~15 live intermediates, so it is bounded by the Unified
Buffer: a 4096-wide block asks for 240 KB against 192 KB and fails to compile.
Loud rather than silent, but it caps how far this kernel can be widened.
… a pointer cast

Ascend's Triton rejects the uint8* -> bfloat16* cast the generic kernel uses for
the cache's RoPE region: 'Casting pointers with unmatched bitwidth!'. The wrapper
now passes k_cache.view(torch.bfloat16) alongside the uint8 pointer and the kernel
indexes it in bf16 units. Every byte offset involved is even -- block stride
37376, 576 bytes per token, 448 NoPE bytes -- so halving them is exact.
…split/join

BiShengIR cannot turn [ROPE_DIM] into [HALF_ROPE_DIM, 2] here: 'cannot align 0
axis' on the expand_shape, and 'collapsing non-contiguous dims' on the way back.
The pairs are now loaded and stored directly at stride 2, which needs no shape
change at all. The arithmetic is unchanged.
The stride-2 pair access it introduced aborts the compiler: FlagTree's
InterleaveOptimization.cpp:113 recognises the `2*i` / `2*i+1` pattern, then
asserts that the addi constant is 1 -- which it is not, because `NOPE_DIM + 1`
folds to 449. The process dies on SIGABRT inside a TBE subprocess, so the
Python caller sees a success exit code and no traceback.

An isolated probe of six formulations found the same abort for the named-offset
form, for `oe | 1` (canonicalised back to addi), and for a runtime `+one`
argument (Triton specialises integer arguments whose value is 1). This restores
the generic RoPE block as the starting point; whether it survives in the full
kernel is the next question.
The pairs are loaded and stored as [HALF_ROPE_DIM, 2] directly, so no shape
change is needed anywhere. This is the only formulation of the three tried that
the toolchain accepts:

  - reshape [ROPE_DIM] -> [HALF_ROPE_DIM, 2] is rejected by BiShengIR here
    (`cannot align 0 axis`, `collapsing non-contiguous dims`);
  - 1-D stride-2 offsets abort the compiler in InterleaveOptimization.cpp,
    which asserts the addi constant is 1 while `NOPE_DIM + 1` folds to 449.

Also whitelist ascend for the fp8 tests. The card reports capability (0, 0), so
without it the whole file skips; the override never uses tl.float8e4nv, it
encodes E4M3 with integer arithmetic exactly as the T-Head override does.
The scale byte is a per-block scalar, and AICore's scalar unit has no
float-to-integer instruction, so `encoded_scale.to(tl.uint8)` failed instruction
selection in bisheng:

    fatal error: error in backend: Cannot select: i64 = fp_to_uint

wrapped in the NaN and lower-bound selects Triton emits for a saturating
conversion. bisheng exited 70, hivmc swallowed that and exited 0, and Triton
surfaced it as `No such file or directory: kernel.o` -- so the stage that
actually failed was not named anywhere in the error.

UE8M0 scales are powers of two, so the stored byte is exactly the biased
exponent of the scale, and both it and the scale itself can be built from the
exponent field with no float-to-integer step. Checked against the float formula
over 200k random magnitudes plus exact powers of two, one ULP either side of
them, the 1e-4 floor and the top of the bf16 range: identical scale and byte
everywhere.
A backend can carry the float8_e4m3fn dtype and still be unable to cast to it.
torch_npu raises `Float8_e4m3fn has not been supported`, which failed 84 cases
in this file on Ascend -- inside the torch reference, before the kernel under
test was ever compared against anything. The eight that passed were the Q-path
cases, which touch no FP8 at all.

The cast is elementwise and device-independent, so doing it on CPU when the
device refuses changes where it runs and nothing else. Everything else in the
reference, including the float-to-uint8 scale conversion, stays on device.

Note that `hasattr(torch, "float8_e4m3fn")` does not answer this question: the
dtype exists on that backend, only the conversion is missing.
torch.exp2 does not return an exact power of two on every backend. On Ascend it
is one ULP low for 492 of 512 integer arguments, and torch.pow(2.0, e) is no
better. The oracle divides by that value, so every quantised element came out a
ULP high, and the ones sitting exactly between two E4M3 codes rounded the other
way -- 16 of 448 bytes per token, each off by a single LSB, which is what the
FP8 region's rtol=0/atol=0 comparison caught.

Those ties are common rather than exotic here: the operator feeds the encoder
`kv / scale` with scale a power of two, so the values keep bfloat16's 8
significand bits while E4M3 has 4.

Measured before changing anything, since the failure direction was not obvious:
the kernel's scale and its stored byte are exact for all 512 magnitudes tried,
torch.exp2 on CPU is exact, torch.exp2 on the device is not. The kernel was the
correct side and the reference was wrong.

Assembling the float from its exponent field is exact by construction; verified
against 2**e for every exponent from -126 to 127. The operator can only reach
-22..120, so the clamp never engages.
The runtime refuses a wider launch:

    KernelLaunch failed because value 532480 for parameter coreDim is invalid.
    Expected value: less than or equal to 65535.

One program per (token, head) means 8192 tokens at 64 heads already asks for
eight times the cap, and the suite goes to 131072 tokens at 128 heads. Because
the launch is asynchronous the failure surfaced later, inside an unrelated
comparison, and left the device in a state that failed every subsequent case in
the process -- which is why all 32 cases of both multi-head tests failed while
the single-head KV tests passed.

Each chunk passes its own program-id offset. Verified the chunks cover the grid
exactly with no overlap for shapes from 65 to 16.9M programs.

This trades one launch for many at large shapes -- 259 of them at the largest --
which is a real cost to revisit once there are performance numbers. Correctness
first; the alternative, a grid-stride loop inside the kernel, puts the whole
body under a `for` with nested conditionals, and in-loop control flow is exactly
what breaks the UB allocator on this backend.
One unit per program is the generic shape, and on this backend it is the whole
runtime: 76 ns per unit, constant from 66K units to 4.2M and indifferent to the
byte count, which put the operator at 26.8 GB/s against a 1433 GB/s device-copy
ceiling. A unit moves about a kilobyte -- far too little to cover the fixed
per-program cost.

Measured on the card with an isolated copy, rows per program vs achieved
bandwidth: 1 -> 51 GB/s, 2 -> 104, 4 -> 203, 8 -> 288, 16 -> 290, 32 -> 299,
64 -> 299. The curve turns over at 8. num_warps is not a lever here; 1, 2 and 4
are within noise of each other.

The body is untouched. It moves into a `_process_one` device function, keeping
its early returns -- a `return` there leaves the unit, not the program -- so
nothing has to be restructured into nested conditionals, which is what breaks
the UB allocator on this backend.

Also cuts the launch count: 259 launches at the largest shape become 33.
Verified the launcher covers every unit exactly once for shapes from 65 to 16.9M
units.

Note for anyone comparing against the 1433 GB/s copy ceiling: a plain Triton
copy on this card tops out near 300 GB/s, so that ceiling is the hardware's, not
Triton's.
The tiled launcher does not compile: ttir_to_linalg aborts on SIGABRT with no
message at all. Measured one value per process, it is the shape and not the
size -- UNITS_PER_PROGRAM of 1, 2, 4 and 8 all abort identically, and 1 is the
same structure with the smallest possible body. Calling a device function that
contains early returns from inside a static_range loop is what the pass cannot
handle.

Restores the last state verified on the card: 52 passed / 8 skipped under
--quick, 81 passed with the full shape set. The operator is correct here and
unoptimised -- 26.8 GB/s against a 1229-1433 GB/s device copy ceiling, with a
plain Triton copy on this card topping out near 300 GB/s.
The operator ran at 26.8 GB/s here, constant across every shape and indifferent
to the byte count: 76 ns per (token, head) unit, and a unit moves about a
kilobyte -- far too little to cover this backend's per-program cost.

The Q path is 98% of those units (one in num_heads + 1 is the KV one) and is the
half with no gather, no quantisation and no cache write, so it is both where a
wider tile pays and where it is safe to take one. It moves into
q_norm_rope_kernel, which handles 16 units as a [16, 512] tile. Measured on the
card, ns per unit: 1 -> 590, 2 -> 64.8, 4 -> 48.7, 8 -> 31.1, 16 -> 24.9.

The KV path keeps the body that is already verified here, unchanged except for
one line: its programs now map to the KV unit of each token rather than to a
flat unit index.

Two things constrained how the new kernel could be written, both found by
measurement after the obvious forms aborted the compiler:

  - no loop, no device function, no early returns. A device function with early
    returns called from a static_range loop aborts ttir_to_linalg on SIGABRT
    with no message, for 1, 2, 4 and 8 units per program alike -- it is the
    shape, not the size. The tile has none of those constructs.
  - on the unstructured pointer path, taken here because position_id is a
    vector, an addptr result may have exactly one user
    (`Invalid: tt.addptr has multiple users`, BlockPtrAnalysis.cpp:2120). cos
    and sin get separate chains with offsets that cannot be merged.

Correctness: bit-identical to a torch reference computed on the device over
16.7M elements. Against the same reference computed on CPU, 63 elements differ,
each by a single bfloat16 ULP -- that is rsqrt, which disagrees between CPU and
device for 8086 of 32768 inputs, not the kernel.
A partial tile addresses past the end of q, and this backend faults on that
rather than honouring the mask: `aicpu exception`, runtime result 507018, which
surfaces at the next synchronize. The Q-path probe missed it because its shape,
512 tokens by 64 heads, divides exactly by the tile width; the suite's
test_q_path uses 8 heads and one token, so a 16-wide tile ran 8 lanes off the
end of the buffer.

Clamping the out-of-range lanes to the last row would be worse than the crash:
it makes several lanes share an address, and a masked store with duplicate lane
addresses is silently dropped on this card. That trades a fault for a wrong
answer.

So only whole tiles are issued, and the remaining units -- fewer than the tile
width -- go through the same kernel at one unit per program. Every lane is then
a real unit and every mask is trivially true. Checked over shapes from 1 unit to
1M that the two launches cover each unit exactly once with no lane out of range.
Swept against the shipped kernel at 4096 tokens by 64 heads, including shapes
that do not divide by the tile width and a check that nothing is written past
the end: 8 units per program gives 19.0 ns per unit, 16 gives 14.4, 32 gives
11.5. 64 does not run -- a [64, 512] float32 tile is 128 KB against a usable
Unified Buffer nearer 36 KB than the nominal 192 KB -- so 32 is both the widest
that fits and the fastest.
The previous comment inferred a UB overflow and guessed the numbers. Measured:

    ub overflow, requires 3215360 bits while 1572864 bits available

392.5 KB wanted against 192 KB. Worth writing down that this is three times
what the tile itself occupies -- [64, 512] in float32 is 128 KB -- because
several intermediates are live at once and multi-buffering asks for more again,
as the compiler's own message says. Sizing a tile by multiplying its dimensions
understates it badly here.
…fting

The subnormal path used three shifts by a per-lane amount, and on this backend
that was half the operator's KV time -- there is evidently no vector
variable-shift instruction, so it gets scalarised.

An E4M3 subnormal is m * 2^-9 for m in 1..7, so its mantissa is |x| * 512
rounded to nearest even. Adding 2^23 and subtracting it again forces exactly
that rounding, with no intrinsic and no shift.

Measured on the card, quantisation per token: 1.276 -> 0.627 us, which is the
same as deleting the subnormal path outright (0.632) and therefore all of what
that path was costing -- at no loss of accuracy. Bit-exact against the previous
encoder over 16384 x 448 values on device, and the replacement rounding checked
separately against the shifting form over 539 values including every exact tie.

How this was found is worth recording, because four targeted guesses missed
first: tiling the KV path (25%, flat by 8 tokens), arithmetic masks in place of
tl.where (nothing), multiply-by-reciprocal in place of the divide (nothing), and
per-program cost (it is compute). Subtraction with the store left on -- so dead
code elimination could not flatter the result -- put 92% of the quantisation in
the encoder, and deleting just the subnormal path found the rest.
Every head of a token shares one position, but the flat (token, head) tile made
`position_id` a vector, so each unit gathered its own 256 bytes of cos/sin --
the same bytes, once per head -- on the unstructured pointer path. Tiling H
heads of one token instead makes the position, and cos and sin, scalar: read
once per program and broadcast.

Measured at 4096 tokens, ns per (token, head) unit and the bandwidth implied:

              64 heads              128 heads
    flat      11.43 (179 GB/s)      16.81 (122 GB/s)
    H = 16     5.24 (391)            5.14 (398)
    H = 32     4.15 (493)            3.99 (513)

2.75x and 4.21x, bit-identical to the flat form.

This also settles something left unexplained: the flat kernel was markedly
slower at 128 heads than at 64, which had no obvious cause. It was this -- more
heads means more repetitions of the same gather. The two are now within 4% of
each other.

And it retires a figure quoted several times in these notes: a Triton copy on
this card reaching ~300 GB/s was taken as what Triton can do here. This kernel
does 493-513, so that number described the copy probe, not the platform.

H is the largest power of two up to 32 that divides num_heads, so every tile is
full: no masks, and nothing can address past the end of q, which this backend
faults on rather than masking. Checked that the launcher covers each unit
exactly once for head counts of 1, 5, 8, 48, 64 and 128.
@cheersluvs cheersluvs changed the title [KMCompiler][MetaX][Hygon][Mthreads][T-Head] fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert backends, plus test and benchmark fixes [KMCompiler][MetaX][Hygon][Mthreads][T-Head][Ascend] fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert backends, plus test and benchmark fixes Aug 20, 2026
…three dead branches

The KV path is 77% of the operator at 64 heads and 64% at 128, and almost all of
it is the quantisation. Two changes, together 1.96x on that path: 0.923 -> 0.470
us per token, bit-exact throughout.

**One [8, 64] tile instead of seven 64-wide groups.** The generic implementation
unrolls the groups, its comment citing load co-issue -- an NVIDIA
consideration. On this vector unit 64 lanes do not fill the machine and 448 do:
one load, one axis=1 reduction giving all seven scales, one encoder pass over
448 lanes. Worth 1.34x by itself. Triton wants a power-of-two block so the tile
is eight rows; the eighth covers the token's RoPE segment, which is real memory
rather than an overrun, and is discarded. Both stores mask it off with an upper
bound over an affine address -- the safe side of this backend's mask defect.

**Three branches that cannot fire.** Each is dead by construction, not by
appeal to the input distribution:

  - the clamp to +/-448: the scale is the smallest power of two with
    block_max / scale <= 448, and dividing by a power of two is exact here;
  - the encoder's saturation: with |x| <= 448, e_n reaches 15 only with
    mantissa 6, never the 15/7 NaN encoding;
  - the encoder's zero case: x == 0 takes the subnormal path, where |x| * 512
    rounds to zero anyway.

Removing them is worth 1.43x on its own. The encoder is no longer a general
E4M3 converter and its docstring now says so at the top.

Two techniques from the Ascend optimisation notes were tried here and did not
help, recorded so they are not tried again: capping the grid at the vector core
count (grid=40 measured 0.907 us/token against 0.909 for one program per token,
with every other cap worse), and replacing the divide with a multiply by an
exact reciprocal (within 1%). Both are sound advice for other operators; this
one is compute-bound in the encoder, so neither touches it.
…he test's

The benchmark file keeps its own copy of `_FP8E4NV_CAPABLE_VENDORS`, and adding
`ascend` to the test's left the two disagreeing. It is invisible today because
the benchmark's other gate, `VLLM_REF_AVAILABLE`, skips first on a card with no
vLLM — but the moment a baseline is present there the benchmark would skip for
the wrong reason, reporting a capability problem where there is none. The card
reports (0, 0) and never uses tl.float8e4nv anyway; it encodes E4M3 with integer
arithmetic, as T-Head does.
`torch.arange(num_tokens_insert)` lands on CPU, and three lines later it is
indexed with a device-side mask. That only executes when slot_mapping holds
SOME negative entries, and no test constructs that: the suite covers all-valid
slots and an all -1 case, and the all -1 case returns early at `num == 0`. So
the mixed case -- which is what DP padding actually looks like, some tokens
inserted and some skipped -- has never run, and it fails immediately when it
does:

    RuntimeError: Expected all tensors to be on the same device.

Found by driving the oracle directly from a probe with two slots set to -1.
Note this is not Ascend-specific; the same line would fail on any accelerator.
The kernel handles the case correctly, so only the reference was blocking it
from being tested.
A launch costs ~450 us on the 910B. The Q and KV kernels were issued
separately, which put a ~0.95 ms floor under every shape: at 1 token the
operator spent all of its time in dispatch, and below a few hundred tokens the
kernels finished long before the floor. One grid now covers both, Q tiles first
and one program per inserted token after, each program classifying itself from
its global id so a chunk boundary may fall anywhere.

Measured against the same code in two launches, output bit-identical either way
including the mixed-negative-slot case:

    1 token,     64 heads   0.990 -> 0.510 ms   1.94x
    64 tokens,   64 heads   0.971 -> 0.495      1.96x
    256 tokens,  64 heads   0.983 -> 0.510      1.93x
    1024 tokens, 128 heads  1.046 -> 1.028      1.02x
    2048 tokens,  64 heads  1.546 -> 1.522      1.02x

The gain stops once the work outgrows the dispatch, because dispatch and
execution overlap -- one launch removes only the part that could not be hidden.

Two things the merge required, both recorded in the kernel docstring:

  * the KV arm cannot use an early `return`. `if kv_slot < 0: return` inside the
    else aborts the compiler with an MLIR use-list assertion that never mentions
    control flow, at every tile width and every num_stages. The positive nested
    form compiles and is bit-exact. Plain early returns elsewhere are fine; what
    breaks is a return nested inside an arm of a two-armed if.
  * the two arms share no variable name, since Triton folds a name assigned in
    both arms into one SSA value and demands a single type at the join.

This also drops the dead per-unit Q branch the KV kernel still carried, and the
two bounds guards an exact grid makes unreachable. num_tokens and
num_tokens_insert are no longer constexpr, so shapes no longer force a recompile.
…ot two"

This reverts 4ec6ce4 while its regression is diagnosed. Measured on the 910B,
same suite and same box minutes apart: 5f165b4 gives 12 failed / 80 passed,
4ec6ce4 gives 24 failed / 68 passed.

The merged kernel is not computing the wrong thing. All 32 parameters of
test_backend_override_matches_reference pass when that function is run on its
own, and the probe that motivated the merge was bit-exact on every shape it
covered including the mixed-negative-slot case. The extra failures appear only
in a full-suite run, so they depend on what ran before -- memory state, or the
process poisoning that an async launch failure causes on this backend.

Reverted rather than fixed forward because the branch is a live PR and the
regression is confirmed, not suspected. 4ec6ce4 stays in history to cherry-pick
back once the cause is known.
…not two"

This reverts ca010cf. The regression it was reverted for is not one.

With the shapes whose torch oracle cannot fit on this card excluded -- 65536,
98304 and 131072 tokens, where the reference builds a 32 GiB float32 copy of a
16 GiB q -- the two versions are identical: 80 passed, 16 skipped, 0 failed,
both. The merged version runs that suite in 5.15s against 13.53s.

What the revert was based on: full-suite failure counts of 13 (two launches)
against 20 (one launch). Those extra failures are downstream of the oversized
cases exhausting the allocator, not caused by the operator. Three things now
say so. The operator's memory is byte-identical between the versions at every
shape measured -- inputs, peak, reserved and allocated after the tensors are
freed, with nothing left behind by either -- while the merged version issues
FEWER launches. --quick is 52 passed / 8 skipped on both. And every failure
seen was an OOM inside the oracle's `xf.pow(2).mean(...)`, with the operator not
on the stack.

The first comparison that produced the revert differed in Triton cache state as
well as in code, and the second attributed a real difference to a memory
mechanism that measurement then refuted.
@cheersluvs
cheersluvs force-pushed the deepseek-v4-quant-insert-metax-hygon branch from 908e98a to c685548 Compare August 24, 2026 00:52
cheersluvs pushed a commit to cheersluvs/FlagGems-vllm that referenced this pull request Aug 24, 2026
Carrier branch for moving benchmark and probe scripts to the 910B box, which
has no SSH but can reach GitHub. Deliberately not tagged [KMCompiler]: that
prefix marks PR flagos-ai#684 commits, and labelling this one that way would make a
branch that must never be merged look like PR work.

Nothing here is imported by the operator, the tests or the benchmark, and none
of it belongs on deepseek-v4-quant-insert-metax-hygon.
Upstream converted the vendor fused/__init__.py files from relative to absolute
imports; this branch had added a new op to them. Resolved by taking upstream's
style and re-adding the op's import and __all__ entry in it.
… files

The merge with main brought in tools/test_ci_absolute_imports.py, which requires
production operator imports to be package-qualified. _metax and _mthreads were
converted while resolving the merge conflict; _ascend, _hygon and _thead are new
files on this branch, so they never conflicted and kept their relative imports.
All three CI import checks pass now.
… vLLM abort collection

The tiled Moore Threads path is no longer blocked. flagtree 0.6.1+mthreads3.6
bundles the fixed llc at triton/backends/mthreads/bin/llc (md5
cec9ff66714e311670b9412ec760e4aa, byte-identical to the build the kernel was
validated against), so the docstring's claim that the file is inert on a stock
install is now false. It now names the version that carries it and keeps the
description of the older toolchain's instruction-selection defect as the reason
the version floor exists.

The vLLM import guard caught only ImportError and AttributeError, so a vLLM that
is installed but misconfigured raised through it and aborted collection of the
whole file rather than degrading to "no baseline": with two platform plugins
registered the import raises RuntimeError("Only one platform plugin can be
activated, but got: ['fl', 'musa']"), which is what happens on the MTT box.
@cheersluvs
cheersluvs force-pushed the deepseek-v4-quant-insert-metax-hygon branch from 135494c to a6bdd8e Compare August 25, 2026 01:07
…when it goes

The comment said "drop this wrapper once MetaX-MACA/mcoplib#59 is fixed". MetaX
has fixed it -- 0.4.9 drops the cudaLaunchKernelEx path that 0.4.6 calls with an
uninitialised cudaLaunchConfig_t -- and the wrapper still has to stay, so the
old note now points the reader at the wrong conclusion.

Two reasons, both verified rather than assumed:

  * No wheel with the fix is reachable. Every MetaX-MACA/mcoplib GitHub release
    carries zero assets, and the C550 image installed mcoplib from a local file
    (direct_url.json shows file:///mnt/wheel/..., a path that no longer exists),
    with no vendor index configured. Anyone running this still has 0.4.6.
  * 0.4.9 is a different operator. Upstream vLLM changed the schema at v0.22.0 --
    q read-only, a q_head_padded argument, the result returned rather than
    written in place -- and 0.4.9 follows it. This file targets v0.21.0, which
    matches the vLLM version the repo pins, so 0.4.9 would not be comparable
    even if a wheel existed.

The kernel is not the problem: rebuilt from MetaX's own 0.4.6 source with their
own 0.4.9 fix it runs on C550 at 96.3% of the copy ceiling, the same figure it
scored under the LD_PRELOAD shim. Only the published binary is unusable.
Removes src/flaggems_vllm/runtime/backend/_thead/fused/ entirely -- both the
override and the fused/__init__.py that exists only to export it. Both were
added by this PR (4fa1a09) and nothing else in the tree imports them:
_thead/__init__.py pulls in VendorDescriptor only, and backend/__init__.py
loads a vendor's `fused` module through _import_module_safe, so its absence is
the normal case -- ten of the sixteen vendor directories have no fused/ at all.

`thead` also comes out of _FP8E4NV_CAPABLE_VENDORS in both the test and the
benchmark, and that part is required rather than tidying. PPU-ZW810E reports
capability (8, 0), so without the whitelist entry `80 < 89` makes
is_support_fp8e4nv() false and the file skips cleanly. Leaving the entry while
removing the override would instead route PPU to the generic kernel, whose
`tl.float8e4nv` that card refuses to compile -- turning a clean skip into a
build failure.

Everything else under _thead/ predates this PR (__init__.py, tune_configs.yaml,
heuristics_config_utils.py from flagos-ai#36) and is untouched, as are device_finder.py's
vendor detection and tools/run_tests.py's env list.
cheersluvs added a commit to cheersluvs/FlagGems-vllm that referenced this pull request Aug 26, 2026
Replaces both test files with the FlagGems versions, which are the reference
suite for this operator and cover cases this repo had no equivalent for:

  prefill  radix_final_config_prefers_large_vocab  dispatch-tier boundary
           large_vocab_partial_nonzero_range       per-row differing [start,end)
           vs_vllm                                 this repo had none at all
           topk_greater_than_row_len               top_k=512 > vocab=257
  decode   topk_greater_than_row_len
           logits_diff_in_8LSBits                  vocab 262144, 256 distinct
                                                   values -> radix tie-breaking

Shapes also widen: num_rows to 16383 (was 2048), vocab 4095/8193 (odd, chosen
to straddle the radix-final threshold) and 262144, and decode gains a batch
dimension it previously did not exercise at all.

Re-applies on top the four fixes the FlagGems versions do not carry, so this is
an upgrade rather than a regression:

  1. call sites route through the top-level binding, not the ops submodule, so
     a vendor override is reachable at all (the PR flagos-ai#684 trap);
  2. device gate is torch_device_fn.is_available(), not torch.cuda, which is
     false on MTT/Ascend/T-Head;
  3. the vLLM import guard catches RuntimeError, so the MTT two-plugin
     collision cannot abort collection (a6bdd8e, for these files);
  4. the decode oracle is pinned to the torch reference instead of drifting to
     vLLM when importable. Since FlagGems' decode had no separate vLLM test,
     one is added, matching what FlagGems' own prefill already does.

_OVERRIDE_ACTIVE guard tests are carried over for both ops.

Known wart, ported faithfully rather than silently changed: decode zips
BATCH_SIZE_LIST (2) with VOCAB_SIZE_LIST (6) and TOP_K_LIST (5), so zip
truncates to 2 cases and vocab 262144 / top_k 1024 never run in non-quick mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cheersluvs added a commit to cheersluvs/FlagGems-vllm that referenced this pull request Aug 28, 2026
…ess defects

FlagGems' tests and benchmarks for this operator pair are the reference suite and
are strictly stronger than this repo's. Adopting them adds cases with no
equivalent here: topk_greater_than_row_len (top_k=512 against vocab=257),
logits_diff_in_8LSBits (vocab 262144 with only 256 distinct values, which hammers
the radix tie-breaking), large_vocab_partial_nonzero_range (per-row differing
[start,end)), radix_final_config_prefers_large_vocab, and a prefill-vs-vLLM
comparison this repo lacked entirely. Shapes widen to num_rows=16383, vocab
4095/8193/262144, and decode gains a batch dimension.

Four defects are fixed on top, each of which let a vendor run look green while
proving nothing:

1. Call sites imported the op from flaggems_vllm.ops, which binds the submodule's
   function object. The registrar rewrites the package globals and never touches
   that, so a vendor override could not be measured at all -- the same trap PR
   flagos-ai#684 fixed for DeepSeek-V4. Calls now go through the top-level binding, with
   the generic kept as _generic_impl for an _OVERRIDE_ACTIVE guard test that
   passes only when an override is genuinely registered.

2. The decode test and benchmark gated the whole file on tl.histogram having a
   mask parameter, but the decode op never calls tl.histogram -- it builds its
   histogram with tl.atomic_add, and s_histogram_ptr is just a buffer name. On
   any vendor Triton lacking that feature the entire decode suite skipped
   silently. The prefill test gated on torch.cuda.is_available(), false on
   several non-NVIDIA backends. Both now use
   runtime.torch_device_fn.is_available().

3. The decode oracle switched to vLLM's kernel whenever vLLM was importable, so a
   full-suite run and a single-file run could disagree about what "correct" means
   depending on collection order. Pinned to the torch reference; the vLLM
   comparison keeps its own skipif-gated test.

4. HAS_VLLM was set from the import alone. A non-CUDA build imports fine while
   exposing no top_k_per_row_* at all, so benchmarks reported a SpeedUp against a
   baseline that did not exist. Now checked with hasattr on the symbol after the
   import; dir() cannot be used, since torch.ops._C resolves lazily.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cheersluvs added a commit to cheersluvs/FlagGems-vllm that referenced this pull request Aug 28, 2026
…ess defects

FlagGems' tests and benchmarks for this operator pair are the reference suite and
are strictly stronger than this repo's. Adopting them adds cases with no
equivalent here: topk_greater_than_row_len (top_k=512 against vocab=257),
logits_diff_in_8LSBits (vocab 262144 with only 256 distinct values, which hammers
the radix tie-breaking), large_vocab_partial_nonzero_range (per-row differing
[start,end)), radix_final_config_prefers_large_vocab, and a prefill-vs-vLLM
comparison this repo lacked entirely. Shapes widen to num_rows=16383, vocab
4095/8193/262144, and decode gains a batch dimension.

Four defects are fixed on top, each of which let a vendor run look green while
proving nothing:

1. Call sites imported the op from flaggems_vllm.ops, which binds the submodule's
   function object. The registrar rewrites the package globals and never touches
   that, so a vendor override could not be measured at all -- the same trap PR
   flagos-ai#684 fixed for DeepSeek-V4. Calls now go through the top-level binding, with
   the generic kept as _generic_impl for an _OVERRIDE_ACTIVE guard test that
   passes only when an override is genuinely registered.

2. The decode test and benchmark gated the whole file on tl.histogram having a
   mask parameter, but the decode op never calls tl.histogram -- it builds its
   histogram with tl.atomic_add, and s_histogram_ptr is just a buffer name. On
   any vendor Triton lacking that feature the entire decode suite skipped
   silently. The prefill test gated on torch.cuda.is_available(), false on
   several non-NVIDIA backends. Both now use
   runtime.torch_device_fn.is_available().

3. The decode oracle switched to vLLM's kernel whenever vLLM was importable, so a
   full-suite run and a single-file run could disagree about what "correct" means
   depending on collection order. Pinned to the torch reference; the vLLM
   comparison keeps its own skipif-gated test.

4. HAS_VLLM was set from the import alone. A non-CUDA build imports fine while
   exposing no top_k_per_row_* at all, so benchmarks reported a SpeedUp against a
   baseline that did not exist. Now checked with hasattr on the symbol after the
   import; dir() cannot be used, since torch.ops._C resolves lazily.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants