Skip to content
About
Main content

Run your own config dump, seed sweep and scale ladder

Measure your machineNeeds Python with NumPy

Problem

A result that cannot be rerun cannot be checked. A reproducible benchmark records everything that could change the answer (versions, kernel, CPU, every non-default setting), generates its data from a fixed seed, keeps every raw measurement rather than only an average, and sweeps the size rather than measuring one point, because costs are often flat until a boundary (a , memory) and then jump, and a single point cannot show where.

Write one script that: (1) records versions, kernel, CPU model and every non-default setting into config.txt; (2) generates a dataset from a fixed seed; (3) runs the benchmark for sizes 1x, 3x, 10x and 30x; (4) writes raw per-run results into a CSV; all for 5 seeds. The benchmark is random lookups into an in-memory array of 8 MB to 240 MB.

The question. Where does the per-key cost jump, and how large is the seed-to-seed spread next to the average?


Predict

Across five seeds at 8 MB, how large is the spread of the per-key cost against its mean?

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.

    run-your-own-config-dump-seed.py66 linesDownload
    View the code
    run-your-own-config-dump-seed.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Run your own: config dump, seed, sweep, raw results.
    
    One command that (1) records versions, kernel, CPU model and every non-default
    setting into config.txt; (2) generates the data from a fixed seed; (3) runs the
    benchmark (random lookups into an in-memory array of keys) at sizes 1x, 3x,
    10x and 30x; (4) writes every raw per-run result into results.csv; all of it
    for 5 seeds. Prints the per-key cost per size with the seed-to-seed spread.
    """
    import argparse, csv, json, os, platform, statistics, subprocess, sys, time
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--base-mb", type=float, default=8, help="Data size at 1x (MB).")
    p.add_argument("--sizes", default="1,3,10,30", help="Size multipliers.")
    p.add_argument("--seeds", type=int, default=5, help="Seeds to run.")
    p.add_argument("--lookups", type=int, default=10_000_000, help="Random lookups per run.")
    p.add_argument("--out", default="run-your-own-out", help="Directory for config.txt and results.csv.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    defaults = {"base_mb": 8, "sizes": "1,3,10,30", "seeds": 5, "lookups": 10_000_000}
    def sh(cmd):
        try: return subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout.strip()
        except Exception: return "unavailable"
    os.makedirs(args.out, exist_ok=True)
    cpu = sh(["sysctl", "-n", "machdep.cpu.brand_string"]) if sys.platform == "darwin" else \
          next((l.split(":", 1)[1].strip() for l in open("/proc/cpuinfo") if l.startswith("model name")), platform.processor())
    config = {"python": sys.version.split()[0], "numpy": np.__version__, "os": platform.platform(), "kernel": platform.release(),
              "cpu": cpu, "cores": os.cpu_count(), "command": " ".join(sys.argv),
              "non_default_settings": {k: v for k, v in vars(args).items() if k in defaults and v != defaults[k]} or "none"}
    with open(os.path.join(args.out, "config.txt"), "w") as f:
        for k, v in config.items(): f.write(f"{k}: {v}\n")
    
    rows = []
    for seed in range(args.seeds):
        rng = np.random.default_rng(seed)
        for m in map(float, args.sizes.split(",")):
            n = int(args.base_mb * m * 1e6 / 8)
            keys = rng.integers(0, 1 << 62, n)                              # the data set, from the seed
            idx = rng.integers(0, n, args.lookups)
            keys[idx[:1000]].sum()                                          # warm up
            t = time.perf_counter(); s = keys[idx].sum(); el = time.perf_counter() - t
            rows.append({"seed": seed, "size_x": m, "size_mb": n * 8 / 1e6, "ns_per_key": el / args.lookups * 1e9})
    with open(os.path.join(args.out, "results.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    
    sizes = []
    for m in sorted({r["size_x"] for r in rows}):
        xs = [r["ns_per_key"] for r in rows if r["size_x"] == m]
        sizes.append({"size_x": m, "size_mb": next(r["size_mb"] for r in rows if r["size_x"] == m), "mean_ns": statistics.mean(xs),
                      "min_ns": min(xs), "max_ns": max(xs), "spread_pct": (max(xs) - min(xs)) / statistics.mean(xs) * 100})
    for i, s in enumerate(sizes):
        s["step_x"] = s["mean_ns"] / sizes[i - 1]["mean_ns"] if i else 1.0
        print(f"{s['size_x']:>4g}x ({s['size_mb']:6.0f} MB): {s['mean_ns']:5.2f} ns per key  (seeds {s['min_ns']:.2f}-{s['max_ns']:.2f}, spread {s['spread_pct']:.1f}%)  x{s['step_x']:.2f} vs previous size")
    print(f"wrote {args.out}/config.txt and {args.out}/results.csv ({len(rows)} raw rows)")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"config": config, "tables": {"sizes": sizes, "runs": 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 run-your-own-config-dump-seed.py
    uv run run-your-own-config-dump-seed.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--base-mb8 Data size at 1x (MB).
--sizes1, 3, 10, 30 Size multipliers.
--seeds5 Seeds to run.
--lookups10,000,000 Random lookups per run.
--outrun-your-own-out Directory for config.txt and results.csv.

Our run

Cost per lookup against data size (mean of 5 seeds)Apple M2 · 8 cores · 8 GB
Data table
Cost per lookup against data size (mean of 5 seeds)
Data size (MB)ns per lookup
81.3
244.25
805.46
2405.74
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20

Per size, over 5 seeds

Per size, over 5 seeds
SizeMBMean nsMinMaxSpread
181.31.12.0573%
3244.253.586.5870.7%
10805.465.235.617.04%
302405.745.645.924.92%
Output · uv run run-your-own-config-dump-seed.py
   1x (     8 MB):  1.30 ns per key  (seeds 1.10-2.05, spread 73.0%)  x1.00 vs previous size
   3x (    24 MB):  4.25 ns per key  (seeds 3.58-6.58, spread 70.7%)  x3.26 vs previous size
  10x (    80 MB):  5.46 ns per key  (seeds 5.23-5.61, spread 7.0%)  x1.29 vs previous size
  30x (   240 MB):  5.74 ns per key  (seeds 5.64-5.92, spread 4.9%)  x1.05 vs previous size
wrote run-your-own-out/config.txt and run-your-own-out/results.csv (20 raw rows)

What we found

The cost jumped between 8 MB and 24 MB, from 1.3 to 4.25 ns per lookup (3.26×), and was nearly flat after (5.74 ns at 240 MB): the boundary is the CPU's cache, which an 8 MB array fits and a 24 MB one does not. The spread across seeds was 73% at 8 MB and 4.92% at 240 MB: the sizes near the boundary are the noisiest, so a single run there would say little.


Read it

A straight line through the 1x and 30x points would hide the jump entirely. Sweeping sizes is how you find the boundaries a system has, and the raw rows and config dump are what let someone else find the same boundary on their machine, or a different one, and know why.


Go further

Check that a colleague can rerun the whole experiment with one command.