Repository navigation
Conversation
…erface for the gfx1250 V4 and V4.1 kernels
…gits_mxfp4 kernel
…ch tile's sequence and writing _pad at emission
…fp4 to drop a dependent prologue round
…sequence, adding the R1W2, R3W1 and R2W2 tiers
…quence, adding the q2 and q6 tiers
… arch so gfx950 instances register without changing the Python interface
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
One backend per PR: PR title tags & labels: |
There was a problem hiding this comment.
🟡 Changes recommended
Missing dense-output width validation permits out-of-bounds writes, and previously supported gfx950 functionality has no replacement.
2 open issues
What changed in this PR
This PR replaces the legacy OPUS MXFP4 MQA-logits API with a registry-based interface for DeepSeek-V4 and V4.1 on gfx1250.
Changes:
- Adds configuration-driven kernel selection and Table, Free, and Prefill scheduling.
- Introduces specialized 64-head and 32-head kernels, including candidate-page gathering and flat outputs.
- Updates Python bindings and JIT configuration while removing the legacy implementation.
| File | Description |
|---|---|
csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/pa_mqa_logits_mxfp4_opus.h |
Removes the legacy API and architecture traits. |
csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/pa_mqa_logits_mxfp4_kernels.cu |
Removes the legacy dual-architecture launcher. |
csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/pa_mqa_logits_mxfp4_gfx1250.cuh |
Removes the previous gfx1250 kernel. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4.h |
Defines the unified API, arguments, and registry. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_sched.cuh |
Adapts Table scheduling to sequence-based row groups. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_pybind.cu |
Connects the replacement Python bindings. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_launcher.cu |
Adds registry discovery, validation, and launch dispatch. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250.cu |
Registers gfx1250 kernel instances and occupancy queries. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250_v41.cuh |
Implements V4.1 full-layer and candidate-layer kernels. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250_traits.h |
Defines shared layouts and kernel-specific traits. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250_h64.cuh |
Implements the replacement V4 64-head kernel. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250_common.cuh |
Adds shared reduction, NaN, and priority helpers. |
csrc/kernels/opus_mqa_logits/mqa_logits_mxfp4/mqa_logits_mxfp4_gfx1250_assign.cuh |
Assigns kernel work across the three scheduling modes. |
csrc/include/rocm_ops.hpp |
Replaces legacy binding declarations. |
aiter/ops/opus/pa_mqa_logits_mxfp4.py |
Removes the legacy Python interface. |
aiter/ops/opus/mqa_logits_mxfp4.py |
Adds configuration, scheduling, and unified launch APIs. |
aiter/jit/optCompilerConfig.json |
Updates source files and per-source compiler flags. |
Review effort: Balanced
…ogits_mxfp4 with qlen 1/2/4/6/8 decode tiers
…and op test file names
…6 instance and the test switches, trim comments, unify names and merge 17 files into 12
…ift limit between the launcher and the gfx1250 registry, state gfx950's 4 GiB KV reach, and put the launch op's tensors first
…ke a config a value compared by its key that holds no device memory, so per-step prefill shapes no longer accumulate configs
…2x32x64 rows kernel (1-8 rows a wave, four-slot CTAs for large batches), cut Free decode in one pass for uniform rows, cap light-row Free slots, and size candidate walks by work in multiples of the XCDs
There was a problem hiding this comment.
🔵 Needs a closer look
The architecture-specific pipeline rewrite requires GPU correctness and graph-replay validation on gfx950 and gfx1250, but the supplied results omit gfx950 V4.1 coverage for the newly selected 32-head kernels.
No open issues 🎉
Review effort: Balanced
…fp4_v2 # Conflicts: # csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/pa_mqa_logits_mxfp4_gfx1250.cuh # csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/pa_mqa_logits_mxfp4_kernels.cu
…ing it to the gfx1250 family in the device guard, the registry's arch and the config's rule, as main's #6767 does for the other ops
There was a problem hiding this comment.
🟡 Changes recommended
A critical lane-bias wraparound allows unused head-reduction partners to overwrite valid output columns at supported large widths, requiring store guards in both kernel variants, including the gathered path.
1 open issue
Review effort: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Critical kernel bugs leave rows unwritten when batches exceed the cached prefix and allow supposedly masked stores to overwrite valid wide-row outputs and race their correct writers.
2 open issues
1 resolved since last review
Review effort: Balanced
…ces past its 256-sequence cache, and max_seq_len is capped under 2^29 so a masked lane group's stores cannot wrap into the row
There was a problem hiding this comment.
🔵 Needs a closer look
The architecture-specific kernel rewrite changes synchronization and compiler-sensitive NaN handling, requiring correctness and latency validation on gfx950 and gfx1250 GPUs unavailable in this review environment.
No open issues 🎉
2 resolved since last review
Review effort: Balanced
…fer resource a page (candidate layers: 64-bit global loads), and test it with a semantics check that moves a case's pages across 4 GiB


Motivation
aiter's MXFP4 paged MQA-logits op (
aiter.ops.opus.pa_mqa_logits_mxfp4) serves the DeepSeek-V4sparse-attention indexer on gfx1250 and gfx950. Its API was built around one kernel family:
pa_mqa_logits_mxfp4_plan_buffers(..., variant="qlen1_kv64")), so every new instance is a caller change;row_to_batchmap;PAGEs of 8-row pages, candidate layers with one table row per query row) has no OPUS path;
This PR replaces that API with one interface for V4 and V4.1 on both arches, where the caller
passes static bounds and the op picks the kernel instance and the schedule mode itself.
Technical Details
API
query_start_loc. Sequencebreads its keys through
block_tables[b], an entry namingpages_per_blockpages, and rowrsees the prefix
[0, row_ends[r]). There is norow_to_batchand norow_starts. A band ofrows is the sliced row tensors plus
query_start_loc.clamp(start, start + count) - start.outis[rows, >= max_seq_len], or flat without_offsets.schedule-free mode, and sizes the grid from the instance's measured occupancy.
the 1024 most recent are cached, so a caller may ask
for one a step with that step's shapes: prefill's varying rows, sequences and widths do not
accumulate configs. A schedule or schedule buffers built from one serve any equal one.
Table. Without one it runsFree(decode) or
Prefill. No path reads device data on the host, so every path can be captured ina CUDA graph.
ndim.
TableFreequery_start_loc/row_endsPrefillFreecuts a decode batch of up to 64 sequences with the same number of rows each, whatevertheir lengths, in one pass over the lane-per-sequence bounds; other batches take the general cut.
Instances and selection
The C++ registry exports each instance's attributes. Python's per-arch rule picks by attributes,
and the launch dispatches by registry id. Names read
h<heads>[g]_r<rows a wave>w<waves>s<stages><T|L>[n][x4]:g: the instance gathers 8-row entries;T: the key scales travel with the tile;L: they are loaded per lane;n: the KV is loaded nontemporal (decode: no other CTA reads a walk again);x4: a CTA's four waves are schedule slots of their own.The picks below are the config's own, and each was the best instance measured for its scenario.
h32_r1w1s2L,h32_r1w2s2T,h32_r3w1s2T,h32_r2w2s2T,h32_r3w2s2T,h32_r4w2s2Th32_r1w1s2L/h32_r1w2s2T/h32_r2w2s2T/h32_r3w2s2T/h32_r4w2s2T(MTP 3:h32_r3w1s2T),Freeh32_r4w2s2T,Prefillh32g_r1w1s4L,h32g_r1w1s3Lh32g_r1w1s4L,Prefillh32g_r1w1s3L(from 16384 rows),Prefillh64_r1w1s2T,h64_r1w2s2T,h64_r1w4s2T,h64_r1w6s2Th64_r1w1s2T/h64_r1w2s2T/h64_r1w4s2T/h64_r1w6s2T/h64_r1w4s2T,Tableh64_r1w4s2T,Prefillh32_r{1,2,3,6,8}w1s3Ln,h32_r4w1s2Ln, each alsox4;h32_r4w1s3Lh32_r1w1s3Ln/h32_r2w1s3Ln/h32_r4w1s2Ln/h32_r6w1s3Ln/h32_r8w1s3Ln(MTP 3:h32_r3w1s3Ln;x4from max_rows x max_seq_len >= 12M),Freeh32_r4w1s3L,Prefillh32g_r1w1s2Lh32g_r1w1s2L,Prefillh32g_r1w1s2L,Prefillh64_r1w1s2L(main'sqlen1_kv64),h64_r1w2s4T,h64_r1w4s4T(new: a sequence's rows share each KV tile through LDS)h64_r1w1s2L/h64_r1w2s4T/h64_r1w4s4T/h64_r1w2s4T/h64_r1w4s4T,Tableh64_r1w1s2L,PrefillThe caller only chooses whether to pass a schedule. Each configuration was ranked by kernel
time, with a schedule build charged over the layers that share it: V4's 30 indexer layers, and
V4.1's three ratio-2 full layers or four candidate layers.
Freereaches 0.88-0.90 of the best.Freereaches0.98-1.00 of the best.
TableandPrefilltie on kernel time, so the build (up to 47 uson gfx1250 and 104 us on gfx950 at 16384 rows) decides it.
A tier above a CTA's rows runs two or three CTAs a sequence. Not registered on gfx950:
qlen1_kv256: it was never the best choice.V4.1 on gfx950 reads V4's 64-row layout: an 8-row page is an aligned eighth of a 64-row
group, and a full layer's table entry names a run of groups.
_gfx950_rows.cuh): a wave scores up to eight rows of onesequence off each KV tile, with MFMA 32x32x64, a row's 32 heads one m-tile. Decode loads the
KV nontemporal; prefill keeps it cached for the sequence's next rows.
Free: one- and two-row waves get at least 32 row-tiles a slot (and no fewer slotsthan SIMDs), and large launches use the
x4CTAs, whose four slots are cut once.launch's size, and a row's parts are a multiple of the 8 XCDs, so the rows of a request, which
share most of their pages, meet in one L2.
needs the plane in the 64-row layout.
Sources:
csrc/kernels/opus_mqa_logits/pa_mqa_logits_mxfp4/, laid out asopus_gemm/opus_moeare:.h: the interface;.cu: the registry, theTablebuilder and the launch;_pybind.cu;_common.cuh: shared device helpers;_<arch>.cu(traits, instances, registry),_<arch>_common.cuh(CTA assignment,tile math) and one
.cuha kernel (_gfx1250_{h32,h64},_gfx950_{kv,qshare,rows}).Caller contract
What the device cannot check, stated in the header and the module docstring:
row_endsdoes not decrease along a sequence's rows;row_ends[r] <= max_seq_len, within whatblock_tables[b]covers;query_start_loc[-1] <= rows; rows past it (graph padding) are neither read nor written;so the
kv_cacheis not bounded at 4 GiB, except that a candidate layer reads within its first2^32 keys (256 GiB, which the launch checks).
Breaking changes
Removed:
pa_mqa_logits_mxfp4,pa_mqa_logits_mxfp4_plan,pa_mqa_logits_mxfp4_plan_buffers,pa_mqa_logits_mxfp4_block_table_width,pa_mqa_logits_mxfp4_variants,MqaLogitsVariant,MqaLogitsBuffers,MqaLogitsPlan. A stalecaller fails at import.
ATOM (
9df8e84) imports the removed names atatom/model_ops/indexer/rows.py:11,atom/model_ops/indexer/score.py:27andatom/model_ops/attentions/deepseek_v4_attn.py:555,and moves to the new API in the same upgrade. TODO: link the ATOM PR, and add
ci:atomhereso aiter CI runs ATOM's tests against this branch.
plan_buffers(variant=...)a poolcfga bucket (max_qlen), withcfg.schedule_buffers()plan(cu_seqlens_q, local_ends, row_to_batch=...)mqa_logits_mxfp4_schedule(cfg, query_start_loc, row_ends)row_to_batchslicequery_start_loc.clamp(start, start + count) - startplanquery_start_loc,row_ends,cfg,scheduleJIT: the module keeps main's name (
module_pa_mqa_logits_mxfp4_opus). Rebuild a buildleft over from main with
AITER_REBUILD=1.Test Plan
Op test (rewritten for the new API), on each arch:
It runs every instance the arch registers in every mode it compiles, against a torch reference
(gfx950's reference rounds after each 32-element block, as its MFMA does):
decode: V4 at bs 1 / 16 / 64 / 128 x MTP 1 / 4 / 6, 64K context;prefill: 16384 rows at4096 / 16384 rows a sequence (V4.1 full-layer and candidate shapes as well, on both arches);
semantics:rows: every row against the reference, every mode bit-identical to its instance'sTable;edges: nine sequences, each carrying one source of NaN / Inf (E8M0 0xFF key or queryscales, with and without a zero q block; NaN / Inf weights; overflowing products), and
nothing written past a window;
bands: a forward scored in bands of rows through clampedquery_start_loc;padding: graph-padding rows neither read nor written;graph: capture and replay with inputs refreshed in place;errors: the other arch's layout, a head count other than the config's, a narrow blocktable or
out, a schedule built from other tensors, rows overmax_rows, int64row_ends,a page geometry no instance reads -- each rejected with a message.
Performance against what ATOM runs today.
gfx1250, V4.1: FlyDSL's
flydsl_fp8_paged_mqa_logits([FlyDSL] [gfx1250] Paged FP8 MQA logits with 8-token KV pages #6863), which ATOM runs for V4.1'sindex scoring on gfx1250 since [gfx1250] DeepSeek-V4.1: run on gfx1250 (MI455X) with FlyDSL kernels ATOM#2617 (
atom/model_ops/indexer/score.py). It is calledas ATOM calls it:
Preshuffle=True, KVBlockSize=8);next_n = 1) on its own table row, for full and candidate layersalike.
ATOM's FP4 index plane (
--index-cache-dtype fp4) is gfx950-only, so on gfx1250 V4.1 FP4scoring exists only through this PR. An FP8 key is 132 bytes against FP4's 68, and an FP8 row
reads its own window where our rows of a sequence share theirs. Part of that gap is the format
and the sharing, not the kernel.
gfx1250, V4: main's OPUS op (the API this PR removes), as ATOM calls it today:
qlen1_kv64atMTP 1,
qlen4_kv64otherwise and for prefill (ROCm/ATOM9df8e84,atom/model_ops/indexer/rows.py), its plan built once a forward.gfx950: FlyDSL row-group ([HIP] [FlyDSL] feat(flydsl): row-group FP4 paged MQA logits for 8- and 64-row pages #6145), which ATOM main uses for decode (feat(v41): mono decode, FP4 index plane, shared mono framework ATOM#2479) and prefill
(refactor: one graph mechanism per region, settled DP step shapes, cheaper V4.1 decode prep ATOM#2688), called as ATOM calls it (
make_fp4_mqa_planfor the call's shape). For V4.1it is ATOM's FP4 scorer (
--index-cache-dtype fp4,flydsl_pa_mqa_logits_fp4on 8-row pages),timed on its own 8-row page layout of the same plane. ATOM's default FP8 plane, scored by
Triton's
deepgemm_fp8_paged_mqa_logits, is not compared.Method (gfx1250; harness in ROCm/opus-ops
opus_logits/gfx1250/tools/mqa_v2/,run_pr.sh+merge_min.py+report_pr.py):before every call), the median of 50 calls (20 for prefill). Each arm is the minimum over every
pass that timed it: 2-3 full passes of 2 reps each (the second rep in reverse order). The card
is shared, and another job only ever slows a call down. The schedule build is timed separately
(it runs once a forward, not once a layer).
The two builds share a module name, so they cannot be loaded in one process.
(to 1e-5 of the largest logit), and every mode must equal its instance's first bit for bit. The
FP8 arm reads random FP8 data and is timed only. Its call was checked separately: on 16-row
pages it equals Triton's
deepgemm_fp8_paged_mqa_logits(relative error 2e-7), and on 8-rowpages it writes exactly each row's window.
psees(p + 1) // 2keys), 8-row pages, a 256-token PAGE a table entry. Decode at MTP 1 / 2 / 4 /6 / 8 on V4's contexts below, at most 16M keys a call (22 fixed and 13 ragged shapes a MTP);
prefill on V4's fresh and chunked shapes.
most 16384 keys). Decode at bs 1-256 x MTP 1 / 6, and two 16384-row prefills.
psees(p + 1) // 4keys).64 / 128 / 256 x 8K / 32K / 128K / 512K (23 shapes), and ragged batches with each context
drawn uniformly from
[L / 8, L], bs 4 / 16 / 64 / 128 / 256 x L = 32K / 128K / 512K (14shapes). A call reads at most 16M KV keys, which leaves out bs 256 at 512K.
ctx = rows a sequence) and chunked.band sizes ATOM's workspace rule gives.
Method (gfx950; harness in ROCm/opus-ops
opus_logits/gfx950/tools/mqa_v2/,run_pr.sh+report_pr.py): device time of the kernels a call launches, from the torch profiler, cold (a1 GiB read before every call evicts L2 and the MALL), the minimum over 3 reps of the median of 50
calls (20 for prefill), every arm on one GPU in one process. The schedule build is timed
separately (it runs once a forward, not once a layer). Before timing, every arm's output is
compared with
h64_r1w1s2LTable: OPUS arms bit-exact, row-group within 1e-5 of the row'slargest logit. Shapes: gfx1250's V4 shapes above.
Method (gfx950, V4.1): the same cold, kernel-only timing, with every call timed on its own. The
MI355X was shared with another job during these runs, and another job only ever slows a call
down, so the two arms alternate call by call, 30-60 calls an arm (40 for prefill), and each arm
is its minimum over 2-6 passes on two GPUs. Ours is the config's own pick for the call's shapes,
without a schedule; both arms get the batch's longest window as the logits width. Before timing,
sweep_v41.pycompares 16 sampled rows of every arm with the reference (to 1e-5 of the largestlogit) and every mode of an instance bit for bit. Shapes, on ATOM's 128-token PAGE (8 / 16 pages
a table entry at ratio 2 / 1):
/ 256 x 32K / 128K tokens, with fixed contexts and ragged ones (each context drawn from
[L / 8, L], each sequence 1-MTP rows): 168 + 168 shapes. Prefill: 16384 rows, fresh at 512 /1024 / 4096 / 16384 rows a sequence, chunked at 32K-256K, and ATOM's bands of one request's
1024-16384 rows at 32K-1M: 27 shapes.
prefill on 11 of the full-layer prefill shapes.
max_model_len / ratio(V4.1's 1M tokens), thecontexts ragged at 4K-32K or 16K-128K, bs 1-64 at MTP 1 / 6 and bs 128 / 256 at MTP 1, and a
forward's eight calls (three ratio-2, one ratio-1, four candidate).
Environment
rocm/fw-bringup-mlperf@sha256:e7fcdb088de972009dcd02778eadd5552374a30a3edfd26a3b35218cb7df116d(gfx1250-atom-20261012)rocm/atom-dev@sha256:066841903594e8d691b1c528123dfd2e17eacd293acfde947d4fb02afffd57dbd7bbac8bb; main778d74d57for the baselined7bbac8bb(V4),791fc8874(V4.1)Test Result
Correctness
err = 0: V4.1 24 decode, 4 prefill shapes x 18 arms; candidate 12, 2 x 4; V4 12, 2 x 12err = 0: V4.1 24 decode, 4 prefill shapes x 39 arms; candidate 12, 2 x 3; V4 12, 2 x 9h64_r1w1s2LTable, row-group within 1e-5. V4.1: 336 full-layer decode shapes (every decode instance inFree) and 27 prefill shapes, 24 candidate decode and 6 prefill shapes: every OPUS arm within 1e-5 of the reference, every mode of an instance bit-identicalPerformance
Each scenario runs its pick (Instances and selection) against what ATOM runs today (Test Plan).
x= baseline us / ours: above 1, ours is faster. Timing is cold and kernel-only, and decodecells are geomeans at MTP 1 / 2 / 4 / 6 / 8. The per-shape tables are in ROCm/opus-ops
opus_logits/common/docs/PR6952_PERF_DETAILS.md.flydsl_fp8_paged_mqa_logits)gfx1250.
own window.
empty there.
reads a 6-row sequence's KV twice;
h64_r1w2s4Treads it three times.TFLOPS).
h64_r1w1s2Lneeds fewer cycles, but it moves more data out of L2 and runs at alower clock under the power limit.
--index-cache-dtype fp4.shapes, by at most 12%.
max_model_len / ratio, 1M tokens unless lowered) the gap widens:ratio-2 calls 1.62x, ratio-1 1.47x, candidate 0.98x; a forward's eight calls 1.31x
(1.02-2.17x). Our cut follows each row's window, whatever the width.
candidate table (16 workgroups a row) whatever a row's window, so its time hardly falls with
the prompt: 481 us at 512-token prompts, 1354 us at 16K. Ours follows the keys: 97 / 589 us.
Submission Checklist
🤖 Generated with Claude Code