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 TMEM

The automatic planner could select:

block_n = 256

That tile looked reasonable for:

N = 256

because one tile could cover the full output width.

But the persistent lowering needed Tensor Memory for two live structures:

output accumulator

+

expanded operand

The combined allocation exceeded Blackwell’s 512-column TMEM limit.

PR #11171 repaired the automatic plan by capping the default:

block_n
→ no larger than 128

for 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 variants

Against 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 runs

It 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 = BF16

There were variants for:

nvfp4_e2m1

nvfp4_e2m1_fiber

Those 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 path

Testing 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 = float16

The important properties were:

M = 256
N = 256
K = 128
mode = ragged
NVFP4 activation
dense BF16 or FP16 RHS
activation-scale HBM swizzling enabled
output dtype = BF16

Each field moved the case closer to the affected lowering.


N = 256 exposed the dangerous default

The earlier cases used:

N = 128

A planner ceiling of 128 is naturally sufficient for such a shape.

The new cases used:

N = 256

That allowed the default N-tile logic to propose:

block_n = 256

The test therefore crossed the exact shape boundary needed to expose the oversized accumulator tile.

Old coverage:

N = 128
→ 128-column tile is sufficient
New coverage:

N = 256
→ planner can select a 256-column tile
→ combined TMEM allocation becomes invalid

The 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=True

In 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 disabled

These 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 required

The 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 = ragged

For 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 slice

than 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 contracts

The 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 128
operation combination:
6 variants
gamma:
False or True
persistent:
False or True
num_warps:
4 or 8 only on Hopper,
otherwise None

The 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 mode

On 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:

None

rather 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 value

Therefore:

2 × 2 × 6 × 2 × 2 × 1
=
96 collected variants

This 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 skipped

after the fix.

12 + 84
=
96

The 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
→ skip

This removes half of each case’s variants.

48 combinations per RHS dtype

→ 24 remain

The 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 >= 128

The parameter matrix offers:

block_m = 16

block_m = 128

Therefore:

block_m = 16
→ skip

block_m = 128
→ continue

The surviving set is halved again.

24

→ 12 combinations per RHS dtype

The 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
→ skip

The original six operation combinations become four:

plain/no scatter

scatter

pad_b

pad_a

Filter 4: pad_b is excluded for a microscaled activation

The activation dtype is:

nvfp4_e2m1

The test’s dtype wrapper marks NVFP4 as a microscaled input.

For an inner-expert case with a microscaled activation, the generic matrix supports:

pad_a

but not:

pad_b

The skip rule is:

activation has MX-style scale

and

inner_expt_opt != "pad_a"

→ skip

The four surviving operation combinations become three:

1. ordinary routed variant

2. scatter variant

3. pad_a inner-expert variant

Gamma doubles each surviving path

The matrix separately tests:

do_gamma = False

do_gamma = True

No 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 on

That produces:

3 operation variants

×

2 gamma settings

=
6 executable variants per RHS dtype

There are two RHS dtypes:

BF16

FP16

So:

6 × 2
=
12 executable variants

The remaining:

96 - 12
=
84

are 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 on

The same six are generated for dense FP16 RHS.

6 BF16 variants

+

6 FP16 variants

=
12

The PR summary confirms that the active set includes:

scatter

gamma

K-ragged variants

Why 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 = 1

and 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 row

The 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 gamma

The 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 created

The ordinary and scatter variants therefore do not represent a completely empty epilogue.

They exercise:

NVFP4 × dense matmul

+

bias

+

optional gamma

+

optional scatter

The 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 exhaustion

The required allocations were:

8 variants
→ 640 columns

4 variants
→ 576 columns

against a hardware budget of:

512 columns

This consistency matters.

The failures did not divide into:

numerical mismatch for gamma

scatter indexing failure

routing mismatch

K-ragged reference failure

unsupported FP16 RHS

All 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 576

It 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 columns

or:

K-ragged caused 576 columns

without 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 matrix

That 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 exhaustion

The fixed branch ran the identical cases:

12 passed

84 skipped

This is stronger than presenting only an after-the-fix PASS.

Same test inputs

Same target hardware

Same test matrix

Old planner
→ fails

New planner
→ passes

The 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 fail

Therefore the cases did reach the invalid resource plan.

Same coverage + fixed planner
→ all active cases pass

Therefore 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 dtypes

The shared matrix inherited multiple orthogonal variants automatically.

scatter

gamma

inner-expert K-ragged

persistent guard

block_m guard

dtype specialization

This 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_a

These 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 activations

does not prove gather correctness.

It records that gather lies outside the current route’s supported contract.

Likewise:

block_m < 128
→ skip

documents a layout requirement.

The matrix therefore serves two purposes.

Executable variants
→ regression coverage

Skipped variants
→ declared unsupported combinations

Both 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 variants

New cases

N = 256

ragged routing

activation-scale swizzling

persistent execution

BF16 and FP16 RHS

scatter, gamma, and K-ragged variants

Removing 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 output

and 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 allocated

and:

the resulting output agrees with the shared reference contract

They 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 run

After the fix:

Valid TMEM plan
→ kernel executes
→ output compared with reference

The regression therefore crosses two boundaries.

1. Resource feasibility

2. Numerical correctness after execution

A 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 × dense

The validation therefore extended beyond the twelve new executions.

The PR reports:

NVIDIA planner and split-K tests:
16 passed, 4 skipped

and:

related MXFP/NVFP4 × dense matmul cases:
444 passed, 516 skipped

This broader run was important because the patch introduced:

a new NVFP4 predicate

and

a shared OR at the block_n safeguard

An 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 behavior

Together, 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 comparison

A 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 plan

A 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 path

The 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 × 128

It is the combination:

ragged

NVFP4 on A

dense BF16 or FP16 on B

activation-scale swizzling

BF16 output

Those 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 = 128 is the fastest feasible tile,

  • that explicit block_n = 256 constraints 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 exceeded

Part 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_warps

The 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 region
New coverage:

ragged 256 × 256 × 128
+
activation-scale swizzling
+
persistent execution
+
BF16/FP16 RHS
→ failing planner region

The 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 kernel

The 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 passes

A 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_n Exhausted Blackwell’s 512-Column Tensor Memory

  • Triton #11171 — Why the Planner Capped Only the Default block_n and 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