Intel Triton XPU #7791 — Why Every Mask Had to Be Validated Before the Loop Was Mutated

Intel Triton XPU RemoveMasks, Part 3 of 3 — Mutation-free preflight, result-less scf.if regions, unsupported stores, and a walker whose current loop had already been erased

The first two articles in this series examined two kinds of failure in Intel Triton’s TritonIntelRemoveMasks pass.

Part 1 examined an invalid replacement:

Always-false masked load
+
no `other` operand
        ↓
null value passed into RAUW
        ↓
compiler crash

Part 2 examined an invalid proof:

Mask resembles a canonical boundary expression

but

its `N`, tile width, or predicate
does not match the generated guard
        ↓
mask can be removed without being implied

PR #7791 also fixed a third boundary.

Even a correct proof and a correct local rewrite can fail if the transformation changes the IR before it has established that the whole transformation is legal.

Loop versioning is not a one-operation edit.

The pass creates a new condition, inserts an scf.if, clones the original loop into two branches, removes masks from the fast-path clone, replaces the original loop’s results, and then erases the original loop.

Original loop
        ↓
prove fast-path condition
        ↓
create `scf.if`
        ↓
clone loop twice
        ↓
remove masks from then branch
        ↓
replace uses
        ↓
erase original loop

Every step changes the graph.

The old implementation could begin this process after considering only one representative mask, even though it would later remove the mask from every collected load.

It could also:

  • collect masked stores even though the versioner could not rewrite them,

  • add a second scf.yield to a no-result scf.if whose builder had already created an implicit terminator,

  • and tell a pre-order walker to continue into a loop that the callback had just erased.

These were not four unrelated coding mistakes.

They were all violations of one transformation rule:

Before mutating IR, a pass must prove that every object it will rewrite is supported, every region it will construct is structurally valid, and every operation it will continue walking still exists.

PR #7791 added that discipline to RemoveMasks. The merged PR describes six defects reproduced on valid MLIR, including validation-before-mutation, the duplicate-yield failure, unsupported store collection, and walker handling after loop erasure.


Loop versioning changes the whole loop at once

Consider a loop with one canonical masked load:

%result = scf.for %iv = %c0 to %ub step %c1
    iter_args(%acc = %init)
    -> tensor<32xf16> {

  %remaining = %N - %iv * %c32
  %mask = %range < splat(%remaining)

  %value = tt.load %ptrs, %mask, %zero
  %next = arith.addf %acc, %value

  scf.yield %next
}

The optimizer may generate a scalar condition such as:

N % 32 == 0

and

N > 32

Then it creates two loop versions.

Condition true
→ all tiles complete
→ fast loop uses an unmasked load
Condition false
→ original masked loop remains

Conceptually:

%result = scf.if %full_tiles
    -> tensor<32xf16> {

  %fast = scf.for ... {
    %value = tt.load %ptrs
    ...
  }

  scf.yield %fast

} else {

  %safe = scf.for ... {
    %value = tt.load %ptrs, %mask, %zero
    ...
  }

  scf.yield %safe
}

The fast path is correct only if its scalar condition proves every mask the pass removes.

The transformation does not version each load independently.

It versions one entire loop.


One branch condition governs every collected load

Suppose the loop contains two masked loads.

Load A

mask A
→ depends on N_A and END_A
Load B

mask B
→ depends on N_B and END_B

The versioner creates one scf.if.

one condition
        ↓
one fast loop clone
        ↓
masks removed from A and B

The required proof is therefore:

condition
⇒
mask A is true for every lane

and:

condition
⇒
mask B is true for every lane

It is not sufficient to prove only:

condition
⇒
mask A

and then remove both masks because both expressions looked canonical.

The mutation set defines the proof set.

Masks that will be removed

=

Masks that must be validated

The old versioner selected one collected mask as its representative

Before the fix, the canonical versioner effectively did this:

Collect masked operations that resemble canonical masks
        ↓
select one collected operation
        ↓
derive a versioning condition from that mask
        ↓
create the versioned loop
        ↓
drop the mask from every collected operation

The selected mask supplied the branch condition.

The remaining collected masks were not all checked against the loop upper bound before the rewrite began.

This created an asymmetry.

Proof derived from:
one mask
Mutation applied to:
all masks

A representative condition is valid only when the pass has already established that every candidate belongs to the same proof class.

The old code assumed that relationship from local pattern recognition.


Local mask validity and whole-loop versionability are different

MaskedOpsCollector already used the associated validator when deciding whether an operation looked like a candidate.

For a canonical tail mask, that local analysis could establish facts such as:

The mask has the expected range expression.

The comparison uses the expected strict less-than form.

The loop induction variable appears in the expected arithmetic.

That makes the mask a plausible candidate.

It does not yet prove:

The loop upper bound uses the same `N`.

The loop upper bound uses the same tile width.

The scalar condition generated for this loop implies this mask.

The distinction is:

isValidMask()

→ Does this operation locally resemble
   a canonical masked access?
canVersion()

→ Can this loop’s versioning condition
   safely remove this exact mask?

PR #7791 added the second question as a mutation-free preflight.


The two-mask regression demonstrates the whole-loop obligation

The regression two-canonical-masks.mlir contains two loads inside one loop.

Load A

N = %arg2

END = 32

Its mask matches the loop upper bound:

(%arg2 + 31) / 32

Load B

N = %arg3

END = 64

Its mask does not follow from the same upper bound.

Both masks have a canonical-looking form.

range(0, END)
<
N - iv × END

But they use different symbols and different tile widths.

If the pass builds the fast-path condition from Load A:

%arg2 % 32 == 0

that says nothing about whether Load B’s sixty-four lanes are valid under %arg3.

Because the fast-path clone would remove both masks, the only correct result is:

do not version the loop

The regression therefore requires:

no `scf.if`

both masked loads remain

The old transformation could cross the mutation boundary too early

Generating a versioning condition is itself an IR mutation.

For a canonical loop, the pass may create operations equivalent to:

%remainder =
    arith.remsi %N, %END

%divisible =
    arith.cmpi eq, %remainder, %zero

%large_enough =
    arith.cmpi sgt, %N, %END

%condition =
    arith.andi %divisible, %large_enough

These operations are inserted before the original loop.

If the pass later discovers that another mask cannot be versioned, it would need to:

  • remove the newly created condition operations,

  • restore every changed use,

  • remove any partially built regions,

  • and guarantee that no mutation escaped.

The simpler and safer design is:

Phase 1:
validate without creating IR
Phase 2:
after every validation succeeds,
create the condition and perform the rewrite

That is the role of canVersion().


canVersion() performs a read-only preflight

The new method checks:

Does the mask resolve to a usable final value?

Does it satisfy the canonical-mask structure?

Does the loop upper bound have the matching canonical form?

Do `N` and `END` match?

It does not create:

  • a remainder operation,

  • a comparison,

  • an and,

  • an scf.if,

  • or a cloned loop.

Conceptually:

bool canVersion(loop, mask) {
    if (!isValidMask(loop, mask))
        return false;

    return hasCanonicalUpperBound(
        loop,
        getMaskInfo(loop, mask)
    );
}

The versioner first calls this preflight for every collected operation.

for every collected load:

    if canVersion(load mask) is false:
        return without changing IR

Only after every mask passes does it call the mutating:

getVersioningCond()

for one representative mask.

The landed source explicitly documents canVersion() as the non-mutating counterpart to getVersioningCond().


A failed preflight now leaves the original loop intact

The new sequence is:

Collect candidates
        ↓
validate Mask A without mutation
        ↓
validate Mask B without mutation
        ↓
one fails
        ↓
return false
        ↓
original loop remains exactly as it was

There is no temporary versioning condition left behind.

There is no partial branch.

There is no fast-path clone containing only some transformed loads.

This gives the pass a transaction-like boundary.

Before commit:
analysis only
After every precondition succeeds:
perform complete rewrite

MLIR does not automatically make arbitrary pass mutations transactional.

The pass must choose where its commit point begins.


After preflight, one condition is sufficient

Why can the pass still generate only one versioning condition after checking all masks?

Because the new preflight establishes that every accepted mask shares the loop upper bound’s:

N

and

END

For canonical masks in the same loop:

same N

+

same END

→ same generated full-tile condition

The conditions would be identical.

The pass therefore does not need:

condition A
and
condition B
and
condition C

when every condition represents the same fact.

It needs to prove the equivalence first.

Before validation:

one representative condition
→ insufficient
After every mask is proven to use the same N and END:

one representative condition
→ sufficient

The important change was not the number of conditions.

It was the proof that one condition legitimately represented the complete mutation set.


Validation-before-mutation also improves failure locality

Suppose one loop contains ten masks.

Nine are valid.

The tenth is not.

A rewrite that mutates as it checks can fail after performing substantial work.

create condition 1

clone loop

rewrite mask 1

rewrite mask 2

...

reach mask 10

discover failure

Now the pass needs rollback logic.

With preflight:

check mask 1

check mask 2

...

check mask 10

reject

No rollback is required.

The failure remains located in analysis rather than becoming an IR-repair problem.


The collector must not promise operations the rewriter cannot handle

A second contract existed between:

MaskedOpsCollector

and:

LoopVersioner

The collector determines which operations the transformation intends to rewrite.

Before #7791, it collected:

tt.load

tt.store

arith.select

across mask validators.

But the loop-versioning implementation knew how to remove a mask only from:

tt.load

Its fast-path rewrite created a replacement unmasked load.

The store path contained only a remaining:

TODO

There was no corresponding transformation that recreated an unmasked tt.store.

The collector’s declared scope was therefore broader than the consumer’s implementation.


Collection is a capability claim

Once an operation enters the collected set, the rest of the pass assumes:

This operation is supported by the upcoming transformation.

That assumption affects:

  • proof construction,

  • branch creation,

  • operation mapping into the cloned loop,

  • and fast-path mask removal.

A collector is not merely a search helper.

It defines the rewrite domain.

Collected operation

→ pass claims it knows how to transform it

If the pass cannot perform that transformation, the operation should not enter the set.


A store-only loop exposed the scope mismatch

The new regression contains a loop with only one masked store.

scf.for ... {

    %mask = ...

    tt.store %ptrs, %value, %mask
}

The store uses a canonical-looking tail mask.

But the versioner has no implemented store-mask removal.

The correct behavior is:

keep the original loop

keep the masked store

do not create a versioning branch

The regression explicitly checks:

no `scf.if`

one `scf.for`

masked `tt.store` remains

Versioning a store-only loop would not create a fast path

Imagine cloning the store-only loop into:

then branch

else branch

but leaving the store mask in both.

Then:
masked store
Else:
same masked store

The scalar condition now adds control flow without removing the operation cost it was created to avoid.

At best:

the transformation is pointless

At worst, unsupported assumptions elsewhere in the versioner can produce invalid IR.

A compiler pass should not version an operation merely because it can recognize the mask.

It should version it only when it can complete the intended rewrite.


The patch narrows collection to implemented operations

The merged collector always gathers:

masked `tt.load`

For the RemovableMaskValidator, which uses the direct dropMask() helper, it also gathers:

arith.select

The direct helper can replace:

  • an always-true or always-false load result,

  • and a select result.

The loop versioners receive only loads because their fast-path rewrite is implemented only for loads.

Masked stores are no longer collected.

The code also simplifies later assertions and casts accordingly:

versioner input
→ guaranteed `tt.load`

rather than:

load or store,
with a store path that is not actually implemented

Fail-closed does not mean stores can never be optimized

The patch does not prove:

A masked store can never be safely unmasked.

It establishes:

This versioner did not implement that rewrite.

A future store optimization would need to define:

  • which store masks are provably true,

  • how the unmasked store is constructed,

  • whether the store has side-effect ordering constraints,

  • how aliasing and memory effects are preserved,

  • and which regression tests protect the path.

Until then:

unsupported store
→ leave it unchanged

is the correct compiler result.


A result-less scf.if already has a terminator contract

The third failure involved SCF region construction.

A versioned loop may produce values.

For example:

%result = scf.for ...
    iter_args(%acc = %init)
    -> tensor<32xf16> {
    ...
    scf.yield %next
}

Versioning such a loop creates an scf.if that also produces a value.

%result = scf.if %condition
    -> tensor<32xf16> {

    %fast = scf.for ...
    scf.yield %fast

} else {

    %safe = scf.for ...
    scf.yield %safe
}

Each branch must explicitly yield the value corresponding to the scf.if result.

That is valid.

But a loop may also produce no SSA results.


A loop can perform useful work without returning a value

The regression’s second function contains:

masked load

then

unmasked store

inside the loop.

scf.for ... {

    %value =
        tt.load %input_ptrs, %mask, %zero

    tt.store %output_ptrs, %value
}

The loop has no iter_args.

It produces no result.

Its effect is the output store.

Versioning it still makes sense.

Fast branch
→ unmasked load
→ store
Fallback branch
→ masked load
→ store

But the surrounding scf.if also has no results.

scf.if %condition {

    scf.for ... {
        ...
    }

} else {

    scf.for ... {
        ...
    }
}

No value needs to be yielded from either branch.


No-result regions receive an implicit scf.yield

The SCF builder supplies a zero-operand terminator for a no-result scf.if region.

Conceptually:

scf.if %condition {

    ...

    scf.yield

} else {

    ...

    scf.yield
}

The terminator is structurally required.

But because it carries no values, the builder can create it automatically.

The transformation must not add another one.


The old versioner added an explicit yield unconditionally

The old canonical versioner cloned the loop into both branches and then created:

scf::YieldOp

for the then region and the else region regardless of result count.

For a result-bearing scf.if, that was necessary.

For a no-result scf.if, each region already contained its implicit terminator.

The result was effectively:

scf.if %condition {

    scf.for ...

    scf.yield

    scf.yield
}

A block cannot have two valid terminators.

The first terminator ends the block.

A second operation after it produces invalid SCF structure.

PR #7791 lists this as a confirmed crash-producing defect on valid MLIR.


The fix checks the result arity before creating yields

The repaired source asks:

if (ifOp.getNumResults() != 0) {
    create then yield;
    create else yield;
}

The two cases are now explicit.

Result-bearing scf.if

number of results > 0
→ create yields carrying cloned-loop values

Result-less scf.if

number of results = 0
→ use the implicit terminators already created by the builder

The pass no longer treats:

zero results

as though it were:

a result list that happens to contain zero operands
and therefore needs another explicit operation

The region builder already owns that case.


Result count is part of region construction

Compiler transformations often focus on operation types:

Create an `scf.if`.

Clone an `scf.for`.

Insert a yield.

But operation arity changes the structural contract.

scf.if with results

≠

scf.if without results

They share one operation name.

Their terminator obligations differ.

This is similar to optional operands in Part 1.

Optional `other`
→ changes replacement construction
Optional result list
→ changes region termination construction

A generic rewrite must preserve both forms.


The regression checks for the absence of a stray yield

The load_only_no_results test does more than verify that triton-opt no longer crashes.

It checks the generated versioning condition:

N % 32 == 0

and

N > 32

It then checks:

`scf.if` has no result

fast branch contains an unmasked load

fallback branch contains the original masked load

Most importantly, it requires that no extra explicit:

scf.yield

appears after either cloned loop and before the region closes.

The test therefore protects:

valid fast-path transformation

and

valid SCF terminator structure

A process that does not crash can still emit invalid IR

A transformation bug does not always fail at the point where it is created.

The pass may construct malformed IR and continue.

A later verifier or pass then reports the failure.

RemoveMasks creates duplicate terminator
        ↓
another stage verifies SCF
        ↓
compiler fails later

That can obscure the origin.

The structural FileCheck test makes the intended region form explicit at the pass boundary that creates it.


A walker cannot descend into an operation that has been erased

The fourth lifecycle problem involved the pass driver.

TritonIntelRemoveMasks walks the module in pre-order.

Visit parent operation

then

visit its nested operations

When the callback encounters an scf.for, it may call:

LoopVersioner::version(forOp, collector)

A successful versioning operation:

  • creates a new scf.if,

  • clones the loop into two branches,

  • redirects uses,

  • and erases the original forOp.

The walker callback is therefore invoked with an operation that may cease to exist before the callback returns.


Pre-order traversal normally expects to visit the current operation’s children

A simplified pre-order walk is:

visit current operation
        ↓
callback returns “advance”
        ↓
walk current operation’s regions
        ↓
continue with later siblings

This is safe when the current operation still exists.

After successful versioning:

current operation
→ erased

Its original regions and body are no longer valid traversal roots.

Returning:

WalkResult::advance()

would tell the walker:

continue descending through this operation

even though the transformation had just deleted it.


The pass now returns WalkResult::skip() after versioning

The repaired callback checks the versioner’s Boolean result.

bool loopVersioned =
    LoopVersioner::version(forOp, collector);

if (loopVersioned)
    return WalkResult::skip();

Here, skip() means:

Do not descend into the current operation’s nested regions.

It does not mean:

Stop the entire module walk.

The walker can continue to later sibling operations.

This is the correct response to:

the current operation has been replaced and erased

The fix was applied to both canonical-mask and invariant-mask versioning walks.


Erasing the current operation changes traversal ownership

Before the callback:

walker owns traversal of original `scf.for`

During callback:

versioner creates replacement `scf.if`

versioner erases original `scf.for`

After callback:

walker must not use original `scf.for`

The versioner owns the new graph.

The old traversal cursor must give up the erased node’s children.

This is another ownership transition.

IR node lifetime

and

walker traversal lifetime

must remain aligned.


Why skip() is better than stopping the whole walk

The pass could avoid use-after-erasure by interrupting traversal entirely.

But then later loops in the same function would never be optimized.

Version first loop
        ↓
stop module walk
        ↓
second loop remains untouched

That would preserve safety at the cost of incomplete pass behavior.

skip() is narrower.

Do not enter deleted current loop

but

continue to following siblings

The regression checks exactly that distinction.


Two sibling loops protect the traversal boundary

The new sibling-loops-versioned.mlir test places two independent, versionable scf.for operations in the same function.

Loop 1
→ canonical masked load
Loop 2
→ another canonical masked load

The expected result contains two separate versioning branches.

`scf.if` for Loop 1

followed by

`scf.if` for Loop 2

Each then branch contains an unmasked load.

Each else branch preserves the masked load.

This proves two traversal properties simultaneously.

The walker does not descend into the loop erased by the first rewrite.
The walker still reaches and versions the later sibling loop.

The test protects continuation, not only crash avoidance

A weaker test could include one versioned loop and verify that the compiler does not crash.

That would not distinguish:

walker safely skips erased operation

from:

walker stops the whole traversal after the first rewrite

The second sibling makes the difference observable.

First loop transformed
+
second loop transformed
→ traversal continued correctly

This is a strong example of a regression input designed around walker control flow rather than only output values.


These fixes form one transformation transaction

The major Part 3 corrections can be placed in one sequence.

1. Candidate collection

Collect only operation kinds
the rewriter actually supports.

After the patch:

loop versioning candidates
→ masked loads

not:

masked stores that cannot be rewritten

2. Preflight validation

Check every collected mask
without modifying IR.

If one mask fails:

reject complete versioning attempt

3. Commit construction

Generate one valid scalar condition

create `scf.if`

clone loop

remove masks from supported loads

4. Arity-correct region termination

Result-bearing if
→ explicit value yields
Result-less if
→ keep implicit zero-operand terminators

5. Original-node retirement

replace result uses

erase original loop

6. Traversal handoff

return WalkResult::skip()

do not descend into erased node

continue to sibling operations

Each stage owns a separate invariant.

A failure in any stage can invalidate the whole pass.


Analysis and rewrite now have a clearer boundary

The transformation can be summarized as two phases.

Analysis phase

Find candidate loads.

Validate local mask shape.

Validate loop-level implication.

Validate operation support.

Determine result requirements.

Rewrite phase

Create guard.

Create branch.

Clone loops.

Remove masks.

Create required yields.

Replace results.

Erase original node.

Repair traversal position.

The patch does not create a formal transaction object.

Architecturally, however, it moves the commit point after the complete precondition set.


Partial validity requires complete rejection

The two-mask example is the clearest case.

Mask A
→ versionable
Mask B
→ not versionable

The pass cannot choose:

remove Mask A in one cloned loop

leave Mask B in place

without defining and testing such a mixed transformation.

The current versioner’s contract is:

remove every collected mask

Therefore its acceptance rule must be:

every collected mask is versionable

If the implementation later supports per-operation partial versioning, that would be a different optimization with a different proof and test matrix.


Unsupported operations should not be smuggled through a generic collector

The masked-store issue shows another common compiler failure mode.

A shared collector can appear convenient:

loads

stores

selects

all have conditions.

But common syntax does not imply common rewrite semantics.

Masked load
→ produces a value
→ rewrite must replace result
Masked store
→ has a side effect
→ rewrite must preserve memory behavior
Select
→ chooses between two values
→ rewrite replaces one result

The collector should follow the capabilities of its consumer, not the superficial similarity of its inputs.


Builder defaults are part of the IR API

The duplicate-yield bug shows that transformations also need to respect what builders create automatically.

A builder is not merely a convenience wrapper around an empty operation.

It may establish required invariants.

No-result `scf.if` builder
→ creates region terminators

A pass that adds another terminator is not being more explicit.

It is violating the structure the builder already guaranteed.

Before adding an operation to a newly created region, the transformer needs to know:

What does the builder already own?

An erased node cannot remain the walker’s current object

The walker issue is the lifetime equivalent.

Callback input:
original loop
Callback output:
original loop erased

The callback must report that change to the traversal mechanism.

Returning the ordinary continuation result hides the lifetime transition.

IR mutation
→ changed traversal validity

The walker result is therefore part of rewrite correctness.


The regressions protect several different kinds of evidence

PR #7791 did not rely on one broad “pass completed” test.

The new files check distinct boundaries.

two-canonical-masks.mlir

One invalid mask among several

→ no partial or whole-loop versioning

→ both masks retained

versioning-no-results.mlir

Store-only loop

→ not collected or versioned

and:

No-result load loop

→ versioned correctly

→ no duplicate `scf.yield`

sibling-loops-versioned.mlir

First loop erased and replaced

→ walker skips its old body

→ later sibling loop still transformed

The tests are structural.

They inspect the exact MLIR produced by triton-opt.

They require no Intel XPU device.


Structure is the result being tested

For these defects, numerical output would be too late or too indirect.

The relevant questions are:

Was an `scf.if` inserted?

Were masks removed from the correct loads?

Did a masked store remain?

Did a region receive an extra terminator?

Did the walker reach the second loop?

Those are graph properties.

FileCheck can assert them directly.

The tests do not need to wait for device code or runtime execution to expose malformed IR.


Valid MLIR should not be punished for selecting an unsupported edge case

The issue states that all six failures were reproduced from valid MLIR.

That matters for Part 3 as well.

The compiler cannot defend itself by saying:

A no-result versioned loop should never occur.

A loop should never contain two different masks.

A masked-store-only loop is malformed.

Two sibling loops should not be walked this way.

Those are legitimate IR forms.

The pass must either:

transform them correctly

or:

leave them unchanged

The old implementation sometimes did neither.


What the patch directly changed in this area

The merged patch directly:

  • adds a mutation-free canVersion() check,

  • validates every collected canonical mask before generating the branch condition,

  • requires the versioning condition to imply every mask removed in the fast branch,

  • stops collecting masked stores,

  • limits loop-versioning candidates to operation kinds the versioner can rewrite,

  • creates explicit scf.yield operations only when the scf.if has results,

  • returns WalkResult::skip() after successful loop replacement,

  • applies the walker fix to canonical and invariant versioning,

  • and adds targeted MLIR/FileCheck regressions for the corresponding boundaries.

The changes are visible in the landed RemoveMasks.cpp and the three regression files discussed above.


What the patch does not establish

The public evidence does not establish that:

  • every RemoveMasks transformation is now transactionally rollback-safe,

  • every mask form supported by Triton is covered,

  • masked stores can never be optimized,

  • all nested loops are supported,

  • every MLIR walker mutation pattern is safe,

  • no later pass can introduce malformed SCF,

  • the new rejection paths have zero performance cost,

  • the pass has been formally verified,

  • or these compiler defects produced a documented numerical failure in a production XPU model.

The supported conclusion is narrower:

The old pass could begin a whole-loop rewrite without proving every mask it would remove, could collect operations its versioner did not support, could construct an invalid no-result scf.if, and could continue walking through an erased loop. The merged patch closes those specific IR-lifecycle boundaries.


The complete three-part series

Part 1 — The memory effect disappeared, but the SSA value did not

Always-false load

→ no memory read

but

→ result still consumed

Without other, the pass had to materialize a typed zero rather than perform a null replacement.

Part 2 — A familiar mask shape was not a proof

Canonical-looking expression

≠

guard implies every lane

Dynamic N, mismatched N and END, and eq or ne predicates had to remain masked.

Part 3 — A correct local fact was not enough for a valid whole-loop rewrite

Validate all masks.

Collect only supported operations.

Respect result arity.

Stop walking erased nodes.

Optimization correctness included the full lifetime of the IR transformation.


The final lesson is that compiler rewrites need a commit point

A pass can recognize one valid pattern and still be wrong to mutate.

The real precondition may involve:

  • every operation in a collection,

  • the relationship among their symbols,

  • the capabilities of the rewriter,

  • the result arity of newly created control flow,

  • and the lifetime of the operation currently being traversed.

The safe structure is:

discover
        ↓
validate complete set
        ↓
commit rewrite
        ↓
retire old IR
        ↓
repair traversal state

Not:

discover first promising operation
        ↓
begin mutation
        ↓
learn about the remaining constraints later

A compiler transformation should cross its mutation boundary only after every assumption consumed by the final graph has already been proven.

Intel Triton XPU #7791 repaired RemoveMasks by moving that boundary later—and by making the rewrite’s supported operations, region structure, and walker lifetime agree with the IR it actually produced.


Previous articles

  • Intel Triton XPU #7791 — Why an Always-False Masked Load Without other Crashed RemoveMasks

  • Intel Triton XPU #7791 — Why a Boundary-Shaped Mask Was Not Enough to Prove It Removable

Link the titles above to Parts 1 and 2 after publication.

Related material


Patch status: Merged into Intel Triton XPU main
Affected pass: TritonIntelRemoveMasks
Whole-loop proof rule: Every collected mask must pass mutation-free canVersion() validation
Commit boundary: Versioning condition and scf.if are created only after complete validation
Supported versioning operation: Masked tt.load
Unsupported collection removed: Masked tt.store
No-result region rule: Preserve builder-created implicit terminators; do not add a second scf.yield
Walker rule: Return WalkResult::skip() after the original loop is erased
Traversal regression: Two sibling loops must both be reached and versioned
Regression form: Valid MLIR through triton-opt and FileCheck
Hardware requirement: None
Evidence boundary: Compiler IR construction and traversal; no XPU runtime-performance claim

This is Part 3 and the final article in the Intel Triton XPU RemoveMasks series.

Part 1 examined why eliminating an always-false memory access still required a valid typed SSA replacement.

Part 2 examined why canonical expression shape did not prove that one scalar guard implied every vector lane.

This final article examined why the pass had to validate the complete mutation set before creating IR, restrict its candidates to operations it could actually rewrite, preserve SCF terminator contracts, and stop traversing a loop that no longer existed.

#Intel #Triton #XPU #MLIR #CompilerOptimization #LoopVersioning #IRTransformation #SCF #CompilerCorrectness #RegressionTesting #CodeAnalysis

Popular posts from this blog

AMD Is Trying to Turn Kernel Diversity Into One Software Asset

PyTorch #188031 — Why Did Forward Use 64-Bit Indexing While Backward Still Used uint32_t?

NVIDIA CUTLASS #3017 — Why load() and store() Mapped the Same Tile Coordinate to Different Addresses