Skip to content

Commit d06790e

Browse files
KenJones-Modularmodularbot
authored andcommitted
[puzzles] Fix the puzzle 29 mbarrier waits
Puzzle 29's double-buffered stencil now uses mbarrier the way the PTX contract specifies, and its page teaches that pattern, so readers no longer learn a wait that only works by coincidence. MODULAR_ORIG_COMMIT_REV_ID: f725593c5c03eb6936c7acc3f841162428ab6237
1 parent b2e2bf5 commit d06790e

3 files changed

Lines changed: 49 additions & 50 deletions

File tree

‎book/src/puzzle_29/memory_barrier.md‎

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ NVIDIA-only.
2525
> - **Precise control**: [`mbarrier_init()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_init) sets up reusable barrier objects with specific thread counts
2626
> - **State tracking**: [`mbarrier_arrive()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_arrive) signals individual thread completion and maintains arrival count
2727
> - **Flexible waiting**: [`mbarrier_test_wait()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_test_wait) polls a completion state without blocking, so you decide what a thread does while waiting
28-
> - **Reusable objects**: Same barrier can be reinitialized and reused across multiple iterations
28+
> - **Reusable objects**: Once every thread arrives, a barrier advances to its next phase on its own, so the same object serves every iteration without being reinitialized
2929
> - **Multiple barriers**: Different barrier objects for different synchronization points (initialization, iteration, finalization)
3030
> - **Hardware primitive**: Maps directly to the NVIDIA `mbarrier` instructions rather than to a single block-wide barrier
3131
> - **Memory semantics**: Explicit control over memory visibility and ordering guarantees
@@ -38,7 +38,7 @@ NVIDIA-only.
3838
>
3939
> - **Buffer role alternation**: An arrival count scoped to the threads that wrote the buffer about to be read
4040
> - **Iteration boundaries**: Separate barrier objects for initialization, the iteration loop, and finalization
41-
> - **State management**: `mbarrier_arrive()` returns a state token, so arrival can be tracked and not merely awaited
41+
> - **State management**: `mbarrier_arrive()` returns a token for the barrier's current phase, so arrival can be tracked and not merely awaited
4242
> - **Composability**: The same primitive tracks `cp.async` and TMA transfer completion on newer hardware
4343
>
4444
> This puzzle demonstrates **synchronization patterns** used in real-world GPU
@@ -194,7 +194,9 @@ when memory operations complete, essential for complex memory access patterns.
194194
loop
195195
- **Buffer swap safety**: Prevents reading from buffer while others still
196196
writing
197-
- **Barrier reinitialization**: Reset barrier state between iterations
197+
- **Phase tracking**: Each thread waits on the token its own `mbarrier_arrive()`
198+
returned; the barrier moves to its next phase by itself, so it is never
199+
reinitialized inside the loop
198200

199201
**Output phase:**
200202

@@ -240,19 +242,23 @@ when memory operations complete, essential for complex memory access patterns.
240242

241243
- Call
242244
[`mbarrier_arrive()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_arrive)
243-
after each thread completes its write operations
245+
after each thread completes its write operations, and keep the token it
246+
returns
244247
- Follow it with a polling loop on
245248
[`mbarrier_test_wait()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_test_wait):
246249
the API is a **non-blocking** check, so call it inside
247-
`while not mbarrier_test_wait(...): pass` to actually wait for every thread
248-
to arrive before the buffer swap
249-
- Reinitialize barriers between iterations for reuse:
250-
[`mbarrier_init()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_init)
251-
- Only thread 0 should reinitialize barriers to avoid race conditions
250+
`while not mbarrier_test_wait(barrier, token): pass` to actually wait for
251+
every thread to arrive before the buffer swap
252+
- Pass the token from `mbarrier_arrive()`, not a thread count: the token
253+
identifies the phase you are waiting on
254+
- Don't reinitialize a barrier inside the loop. The barrier starts its next
255+
phase on its own once every thread arrives, and a reinit can reset it while
256+
a slower thread is still polling the phase that just completed, leaving that
257+
thread waiting forever
252258
- Insert a [`barrier()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/barrier/)
253-
after each `mbarrier_init` call (initial setup and per-iteration reinit) so
254-
every thread observes the initialized barrier before any thread calls
255-
`mbarrier_arrive`. This matches the
259+
after the initial `mbarrier_init` calls so every thread observes the
260+
initialized barriers before any thread calls `mbarrier_arrive`. This matches
261+
the
256262
[NVIDIA Async Barriers initialization pattern](https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/async-barriers.html#initialization)
257263

258264
### **Output selection**
@@ -379,10 +385,13 @@ Understanding the mbarrier coordination pattern:
379385
- **[mbarrier_init()](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_init)**:
380386
Initialize barrier for specific thread count (TPB)
381387
- **[mbarrier_arrive()](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_arrive)**:
382-
Signal individual thread completion of write phase
388+
Signal individual thread completion of write phase, returning a token for the
389+
barrier's current phase
383390
- **[mbarrier_test_wait()](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_test_wait)**:
384-
Non-blocking poll of barrier state—call it in a loop until it returns true
385-
- **Reinitialization**: Reset barrier state between iterations for reuse
391+
Non-blocking check of whether the token's phase has completed—call it in a
392+
loop until it returns true
393+
- **Phase advance**: When the last thread arrives, the barrier moves to its next
394+
phase by itself, so one barrier object serves every iteration with no reinit
386395

387396
**Critical timing sequence:**
388397

@@ -396,8 +405,9 @@ Understanding the mbarrier coordination pattern:
396405
2. **All threads write**: Each thread updates its assigned buffer element
397406
3. **Signal completion**: Each thread calls
398407
[`mbarrier_arrive()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_arrive)
408+
and keeps the token it returns
399409
4. **Poll until all arrived**: Every thread spins in
400-
`while not mbarrier_test_wait(...): pass` —
410+
`while not mbarrier_test_wait(barrier, token): pass` —
401411
[`mbarrier_test_wait()`](https://max.modular.com/api/mojo/max/gpu/sync/sync/mbarrier_test_wait)
402412
is a non-blocking check, so a single call is not a wait
403413
5. **Safe to proceed**: Now safe to swap buffer roles for next iteration
@@ -462,12 +472,12 @@ var stencil_input = buffer_B[10] # Undefined behavior!
462472
# All threads write their results
463473
buffer_B[local_i] = stencil_result
464474
465-
# Signal write completion
466-
_ = mbarrier_arrive(iter_barrier.ptr)
475+
# Signal write completion; the token names this phase of the barrier
476+
var token = mbarrier_arrive(iter_barrier.ptr)
467477
468478
# Poll until ALL threads have completed writes. mbarrier_test_wait is
469479
# non-blocking, so a single call is NOT a wait — it must run in a loop.
470-
while not mbarrier_test_wait(iter_barrier.ptr, TPB):
480+
while not mbarrier_test_wait(iter_barrier.ptr, token):
471481
pass
472482
473483
# Now safe to read - all writes guaranteed complete
@@ -506,7 +516,8 @@ else:
506516
**Synchronization overhead:**
507517

508518
- **mbarrier cost**: Higher than basic `barrier()`, and this pattern still needs
509-
a `barrier()` after every `mbarrier_init` to publish the barrier state
519+
one `barrier()` after the initial `mbarrier_init` calls to publish the
520+
barrier state
510521
- **Iteration scaling**: Overhead increases linearly with iteration count
511522
- **Thread efficiency**: All threads remain active throughout processing
512523

‎problems/p29/p29.mojo‎

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ def double_buffered_stencil_computation[
155155

156156
# Wait for buffer_A initialization. mbarrier_test_wait is a non-blocking
157157
# poll, so spin until it reports completion.
158-
_ = mbarrier_arrive(init_barrier.ptr)
159-
while not mbarrier_test_wait(init_barrier.ptr, TPB):
158+
var init_token = mbarrier_arrive(init_barrier.ptr)
159+
while not mbarrier_test_wait(init_barrier.ptr, init_token):
160160
pass
161161

162162
# Iterative stencil processing with double-buffering
@@ -174,19 +174,13 @@ def double_buffered_stencil_computation[
174174
...
175175

176176
# Memory barrier: wait for all writes before buffer swap. test_wait is
177-
# non-blocking, so poll until every thread has arrived.
178-
_ = mbarrier_arrive(iter_barrier.ptr)
179-
while not mbarrier_test_wait(iter_barrier.ptr, TPB):
177+
# non-blocking, so poll until every thread has arrived. The barrier
178+
# advances to its next phase on its own, so it is not re-initialized,
179+
# and each thread waits on the token its own arrive returned.
180+
var iter_token = mbarrier_arrive(iter_barrier.ptr)
181+
while not mbarrier_test_wait(iter_barrier.ptr, iter_token):
180182
pass
181183

182-
# Reinitialize barrier for next iteration
183-
if local_i == 0:
184-
mbarrier_init(iter_barrier.ptr, TPB)
185-
186-
# Make the reinitialized barrier visible before the next iteration's
187-
# mbarrier_arrive.
188-
barrier()
189-
190184
# Write final results from active buffer
191185
if local_i < TPB and global_i < size:
192186
comptime if STENCIL_ITERATIONS % 2 == 0:
@@ -197,8 +191,8 @@ def double_buffered_stencil_computation[
197191
output[global_i] = buffer_B[local_i]
198192

199193
# Final barrier — poll until every thread has arrived.
200-
_ = mbarrier_arrive(final_barrier.ptr)
201-
while not mbarrier_test_wait(final_barrier.ptr, TPB):
194+
var final_token = mbarrier_arrive(final_barrier.ptr)
195+
while not mbarrier_test_wait(final_barrier.ptr, final_token):
202196
pass
203197

204198

‎solutions/p29/p29.mojo‎

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ def double_buffered_stencil_computation[
206206

207207
# Wait for buffer_A initialization. mbarrier_test_wait is a non-blocking
208208
# poll, so spin until it reports completion.
209-
_ = mbarrier_arrive(init_barrier.ptr)
210-
while not mbarrier_test_wait(init_barrier.ptr, TPB):
209+
var init_token = mbarrier_arrive(init_barrier.ptr)
210+
while not mbarrier_test_wait(init_barrier.ptr, init_token):
211211
pass
212212

213213
# Iterative stencil processing with double-buffering
@@ -253,19 +253,13 @@ def double_buffered_stencil_computation[
253253
buffer_A[local_i] = buffer_B[local_i]
254254

255255
# Memory barrier: wait for all writes before buffer swap. test_wait is
256-
# non-blocking, so poll until every thread has arrived.
257-
_ = mbarrier_arrive(iter_barrier.ptr)
258-
while not mbarrier_test_wait(iter_barrier.ptr, TPB):
256+
# non-blocking, so poll until every thread has arrived. The barrier
257+
# advances to its next phase on its own, so it is not re-initialized,
258+
# and each thread waits on the token its own arrive returned.
259+
var iter_token = mbarrier_arrive(iter_barrier.ptr)
260+
while not mbarrier_test_wait(iter_barrier.ptr, iter_token):
259261
pass
260262

261-
# Reinitialize barrier for next iteration
262-
if local_i == 0:
263-
mbarrier_init(iter_barrier.ptr, TPB)
264-
265-
# Make the reinitialized barrier visible before the next iteration's
266-
# mbarrier_arrive.
267-
barrier()
268-
269263
# Write final results from active buffer
270264
if local_i < TPB and global_i < size:
271265
comptime if STENCIL_ITERATIONS % 2 == 0:
@@ -276,8 +270,8 @@ def double_buffered_stencil_computation[
276270
output[global_i] = buffer_B[local_i]
277271

278272
# Final barrier — poll until every thread has arrived.
279-
_ = mbarrier_arrive(final_barrier.ptr)
280-
while not mbarrier_test_wait(final_barrier.ptr, TPB):
273+
var final_token = mbarrier_arrive(final_barrier.ptr)
274+
while not mbarrier_test_wait(final_barrier.ptr, final_token):
281275
pass
282276

283277

0 commit comments

Comments
 (0)