Skip to content

[FlyDSL] MiniMax-M3 index score: MXFP4 index-cache decode scorer - #7060

Merged
valarLip merged 11 commits into
mainfrom
ganyi/m3-index-score-decode-fp4
Nov 2, 2026
Merged

valarLip merged 11 commits into
mainfrom
ganyi/m3-index-score-decode-fp4

Conversation

@ganyi1996ppo

@ganyi1996ppo ganyi1996ppo commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a MiniMax-M3 index-score decode kernel that reads an MXFP4 index K cache (e2m1 data plus one e8m0 scale per 32 elements), taking a bf16 or an e4m3 query. M3.1 can keep its index cache in MXFP4, which halves the index-cache bytes per token (and so raises the KV pool capacity); this kernel is the decode reading side. The fp8 index-score builder gains an mxfp4 specialisation; the generated code (LLVM IR and ISA) of the existing fp8 kernels is unchanged.

Files: aiter/ops/flydsl/kernels/minimax_m3_index_score.py (1474 to 1928 lines), op_tests/test_flydsl_minimax_m3_index_score.py (2169 to 2605 lines, the MXFP4 tests added), and two lazy exports in aiter/ops/flydsl/__init__.py: minimax_m3_index_score_fp4_flydsl (score_flydsl_fp4) and minimax_m3_index_score_fp4_flydsl_supported (index_score_supported_fp4). No new files, no C++.

Split from #7058, the earlier combined version that this PR and its prefill sibling #7061 replace (whether #7058 gets closed is pending the author's decision).

Format

  • MXFP4: e2m1 data, one e8m0 scale per 32 elements.
  • Page of 128 tokens = [128 x 64 B data][128 x 4 B scale] = 8704 B. The kernels take two views, k_data [pages,128,64] and k_scale [pages,128,4], each with its own page stride, so the layout is the caller's choice (separate tensors or the single-region page above both work; a test checks the two agree bitwise).
  • e8m0 bytes 0 and 255 are outside the contract. e2m1 x 2^e is exact in e4m3 for biased e8m0 in [119, 133], so widening an MXFP4 cache to the unit-scale fp8 index cache (the baseline in bench_score_mxfp4) saturates outside that window. The MXFP4 scorers apply the scale themselves. Values outside the window were not measured on real tensors; the tests cover wide scale ranges against the oracle.

Kernel

score_flydsl_fp4 uses the same work map, geometry, Q loading, causal cutoff and score store as the fp8 scorer: MXFP4 is a compile-time specialisation (mxfp4, plus q_fp8 for an e4m3 Q) of build_index_score, and only the K data+scale loads and the dequant/MFMA step are MXFP4-specific. The kernel signature differs (two cache views), so the shared body is a closure called from two thin kernels. Generated LLVM IR and ISA of the fp8 kernels are unchanged, and the MXFP4 kernels match the earlier separate MXFP4 kernel (checked at the fold, outputs bit-equal; unchanged since: 31 specialisations, 13 fp8 and 18 MXFP4, byte-identical before and after the last cleanup). The MXFP4 entry points take no q_scale and no CP shard (no caller needs them); score_flydsl and index_score_supported gain an optional k_scale that selects the MXFP4 cache.

  • bf16 Q: K is expanded to bf16 with v_cvt_scalef32_pk_bf16_fp4 and fed to the bf16 16x16x32 MFMA, so the numerics contract (K lifted to Q's precision) is unchanged.
  • e4m3 Q: one v_mfma_scale_f32_16x16x128_f8f6f4 per 16-token tile, A = fp4 K with its e8m0 byte, B = Q as e4m3; products are exact in fp32.
  • K tiles in flight = 6 (the fp8 scorer uses 3; an MXFP4 tile is half the bytes). In a 2/3/4/6/8 sweep, 6 was 15-35% faster than 3 on small batch x context cells.

Performance

Measured on gfx950 (MI355X), one GPU, FlyDSL 0.4.0, image rocm/atom-dev:nightly_202610261558. Method: CUDA-graph replay of K calls (K = 2..16 decode) over disjoint regions of a 300k-page pool to defeat the MALL, median of 25 replays, same card and process, every cell run in two rounds with the arm order reversed (the table reports the mean of the two rounds). Random K data, e8m0 in [122,127], uniform context per batch, no padding bound beyond the context. Times are microseconds per call. The fp8 baseline is the existing fp8 kernel with a bf16 Q. Ratios below 1 mean MXFP4 is faster.

S = queries per request, H = index heads, B = batch. Range over the 9 cells below: bf16 Q / fp8 = 0.48-0.65, e4m3 Q / fp8 = 0.47-0.59. Kernel-level numbers only. The table was taken with the earlier separate MXFP4 kernel; the folded kernel has identical ISA, and a base-vs-folded re-run of the same protocol (A-B-B-A) was within noise (geomean 1.003 fp8, 1.008 MXFP4 bf16 Q, 1.006 e4m3 Q, worst cell 1.021, run-to-run spread up to 3%). Cells measured with a CP4 shard were dropped with the CP kwarg.

Decode table (9 cells)
shape B ctx fp8 (us) MXFP4 bf16 Q (us) MXFP4 e4m3 Q (us) bf16 Q / fp8 e4m3 Q / fp8
S4 H1 8 32768 9.65 6.24 5.71 0.65 0.59
S4 H1 8 100000 24.09 13.48 12.29 0.56 0.51
S4 H1 8 131072 26.72 14.59 13.47 0.55 0.50
S4 H1 16 32768 15.25 8.88 8.39 0.58 0.55
S4 H1 16 100000 40.33 20.88 19.99 0.52 0.50
S4 H1 16 131072 48.79 24.83 23.55 0.51 0.48
S4 H1 32 32768 26.65 14.38 13.52 0.54 0.51
S4 H1 32 100000 78.49 38.35 36.67 0.49 0.47
S4 H1 32 131072 93.79 45.37 44.20 0.48 0.47

Tests

python op_tests/test_flydsl_minimax_m3_index_score.py                     # 90 MXFP4 correctness cases (PASS/FAIL table, exit 1 on any failure), then the decode, MXFP4 and prefill perf tables
pytest op_tests/test_flydsl_minimax_m3_index_score.py                     # main's existing pytest suite, unchanged: 269 passed, 4 skipped

Run on one MI355X (gfx950), image rocm/atom-dev:nightly_202610261558, with this branch's aiter/ops/flydsl/ and op_tests/ overlaid on the image's aiter (41d4d8d), because main's full Python tree does not import against the image's csrc. The pre-existing fp8 tests in that file are unchanged and give 269 passed, 4 skipped (as before).

The tests are standalone: the cache is built with aiter's per_1x32_f4_quant and dequantized with fp4_utils, with no dependency on any writer kernel. Scores are judged against the fp8 test's own oracle. They cover decode geometries, CUDA-graph buffers, split vs single-region views, alignment and rejection rules (including a non-positive or NaN sm_scale), and e8m0 extremes.

Mutation check: swapping the decode nibble order failed 60 tests when measured on the earlier combined branch; not re-run after the split or the later cleanups.

Known limits / not measured

  • Only gfx950 was tested.
  • Kernel numerics on real M3 tensors were not measured; the tests use synthetic caches quantized with aiter's quantizer.
  • e8m0 values outside [119, 133] are the saturating range of the fp8-cache widening used as the baseline; they have no real-data measurement.
  • End-to-end numbers are not part of this PR; they belong to the ATOM integration PR.
  • black 26.10.1 and ruff 0.16.0 are clean on the two changed Python files at this head; the whole-repo ruff check . / black --check . was not re-run at this head.

PR Self-Review

Reviewed revision

827441b

Behavior and intent

Adds score_flydsl_fp4 / index_score_supported_fp4 (and an optional k_scale on score_flydsl / index_score_supported): the MiniMax-M3 decode block scorer reading an MXFP4 index K cache (e2m1 data plus one e8m0 byte per 32 elements, 68 B per token against 128 B for fp8), with a bf16 or an e4m3 query. The existing decode scorer takes only an fp8 index cache, so an MXFP4 cache could not be scored without first expanding it to fp8; M3.1 can keep the cache in MXFP4, which halves its bytes; the decode kernel takes 0.47-0.65 of the fp8 kernel's time in the table above (measured at an earlier head). Behaviour of the fp8 path is unchanged.

Validation and unverified scope

Measured at this head (827441b), on one MI355X (gfx950), image rocm/atom-dev:nightly_202610261558 with this branch's aiter/ops/flydsl/ and op_tests/ overlaid on the image's aiter (see Tests), FlyDSL cache disabled and the aiter JIT cache removed:

  • python op_tests/test_flydsl_minimax_m3_index_score.py run from the repo root and again from /tmp: both exit 0, 90 of 90 MXFP4 correctness cases PASS, and the decode, MXFP4 and prefill perf tables are printed.
  • LLVM IR and final ISA, before vs after this head's commit, for 8 fp8 kernels (4 decode, 4 prefill compiled from the same tree) and 8 MXFP4 decode kernels (4 shapes, bf16 and e4m3 Q): all byte-identical.
  • black 26.10.1 and ruff 0.16.0 are clean on the two changed Python files.
    Unverified scope: other GPUs than gfx950; real M3 tensors; end-to-end use (ATOM integration PR); the pytest run of main's existing fp8 suite (269 passed, 4 skipped, quoted above) was done at an earlier head and not repeated at this one; the performance table is from an earlier head; the mutation check quoted under Tests was not re-run; CI results at this head.

PR-specific question

The self-review bot lists only the four generic questions on this PR, no PR-specific one. Its question 3, what each changed op test would fail on before this PR and why existing canonical coverage does not already provide it: before this PR score_flydsl_fp4 and index_score_supported_fp4 do not exist, so every MXFP4 case fails with an attribute error. The pre-existing pytest cases in this file cover fp8 caches only (they build no MXFP4 cache, never pass k_scale) and are unchanged.

Changed op tests

op_tests/test_flydsl_minimax_m3_index_score.py (the only op test changed): the existing fp8 pytest cases are untouched; this PR adds an MXFP4 section driven from main(), 90 cases, each of which would fail as follows:

  • matches_oracle_* and every_geometry_*: scores against an independent oracle on the dequantised cache, over the decode matrix and every geometry the selector reaches, bf16 and e4m3 Q. Catches a wrong nibble order, k-axis map or scale byte.
  • wide_scale_range_*: scale bytes across 2^+-20, so each lane group must pick its own byte.
  • graph_buffers_*: preallocated score buffers and work map captured once in a CUDA graph and replayed with different lengths and block tables.
  • split_and_single_region_views_agree_bitwise, e4m3_q_rows_must_be_16_byte_aligned, invalid_cache_rejected_*, metadata_rejects_cpu, supported_accepts_valid_inputs: layout and rejection contract (dtypes, shapes, strides, overlap, misalignment).
  • sm_scale_must_be_positive_finite (added at this head): sm_scale of -1, 0 and NaN must raise a ValueError, because the epilogue takes the page max before multiplying by the scale and a non-positive scale would pick the wrong block silently. The existing test_scale_fp32_range covers only 1e-50 and 1e39.
  • default_score_buffer_reads_as_minus_inf: with the default (kernel-allocated) score buffer, slots past a request's end read as -inf and the rest are finite.
  • bench_score_mxfp4: the perf sweep (us, TFLOPS, TB/s for MXFP4 against the fp8 scorer), each result compared with the oracle.
    These are not redundant with the fp8 cases in the same file: none of them builds an MXFP4 cache.

Related work

Independent of the other MXFP4 index/KV PRs: this one builds and passes its tests on main alone. Siblings, each reviewable on its own:

Both index-score halves add their own export lines to aiter/ops/flydsl/__init__.py; they merge in either order without conflict (checked by merging both branches in both orders).

🤖 Generated with Claude Code

@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 7060 --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.

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

The benchmark unfairly includes full-cache conversion in every timed fp8 invocation while using precomputed fp4 inputs, inflating fp8 latency with work omitted from its reported byte count.

1 open issue
What changed in this PR

Adds MXFP4 index-cache decode scoring for MiniMax-M3, extending the existing FP8 scorer with a lower-storage cache format. This PR covers decode; prefill is handled separately in #7061.

Changes:

  • Specializes the shared scorer for MXFP4 caches with BF16 or E4M3 queries and optional query scaling.
  • Exposes lazy-loaded MXFP4 scoring and support-check APIs.
  • Adds correctness, cache-layout, graph-replay, input-validation, and performance tests.
File Description
op_tests/​test_flydsl_minimax_m3_index_score.py Adds MXFP4 tests and benchmarks; runs correctness checks before script-mode benchmarks.
aiter/​ops/​flydsl/​kernels/​minimax_m3_index_score.py Adds MXFP4 specialization, cache validation, and public wrappers to the shared scorer.
aiter/​ops/​flydsl/​__init__.py Exports the MXFP4 scorer and support predicate through lazy imports.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 28, 2026 08:34

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.

🔵 Needs a closer look

Negative query scales can bypass validation, score tests can miss infinity-sign mismatches, and FP8 benchmarks include conversion overhead absent from the FP4 path.

1 open issue
2 previously missed issues

In code that hasn't changed since last review

Medium severity Reject negative query scale independently of folded multiplier

aiter/​ops/​flydsl/​kernels/​minimax_m3_index_score.py:1876

With sm_scale=-1.0 and q_scale=-1.0, the folded multiplier is positive, so both the entry point and support predicate accept a query scale that violates the documented contract. Validate q_scale > 0 independently in _score and _supported (line 1733), while retaining the combined multiplier's finite FP32 range check.

Medium severity Use check_scores to validate negative-infinity entries

op_tests/​test_flydsl_minimax_m3_index_score.py:2125

This helper accepts +inf where the top-k reference contains -inf: both finite masks are false, so those entries are excluded from the error check. Replace the local comparison with check_scores(a, b) to check the negative-infinity mask and use the required checkAllclose path with standard mismatch diagnostics.

Review effort: Balanced

@ganyi1996ppo

Copy link
Copy Markdown
Contributor Author

Review items handled in bc2caf4:

  • Negative query scale (kernels/minimax_m3_index_score.py): valid, fixed. q_scale <= 0 or NaN is now refused on its own in _score and _supported (the folded-multiplier FP32 range check stays); sm_scale = q_scale = -1 is rejected by both, with a new test.
  • Infinity-sign mismatch in the top-k check (op_tests): valid, fixed. assert_topk_agrees now calls check_scores, which compares the -inf mask and runs checkAllclose; a new test covers a +inf where the reference has -inf.
  • fp8 baseline timing: same as the inline thread (fixed there).

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.

🔵 Needs a closer look

The gfx950 FP4/scaled-MFMA paths and shared fp8 refactor require exact-head GPU correctness, generated-code equivalence, and paired performance validation that this source-only review cannot independently verify.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

ganyi1996ppo and others added 6 commits October 28, 2026 21:20
New kernel `score_flydsl_fp4` scores an MXFP4 index cache (e2m1 data plus one
e8m0 scale per 32 elements) given as two views, k_data [pages,128,64] and
k_scale [pages,128,4], each with its own page stride, so separate tensors and
a single-region 8704 B page both work.

Query dtype selects the K path:
- bf16 Q: K is widened to bf16 with v_cvt_scalef32_pk_bf16_fp4 and fed to the
  same bf16 16x16x32 MFMA as the fp8 scorer; the numerics contract (K lifted to
  Q's precision) is unchanged.
- e4m3 Q: one v_mfma_scale_f32_16x16x128_f8f6f4 per 16-token tile, A = the
  lane's 16 B of fp4 K plus its e8m0 byte, B = Q as e4m3 (unit scale). Products
  are exact in fp32, so scores are those of dequantize(K) against Q as stored.
  An optional q_scale (default 1.0, positive) is folded into the score
  multiplier.

K tiles in flight is 6 rather than the fp8 scorer's 3: an MXFP4 tile is half
the bytes, and 6 was 15-35% faster than 3 on small batch*context cells in a
2/3/4/6/8 sweep.

Work map, geometry and CP shard are imported from the fp8 scorer. The builder
is a copy of `build_index_score` with the K side replaced, not a
parameterisation of it, so the two can drift and an epilogue or work-map change
must be mirrored. e8m0 bytes 0 and 255 are outside the contract.

Only exports are added to aiter/ops/flydsl/__init__.py; the fp8 path is
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Generated LLVM IR and ISA are byte-identical across 14 specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>
The sweep used checkAllclose, which only logs, so CI (python3 file)
could not fail on a wrong kernel; use the fp8 test's check_scores.
Import the fp8 test as siblings do and build the case matrix once.

Co-Authored-By: Claude <noreply@anthropic.com>
python3 op_tests/<file> now runs the pytest matrix before the perf sweep
(as test_flydsl_hc_mix.py does), so CI can fail on a wrong kernel. Import
the fp8 test with a script-mode fallback, hoist torch, fix the docstring
about e8m0 scale clamping. ruff 0.16.0 (ruff check .) and black are clean.

Co-Authored-By: Claude <noreply@anthropic.com>
One builder and one test file now cover both index caches: the MXFP4 cache
is an `mxfp4` specialisation of build_index_score, and score_flydsl_fp4 /
index_score_supported_fp4 are thin wrappers over the shared host path.
Generated LLVM IR and ISA are byte-identical to the separate kernels for 39
specialisations (13 fp8, 26 MXFP4); outputs are bit-equal on 54 cases.
python3 op_tests/<file> runs the pytest checks before the perf sweeps.

Co-Authored-By: Claude <noreply@anthropic.com>
Replaces the internal ASTRewriter.transform call, so a FlyDSL bump cannot
break the import. q_scale's docstring now says it scales a bf16 Q too (the
code always did) and a test checks it. LLVM IR and ISA are byte-identical
for 39 specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>
q_scale and the CP kwargs have no caller in ATOM#2770 or vLLM#64198, so the
MXFP4 entry points lose them, and score_flydsl / index_score_supported take an
optional k_scale instead of a parallel private core. Remove the tests that only
re-checked other tests' logic (top-k, fp8 cross-check, bf16-vs-e4m3 gap,
quantizer rule) and the CP and q-dtype multiples of graph and view tests.
IR and ISA are byte-identical for all 31 remaining specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 02:39

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.

🔵 Needs a closer look

The new oracle and wide-scale tests bypass the required shared checkAllclose path and should use check_scores to retain standard mismatch diagnostics and special-value handling.

1 open issue

Review effort: Balanced

@zufayu
zufayu removed the request for review from yadaish October 29, 2026 10:16
The MXFP4 tests this PR added used pytest fixtures, parametrize, skip, raises
and a pytest.main call in __main__. They are now plain check functions driven
from main() by a case table (PASS/FAIL table, exit 1 on any failure), like the
sibling flydsl op_tests, and __main__ is main's again. The same 89 cases run.
They compare with check_scores instead of the local compare. The pytest code
that was already on main is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 02:16
@aiter-bot

aiter-bot commented Oct 30, 2026 •

Copy link
Copy Markdown

NOT READY FOR HUMAN REVIEW

PR Self-Review

Revision 3c019db0daf1 · mode shadow

Waiting for the PR author. The readiness report will appear after the self-review answers are complete for this revision.

Questions to answer in the PR description

  1. What behavior does this PR change, and why is the existing behavior insufficient?
  2. What did you validate, and what remains unverified? Include commands, hardware/configuration and results.
  3. What behavior would each changed op test fail on before this PR, and why is that coverage not already provided by an existing canonical test?
  4. For every changed op_tests/ file, what behavior does it cover and why is it not redundant? Write None if no op test changed.

Edit the PR Self-Review section in the PR description. Include the current revision SHA; this comment refreshes automatically.

Missing or invalid answers

  • FAIL — Reviewed revision must contain at least the first 7 characters of the current head SHA

Shadow mode reports what would block but keeps the status successful. Set AITER_PR_SELF_REVIEW_MODE=enforce only after reviewing real false positives.

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.

🔵 Needs a closer look

Human assessment is needed to resolve the mismatch between the documented e4m3 saturation contract and the directly scaled MXFP4 decode path, which can produce different scores.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

…tion check

Kernel: use fx.rocdl for the scaled MFMA like the rest of the file, name the
0x7F7F7F7F unit e8m0 scale UNIT_SCALE, say why the MFMA is raw. No change to
the generated code.
Test: add a check that sm_scale <= 0 and NaN are refused by score_flydsl_fp4.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 02:39
@ganyi1996ppo

ganyi1996ppo commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Current head: 827441b. Any other SHA quoted in my older replies on this PR belongs to history that was rewritten by a rebase; read this head.

What changed since accbe63:

  • kernel: fx.rocdl. used consistently (the bare rocdl import is gone); the unit e8m0 scale 0x7F7F7F7F is the named constant UNIT_SCALE; one comment says the scaled MFMA is called raw on dword vectors (I did not try the fx.gemm MFMA_Scale atom here). Generated code unchanged: LLVM IR and ISA byte-identical before/after for 8 fp8 and 8 MXFP4 decode kernels.
  • op test: new sm_scale_must_be_positive_finite check, score_flydsl_fp4 must raise ValueError for sm_scale of -1, 0 and NaN (my earlier comment of 2026-10-29 02:16 said sm_scale = q_scale = -1 is rejected by both with a new test; q_scale has since been removed and no test of sm_scale <= 0 or NaN was in the tree at the previous head, so this adds it).
  • Run on gfx950: python op_tests/test_flydsl_minimax_m3_index_score.py from the repo root and from /tmp, exit 0, 90/90 MXFP4 checks PASS, perf tables printed.
  • PR description updated (stale numbers, the PR Self-Review sections).

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.

🔵 Needs a closer look

Human assessment is needed to determine how buffer initialization included in benchmark timing but excluded from throughput accounting affects the reported performance results.

No open issues 🎉

Review effort: Balanced

@yhl-amd

yhl-amd commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

@valar-cop review this pr

@valar-cop valar-cop left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

some changes needed

any perf number? and confirm the MI35X shard actually ran the 90 mxfp4 checks

ci green.

…m0 255 doc

- every-geometry check now runs the CP points (cp_world>1 with k_scale) against
  the oracle; MXFP4 + CP works, so it is covered rather than rejected.
- q_for keeps q's strides, so the qslice e4m3 case reaches load_q8's strided path.
- bench_score_mxfp4 allocates the score buffer once, outside the timed region.
- Docs: e8m0 byte 255 is +inf as f32 (bf16-Q path) and NaN in the scaled MFMA
  (e4m3-Q path); measured, the score is NaN on both.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 30, 2026 09:54

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.

🔵 Needs a closer look

The shared-kernel refactor and new gfx950 FP4 scaled-MFMA paths require target-GPU correctness, generated-code equivalence, graph-replay, and paired performance checks that this source-only review cannot perform.

No open issues 🎉

Review effort: Balanced

@valarLip
valarLip merged commit 514c7cd into main Nov 2, 2026
65 checks passed
@valarLip
valarLip deleted the ganyi/m3-index-score-decode-fp4 branch November 2, 2026 03:08
solinzby1 pushed a commit that referenced this pull request Nov 2, 2026
* [FlyDSL] M3 index score: MXFP4 index-cache decode scorer

New kernel `score_flydsl_fp4` scores an MXFP4 index cache (e2m1 data plus one
e8m0 scale per 32 elements) given as two views, k_data [pages,128,64] and
k_scale [pages,128,4], each with its own page stride, so separate tensors and
a single-region 8704 B page both work.

Query dtype selects the K path:
- bf16 Q: K is widened to bf16 with v_cvt_scalef32_pk_bf16_fp4 and fed to the
  same bf16 16x16x32 MFMA as the fp8 scorer; the numerics contract (K lifted to
  Q's precision) is unchanged.
- e4m3 Q: one v_mfma_scale_f32_16x16x128_f8f6f4 per 16-token tile, A = the
  lane's 16 B of fp4 K plus its e8m0 byte, B = Q as e4m3 (unit scale). Products
  are exact in fp32, so scores are those of dequantize(K) against Q as stored.
  An optional q_scale (default 1.0, positive) is folded into the score
  multiplier.

K tiles in flight is 6 rather than the fp8 scorer's 3: an MXFP4 tile is half
the bytes, and 6 was 15-35% faster than 3 on small batch*context cells in a
2/3/4/6/8 sweep.

Work map, geometry and CP shard are imported from the fp8 scorer. The builder
is a copy of `build_index_score` with the K side replaced, not a
parameterisation of it, so the two can drift and an epilogue or work-map change
must be mirrored. e8m0 bytes 0 and 255 are outside the contract.

Only exports are added to aiter/ops/flydsl/__init__.py; the fp8 path is
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>

* Trim MXFP4 decode scorer dead indirection and docstring

Generated LLVM IR and ISA are byte-identical across 14 specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>

* Make MXFP4 index-score sweep assert like the fp8 sweep

The sweep used checkAllclose, which only logs, so CI (python3 file)
could not fail on a wrong kernel; use the fp8 test's check_scores.
Import the fp8 test as siblings do and build the case matrix once.

Co-Authored-By: Claude <noreply@anthropic.com>

* Run MXFP4 index-score checks from main() and fix import/lint

python3 op_tests/<file> now runs the pytest matrix before the perf sweep
(as test_flydsl_hc_mix.py does), so CI can fail on a wrong kernel. Import
the fp8 test with a script-mode fallback, hoist torch, fix the docstring
about e8m0 scale clamping. ruff 0.16.0 (ruff check .) and black are clean.

Co-Authored-By: Claude <noreply@anthropic.com>

* Fold the MXFP4 decode scorer into the fp8 index-score builder

One builder and one test file now cover both index caches: the MXFP4 cache
is an `mxfp4` specialisation of build_index_score, and score_flydsl_fp4 /
index_score_supported_fp4 are thin wrappers over the shared host path.
Generated LLVM IR and ISA are byte-identical to the separate kernels for 39
specialisations (13 fp8, 26 MXFP4); outputs are bit-equal on 54 cases.
python3 op_tests/<file> runs the pytest checks before the perf sweeps.

Co-Authored-By: Claude <noreply@anthropic.com>

* Use public flyc.jit for the shared index-score body

Replaces the internal ASTRewriter.transform call, so a FlyDSL bump cannot
break the import. q_scale's docstring now says it scales a bf16 Q too (the
code always did) and a test checks it. LLVM IR and ISA are byte-identical
for 39 specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>

* Address review: validate q_scale on its own, fair fp8 baseline, sign-aware top-k check

q_scale was only checked through the folded multiplier, so sm_scale = q_scale = -1
passed; reject q_scale <= 0 in the entry point and the predicate. The perf sweep
now converts the fp8 cache once outside the timed callable. assert_topk_agrees
uses check_scores, which also compares the -inf mask (a +inf slot slipped through).

Co-Authored-By: Claude <noreply@anthropic.com>

* Drop MXFP4 scorer routes nothing calls

q_scale and the CP kwargs have no caller in ATOM#2770 or vLLM#64198, so the
MXFP4 entry points lose them, and score_flydsl / index_score_supported take an
optional k_scale instead of a parallel private core. Remove the tests that only
re-checked other tests' logic (top-k, fp8 cross-check, bf16-vs-e4m3 gap,
quantizer rule) and the CP and q-dtype multiples of graph and view tests.
IR and ISA are byte-identical for all 31 remaining specialisations.

Co-Authored-By: Claude <noreply@anthropic.com>

* Run the MXFP4 decode op tests from main() instead of pytest

The MXFP4 tests this PR added used pytest fixtures, parametrize, skip, raises
and a pytest.main call in __main__. They are now plain check functions driven
from main() by a case table (PASS/FAIL table, exit 1 on any failure), like the
sibling flydsl op_tests, and __main__ is main's again. The same 89 cases run.
They compare with check_scores instead of the local compare. The pytest code
that was already on main is untouched.

Co-Authored-By: Claude <noreply@anthropic.com>

* MXFP4 decode: fx.rocdl consistently, named unit scale, sm_scale rejection check

Kernel: use fx.rocdl for the scaled MFMA like the rest of the file, name the
0x7F7F7F7F unit e8m0 scale UNIT_SCALE, say why the MFMA is raw. No change to
the generated code.
Test: add a check that sm_scale <= 0 and NaN are refused by score_flydsl_fp4.

Co-Authored-By: Claude <noreply@anthropic.com>

* MXFP4 decode tests: CP shards, strided e4m3 Q, prealloc bench; fix e8m0 255 doc

- every-geometry check now runs the CP points (cp_world>1 with k_scale) against
  the oracle; MXFP4 + CP works, so it is covered rather than rejected.
- q_for keeps q's strides, so the qslice e4m3 case reaches load_q8's strided path.
- bench_score_mxfp4 allocates the score buffer once, outside the timed region.
- Docs: e8m0 byte 255 is +inf as f32 (bf16-Q path) and NaN in the scaled MFMA
  (e4m3-Q path); measured, the score is NaN on both.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: r <r@x>
wanzhenchn added a commit to ROCm/ATOM that referenced this pull request Nov 12, 2026
The MXFP4 CP decode hands aiter's scorer k_scale and the shard (cp_world /
cp_rank), and its work map and capacity calls the shard. A new GPU-free
test runs those three calls and binds each to the installed aiter's
signature, so an aiter older than ROCm/aiter#7060 (no k_scale) fails at
test time rather than at the first MXFP4 CP decode. It skips where
aiter.ops.flydsl does not import.

Co-authored-by: Cursor <cursoragent@cursor.com>
wanzhenchn added a commit to ROCm/ATOM that referenced this pull request Nov 12, 2026
The MXFP4 CP decode hands aiter's scorer k_scale and the shard (cp_world /
cp_rank), and its work map and capacity calls the shard. A new GPU-free
test runs those three calls and binds each to the installed aiter's
signature, so an aiter older than ROCm/aiter#7060 (no k_scale) fails at
test time rather than at the first MXFP4 CP decode. It skips where
aiter.ops.flydsl does not import.

Co-authored-by: Cursor <cursoragent@cursor.com>
wanzhenchn added a commit to ROCm/ATOM that referenced this pull request Nov 12, 2026
The MXFP4 CP decode hands aiter's scorer k_scale and the shard (cp_world /
cp_rank), and its work map and capacity calls the shard. A new GPU-free
test runs those three calls and binds each to the installed aiter's
signature, so an aiter older than ROCm/aiter#7060 (no k_scale) fails at
test time rather than at the first MXFP4 CP decode. It skips where
aiter.ops.flydsl does not import.

Co-authored-by: Cursor <cursoragent@cursor.com>
wanzhenchn added a commit to ROCm/ATOM that referenced this pull request Nov 12, 2026
The MXFP4 CP decode hands aiter's scorer k_scale and the shard (cp_world /
cp_rank), and its work map and capacity calls the shard. A new GPU-free
test runs those three calls and binds each to the installed aiter's
signature, so an aiter older than ROCm/aiter#7060 (no k_scale) fails at
test time rather than at the first MXFP4 CP decode. It skips where
aiter.ops.flydsl does not import.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants