GPU Architecture · Cooperation & Optimization
__syncthreads(): the block barrierHow the threads of a block cooperate safely through shared memory — the primitive every tiling kernel is built on.
You've met shared memory (Lesson 5) and the reason it's fast (banks). But cooperation has a catch you already have all the pieces to see. From Lesson 2: the warps of a block interleave in no guaranteed order — the scheduler runs whichever is ready. So when thread A writes a shared slot that thread B reads, who says A ran first? Nobody. That gap is what __syncthreads() closes.
When a thread calls __syncthreads(), it waits until every thread in its block has reached that same line. Only then does any of them continue. "Nobody moves until everybody's here." It gives two guarantees, and you need both:1
That second guarantee is the real payload: not just "wait," but "wait — and now everyone can see what everyone wrote."
Here is the bug, in the tiling shape from Lesson 4. Each thread writes its own shared slot, then reads a neighbour's:
__shared__ float tile[256];
tile[threadIdx.x] = input[gid]; // each thread writes ITS slot
// ✗ RACE if you read here — the neighbour's warp may not have run yet
float x = tile[threadIdx.x + 1]; // reads a NEIGHBOUR's slot
Thread 0 reads tile[1] — but the warp owning tile[1] might not have been scheduled yet, so the slot is uninitialised garbage. Wrong answers that change with occupancy. The barrier turns "warps run in unknown order" into clean phases:
A real tiling loop usually has two syncs per iteration, each guarding an opposite hazard:
tile[threadIdx.x] = input[...]; // LOAD phase
__syncthreads(); // ① don't READ until the tile is fully written
sum += tile[threadIdx.x] + tile[threadIdx.x ^ 1]; // COMPUTE phase (reads tile)
__syncthreads(); // ② don't OVERWRITE until everyone finished reading
tile[threadIdx.x] = input[...next...]; // next LOAD would clobber otherwise
There is deliberately no cross-block __syncthreads(). Recall Lesson 3: blocks are independent — the GigaThread Engine runs them in any order, in waves, on different SMs; two blocks may not even be resident at the same time. Asking block A to wait for block B, when B hasn't launched yet, would deadlock by construction. So the only grid-wide barrier is the end of the kernel: if phase 2 needs all blocks of phase 1 done, you split it into two kernel launches.
Cheap. Each SM keeps a per-block arrival counter. A warp that hits the barrier is marked "arrived" and becomes not-ready — this is literally the stall_barrier warp-stall reason from Lesson 2. When the count reaches the block's warp total, the SM releases them all. Just a counter and a stall bit — the same "simple hardware, in-order per warp" bargain as the scheduler.
Because the barrier waits for every thread, all threads must reach the same __syncthreads() — or none. Hide it behind a thread-divergent branch and you break it:
if (threadIdx.x < 100) {
__syncthreads(); // ✗ threads 100+ never arrive → barrier never completes → hang
}
The threads that skip it never increment the counter, so the waiting ones hang forever (pre-Volta: hard deadlock; generally: undefined behaviour). Rule: a barrier inside a conditional is safe only if the condition is uniform across the whole block (e.g. based on blockIdx) — never on a threadIdx-divergent path. compute-sanitizer --tool synccheck catches violations.2
The companion program reverses each block's segment through shared memory — the reversed write reads a neighbour's slot, so it needs the barrier. Build it both ways:
# correct — barrier present
nvcc -arch=sm_120 02-syncthreads-reverse.cu -o rev && ./rev
# barrier removed — a data race (may pass by luck, or FAIL)
nvcc -arch=sm_120 -DSKIP_SYNC 02-syncthreads-reverse.cu -o rev_bad && ./rev_bad
# let the tool find the race the naked eye can't:
compute-sanitizer --tool racecheck ./rev_bad
./rev prints PASS. The rev_bad build may still pass on a quiet GPU — races are nondeterministic — but racecheck will report the shared-memory hazard regardless. That's the lesson: correctness you can't see is exactly why the barrier and the sanitizer both exist. File: code/02-syncthreads-reverse.cu.
From memory — one click locks each answer.
Recall__syncthreads() makes the calling thread wait until —
It's a block-scoped barrier: all threads of the block must arrive before any continue. Not grid-wide (blocks are independent), not SM-wide, and it's a barrier — not merely a memory drain (though it does also fence memory).
MechanismYou need a barrier between writing your shared slot and reading a neighbour's because —
Warps are scheduled in no fixed order (Lesson 2), so the neighbour's write may not have happened yet. The barrier forces all writes to complete and become visible first. Shared memory is fast, and all threads of a block share one SM — neither is the issue.
FootgunPutting __syncthreads() inside if (threadIdx.x < 100) risks —
Threads with threadIdx.x ≥ 100 skip the barrier and never arrive, so the arrival counter never fills and the waiting threads hang (undefined behaviour / deadlock). A barrier in a conditional is safe only if the condition is uniform across the whole block.
CUDA C++ Programming Guide — "Synchronization Functions." The precise semantics of __syncthreads() (barrier + memory ordering) and the uniform-conditional rule. Read it here. Also excellent: Mark Harris, "Using Shared Memory in CUDA."
This is the key that unlocks tiling. Next in the optimization arc: a real tiled kernel — load a chunk into shared memory once (barrier), reuse it many times (barrier), and measure the achieved bandwidth climbing toward your 320 GB/s. Then profile it under Nsight and read the stall_barrier / stall_long_scoreboard reasons on your own kernel. Ask when you're ready — or ask me anything unclear here first.
__syncthreads() waits until all threads in the block reach it and acts as a memory fence for shared and global accesses made by the block. docs.nvidia.com__syncthreads() is permitted in conditional code only if the condition evaluates identically across the entire thread block; otherwise execution is likely to hang or behave unexpectedly.