Repository navigation
[FlyDSL] [Perf] pa_decode: K/V streaming, MTP query split and sparse split-major on gfx950 - #7858
Merged
fsx950223 merged 3 commits intoNov 12, 2026
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings three of #7709's gfx942
pa_decodeschedule policies to gfx950, each re-tuned on MI355X: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 base branch (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 is stacked.
fsx950223/pa-stack-7700-7701-7709, is the merge of [FlyDSL] [Perf] pa_decode: gfx950 fused BF16 MTP4 verify tile, decode load scheduling and lean FP8 MTP4; four paths extended to gfx942 #7700, [FlyDSL] [Perf] pa_decode: gfx950 reducer issues all partition loads in one rung #7701 and [FlyDSL] [Perf] pa_decode: gfx942 schedule tuning (K/V streaming, latency hiding, grid-size policies) #7709 (see "Base branch" below). Please review only the three commits here.main, so this PR gets them once it is retargeted tomainafter its bases land.59d523033c, [FlyDSL] [Perf] pa_decode: gfx950 reducer issues all partition loads in one rung #770102dc5d11b8and [FlyDSL] [Perf] pa_decode: gfx942 schedule tuning (K/V streaming, latency hiding, grid-size policies) #770965eed8399b. Since then [FlyDSL] [Perf] pa_decode: gfx950 fused BF16 MTP4 verify tile, decode load scheduling and lean FP8 MTP4; four paths extended to gfx942 #7700 has moved tobdabd08dad: it extends four gfx950 paths to gfx942, adopts a named cache key, and keeps main's Q absmax above D256 on gfx942. [FlyDSL] [Perf] pa_decode: gfx950 reducer issues all partition loads in one rung #7701 has moved toa8b9f032b9. The stack will be rebuilt and re-measured once those heads settle.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:
Three gfx942 policies did not carry over and stay gfx942-only:
waves_per_eu=2on busy grids: [FlyDSL] [Perf] pa_decode: gfx950 fused BF16 MTP4 verify tile, decode load scheduling and lean FP8 MTP4; four paths extended to gfx942 #7700's gfx950 MTP plan holds capacity at one or two slots per CU, so it changes no case.Changes
All changes are in
aiter/ops/flydsl/kernels/pa_decode/{traits,op_epilog}.pyandpa_decode_kernel.py, one commit each:372b2bc843K/V streaming on gfx950 (STREAM_KV).gfx942_busy->busy_grid,gfx942_two_per_cu->two_per_cu. All their other consumers stay gfx942-only.50e1d38283MTP 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: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).cbffdd201bSplit-major launches on sparse gfx950 grids.grid_dim.x. With a split-major grid that is heads x splits, so partials were stored at wrong offsets.block_id("y")as the KV head.Not changed:
plan_pa_decodeand its budgets;Base branch
fsx950223/pa-stack-7700-7701-7709is built from:59d523033c;02dc5d11b8;8274dfbc42;65eed8399b.The #7709 merge resolves 19 conflict hunks in
traits.py,pipeline.py,op_lds.pyandpa_decode_kernel.pyas 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:
03894b767e(the stack branch), after =cbffdd201b.op_tests/test_flydsl_pa_decode.pythat 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).mtp4-g16-page128-direct...-direct-d64Per commit, each against its parent, over the cases it changes:
By family over all 817 cases:
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
Results:
op_tests/test_flydsl_pa_decode.pypasses in full atcbffdd201b: 355 passed, 37 skipped.-direct,-direct-d64and BF16-clustercases), so the existing tests cover them.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
The API, the plan and the set of supported shapes are unchanged; the change is internal to the gfx950 tile schedule.
Dependencies
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:
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.det_check.py).Evidence on gfx942:
COMPILE_ONLY=1) of every matrix case: gfx942 and gfx1250 binaries identical to the base branch.Unverified:
max_context_lengthget no tile bound, so their FP8 MTP rows do not split on gfx950.main.bdabd08dad/ [FlyDSL] [Perf] pa_decode: gfx950 reducer issues all partition loads in one rung #7701a8b9f032b9; every number above is for the base SHAs listed in the summary.PR-specific question
The most important 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.
Two such paths existed:
grid_dim.x);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 directblock_id/grid_dimreads 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, socreate()'s positional unpacking and the test'scache_key[-2:]are unchanged; the pipe-policy tests pass.Changed op tests
None. The existing
test_pa_decodecases reach every new gfx950 specialization: thehead1024/head512/head192streamed rows, the MTP4/MTP8-direct,-merge,-probability_tailand window cases for the query split, and the-direct/-clustercases for split-major. The negative control shows they fail when the split-major axis fixes are missing.🤖 Generated with Claude Code