Skip to content

[FlyDSL] [Perf] pa_decode: K/V streaming, MTP query split and sparse split-major on gfx950 - #7864

Open
fsx950223 wants to merge 51 commits into
mainfrom
fsx950223/pa-gfx950-gfx942-policies
Open

fsx950223 wants to merge 51 commits into
mainfrom
fsx950223/pa-gfx950-gfx942-policies

Conversation

@fsx950223

Copy link
Copy Markdown
Contributor

Summary

Brings three of #7709's gfx942 pa_decode schedule policies to gfx950, each re-tuned on MI355X:

  • K/V streaming for long head rows;
  • MTP query splitting over idle CUs;
  • split-major launches on sparse grids.

gfx942 and gfx1250 kernels are bit-for-bit unchanged.

On MI355X, over all 817 cases of the unit-test-geometry matrix described below: geomean 1.19x against the merged dependencies 03894b767e, i.e. #7700+#7701+#7709 without this PR's commits (unit-test cases 1.22x, grid cases 1.19x). The 333 cases whose kernels change run 1.54x faster, and none is slower than 0.98x.

This PR depends on #7700, #7701 and #7709.

Motivation

#7709 found four schedule problems on gfx942. To see which also hold on gfx950, I opened each of #7709's gfx942-only gates on gfx950 and measured it on its own, against main and against #7700+#7701. Three hold:

  1. Spills at long head rows. The tile holds a whole next-tile K prefetch plus the tile's V gather in VGPRs. Once a lane's K row reaches 1 KiB (BF16 D512+, FP8 D1024), gfx950 spills as gfx942 does. Single and paired MTP M-tiles with 384/256-byte rows drop to one wave per SIMD on busy grids. Streaming runs these 1.8-7.7x faster.
  2. Idle CUs under MTP. With few sequences, one CTA's serial latency over all query positions is the decode time while most CUs idle.
  3. Plan-order dispatch on small grids. The tile grid dispatches x first, so later query splits and KV heads queue behind every empty plan slot.

Three gfx942 policies did not carry over and stay gfx942-only:

Changes

All changes are in aiter/ops/flydsl/kernels/pa_decode/{traits,op_epilog}.py and pa_decode_kernel.py, one commit each:

  1. 372b2bc843 K/V streaming on gfx950 (STREAM_KV).
  2. 50e1d38283 MTP query split over idle gfx950 CUs, using [FlyDSL] [Perf] pa_decode: gfx942 schedule tuning (K/V streaming, latency hiding, grid-size policies) #7709's rules with two gfx950 differences:
    • Per-token FP8 keeps its fused MTP4 tiles. Splitting them ran 0.75x.
    • The FP8 split also requires at most eight KV tiles per CU. This uses tile_demand, the sequence x head x tile count before the plan's capacity cap. gfx950 MTP plans fix capacity regardless of load, so a capacity bound no longer means idle CUs (B64/32K per-tensor ran 0.87x without the cap).
    • The busy BF16 D128 split stays gfx942-only.
  3. cbffdd201b Split-major launches on sparse gfx950 grids.
    • gfx950 uses split-major only while at most a quarter of the CUs hold work. On fuller grids slot-major ran up to 1.25x faster.
    • It applies to FP8, and to BF16 up to D128 (LDS-forced BF16 D192/D256 splits ran 0.91-0.93x). MXFP4 keeps slot-major.
    • Two gfx950-only paths assumed the slot-major axes and now follow the grid:
      • The BF16 D128 planned output's slot count read grid_dim.x. With a split-major grid that is heads x splits, so partials were stored at wrong offsets.
      • The one-partition plan's empty-row zero fill read block_id("y") as the KV head.

Not changed:

  • the API, plan_pa_decode and its budgets;
  • the reducer;
  • softmax math;
  • MXFP4 paths;
  • every gfx942 and gfx1250 binary.

Merged dependencies

The commits before the last three (up to 03894b767e) are built from:

The #7709 merge resolves 19 conflict hunks in traits.py, pipeline.py, op_lds.py and pa_decode_kernel.py as a union of both sides: #7709's policies stay gfx942-only and #7700's paths gfx950-only. A compile-only replay of every matrix case gives:

While porting streaming, I found a first-tile LDS race in #7709's streamed path. It is fixed in #7709 itself (65eed8399b), not here.

Performance (if applicable)

How these numbers were measured:

  • Hardware: MI355X (gfx950, 256 CUs), ROCm 7.2.4, torch 2.10, FlyDSL 0.5.0. Before = 03894b767e (the merged dependencies), after = cbffdd201b.
  • Matrix (817 cases): every geometry of op_tests/test_flydsl_pa_decode.py that runs on gfx950 at its unit-test lengths (115 cases), plus each geometry at batch {1, 16, 64} x context {1K, 8K, 32K} with the default plan (702 cases).
  • Method: paired in-process A/B, both trees loaded in one process. CUDA-graph replays of 10 calls, 10 replays x 20 ABBA pairs per case; median ratio.
  • Noise: the 484 cases whose kernels are binary-identical read geomean 1.0000, 1st-99th percentile 0.984-1.018.
Hardware Workload/configuration Before After Change
MI355X Full matrix, 817 cases (geomean) - - 1.19x
MI355X The 333 changed cases (geomean) - - 1.54x (min 0.984x)
MI355X BF16 D1024 MTP4 GQA16, B16 ctx1K 202.2 us 26.2 us 7.72x
MI355X BF16 D1024 MTP4 GQA16, B16 ctx8K 1662.0 us 389.5 us 4.27x
MI355X BF16 D512 MTP4 GQA16, B16 ctx8K 326.5 us 91.5 us 3.57x
MI355X FP8 D1024 GQA4, B16 ctx8K 116.7 us 49.6 us 2.35x
MI355X FP8 MTP4 GQA16 page128, UT mtp4-g16-page128-direct 20.2 us 9.3 us 2.17x
MI355X BF16 D64 MTP4 GQA16 page128, UT ...-direct-d64 20.3 us 12.4 us 1.64x
MI355X FP8 per-token MTP4 window 257, B1 ctx8K 9.2 us 6.4 us 1.45x
MI355X Worst changed case: FP8 D256 MTP4 2 heads page128, B64 ctx32K 362.6 us 368.5 us 0.98x

Per commit, each against its parent, over the cases it changes:

Hardware Commit / workload Change
MI355X 1: K/V streaming, 96 cases geomean 2.67x (BF16 3.88x, FP8 1.83-2.07x), min 0.984x
MI355X 2: MTP query split, 164 cases geomean 1.19x (UT 1.51x), min 1.004x
MI355X 3: sparse split-major, 166 cases geomean 1.13x (grid 1.18x, UT 1.00x), min 0.982x

By family over all 817 cases:

  • BF16: grid 1.35x, UT 1.41x;
  • FP8 per-tensor: grid 1.11-1.16x, UT 1.21-1.33x;
  • FP8 per-token: grid 1.09-1.11x, UT 1.03-1.06x.

Known regressions: no changed case is below 0.98x. Three unchanged cases read 0.974-0.976x; their binaries are identical, so those readings are noise.

Testing

  • Unit tests added or updated
  • Performance benchmarks run
  • Tested on MI250X
  • Tested on MI300X

Results:

No unit test is added: the existing cases reach every new gfx950 specialization. Not tested on MI250X (gfx90a is unsupported by this kernel), MI300X (304 CUs) or MI325X.

Documentation

  • Docstrings updated
  • User guide updated
  • Performance guide updated
  • No documentation change is needed, with the reason stated below

The API, the plan and the set of supported shapes are unchanged; the change is internal to the gfx950 tile schedule.

Dependencies

  • No new third-party dependencies added
  • New dependencies are listed and justified below

Breaking Changes

None

PR Self-Review

Reviewed revision

cbffdd2

Behavior and intent

On gfx950 the pa_decode tile schedule now uses three policies #7709 introduced for gfx942:

  • K/V streaming for long head rows removes the next-tile K prefetch spill.
  • The MTP query split spreads one sequence's query positions over idle CUs.
  • Split-major launches stop small grids' later splits and heads from queuing behind empty plan slots.

Each is re-tuned on MI355X where the gfx942 rule lost: per-token FP8 keeps its fused MTP4 tiles, the FP8 split is bounded by the uncapped tile count, and split-major is limited to sparse grids and to FP8 or BF16 up to D128. Without these policies, gfx950 spilled at D512/D1024 and left CUs idle under MTP (1.19x geomean over the matrix). gfx942 and gfx1250 binaries are unchanged.

Validation and unverified scope

Evidence on MI355X:

  • pytest op_tests/test_flydsl_pa_decode.py: 355 passed, 37 skipped. The negative control with commit 3's axis fixes reverted fails 9 cases.
  • Paired in-process A/B: per commit and end-to-end over all 817 matrix cases. Geomean 1.19x, no changed case below 0.98x, 0 errors, 0 non-finite.
  • Determinism: every one of the 333 changed cases repeats bit-exactly over 20 launches (det_check.py).
  • Accuracy: 19 changed cases are not bit-exact against the base (streamed accumulation order). On each, the max error against the fp32 reference equals the base's.

Evidence on gfx942:

  • pytest on MI308X: 285 passed, 107 skipped.
  • Compile-only replay (COMPILE_ONLY=1) of every matrix case: gfx942 and gfx1250 binaries identical to 03894b767e.

Unverified:

PR-specific question

op_tests/test_flydsl_pa_decode.py is the only changed op test in this diff, and its change comes from #7700 (0a4638a355). It is not one of this PR's three commits.

This PR's own commits change no test. Their main boundary is the split-major grid on gfx950: it swaps which block-id axis names the plan slot. Any gfx950-only path that still assumes slot-major axes silently reads the wrong slot or head. is the split-major grid on gfx950: it swaps which block-id axis names the plan slot. Any gfx950-only path that still assumes slot-major axes silently reads the wrong slot or head.

Two such paths existed:

  • the BF16 D128 planned output's slot count (grid_dim.x);
  • the one-partition empty-row zero fill (block_id("y")).

Opening split-major on gfx950 without fixing them produced relative errors of 1-19 on BF16 D128 two-head shapes. Both now select the axis from SPLIT_MAJOR, and their IR is unchanged when it is off. The remaining direct block_id/grid_dim reads are gfx1250-only pipelines, or the MXFP4 direct path, which keeps slot-major. Evidence: with the two fixes reverted, the existing UT fails 9 cases; with them, all pass; every changed case is deterministic and matches the base's fp32-reference error.

The second boundary is the cache key. The renamed busy flags keep their positions ahead of D256_PIPE, so create()'s positional unpacking and the test's cache_key[-2:] are unchanged; the pipe-policy tests pass.

Changed op tests

op_tests/test_flydsl_pa_decode.py: changed by #7700 (0a4638a355), not by this PR's three commits. Its plan-structure check now accepts gfx950's balanced min-max planner, asserting each sequence's task count equals min(ceil(tiles / T), max_partitions) for the smallest feasible bound T. It is not redundant, because no other test checks the plan's task layout.

This PR's commits add no test, because the existing test_pa_decode cases reach every new gfx950 specialization: the head1024/head512/head192 streamed rows, the MTP4/MTP8 -direct, -merge, -probability_tail and window cases for the query split, and the -direct/-cluster cases for split-major. The negative control shows they fail when the split-major axis fixes are missing.

🤖 Generated with Claude Code

fsx950223 and others added 30 commits November 10, 2026 03:48
… load scheduling, lean FP8 MTP4

gfx950-only tile-kernel changes, each behind a traits flag that requires is_gfx950:

- BF16_MTP_FUSED: BF16 multi-query (MTP4 verify) page-128 D128 tiles keep Q in padded LDS rows,
  give every M-tile a private P/Lsum slot (one publication barrier), carry V across M-tiles and
  pair K16 packs into gfx950's K32 BF16 MFMA. The resident set fits 256 VGPRs (2 CTAs per CU).
  The task's last tile is peeled and carries the V tail mask; unowned KV rows stay masked.
- BF16_DECODE_NT_KV / DECODE_Q_EARLY / DECODE_LAST_V_AFTER_QK: BF16 single-M decode streams K and
  V nontemporally, issues Q ahead of the page ids, and loads the last tile's V right after QK.
- FP8_SCALES_AFTER_Q / FP8_LOOP_SCALES_LATE: per-token FP8 page-128 tiles stage scales so their
  vmcnt waits stop draining K and V ahead of the softmax.
- FP8_MTP4_LEAN / FP8_MTP4_Q_LDS / FP8_MTP4_PAGES_AHEAD: FP8 MTP4 verify folds the K scale and the V
  normalization into exp2 and the PV MFMA's P scale, rereads Q from LDS and carries the next
  tile's page ids, with waves_per_eu=2.
- The gfx950 BF16-KV tile launch preloads its leading kernel arguments.

gfx942 and gfx1250 code paths are unchanged (gfx942: every compiled PA kernel bit-identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in one rung

On gfx950 the dual-mode reducer replaces the (1, 2, 4, 8, 16, 32) rung ladder with one
32-partition rung. LLVM hoisted the ladder's shared leading loads above the count branch,
waited on them there and trickled the rest in small batches: 32 live partitions took several
serial L2 round trips. One rung issues every load at once; partitions past the live count read
zeros from the per-sequence buffer bound. Above 32 live partitions, each group's loads now issue
before the weight barrier, as on gfx1250.

Trade-off: sequences with few live partitions now pay the 32-load rung (see the PR description).
gfx942 and gfx1250 are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-16 tiles

gfx950 scalar-scale (per-tensor) FP8 page-16 single-M tiles now take the existing V-prefetch pipe
(PAGE16_VPIPE) that per-token FP8 page 16 already uses; before, only page 128 qualified, so page-16
tiles issued V after the softmax barriers. Single-CTA page-16 cost drops from ~2.7 us per 256-token tile.

Paired in-process A/B vs the previous head on MI355X (FlyDSL 0.5.0): 32 page-16 review shapes 1.035x
geomean (single-CTA 30K context 1.53x, H8 B32 1.18x), outputs bit-exact; other paths unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gfx950 BF16 page-16 single-M decode tiles take the V-prefetch pipe as well (they had no V
prefetch: ~3.7 us per 256-token tile in a single CTA).

Paired A/B on MI355X: single-CTA 30K context 1.58x, random-length B16 1.04x, B1 short contexts
+1-2%, large grids -0.4..-0.8%; outputs bit-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…128 decode

gfx950 FP8 single-M page-128 decode loads V with nontemporal flat loads (FP8_DECODE_NT_V), as BF16
decode already streams K and V. K stays cached: streaming K as well slowed the short single-tile
tasks, and on page 16 streaming fights the V-prefetch pipe (single-CTA 30K -21%).

Paired A/B on MI355X over the FP8 page-128 decode shapes: 1.033x geomean (b64 32K 1.18x, b4 32K
1.13x, b32 8K 1.05x), no shape below 0.998x; outputs bit-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d of rebuilding them

pa_decode built a fresh one-element scale tensor on every call for scalar FP8 KV scales
(torch.ones for None, torch.tensor for a Python float). Under a CUDA graph each is a captured fill
or host copy: a scale-less FP8 call ran 4 dispatches instead of 2. The scale is now one persistent
tensor per (device, value), cached only when built outside graph capture (a captured fill has not
run yet and lives in that graph's pool), so captured-first calls keep working.

Host-only; kernels unchanged on every architecture. Paired A/B on MI355X over the 51-shape review
set: 1.245x geomean; short scalar-FP8 calls 9.9 -> 6.6 us; outputs bit-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from the tile kernel

A plan with max_partitions == 1 gives every sequence at most one task (capacity == batch), yet gfx950
FP8/BF16 still wrote packed partials and launched the reducer to copy them out. The tile kernel now
stores the normalized rows itself, as gfx1250 and MXFP4 already did: slot s runs packed task s and
zero-fills sequence s when it owns no task (reduce_info count 0), so the second dispatch and the
partial scratch round trip go away. Sinks and strided outputs keep the reducer. The direct store
takes the query's row/head strides for any query length.

Paired A/B on MI355X: W6 fp8 page-16 B64 mp1 1k 10.55 -> 8.65 us (1.21x), W5 bf16 B64H8 mp1
1.07x, R7 single-CTA 30k 1.01x; outputs bit-exact; other shapes compile the same kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… V; FP8 issues next K after QK

gfx950 scalar-scale page-16 tiles issue this tile's V at tile start, then load the next tile's page ids
from the block table and stage them in LDS. In-order vmcnt makes that staging wait drain V as well,
so V overlapped only QK, and the next K went out only after the P barrier, with PV left to hide it.
The next page ids now load ahead of V (they are masked past the task and staged only under the
existing next-tile guard), and FP8 issues the next K from them right after QK. BF16 keeps K after
the P barrier: its next K needs twice the registers and was 4% slower per tile.

Single-CTA cost per 256-token tile on MI355X: FP8 1.693 -> 1.412 us, BF16 2.227 -> 2.152 us.
Paired A/B: R7 single-CTA 30k 1.20x, R5b budget=CU 32k 1.16x, W6 B64 mp1 1.10x, R1 128k 1.04x,
P2 H8 B32 1.03x; outputs bit-exact; other shapes within +-1% (re-measured candidate-first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e bound

The planner gives every nonempty sequence one task, then spreads the other slots in proportion to
tiles. With mixed lengths and about two slots per sequence the long sequences keep the longest tasks:
16 tiles on a P2 H8 B32 random 1k-8k batch whose average is 8.5, so the kernel ends on that tail. On
gfx950, single-block plans now give a sequence of t tiles min(ceil(t / T), max_partitions) tasks,
with T the smallest bound from ceil(total / capacity) up that fits the capacity. A binary search over
[ceil(total / capacity), ceil(total / (capacity - rows))] finds it; one block reduction per halving,
int32 math, skipped for full plans. Equal-length batches keep their task size and may use fewer
slots, which also shortens the reducer.

Paired A/B on MI355X (decode only, plans built outside the graph): H2 D256 H8 B32 1.16x, P2 B32
1.07x, H1 B32 1.05x, MiniMax b3 225k four shapes 1.02-1.05x, P4 B8 1.03x; P1 B128 0.99x; 87-shape
geomean 1.006-1.008x. Planner refresh +0.06..0.51 us. test_pa_decode_plan asserts the min-max
counts when the planner compiles the balanced variant and the proportional split otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion

The one-partition direct output generalized the direct store's address to query lengths above 1.
Single-query kernels, including the MXFP4 plan-free ones that already stored directly, now keep the
original expression, so their generated code stays as it was; only longer queries take the
position/head split. Paired A/B: W3/W4 MXFP4 1.000x/1.001x, W5/W6/R7 within noise, outputs
bit-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gfx942

On gfx942 the D1024 FP8/BF16 and BF16 D512 MTP tile kernels hold the whole
next-tile K prefetch and the tile's whole V gather in registers. They hit
512 VGPRs and spill 350-2000 registers to scratch.

Where a lane's K row reaches 384 bytes (BF16 D192+, FP8 D512+; with
several M-tiles, 1 KiB), the tile now carries only page ids. K is
streamed in pieces of four head groups (two in flight) and V in head
chunks (four in flight), with a scheduling fence after each chunk. M-tiles
pack P into private slots behind one barrier and share every K/V chunk.
Spills drop to zero. gfx950 and gfx1250 binaries are unchanged.

Paired in-process A/B vs main on MI308X (gfx942, 80 CU):
- 70 D1024 cases: 1.87-6.14x, all bit-exact
- BF16 D512 MTP4: 2.30-4.18x, same fp32-reference error
- BF16 D256 page16: 1.28-1.41x at batch >= 16, 0.81-0.94x at batch 1
- BF16 D192 page64: 1.21-1.27x at batch >= 16

107 cases: geomean 2.48x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nes use DPP

The reducer's softmax weights reduce each row's partition max and sum across lanes with shuffle_xor.
On gfx950 the 8/4/2/1-lane steps lowered to ds_swizzle, an LDS crossbar round trip with a full
lgkmcnt wait each: eight serial ~44-cycle waits per reducer wave, on the critical path of every
short call. Those steps now use the 16-lane DPP xor patterns (the same partner lane, so results are
bit-identical); the 16/32-lane steps keep permlane swaps. Other architectures are unchanged.

Paired A/B on MI355X over 87 shapes: 1.0069x, all outputs bit-exact; short calls 1-3% (MiniMax
fp8/bf16 b1 decode, R1/R5b/H1 page 16), the lowest shapes within +-0.4% when re-measured alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gfx9 tile issues a single M-tile's V loads only after the P barrier, and
PV consumes them right away. The load latency therefore sits on every tile's
critical path. ATT on MI308X showed the PV MFMAs waiting on vmcnt one V load
at a time. gfx950 already prefetches V at the tile start for its tuned FP8
shapes; gfx942 never did.

On gfx942, single-M-tile FP8 (up to D256, any page size) and BF16 page16
D128 now take the existing prefetch_v schedule. V is loaded at the tile start
and overlaps QK and softmax. BF16 page128 and BF16 D64 measured neutral and
are left as they were; BF16 D192+ streams instead. gfx950 binaries are
unchanged.

Paired in-process A/B vs c52a931 on MI308X (gfx942, 80 CU):
- 135 single-M-tile grid cases: geomean 1.052x
  - FP8 page16 D128: 1.02-1.42x
  - BF16 page16 D128: 1.04-1.21x
  - FP8 page128 vectorized V at batch 1: 0.98-0.99x
- 111 unit-test cases: geomean 1.024x, worst 0.96x

Per-token outputs move by at most 1e-6 relative; the max error against the
fp32 reference is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rids

When more CTAs than CUs hold work, occupancy matters more than one CTA's
latency. When fewer do, the reverse holds. The schedule now records
whether the host-bounded busy-CTA count (the existing split_workgroups bound
times query splits) exceeds the CU count, and uses that on gfx942:

- FP8 D128 MTP tiles (2-4 M-tiles) need 260-272 VGPRs, i.e. one wave per
  SIMD. On busy grids they compile with waves_per_eu=2. That caps them at
  256 VGPRs at the cost of about ten spills, and two CTAs share each CU.
- Single M-tile K/V streaming between 384-byte and 1 KiB K rows (BF16
  D192/D256) now runs only on busy grids. On small grids the K prefetch it
  gives up was the latency that mattered: BF16 D256 batch 1 was 0.81x vs
  main. From 1 KiB rows, kernels always stream, because the unstreamed
  kernels spill.

gfx950 and gfx1250 binaries are unchanged.

Paired in-process A/B vs a5a617e on MI308X (gfx942, 80 CU), 310 cases:
geomean 1.077x.
- FP8 D128 MTP4/5/8 at batch >= 16 (or batch 64 ctx1K): 1.13-1.40x
- BF16 D256 page16 batch 1 ctx 1K/8K: 1.23x, back to 1.0x vs main
- Small grids: 0.97-1.00x. The worst is a long-row UT case
  (mtp4-g16-page128-varied), which is busy by the host bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tition decode

A one-partition plan gives each sequence a single CTA that walks all of its tiles; on sparse grids
that is a latency chain of four waves, one per SIMD. On gfx950, when the grid has at most one CTA
per CU, single-query decode tiles (scalar FP8 or BF16, no window, not the head-256 pipe) now launch
eight waves as two four-wave groups. Group g walks tiles start + g, + 2, ... with its own copy of the
LDS layout; both run the same iteration count (group 1's last tile may lie past the task and is fully
masked), so their CTA barriers pair up. After the loop group 1 publishes its (O, max, sum) per lane
and group 0 rescales both to the shared maximum and stores, directly or as the task's partial.
The grid condition joins the schedule key ahead of its last two fields, which tests read.

Single-CTA cost per 256-token tile on MI355X: FP8 page 16 1.42 -> 1.15 us, BF16 page 16 2.15 ->
1.54 us, FP8 page 128 1.69 -> 1.41 us. Paired A/B: R7 single-CTA 30k 1.19x, W6 B64 mp1 1.04x; all
other shapes compile the same kernels. FP8 P is quantized against each group's running maximum, so
those outputs move within FP8 rounding (max 3.4e-2 of the output max).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… when both LDS copies fit

MXFP4 tiles have no per-tile CTA barrier: each wave owns 64 tokens and the four waves merge once
after the loop, so a plan-free sequence is one CTA's latency chain. The dual-group CTA now covers it
when two copies of the MXFP4 layout (BF16 V staging and staged page ids) fit gfx950's 160 KiB of
LDS. The groups merge lane-wise (O, max, sum) before group 0's four-wave merge, and a barrier keeps
that merge's LDS writes behind every lane's read of the merge slots, which share the space.

Paired A/B on MI355X: W3 MXFP4 plan-free 64k 316.5 -> 226.2 us (1.40x); W4 (a 512k table stages
4096 page ids, so it keeps one group) unchanged; every other shape compiles the same kernels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… max

Every FP8 tile quantizes Q to e4m3 per row, first reducing |q| over each lane's 8-element BF16/FP16
unit. LLVM widened every element to f32 for the max: about 30 VALU per unit on the prologue's
critical path, 120 per lane for an MTP4 tile. A 16-bit float's magnitude orders like its
sign-cleared bit pattern, so on gfx950 the unit now reduces as a packed 16-bit integer max over
sign-cleared words, with one conversion of the winner. The result is the same value.

Paired A/B on MI355X over 87 shapes: 1.0045x, all outputs bit-exact; FP8 MTP4 verify b3 32k 1.03x,
short page-16 FP8 calls ~1-2%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de tiles

Single-query decode without a sliding window can only see tokens past a row's bound in a task's last
tile, which the pipeline already peels for the V tail mask. The head-256 pipe still compared and
selected every score chunk of every tile. Interior tiles now skip that mask (single_scores and
pack_single take the tile's mask flag); the peeled tile and every other configuration keep it. On
the page-16 V pipe the masks are free and the change measured 1.5% slower, so it is left out there.

Paired A/B on MI355X: X2 D256 page-64 per-token B8 mp1 1.12x, X1 B1/B8 1.06x/1.05x, X3 page 16
1.04x; 87-shape geomean 1.0015x. Without the select, the multiply contracts into the exp's FMA, so
head-256 outputs move within rounding (max 2.4e-3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h integer DPP max

After each lane's |q| max, the row's 16 lanes combined theirs with four xor DPP steps of maxnumf.
Each step cost a copy, one or two v_mov_b32_dpp, an s_nop and a canonicalizing v_max_f32 besides the
max: about 22 VALU per M-tile on the prologue's critical path. The magnitudes are non-negative, so on
gfx950 the reduction now runs on their bits with integer max. Rotate-by-8 and rotate-by-4 within the
row and the two quad swaps cover all 16 lanes, each a single full-mask DPP pattern that LLVM folds
into the max. The result is the same value.

Paired A/B on MI355X over 87 shapes: 1.0024x (FP8 shapes 1.0037x), all outputs bit-exact; FP8 MTP4
verify b3 32k 1.025x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es' page ids

Plan-free MXFP4 stages the task's page ids in LDS, up to 4096 of them when the table is long. Two
copies of that next to two BF16 V staging areas exceeded gfx950's 160 KiB, so long tables kept one
tile group. A dual group only walks tiles part_start + 2j, so it now stages just those ids (each tile's
pair contiguous) and looks a tile up at ((tt - part_start) / 2) * pages_per_tile; the page region of
each copy halves and both copies fit.

Paired A/B on MI355X: W4 MXFP4 plan-free 2k context in a 512k table 14.10 -> 12.53 us (1.13x); W3 and
every other shape unchanged and bit-exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…head of the page ids

Single-M FP8 page-128 decode with a full 16-row tile already issues Q before the page ids and stages
it while K is in flight; scalar page-16 tiles still loaded Q after them. Page 16 now takes the early
path too, including partial tiles (GQA 8): a padded row's early load reads the last real row so it
stays inside the query, and the existing runtime row guard never stages it.

Paired A/B on MI355X over 87 shapes: 1.0046x, all outputs bit-exact; FP8 page-16 shapes 1.010x
(short calls up to 1.03x).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6 page-16 decode

The scalar head-256 pipe and BF16 page-16 single-M decode tiles now issue Q ahead of the page ids
and stage it while K is in flight, as FP8 head-128 decode does (partial tiles load the last real row
for their padded rows). Per-token head-256 keeps Q after its scale loads: its prologue orders the
scales ahead of K, and early Q cost it 10%.

Paired A/B on MI355X over 87 shapes: 1.0023x, all outputs bit-exact; short head-256 calls 1.04-1.05x
(P3 B1/B8, W1 B1, R5 4k), BF16 page-16 R2 up to 1.025x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff 0.16.0, the version CI pins, flags the dpp_utils import placed after tensor_shim (I001).
Import order only; no behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Black wrapped the condition, which left the noqa on the closing line; ruff 0.16.0, the version CI
pins, then reports SIM102 and an unused noqa. Comment placement only; no behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #7388 (FlyDSL-dtype tile kernel keys, no batch-first plan grid). This branch's traits and
kernels now use its names: kv_dtype/query_dtype FlyDSL types, OUT_DTYPE, KV_COMPUTE_BLOCK and
MXFP4_KV_DTYPE in place of MXFP4/is_bf16_kv/Q_IN_DTYPE/Q_DTYPE/KV_DTYPE/TILE_TOK; the gfx950
one-partition direct-output allowance moves into the schedule's direct_output check with #7388's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gfx9 prologue issued a CTA's Q loads after the first tile's page-table
and K loads. Q staging (quantize, LDS write, barrier) therefore waited on the
in-order vmcnt chain for every K load: ATT on MI308X showed a 1056-cycle
wait in a 9500-cycle single-tile task.

gfx942 now issues Q loads first, as gfx950 already does for MTP4-fused and
MXFP4 kernels. Padding rows of a partial last M-tile load the last real row
(clamped index) and staging still zero-fills them. Streamed kernels start
with K pieces and keep their order. gfx950 binaries are unchanged.

Paired in-process A/B vs 9428a09 on MI308X (gfx942), 111 unit-test
cases: geomean 1.016x (FP8 1.01-1.05x, BF16 1.00-1.03x, streamed D1024
0.98-0.99x before excluding them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gfx942

Tiles that load V at the tile start (V prefetch, MTP M-tiles) issued the
next tile's block-table read only later, behind those V loads. Its
pin-to-context select then waited on vmcnt(0), i.e. for all of V; ATT on
MI308X showed about 1000 cycles of a 4400-cycle FP8 page16 tile spent
there.

The page-table read now issues before the V loads, fenced so the scheduler
keeps that order, and the pin happens at the use. The wait drops to
vmcnt(8). Applied on gfx942 to FP8 per-tensor single M-tiles and to MTP
tiles, except BF16 with two M-tiles. gfx950 binaries are unchanged.

Paired in-process A/B vs the previous head on MI308X (gfx942):
- FP8 per-tensor single M-tile: up to 1.18x (page128 plain V); page16
  1.03-1.07x at batch 64
- MTP M-tiles: 1.00-1.12x
- Excluded: FP8 per-token and BF16 page16 single tiles (0.95-0.98x) and
  BF16 two-M-tile MTP (0.95-0.98x)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…busy gfx942 grids

V prefetch at the tile start keeps the V registers live through QK and
softmax. For FP8 per-tensor page128 with a vectorized V cache, that cost more
than the hidden latency on small grids: 0.955-0.967x on the latency-bound
unit-test shapes. It still gains 1.11-1.15x on busy grids. Such tiles now
prefetch only when more CTAs than CUs hold work. Per-token, plain-V and
page16 tiles are unchanged.

Paired in-process A/B on MI308X (gfx942): small_query/probability_tail
page128 UT cases 1.04x, busy grids unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…D256 on gfx942

A streamed BF16 D256 page16 tile needs just over 256 VGPRs, so it ran one
wave per SIMD. Capped at 256 with waves_per_eu=2 (254 VGPRs, no spills),
two CTAs share each CU. That only matters on busy grids, where this path
is selected.

Paired in-process A/B on MI308X (gfx942): BF16 D256 page16 B64 ctx32K 1.38x
(main 0.94x before, now 1.30x vs main); other sizes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… grids

MTP tiles with two M-tiles and K rows of 256 bytes or more need 272-324 VGPRs
(FP8 D256 GQA8 MTP4, BF16 D128 GQA8 MTP4). That leaves one wave per SIMD.
On busy grids they now take the streamed multi-M-tile schedule from
c52a931: K and V stream per chunk and both M-tiles share every chunk.
That frees the registers held by the next-tile K prefetch and the tile's V
gather, and two CTAs fit per CU. Small grids keep the prefetching schedule.

Paired in-process A/B on MI308X (gfx942), busy sizes (batch >= 16, or batch 1
with a 32K context):
- FP8 D256 page128 GQA8 MTP4 (2 KV heads): 1.18-1.28x
- FP8 D256 page64 GQA8 MTP4 per-token: 1.23-1.52x
- BF16 D128 page64 GQA8 MTP4: 1.17-1.20x
Max error against the fp32 reference is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fsx950223 and others added 20 commits November 10, 2026 09:38
… undefined

fp8_words packs four f32 values into one word with two cvt_pk_fp8_f32:
the first writes the low half and the second the high half. The first
conversion's tied old word was a literal 0, so every packed word cost a
v_mov_b32 0 ahead of it (22 per FP8 MTP4 verify tile, on the P path).
On gfx950 that old word is now undef; the result is bit-identical.
gfx942 keeps the zero, so its kernels are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts first loads' and preload them

The reducer's first load is reduce_info, whose pointer sat at byte 0x50 of
the kernel arguments; on gfx950 nothing was preloaded, so every call
waited on a kernarg round trip before it could fetch its partition range.

gfx950 now orders the reducer's arguments as reduce_info, the statistics
and partials pointers, their strides, query_rows and num_kv_heads, which
are the 14 dwords that preload into user SGPRs, and enables the preload.
reduce_info is then fetched at once, with the remaining arguments in the
same round trip. The kernel body is shared; gfx942 and gfx1250 keep their
argument order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CTA runs all query positions of an MTP row together. On small grids (few
sequences, short contexts, sliding windows) most CUs are idle, and the
CTA's serial latency (four M-tiles of QK, softmax and PV per tile) is the
decode time. On gfx942 the schedule now splits the query positions over
more CTAs when they fit; each re-reads the row's K/V, mostly from L2:
- FP8 up to D128: one M-tile per CTA (4-way) while 4x the host-bounded busy
  CTAs fit about three per CU, else 2-way while two per CU fit.
- BF16: 2-way while one per CU fits.
Busy grids keep the unsplit occupancy schedules. Outputs and their fp32-
reference error are unchanged, since rows are independent.

Paired in-process A/B vs 9428a09 on MI308X (gfx942):
- 111 unit-test cases: geomean 1.19x (MTP4/8 UT cases 1.4-2.4x)
- 180 MTP grid cases: geomean 1.056x (batch 1 up to 1.45x; busy grids 1.00x)
FP8 D256 (0.93-1.05x) is excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…page64 on gfx942

FP8 per-token D256 page64 single tiles with V prefetch take the per-token
M1 schedule, which was tuned on gfx950. On busy gfx942 grids it was
0.88-0.93x vs main (B16 ctx32K, B64 ctx8K/32K). On small grids it gains
1.01-1.07x. Busy grids keep the unprefetched schedule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sk slot per CU

A multi-query (MTP) tile quantizes and stages all of its query rows and
publishes their maxima before any KV work, a cost paid once per task. With
the default budget of two slots per CU, MTP4 verify ran twice as many,
shorter tasks, and the reducer merged twice as many partials.

On gfx950, plans with query_length > 1 and no explicit workgroup_budget now
default to one slot per CU. Paired over the 14 MiniMax-M3 MTP4 verify
workloads: 12 faster (up to 1.14x), FP8 b3 225K 0.972x and b16 32K 0.993x;
all 87 shapes 1.0084x. Explicit budgets, other architectures and
single-query plans are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tile grid is (plan capacity, KV heads x query splits). Dispatch walks x
first, so split 1 of task 0 dispatches only after every slot of split 0,
padding slots included. On small grids, where the plan has many more slots
than tasks, the splits ran in sequential waves of mostly empty CTAs. On
large grids a task's splits ran far apart, each re-reading the task's K/V
from HBM.

On gfx942, split kernels now launch (KV heads x query splits, capacity), so a
task's splits dispatch together. They run concurrently and share the K/V
they re-read. The cache key records the choice. BF16 splits forced by the LDS
limit at large head dims only use it when tasks own at least four tiles; with
fewer, the split-major grid measured 0.93-0.97x.

Paired in-process A/B vs 8f9fe39 on MI308X (gfx942):
- 180 MTP grid cases: geomean 1.092x
  - FP8 MTP4/8 batch 1: 1.04-1.45x
  - FP8 sliding-window MTP4 batch 1/16: 1.28-1.63x
  - BF16 D1024 MTP batch >= 16 with ctx >= 8K: 1.10-1.26x
- 111 unit-test cases: geomean 1.018x (MTP4/8 and window cases 1.16-1.20x)
gfx950 binaries are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or on small grids

The same dispatch-order effect as query splits: with grid (capacity, KV
heads), a second KV head's tasks dispatch only after every slot of the
first head, including the plan's empty padding slots. On small gfx942 grids
the planned capacity far exceeds the tasks, so heads now launch head-major
there. Busy grids keep the slot-major order, which measured slightly better
for them (0.93-0.98x head-major).

Paired in-process A/B vs ece6342 on MI308X (gfx942), two-KV-head
geometries: batch 1 ctx 1K/8K 1.11-1.29x (FP8 D64 MTP3, FP8 D256 page16,
BF16 D64 MTP4); other grid sizes and the unit-test cases unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngs after the weight barrier

Above 32 live partitions the gfx950 reducer issued each group's partial
loads before the weight barrier and held them in registers until then.
With one partition group, which large batches get, that rung spans all
256 partitions: the kernel compiled to 266 VGPRs, one wave per SIMD, and
every partition count ran at that occupancy. Batches of 128-256 sequences
with 2-8 partitions each were 4-9% slower than main.

gfx950 now issues loads ahead of the barrier only when a group's rung is
at most 64 partitions; a single group keeps main's order and reuses the
LDS allocated up front. That kernel drops to 51 VGPRs. Outputs are
bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rids

A sliding window bounds every MTP task to a few KV tiles. The task's serial
latency therefore stays the decode time even on grids the idle-CU rule
counts as busy. Windowed MTP tiles on gfx942 now split FP8 into single-M-tile
CTAs on any grid, and BF16 two-way while four split CTAs fit per CU.
Unwindowed rows keep the idle-CU thresholds.

Paired in-process A/B vs db5c85c on MI308X (gfx942), 70 windowed MTP4
cases: geomean 1.045x (grid 1.050x)
- BF16 window 257 batch 16: 1.32-1.34x
- BF16 window 700 batch 64: 1.12-1.18x
- FP8 per-tensor vectorized V batch 64: 1.18-1.20x
Unit-test cases unchanged. The max error against the fp32 reference is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BF16 D64 rows are 128 bytes, the same as FP8 D128, and their split CTAs
stay small. BF16 D64 MTP rows now split two-way while the unsplit busy CTAs
fit one per CU, as FP8 does, rather than only while the split ones do.

Paired in-process A/B vs 922fde0 on MI308X (gfx942), BF16 D64 MTP
geometries:
- UT mtp4-g16-page128-cluster-d64: 1.38x
- B1 ctx32K and B16 ctx1K: 1.12-1.13x
- everything else 0.99-1.01x
41 cases: geomean 1.017x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… per CU on gfx942

waves_per_eu=2 trades spills (FP8 D128 MTP) or nothing (streamed BF16 D256)
for a second resident CTA per CU. That only pays when the host-bounded busy
CTAs fill two per CU. Between one and two per CU it only costs spills: the
long-row UT case mtp4-g16-page128-varied (6 rows up to 4100 tokens) ran at
0.94-0.97x vs main. The cache key gains the two-per-CU flag (before
D256_PIPE).

Paired in-process A/B vs 26f6401 on MI308X (gfx942):
mtp4-g16-page128-varied 1.01-1.10x; grid MTP and BF16 D256 cases unchanged
(0.99-1.01x).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four BF16 D128 M-tiles per CTA hold 360 VGPRs: one wave per SIMD. With
split-major launches (ece6342) a two-way split no longer re-reads K/V
from HBM, since a task's two CTAs dispatch together and share it. Each CTA
then holds two M-tiles and, on busy grids, takes the streamed two-M-tile
schedule at two CTAs per CU. Busy BF16 D128 MTP rows with at least four
M-tiles now split two-way. D192 (0.76-0.83x) and two-M-tile rows
(0.85-0.96x) measured slower and keep their schedules.

Paired in-process A/B vs df92172 on MI308X (gfx942): BF16 D128
MTP4/MTP8 page128 at batch >= 16: 1.03-1.10x; other BF16 MTP cases
unchanged. 92 cases: geomean 1.020x. Max error against the fp32 reference
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on gfx950

The previous commit gave gfx950 multi-query plans one task slot per CU
by default. With 128-256 sequences that leaves one or two tasks per
sequence, so uneven lengths can no longer be split: on MiniMax-M3 MTP4
verify with lengths uniform in 1K-8K, FP8 B256 ran 0.96x of main and
BF16 B256 1.25x instead of 1.66x.

The default budget is now four slots per sequence and KV head, clamped
to one to two slots per CU. Batches up to 64 sequences keep one slot per
CU (all 14 fixed-length MTP4 verify workloads plan exactly as before);
larger batches return to two. Random-length MTP4 verify at B64-B256:
1.17x-1.61x of main (FP8 B256 1.32x).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Union of both sides in the pa_decode tile kernel. #7709's policies stay
gfx942-only and #7700's paths gfx950-only:

- traits: the cache key carries #7700's SPARSE_GRID and #7709's busy,
  two-per-CU and split-major flags ahead of D256_PIPE; prefetch_v keeps both
  arches' rules; P_BUFFERS gives each M-tile a slot for MTP4_FUSED,
  D256_PIPE, BF16_MTP_FUSED or STREAM_KV.
- pipeline: tile() issues both next-page lookaheads (PAGE16_PAGES_FIRST,
  PAGES_EARLY). single_query_tile takes #7709's STREAM_KV K/V branch, else
  #7700's late-scale / V-after-QK path with PAGES_EARLY ahead of
  REUSE_KV_PAGES.
- op_lds: early Q covers MTP4_FUSED, DECODE_Q_EARLY, GFX942_Q_EARLY and
  MXFP4; the padded-row clamp takes #7700's form.
- launch: the split-major grid with CTA_THREADS blocks, the gfx950 kernarg
  preload, and waves_per_eu=2 for FP8_MTP4_LEAN or WAVES_PER_EU_2.

Compiled binaries: gfx950 and gfx1250 match #7700+#7701; gfx942 matches
#7709 except the padded-row kernels, whose clamp compares against
CTA_ROWS - 1 instead of CTA_ROWS (same rows selected).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re restaging

STREAM_KV tiles stage the next tile's page ids in LDS at the start of the
tile, so the page-table wait does not drain the K stream. The first tile's
ids are staged in the prologue and read back by every wave after its
barrier, but nothing ordered that read before the first tile's restaging:
a fast wave could overwrite the ids while a slow wave had yet to read them,
and the slow wave then gathered the first tile's V from the second tile's
pages. Later tiles are ordered by the tile barriers. Streamed tasks now
take one barrier after the read.

The race showed as run-to-run output differences of up to 1e-3 on FP8
per-tensor D256 MTP4 two-head busy grids (1-2 of 40 runs on gfx942; 8-16
of 20 on gfx950 with streaming enabled). After the fix, every streamed
case repeats bit-exactly; the cost is 1.0005x geomean over 161 streamed
gfx942 cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The barrier follows the first tile's page-id read in the merged prologue,
ahead of #7700's Q-operand selection. STREAM_KV is gfx942-only in this
stack, so gfx950 and gfx1250 binaries are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gfx950 pa_decode has the same register problem #7709 solved on gfx942: a
lane holding a whole next-tile K prefetch plus the tile's V gather spills
once its K row reaches 1 KiB (BF16 D512+, FP8 D1024), and single or
paired M-tiles with 384/256-byte rows drop to one wave per SIMD on busy
grids. Let STREAM_KV select on gfx950 too, outside the fused BF16 MTP and
head-256 pipes, which keep their own schedules; streamed tiles skip the
dual-group CTA. The busy-grid flags it reads are now arch-neutral
(renamed busy_grid / two_per_cu); every other consumer stays gfx942-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#7709's gfx942 query split also pays on gfx950: when few CTAs hold work,
per-CTA latency dominates, and splitting the MTP query positions over
more CTAs (re-reading K/V from L2) shortens the critical path. Apply it on
gfx950 with two differences found by measurement:

- per-token FP8 keeps its fused MTP4 tiles (splitting them ran 0.75x);
- gfx950 MTP plans fix capacity at one or two slots per CU regardless of
  load, so a capacity bound no longer means idle CUs. The FP8 split also
  requires at most eight KV tiles per CU (tile_demand, the uncapped
  sequence x head x tile count); B64/32K per-tensor ran 0.87x without it.

The BF16 D128 split for busy grids stays gfx942-only (0.6-0.9x on gfx950).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#7709 launches gfx942 query splits and multi-head grids split-major, so a
slot's splits (or a small grid's heads) dispatch together instead of
queueing behind every empty plan slot of the previous split. On gfx950 the
order only pays while at most a quarter of the CUs hold work; on fuller
grids slot-major order measured up to 1.25x faster, so gfx950 takes it
only below that bound, for FP8 and for BF16 up to D128 (LDS-forced BF16
D192/D256 splits ran 0.91-0.93x). MXFP4 keeps slot-major.

Two gfx950-only paths assumed the slot-major axes and now follow the grid:
the BF16 D128 planned output's slot count (grid_dim.x; with a split-major
grid it read heads x splits and stored partials at wrong offsets) and the
one-partition plan's empty-row zero fill (block_id("y") as the KV head).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

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

Extended tests (opt-in via labels):

Label Tests
ci:mi300x Run Aiter standard tests on MI300X in PRs; main branch always runs them
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 Radeon R9700 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 7864 --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.

Reviews:
A reviewer is usually assigned within one working day after the PR is ready for review (see Code Review Process). If nobody has responded for about a week, mention the area contact listed in CONTACTS.md. Want to help review? See REVIEWERS.md.

@fsx950223
fsx950223 marked this pull request as ready for review November 12, 2026 07:44
@fsx950223
fsx950223 requested review from a team and a balanced review from Copilot November 12, 2026 07:44

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@aiter-bot

aiter-bot commented Nov 12, 2026 •

Copy link
Copy Markdown

NOT READY FOR HUMAN REVIEW

PR Self-Review

Revision cbffdd201bfb · mode shadow

Author self-review accepted. Readiness report:

Author answers

  • PASS — current-SHA answers are present and structurally specific
Show the author's self-review answers

Behavior and intent

On gfx950 the pa_decode tile schedule now uses three policies #7709 introduced for gfx942:
- **K/V streaming** for long head rows removes the next-tile K prefetch spill.
- **The MTP query split** spreads one sequence's query positions over idle CUs.
- **Split-major launches** stop small grids' later splits and heads from queuing behind empty plan slots.

Each is re-tuned on MI355X where the gfx942 rule lost: per-token FP8 keeps its fused MTP4 tiles, the FP8 split is bounded by the uncapped tile count, and split-major is limited to sparse grids and to FP8 or BF16 up to D128. Without these policies, gfx950 spilled at D512/D1024 and left CUs idle under MTP (1.19x geomean over the matrix). gfx942 and gfx1250 binaries are unchanged.

Validation and unverified scope

Evidence on MI355X:
- `pytest op_tests/test_flydsl_pa_decode.py`: 355 passed, 37 skipped. The negative control with commit 3's axis fixes reverted fails 9 cases.
- Paired in-process A/B: per commit and end-to-end over all 817 matrix cases. Geomean 1.19x, no changed case below 0.98x, 0 errors, 0 non-finite.
- Determinism: every one of the 333 changed cases repeats bit-exactly over 20 launches (`det_check.py`).
- Accuracy: 19 changed cases are not bit-exact against the base (streamed accumulation order). On each, the max error against the fp32 reference equals the base's.

Evidence on gfx942:
- pytest on MI308X: 285 passed, 107 skipped.
- Compile-only replay (`COMPILE_ONLY=1`) of every matrix case: gfx942 and gfx1250 binaries identical to `03894b767e`.

Unverified:
- Not run on MI350X or other gfx950 SKUs with fewer CUs; the CU/4 and 8-tiles-per-CU bounds were measured at 256 CUs.
- Shapes outside the matrix are not covered.
- Callers that pass no `max_context_length` get no tile bound, so their FP8 MTP rows do not split on gfx950.
- Not yet rebuilt on #7700 `bdabd08dad` / #7701 `a8b9f032b9`; every number above is for the dependency SHAs listed in the summary.

PR-specific question

`op_tests/test_flydsl_pa_decode.py` is the only changed op test in this diff, and its change comes from #7700 (`0a4638a355`). It is not one of this PR's three commits.
- The plan-structure check used to assert that every sequence gets one task plus a tile-proportional share of the spare slots. #7700's gfx950 min-max planner instead sizes tasks by the smallest per-task tile bound whose tasks fit the capacity. Before the update, that assertion fails on every gfx950 single-block plan.
- No other test checks the plan's task layout, so the update is the canonical coverage for the balanced planner.

This PR's own commits change no test. Their main boundary is the split-major grid on gfx950: it swaps which block-id axis names the plan slot. Any gfx950-only path that still assumes slot-major axes silently reads the wrong slot or head. is the split-major grid on gfx950: it swaps which block-id axis names the plan slot. Any gfx950-only path that still assumes slot-major axes silently reads the wrong slot or head.

Two such paths existed:
- the BF16 D128 planned output's slot count (`grid_dim.x`);
- the one-partition empty-row zero fill (`block_id("y")`).

Opening split-major on gfx950 without fixing them produced relative errors of 1-19 on BF16 D128 two-head shapes. Both now select the axis from `SPLIT_MAJOR`, and their IR is unchanged when it is off. The remaining direct `block_id`/`grid_dim` reads are gfx1250-only pipelines, or the MXFP4 direct path, which keeps slot-major. Evidence: with the two fixes reverted, the existing UT fails 9 cases; with them, all pass; every changed case is deterministic and matches the base's fp32-reference error.

The second boundary is the cache key. The renamed busy flags keep their positions ahead of `D256_PIPE`, so `create()`'s positional unpacking and the test's `cache_key[-2:]` are unchanged; the pipe-policy tests pass.

Changed op tests

`op_tests/test_flydsl_pa_decode.py`: changed by #7700 (`0a4638a355`), not by this PR's three commits. Its plan-structure check now accepts gfx950's balanced min-max planner, asserting each sequence's task count equals `min(ceil(tiles / T), max_partitions)` for the smallest feasible bound `T`. It is not redundant, because no other test checks the plan's task layout.

This PR's commits add no test, because the existing `test_pa_decode` cases reach every new gfx950 specialization: the `head1024`/`head512`/`head192` streamed rows, the MTP4/MTP8 `-direct`, `-merge`, `-probability_tail` and window cases for the query split, and the `-direct`/`-cluster` cases for split-major. The negative control shows they fail when the split-major axis fixes are missing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

PR state

  • PASS — pull request is marked ready for review

Copilot findings

  • PASS — no active Copilot finding lacks a substantive author reply
  • ADVISORY — Copilot could not review the current head (for example, quota or service failure)

Code and op-test hygiene

  • PASS — no committed artifact or conflict marker detected

Required CI

  • PASS — Black: passed
  • PASS — Ruff: passed
  • PASS — Repository dependency: passed
  • FAIL — Aiter Test Gate: failed
  • Required CI failed; fix it before requesting human review.

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants