AllToAll: A Different Slice for Every Peer¶
Every rank sends a distinct slice to every peer and receives a distinct slice from every peer — the most general of the point-to-point patterns — then the builtin does it in one call.
Prerequisites: 19-reduce_scatter. Any number of devices ≥ 2 (the examples use 2 and 4 sim devices).
Suggested reading order: 01 → … → 14 → 15 — this page is step 15.
The idea¶
All the previous collectives move the same shape everywhere: broadcast moves
one slice to all, allgather moves each rank's slice to all, reduce-scatter
reduces. All-to-all is different: rank r sends a different N/P slice to
every peer — the slice for destination d is not the slice for destination
e. This is the personalized exchange.
| Aspect | AllToAll |
|---|---|
| Data in | P distinct chunks per rank (one per destination) |
| Data out | P distinct chunks per rank (one from each source) |
| Pattern | Push each dest chunk (put) → barrier → read back |
| Cost | (P-1)/P · N bytes received; no two ranks want the same bytes |
This is the pattern behind real dispatch/combine workloads — distributed MoE sends each token-group to a different expert's rank; AllGather-GEMM pipelines route per-shard data. The walkthrough's See-also links those applications by name (without restating them).
Run it¶
# Hand-rolled: put each dest chunk, barrier, read back.
python examples/distributed/15_all_to_all.py -p a2a3sim -d 0,1
# Reveal: pld.tensor.all_to_all in one call.
python examples/distributed/15_all_to_all.py -p a2a3sim -d 0,1 --mode builtin
# The same source at P=4:
python examples/distributed/15_all_to_all.py -p a2a3sim -d 0,1,2,3
python examples/distributed/15_all_to_all.py -p a2a3sim -d 0,1,2,3 --mode builtin
Expected output:
The golden is constructed so each chunk is unique: input[r, d, j] =
r*1000 + d*100 + j — the source, destination, and element are all encoded in
the value. Any routing mistake (wrong source, wrong destination) shows up as
a wrong value, not a subtle shape issue.
Walkthrough¶
Both modes share one [nr, SIZE] window. Rank r writes its chunk-for-d
at row r of destination d's window, then reads row src of its own
window. The hand-rolled kernel uses step 06's put:
@pl.function(type=pl.FunctionType.InCore)
def hand_step(self, x, y, data, signal):
ctx = pld.get_comm_ctx(data)
my_rank = pld.rank(ctx)
nranks = pld.nranks(ctx)
# Phase 1 — push: write chunk-for-dest into dest's window at our row.
for dest in pl.range(nranks):
pld.tensor.put(data, dest, x, [my_rank, 0], [dest, 0], [1, SIZE])
# Phase 2 — barrier: notify every peer, wait on every peer slot.
for peer in pl.range(nranks):
if peer != my_rank:
pld.system.notify(signal, peer=peer, offsets=[my_rank, 0],
value=1, op=pld.NotifyOp.Set)
for src in pl.range(nranks):
if src != my_rank:
pld.system.wait(signal, offsets=[src, 0], expected=1,
cmp=pld.WaitCmp.Ge)
# Phase 3 — read-back: row src of our window holds src's chunk for us.
for src in pl.range(nranks):
chunk = pl.load(data, [src, 0], [1, SIZE])
y = pl.store(chunk, [src, 0], y)
return y
- The push is a loop over destinations.
pld.tensor.put(data, dest, x, [my_rank, 0], [dest, 0], [1, SIZE])writesx's rowdestinto peerdest's window at our row. Each iteration targets a different peer with a different source row — the "personalized" part. SetnotAtomicAddfor the notify. Each rank is the only writer of its row ([my_rank, 0]), so the barrier here usesSet/Ge(1)— the single-writer form from step 04's walkthrough.- Read-back completes the exchange. After the barrier, row
srcof our own window holds the chunk ranksrcintended for us.
The reveal replaces phases 1–3 with one call:
result = pld.tensor.all_to_all(x, data, signal)
for src in pl.range(nranks):
chunk = pl.load(result, [src, 0], [1, SIZE])
y = pl.store(chunk, [src, 0], y)
return y
- The source is your local
x(a plainpl.Tensor) with rowd= the chunk for destinationd— the same layout the hand-rolled loop used. - The window becomes the result (row
src= the chunk from ranksrc), andinput/targetmust be separate buffers.
The IR diff (the teaching artifact)¶
--mode handlowers to the three phases above:Pputs (one per destination), theSet/Ge(1)barrier, and the read-back loop.--mode builtinexpands into the same shape: your per-destination chunks pushed viapld.tile.put, then the barrier, then the row read-back. Note the barrier's role here — because the transfers come first, it is a completion barrier telling you every peer's push has landed, not a readiness barrier gating a read. The composite also appends a self-clearing epilogue, so the signal is reusable on the next call. (The HOST builtin adds orchestration-level chunking for large transfers.)
Cost card (per rank): every rank sends a different N/P slice to each
peer — (P-1)/P · N bytes received, and no two ranks receive the same bytes.
This is why all-to-all is the most expensive point-to-point pattern to
schedule: every pair exchanges distinct data.
Edge cases¶
Fatal pitfall — reusing one window for source and result. All-to-all delivers its result in place —
targetis rebound as the window-as-result — but the source is a separate buffer: the API requiresinputandtargetto be different — and on the InCore pathxis a plainpl.Tensorwhiledatais the window, so they are two separate buffers of two different kinds, not one window used twice. Passing the same window as both lets the push overwrite chunks you still need to send. Fix: allocate a distinct target window (as the example does) — the builtin enforces this too.
| Symptom | Likely cause | Fix |
|---|---|---|
Chunk for dest d lands in the wrong peer |
put peer ≠ destination row |
put(data, dest, x, ..., [dest, 0], ...) |
| Output has my own chunks, not peers' | Read-back from own rows | Read row src after the barrier |
| Source/target aliasing corruption | Same buffer as input and target | Separate x and data windows |
| One rank hangs | Set notify/wait mismatch |
Notify writes row my_rank of peer; wait reads row src of self |
| Wrong values at P=4 only | Missing barrier before read-back | Barrier between the put loop and the read loop |
See also¶
- 05-tutorials — the tutorial index (this step = row 15)
- 01-collectives §AllToAll — the full API
- 03-execution —
DistributedWorkerand device-side staging for production all-to-all examples/runtime/distributed_callback.py— host-side runtime-bound callbacks around an L3 distributed program- 11-put_get — the
put/getsubstrate this step builds on - More advanced applications (not restated here): pypto-lib #869 (AllGather-GEMM, an allgather pattern from step 13) and the DeepSeek-V4 distributed MoE dispatch/combine (an all-to-all pattern from this step)
- 04-debugging — the canonical failure catalog for distributed programs
- Next step: 21-putting_it_together — compose three collectives in one kernel