Resilient & resumable sweeps¶
A long method × seed sweep should not throw away hours of good runs because
one cell hit a transient failure — and it must never let you compute statistics
on a grid with holes. mushin gives you fail-soft runs (on_error="nan"),
a manifest, and resume. This notebook walks the full loop end to end.
from __future__ import annotations
import json
import tempfile
from pathlib import Path
import torch
from mushin import multirun
from mushin.benchmark import IncompleteSweepError, compare_methods
from mushin.workflows import MultiRunMetricsWorkflow
One stable working directory for the whole notebook, plus a sentinel file that
stands in for "we fixed the root cause". The mlp / seed=3 cell fails until the
sentinel exists.
WORK = Path(tempfile.mkdtemp())
SENTINEL = WORK / "cause_fixed.flag"
class FlakySweep(MultiRunMetricsWorkflow):
@staticmethod
def task(method: str, seed: int) -> dict:
if method == "mlp" and seed == 3 and not SENTINEL.exists():
raise RuntimeError("simulated transient failure on mlp/seed=3")
torch.manual_seed(seed)
base = 0.90 if method == "cnn" else 0.82
return dict(accuracy=base + 0.01 * torch.rand(1).item())
1. Fail-soft: on_error="nan"¶
The bad cell becomes NaN, a warning is emitted, and every other cell finishes.
wf = FlakySweep()
wf.run(
method=multirun(["cnn", "mlp"]),
seed=multirun([0, 1, 2, 3, 4]),
working_dir=str(WORK),
on_error="nan",
)
print("is_complete:", wf.is_complete)
wf.failures
[2026-07-14 21:29:40,884][HYDRA] Launching 10 jobs locally
[2026-07-14 21:29:40,884][HYDRA] #0 : +method=cnn +seed=0
[2026-07-14 21:29:40,953][HYDRA] #1 : +method=cnn +seed=1
[2026-07-14 21:29:41,018][HYDRA] #2 : +method=cnn +seed=2
[2026-07-14 21:29:41,079][HYDRA] #3 : +method=cnn +seed=3
[2026-07-14 21:29:41,140][HYDRA] #4 : +method=cnn +seed=4
[2026-07-14 21:29:41,202][HYDRA] #5 : +method=mlp +seed=0
[2026-07-14 21:29:41,265][HYDRA] #6 : +method=mlp +seed=1
[2026-07-14 21:29:41,326][HYDRA] #7 : +method=mlp +seed=2
[2026-07-14 21:29:41,473][HYDRA] #8 : +method=mlp +seed=3
[2026-07-14 21:29:41,534][HYDRA] #9 : +method=mlp +seed=4
is_complete: False
/Users/josuemartinez/code/mushin/src/mushin/workflows.py:595: UserWarning: 1 run(s) failed: ['method=mlp,seed=3']; grid cells set to NaN. self.jobs_post_process()
[{'combo': 'method=mlp,seed=3',
'exception': "RuntimeError('simulated transient failure on mlp/seed=3')",
'working_dir': '/var/folders/y9/wzls976d3f9074y29dbrygw80000gn/T/tmp_809_m3y/8'}]
The grid has exactly one hole:
ds_before = wf.to_xarray()
ds_before["accuracy"]
<xarray.DataArray 'accuracy' (method: 2, seed: 5)> Size: 80B
array([[0.90496257, 0.90757632, 0.90614695, 0.90004264, 0.90559639],
[0.82496257, 0.82757632, 0.82614695, nan, 0.82559639]])
Coordinates:
* method (method) <U3 24B 'cnn' 'mlp'
* seed (seed) int64 40B 0 1 2 3 4Every requested cell's status is recorded in the sweep manifest — this is what
makes a resume possible. The manifest maps each combo to its status
(completed / failed), so the holes are explicit.
manifest = json.loads((WORK / "mushin_sweep_manifest.json").read_text())
[(combo, cell["status"]) for combo, cell in manifest["cells"].items()]
[('method=cnn,seed=0', 'completed'),
('method=cnn,seed=1', 'completed'),
('method=cnn,seed=2', 'completed'),
('method=cnn,seed=3', 'completed'),
('method=cnn,seed=4', 'completed'),
('method=mlp,seed=0', 'completed'),
('method=mlp,seed=1', 'completed'),
('method=mlp,seed=2', 'completed'),
('method=mlp,seed=3', 'failed'),
('method=mlp,seed=4', 'completed')]
2. Statistics refuse an incomplete sweep¶
The missing cell is missing data, not a measurement, so compare_methods
raises rather than quietly averaging over the hole.
try:
compare_methods(ds_before)
except IncompleteSweepError as e:
print("IncompleteSweepError:", e)
IncompleteSweepError: 1 run(s) failed (method=mlp,seed=3); fix the cause and re-run with resume=True to complete the sweep before comparing.
3. Resume: fill only the failed cell¶
"Fix the cause" (touch the sentinel), then re-run against the same
working_dir with resume=True. Completed cells are reused from disk; only the
failed cell re-executes.
SENTINEL.write_text("fixed")
wf2 = FlakySweep()
wf2.run(
method=multirun(["cnn", "mlp"]),
seed=multirun([0, 1, 2, 3, 4]),
working_dir=str(WORK),
resume=True,
)
print("is_complete:", wf2.is_complete)
ds_after = wf2.to_xarray()
ds_after["accuracy"]
[2026-07-14 21:29:41,942][HYDRA] Launching 10 jobs locally
[2026-07-14 21:29:41,942][HYDRA] #0 : +method=cnn +seed=0
[2026-07-14 21:29:41,986][HYDRA] #1 : +method=cnn +seed=1
[2026-07-14 21:29:42,029][HYDRA] #2 : +method=cnn +seed=2
[2026-07-14 21:29:42,071][HYDRA] #3 : +method=cnn +seed=3
[2026-07-14 21:29:42,111][HYDRA] #4 : +method=cnn +seed=4
[2026-07-14 21:29:42,152][HYDRA] #5 : +method=mlp +seed=0
[2026-07-14 21:29:42,194][HYDRA] #6 : +method=mlp +seed=1
[2026-07-14 21:29:42,235][HYDRA] #7 : +method=mlp +seed=2
[2026-07-14 21:29:42,278][HYDRA] #8 : +method=mlp +seed=3
[2026-07-14 21:29:42,339][HYDRA] #9 : +method=mlp +seed=4
is_complete: True
<xarray.DataArray 'accuracy' (method: 2, seed: 5)> Size: 80B
array([[0.90496257, 0.90757632, 0.90614695, 0.90004264, 0.90559639],
[0.82496257, 0.82757632, 0.82614695, 0.82004264, 0.82559639]])
Coordinates:
* method (method) <U3 24B 'cnn' 'mlp'
* seed (seed) int64 40B 0 1 2 3 44. The grid, before and after¶
The single NaN cell (left) is filled in on resume (right).
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(1, 2, figsize=(7, 2.8), sharey=True)
for ax, ds, title in [
(axes[0], ds_before, "after fail-soft run"),
(axes[1], ds_after, "after resume"),
]:
arr = ds["accuracy"].transpose("method", "seed").values
im = ax.imshow(arr, vmin=0.8, vmax=0.95, cmap="viridis", aspect="auto")
ax.set_xticks(range(ds.sizes["seed"]))
ax.set_xticklabels(ds["seed"].values)
ax.set_yticks(range(ds.sizes["method"]))
ax.set_yticklabels(ds["method"].values)
ax.set_xlabel("seed")
ax.set_title(title)
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
txt = "NaN" if np.isnan(arr[i, j]) else f"{arr[i, j]:.2f}"
ax.text(j, i, txt, ha="center", va="center", color="w", fontsize=8)
fig.tight_layout()
plt.show()
5. Now the comparison runs¶
With a complete grid, compare_methods returns a tidy pairwise-comparison
table (one row per metric × method-pair).
compare_methods(ds_after)
/var/folders/y9/wzls976d3f9074y29dbrygw80000gn/T/ipykernel_28461/3332771622.py:1: UserWarning: test='wilcoxon' cannot reach alpha=0.05 with 5 seeds (best-case p=0.0625); use more seeds or a parametric test such as test='welch'. compare_methods(ds_after)
| metric | method_a | method_b | mean_diff | effect_size | p_value | p_corrected | significant | |
|---|---|---|---|---|---|---|---|---|
| 0 | accuracy | cnn | mlp | 0.08 | 27.939648 | 0.0625 | 0.0625 | False |
See also
- Resilient & resumable sweeps guide — provenance,
capture_env, and the same options threaded throughStudy.
Beyond in-process failures¶
The loop above recovers from an in-process error. resume=True is also durable
across a hard kill (OOM, SLURM preemption) — completed cells are recorded from
inside each job, so a killed sweep resumes without recomputing them. A long cell
can resume its own training by declaring a mushin_resume parameter; see the
resilience guide.