Skip to content
About
Main content

Simulate the spot gang

Simulate a modelNeeds Python with NumPy

Problem

(preemptible) machines are cheap because the provider can take them back at any moment. For an independent task, an interruption costs a retry. For a gang, a job that needs all of its nodes at once (a training run, a tightly coupled MPI job, a stage with a barrier), one interruption anywhere stops everything. The chance that n nodes all survive is (1 − p)^n, which collapses quickly as n grows. Checkpointing limits the damage: an interruption then loses only the work since the last , at the cost of writing checkpoints.

Simulate 100 nodes, each interrupted with probability 5% per hour, and a job that needs all of them. Extend it with a checkpoint interval and count the expected lost work per interruption. Then change the model so that only the workers are spot and the coordinator is not, and compare the expected completion time.

The script simulates the one-hour survival, and uses the closed form for the completion time of a 10-hour job (a segment restarts until it runs clean, which takes (e^(λτ) − 1)(1/λ + R) hours on average).

The question. What is the chance that a 100-node spot gang survives an hour, and how does the checkpoint interval set how long a 10-hour job really takes?


Predict

With hourly checkpoints, how long does the 10-hour job take on a 100-node spot gang?

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.

    simulate-the-spot-gang.py57 linesDownload
    View the code
    simulate-the-spot-gang.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["numpy"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Simulate the spot gang.
    
    A job that needs all of its 100 spot nodes at once survives an hour only if
    none of them is interrupted (simulated). Then checkpoints: the expected work
    lost per interruption, and the expected completion time of a 10-hour job,
    with all nodes on spot and with only the workers on spot (closed form).
    """
    import argparse, json
    import numpy as np
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--nodes", type=int, default=100, help="Nodes the job needs at once.")
    p.add_argument("--p", type=float, default=0.05, help="Probability that one node is interrupted in an hour.")
    p.add_argument("--job-hours", type=float, default=10, help="Work in the job, in hours on all nodes.")
    p.add_argument("--checkpoints", type=lambda s: [float(v) for v in s.split(",")], default=[0.02, 0.05, 0.1, 0.2, 0.5, 1],
                   help="Checkpoint intervals in hours, comma-separated.")
    p.add_argument("--restart-hours", type=float, default=0.1, help="Time to replace a node and restore from the checkpoint.")
    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)
    
    survive = np.mean(np.all(rng.random((100_000, args.nodes)) > args.p, axis=1))
    print(f"survive one hour with {args.nodes} spot nodes: {survive:.4f}   (0.95^{args.nodes} = {(1 - args.p) ** args.nodes:.4f})")
    
    def completion(interval, spot_nodes):
        """Expected hours to finish job_hours of work, checkpointing every `interval` hours. Interruptions arrive
        at rate lam across the gang; a segment of length tau restarts until it runs clean, which takes
        (e^(lam*tau) - 1) * (1/lam + restart) hours on average. Lost work per interruption is what ran before it."""
        lam = -np.log(1 - args.p) * spot_nodes                  # interruptions per hour across the gang
        segments = args.job_hours / interval
        per_segment = (np.exp(lam * interval) - 1) * (1 / lam + args.restart_hours)
        lost = 1 / lam - interval / (np.exp(lam * interval) - 1)  # mean work lost per interruption
        return float(segments * per_segment), float(lost)
    
    rows = []
    for interval in args.checkpoints:
        for label, spot in (("all nodes on spot", args.nodes), ("workers on spot, coordinator on demand", args.nodes - 1)):
            t, l = completion(interval, spot)
            rows.append({"checkpoint_h": interval, "setup": label, "completion_h": t, "lost_h": l})
        print(f"checkpoint every {interval:>4g} h: completion {rows[-2]['completion_h']:6.1f} h (all spot)   "
              f"{rows[-1]['completion_h']:6.1f} h (coordinator on demand); {args.job_hours:g} h of work")
    interrupt_rate = -np.log(1 - args.p) * args.nodes
    print(f"interruptions per hour across the gang: {interrupt_rate:.2f}; lost work per interruption: "
          + ", ".join(f"{r['lost_h'] * 60:.1f} min at {r['checkpoint_h'] * 60:g} min" for r in rows[::2]))
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"checkpoints": rows, "summary": [{"survive": float(survive), "formula": (1 - args.p) ** args.nodes,
                       "interrupts_per_h": interrupt_rate}]}}, 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 simulate-the-spot-gang.py
    uv run simulate-the-spot-gang.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--nodes100 Nodes the job needs at once.
--p0.05 Probability that one node is interrupted in an hour.
--job-hours10 Work in the job, in hours on all nodes.
--checkpoints0.02, 0.05, 0.1, 0.2, 0.5, 1 Checkpoint intervals in hours, comma-separated.
--restart-hours0.1 Time to replace a node and restore from the checkpoint.
--seed0 Random seed.

Our run

Expected completion time of 10 hours of workApple M2 · 8 cores · 8 GB
all nodes on spotworkers on spot, coordinator on demand
Data table
Expected completion time of 10 hours of work
Checkpoint interval (hours)seriesHours to finish
0.02all nodes on spot15.9
0.02workers on spot, coordinator on demand15.9
0.05all nodes on spot17.2
0.05workers on spot, coordinator on demand17.2
0.1all nodes on spot19.8
0.1workers on spot, coordinator on demand19.6
0.2all nodes on spot26.4
0.2workers on spot, coordinator on demand26.1
0.5all nodes on spot70.8
0.5workers on spot, coordinator on demand69.3
1all nodes on spot495
1workers on spot, coordinator on demand473
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · run 2026-10-20
Output · uv run simulate-the-spot-gang.py
survive one hour with 100 spot nodes: 0.0057   (0.95^100 = 0.0059)
checkpoint every 0.02 h: completion   15.9 h (all spot)     15.9 h (coordinator on demand); 10 h of work
checkpoint every 0.05 h: completion   17.2 h (all spot)     17.2 h (coordinator on demand); 10 h of work
checkpoint every  0.1 h: completion   19.8 h (all spot)     19.6 h (coordinator on demand); 10 h of work
checkpoint every  0.2 h: completion   26.4 h (all spot)     26.1 h (coordinator on demand); 10 h of work
checkpoint every  0.5 h: completion   70.8 h (all spot)     69.3 h (coordinator on demand); 10 h of work
checkpoint every    1 h: completion  495.2 h (all spot)    473.5 h (coordinator on demand); 10 h of work
interruptions per hour across the gang: 5.13; lost work per interruption: 0.6 min at 1.2 min, 1.4 min at 3 min, 2.7 min at 6 min, 5.0 min at 12 min, 9.2 min at 30 min, 11.3 min at 60 min

What we found

The gang survived an hour 0.006 of the time, against 0.95¹⁰⁰ = 0.006, as expected. Across 100 nodes that is 5.13 interruptions per hour, one every 12 minutes or so, so the checkpoint interval decides everything: with a checkpoint every 1.2 minutes the 10-hour job took 15.9 hours, with one every 30 minutes 70.8 hours, and with hourly checkpoints 495 hours. Taking the coordinator off spot barely helped (473 hours at hourly checkpoints): it is one node of 100, and the gang fails just as often through its workers.


Read it

A gang turns a per-node risk into a per-job certainty: (1 − p)^n shrinks exponentially in n. The cost of a segment grows like e^(λτ), so checkpoints must come much more often than interruptions do; past that point the job spends its time redoing work and the spot discount is gone.

Spot pays for work that can lose a node without losing the job: independent tasks, materialized stage outputs, frequent cheap checkpoints. A gang of hundreds of nodes on spot is a bet against arithmetic.