Skip to content

feat(hipblaslt): add SwizzleA and swizzleB for subtile MXFP4 - #13282

Merged
liu-yiqian merged 94 commits into
developfrom
users/liu-yiqian/mxf4-swizzleb
Oct 28, 2026
Merged

liu-yiqian merged 94 commits into
developfrom
users/liu-yiqian/mxf4-swizzleb

Conversation

@liu-yiqian

@liu-yiqian liu-yiqian commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

JIRA ID : AIHPBLAS-4083

Motivation

SwizzleTensorA and SwizzleTensorB mean the host has already interleaved the operand into the MFMA-friendly 16-row layout, so the kernel does not pay for the rotation. This enables both of them on the subtile MXFP4 path, which previously supported neither.

Technical Details

1. Reading the pre-shuffled layout

TileInfo grows an isPreShuffled flag and the emitters branch on it. A pre-shuffled tensor interleaves one K tile across all rows of an MFMA tile, so: the GR pointer advance steps 16 row fragments per DepthU; GR offsets index by rowId * depthUBytes rather than rowId * stride; the colId swizzle skips pre-shuffled tensors, so the operand keeps the host's layout while the other one still gets the rotation; the aligned 16-row HBM padding is folded back in; the SRD limit and tile start scale by the 16-row block stride; a StreamK range that starts mid-tile scales its start by the same row count; and the MX scale buffer is sized the way the layout pads it.

Where nothing is pre-shuffled the two colIds are equal, so B stays derived from A by copy rather than recomputed — that is what keeps kernels off this path byte-identical.

2. Keeping it off the architectures it was not built for

gfx942 ships 35 logic files (30 swizzle-A, 5 swizzle-B) that set the same ProblemType flags for the classic DirectToVgpr swizzle, which is a different host layout with a different K step. Three changes here first read those flags and so leaked onto that path; all three now ask isSubtileSwizzledTensor instead:

  • graAddresses stride alignment. The subtile path pads to the pre-shuffled block stride (swizzleLayoutK), the classic path to the per-wave block (swizzleK). They differ whenever swizzleK exceeds one operand — 32 against 8 for gfx942 f16, 64 against 16 for f8.
  • StreamK start offset. Only the subtile global read walks whole blocks, so scaling the classic path's start moves a workgroup off its own range.
  • The DepthU ceiling. 382 of those gfx942 solutions ship DepthU 1024, above the subtile ceiling.

Guards written inside Components/Subtile/ are implicitly scoped because only that module constructs a TileInfo; these three live outside it, which is why they needed an explicit gate.

3. The DepthU ceiling is derived, not hard-coded

A K step covers every row of a swizzle block, so the byte step is rows * DepthU * bpe and has to fit the 12-bit DTL immediate. swizzleTensorMaxDepthU(rowsPerBlock, bpe) returns that limit: 512 for MXF4's half-byte element over 16 rows, 256 for FP8, 128 for BF16. It is enforced both when deriving a solution and when emitting one, and the rejection message now prints the real limit for the data type at hand.

4. Gate naming

isSubtileSwizzledTensor is named off the SwizzleTensorA/SwizzleTensorB parameters it reads, to distinguish it from the MX scale swizzle, which has its own layout and its own read. It currently delegates to isMxf4SubtilePath: nothing about reading a pre-swizzled operand is MXF4-specific, but FP8 and BF16 are untuned and untested here and out of scope for this ticket. Widening it later is one edit in one place rather than three. The store and schedule gates stay on isMxf4SubtilePath, which is genuinely MXF4-in/bf16-out specific.

5. Logic files

Swizzle is a ProblemType property rather than a solution parameter, so it needs its own logic files rather than extra entries in the existing ones. Six are added, from gfx950 tuning runs, mirroring the ProblemType of the shipped F4BS library with SwizzleTensorA or SwizzleTensorB set — one STA and one STB in each of Origami/, Origami_nta4/ and Origami_ntb4/, 76 solutions each. Equality, GridBased and Range are untouched.

The library grows by exactly 152 kernels: 76 STA and 76 STB, since the three NonTemporal variants resolve to the same kernels.

Test Plan / Test Result

check result
Unit suite (tensilelite/Tests/unit) 11536 passed, 0 failures
Characterization snapshots 802 passed, none changed
Full gfx950 regeneration, this branch against its merge base 74970 → 75122 kernels: 152 added, 0 removed, 0 changed
GPU correctness, gfx950 subtile_mxfp4_swizzlea.yaml and subtile_mxfp4_swizzleb.yaml

The regeneration is the load-bearing one for scope: every kernel the library built before this change is byte-identical afterwards, so the only new code any existing problem can reach is none.

Neither SwizzleTensorA nor SwizzleTensorB had unit coverage on the subtile path before this — the existing tests all drive the classic DirectToVgpr path, which never builds a subtile TileInfo, so isPreShuffled was False everywhere and every arm it guards shipped untested. The new designed gfx950 config has problem groups differing only in SwizzleTensorA or SwizzleTensorB, pinning the pre-shuffled code against an otherwise identical control. It deliberately sets no DirectToVgpr, which is what pins the subtile path's exemption from that requirement: if the exemption regresses, the config stops producing solutions and the test fails rather than silently passing.

The A/B asymmetry gets its own test, since a check written per kernel rather than per tensor passes a markers-present test and is still wrong. test_r9_preshuffled_char.py additionally pins the three scoping gates from section 2 against their classic-swizzle counterparts, the derived DepthU ceiling and its boundary, and the equivalence of the two gate helpers so they cannot silently drift. test_subunit_division.py covers two division fixes directly, including the operands that used to raise.

Submission Checklist

liu-yiqian and others added 22 commits September 29, 2026 23:10
The optimizations that follow all apply to one narrow family of kernels:
fp4 inputs with a bf16 destination, built through UseSubtileImpl on gfx950.
Everything else in the library has to come out byte-identical, so the
question "is this kernel in scope?" gets asked from the solution resolver,
the kernel namer, and a dozen emit sites.

Giving that question a single answer -- isMxf4SubtilePath -- keeps those
callers from each spelling out their own type test and drifting apart. The
narrower helpers it is built from are exposed too, because the tile-shape
predicates (plsinBlockSchedTile, plsinEarlyStoreTile) select subsets of the
same family for individual optimizations.

These helpers are deliberately tolerant of partially-built solution state:
assignPostLoopStoreInNll runs before the ProblemType is fully populated, so
the predicate has to return False rather than raise on a half-filled dict.
The accompanying tests pin that, along with the exact tile shapes that do
and do not qualify.

Co-authored-by: Cursor <cursoragent@cursor.com>
PostLoopStoreInNll fuses the D-tile buffer_store into the No-Load-Loop, so
accumulator pairs that finish early are stored while the final-K-step MFMAs
for later pairs are still running.

The parameter defaults on and assignPostLoopStoreInNll turns it off for
everything that cannot support it, rather than the other way around, so a
tuning file does not have to opt in per solution. The eligibility test
routes its destination-type check through plsinSubtileTypes instead of
spelling one out, which keeps it from accepting kernels the emit side will
later decline to build -- a mismatch there allocates a flag SGPR and
renumbers every register after it for no reason.

Naming needs a matching exception. Required parameters are appended to every
kernel name, so naming this one unconditionally would put PLSIN0 into the
name of every kernel in the library and change its hash. It is dropped for
kernels that cannot set it.

No kernel emits anything differently yet; this commit only resolves and
names the parameter. The golden updates are that new key appearing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Fusing the store into the No-Load-Loop only pays off if the loop hands it
work early, which takes three changes to the subtile scheduler.

Block scheduling groups the MFMAs so that a whole accumulator tile retires
at once instead of dribbling out, giving the fused store a complete tile to
write while later tiles are still computing. It is restricted to 256x256
macro tiles, where there are enough MFMAs in flight to hide the store; at
smaller tiles the grouping costs more in occupancy than it returns.

The preloop global read is now unconditional. It used to be conditional on
scheduling state that the block-scheduled path invalidates, and reasoning
about when it did and did not run was the source of most of the scheduler's
complexity here.

The MFMA-to-ACC read latency is modelled from a new arch capability rather
than assumed. gfx950 needs 17 cycles between an MFMA and a read of its
accumulator, and the store weaving has to respect that or it reads a result
that has not landed.

A D accumulator tile that spills into the arch VGPR pool stays live from the
MFMA loop through the epilogue, so anything that writes those registers
corrupts one 16x16 output block per wave, silently. The scheduler records
the spilled ranges and assertNotSpilledDTile turns that into a generation
failure instead of a wrong answer.

The rocisa side gains the latency capability plus TextBlock handling in
macro inlining, since CustomSchedule annotates the main loop with comments
that were being dropped when the macro was expanded.

Co-authored-by: Cursor <cursoragent@cursor.com>
This is the store itself: the D-tile buffer_store moves into the No-Load-Loop,
a flag SGPR records which workgroup owns the fused store so the post-loop body
can skip the work it already did, and the epilogue kernarg load learns to skip
its reload for that owner.

The skip target is shared with the existing GSU skip, since both mean "this
kernel does not store here", but it keeps develop's label name whenever GSU is
the only condition reaching it -- the name is cosmetic and reusing it leaves
every out-of-scope kernel byte-identical.

The StreamK changes are scoping rather than new behavior. The partials store
was biasing an absolute VGPR index by a ValuC base, which makes the workspace
store read the wrong accumulators; correcting it is gated on the MXF4 subtile
path because every other subtile kernel ships against the uncorrected form.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ions

Nothing in the unit suite covered this feature surface, so a scope leak only
showed up by generating the whole gfx950 library and diffing it -- an
expensive way to find out that a type test was one predicate too wide.

The config carries two problem groups that differ only in DestDataType, one
bf16 and one fp32, at two macro tiles. That gives each test an in-scope case
and a control that should look exactly like develop, so a leak fails a test
instead of surviving to the library diff. The tile pair does the same for
block scheduling, which applies at 256x256 but not at 128x128.

The store-repack assertion matches the permlane16 that swaps dwords rather
than any permlane16, because develop already emits one for swizzling and
matching that would pass no matter what this branch did.

Co-authored-by: Cursor <cursoragent@cursor.com>
_selectPlsinFusedStorePolicy takes a different branch once a macro tile exceeds
256: last-K MFMAs move into the store gaps and the K=0 A/B tiles are lent out so
store temps can reuse those holes.  Every PLSIN test so far sits at or below
256x256, so that branch shipped untested.

MT320x256 exercises it, and the characterization test pins the resolution
(PostLoopStoreInNll on, MacroTile0 320, MacroTile1 256) so a future change to
the policy cannot silently reclassify these kernels as small-tile.

The mxfp8 MT320x256 and MT256x320 regression configs already covered the
geometry; what was missing was an MXF4 case that actually resolves PLSIN.
0754223 dropped the batch-tail workgroup barrier when the gfx950
permlane16 store path was active, reasoning that "the permlane16 store
has no LDS traffic, so its single-DU path keeps the read drain but does
not need the following workgroup barrier". b2da475 then widened that
to every single-DU subtile path.

The barrier was not only protecting this wave's own LDS traffic: it also
kept a sibling wave's bias/SAV LDS reads from overlapping this wave's
stores. The retained s_waitcnt dscnt(0) is a per-wave counter and cannot
order anything across waves, so dropping the barrier dropped a cross-wave
guarantee with nothing standing in for it.

The result was a race in the store path for single-DU subtile kernels
with bias or ScaleAlphaVec active. It surfaced as wrong data at correct
addresses, confined to partial-M edge tiles where the skewed wave timing
opens the window: MT256x320 MIWT8_10 at 12384x4096x4096 miscompared on
78% of runs, with MT128x384, MT128x192 and MT256x192 also affected.

Restoring the barrier for every path that drains bias/SAV takes that
shape to 0 failures over 1700 verifications spanning 71 macro-tile
configurations, and leaves the emitted barrier count identical to
develop's. Measured cost on 6144x32768x4096 is 1.3% for MT256x256
PLSIN1; 0754223 had claimed 0.4% for the removal.

Co-authored-by: Cursor <cursoragent@cursor.com>
f0c5ded restored this barrier on every store batch, which is more than
correctness needs and cost 1.3-1.7% on 6144x32768x4096.

Every LDS op a store batch issues is a load: bias and ScaleAlphaVec are
staged into LDS before the store loop, never inside it. Readers cannot
race readers, so the batches do not need fencing from each other. What
does need fencing is the end of the store against whatever rewrites that
LDS next -- under StreamK the same workgroup returns to the mainloop and
restages A/B while a sibling wave may still be reading bias/SAV -- and
one barrier on the last batch covers that.

Multi-DU keeps the per-batch placement: it emits its stores after the
barrier rather than before, so the two are not interchangeable there.

An assert on the non-barrier batches holds the "loads only" premise, so
a future change that introduces a ds_write into the store fails the build
instead of silently reintroducing the race.

Verified on gfx950: the 71-config all-solutions sweep at m=12384 over 5
passes is 1700 results with worst norm_error exactly 0, and the fp4 /
swizzleA / swapAB suites are clean. The 7 remaining failures are the
pre-existing swizzle-B k<=128 miscompares, which fail identically without
this change. Measured back at parity with the unsynchronized baseline:
MT256x320 PLSIN0 4331.8 vs 4331.7, MT256x256 PLSIN1 4532.0 vs 4549.9.

Co-authored-by: Cursor <cursoragent@cursor.com>
Three knobs on the mxfp4-bf16 subtile store's fused arm, all default off, all
measured neutral on the eleven perf shapes. They are kept because the placement
they buy is real and the assembly is now where the FlyDSL reference says it
should be; what they show is that store placement is not what bounds this
kernel.

TENSILE_PLSIN_TAIL_PARTM splits the tail partition grid along M. An N split
floors at four partitions once the MX scale granularity forbids a partition
narrower than two MFMA tiles, so M is the only axis left. The store enumerates
its elements N-group-outermost, so an M split interleaves the partitions rather
than giving each a contiguous run; the cut still works because a boundary marker
may only open a later stage, but the stages come out lopsided, so
buildSubtileFusedStore sorts the element list into partition order. An element's
D address comes from its tuple while the accumulator it drains comes from its
position, so the acc-read queue is permuted in lockstep -- without that every
accumulator pairs with the wrong address. Both are scoped to the list this one
fused store consumes. 2x4 is the floor: 4x4 fails vgpr tile assignment.

TENSILE_PLSIN_DRAIN_EARLY places each drain unit inside its own partition at the
bar where its accumulators go final, instead of deferring whole stages to the
next partition where every bar is vacuous. First store moves from MFMA 72 to 28
of 256, cover 184 to 228 against a reference of 232, trailing stores 6 to 3.

TENSILE_PLSIN_TAIL_DROP_LAST_SYNC drops the one barrier build_nll keeps in the
four-deep body. That barrier separates the body's final LDS read from the next
macro tile's global reads, but since ee8f6c3 the fused arm ends with the
store's own cross-wave barrier, which sits a whole store later and separates the
same two things. ATT confirms the removal: eight barriers become seven, its
22604 cycles do not resurface elsewhere, total stall falls 2.85%.

Verified on the eleven perf shapes (11/11, worst norm_error 0), the ten verify
suites (212 results, only the known pre-existing STB failures), and five passes
of the all-solutions sweep at m=12384 (71 of 71 configurations clean).
Performance against the baseline e6d15de is 1.000x geomean, 0.992x to 1.015x.

Co-authored-by: Cursor <cursoragent@cursor.com>
The three knobs landed off so they could be measured. They are correct and
neutral, and the placement they buy is what the FlyDSL reference describes, so
they become the default for the tiles block scheduling is already scoped to:
MXF4 in, bf16 out, Weave store, MacroTile exactly 256x256. Every other geometry
keeps its shipped schedule, because none of them has been traced.

Checked by generating the eleven kernels of the F4BS STA Equality logic three
ways -- new defaults, all three knobs forced off, all three forced on. Only
MT256x256 MIWT8_8 moves, at both PLSIN0 and PLSIN1, and it lands byte-identical
to the forced-on build. MT256x192, MT256x320, MT320x256 and MT256x256 MIWT4_16
are byte-identical to the forced-off build.

A plain build of the full library verifies 11/11 on the perf shapes with worst
norm_error 0.

Co-authored-by: Cursor <cursoragent@cursor.com>
The tail entry waited vmcnt(0) because a scale local-read was placed two
sub-k-iterations ahead of its consumer, putting it in front of the only
barrier that makes the other unroll half's LDS writes visible. Nothing
else could cover that half, so the entry had to drain every outstanding
load before the body could start.

Clamp the scale read to one sub-k-iteration of lookahead in the tail
sub-scheduler only, which leaves room for a second wait/barrier pair, and
emit that pair in front of the first read that touches the in-flight
half. The entry wait then only has to make the resident half visible, so
it drops to one DepthU batch of loads.

On MT256x256 fp4 the entry goes from vmcnt(0), charged 354 cycles per
wave, to vmcnt(18) charged 28; the second wait costs 73. Tail vmcnt stall
falls 54%, from 91.7k cycles to 42.4k. Throughput is unchanged at 1.001x
geomean over 11 shapes: the arm is MFMA-bound at ~54% v_mfma occupancy,
so the recovered cycles land back on the critical path. Kept on because a
bounded wait is the correct shape for a half the barrier already covers.

Scoped to the block-scheduled tiles, so only three MT256x256 MIWT8_8
PLSIN1 kernels change; mainloops are byte-identical. Verified over 1912
results: 11-shape suite, a five-pass all-solutions sweep at m=12384
covering 71 macrotile configurations, and 10 verify suites. The only 7
failures are tiny-K STB cases that reproduce with identical norm_error on
the pre-change library.

TENSILE_PLSIN_TAIL_SPLIT_WAIT=0 restores the single vmcnt(0).

Co-authored-by: Cursor <cursoragent@cursor.com>
The five picked commits carry TENSILE_PLSIN_* env overrides, which this
branch removed. Fold each back to the default it shipped with: the finer
tail grid at 4 M tiles per partition, early drain on with reserve 2 and
no extra spacing, the last-sync drop on, and the split entry wait on.
Scoping is unchanged -- plsinStagingEligible still holds the grid and the
sync drop to the tiles they were measured on.
The picked commit narrowed the barrier for every subtile kernel. The
loads-only premise behind it was read off the MXF4 store epilogue and
measured there, so scope it the same way the rest of this branch is
scoped and leave other subtile kernels on develop's per-batch barrier.
_plsinWeaveStat is debug instrumentation this branch removed with the env
gates; it was default-off upstream and emits nothing. The scoping test
pinned the tail at four partitions, which was the N-only split -- the 2x4
grid makes it eight.
Dropping the scope here added lgkmcnt waits to 552 kernels outside the
MXF4 subtile path -- F4SS, F4HS, F8 and BBS -- which this branch holds
byte-identical to develop. The two forms agree for in-scope kernels, so
the restore costs the race fix nothing: single-DU MXF4 still takes the
per-element route, and the barrier it needs is emitted at the store.
plsinBlockSchedTile scoped on tile and operand types alone, so a kernel
whose PostLoopStoreInNll had been disabled -- StreamK atomic, a non-zero
StoreRemapVectorWidth, wave32, MultipleBuffer accumulation -- could still
be built block-scheduled. It then emitted the NGLL/NLL arm split for a
fused store that does not exist.

No shipped kernel reaches that corner today: all three MT256x256 F4BS
solutions resolve PLSIN on. PLSIN-on codegen is byte-identical with this
check, so the gate now states the dependency the staged store already
had.
The mock carried tile and operand types only, so it described a kernel
with no PostLoopStoreInNll key at all -- which the block-scheduling gate
now reads. Give it the parameter, defaulting on as every real MT256x256
fp4 kernel resolves it, and test the off case directly.
Removing the TENSILE_PLSIN_* env overrides left two comments pointing at
things a reader cannot reach: the split entry wait still described a knob
that overrides its bound, and both sites cited a FINDINGS document that
does not exist in the tree. The bounds they describe are now derived and
fixed, so say only that.

Co-authored-by: Cursor <cursoragent@cursor.com>
The block-scheduled tail, its staged store and the finer store grid are
all reached through plsinStagingEligible, while the store barrier and the
per-element wait accounting are scoped on isMxf4SubtilePath directly.
Nothing checks that the first is contained in the second, so a tile
predicate that was loosened off the MXF4 path would build the block
schedule for kernels none of this was measured on, and the only sign of
it would be a library diff.

Sweep the type, subtile, tile and PLSIN axes and assert the containment
holds at every point. Dropping the type check from plsinEarlyStoreTile
fails it on an fp4-in/fp32-out kernel, which is the leak being guarded.

Co-authored-by: Cursor <cursoragent@cursor.com>
Develop moved StreamK behind the TileProcessingStrategy/ExecutionPolicy
abstraction and renamed its constant-parking machinery from StreamK* to
Persistent*, so the resolutions worth recording are:

* The solution-time StreamK check became isStreamK(state); the codegen-time
  ones became isStreamK(kernel) and isPersistentDataParallel(kernel), since
  develop no longer carries a "StreamK" key in either dict.

* Develop's #12615 narrowed subtileBarrierDrains to multi-DU for every subtile
  kernel, which is what this branch's needsCrossWaveBarrier was doing for the
  MXF4 path alone. The indirection is now tautological in the wait accounting,
  so it is dropped there; the last-batch barrier at the emission site, which
  develop did not change, is kept.

* The post-loop store-init is emitted once. This branch relocates it after the
  fused-store dedup guard, and develop added skipPhantomTileStore next to the
  original call site; keeping both emitted the SRD init twice and collided the
  per-channel labels.

* Persistent constants are parked in VGPRs on gfx950 under PostLoopStoreInNll.
  The fused epilogue peaks at 104 SGPRs against a cap of 102 on tiles above
  256, and parking the kernarg run frees a contiguous block to cover it.
Develop added subtile fp4 support for the NN/NT/TT layouts, along with the
GRTag_TLU1 strided-K pointer update they need and 1559 lines of new coverage in
subtile_mxfp4.yaml. Those kernels satisfy isMxf4SubtilePath as it stood
(UseSubtileImpl plus fp4 in / bf16 out), so merging develop silently opted them
into optimizations that were only ever built and measured on TN. The pre-loop
SRD hold and the fused post-loop store reason about the TN global-read shape,
which those kernels do not have, and 170 of the new non-TN cases miscompared.

Add the layout to the gate, which is where the rest of the scoping already
lives, so the non-TN kernels keep develop's schedule exactly. subtile_mxfp4 then
matches the develop baseline at 9937 passed / 0 failed on gfx950.

Also give _emitGRPtrUpdate_TLU1 the dispatcher's holdOnLastIter parameter. It
was added to the TLU=0 implementation only, so any TLU=1 kernel aborted codegen
with a TypeError. It is accepted and deliberately not honoured, for the same
reason the gate is now TN-only.

test_mxf4_subtile_scope covers all four layouts, so the restriction is pinned
down rather than implicit. Its fixture previously omitted the transpose keys
entirely.
This preserved Macro::addComment0 bodies when a macro was expanded, so that
CustomSchedule's mfmaIndex annotations survived into the assembly. It broke
rocisa's test_prnd_generator: PRND_GENERATOR's macro body opens with a comment
naming the macro, so the inlined output started carrying the string the test
asserts is absent.

The annotations are inert and nothing here needs them. The subtile path builds
no macros at all, so the only bodies this ever affected belong to CustomSchedule
and PRND_GENERATOR, neither of which is part of this work. Dropping it keeps the
change confined to the MXF4 subtile path, leaves the develop-owned test
untouched, and restores develop's behaviour of discarding those comments.

No effect on the shipped library: the 12 gfx950 kernels carrying mfmaIndex
comments get them from Module::addComment0 rather than from macro expansion, and
that count is the same on develop.
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.52336% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...te/tensilelite/Components/Subtile/SubtileGREmit.py 89.80% 2 Missing and 3 partials ⚠️
...ensilelite/tensilelite/SolutionStructs/Solution.py 78.57% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #13282      +/-   ##
===========================================
- Coverage    76.11%   72.48%   -3.63%     
===========================================
  Files         2886     2948      +62     
  Lines       483963   490629    +6666     
  Branches     73003    71998    -1005     
===========================================
- Hits        368338   355608   -12730     
- Misses       90392   110164   +19772     
+ Partials     25233    24857     -376     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 50.81% <ø> (ø)
TensileLite-Unit 84.10% <92.52%> (+0.04%) ⬆️
hipBLAS 90.62% <ø> (ø) Carriedforward from 67f97dc
hipBLASLt 66.21% <ø> (+0.03%) ⬆️
hipCUB 82.68% <ø> (ø) Carriedforward from 67f97dc
hipDNN 86.64% <ø> (-0.21%) ⬇️ Carriedforward from 67f97dc
hipFFT 54.32% <ø> (+16.30%) ⬆️ Carriedforward from 67f97dc
hipRAND 76.12% <ø> (ø) Carriedforward from 67f97dc
hipSOLVER 68.96% <ø> (-0.71%) ⬇️ Carriedforward from 67f97dc
hipSPARSE 86.99% <ø> (-0.04%) ⬇️ Carriedforward from 67f97dc
rocBLAS 48.25% <ø> (-20.48%) ⬇️ Carriedforward from 67f97dc
rocFFT 48.78% <ø> (+1.44%) ⬆️ Carriedforward from 67f97dc
rocRAND 57.42% <ø> (+0.11%) ⬆️ Carriedforward from 67f97dc
rocSOLVER 76.83% <ø> (+0.09%) ⬆️ Carriedforward from 67f97dc
rocSPARSE 74.75% <ø> (-2.40%) ⬇️ Carriedforward from 67f97dc
rocThrust 78.45% <ø> (-14.70%) ⬇️ Carriedforward from 67f97dc

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...blaslt/tensilelite/tensilelite/Common/Utilities.py 100.00% <100.00%> (ø)
...aslt/tensilelite/tensilelite/Components/StreamK.py 83.75% <100.00%> (+0.06%) ⬆️
...nsilelite/tensilelite/Components/Subtile/Kernel.py 85.98% <100.00%> (+1.76%) ⬆️
...te/tensilelite/Components/Subtile/SubtileLREmit.py 92.93% <100.00%> (+1.23%) ⬆️
.../hipblaslt/tensilelite/tensilelite/KernelWriter.py 84.89% <100.00%> (+0.01%) ⬆️
...lt/tensilelite/tensilelite/KernelWriterAssembly.py 79.42% <100.00%> (+0.03%) ⬆️
...ensilelite/tensilelite/SolutionStructs/Solution.py 68.66% <78.57%> (+0.04%) ⬆️
...te/tensilelite/Components/Subtile/SubtileGREmit.py 87.53% <89.80%> (+0.20%) ⬆️

... and 737 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

GlobalWriteBatch imported plsinEarlyStoreTile and never called it, in the
commit that added the import or in any since. Remove it; every other name on
that line is used.

The docstring was stale in a way that misread as a limit on PLSIN itself. It
opened by explaining that the predicate exists so several epilogue knobs could
be scoped alike, but those knobs were inlined at their shipped values earlier
in this branch, leaving plsinBlockSchedTile as the only caller -- which
narrows the bound to the MT256x256 equality, so it decides nothing on its own.

Say that, say why the two predicates stay separate rather than collapsing into
one constant, and say the part that was actually missing: MT>256x256 does get
PLSIN, through largeTileLend. What it cannot take is an *early* store, because
it lends the K=0 operand registers a store moved earlier would still be reading.

Emission is unchanged: the designed scoping pair hashes identically before and
after, both problem groups.

Co-authored-by: Cursor <cursoragent@cursor.com>
@liu-yiqian
liu-yiqian force-pushed the users/liu-yiqian/mxf4-swizzleb branch from 9081f10 to 91ef49d Compare October 8, 2026 14:39
The TextBlock handling was removed from the C++ macro_inline pass but not from
its Python mirror in the stinkytofu adaptor, leaving the two implementations
disagreeing about whether a macro-body comment survives inlining. Remove it
there too, for the reason it was removed from C++: the subtile path builds no
Macros at all, so this only ever reshaped CustomSchedule and PRND_GENERATOR,
both out of scope for this work.

mfma.hpp's added #include <optional> was hygiene, not a requirement -- the
file's existing std::optional members already resolve through a transitive
include, and rocisa builds clean without it. It and a stray blank line were
the only rocisa changes outside hardware_caps.hpp; revert both so the rocisa
diff is exactly the MfmaToAccReadLatency capability, which is load-bearing.

Both files are now byte-identical to develop. Emission is unchanged: the
designed scoping pair hashes the same before and after, and rocisa's suite
passes 130 including test_prnd_generator, the test this had broken in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
liu-yiqian and others added 5 commits October 14, 2026 23:04
Same defect the MT320x256 weave config had: findConfigs turns a declared mark
into a skip only on exact string equality with a detected architecture, and
gfx1250-strict is its own architecture name rather than a qualifier on
gfx1250. Both configs declared skip-gfx1250 alone, so both would have run on
the strict lane, where these gfx950-only subtile kernels cannot build.

With this, all 53 configs under Tests/common/gemm/gfx950 declare the pair.
The pre-shuffled global read enumerates one K-subtile at a time, but three
places scaled by the whole DepthU window instead. Those agree only when
localSubtileGrid[1] is 1, which for FP4 means DepthU 256. At DepthU 512 the
window splits in two and each wave read every other 128-byte swizzle
sub-block, so the MFMA multiplied rows against the wrong K positions and
produced wrong results rather than failing.

  - the per-row step becomes subIterKBytes, the K-subtile extent, instead of
    the whole DepthU window
  - the aligned 16-row padding correction uses the same K-subtile extent
  - a K-subtile step is scaled by the rows in a swizzle block, since one K
    step consumes a K-extent on every row

Guard the boundary at both layers so an unsupported DepthU cannot come back
silently. Deriving a swizzled solution past SWIZZLE_MAX_DEPTHU is rejected
with a swizzle-aware reason; until now DepthU 1024 was only turned away by
the LDS budget, which says nothing about why. The emitter additionally
refuses a K-window step that does not fit the 12-bit MUBUF immediate, which
rocisa does not range-check and would otherwise emit verbatim.

All eight shipped swizzle kernels stay byte-identical, and 96 validations
pass on gfx950 across both operands, DepthU 256 and 512, tail and odd sizes,
and StreamK 0 and 3.
Three defects made the host pre-swizzled path read the wrong operand data.

The global-read stride rounded K up to swizzleK, which covers a whole wave,
but the host pads K to a whole operand before reshaping and that padded K is
the 16-row block stride. For any K landing between the two, every block
after the first was addressed at the wrong offset. Round to the host's
granularity instead.

A K shorter than one DepthU window makes the row padding correction
negative. It was computed in elements and narrowed to bytes by shifting the
product, which is a logical shift of a two's-complement value and turned a
small negative step into roughly 2**31. Narrow the stride before the
multiply instead.

StreamK's partial-tile start offset stepped by DepthU in unswizzled terms. A
pre-shuffled block interleaves 16 rows into one K step, which the unroll loop
already accounts for, so a workgroup whose range started mid-tile read 16x
too close to the tile start.

Verified on gfx950: the 15 sizes from the reported tuning run go from 9/15 to
15/15 and a 26-point K sweep from 13/26 to 26/26, while the existing
96-validation swizzle sweep, both shipped swizzle configs and the unswizzled
control stay green. Regenerating gfx950/Origami changes exactly the 8
pre-shuffled kernels and leaves the other 61373 byte-identical.
@liu-yiqian
liu-yiqian added this pull request to stack #14002 October 20, 2026 15:06
Block_32_UE8M0_32_8_EXT scales go through preSwizzleScalesGFX950, which lays
them out as a 32x8 grid: the MN extent pads to 32 and the K-block extent to 8.
The bench sized that buffer with the granularity of swizzle_mx_scale instead,
128 / MXBlock = 4 K blocks and no MN padding, so whenever K was not a multiple
of 256, or M or N not a multiple of 32, the allocation came up a whole group
short and the swizzle's tail was truncated.

The kernels then read the missing group as zeros. Every F4BS solution failed
identically, with a relative norm error of sqrt(4 / (K / 32)) when only K was
unaligned: 0.279 at K=2944, 0.519 at K=384.

Verified on gfx950: 229/229 heuristic solutions pass at 4096x32768x2944 with
swizzled A, where all 229 failed at 0.2708 before. K=384, 640, 896, 1152, 1408
and 2944 and M or N of 1000 and 1008 all pass now; the already-aligned sizes
are unchanged.
@therock-pr-bot

therock-pr-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ✅ Pass —
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

therock-pr-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

Name the instruction the path actually uses instead of an external project,
so the comments describe the code in front of the reader.
…lsin-blksched-preloop-v2

# Conflicts:
#	projects/hipblaslt/tensilelite/tensilelite/KernelWriterAssembly.py
#	projects/hipblaslt/tensilelite/tensilelite/SolutionStructs/Naming.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/characterization/SolutionClass/__snapshots__/test_solution_class_char.ambr
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_subtile_bias_sav_waits.py
@liu-yiqian
liu-yiqian marked this pull request as ready for review October 21, 2026 16:42
@liu-yiqian
liu-yiqian requested a review from a team as a code owner October 21, 2026 16:42
Restore per-file coverage above the ratchet baseline for the three
Subtile files this PR touches:

  InstructionEmitter.py   94.92 -> 97.56  (baseline 97.35)
  LogicalScheduler.py     91.35 -> 93.80  (baseline 93.46)
  SubtileLREmit.py        92.31           (baseline 89.12)

Adds unit coverage for the loop-control setup hoist, LR tile set
counting, tail tile release, the tail entry wait bounds, the
multi-partition GR wait counts, and the schedule dump printers.
…liu-yiqian/mxf4-swizzleb

Resolve testing_matmul.hpp in favor of develop's blockScaleBufferSize.

Develop landed a helper that computes the same pre-swizzled scale extent
this branch was computing inline: for Block_32_UE8M0_32_8_EXT it routes
to scaleBufferSize, which pads the K-block count to 8 and the MN extent
to 32, matching mxPreSwizzledScaleSize. The non-swizzled padding path
matches as well, so the inline block and its helper are dropped.
SWIZZLE_MAX_DEPTHU bounds the subtile pre-shuffled global read, whose K
step rides in the MUBUF immediate. The derivation gate asked it of every
SwizzleTensorA/B solution instead, including the classic swizzle path,
which builds its step differently and has shipped well above it: gfx942
F8 swizzle-A logic carries 191 solutions at DepthU 1024 and 2048.

A rejection raised during library-logic parsing rather than skipping the
solution, so TensileCreateLibrary aborted and the whole gfx942 build
failed on the first one.

Scope the check to isMxf4SubtilePath, which is also what decides whether
the subtile emitter runs, so derivation and emission agree.
Two of the pre-shuffled global-read changes keyed off SwizzleTensorA/B alone,
so they also moved codegen for the classic swizzle kernels gfx942 ships.  All
514 swizzle tests across the six gfx94X shards failed with unspecified launch
failure; the same shards pass on the base PR.  They had never run before: the
gfx942 build died on the DepthU ceiling until that was scoped too.

graAddresses aligned the A/B stride to swizzleLayoutK, the granularity the host
pads to when it lays out a pre-shuffled tensor.  That is right for the subtile
path but not for the classic one, and the two values are far apart -- 8 against
32 for gfx942 f16, 16 against 64 for f8 -- so the SRD addressed outside the
buffer.  Align to swizzleLayoutK only on the subtile path.

StreamK._preShuffleRows scaled a mid-tile range start by the rows in a block
for any swizzled tensor.  Only the subtile global read walks blocks that way;
the classic path steps one row per K window, so the scale moved those
workgroups off their own range.  Return 1 off the subtile path.

The remaining two arithmetic changes -- the fractional bpe in swizzleGeometry
and the fractional MX read ratio in KernelWriter -- are unreachable rather than
unscoped: both agree with the old expression everywhere the old one did not
divide by zero, checked exhaustively over the operand ranges.

The r9 StreamK test asserted the unscoped behaviour, so it now builds a subtile
kernel and a companion pins the classic path at 1.  11290 unit tests pass and
all 802 snapshots are unchanged.
Base automatically changed from users/liu-yiqian/plsin-blksched-preloop-v2 to develop October 27, 2026 14:21
…xf4-swizzleb

# Conflicts:
#	projects/hipblaslt/tensilelite/tensilelite/Components/Subtile/LogicalScheduler.py
#	projects/hipblaslt/tensilelite/tensilelite/Components/Subtile/SubtileGREmit.py
#	projects/hipblaslt/tensilelite/tensilelite/KernelWriterAssembly.py
#	projects/hipblaslt/tensilelite/tensilelite/SolutionStructs/Naming.py
#	projects/hipblaslt/tensilelite/tensilelite/SolutionStructs/Solution.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/characterization/SolutionClass/__snapshots__/test_solution_class_char.ambr
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_SubtileBasedLogicalScheduler.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_assign_post_loop_store_in_nll.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_mxf4_subtile_scope.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_plsin_srd_hold.py
#	projects/hipblaslt/tensilelite/tensilelite/Tests/unit/test_vgpr_overflow_force_generate.py
… gate

Review feedback. SWIZZLE_MAX_DEPTHU = 512 was the MXF4 instance of a
limit that belongs to the 12-bit DTL immediate, so derive it from the
rows in a swizzle block and the element size instead.

isSubtileSwizzledTensor names the question the pre-swizzled global read
actually asks. Named off SwizzleTensorA/B so it cannot be read as the MX
scale swizzle, which has its own layout and its own read. It delegates
to isMxf4SubtilePath: MXFP8 and BF16 are out of scope here.
swizzleMaxDepthU read as though it covered the MX scale swizzle too. It
does not: the scale blocks have their own layout and their own read.
Swept in by accident; it is a build artifact of a local library
regeneration and holds absolute paths into my checkout.
@liu-yiqian
liu-yiqian enabled stacked auto-merge (squash) October 27, 2026 20:55

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

Thanks for your update.
I do not have further comments.
Please make sure no CI regression due to this change.

@liu-yiqian
liu-yiqian merged commit c75cafc into develop Oct 28, 2026
169 of 172 checks passed
@liu-yiqian
liu-yiqian deleted the users/liu-yiqian/mxf4-swizzleb branch October 28, 2026 17:33
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants