Skip to content
About
Main content

Fit a USL curve to something real

Measure your machineNeeds Python with NumPy, SciPy

Problem

The Universal Scalability Law fits at N workers with two parameters: α, the share of work that serializes (contention), and β, the cost of keeping workers coherent, which grows with every pair. With β > 0 the fitted curve has a peak and falls after it. A fit is a model of the measured range; whether its prediction beyond that range holds depends on whether the cause it assumes (coherence) is the one at work.

Take a parallel workload you control and measure throughput at several values of N, then fit α and β. Here the workload is N processes that each scan their own 64 MB array, a load that shares nothing but memory . Fit at N = 1, 2, 3, 4, 6, 8, then test beyond the predicted peak (N = 12, 16, 24).

The question. What α and β does the fit give, and does throughput really fall beyond the predicted peak?


Predict

At 24 workers, well beyond the fitted peak, how does throughput compare with one worker's?

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.

    fit-a-usl-curve-to-something.py56 linesDownload
    View the code
    fit-a-usl-curve-to-something.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy", "scipy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Fit a USL curve to something you run.
    
    A parallel workload: N worker processes each scan their own 64 MB array
    (a memory-bandwidth-bound sum) for a fixed time; throughput is the total GB
    scanned per second. Measured at N = 1, 2, 3, 4, 6, 8; the Universal
    Scalability Law is fitted to those points, and then throughput is measured
    beyond them (N = 12, 16, 24) to test the fit's prediction.
    """
    import argparse, json, multiprocessing as mp, time
    import numpy as np
    from scipy.optimize import curve_fit
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--fit-n", default="1,2,3,4,6,8", help="Worker counts used for the fit.")
    p.add_argument("--test-n", default="12,16,24", help="Worker counts beyond the fit.")
    p.add_argument("--seconds", type=float, default=3.0, help="Seconds per measurement.")
    p.add_argument("--mb", type=int, default=64, help="Array per worker (MB).")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    def worker(ready, seconds, out):
        a = np.ones(args.mb * 2**20 // 8); a.sum(); n = 0
        ready.wait()                                                       # every worker is up and has its array
        stop_at = time.time() + seconds
        while time.time() < stop_at: a.sum(); n += 1
        out.put(n * a.nbytes)
    
    def measure(n):
        out = mp.Queue(); ready = mp.Barrier(n)
        ps = [mp.Process(target=worker, args=(ready, args.seconds, out)) for _ in range(n)]
        for q in ps: q.start()
        total = sum(out.get() for _ in ps)
        for q in ps: q.join()
        return total / args.seconds / 1e9
    
    if __name__ == "__main__":
        fit_n = [int(x) for x in args.fit_n.split(",")]; test_n = [int(x) for x in args.test_n.split(",")]
        X = {n: measure(n) for n in fit_n + test_n}
        N = np.array(fit_n, dtype=float); C = np.array([X[n] for n in fit_n]) / X[1]
        usl = lambda n, a, b: n / (1 + a * (n - 1) + b * n * (n - 1))
        (a, b), _ = curve_fit(usl, N, C, bounds=(0, 1))
        peak = float(np.sqrt((1 - a) / b)) if b > 0 else None
        rows = [{"N": n, "gb_s": X[n], "relative": X[n] / X[1], "fit": float(usl(n, a, b)), "used_for_fit": "yes" if n in fit_n else "no (test)"} for n in fit_n + test_n]
        summary = {"alpha": float(a), "beta": float(b), "peak_n": peak, "best_n": max(X, key=X.get), "best_gb_s": max(X.values())}
        print(f"alpha={a:.4f} beta={b:.6f} " + (f"peak at N*={peak:.0f}" if peak else "no peak (beta = 0)"))
        for r in rows: print(f"N={r['N']:>3}  {r['gb_s']:6.1f} GB/s  relative {r['relative']:5.2f}  USL fit {r['fit']:5.2f}  ({r['used_for_fit']})")
        if args.json:
            with open(args.json, "w") as f:
                json.dump({"summary": summary, "tables": {"throughput": rows}}, 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 fit-a-usl-curve-to-something.py
    uv run fit-a-usl-curve-to-something.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--fit-n1, 2, 3, 4, 6, 8 Worker counts used for the fit.
--test-n12, 16, 24 Worker counts beyond the fit.
--seconds3 Seconds per measurement.
--mb64 Array per worker (MB).

Our run

Relative throughput and the USL fitApple M2 · 8 cores · 8 GB
MeasuredUSL fit (fitted on N ≤ 8)
Data table
Relative throughput and the USL fit
Workers NMeasuredUSL fit (fitted on N ≤ 8)
111
21.41.35
31.481.48
41.51.53
61.481.51
81.471.44
121.471.28
161.461.14
241.440.922
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · scipy 1.18.1 · run 2026-10-20

Throughput

Throughput
NGB/sRelativeUSL fitUsed for fit
143.211yes
260.71.41.35yes
363.81.481.48yes
464.71.51.53yes
664.11.481.51yes
863.51.471.44yes
1263.61.471.28no (test)
1663.21.461.14no (test)
2462.31.440.922no (test)
Output · uv run fit-a-usl-curve-to-something.py
alpha=0.4310 beta=0.027373 peak at N*=5
N=  1    43.2 GB/s  relative  1.00  USL fit  1.00  (yes)
N=  2    60.7 GB/s  relative  1.40  USL fit  1.35  (yes)
N=  3    63.8 GB/s  relative  1.48  USL fit  1.48  (yes)
N=  4    64.7 GB/s  relative  1.50  USL fit  1.53  (yes)
N=  6    64.1 GB/s  relative  1.48  USL fit  1.51  (yes)
N=  8    63.5 GB/s  relative  1.47  USL fit  1.44  (yes)
N= 12    63.6 GB/s  relative  1.47  USL fit  1.28  (no (test))
N= 16    63.2 GB/s  relative  1.46  USL fit  1.14  (no (test))
N= 24    62.3 GB/s  relative  1.44  USL fit  0.92  (no (test))

What we found

The fit gave α = 0.431 and β = 0.027, with a peak at N* = 4.56. Beyond it, throughput did not fall: 62.3 GB/s at 24 workers against 64.7 at 4, where the fit predicted 0.922 of one worker's throughput and the measurement was 1.44. The workload saturates memory bandwidth and stays flat; the fit read that plateau, plus noise, as coherence cost.


Read it

A fit always produces a β, and a β always produces a peak. Whether the decline is real depends on the mechanism: coherence traffic (locks, shared counters, cache-line ping-pong) does make systems slower as workers are added; a saturated shared resource only makes them stop getting faster. Test beyond the peak before acting on it.