Collectives are kernels
A collective is not a primitive; it is a kernel somebody wrote. On a TPU pod, chips along a mesh axis form physical rings, and an all-gather is what it looks like: N−1 hops where every chip pushes one shard to its neighbor. The push is the TPU's native distributed move: an async remote DMA that writes directly into the neighbor's VMEM and signals a semaphore, leaving the MXU free the whole time. Once you have built one collective from these parts, the opaque names (all-gather, reduce-scatter, all-to-all) resolve into loop shapes you can reason about like any other schedule.
Overlap is the economics
The economics that make this stage matter at scale: at frontier size, the dominant tax is not slow kernels but exposed communication, the fraction of each step where compute units idle waiting on the interconnect. The cure is overlap: transfer shard t+1 while computing on shard t, which is exactly the double-buffering pattern from stage 1 with the arrows crossing chips. EX·04 shows it: the same streaming attention schedule as EX·03 with the KV transport swapped from HBM DMA to remote DMA. Same algebra, longer arrows.
Deadlock, met safely
With semaphores comes the one genuinely new failure mode of this stage: deadlock. A send waiting on a recv that waits on the send. LAB·4.1 has you build a deadlock on purpose in interpret mode, because meeting this failure for the first time in a real training run is a bad week, and meeting it in a toy is an afternoon.
Composition closes the loop
The composition payoff closes the loop on the whole track: ring attention is LAB·3.1's monoid folded over shards arriving via LAB·4.1's ring, and nothing new needs deriving because associativity was proven once. When compositions keep coming out correct because one theorem was proven early, you have learned the track's deepest lesson: the famous kernels are few theorems wearing many schedules.
The famous kernels are few theorems wearing many schedules.
Instruments
01/06prologue · Q pins resident in VMEM; K0, V0 stream in from the neighbor chip
00/05tick 0: chip 0 send (hop 1) fires · chip 1 send (hop 1) fires · chip 2 send (hop 1) fires · chip 3 send (hop 1) fires
all-gather, ring over x: at hop 0, each chip holds 1 of 4 shards. Fixed example: a 32 MB shard (32,000,000 bytes) moves over every active link each hop.
- link bandwidth
- 45.0 GB/s one-way (TPU v5e ICI, chips.json's only ICI field, used for every link shown)
- bytes this hop
- 0 MB, initial state
- time this hop
- n/a, no hop taken yet
- elapsed
- 0.000 ms (0 MB moved / link)
- full collective, 3 hops
- 2.133 ms
What gets built
- Ring all-gather from raw remote copies, validated against the collective
- A deliberate deadlock, observed and explained
- A toy ring attention step: the streaming algebra is indifferent to where a block came from
Readings
- Distributed computing in Pallas for TPUs · the RDMA model, semaphores, and the ring all-gather this stage rebuilds
The work, in order
week 9 · the ring
- LAB·4.1: ring all-gather as ppermute hops, validated exactly against lax.all_gather
- On a real slice: the same loop as Pallas remote DMAs, bitwise validated, overlap found in the profile
- Break it on purpose: reorder one semaphore wait under interpret mode and study the deadlock
week 10 · the composition
- LAB·4.2: ring attention from the stage 3 monoid plus the ring; verify equal to full attention
- On a slice: swap hops for remote DMAs, put the flash kernel inside the loop, profile the hop hiding under compute
- Note what you did not do: re-derive anything; write the paragraph explaining why causal ring attention is free
Labs
From the notebook
# the ring algorithm: after step t, every device holds t+1 shards
mesh = Mesh(np.array(devs), ("x",))
AXIS = len(devs)
@partial(shard_map, mesh=mesh, in_specs=P("x", None), out_specs=P("x", None))
def ring_all_gather(shard):
n = AXIS # static mesh size from the closure
idx = jax.lax.axis_index("x")
out = jnp.zeros((n, *shard.shape[1:]), shard.dtype)
out = out.at[idx].set(shard[0])
recv = shard[0]
def hop(t, carry):
out, recv = carry
recv = jax.lax.ppermute(recv, "x", [(i, (i + 1) % n) for i in range(n)])
src = (idx - t - 1) % n
return out.at[src].set(recv), recv
out, _ = jax.lax.fori_loop(0, n - 1, hop, (out, recv))
return out[None]
x = jax.random.normal(jax.random.key(0), (AXIS, 4, 128))
got = ring_all_gather(x).reshape(AXIS, AXIS, 4, 128)
want = jnp.broadcast_to(x[None], (AXIS, AXIS, 4, 128))
check("ring all-gather == replicated truth", got, want, tol=0)
print(f"validated over {AXIS} devices: every device reconstructed all {AXIS} shards")# LAB 3.1's summarize/combine, verbatim: transport changes, algebra does not
def summarize(s_block, v_block):
m = jnp.max(s_block, axis=-1)
p = jnp.exp(s_block - m[..., None])
return (m, jnp.sum(p, axis=-1), p @ v_block)
def combine(a, b):
m1, l1, acc1 = a
m2, l2, acc2 = b
m = jnp.maximum(m1, m2)
a1, a2 = jnp.exp(m1 - m), jnp.exp(m2 - m)
return (m, l1 * a1 + l2 * a2, acc1 * a1[..., None] + acc2 * a2[..., None])
mesh = Mesh(np.array(devs), ("x",))
AXIS = len(devs)
@partial(shard_map, mesh=mesh,
in_specs=(P("x", None), P("x", None), P("x", None)), out_specs=P("x", None))
def ring_attention(q, k, v):
n = AXIS # static mesh size from the closure
perm = [(i, (i + 1) % n) for i in range(n)]
state = summarize(q @ k.T, v) # my resident KV shard
def hop(t, carry):
state, k, v = carry
k = jax.lax.ppermute(k, "x", perm) # the shard rides the ring...
v = jax.lax.ppermute(v, "x", perm)
return combine(state, summarize(q @ k.T, v)), k, v # ...while we fold it in
state, _, _ = jax.lax.fori_loop(0, n - 1, hop, (state, k, v))
m, l, acc = state
return acc / l[..., None]SQ, SKV, D = 16 * AXIS, 32 * AXIS, 64
q = jax.random.normal(jax.random.key(0), (SQ, D))
k = jax.random.normal(jax.random.key(1), (SKV, D))
v = jax.random.normal(jax.random.key(2), (SKV, D))
got = ring_attention(q, k, v)
want = jax.nn.softmax(q @ k.T, axis=-1) @ v
check("ring attention == full attention", got, want, tol=1e-5)
print(f"distributed over {AXIS} devices, no device ever held more than 1/{AXIS} of KV")Can you
The gate
| criterion | measured |
|---|---|
| Ring all-gather matches lax.all_gather bitwise, overlap visible in the profile | exact on 8 simulated devices; slice profile pending |
Public artifact for this stage: a collective is just a kernel.