Triton #11334 — How Counter Values 0, 1, 2, and 3 Encode Two mbarrier Slots and Parity
Triton NVIDIA Barrier, Part 2 of 3 — Why reading the counter one step late can move a warp onto a different barrier generation
Update — August 18, 2026
The PTX delayed-counter dynamic regression test introduced with PR #11334 was removed in follow-up PR #11336 after timing out on GB200. The cluster_barrier compiler-ordering fix and the LLVM static regression checks remain in Triton main. References below to the dynamic test describe the test design that originally landed with #11334, not the test suite currently retained.
Part 1 examined why Triton changed the ordering around the shared phase counter in NVIDIA’s cluster_barrier lowering.
Before the fix:
local barrier
→ counter loadAfter the fix:
counter load
→ local barrierAt the source level, the patch looked like a small reordering.
But the value being protected was not an ordinary loop counter.
The shared counter selected all of the following:
the physical
mbarrierslot used by the current operationthe parity expected for that slot
the logical barrier generation that the warp should wait for
If two warps participating in the same source-level cluster_barrier read different counter values, they no longer wait on the same hardware-level barrier.
Two physical barrier slots represent four logical states
The relevant part of the lowering can be simplified to:
Value counter =
load(counterPtr);
Value barrierIdx =
counter & 1;
Value parity =
counter >> 1;
Value nextCounter =
(counter + 1) & 3;Because nextCounter is masked with 3, the counter cycles through four values:
0 → 1 → 2 → 3 → 0Those four values are interpreted as follows:
counter binary mbarrier slot parity
0 00 slot 0 0
1 01 slot 1 0
2 10 slot 0 1
3 11 slot 1 1The low bit selects the physical barrier slot:
counter & 1The high bit selects the expected parity:
counter >> 1A two-bit value therefore encodes two separate pieces of state:
Which physical mbarrier object should be used?
Which logical generation of that object should be awaited?The counter is not merely a call number
It is easy to interpret counter values 0 and 1 as little more than:
first call
second callBut changing the counter from 0 to 1 changes the physical barrier address.
counter = 0
→ slot 0
→ parity 0counter = 1
→ slot 1
→ parity 0Changing the counter from 0 to 2 keeps the physical slot but changes the generation:
counter = 0
→ slot 0 / parity 0counter = 2
→ slot 0 / parity 1The two values refer to the same barrier storage, but not to the same barrier event.
The same barrier address does not necessarily mean the same barrier generation.
Why alternate between two physical slots?
Imagine using only one mbarrier slot for every cluster synchronization.
Round 0
→ slot 0
Round 1
→ slot 0 again
Round 2
→ slot 0 againA fast CTA could complete one round and begin reusing the barrier while a slower CTA was still waiting for the previous round.
Slow CTA
→ still waiting for Round 0
Fast CTA
→ has already reused slot 0 for Round 1The previous generation and the next generation would then occupy the same physical state too closely together.
Triton avoids immediate reuse by alternating between two slots:
Round 0 → slot 0
Round 1 → slot 1
Round 2 → slot 0
Round 3 → slot 1A slot is reused only after the protocol has passed through the other slot.
slot 0
→ slot 1
→ slot 0The source comment describes the same intent: a delayed CTA should not miss a phase because a peer reused one mbarrier twice before the delayed CTA began waiting.
Two slots alone are still not enough
Slot 0 is reused after two rounds:
Round 0 → slot 0
Round 2 → slot 0The physical address is the same in both rounds.
Parity distinguishes those two logical uses:
Round 0
→ slot 0 / parity 0
Round 2
→ slot 0 / parity 1Slot 1 follows the same pattern:
Round 1
→ slot 1 / parity 0
Round 3
→ slot 1 / parity 1The protocol therefore forms a four-state ring:
slot 0 / parity 0
↓
slot 1 / parity 0
↓
slot 0 / parity 1
↓
slot 1 / parity 1
↓
slot 0 / parity 0There are two physical barrier objects.
The combination of slot and parity creates four logical states.
Parity prevents an old completion from looking like a new one
Suppose slot 0 completed during an earlier round.
When the protocol later returns to slot 0, checking only:
Has slot 0 completed?would be ambiguous.
The completion state from the previous use may still be visible.
Parity changes the question to:
Has slot 0 completed with the parity expected by this round?These are different events:
slot 0 / parity 0 completed
≠
slot 0 / parity 1 completedThe wait operation therefore depends on both:
barrier pointer
+
expected parityTwo warps can calculate the same barrierPtr while still waiting for different generations if their parity values differ.
Reading the counter one step late always selects another slot
The low bit changes every time the counter advances by one.
0 → 1
1 → 2
2 → 3
3 → 0Expanded into slot and parity:
0 → 1
slot 0 / parity 0
→
slot 1 / parity 01 → 2
slot 1 / parity 0
→
slot 0 / parity 12 → 3
slot 0 / parity 1
→
slot 1 / parity 13 → 0
slot 1 / parity 1
→
slot 0 / parity 0A one-step difference does not merely move to the next numerical value.
It always selects the other physical slot.
On some transitions, parity changes as well.
What happens when one warp reads 0 and another reads 1?
Assume the current shared counter is 0:
counter = 0
→ slot 0
→ parity 0One warp runs immediately after the old entry barrier.
The remaining warps are delayed before their counter loads.
Fast warp
loads counter 0
→ selects slot 0
→ expects parity 0Delayed warps
have not loaded the counter yetThe fast path begins the current cluster-barrier protocol.
Assume the relevant peer CTAs also participate using:
slot 0 / parity 0Once the current operation completes, thread 0 calculates:
nextCounter
=
(0 + 1) & 3
=
1and stores it into shared memory:
shared counter:
0 → 1A delayed warp now executes its first counter load:
loads counter 1
→ selects slot 1
→ expects parity 0The same source-level barrier invocation now contains two views:
Fast warp
→ slot 0 / parity 0
Delayed warp
→ slot 1 / parity 0They are no longer waiting for the same barrier generation.
The same function call can lower to different hardware waits
At the source level, every warp reached the same operation:
ttgl.barrier(cluster=True)But the generated behavior can diverge:
Warp 0:
wait on mbarrier slot 0
with parity 0Warp 1:
wait on mbarrier slot 1
with parity 0The source-level operation is identical.
The hardware-level target is not.
Therefore:
reaching the same cluster_barrier calldoes not by itself guarantee:
waiting on the same barrier generationThat guarantee exists only when all local participants derive their slot and parity from the same counter snapshot.
The wrong slot may receive no arrival for the current round
Suppose the peer CTAs send their current-round arrivals to:
slot 0 / parity 0A delayed warp that selects slot 1 now waits on a barrier state that may receive no arrival for the current round.
Current peer arrivals
→ slot 0
Delayed warp’s wait
→ slot 1Possible outcomes depend on timing and later activity:
the delayed warp may wait indefinitely
it may observe a future round’s signal as though it belonged to the current round
some warps may reach the final local barrier while others remain blocked
the CTA or CUDA context may deadlock
The exact failure does not need to be identical on every execution.
The protocol has already failed once current-round and next-round state are no longer separated.
The final local barrier cannot repair an earlier phase split
The old lowering also contained a local barrier after storing the next counter:
cluster wait
→ nextCounter store
→ tail local barrierCould that final barrier reunify the warps?
No.
A warp that selected the wrong slot or parity may never finish its mbarrier wait.
Fast warp
→ correct wait completes
→ stores or observes the next state
→ reaches tail barrierDelayed warp
→ waits on wrong slot or parity
→ does not complete
→ never reaches tail barrierA local barrier releases only after all participating threads arrive.
If the barrier generation has already split before that point, the tail barrier cannot recover the lost agreement.
It may instead become another deadlock point.
A two-step difference selects the same slot but the opposite parity
Now suppose the expected counter is 0, but a thread reads 2.
Expected:
counter 0
→ slot 0 / parity 0Observed:
counter 2
→ slot 0 / parity 1The physical barrier pointer is the same:
slot 0The expected generation is different:
parity 0
versus
parity 1This demonstrates another important rule:
Calculating the same
barrierPtrdoes not prove that two threads are waiting for the same barrier.
The logical generation is encoded separately.
One step late means the wrong slot; two steps late means the wrong parity
Counter drift can be summarized as follows.
One-step difference
counter + 1
→ another physical slot
→ sometimes a different parity as wellTwo-step difference
counter + 2
→ the same physical slot
→ the opposite parityThree-step difference
counter + 3
→ another slot
→ another logical generationAny nonzero difference changes at least one part of the barrier identity.
same counter
→ same slot and parity
different counter
→ different slot, parity, or bothThat is why coherent counter snapshots are a prerequisite for the entire protocol.
Double buffering and parity do not create snapshot consistency
The two-slot and parity scheme protects barrier reuse.
It does not automatically ensure that every local thread selects the same state.
The responsibilities are different.
Two physical slots
→ avoid immediately reusing the same barrier object in consecutive roundsParity
→ distinguish separate generations that reuse one slotCoherent local counter snapshot
→ ensure all threads in the current call select the same slot and parityThe first two mechanisms cannot replace the third.
Double buffering is safe only when every participant agrees on which buffer is current.
The old ordering aligned the starting point, not the read completion
Before the fix:
local barrier
→ counter loadThe local barrier guaranteed that all threads reached the instruction boundary before the load.
After the barrier released, scheduling could diverge again:
Warp 0
→ immediately executes the load
Warp 1
→ delayed by scheduling
Warp 2
→ waiting on another dependency
Warp 3
→ not selected yetThe old ordering proved:
Every participating thread reached the region before the counter load.The protocol actually required:
Every participating thread completed the current counter load.Those are different invariants.
The fixed ordering closes the reader set
After the fix:
counter load
→ local barrierEach local thread first reads the published counter into a register.
Warp 0 → counter loaded
Warp 1 → counter loaded
Warp 2 → counter loaded
Warp 3 → counter loadedOnly then can all of them reach and release the local barrier.
all counter loads complete
↓
local barrier releasesEven if the shared counter changes later, every warp already owns the current snapshot in a private register.
shared counter:
0 → later becomes 1current local snapshots:
all remain 0After the barrier, every warp derives:
slot 0 / parity 0from the same state.
A single writer does not guarantee coherent readers
Only thread 0 stores the next counter.
The lowering creates a leader predicate equivalent to:
threadId == 0The next state is therefore written by one thread:
thread 0
→ stores nextCounter
all other threads
→ do not write itThis avoids multiple writers racing with one another.
But it does not automatically make all reads coherent.
single writer
≠
all readers observed the same versionEven with one writer, some readers can observe the old value while others observe the new value unless the read and write phases are ordered correctly.
The race in #11334 occurred at precisely that boundary.
The repaired protocol as a state transition
Assume the current counter is 0.
1. Publish the current state
shared counter = 0The warp-specialization entry or previous tail barrier establishes this value for the new round.
2. Every local participant takes a snapshot
Warp 0 → reads 0
Warp 1 → reads 0
Warp 2 → reads 0
Warp 3 → reads 03. Seal completion of the reads
local barrier
→ no thread may continue until all snapshots exist4. Select one barrier generation
Every warp derives:
slot 0
parity 05. Execute the cluster barrier
send or receive peer arrivals
+
wait for slot 0 / parity 06. Advance the shared state
Thread 0 stores:
nextCounter = 17. Publish the next state
The tail local barrier ensures that the next operation begins only after the current transition has completed locally.
The full sequence is:
publish
→ snapshot
→ seal
→ use
→ advance
→ publishThe counter is a compressed state machine
The shared integer represents a small state machine:
State 0
slot 0 / parity 0
↓
State 1
slot 1 / parity 0
↓
State 2
slot 0 / parity 1
↓
State 3
slot 1 / parity 1
↓
State 0Every local thread participating in one transition must observe the same starting state.
If one thread observes State 0 while another observes State 1, one logical transition splits into two different actions:
same source operation
↓
different state snapshots
↓
different barrier selectionsThat is not a minor timing variation.
It is a divergence in the protocol’s state machine.
A small numerical difference can represent a large semantic difference
In ordinary arithmetic, the difference between 0 and 1 is small.
In encoded state, it can be structural.
0
→ physical barrier address A
1
→ physical barrier address BSimilarly:
0
→ one generation of slot 0
2
→ another generation of slot 0The numerical difference may be one or two.
The execution difference is an entirely different wait condition.
This pattern appears often in low-level systems:
small integer
+
state-selection semantics
→ large behavioral differenceReading the counter is therefore not just reading shared data.
It selects the identity of the current barrier operation.
What does it mean to wait on the same barrier?
The question cannot be answered only by checking whether threads reached the same source line.
They must agree on all of the following:
the same counter snapshot
the same physical mbarrier slot
the same expected parityThe actual contract is:
Threads participate in the same barrier only when they reach the same operation and derive the same physical slot and logical generation from the same published state.
If any one of those values differs, they are not waiting for the same barrier in execution terms.
Part 3: forcing the race instead of waiting for it
Under ordinary scheduling, every warp may read the counter quickly enough that the defect remains hidden.
The new regression test did not repeatedly launch the kernel and wait for a bad schedule to occur by chance.
It obtained the generated PTX and inserted a deliberate delay immediately before the counter load:
first warp
→ proceeds immediately
remaining warps
→ spin before the counter loadThe test then compared the two orderings under the same forced schedule.
Old:
barrier → delayed load
→ fast warp can advance the counterFixed:
delayed load → barrier
→ fast warp must wait for every readerBecause the old behavior can deadlock the CUDA context, the dangerous launch is executed inside a timeout-bounded subprocess.
Part 3 examines how that PTX-level perturbation turned an intermittent scheduler race into a deterministic regression test.
Previous article
Triton #11334 — Why cluster_barrier Could Advance the Phase Before Every Warp Had Read It
Link the title above to Part 1 after publication.
Related material
Patch status: Merged into Triton main
Physical barrier slots: 2
Logical counter states: 4
State mapping: Low bit selects the slot; high bit selects parity
Core invariant: Every local participant in one cluster_barrier invocation must capture the same counter before thread 0 advances it
This is Part 2 of a three-part series on Triton’s NVIDIA cluster_barrier phase race.
Part 1 explained why the old barrier → load ordering failed to prove that every warp had completed its phase read.
This article examined how four counter states encode two physical mbarrier slots and parity, and why reading the counter one step late can move a warp onto another barrier generation.
Part 3 examines how Triton modified generated PTX to delay selected warps at the exact counter-load boundary and isolated the potentially deadlocking launch inside a timeout-bounded subprocess.
#Triton #NVIDIA #CUDA #Hopper #MBarrier #WarpSpecialization #GPUConcurrency #RaceCondition #CodeAnalysis #SoftwareArchitecture