Triton #11401 — Why an Internal Cluster Barrier Had to Clear the Scratch Write but Preserve the Read

Multi-CTA scratch reuse, atomic-result broadcasts, and the difference between a later read and a later overwrite

The barrier was already inside the operation.

The compiler still had to remember something after it.

Several Triton operations use shared-memory scratch as a temporary broadcast path across multiple CTAs, or thread blocks.

Their internal memory sequence is:

write scratch
        ↓
internal cluster barrier
        ↓
read scratch

The barrier resolves the first half of that sequence.

By the time the operation reaches its read phase, the earlier scratch write has been synchronized.

But the read itself has just happened.

That read remains relevant if another operation wants to reuse the same scratch allocation.

The correct outgoing state is therefore:

scratch write
→ no longer pending
scratch read
→ still pending

Before Triton PR #11401, the NVIDIA cluster-barrier analysis represented that distinction for some operations, including cross-CTA reductions and layout conversions.

It did not recognize every operation that used the same cross-CTA scratch protocol.

The missing set included paths such as:

atomic compare-and-swap

atomic read-modify-write

timed atomic polling

local atomic scatter RMW

That incomplete classification could produce two opposite barrier mistakes.

Next reuse of the same scratchCorrect result
Another operation only reads itNo additional barrier
Another operation overwrites itCluster barrier required

If the earlier operation’s internal write remained visible to the analysis, a later read could receive a redundant barrier.

If its outgoing cross-CTA read was not classified correctly, a later local write could receive no barrier at all.

PR #11401 repaired both outcomes by giving the analysis one accurate timeline:

W(scratch)
→ internal cluster barrier
→ R(scratch)

An internal barrier can retire the accesses before it. It cannot retire a read that occurs after it.


The minimum two-operation example

Consider two CTAs sharing a scratch allocation.

The first operation computes an atomic result and broadcasts it through scratch.

Operation A:

CTA-local scratch write
        ↓
cluster-wide synchronization
        ↓
cross-CTA scratch read

Now consider two possible operations after it.

Case 1 — the next operation only reads

Operation A:
W → barrier → R

Operation B:
R

The relevant unresolved relationship is:

R → R

Two reads do not overwrite one another.

No additional cluster barrier is needed between them.

Case 2 — the next operation writes

Operation A:
W → barrier → R

Operation C:
W

The unresolved relationship is now:

R → W

Operation C must not overwrite the scratch allocation until every CTA participating in Operation A has completed its read.

This is a write-after-read hazard.

A cluster barrier is required before Operation C.

The compiler does not need to remember the complete history of Operation A.

It needs the part of that history that remains unresolved at its exit.

Retired:
the write before the internal barrier
Still live:
the read after the internal barrier

Why cross-CTA atomic results use scratch

An atomic operation may be performed by only part of the logical Triton tensor.

Its result may still need to appear consistently to threads that did not execute the atomic instruction directly.

For a scalar result, the lowering can use a small shared-memory scratch region.

Conceptually:

participating thread performs atomic
        ↓
result written to scratch
        ↓
CTAs rendezvous
        ↓
other threads or CTAs read the result

The same broad mechanism can appear for a tensor result with replicated or broadcast dimensions.

The scratch storage is not the user’s explicit destination.

It is compiler-managed temporary memory used to preserve Triton’s block-level value semantics.

That distinction matters for barrier analysis.

The atomic may access:

the user-visible atomic destination

and

an internal scratch allocation

PR #11401 concerns the synchronization state of the second one.


How Triton tracks shared-memory hazards

Triton’s membar analysis stores accesses in two collections:

syncReadSlices

syncWriteSlices

Each entry identifies:

  • a shared-memory allocation slice,

  • the operations that accessed it,

  • and whether the access was a read or write.

MembarInfo.pending represents the accesses that have occurred since the most recent relevant synchronization.

When a new operation arrives, the analysis checks for:

RAW
write followed by read
WAR
read followed by write
WAW
write followed by write

If two overlapping scratch accesses create one of those relationships and the access is distributed across CTAs, the pass may insert:

ttng.cluster_barrier

The challenge in #11401 was not detecting that the scratch intervals overlapped.

The allocation analysis already knew which temporary region each operation used.

The challenge was describing where the operation’s own internal synchronization divided its accesses.


The required state transition

For a scratch-backed cross-CTA operation, the merged analysis performs the following sequence.

1. Record the scratch write

current operation state:

W(scratch)

2. Resolve an incoming conflict if necessary

Suppose a previous operation still has a pending read of the same allocation.

pending:
R(scratch)

current:
W(scratch)

The pass inserts a cluster barrier before the current operation.

That barrier clears the incoming state.

It does not delete the current operation’s scratch write because that write occurs after the inserted barrier.

before inserted barrier:

pending = R
current = W
after inserted barrier:

pending = empty
current = W

3. Model the operation’s internal barrier

When the current operation uses cross-CTA scratch, its own lowering contains a synchronization between the scratch write and scratch read.

The analysis commits the write, processes the internal synchronization, and clears the current write state.

W(scratch)
        ↓
internal barrier
        ↓
write retired

4. Record the outgoing read

After that internal barrier, the operation reads the shared result.

outgoing state:

R(scratch)

This final read is added to the pending state used when analyzing the next operation.

That sequence is the core of the patch.


What the old classifier recognized

The previous isDistributedMultiCTAOp() contained direct logic for cross-CTA scratch use by operations such as:

convert_layout

reduce

It also handled several other explicitly named multi-CTA operations, including selected TMA, tensor-memory, and barrier paths.

But atomic-result scratch use was defined elsewhere.

Allocation analysis already had a broader helper:

hasCrossCTAScratch(op)

That helper knows whether an operation’s temporary storage crosses CTA boundaries.

Its covered cases include:

cross-CTA layout conversion

reduction split across CTAs

timed atomic poll with a consumed result

atomic operations whose result requires broadcast

local atomic scatter RMW with cross-CTA result use

The cluster-barrier classifier had duplicated only part of that knowledge.

The result was two separate definitions of:

Which operations use cross-CTA scratch?

They no longer agreed.


The patch reused the existing allocation contract

The repaired classifier begins with:

if (hasCrossCTAScratch(op))
    return isRead;

That line carries two decisions.

Decision 1 — use the shared source of truth

The barrier pass no longer maintains its own incomplete list of scratch-producing operations.

It asks the same helper used by shared-memory allocation analysis.

Allocation analysis:
Does this operation need cross-CTA scratch?
Barrier analysis:
Use the same answer.

Decision 2 — only the read is distributed

The helper may report that the operation uses scratch across CTAs.

That does not mean both phases are remote.

The merged source states:

scratch write
→ CTA-local
scratch read
→ may access another CTA’s shared memory

Therefore:

isDistributedMultiCTAOp(
    op,
    /*isRead=*/false
)

returns false for this scratch protocol.

But:

isDistributedMultiCTAOp(
    op,
    /*isRead=*/true
)

returns true.

That directional answer is what allows the analysis to distinguish:

later read
→ no barrier

from:

later write
→ barrier

The same hasCrossCTAScratch() contract remains in the current Triton source.


Why a later read must not receive another barrier

One regression begins with a cross-CTA atomic CAS.

The operation broadcasts its scalar result through scratch.

It is followed by:

ttng.tmem_copy

The two-CTA tensor-memory copy reads a shared-memory allocation that reuses the same physical scratch interval.

After the atomic’s internal barrier, its outgoing scratch state is already:

R

The copy contributes another:

R

So the test requires:

atomic CAS

no extra cluster barrier

tmem_copy

The same negative-barrier pattern is checked for:

cross-CTA reduction

atomic RMW

timed atomic poll

local atomic scatter RMW

“No extra barrier” does not mean these operations contain no internal synchronization.

It means the pass must not insert another cluster barrier between two already ordered read phases.


Why a later local reduction does need one

The companion regressions replace the read-only copy with a CTA-local reduction.

That local reduction reuses the same scratch interval for a new write.

The previous cross-CTA operation still has an outgoing read pending.

previous:
R(scratch)
next:
W(scratch)

The test therefore requires:

tt.atomic_cas
ttng.cluster_barrier
"tt.reduce" ... axis = 0

The barrier appears immediately before the local reduction.

Equivalent positive-barrier checks are included for:

atomic RMW

timed atomic poll

local atomic scatter RMW

The local reduction itself does not need to be distributed across CTAs for this barrier to matter.

It is writing storage that a prior distributed read may still be using.

The cross-CTA property belongs to the unresolved earlier access.


The tests check both over-synchronization and under-synchronization

Barrier patches are often evaluated only by asking:

Did the compiler add a missing barrier?

PR #11401 protects both directions.

Too many barriers

cross-CTA scratch read

followed by

another read

Expected:

no additional cluster barrier

An extra barrier can serialize CTAs and add unnecessary synchronization cost.

The PR does not publish a benchmark quantifying that cost.

Too few barriers

cross-CTA scratch read

followed by

scratch overwrite

Expected:

cluster barrier before the write

Without it, one CTA could begin reusing the allocation while another CTA still depends on its previous contents.

No runtime wrong-result reproducer was published with this PR, so the article should not claim a measured corruption.

The static ordering defect and repaired barrier placement are directly covered.


The operation matrix

The added MLIR coverage includes these scratch-producing operations:

First operationFollowing reuseExpected extra barrier
Cross-CTA reductiontmem_copy readNo
Atomic CAStmem_copy readNo
Atomic CASLocal reduction writeYes
Atomic RMWtmem_copy readNo
Atomic RMWLocal reduction writeYes
Timed atomic polltmem_copy readNo
Timed atomic pollLocal reduction writeYes
Local atomic scatter RMWRead-only reuseNo
Local atomic scatter RMWLocal scratch writeYes

This pairing makes the intended state visible.

For each operation class, the tests ask two different questions:

Did the internal barrier retire its earlier write?

and:

Did the later read remain pending?

A model that clears everything would fail the write-reuse case.

A model that clears nothing would fail the read-only case.


Why the atomic poll test includes a timeout

The allocation helper does not assign broadcast scratch to every atomic poll.

A poll without a timeout can only complete through the successful condition represented by that form and may not require a returned result to be broadcast through shared scratch.

The covered timed-poll path requires:

a timeout

and

a result that is actually consumed

That is the case for which hasCrossCTAScratch() reports the distributed scratch protocol.

The test is therefore attached to the operation form that reaches the repaired analysis path.

This avoids turning a specific scratch contract into a claim about every atomic poll.


Local atomic scatter has two memory stories

local_atomic_scatter_rmw is slightly more complicated than the scalar atomic cases.

It accesses:

an explicit shared-memory destination

and

compiler-managed scratch used to broadcast its result

Most scratch operations are expected not to carry unrelated explicit shared-memory dependencies in the same analysis step.

The source contains a specific exception for local atomic scatter RMW because it legitimately has both.

The patch does not merge those two memory regions into one abstract access.

It keeps the explicit destination effects and models the temporary scratch’s internal barrier separately.

This is why its regression is valuable.

It protects a case where one operation participates in two distinct shared-memory contracts.


The pass is limited to NVIDIA multi-CTA targets

runClusterBarrierInsertion() returns without doing this analysis when:

compute capability < 90

or:

number of CTAs = 1

The new regressions use modules configured for two CTAs, including cuda:100 cases.

The direct patch scope is therefore:

NVIDIA cluster-capable multi-CTA lowering

It does not establish the same issue in:

  • single-CTA kernels,

  • AMD lowering,

  • CPU execution,

  • or older NVIDIA targets that do not enter this cluster-barrier pass.

The source is part of Triton’s NVIDIA GPU transform pipeline, not its generic frontend semantics.


What the merged evidence confirms

The source and tests confirm that:

  • cross-CTA scratch operations contain logically separate write and read phases;

  • an internal cluster barrier separates those phases;

  • the previous barrier classifier had narrower operation coverage than allocation analysis;

  • atomic CAS, atomic RMW, timed atomic poll, and local atomic scatter RMW can use the same cross-CTA scratch protocol;

  • the merged classifier delegates that classification to hasCrossCTAScratch();

  • scratch writes are treated as CTA-local;

  • scratch reads are treated as distributed when the helper reports cross-CTA scratch;

  • read-only reuse receives no additional barrier;

  • a later local scratch write receives a cluster barrier;

  • PR #11401 merged into Triton main;

  • and the repaired classification remains in current main.


What the evidence does not establish

The public record does not establish that:

  • a production model returned an incorrect result before the patch;

  • one specific GPU race was captured at runtime;

  • every atomic operation allocates scratch;

  • every read-after-read barrier had measurable performance cost;

  • every write-after-read omission manifests deterministically;

  • the fix affects single-CTA kernels;

  • AMD requires the same barrier model;

  • or the pass is now complete for every possible cross-CTA operation.

The supported conclusion is narrower:

Triton’s NVIDIA membar analysis did not consistently recognize every operation whose result passed through cross-CTA scratch. The merged patch models those operations as a local scratch write, an internal cluster barrier, and an outgoing distributed read, then inserts later barriers according to the access that actually remains unresolved.


The contract that remains

A synchronization point does not erase an operation as a whole.

It partitions the operation’s memory history.

accesses before the barrier
→ synchronized
accesses after the barrier
→ still relevant

Treating the complete operation as either:

fully unsynchronized

or:

fully synchronized

loses the ordering information needed for safe scratch reuse.

Barrier analysis must preserve which side of the barrier each memory effect belongs to.

That is the compiler contract repaired by Triton #11401.


Related material


Patch status: Merged into Triton main on August 24, 2026, as 5c955485.

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