Triton #11171 — Why Two New NVFP4 Test Cases Produced Twelve Tensor-Memory Failures
Triton Blackwell NVFP4 × Dense Matmul, Part 3 of 3 — From plain dtype coverage to a routed, scale-swizzled persistent matrix with scatter, gamma, and K-ragged variants
The first two articles in this series examined an invalid tile decision in Triton’s Blackwell matmul planner.
The affected path combined:
NVFP4-scaled activation
×
dense BF16 or FP16 operand
+
persistent execution
+
an expanded operand staged in TMEMThe automatic planner could select:
block_n = 256That tile looked reasonable for:
N = 256because one tile could cover the full output width.
But the persistent lowering needed Tensor Memory for two live structures:
output accumulator
+
expanded operandThe combined allocation exceeded Blackwell’s 512-column TMEM limit.
PR #11171 repaired the automatic plan by capping the default:
block_n
→ no larger than 128for the affected persistent NVFP4 × dense path.
The code change was small.
The regression test was not simply:
Run one 256 × 256 × 128 matmul.Two top-level Case objects entered Triton’s existing matmul test matrix.
That matrix expanded them across:
output-tile variants,
persistent and non-persistent requests,
gather and scatter modes,
gamma scaling,
inner-expert padding,
and backend-specific warp parameters.
On the GB300 validation system, those two cases produced:
96 collected variants
84 expected skips
12 executable variantsAgainst the unmodified planner, all twelve executable variants failed with Tensor Memory exhaustion.
Against the fixed planner, the identical twelve variants passed.
The regression therefore proved more than:
one hand-built matmul now runsIt proved:
The planner correction remained valid across the shared matmul matrix’s scatter, gamma, K-ragged, BF16, and FP16 variants.
The existing tests already mentioned NVFP4
Before the new coverage was added, the matrix already contained NVFP4 × dense cases.
The relevant entries included:
M = 128
N = 128
K = 128
mode = plain
activation = NVFP4
dense RHS = BF16There were variants for:
nvfp4_e2m1
nvfp4_e2m1_fiberThose tests established useful facts.
NVFP4 activation construction worked.
The dense BF16 matmul route existed.
The result could be compared with the reference path.
Basic NVFP4 scaling variants were represented.But they did not combine the conditions that exposed the planner failure.
N = 128
→ did not invite a 256-column default tile
plain mode
→ did not create the same routed workload
no activation-scale HBM swizzling
→ did not require the same persistent layout pathTesting the dtype was not the same as testing the planner region.
The new cases were added beside those earlier plain cases rather than replacing them.
The missing case was defined by shape, layout, and execution mode together
PR #11171 added two top-level cases:
Case(
256,
256,
128,
"ragged",
"nvfp4_e2m1",
rhs_dtype,
"bfloat16",
a_hbm_swizzling=True,
)with:
rhs_dtype = bfloat16
and
rhs_dtype = float16The important properties were:
M = 256
N = 256
K = 128mode = raggedNVFP4 activationdense BF16 or FP16 RHSactivation-scale HBM swizzling enabledoutput dtype = BF16Each field moved the case closer to the affected lowering.
N = 256 exposed the dangerous default
The earlier cases used:
N = 128A planner ceiling of 128 is naturally sufficient for such a shape.
The new cases used:
N = 256That allowed the default N-tile logic to propose:
block_n = 256The test therefore crossed the exact shape boundary needed to expose the oversized accumulator tile.
Old coverage:
N = 128
→ 128-column tile is sufficientNew coverage:
N = 256
→ planner can select a 256-column tile
→ combined TMEM allocation becomes invalidThe regression did not merely make the tensor larger.
It selected a different planning branch.
Activation-scale swizzling forced the relevant persistent path
The new case sets:
a_hbm_swizzling=TrueIn the shared test implementation, activation-scale swizzling has strict preconditions.
The case is skipped unless:
target is Blackwell or newer
activation has microscaling
persistent execution is enabled
block_m is at least 128
gather is disabledThese guards ensure that the test does not silently fall into an unrelated execution route.
Activation-scale-swizzled NVFP4
→ persistent kernel
→ Blackwell-specific scale layout
→ block_m = 128 region
→ expanded-operand TMEM staging
→ block_n resource safeguard requiredThe layout was not decoration.
It was part of the dispatch key.
Ragged mode added routing rather than a plain matrix
The new case uses:
mode = raggedFor a Case whose slice count is not supplied explicitly, the test model defaults ragged and batched modes to multiple slices.
The shared matrix then constructs routing metadata and generates inputs according to those slices.
The operation is therefore closer to:
routed activation rows
→ potentially different expert slices
→ persistent scheduled matmul
→ dense RHS per routed slicethan to one ordinary unbatched GEMM.
This matters because the planner’s grid, expected slice size, persistence decision, and tile reuse can all depend on routing information.
Why the patch moved the scenario into the shared matrix
Within the PR history, the scenario initially existed as a dedicated Blackwell NVFP4 × dense test.
That one-off test manually constructed:
routing metadata,
NVFP4 values,
activation scales,
the Blackwell scale layout,
the dense RHS,
the precision configuration,
and the reference output.
A later test-only commit removed that standalone setup and inserted the two cases into _build_test_op_cases() instead.
The PR summary states the intended direction explicitly:
Extend the existing matmul matrix.
No standalone planner-test setup is needed.This gave the regression several advantages.
Shared input construction
Shared routing setup
Shared NVFP4 quantization
Shared scale-layout conversion
Shared epilogue combinations
Shared reference implementation
Shared result comparison
Shared skip contractsThe issue stopped being protected by a bespoke reproduction alone.
It became part of the matmul system’s ordinary behavioral matrix.
One top-level Case is not one pytest execution
The test function is parameterized across several dimensions.
At the merged commit, each Case is combined with:
block_m:
16 or 128operation combination:
6 variantsgamma:
False or Truepersistent:
False or Truenum_warps:
4 or 8 only on Hopper,
otherwise NoneThe six operation combinations are:
1. no gather, no scatter, no inner-expert padding
2. gather only
3. scatter only
4. gather and scatter
5. pad_b inner-expert mode
6. pad_a inner-expert modeOn GB300, is_hopper() is false because the helper returns true only when the device’s major compute capability is exactly 9.
GB300 was validated as SM 10.3, so the num_warps parameter contributed only:
Nonerather than both 4 and 8.
The two cases expanded into 96 collected variants
The visible parameter dimensions on GB300 produce:
2 RHS dtypes
×
2 block_m values
×
6 operation combinations
×
2 gamma values
×
2 persistence values
×
1 num_warps valueTherefore:
2 × 2 × 6 × 2 × 2 × 1
=
96 collected variantsThis count is not written as one constant in the source.
It follows from the two added Case entries and the pytest parameterization active on a non-Hopper Blackwell target.
The PR validation then reports:
12 passed
84 skippedafter the fix.
12 + 84
=
96The reported result matches the source-level expansion.
Most combinations were supposed to be skipped
The 84 skipped variants were not 84 missing test results.
They were combinations that violated one of the selected path’s explicit contracts.
The matrix begins broad.
The skip rules narrow it to configurations that make semantic and architectural sense.
For the new cases, several filters apply.
Filter 1: non-persistent variants are excluded
Activation-scale HBM swizzling requires the persistent path.
is_persistent = False
→ skipThis removes half of each case’s variants.
48 combinations per RHS dtype
→ 24 remainThe test is not permitted to “almost” reproduce the target path through a non-persistent fallback.
Filter 2: block_m = 16 is excluded
The Blackwell activation-scale-swizzled layout requires:
block_m >= 128The parameter matrix offers:
block_m = 16
block_m = 128Therefore:
block_m = 16
→ skip
block_m = 128
→ continueThe surviving set is halved again.
24
→ 12 combinations per RHS dtypeThe test is now in the same 128-row tile region involved in the TMEM failure.
Filter 3: gather variants are excluded
The activation-scale-swizzled path does not support gathered activations.
Therefore:
gather only
→ skip
gather + scatter
→ skipThe original six operation combinations become four:
plain/no scatter
scatter
pad_b
pad_aFilter 4: pad_b is excluded for a microscaled activation
The activation dtype is:
nvfp4_e2m1The test’s dtype wrapper marks NVFP4 as a microscaled input.
For an inner-expert case with a microscaled activation, the generic matrix supports:
pad_abut not:
pad_bThe skip rule is:
activation has MX-style scale
and
inner_expt_opt != "pad_a"
→ skipThe four surviving operation combinations become three:
1. ordinary routed variant
2. scatter variant
3. pad_a inner-expert variantGamma doubles each surviving path
The matrix separately tests:
do_gamma = False
do_gamma = TrueNo rule excludes gamma for these cases.
Each of the three surviving operation variants is therefore executed twice:
ordinary
→ gamma off / gamma on
scatter
→ gamma off / gamma on
pad_a
→ gamma off / gamma onThat produces:
3 operation variants
×
2 gamma settings
=
6 executable variants per RHS dtypeThere are two RHS dtypes:
BF16
FP16So:
6 × 2
=
12 executable variantsThe remaining:
96 - 12
=
84are expected skips.
The twelve active variants can be summarized explicitly
For dense BF16 RHS:
1. ordinary routed, gamma off
2. ordinary routed, gamma on
3. scatter, gamma off
4. scatter, gamma on
5. pad_a / inner-expert K-ragged, gamma off
6. pad_a / inner-expert K-ragged, gamma onThe same six are generated for dense FP16 RHS.
6 BF16 variants
+
6 FP16 variants
=
12The PR summary confirms that the active set includes:
scatter
gamma
K-ragged variantsWhy pad_a represents the K-ragged boundary
The input activation has logical shape:
A[M, K]In ordinary ragged mode, routing is generally applied along the row or M dimension.
When inner_expt_opt is active, the test changes the activation’s ragged dimension.
For pad_a, it constructs the activation with:
ragged_dim = 1and enables ragged padding on A.
Dimension 1 of A[M, K] is K.
The generated variant therefore exercises an inner-expert or K-ragged boundary rather than only routed M rows.
This matters because K-ragged handling changes which operand regions are valid and how padded work participates in the matmul.
Scatter changes where output rows are written
When do_scatter is active, the matrix creates a random permutation of output-row positions.
logical computed row
→ scatter index
→ physical output rowThe core dot operation still uses the same persistent NVFP4 × dense planner region.
But the output path carries additional indexing and storage behavior.
A planner repair that works only for the simplest output route would not be sufficient.
The scatter variants show that the TMEM-safe tile remains compatible with a nontrivial output placement path.
Gamma adds a per-row scaling input
When do_gamma is active, the matrix creates gamma values for the output rows and passes them into the matmul path.
matmul result
×
per-row gammaThe public source does not state that gamma changes the specific accumulator-versus-expanded-operand TMEM split.
Its role in the matrix is broader:
The planner fix should not break a supported fused numerical variant.Both gamma settings failed before the resource repair and passed after it as part of the same generated matrix.
The ordinary variants also exercise the shared bias route
For cases without inner-expert padding, the shared test setup enables bias by default.
inner_expt_opt is None
→ bias is createdThe ordinary and scatter variants therefore do not represent a completely empty epilogue.
They exercise:
NVFP4 × dense matmul
+
bias
+
optional gamma
+
optional scatterThe pad_a inner-expert variants disable that default bias route because inner_expt_opt is present.
This gives the active twelve cases more epilogue diversity than two isolated dtype tests would suggest.
The same root failure appeared across every active variant
The twelve pre-fix failures were all classified as:
Tensor Memory exhaustionThe required allocations were:
8 variants
→ 640 columns
4 variants
→ 576 columnsagainst a hardware budget of:
512 columnsThis consistency matters.
The failures did not divide into:
numerical mismatch for gamma
scatter indexing failure
routing mismatch
K-ragged reference failure
unsupported FP16 RHSAll active variants reached the same resource boundary.
That strongly localized the defect to planning and lowering rather than to one epilogue feature.
The public record does not map every variant to 576 or 640
The PR reports the aggregate distribution:
8 require 640
4 require 576It does not publish a complete table mapping each of the twelve generated pytest IDs to one of those column totals.
It would therefore be speculative to claim:
gamma caused 640 columnsor:
K-ragged caused 576 columnswithout additional compiler artifacts.
The supported conclusion is narrower:
Every executable variant exceeded the 512-column limit, with the generated requests falling into two observed totals.
The exact per-variant allocation breakdown remains outside the published evidence.
The test-only change created a negative control
A particularly strong aspect of the validation is the order of operations.
The PR history contains a separate commit titled:
Cover NVFP4 dense matmul in the existing test matrixThat commit added the two cases to the shared matrix and removed the bespoke standalone setup.
It did not contain the final planner fix.
The new test coverage was then run against a compiler built from the relevant unmodified main revision.
The result was:
12 failures
all TMEM exhaustionThe fixed branch ran the identical cases:
12 passed
84 skippedThis is stronger than presenting only an after-the-fix PASS.
Same test inputs
Same target hardware
Same test matrix
Old planner
→ fails
New planner
→ passesThe planner change controls the outcome.
Why an after-only passing test would be weaker
Suppose only the fixed branch had been tested.
The twelve cases might pass because:
the test never reached the problematic path,
skip rules excluded the important variants,
the planner selected a different route for an unrelated reason,
or the new cases were not actually sensitive to
block_n.
The negative control demonstrates otherwise.
Coverage commit + old planner
→ all active cases failTherefore the cases did reach the invalid resource plan.
Same coverage + fixed planner
→ all active cases passTherefore the new safeguard closed the observed boundary.
Moving into the shared matrix also prevented one-path overfitting
A standalone test could have exercised:
one routing configuration
one gamma setting
one output route
one K shape
one epilogue
two RHS dtypesThe shared matrix inherited multiple orthogonal variants automatically.
scatter
gamma
inner-expert K-ragged
persistent guard
block_m guard
dtype specializationThis made it harder for the fix to succeed only on the one hand-constructed configuration.
The test matrix asked whether the planner rule remained valid while neighboring parts of the operation changed.
The skip logic is part of the regression contract
A large parameter matrix can create invalid combinations.
Simply running every Cartesian product would produce noise.
The skip rules encode supported relationships.
Activation-scale swizzling
→ requires persistent execution
Activation-scale swizzling
→ requires block_m >= 128
Activation-scale swizzling
→ does not support gather
Microscaled activation in inner-expert mode
→ requires pad_aThese skips ensure that the twelve active cases are not arbitrary survivors.
They are the combinations for which the tested Blackwell route is intended to exist.
A future source change that alters one of these contracts may also alter which variants run.
The reported 12/84 distribution therefore belongs to the merged test and validation environment, not an eternal property of the test name.
Skipped variants still document unsupported boundaries
A skip such as:
X swizzling does not support gathered activationsdoes not prove gather correctness.
It records that gather lies outside the current route’s supported contract.
Likewise:
block_m < 128
→ skipdocuments a layout requirement.
The matrix therefore serves two purposes.
Executable variants
→ regression coverage
Skipped variants
→ declared unsupported combinationsBoth are useful, provided they are not confused.
The old 128×128×128 cases were still necessary
The new tests do not make the earlier plain tests obsolete.
The cases protect different regions.
Earlier cases
small N
plain layout
basic NVFP4 × BF16 execution
fiber and non-fiber scale variantsNew cases
N = 256
ragged routing
activation-scale swizzling
persistent execution
BF16 and FP16 RHS
scatter, gamma, and K-ragged variantsRemoving the smaller cases would trade one coverage region for another.
A useful test matrix accumulates distinct contracts rather than replacing every simple case with a complex one.
The matrix also compared Triton with a reference implementation
The generic test does not stop after kernel construction.
It creates:
Triton output
and
PyTorch/reference outputand compares them through the existing tolerance machinery.
For NVFP4 and other microscaled formats, the shared test infrastructure also handles:
quantization,
scales,
layout conversion,
optional output re-expansion,
and format-specific tolerances.
The fixed twelve cases therefore establish both:
kernel can be allocatedand:
the resulting output agrees with the shared reference contractThey are not compile-only planner tests.
Feasibility and numerical correctness are different checks
Before the fix, the operation failed before a meaningful numerical comparison could complete.
Invalid TMEM plan
→ kernel cannot runAfter the fix:
Valid TMEM plan
→ kernel executes
→ output compared with referenceThe regression therefore crosses two boundaries.
1. Resource feasibility
2. Numerical correctness after executionA planner fix that merely avoids the compiler error but produces incorrect output would still fail the matrix.
The broader related matrix protected neighboring policy
The change touched a shared planner condition involving both:
MXFP × dense
and
NVFP4 × denseThe validation therefore extended beyond the twelve new executions.
The PR reports:
NVIDIA planner and split-K tests:
16 passed, 4 skippedand:
related MXFP/NVFP4 × dense matmul cases:
444 passed, 516 skippedThis broader run was important because the patch introduced:
a new NVFP4 predicate
and
a shared OR at the block_n safeguardAn overly broad predicate could have changed existing MXFP tuning or unrelated NVFP4 paths.
The larger matrix provided evidence that the local fix did not create an obvious neighboring regression.
Why 516 skipped cases do not invalidate the broader run
The related matrix includes many combinations that are unsupported on a given architecture, layout, dtype pair, or execution mode.
The large skip count reflects that broad Cartesian test design.
The useful factual statement is not:
All theoretically possible matmuls were tested.It is:
Every combination deemed executable by the current guards passed,
and unsupported combinations were skipped according to explicit rules.The skip reasons remain part of the test contract and should be inspected when evaluating missing coverage.
The test matrix is effectively a dispatch specification
Each Case and pytest parameter combination describes more than tensor values.
It selects:
shape
data format
scale format
routing mode
layout
persistent policy
tile constraint
epilogue features
output behaviorTogether, those values determine which planner and lowering branch executes.
The matrix therefore behaves like an executable dispatch specification.
Input configuration
→ supported or skipped
→ planner selection
→ lowering
→ resource allocation
→ result comparisonA test named only by dtype would not capture this complexity.
Test coverage must include the conditions that created the plan
The original defect was not:
NVFP4 matmul is always broken.It was:
NVFP4 activation
+
dense FP16/BF16 RHS
+
Blackwell
+
persistent execution
+
scale-swizzled activation
+
N large enough for block_n = 256
→ impossible automatic TMEM planA regression fixture must preserve those predicates.
Remove one, and the test may silently enter a valid but irrelevant path.
Use N = 128
→ no 256-column default
Remove scale swizzling
→ persistence may no longer be required
Use non-persistent execution
→ different TMEM behavior
Use FP4 RHS
→ different dot pathThe new case records the planner entrance, not merely the public operator.
The most valuable two lines were the ones that selected the path
The code addition is compact:
Case(
256,
256,
128,
"ragged",
"nvfp4_e2m1",
rhs_dtype,
"bfloat16",
a_hbm_swizzling=True,
)The critical information is not only:
256 × 256 × 128It is the combination:
ragged
NVFP4 on A
dense BF16 or FP16 on B
activation-scale swizzling
BF16 outputThose fields cause the generic matrix to manufacture the actual resource-sensitive execution path.
The patch repaired observability as well as planning
Before the new coverage:
The planner had an invalid NVFP4 persistent region.
The existing matrix did not enter it.After the coverage and fix:
The matrix enters the region.
Old planner produces 12 failures.
New planner produces 12 passes.
Neighboring planner and format matrices remain green.The resource rule became observable and repeatable.
A hidden planner assumption became a regression contract.
What the test evidence directly confirms
The source and PR validation confirm that:
two new top-level cases were added for BF16 and FP16 dense right-hand operands,
both use
M=256,N=256,K=128,both use ragged mode and activation-scale HBM swizzling,
the shared pytest matrix expands them across multiple parameter dimensions,
the GB300 run produced 96 collected variants,
84 were skipped by explicit path guards,
12 reached execution,
all 12 failed with TMEM exhaustion before the fix,
all 12 passed after the fix,
the active set included scatter, gamma, and K-ragged variants,
and the broader related matrices also passed their executable cases.
What the test evidence does not confirm
The public material does not establish:
the exact 576- or 640-column allocation for each individual pytest variant,
that every routed NVFP4 workload is represented,
that every gamma or scatter implementation uses identical TMEM,
that
block_n = 128is the fastest feasible tile,that explicit
block_n = 256constraints are covered by these cases,that every Blackwell model uses SM 10.3 behavior identical to GB300,
that every skipped combination is permanently unsupported,
or that all future expanded-operand paths are automatically protected by this predicate.
The regression proves the documented planner region and generated matrix.
It does not convert one hardware result into a universal theorem about every FP4 matmul.
The complete three-part structure
Part 1 — The accumulator was not the only TMEM resident
block_n = 256
→ large accumulator
+
expanded NVFP4 operand in TMEM
→ 576 or 640 columns requested
→ 512-column budget exceededPart 2 — Shared hardware pressure did not justify shared tuning identity
MXFP and NVFP4
→ both need accumulator headroom
but
→ do not automatically share block_k or num_warpsThe patch capped only the planner-owned default and preserved explicit constraints.
Part 3 — The test had to enter the exact planner region
Old coverage:
plain 128 × 128 × 128
→ safe regionNew coverage:
ragged 256 × 256 × 128
+
activation-scale swizzling
+
persistent execution
+
BF16/FP16 RHS
→ failing planner regionThe shared matrix expanded two cases into twelve active negative controls.
The final lesson is that a test case is often a test generator
A line such as:
Case(...)does not necessarily represent one execution.
Inside a parameterized systems test, it can represent a family of contracts.
One case definition
↓
tile dimensions
routing variants
epilogue variants
layout guards
persistent policy
backend guards
↓
many collected executions
↓
supported subset reaches the kernelThe useful question is not:
How many Case objects were added?It is:
Which execution paths did those cases generate,
which were skipped,
and did the active set fail before the fix?For Triton #11171, the answer was unusually clear.
Two top-level cases
→ 96 collected variants
→ 84 expected skips
→ 12 active executions
→ 12 old-planner TMEM failures
→ 12 fixed-planner passesA regression test is strongest when it preserves the dispatch conditions that created the failure and demonstrates that the same generated executions fail before the repair and pass afterward.
The planner fix made the tile valid.
The shared matrix made the validity boundary visible.
Previous articles
Triton #11171 — Why a Larger
block_nExhausted Blackwell’s 512-Column Tensor MemoryTriton #11171 — Why the Planner Capped Only the Default
block_nand Kept NVFP4 Separate from MXFP
Related material
Patch status: Merged into Triton main
Top-level regression cases: Two — dense BF16 and dense FP16 RHS
Core shape: M=256, N=256, K=128
Execution mode: Ragged, activation-scale-swizzled, persistent Blackwell path
GB300 parameter expansion: 96 collected variants
Expected skips: 84
Active variants: 12
Pre-fix result: All 12 failed with Tensor Memory exhaustion
Observed allocation requests: Eight at 640 columns and four at 576 columns
Post-fix result: All 12 active variants passed
Active feature coverage: Scatter, gamma, and K-ragged variants across BF16 and FP16 RHS
Broader validation: 16 planner/split-K passes and 444 related MXFP/NVFP4 × dense passes among supported combinations
This is Part 3 and the final article in the Triton Blackwell NVFP4 × dense matmul series.
Part 1 examined why a 256-column accumulator tile and an expanded operand could not coexist within Blackwell’s 512-column TMEM budget.
Part 2 examined why the repair changed only the planner-owned default and shared one TMEM rule without merging NVFP4 into every MXFP tuning policy.
This final article examined why the existing plain NVFP4 cases did not enter the failing planner region, how two routed and scale-swizzled cases expanded into twelve active executions, and why the old-planner failures formed a strong negative control for the final fix.
#Triton #NVIDIA #Blackwell #GB300 #NVFP4 #FP4 #TensorMemory #TMEM #RegressionTesting #KernelPlanning #MatrixMultiplication #CodeAnalysis