"They all finished in twenty milliseconds. I finished in two hundred. We are, the scheduler insists, a team, so the other thousand of them are now waiting on me."
A Straggler Node, Still Catching Up
In a synchronous distributed job the slowest worker, not the average worker, sets the pace: every barrier waits for the last arrival, so a single straggler stalls the entire cluster. This is the tail-at-scale problem, and it has a sharp mathematical edge. The time a barrier waits for is the maximum of the worker times, and the expected maximum of $K$ random durations grows as $K$ grows. A job that runs comfortably on eight workers can spend most of its wall-clock idle on a thousand, not because the work got harder but because the chance that some worker is having a bad moment climbs toward certainty. The previous section gave us the coordination machinery that creates these barriers; this section explains why barriers get more expensive as you scale, and what to do about it. The rest of the book treats straggler mitigation as a first-class design concern, from MapReduce backup tasks to elastic training that drops the slowest replica.
The earlier sections of this chapter built up a vocabulary for distributed coordination: workers and coordinators, synchronization and barriers, partitioning and replication, and the consensus that keeps the control plane honest. We now turn that vocabulary on a problem that is easy to state and hard to escape. When many workers must reach a synchronization point together, the group can only move as fast as its slowest member. A worker that lags, for any of a dozen mundane reasons, is called a straggler, and the resource or stage that limits the whole system is called a bottleneck. The two ideas are cousins: a straggler is a transient, statistical bottleneck that lands on a different worker each step, while a classic bottleneck is a fixed stage that limits throughput every time. Both turn extra machines into idle machines, and both get worse with scale.
1. The Slowest Worker Sets the Pace Beginner
Recall the bulk-synchronous parallel (BSP) pattern from Section 2.2: each superstep is local compute, then a barrier, then a communication step such as the gradient all-reduce. The barrier is the trap. By definition no worker may pass it until every worker has arrived, so the duration of the compute phase is the maximum of the per-worker compute times, never the average. If seven workers finish a step in twenty milliseconds and one takes two hundred, the step takes two hundred milliseconds and the seven fast workers spend ninety percent of the step idle, holding expensive accelerators that compute nothing. Figure 2.7.1 shows exactly this geometry: the useful work is the green bars, the waste is the hatched region, and the straggler alone decides how large that waste is.
This is why stragglers are not a tuning nuisance but a structural property of synchronous distribution. In data-parallel training (Chapter 15), every step is a BSP superstep, so every step pays for its slowest worker. The effect compounds over the hundreds of thousands of steps in a training run: a worker that is ten percent slow on average drags the entire job by close to ten percent, multiplying directly into wall-clock and dollars. The bottleneck need not even be a worker. A single overloaded parameter shard, one saturated network link, or a coordinator that serializes requests can be the fixed stage that throttles everyone, the classic bottleneck that Chapter 3 formalizes through Amdahl's law: the serial or slowest fraction of a job caps the speedup no matter how many workers you add.
Averages lie about synchronous systems. A cluster where the typical worker is fast but the slowest worker is slow behaves like a cluster of slow workers, because the barrier pays the maximum every step. To predict or improve a BSP job's speed, reason about the tail of the per-worker time distribution, not its center. Halving the average compute does little if the tail is untouched; shrinking the tail, even at the cost of a slightly higher average, can be the larger win.
2. Where Stragglers Come From Beginner
Stragglers are not exotic; they are the accumulated noise of running real software on real hardware, and naming the sources makes them addressable. Heterogeneous hardware is the most direct cause: a cluster assembled over time mixes accelerator generations and clock speeds, so identical work takes unequal time. Uneven shards are next: if the data or model is partitioned so that one worker holds more rows, longer sequences, or a hotter embedding range, that worker simply has more to do, a skew problem that Chapter 7 treats in depth for data pipelines. Background interference is the shared-tenant tax: a co-located job, a logging daemon, or a checkpoint write steals CPU, memory bandwidth, or network from the worker for a moment. Garbage collection and runtime pauses freeze a worker mid-step in managed runtimes. Thermal throttling quietly down-clocks an accelerator that has been hot for too long. And network hotspots, a congested switch or an oversubscribed link, delay the messages that a worker needs before it can proceed.
What unites these causes is that they are independent and transient. On any given step, each worker has some small probability of being hit by one of them, and the unlucky worker changes from step to step. That independence is exactly what makes the problem scale-sensitive, as the next section makes precise. It is also why a straggler differs from a permanent bottleneck: a permanently slow node can be detected and removed once, but transient stragglers are a fresh lottery every step, and at large $K$ that lottery is nearly always lost by someone.
Suppose every worker independently has a one-in-a-hundred chance of a slow step. With one worker, ninety-nine percent of steps are clean. With a hundred workers, the chance that all of them are clean on a given step is $0.99^{100} \approx 0.37$, so roughly two steps in three contain a straggler. With a thousand workers it is $0.99^{1000} \approx 0.00004$: a clean step becomes a small miracle. Faster individual workers do not help, because the issue is not any one worker's speed but the rising probability that somebody stumbles.
3. The Mathematics of the Tail at Scale Intermediate
Model worker $k$'s time for one step as a random variable $T_k$, with the $T_k$ independent and identically distributed (the clean case; correlated delays are worse). The barrier waits for the slowest worker, so the step time is the maximum,
$$T_{\text{barrier}}^{(K)} = \max_{1 \le k \le K} T_k, \qquad \mathbb{E}\!\left[T_{\text{barrier}}^{(K)}\right] = \mathbb{E}\!\left[\max_{1 \le k \le K} T_k\right].$$The central fact is that this expected maximum is non-decreasing in $K$ and, for any distribution with an unbounded or heavy right tail, grows without limit as $K \to \infty$. We can see the mechanism through the distribution function. If each $T_k$ has cumulative distribution $F$, then because the workers are independent,
$$\Pr\!\left[T_{\text{barrier}}^{(K)} \le t\right] = \Pr[T_1 \le t, \dots, T_K \le t] = F(t)^{K}.$$Raising $F(t)$ (a number below one) to the $K$-th power pushes the whole distribution of the maximum to the right: the more workers, the more probability mass piles up at large $t$. For a concrete feel, if the per-worker time were exponential with rate $\lambda$, the expected maximum is the harmonic-number expression $\mathbb{E}[\max] = \tfrac{1}{\lambda} H_K = \tfrac{1}{\lambda}\sum_{j=1}^{K} \tfrac{1}{j} \approx \tfrac{1}{\lambda}(\ln K + \gamma)$, which grows like $\ln K$. Light tails give slow ($\ln K$ or $\sqrt{\ln K}$) growth; heavy tails give much faster growth. Either way the direction is the same and it is the wrong one: the synchronous step gets slower as you add workers, purely from the statistics of the maximum, before any communication cost is counted. This is the formal heart of the tail-at-scale phenomenon, and it is why Chapter 3 folds a straggler term into its scaling models rather than assuming perfect balance.
The simulation below makes the growth visible. It draws $K$ worker times from a distribution with a realistic right tail (a baseline plus a lognormal interference term), takes the per-step maximum (the barrier time), and reports how the expected barrier time and its 99th percentile grow as $K$ climbs from one worker to a thousand.
import numpy as np
rng = np.random.default_rng(7)
TRIALS = 200_000 # Monte-Carlo trials per cluster size
KS = [1, 2, 4, 16, 64, 256, 1024]
# Each worker's step time is modeled as a baseline of 1.0 plus a heavy-ish
# right tail (a lognormal "interference" term): most steps are quick, a few
# are badly delayed by GC, thermal throttling, or a network hotspot.
def worker_times(shape):
return 1.0 + rng.lognormal(mean=-0.7, sigma=0.9, size=shape)
print(f"{'K':>5} | {'E[max] (barrier)':>16} | {'p99 of max':>11} | "
f"{'slowdown vs 1':>13}")
print("-" * 56)
base_mean = None
for K in KS:
# The synchronous barrier finishes only when the SLOWEST of K workers does:
# the step time is the row-wise maximum over the K worker draws.
times = worker_times((TRIALS, K))
barrier = times.max(axis=1) # E[max] is what the barrier waits for
e_max = barrier.mean()
p99 = np.percentile(barrier, 99)
if base_mean is None:
base_mean = e_max
print(f"{K:>5} | {e_max:>16.3f} | {p99:>11.3f} | {e_max / base_mean:>12.2f}x")
max over each trial's $K$ worker draws is exactly what a synchronous barrier waits for; averaging over trials estimates the expected maximum. K | E[max] (barrier) | p99 of max | slowdown vs 1
--------------------------------------------------------
1 | 1.744 | 5.030 | 1.00x
2 | 2.100 | 6.067 | 1.20x
4 | 2.546 | 7.145 | 1.46x
16 | 3.770 | 10.021 | 2.16x
64 | 5.475 | 13.620 | 3.14x
256 | 7.775 | 18.370 | 4.46x
1024 | 10.765 | 24.155 | 6.17x
The numbers tell the whole story. Each worker is drawn from the identical distribution at every $K$, yet the barrier time climbs from $1.74$ to $10.77$, a $6.17\times$ slowdown, as the cluster grows from one to a thousand. No worker got slower; the cluster did, because the barrier always waits for the worst of the bunch and the worst of a thousand draws is far out in the tail. This is the quantitative reason a naively synchronous job becomes more straggler-bound the more you scale it, and it motivates every mitigation in the next section.
4. Mitigations: Cutting the Tail Intermediate
Because the problem is the tail of the maximum, every mitigation is, at bottom, a way to keep one slow worker from pricing the whole step. Four families recur throughout the book. The first is backup or speculative tasks, introduced by MapReduce (Chapter 6): near the end of a stage, launch a redundant copy of each still-running task on another node and take whichever finishes first. A duplicated step now finishes in the minimum of two draws instead of one, and the minimum is far less likely to be a tail value, so the maximum over workers shrinks dramatically. The second is load balancing and over-decomposition: cut the work into many more pieces than there are workers, so a worker that draws a heavy piece simply gets fewer pieces, smoothing the per-worker totals. The third is asynchrony: drop the barrier entirely and let fast workers proceed without waiting, the route taken by asynchronous SGD and bounded-staleness parameter servers (Chapter 3 sets up the trade-off and Section 2.5 covered the consistency cost). The fourth is the bluntest: drop the slowest, completing the step from the first $K - m$ arrivals and ignoring the last $m$, which elastic training (Chapter 18) generalizes into removing chronically slow or failed replicas from the job.
The extension below adds backup tasks to the simulation at $K = 256$. Each worker's step now runs as two independent attempts and keeps the faster one (a min of two draws); the barrier then takes the maximum over those per-worker minimums. The cost is one redundant copy of the work; the payoff is a much shorter tail.
# Backup / speculative tasks: at K = 256, launch a redundant copy of each
# worker's step on a separate node; the result is whichever finishes first,
# so each "effective" step time is the MIN of two independent draws. The
# barrier then takes the max over those K minimums.
K = 256
plain = worker_times((TRIALS, K)).max(axis=1)
primary = worker_times((TRIALS, K))
backup = worker_times((TRIALS, K))
with_backup = np.minimum(primary, backup).max(axis=1) # min-of-2, then max
print(f"At K = {K}:")
print(f" barrier E[max], no backup : {plain.mean():6.3f}")
print(f" barrier E[max], 1 backup each : {with_backup.mean():6.3f}")
print(f" tail cut by backup tasks : "
f"{(1 - with_backup.mean() / plain.mean()) * 100:5.1f}% faster barrier")
print(f" extra compute spent on backups : 2.0x (one redundant copy per step)")
At K = 256:
barrier E[max], no backup : 7.776
barrier E[max], 1 backup each : 3.293
tail cut by backup tasks : 57.6% faster barrier
extra compute spent on backups : 2.0x (one redundant copy per step)
The lesson generalizes past this toy distribution. Redundancy is most cost-effective when applied selectively to the suspected stragglers rather than to all work, which is precisely what MapReduce does by launching backups only for the last few stragglers in a stage. The same logic reappears, dressed differently, throughout the book: hedged requests in distributed serving send a second copy of a slow query, and elastic training reconfigures around a node that keeps losing the lottery.
Who: A training infrastructure engineer running a data-parallel pretraining job on a shared 1,024-GPU cluster.
Situation: Step time had quietly drifted from 410 ms to roughly 700 ms over two days, with no change to the model, batch size, or data.
Problem: Average GPU utilization looked healthy, but the synchronous all-reduce barrier was idling hundreds of GPUs every step while it waited for a long tail of late arrivals.
Dilemma: Buy back speed by running backup steps everywhere (doubling compute cost), or find and evict the specific stragglers (cheaper, but only if the cause is localized rather than spread across the fleet).
Decision: Measure first. Per-rank step-time logging showed the tail was dominated by a handful of ranks on two physical nodes, not a fleet-wide slowdown, so eviction beat blanket redundancy.
How: Those nodes were thermally throttling under a failing fan; the engineer drained them, let elastic training (Chapter 18) reconfigure the job to the remaining healthy ranks, and added a per-rank p99 step-time alert.
Result: Step time returned to 415 ms, recovering close to the full $700/415 \approx 1.7\times$ that the tail had stolen, at the cost of two GPUs rather than doubled compute.
Lesson: Diagnose the tail before paying to hide it. When stragglers are localized, removing them is far cheaper than redundancy; when they are diffuse, redundancy or asynchrony is the right tool. Output 2.7.1 is why the symptom looked like a fleet-wide slowdown when only two nodes were sick.
The backup-task logic of Code 2.7.2, detect a slow task, launch a duplicate, keep the first to finish, is built into mature data engines, so you enable it with a flag rather than coding the race yourself. In Spark a single configuration switch turns it on for every stage:
from pyspark.sql import SparkSession
spark = (SparkSession.builder
.config("spark.speculation", "true") # launch backups for slow tasks
.config("spark.speculation.multiplier", "1.5") # "slow" = 1.5x the median task time
.config("spark.speculation.quantile", "0.9") # only after 90% of tasks finish
.getOrCreate())
# Any DataFrame job now duplicates straggling tasks automatically; the engine
# keeps whichever attempt finishes first and cancels the loser.
5. From Stragglers to the Locality That Prevents Them Intermediate
Stragglers and bottlenecks are the price a synchronous distributed AI system pays for coordinating at scale, and we have now seen all three faces of that price: the structural fact that the barrier waits for the maximum, the statistical fact that the maximum grows with $K$, and the engineering fact that backup tasks, balancing, asynchrony, and eviction can cut the tail back. None of these mitigations is free; backups spend compute, asynchrony spends consistency, and dropping the slowest spends a little of the answer. Choosing among them is an exercise in measuring the tail and pricing each remedy against it, which is why Chapter 3 gives the scaling models and Chapter 33 gives the gang scheduling that places a synchronous job on uniform, co-located hardware so that the per-worker time distribution is tight to begin with.
One large source of straggling we have not yet addressed is where the data lives relative to where the computation runs. A worker that must pull its shard across a congested network before it can start is a straggler waiting to happen, and a bottleneck stage is often a stage that fetches more data than it processes. The next section turns to that question directly: how placing computation near its data, and data near its computation, removes a whole class of stragglers before they form. That is the subject of Section 2.8, on data locality and compute locality.
Straggler mitigation has moved from data pipelines into the heart of foundation-model training, where a single slow rank among tens of thousands is a serious cost. Production training stacks now ship dedicated detectors: NVIDIA's NeMo and the broader ecosystem expose straggler-detection callbacks that flag ranks whose per-step time drifts above a threshold, and operators of frontier runs have reported (for example around Meta's Llama 3 fleet, 2024) that transient slowdowns and failures on tens of thousands of GPUs make automated straggler handling a requirement rather than an option. On the asynchronous side, the DiLoCo line of work (Douillard et al., 2024) and its descendants let workers take many local steps between synchronizations, which sharply reduces how often the barrier-maximum is paid and tolerates heterogeneous, even geo-distributed, workers; follow-on streaming variants push the idea toward over-the-internet training where stragglers are the norm. A parallel thread studies elastic and fault-tolerant collectives that reconfigure around a slow or dead rank mid-run without restarting, which Chapter 18 develops in full. The common thread is that, at frontier scale, the tail of Output 2.7.1 is treated as a quantity to be engineered down continuously, not a fixed cost to be accepted.
A team reports that their data-parallel job's "average per-worker step time" is 50 ms and concludes that the job should complete a step in about 50 ms. Using the reasoning of Section 1 and the numbers in Output 2.7.1, explain why this conclusion is wrong for a synchronous (BSP) job and what single statistic they should report instead. Then state one realistic scenario in which the average would be a fair predictor of step time, and say what property of the system makes it so.
Extend Code 2.7.1 and 2.7.2 to sweep the number of backup copies per worker from zero to three at a fixed $K = 256$, reporting the barrier time and the total compute multiplier (number of attempts) for each. Then change the worker-time distribution from the lognormal tail to a light-tailed one (for example a tight Gaussian clipped at zero) and rerun. Explain, using your two tables, why backup tasks help enormously under a heavy tail but barely move the barrier when the tail is light, and what this implies about when a system should bother running speculative copies.
Suppose $K = 64$ workers each process a shard, and 63 shards take 100 ms while one "hot" shard takes 250 ms every step (a deterministic skew, not random noise). Compute the barrier time, the total idle worker-time per step (summed over the 63 fast workers), and the fraction of cluster compute wasted. Now suppose you over-decompose the work into 640 pieces handed out round-robin so the hot region is spread across ten pieces of 25 ms each; estimate the new per-worker totals and the new barrier time, and quantify the improvement. Tie your answer to Amdahl's law as framed in Chapter 3: which term does over-decomposition shrink?