Skip to content
About
Main content

Barriers multiply variance

Simulate a modelNeeds Python with NumPy

Problem

When n tasks must all finish before the next step starts (a ), the step takes as long as its slowest task, not its average one. Task durations vary. With a 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. (Bulk Synchronous Parallel) systems run a computation as rounds, called , with a global barrier after each one.

Reproduce /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

Over 30 rounds on 1,000 workers, how much slower is the job with a barrier after every round?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    barriers-multiply-variance.py55 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat 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

Slowest task over the average taskApple M2 · 8 cores · 8 GB
Simulated1 + c·√(2 ln n)
c
Data table
Slowest task over the average task
Tasks per stage (n)Simulated1 + c·√(2 ln n)
101.461.64
1001.751.91
1K1.972.12
10K2.162.29
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20 · 3 runs

With and without barriers · default knobs

With and without barriers · default knobs
With barriersWithoutRatioHeavy tail: max / mean
59.835.21.7×14.8
Output · uv run barriers-multiply-variance.py --c 0.3
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.8

What 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 , 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.