Abstract

Nearly every performance problem in training is a data-movement problem, and there are two standard answers to it. Inside a device, tiling restructures a computation so that each value loaded from slow memory is used many times before it is evicted. Between devices, collective operations move and combine data among all workers at once, at a cost that a naive point-to-point implementation cannot match. This article develops both from first principles: the arithmetic-intensity argument that says what tile size is needed, the capacity constraint that says what tile size is affordable, and then the six collectives that appear in distributed training, each with a worked four-rank example and a cost model. It builds on LLM Training: The End-to-End Flow, and it is the mechanism underneath the gradient synchronization studied in PyTorch Distributed Training Internals.

1. The Only Number That Matters

A kernel is limited either by arithmetic or by memory traffic, and which one is decided by a single ratio. The arithmetic intensity of a computation is

I=FLOPs performedbytes moved. I = \frac{\text{FLOPs performed}}{\text{bytes moved}} .

A device has its own ratio, the ridge point, where its peak arithmetic rate and its peak bandwidth balance:

I=peak FLOP/speak bytes/s. I^{*} = \frac{\text{peak FLOP/s}}{\text{peak bytes/s}} .

For an H100 SXM at roughly 990 TFLOP/s of dense BF16 tensor-core throughput and 3.35 TB/s of HBM bandwidth, I295I^{*} \approx 295 FLOP per byte. A kernel with I<II < I^{*} cannot reach peak arithmetic no matter how well its instructions are scheduled; it is waiting on memory. The number is remarkable in itself: modern accelerators demand hundreds of operations per byte before they will run at full speed, and almost nothing achieves that naively.

Tiling exists to raise II. Collectives exist because the same ratio applies across a network, where bandwidth is one to two orders of magnitude lower still.

2. Tiling

2.1 The problem, in one matrix multiply

Consider C=ABC = AB with all three matrices N×NN \times N at bb bytes per element. The arithmetic is fixed at 2N32N^3 FLOPs. The traffic is not.

Computing one output element at a time reads an entire row of AA and an entire column of BB2N2N elements — to produce a single value with 2N2N FLOPs. The intensity is

Inaive=2N2Nb=1b, I_{\text{naive}} = \frac{2N}{2Nb} = \frac{1}{b},

one FLOP per element regardless of NN — and since BF16 elements are b=2b = 2 bytes, half a FLOP per byte. Every byte fetched from HBM buys half an operation, against a ridge point that demands 295, so the multiply runs some 600 times slower than the tensor cores could execute it.

Nothing is wrong with the arithmetic; the problem is that every element loaded is used exactly once.

2.2 The fix

Compute a T×TT \times T block of CC instead of one element. That block needs TT rows of AA and TT columns of BB, and every one of those loaded values participates in TT different output elements.

An output matrix with a single highlighted element requiring a full row and column, beside the same matrix with a highlighted four-by-four block requiring four rows and four columns to produce sixteen outputs.
Figure 1. The same loads, spread over more outputs. One output element consumes a whole row of A and column of B and produces a single value. A 4×4 block consumes four rows and four columns — twice the traffic — and produces sixteen values, so the traffic per output falls by a factor of four. In a real kernel the block is 128×128 and the factor is 128.

Counting the whole product takes three steps. First, CC is divided into

(NT)2 tiles of T×T. \left(\frac{N}{T}\right)^{2} \ \text{tiles of } T \times T .

Second, producing one tile requires the T×NT \times N strip of AA whose rows it spans and the N×TN \times T strip of BB whose columns it spans — 2NT2NT elements loaded, serving T2T^2 outputs. Third, multiply the two:

elements read  =  (NT)2tiles×2NTper tile  =  2N3T, \text{elements read} \;=\; \underbrace{\left(\frac{N}{T}\right)^{2}}_{\text{tiles}} \times \underbrace{2NT}_{\text{per tile}} \;=\; \frac{2N^3}{T},

which is 2N3b/T2N^3b/T bytes. The same count read the other way: each element of AA is loaded once per block-column of CC, so N/TN/T times instead of NN, and likewise for BB. Setting T=1T = 1 returns 2N32N^3 elements, the naive schedule, as it must.

The arithmetic has not changed, so dividing the fixed 2N32N^3 FLOPs by that traffic gives

Itiled=2N32N3b/T=Tb. I_{\text{tiled}} = \frac{2N^3}{2N^3 b / T} = \frac{T}{b} .

A toy example. Take N=4096N = 4096 in BF16 (b=2b = 2), a single layer’s worth of matrix multiply. The arithmetic is 2N3=1372N^3 = 137 GFLOP either way — 0.14 ms at 990 TFLOP/s. Only the traffic changes:

ScheduleElements readBytesTime at 3.35 TB/s
One output element at a time2N3=1372N^3 = 137 G275 GB82 ms
128×128128 \times 128 tiles2N3/T=1.12N^3/T = 1.1 G2.1 GB0.64 ms
Lower bound: each matrix once3N2=503N^2 = 50 M100 MB0.03 ms

The untiled schedule spends 82 ms moving data to cover 0.14 ms of arithmetic — the tensor cores are idle 99.8% of the time. Tiling at T=128T = 128 cuts that to 0.64 ms, which is a 128-fold reduction and still 4.6 times the arithmetic. That residual factor is not a rounding error; §2.3 shows it is exactly the gap between this tile’s intensity and the hardware’s ridge point.

The intensity is set by the tile size, and by nothing else. This single expression is the whole theory of tiling: make the tile as large as it can be, because intensity is linear in it.

2.3 What stops the tile from growing

Capacity. The tile must fit in the fast memory it is being reused from.

Count what has to be resident at one instant. At each step of the kk loop the kernel holds one T×TkT \times T_k chunk of AA and one Tk×TT_k \times T chunk of BB:

TTkA chunk+TkTB chunk  =  2TTk elements. \underbrace{T\,T_k}_{A \text{ chunk}} + \underbrace{T_k\,T}_{B \text{ chunk}} \;=\; 2\,T\,T_k \ \text{elements} .

Those chunks are double-buffered — while the tensor cores consume step ii, the copy engine is already fetching step i+1i+1, which is what keeps the loop from stalling on memory — so two such pairs are live at once. Multiplying by bb bytes per element gives the constraint:

2buffers× 2TTk×b  =  4TTkb  SMEM capacity. \underbrace{2}_{\text{buffers}} \times\ 2\,T\,T_k \times b \;=\; 4\,T\,T_k\,b \ \le\ \text{SMEM capacity} .

On an H100, with up to 228 KB of shared memory per block, T=128T = 128 and Tk=64T_k = 64 in BF16 needs 4128642=644 \cdot 128 \cdot 64 \cdot 2 = 64 KB, a comfortable fit. The resulting intensity is T/b=64T/b = 64 FLOP per byte.

Note what is absent from that inequality: the T×TT \times T output tile. Accumulators live in registers, not shared memory, which gives a second and usually tighter constraint. In fp32 they occupy 4T24T^2 bytes against a 256 KB register file per SM — 64 KB at T=128T = 128, the whole file at T=256T = 256.

Both numbers matter for the next question. An intensity of 64 is well below the ridge point of 295, so a 128×128128 \times 128 tile cannot reach peak from HBM alone. Reaching 295 at b=2b = 2 would need T590T \approx 590, and such a tile needs 45906422954 \cdot 590 \cdot 64 \cdot 2 \approx 295 KB of shared memory — already past the 228 KB limit — while its accumulator would need 45902=1.44 \cdot 590^2 = 1.4 MB of registers, five times the entire file. The tile cannot grow that far on either resource.

2.4 Tiles all the way down

The consequence of the previous section is that tiling is never a single decision. Each level of the memory hierarchy gets its own tile, sized to its own capacity, nested inside the tile of the level above.

An output matrix showing a large block tile subdivided into warp tiles, one of which is subdivided into a single matrix-instruction fragment.
Figure 2. Three nested tiles over the same output matrix. The block tile is what one thread block owns and what shared memory holds; the warp tile is what one warp accumulates in its registers; the fragment is what a single tensor-core instruction produces. Each level is chosen for the capacity and reuse of one level of the hierarchy.
LevelCapacity (H100)BandwidthWhat is tiled for it
Registers256 KB per SM~20 TB/sThe MMA fragment and the accumulator
Shared memory228 KB per block~15 TB/sThe block tile of AA and BB, double-buffered
L250 MB~7 TB/sThe working set of a concurrent wave of blocks
HBM80 GB3.35 TB/sNothing — this is what tiling is avoiding

Reading the table top to bottom, capacity grows by five orders of magnitude and bandwidth falls by one. Every tile boundary is a decision about which of those two facts to pay for.

2.5 The same idea outside GEMM

Tiling is not a matrix-multiply trick, and three examples from elsewhere in this series make that clear.

Attention. FlashAttention tiles the query and key dimensions and keeps the score tile on chip, never materializing the N×NN \times N score matrix in HBM. The online-softmax recurrence exists precisely to make the tiled schedule produce the same answer as the untiled one. That is tiling with an extra requirement: the reduction over the tiled axis must be exact.

The loss. The basics article notes that logits for a large vocabulary can exceed the size of every Transformer block combined. Production trainers compute cross-entropy in chunks over the sequence — a tile over positions — so the full B×L×VB \times L \times |V| tensor never exists at once.

Activation checkpointing. The same trade in the time dimension: instead of holding every intermediate, hold a few and recompute the rest. Recomputation is to activation memory what tiling is to bandwidth — spend arithmetic to avoid storage.

3. Collective Operations

Tiling handles the memory hierarchy of one device. Distributed training adds a slower level below HBM — the interconnect — and the operations that cross it are collectives.

A collective is an operation that every rank in a group calls, and that completes only when all of them have. That property is what allows an implementation to schedule the transfer as a single coordinated pattern rather than as a set of independent messages.

3.1 The six that matter

The vocabulary is small. Take four ranks, each holding a vector, and the picture is complete.

OperationBeforeAfter
BroadcastOne rank has the dataEvery rank has the same copy
ReduceEach rank has a vectorOne rank has the elementwise sum
All-reduceEach rank has a vectorEvery rank has the elementwise sum
Reduce-scatterEach rank has a full vectorEach rank has one distinct slice of the sum
All-gatherEach rank has one sliceEvery rank has the whole concatenation
All-to-allEach rank has one slice per destinationEach rank has one slice from every source

Two relationships are worth committing to memory, because implementations and sharding strategies are built on them:

all-reduce=reduce-scatter+all-gather,all-reduce=reduce+broadcast. \text{all-reduce} = \text{reduce-scatter} + \text{all-gather}, \qquad \text{all-reduce} = \text{reduce} + \text{broadcast} .

The first decomposition is the efficient one and the second is not, for reasons §3.4 makes precise.

3.2 A worked example

Let four ranks each hold a four-element vector:

v0=[1,2,3,4],v1=[5,6,7,8],v2=[9,10,11,12],v3=[13,14,15,16]. v_0 = [1,\,2,\,3,\,4], \quad v_1 = [5,\,6,\,7,\,8], \quad v_2 = [9,\,10,\,11,\,12], \quad v_3 = [13,\,14,\,15,\,16].

All-reduce (sum). Every rank ends with the elementwise sum of all four:

[1+5+9+13,  2+6+10+14,  3+7+11+15,  4+8+12+16]=[28,32,36,40]. [1{+}5{+}9{+}13,\; 2{+}6{+}10{+}14,\; 3{+}7{+}11{+}15,\; 4{+}8{+}12{+}16] = [28,\,32,\,36,\,40].

This is what DDP calls on a gradient bucket, with a division by four afterwards to make it a mean.

Reduce-scatter (sum). The same sums are computed, but each rank keeps only its own slice: rank 0 holds 2828, rank 1 holds 3232, rank 2 holds 3636, rank 3 holds 4040. Each rank ends with a quarter of the data. This is the first half of the ring algorithm, and it is also exactly what a sharded optimizer wants — rank rr only needs the gradient for the parameters it owns.

All-gather. The inverse. If rank rr holds the single value 28,32,36,4028, 32, 36, 40 respectively, all-gather leaves every rank with [28,32,36,40][28, 32, 36, 40]. FSDP calls this on parameters immediately before a layer executes, and frees the result immediately after.

Four ranks with four-element vectors, shown before and after all-reduce, where every rank ends with the elementwise sum, and before and after reduce-scatter, where each rank ends with one element of the sum.
Figure 3. The reduce family on four ranks. All-reduce leaves the full sum everywhere; reduce-scatter computes the same sums but leaves each rank holding only the slice it owns — a quarter of the bytes, for the same arithmetic. Green marks a finished sum.

All-to-all. No reduction at all — a transpose across ranks. If rank rr holds [xr0,xr1,xr2,xr3][x_{r0}, x_{r1}, x_{r2}, x_{r3}], where xrcx_{rc} is destined for rank cc, then afterwards rank cc holds [x0c,x1c,x2c,x3c][x_{0c}, x_{1c}, x_{2c}, x_{3c}]. This is the collective that Mixture-of-Experts routing runs twice per layer: once to send each token to the rank owning its expert, and once to send the results back.

All-gather turning one slice per rank into the full vector on every rank, and all-to-all transposing the slice grid so each rank receives one slice from every other rank.
Figure 4. All-gather and all-to-all. Neither combines values — they only move them. All-gather replicates, so every rank ends with W times the data it started with; all-to-all transposes the grid, so the volume per rank is unchanged.

3.3 Reduction operators, and a warning

The combining operation is a parameter: sum, product, min, max, or average. Training uses sum almost exclusively, followed by a division.

The warning is that floating-point addition is not associative, so a collective that combined values in a different order on different ranks would return slightly different results to each of them — and in data-parallel training, where the correctness argument depends on every replica applying the identical update, that divergence compounds silently. Implementations therefore fix the reduction order for a given group and message size. It is also why changing the world size can perturb a loss curve slightly: the arithmetic is the same, the order is not.

3.4 What they cost

Model the cost of one transfer as a latency term plus a bandwidth term, α+S/B\alpha + S/B for SS bytes on a link of bandwidth BB. Then for WW ranks and a payload of SS bytes per rank:

OperationBytes per rankStepsNotes
Broadcast (tree)S\approx Slog2W\log_2 WLatency grows logarithmically
Reduce-scatter (ring)W1WS\frac{W-1}{W} SW1W - 1Bandwidth-optimal
All-gather (ring)W1WS\frac{W-1}{W} SW1W - 1Bandwidth-optimal
All-reduce (ring)2W1WS2\frac{W-1}{W} S2(W1)2(W-1)The sum of the two above
All-to-allW1WS\frac{W-1}{W} SW1W - 1No reduction; pure permutation

The naive reduce-then-broadcast decomposition is absent from that table because its cost is O(W)O(W) in bandwidth at the root: the root receives W1W-1 copies of the payload. The ring decomposition costs 2S2S asymptotically, independent of WW. That difference is the entire reason collective libraries exist.

A four-by-four grid of ranks and chunks in three states: every rank holding partial values, then each rank owning the finished sum of one chunk, then every rank holding every finished chunk.
Figure 5. How the ring reaches those numbers. Each bucket is split into W chunks; reduce-scatter circulates them for W−1 steps until each rank owns one finished sum, and all-gather circulates the finished chunks for another W−1 steps. Every rank sends and receives on every step of both phases, so no link is ever idle and no rank is a bottleneck.

A concrete step. Take a 7B-parameter model in BF16, so the gradient is S=14S = 14 GB, on eight H100s connected by NVLink at roughly 450 GB/s per direction:

Tall-reduce27814 GB450 GB/s54 ms. T_{\text{all-reduce}} \approx 2\cdot\frac{7}{8}\cdot\frac{14\ \text{GB}}{450\ \text{GB/s}} \approx 54\ \text{ms}.

The same collective across nodes over a 400 Gb/s InfiniBand link — 50 GB/s — takes about 490 ms. Nine times the cost for the same bytes, which is why NCCL builds hierarchical rings that cross the slow level as few times as possible, and why a step whose backward pass takes 200 ms can hide the first number and cannot hide the second.

3.5 Where each one appears

StrategyCollectiveWhen
Data parallel (DDP)All-reduce of gradientsOnce per bucket, during the backward pass
ZeRO / FSDPAll-gather of parameters, reduce-scatter of gradientsPer layer, forward and backward
Tensor parallelAll-reduce of activationsTwice per Transformer block, in both passes
Sequence parallelReduce-scatter and all-gather of activationsReplaces the tensor-parallel all-reduce
Pipeline parallelPoint-to-point send and receiveAt each stage boundary
Mixture-of-ExpertsAll-to-allTwice per MoE layer
Metrics and checkpointingSmall all-reduce, broadcastOnce per step or per checkpoint

The rows differ in one respect that matters more than the collective they name: what the volume scales with. Data-parallel traffic scales with parameter count and is independent of batch size. Tensor-parallel and sequence-parallel traffic scales with activations, so it grows with batch and sequence length. Expert routing scales with tokens. A configuration is chosen by deciding which of those quantities the interconnect can afford to move.

4. The Same Argument, Twice

The two halves of this article are the same optimization at different scales.

Tiling raises intensity by reusing each loaded byte across more arithmetic. Ring collectives raise effective bandwidth by making every link carry useful traffic simultaneously instead of funnelling through one endpoint. Both replace a schedule whose cost grows with the size of the problem — O(N)O(N) reads per output, O(W)O(W) copies through a root — with one whose cost is bounded by the hardware’s own limits.

And both fail in the same recognizable way. A tile too large for shared memory spills; a bucket too small for the interconnect pays latency instead of bandwidth. In each case the fix comes from the same two numbers: how much fast storage there is, and how many operations each transferred byte can be made to serve.

5. Summary

  • Arithmetic intensity, measured against the device’s ridge point, decides whether a kernel is limited by math or by memory. An H100 needs roughly 295 FLOP per byte.
  • Tiling raises intensity linearly in the tile size, I=T/bI = T/b, and the tile size is capped by the capacity of the memory it is reused from. This is why tiles are nested, one per level of the hierarchy.
  • The same reasoning applies outside GEMM: FlashAttention, chunked cross-entropy, and activation checkpointing are all tilings.
  • Six collectives cover distributed training. All-reduce decomposes into reduce-scatter plus all-gather, and that decomposition, not reduce-plus-broadcast, is the efficient one.
  • Ring collectives move W1WS\frac{W-1}{W}S bytes per rank per phase, so cost tracks payload rather than worker count — but only within a level of the interconnect hierarchy. Crossing nodes costs roughly an order of magnitude more per byte.

References

  1. Samuel Williams, Andrew Waterman, and David Patterson. Roofline: An Insightful Visual Performance Model for Multicore Architectures. CACM 2009.
  2. Pitch Patarasuk and Xin Yuan. Bandwidth Optimal All-reduce Algorithms for Clusters of Workstations. JPDC 2009.
  3. Rajeev Thakur, Rolf Rabenseifner, and William Gropp. Optimization of Collective Communication Operations in MPICH. IJHPCA 2005.
  4. Tri Dao et al. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. NeurIPS 2022.
  5. Vijay Thakkar et al. CUTLASS. NVIDIA, 2023.
  6. Samyam Rajbhandari, Jeff Rasley, Olatunji Ruwase, and Yuxiong He. ZeRO: Memory Optimizations Toward Training Trillion Parameter Models. SC 2020.