Skip to content

[ASM] [HIP] Mha v4: LSE on sorted-sparse, kernel race fixes, gfx950 refresh - #6841

Open
jcaraban wants to merge 30 commits into
mainfrom
mha_v4_lse_sparse_etc
Open

jcaraban wants to merge 30 commits into
mainfrom
mha_v4_lse_sparse_etc

Conversation

@jcaraban

@jcaraban jcaraban commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ These quantized MHA kernels are mainly tested & intended for Diffusion Inference workloads.
However, mha_v4 gfx950 bf16 is virtually as accurate as mha_v3 at +100 TFlops 👌

Motivation

#5798 added the LSE epilogue to the dense gfx950 recipes; sorted-sparse still raised. This completes it, so block-sparse MHA v4 can also run under ring / context parallelism, which merges per-rank partials via LSE.

Re-validating all 22 gfx950 hd128 objects for the LSE epilogue surfaced a set of correctness bugs that are fixed here, and the same sweep carries a throughput refresh.

Technical Details

Kernels

  • LSE epilogue on the sorted-sparse path, where a LUT row that selects no KV tile exports -inf.
  • bugfix: the epilogue zero-denominator guard compared L as an integer, so -0.0 might have turn into -Inf
  • bugfix: bf16fp8 might have produced non-finite output on long sequences, leading to black images/videos.
  • bugfix: three potential races where an operand could be consumed before its LDS read landed.
  • bugfix: the Q-scale E8M0 exponent fold wrapped past zero below 2^-118 and could overflow
  • The fp6-P recipes raise the softmax overflow gate to P = 2^32, from the previous 2^8.8
  • SGPR reorganization across kernels to prevent the LSE descriptor from colliding with the block-sparse LUT state.

Host

  • Optional lse on fmha_v4_fwd_sparse, through the cu, header, pybind, custom op and both fakes.
    • One shared populate_lse_kernarg for dense and sparse, so _LSE_CAPABLE_QV is the only gate.
  • bugfix: the LSE K-smoothing correction contracted a per-KV-head mean against Q, so return_lse raised under GQA — on the dense path too, not just the newly enabled sparse one.
  • The exported LSE is the logsumexp of the quantized scores, not the exact ones.
    • Safe for same-recipe ring chunks; wrong as an absolute value or across recipes. Documented in both docstrings and the .md.
  • _plain_block_scale docstring: K's gather does overshoot the logical rows, and num_records is what clamps it.

Minor

  • 16 sparse-LSE tests, 18 GQA × LSE cases, and three rejection tests pinning the sorted-sparse host guards.
  • bench_sage.py adds peaked and tiebreak distributions; tiebreak's head mapping follows the GQA grouping.
  • Defer the MoE import in the sage quant wrappers; the moe_wgrad autotune was repeatedly invoked.
  • 22 gfx950 hd128 code objects refreshed (11 dense + 11 sparse), with no new files or dependencies.

Test Plan

  1. Run the complete MHA v4 suite on MI350/gfx950.
  2. Per-recipe dense and sparse validation against Torch; full-LUT sparse bit-exact vs dense.
  3. Compare the sparse LSE against torch.logsumexp over the selected blocks.
  4. Race probes under controlled HBM pressure, interleaved and order-rotated, plus frozen relaunches for nondeterminism.
  5. Interleaved A/B throughput, paired within run on one idle GPU.
  6. black --check and ruff on PR-touched Python files.

Test Result

  1. 361 passed, 1 skipped in test_mha_v4.py + test_mha_v4_sparse.py on MI350/gfx950.
  2. Dense 24/24 including ragged sk, sparse 70/70 including empty rows, discontinuous LUTs and GQA; the bounded V-scale deploy ran 120/120 and 350/350, the i8fp8 refresh 264/264 and 770/770.
  3. Sparse LSE vs torch.logsumexp: bf16 0.0009 nats, bf16fp8 0.03, fp8 0.05.
  4. Race probes collapse to 1 distinct output per recipe under pressure; 400 and 60 frozen relaunches at sq 8192 / 65536 with zero nondeterminism.
  5. At b1/hq5/sq65536/d128: i8fp8 2376 → 2534 TFLOPS, mxfp6 +3.40% dense / +3.87% sparse, f6f4 +2.09%, mxfp4 +1.38% / +0.40%, mxfp8 +0.38% / +0.61%.
  6. black --check and ruff on the six PR-touched Python files: passed.

Previous PRs: #5798, #5335, #5005, #4967, #4627

image

Three rejection tests for the preconditions the sparse ASM relies on to skip
apply_mask: ragged key length, and per-batch seqlens_k on the raw and packed
entry points. They match on "sorted-sparse" because _check_varlen_capable's
message also contains "per-batch key lengths", so a looser match passes with
the guard removed.

_plain_block_scale is safe for K not because the gather stays inside the
logical rows -- it does not, the K pipeline prefetches one tile past the end --
but because that gather goes through a buffer descriptor whose num_records
clamps the overshoot in hardware.
downcast_to_mxfp is used by sage_quant_mxfp4 alone, but importing it at module
scope runs aiter/ops/triton/moe/__init__.py, which eagerly builds the moe_wgrad
autotune decorator and its tuned-config lookup. Every MHA v4 caller paid that:
mha_v4 -> mha_v4_quant -> these wrappers, for five FP4 sizing constants that
have nothing to do with MoE.

Move the import into the one function that needs it. sage_quant_mxfp4 is
unchanged; the MoE namespace is now touched only when it actually runs.
Thread an optional lse tensor through fmha_v4_fwd_sparse (cu, header, pybind,
the custom op and both fakes), and share one populate_lse_kernarg between the
dense and sorted-sparse entries so the two cannot drift. _check_lse_capable
loses its `sparse` parameter, leaving _LSE_CAPABLE_QV as the single gate for
both paths.

Refresh the gfx950 fmha_v4 objects: every row now carries the LSE epilogue, and
a LUT row that selects no KV tile exports -inf instead of a finite value.

Measured against torch.logsumexp over the selected blocks: bf16 0.0009 nats,
bf16fp8 0.03, fp8 0.05. Adds 16 sparse-LSE tests covering a per-selection
reference and the empty-row case; 358 passed, 1 skipped.
Pure SGPR renumber across the eight quantized hd128 kernels. The softmax-skip
threshold pair now shares the block-sparse LUT base registers -- the two features
are mutually exclusive and asserted so -- which keeps the LSE descriptor clear of
the LUT state and leaves a 4-aligned scalar quad that no build toggle can claim.

No instruction is added, removed or reordered, so the objects are numerically
unchanged: outputs are bitwise identical to the previous ones on every shape
checked, 88 dense and 770 sparse validation cases pass, and the op tests report
358 passed, 1 skipped.
return_lse exports the logsumexp of the scores the kernel computed, not of the
exact-math scores. Error tracks the row max at each format's score precision:
~0.001 nats for BF16 Q/K, 36 for MXFP6, 153 for MXFP4, against 1418 nats of
range. Safe for merging same-recipe ring chunks, wrong as an absolute quantity
or across mixed recipes. Documented in both docstrings and the .md.

Also restore black/ruff cleanliness after a11f99f: three lse: Optional[Tensor]
annotations needed # noqa: UP045 (the union form graph-breaks Dynamo) and two
files needed reformatting.
Three distributions now span how much attention averages over V rows, which is
what decides whether V quantization error survives into the output. Measured at
b1/h8/s8192 with Q/K pinned MXFP6, as a fraction of V's own round-trip error:
diffusion 0.35x (n_eff 191), peaked 0.82x (n_eff 13), tiebreak 0.98x (n_eff 1.6).

`peaked` widens the logit spread onto ~10-20 effective tokens. Against 74 dumped
heads in that n_eff band it lands at percentile 51 on V amplification (0.82 vs
their median 0.81) - a typical head, not an optimistic one. It does not reach
the 43% of that band which amplifies above 1x.

`tiebreak` drives attention onto ~2 near-tied keys, so nothing averages and the
output inherits V's error whole. The tie gaps are spread geometrically across
the formats' mantissa resolution: with a single gap no format resolves the pair,
each breaks the tie by whichever way its rounding falls, and the resulting
ranking is arbitrary - MXFP8 Q/K measured 0.025 below FP8 Q/K at s=65536 but
0.009 above it at s=16384. With six spread gaps --kernel=all is monotone in
precision at s=65536, s=8192 and s=4096.

--compare-to-ref gets a wide 25% bound for tiebreak, which diverges from BF16 by
construction; it is a backstop against gross breakage, not a quality bar.
bf16fp8 could emit non-finite output on long sequences: a 75600-token head produced NaN on 18478 of
its rows, which surfaced end to end as a black video. The frozen-max rollback rebases by shifting
the scores exactly again, and the P pack divisor is now held in an SGPR for the life of the kernel.

Validated over 64 captured 75600-token attention heads: all rows finite, worst cosine 0.999409
against the bf16 kernel. Dense and sparse recipe checks pass, including the bit-exact sparse cases.

bf16 picks up the K-wait drain and softmax-tail scheduling from the same sweep.
Four V LDS slots with V loaded two tiles ahead, closing the cross-wave V
publication race: the consuming band read a direct-to-LDS payload one barrier
generation before the producing band's own vmcnt(0) drained it. Three slots are
not enough -- the refill then targets the buffer the PV phase read earlier in
the same matrix phase.

Race under controlled HBM pressure, interleaved with the previous objects and
order rotated: f8f6 50-62 -> 1 distinct output, mxfp6 50-59 -> 1, f6f4 54-64 ->
1, mxfp4 49-61 -> 1. Dense 24/24 ragged sweep and sparse 70/70 incl. bitexact
for every recipe.

b1/hq5/sq65536/d128 on an idle board: f8f6 -0.50%, mxfp6 -0.24%, f6f4 +0.04%,
mxfp4 +0.84%. Occupancy is unchanged (VGPR-bound); only LDS grows, by one V slot.
Three V LDS slots and a retuned pad. Race probe states=1 at sk 384/512/576
under pressure; dense 264/264, sparse 770/770; quality cosines unchanged.
2379.0 vs 2378.7 TFLOPS at hq=40/sq=65536.
The Q-scale E8M0 exponent fold now guards against underflow: a Q block below
2^-118 previously wrapped past zero and clamped to the MAXIMUM exponent instead
of the minimum. Alignment pads retuned for the resulting prologue size.

mxfp6 sparse +1.2%; the other three objects hold their previous throughput.
Dense 24/24, sparse 70/70 on both recipes.
Score conversion leaves the VALU: the i32 QK accumulator is seeded with the bit
pattern of 1.5*2^23, so 64 v_cvt_f32_i32 per tile become two bf16 MFMAs
(+5.07%). Both L matmuls issue at the PV front interleaved with the V LDS reads
(+0.68%), and the alignment pad is retuned (+0.74%).

2376 -> 2534 TFLOPS. Dense 24/24, sparse 70/70, bitwise-stable over 256
launches.
Fixes two s_waitcnt lgkmcnt races where a K operand could be consumed before
its LDS read had landed; output was bit-identical either way, so the window
was closed by instruction timing rather than by construction.

Enables the dynamic P pack gain, hoists the gain compare into the freeze
gate's branch hazard shadow, and reclaims a dead VGPR pair (248 -> 246).

Dense 24/24 and sparse 70/70 vs the torch reference; +0.21% at
b1/hq40/sq65536/d128.
GPU7 paired +2.09% (20/20); dense 24/24, sparse 70/70.
GPU7 paired dense +1.10%, sparse +0.27%; dense 24/24, sparse 70/70.
gemm_QK skipped the lgkm wait for a PV-prefetched K subtile 0 on the
assumption that "resident" implied landed. Nothing drains those reads
between gemm_PV issuing them and the first QK MFMA consuming them, so the
subtile and its descale were read racing. Emit the wait; the running budget
is already exactly the queued reads.

The VGPR file is reordered into QK sources -> PV sources -> matrix
destinations so that no bank is both a matrix source and a matrix
destination: the softmax denominator MFMA now owns its own accumulator
instead of borrowing K bank 0, which lets the next tile's subtile 0
prefetch straight into bank 0 and retires both the separate prefetch bank
and the phantom lgkm credit it needed. The fp6 block scales also move out
of the shared scratch range, where the QK bias split clobbered them
immediately after the pack. 238 VGPRs, still 2 waves/SIMD.

Dense and sparse validated 24/24 and 70/70, race-free, output digests
bit-identical to the previous objects. Dense is at parity on
b1/hq5/sq65536/d128; sparse is ~2% behind, accepted for the race fix.
…n the fp6-P recipes

Rebuilt across all nine hd128 recipes (dense and sparse).

1) The epilogue guard that substitutes 0 for a zero denominator compared L
   as an integer, so it matched only the bit pattern +0.0 and let -0.0 reach
   v_rcp_f32, yielding -Inf instead of 0. Now a float compare, which matches
   both zeros. Affects f4f4, f6f4, f6f8, f8f6, fp8, i8fp8, mxfp4, mxfp6 and
   mxfp8; bf16 and bf16fp8 already did this correctly. A tiny-but-nonzero L
   still reaches the reciprocal, which is intended.

2) f6f4, mxfp4 and mxfp6 pack P as fp6 with a per-K-block E8M0 scale, so the
   pack re-centres on each block's own peak and the old fp8 e4m3 ceiling no
   longer binds their softmax overflow-gate threshold. Raised to P = 2^32,
   matching f8f6. The recipes that pack P as fp8 are unchanged.

3) f4f4 also drops a dead per-row bias left over from before the exp-fold;
   with the running max frozen the QK prefill already biases the accumulator.
   This is the only size change here, -32 bytes.

Every object: emitted assembly differs only in the intended instructions,
24/24 dense and 70/70 sparse correctness cases.
Both recipes now stage P through the pknorm conversion before reducing
it, so the softmax block max, the overflow gate and the rare rollback
all run on packed 16-bit patterns instead of f32 coordinates. The
conversion is monotone, so the reduction yields the same block max as
before; mxfp4 additionally uses the 3-input f16 max and parks P = 1 an
octave band lower to keep that form well inside its valid range.

f6f4 also moves its V priming reads and the V-slot m0 advance onto the
QK tail MFMA shadows, holds the V-scale lane term in a resident
register, and retunes its code-placement padding for both arms.

Measured on gfx950 at b1 hq5 sq65536: mxfp4 and f6f4 both gain several
percent over the previous objects. Register and LDS footprints are
unchanged (mxfp4 240/90, f6f4 252 dense and 256 sparse).

op_tests/test_mha_v4.py and test_mha_v4_sparse.py pass for both recipes.
Dense +3.40% and sparse +3.87% on b1 hq5 sq65536 d128, GPU 7, within-run paired
over 15 and 12 rounds. The gain comes from moving the V primes and the m0 slot
advance onto the QK tail shadows, hoisting the V-scale lane term into a resident
VGPR, correcting the PV lgkm credits for the next-tile K0/scale prefetch, and
retuning the code-alignment pad for both arms.

Resources unchanged: dense 252 VGPR / 90 SGPR / 75,776 B LDS; sparse 256 / 98 /
108,552 B.

Validated: test_mha_v4.py and test_mha_v4_sparse.py 361 passed / 1 skipped; the
dense KV sweep 24/24 including ragged lengths; sparse 70/70 covering empty rows,
discontinuous LUTs and GQA; a static lgkm race check clean on both arms; 400 and
60 frozen relaunches at sq 8192 and 65536 with zero nondeterminism; and the
sparse empty-row case over 10 fresh processes.

Output is bit-identical to the previous objects on synthetic shapes and on 288
of 292 real captured heads. The 4 that differ are massive-activation heads where
the softmax rollback fires; they move by ~1e-7 in cosine (0.9993 against a 0.97
floor), from the rollback now rounding its octave shift in the integer domain.
Reschedules the V-slot m0 rotation onto the QK MFMA shadows and retunes the
hot-loop alignment pad. Output is bit-identical on 6 shapes incl. 2 ragged.

b1 hq5 sq65536 d128 on MI350, paired across 6 GPUs: dense +1.38%, sparse +0.40%.

Validation: 361 passed / 1 skipped (test_mha_v4 + test_mha_v4_sparse), 24/24
dense sweep incl. ragged sk, 70/70 sparse, 0/400 and 0/60 mismatching frozen
relaunches at sq 8192 and 65536, 10/10 sparse empty-row rounds in fresh
processes, and the static LDS race check clean on both arms.
+0.38% dense / +0.61% sparse at b=1 hq=5 sq=65536 d=128, measured across the
full code-placement sweep rather than a single layout.

24/24 dense and 70/70 sparse validation, race-clean, 361 mha_v4 tests pass.
Quality over 288 captured heads: 274 metric triples unchanged, the rest within
rel MAE +0.30% and cosine 2.3e-5, no non-finite outputs.
The line passes on main and was introduced by this branch, so CI's black job
would have failed on it.

Signed-off-by: jcaraban <jcaraban@amd.com>
@jcaraban
jcaraban requested review from a team, juuso-oskari and nsakkine October 22, 2026 18:49
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:gfx1250-ffm-triton Run the five-shard gfx1250 FFM Triton test suite
ci:triton-300x Run an additional Triton test job on MI300X in PRs (added automatically when gfx942 configs change); main branch always runs both MI35X and MI300X
ci:triton-355 Run the full Triton test suite on MI35X, not only the tests the change affects
ci:windows Windows JIT op smoke test on the gfx1201 ROCm runner
ci:multi-gpu Aiter multi-GPU tests on the 8-GPU runner
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: DeepSeek-V4-Pro, MiniMax-M3-MXFP4
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 6841 --add-label <label>

One backend per PR:
A PR changes one kernel backend: [Triton/Gluon] (Triton and Gluon count as one), [HIP], [ASM], [CK], [OPUS] or [FlyDSL]. If the title ends up with two backend tags, split the PR -- as stacked pull requests when one part cannot merge without the other.

PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to stop the title rewrites; labels stay in sync either way.

@github-actions github-actions Bot changed the title Mha v4: LSE on sorted-sparse, kernel race fixes, gfx950 refresh [Triton/Gluon] [ASM] [HIP] Mha v4: LSE on sorted-sparse, kernel race fixes, gfx950 refresh Oct 22, 2026
@github-actions github-actions Bot added the ASM label Oct 22, 2026

@azaidy azaidy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Different backends need to be different PRs @jcaraban

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Sparse GQA with LSE can fail at runtime, and architecture-dependent tests and benchmark head mapping need correction.

4 open issues
What changed in this PR

This PR extends the dense LSE support from #5798 to sorted-sparse MHA v4 on gfx950, enabling partial attention results to be merged for ring and context parallelism.

Changes:

  • Threads optional LSE output through Python, custom-op, and C++ sparse launch interfaces.
  • Adds sparse LSE and rejection tests, plus peaked and tied-key benchmark inputs.
  • Defers a MoE import and clarifies LSE semantics and scale-buffer documentation.
File Description
op_tests/​test_mha_v4.py Updates the unsupported-LSE rejection case.
op_tests/​test_mha_v4_sparse.py Adds sparse LSE checks and host-guard tests.
op_tests/​op_benchmarks/​triton/​bench_sage.py Adds peaked and tied-key input distributions.
csrc/​pybind/​mha_v4_fwd_pybind.cu Exposes optional sparse LSE output.
csrc/​py_itfs_cu/​asm_mha_v4_fwd.cu Shares LSE validation and populates sparse launch arguments.
csrc/​include/​torch/​mha_v4_fwd.h Adds the optional sparse LSE parameter.
aiter/​ops/​triton/​quant/​sage_attention_quant_wrappers.py Defers the MoE quantizer import.
aiter/​ops/​mha_v4.py Enables sparse LSE and updates custom-op mutation declarations.
aiter/​ops/​mha_v4.md Documents quantized-score LSE semantics.
aiter/​ops/​mha_v4_quant.py Clarifies scale-buffer bounds in a docstring.

Review effort: Balanced

Comment thread aiter/ops/mha_v4.py
Comment thread op_tests/op_benchmarks/triton/bench_sage.py Outdated
Comment thread op_tests/test_mha_v4.py Outdated
Comment thread op_tests/test_mha_v4_sparse.py Outdated
@jcaraban

Copy link
Copy Markdown
Contributor Author

Different backends need to be different PRs @jcaraban

@azaidy by different backends you mean mixing Triton into the PR? the issue is that these ASM Sage kernels are benched with the same Triton bench as the Triton Sage kernels. Are you simply asking that the changes to aiter/ops/triton/quant/sage_attention_quant_wrappers.py and op_tests/op_benchmarks/triton/bench_sage.py move to another PR?

…hitecture

Review findings on the LSE work, plus two of the same kind found alongside them.

_restore_k_mean_in_lse contracted a per-KV-head mean against Q, which is a shape
error under GQA. Every quantized recipe derives a k_mean, so return_lse with
hq != hk raised `einsum(): subscript h has size ...` on the dense path too --
this predates the sorted-sparse work and was simply never covered. Expand the
mean over the query-head group first, the way the kernel maps query head j to KV
head j // ratio. New dense and sparse cases over six and three recipes at ratios
2 and 4 check the result against a reference built the same way, so a wrong
expansion fails numerically rather than merely not crashing.

bench_sage's tiebreak distribution mapped query directions with `arange(hq) % hk`
and so disagreed with GQA's own grouping for any ratio above 1: two of four heads
pointed at keys they never read. Matches test_mha_common's expansion now.

Three guard tests asserted on a message that only the gfx950 ordering produces:

- The LSE rejection reaches the format check only on gfx950; elsewhere the
  architecture gate fires first. Pick the expected message from the architecture.
- test_mha_v4_rejects_unusable_seqlens_k wants the dtype and shape errors, which
  sit behind the same gate. Restrict it to gfx950; its sorted-sparse assertion is
  already covered arch-independently in test_mha_v4_sparse.py.
- Two sparse guard tests built a mask from mha_v4_kv_tile() outside the raises
  block, so the manifest read, not the guard, would fail on an architecture with
  no sparse row. One takes a literal shape, which is enough because that guard
  runs before the LUT is built; the other reaches _block_mask_to_lut, which reads
  the manifest itself, so it takes the arch gate its siblings already have.

Signed-off-by: jcaraban <jcaraban@amd.com>
One kernel backend per PR: these two files carry the only Triton-tagged changes
here and neither half needs the other, so they go to a parallel PR off main
rather than a stacked one. The wrapper change only moves an import inside a
function, so the API this branch consumes is unchanged.

Signed-off-by: jcaraban <jcaraban@amd.com>
@github-actions github-actions Bot changed the title [Triton/Gluon] [ASM] [HIP] Mha v4: LSE on sorted-sparse, kernel race fixes, gfx950 refresh [ASM] [HIP] Mha v4: LSE on sorted-sparse, kernel race fixes, gfx950 refresh Oct 23, 2026
@jcaraban

Copy link
Copy Markdown
Contributor Author

@azaidy done the Triton split to #6869 ; the ASM / HIP split unfortunately seems impossible because the HIP quantization / rotation / packing changes need to land along the ASM changes, else the contract breaks and those kernels no longer work in upstream.

mha_v4_kv_tile() is @functools.cache'd and derives from get_gfx(), and nothing
clears it. A test that monkeypatches get_gfx and touches the tile leaves the
cache holding the other architecture's value: monkeypatch restores the
attribute, not the cache, so every later test in the process reads the wrong KV
tile -- including test_mha_v4_sparse.py, which pytest runs next in the same
process. That is silently wrong mask geometry rather than an error.

Not live today: the only get_gfx patch stays on the dense path and never warms
the tile.

Signed-off-by: jcaraban <jcaraban@amd.com>
@jcaraban
jcaraban requested a review from azaidy October 23, 2026 13:38
The ASM Q-scale gather used to be an unguarded global load covering a whole
256-row query tile, so a partial final tile addressed rows past the sequence and
the producers reserved zeroed slack to keep those reads mapped. The gfx950
objects this branch ships promote q_descale to a buffer descriptor, so num_records
clamps the overshoot in hardware and the slack buys nothing. Q scales now
allocate at their logical extent like K's, which also makes the three MXFP*_q
producers agree with their own register_fake instead of over-allocating relative
to it, and removes the second copy of 256 that lived across the language
boundary.

Nothing numeric can police this. The overshot rows are past seqlen_q, so their
values never reach the output: poisoning the slack with a large E8M0 exponent
leaves every recipe bit-identical, on the unbounded objects as much as the
bounded ones. The symptom of a regression is a fault at an unrelated later
synchronization, and only when the over-read crosses an unmapped page. So the
guard is now the object itself -- a disassembly check that no DISPATCHABLE
fmha_v4 object fetches a scale with global_load_dword/ubyte. It is driven off
the manifest, not the directory, because fwd_hd128_f4f4*.co still ship unguarded
and are commented out there; re-enabling a row without rebuilding it fails the
test. Verified to fail against main's objects (12 offenders) and pass against
these.

K keeps its padding, its slack constants and its C++ check: its two-tile
producer lead is unchanged, as are the FP4 K/V gathers.

366 passed, 1 skipped (was 379: -15 Q backing-storage cases, +2 arch arms), and
all five MX-Q recipes stay finite and fault-free at seqlen_q 1/100/255/257/513.

Signed-off-by: jcaraban <jcaraban@amd.com>
f4f4 carries the same F4 Q/K/V triple as mxfp4, and mxfp4 took over its v_pack=1
FP6-P V order, so the two resolve to one row and mxfp4 wins it. Its manifest
entries have been commented out for a while; this drops the dead entries and the
two objects they named, which were the only ones still shipping an unguarded
scale gather. Every manifest row now resolves to a shipped object and no object
is unreferenced.

Two tests said f4f4 but dispatched MXFP4/MXFP4, which is mxfp4 for as long as
f4f4 has no manifest row; relabelled so the name matches the object under test.
The gather invariant keeps reading the manifest rather than the directory --
that now guards a commented row being re-enabled without a rebuild, instead of
the f4f4 objects specifically.

The Triton-side f4f4 helpers (sage_quant_v_f4f4, _f4f4_v_kperm, the mha4_f4f4
bench spec) are left alone here; they belong to the bench_sage change set.

366 passed, 1 skipped.

Signed-off-by: jcaraban <jcaraban@amd.com>
The FP6-P V order is consumed by mxfp4 and f6f4 now; f4f4 is deprecated and its
row no longer exists. Last place in the tree that still implied otherwise.

Signed-off-by: jcaraban <jcaraban@amd.com>
@azaidy

azaidy commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@azaidy done the Triton split to #6869 ; the ASM / HIP split unfortunately seems impossible because the HIP quantization / rotation / packing changes need to land along the ASM changes, else the contract breaks and those kernels no longer work in upstream.

Thanks @jcaraban

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants