Barriers multiply variance
Simulate a modelNeeds Python with NumPy
Problem
When n tasks must all finish before the next step starts (a barrier), the step takes as long as its slowest task, not its average one. Task durations vary. With a coefficient of variation c (the standard deviation divided by the mean; c = 0.3 means a typical task is about 30% above or below the mean), the slowest of n light-tailed tasks takes about 1 + c·√(2 ln n) times the mean. BSP (Bulk Synchronous Parallel) systems run a computation as rounds, called supersteps, with a global barrier after each one.
Reproduce E[max]/E[mean] ≈ 1 + c·√(2 ln n) and the BSP rule that the penalty multiplies by the number of rounds.
You will measure the slowest task over the average task for 10 to 10,000 tasks against the formula, a 30-round job on 1,000 workers with and without a barrier every round, and the same slowest-over-average under a heavy-tailed (Pareto) distribution.
The question. How much longer than the average task does the slowest one take as n grows, and how much does a barrier after every round add over 30 rounds?
Predict
Expect about 1.6 at n = 10 and about 2.1 at n = 1,000 for the formula, and simulation values close to it. With barriers in 30 rounds the total time is clearly higher than without barriers (roughly 1.7× in this setup), since the without-barrier workers can average their luck across rounds. The heavy-tail ratio is much larger than the formula predicts, which shows that the formula is the optimistic case.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
barriers-multiply-variance.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Barriers multiply variance. E[max]/E[mean] of n task durations against 1 + c*sqrt(2 ln n), a BSP job with and without a barrier every round, and the same max under a heavy tail. """ import argparse, json import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--c", type=float, default=0.3, help="Coefficient of variation of a task's duration (mean 1).") p.add_argument("--task-counts", type=lambda s: [int(v) for v in s.split(",")], default=[10, 100, 1000, 10000], help="Values of n (tasks per stage), comma-separated.") p.add_argument("--trials", type=int, default=2000, help="Simulated stages per value of n.") p.add_argument("--rounds", type=int, default=30, help="BSP supersteps.") p.add_argument("--workers", type=int, default=1000, help="BSP workers.") p.add_argument("--pareto-alpha", type=float, default=2.5, help="Tail index of the heavy-tailed durations.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() rng = np.random.default_rng(args.seed) c = args.c rows = [] for n in args.task_counts: d = np.clip(rng.normal(1.0, c, (args.trials, n)), 0.05, None) # trials of n tasks sim, formula = d.max(axis=1).mean(), 1 + c * np.sqrt(2 * np.log(n)) rows.append({"n": n, "simulated": float(sim), "formula": float(formula)}) print(f"n = {n:>6,} simulated {sim:.2f} formula {formula:.2f}") # BSP: every round waits for its slowest worker, versus no barrier d = np.clip(rng.normal(1.0, c, (args.rounds, args.workers)), 0.05, None) bsp = d.max(axis=1).sum() # every round waits for its slowest worker async_ = d.sum(axis=0).max() # each worker runs its rounds back to back print(f"\n{args.rounds} rounds x {args.workers} workers with barriers {bsp:.1f} without {async_:.1f} " f"ratio {bsp / async_:.2f}x") # heavy tail: a Pareto-like distribution instead of the normal h = rng.pareto(args.pareto_alpha, (args.trials, args.workers)) + 1 heavy = h.max(axis=1).mean() / h.mean() print(f"heavy-tail max/mean ({args.workers} tasks): {heavy:.1f}") if args.json: with open(args.json, "w") as f: json.dump({"tables": { "max_over_mean": rows, "bsp": [{"with_barriers": float(bsp), "without": float(async_), "ratio": float(bsp / async_), "heavy_tail_max_over_mean": float(heavy)}], }}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run barriers-multiply-variance.py uv run barriers-multiply-variance.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --c0.3 | Coefficient of variation of a task's duration (mean 1). |
| --task-counts10, 100, 1000, 10,000 | Values of n (tasks per stage), comma-separated. |
| --trials2000 | Simulated stages per value of n. |
| --rounds30 | BSP supersteps. |
| --workers1000 | BSP workers. |
| --pareto-alpha2.5 | Tail index of the heavy-tailed durations. |
| --seed0 | Random seed. |
Our run
Data table
| Tasks per stage (n) | Simulated | 1 + c·√(2 ln n) |
|---|---|---|
| 10 | 1.46 | 1.64 |
| 100 | 1.75 | 1.91 |
| 1K | 1.97 | 2.12 |
| 10K | 2.16 | 2.29 |
With and without barriers · default knobs
| With barriers | Without | Ratio | Heavy tail: max / mean |
|---|---|---|---|
| 59.8 | 35.2 | 1.7× | 14.8 |
n = 10 simulated 1.46 formula 1.64
n = 100 simulated 1.75 formula 1.91
n = 1,000 simulated 1.97 formula 2.12
n = 10,000 simulated 2.16 formula 2.29
30 rounds x 1000 workers with barriers 59.8 without 35.2 ratio 1.70x
heavy-tail max/mean (1000 tasks): 14.8What we found
At c = 0.3 the formula gives 1.64 at n = 10 and 2.12 at n = 1,000, as expected. The simulation sits a little below it (1.46 and 1.97): 1 + c·√(2 ln n) is the large-n approximation and overstates the slowest task for small n, and the gap narrows as n grows. Switch c on the chart: at c = 0.5 the slowest of 1,000 tasks takes 2.61 times the mean.
Barriers after every round made the 30-round job 1.7× slower than letting each worker run its rounds back to back, as the chapter expects. Under the heavy tail, the slowest of 1,000 tasks took 14.8× the mean, against about 2.12 from the formula.
Read it
What BSP adds is that the penalty does not average out across rounds. It multiplies by the number of rounds. A fast task in superstep k+1 cannot start early to make up for a slow one in superstep k, because the barrier forbids it. Without the barrier, each worker's luck evens out over its 30 rounds, so the job ends close to the average worker's total.
The reason to accept a barrier is not performance: it makes the computation deterministic and analyzable. What it costs grows with the variance of task times, not with their total, which is why reducing variance (smaller, evener tasks) usually buys more than adding capacity.