v0.10.0
[0.10.0] - 2026-07-22
Added
- A pre-launch cell-count gate guards against an accidentally huge grid.
run(..., confirm_above=N)refuses to launch a sweep with more thanNcells,
raising aValueErrorthat reports the count and how to proceed (preview with
dry_run=True, or raise the ceiling). TheMUSHIN_MAX_CELLSenvironment
variable supplies a default ceiling when a call doesn't set one — so a shared
cluster or CI profile can cap every sweep — and an explicitconfirm_above=
wins over it. A malformedMUSHIN_MAX_CELLSis ignored, anddry_run=True
bypasses the gate so an over-limit sweep can still be previewed. mushin.best(root, metric)returns the completed cell that optimizes a metric
across a sweep —mode="max"(default) ormode="min". Likemushin.showit
reads the per-cell sidecars directly (offline, no Hydra/xarray). The returned
BestResultcarries the winningcombo(swept params), the optimizedvalue,
the fullmetricsdict, the cellstatus, and its jobdir(for locating
checkpoints/artifacts). Failed/running/skipped cells and non-finite metric
values are ignored; an unknown metric raises aValueErrorthat lists the
available scalar metrics.mushin.diff(a, b)compares two sweep directories. It aligns cells by their
swept-parameter combination and, for each shared cell, reports the delta
b - aof every metric that is a finite scalar in both — printed as aΔmetric
table. Cells present in only one sweep are listed (only_in_a/only_in_b),
and the two runs' environment provenance is diffed field-by-field
(git/packages/python/accelerator), excluding volatile fields like the per-cell
timestamp. Likeshow/bestit reads the per-cell sidecars directly (offline,
no Hydra/xarray); the returnedDiffResultexposes.rows,.only_in_a,
.only_in_b,.provenance, and.table.mushin.export.table(root)exports a sweep as CSV — one row per cell with its
swept parameters,status, and metrics. It reads the per-cell sidecars directly
(offline, no Hydra/xarray) and writes full-precision values so pandas parses
numbers numerically. Returns the CSV string, or writes topath=and returns the
Path;metrics=restricts the metric columns. A durable, spreadsheet-friendly
substrate for researchers who prefer pandas to xarray.mushin.show(root)prints (and returns) a status/metrics table for a sweep
directory. It reads each cell's status and metrics sidecars directly — pure
JSON, so it needs neither Hydra nor xarray and works mid-sweep — making it handy
for watching a live sweep or eyeballing a finished one before the full
to_xarrayload. Each row carries the cell's swept parameters, its status
(completed/running/failed/skipped/…), and its metric values;metrics=
restricts the metric columns andsort=orders the rows. The returned
ShowResultexposes.rows(one dict per cell) and.table.run()now validates up front that every required task (andpre_task)
parameter is satisfied — by the base config, an override, or the raw
overrides=[...]list — and raises a clearValueErrornaming the missing
parameter(s) before launching. Previously a genuinely missing parameter surfaced
as an opaque per-job HydraConfigAttributeErrorafter the sweep had already
started, once per cell. A parameter supplied only via an override is still valid
(the common decorator case, where the base config is empty), so this adds no
false positives.run(..., cache_dir=...)adds a content-addressed cache of completed cells that
is shared acrossworking_dirs. A cell whose resolved config AND task source
match a previously-computed cell — keyed on the same fingerprints asresume—
reuses that result instead of recomputing, so a cell computed in one sweep is
free in another (a coarse grid refined into a finer one, a re-run in a fresh
directory). Newly-computed cells are stored in the cache automatically. It
complementsresume(which reuses only within a singleworking_dir); a changed
non-swept config value or an edited task body is a cache miss. Cache writes are
best-effort and never fail the cell.run(..., dry_run=True)(and@mushin.sweep.run(dry_run=True)) previews a
sweep instead of launching it: it prints the cell count and each swept axis with
its values — so a range typo shows up as an unexpectedly wide axis before any
compute is spent — and returns a summary dict (num_cells,axes,fixed,
working_dir) with no jobs run.run(..., max_total_seconds=T)adds a graceful wall-clock budget. Once the
budget is exhausted the remaining grid cells are skipped — recorded'skipped'
in the manifest and per-cell status, NaN in the dataset, and surfaced in
self.skipped(and amushin_skippeddataset attr) — instead of the sweep
running to the end. The clock starts at the first computed cell, so at least one
cell always runs and resume cache hits don't consume the budget; a cell already
running is never interrupted. Because a skipped cell is notcompleted, a later
resume=Truewith more time finishes exactly the skipped cells. The budget is
measured per launcher process, so it's best paired with the default sequential
launcher.run(..., notes="...", tags=[...])annotates a sweep with free-form lineage. The
note and tags are recorded in the sweep manifest, exposed on the workflow as
wf.notes/wf.tags(and preserved when a sweep is reloaded with
load_from_dir), and carried on the dataset as themushin_notes/
mushin_tagsattrs — so "why did I run this?" travels with the results.tags
must be a list of strings andnotesa string. A resume that does not re-pass
notes/tagspreserves the original run's lineage rather than wiping it.run(..., sample=K)runs only a randomK-cell subset of the grid — the rest
are skipped (NaN, and listed inself.skipped) — for fast exploration of a large
grid without paying for every cell. The subset is chosen deterministically from
the Hydra job indices (seeded bysample_seed, default 0), so it is reproducible
and identical across a resume; resuming withoutsamplefills in the remaining
cells.sample >= n_cellsruns everything. Because selection is by job index it
is launcher- and axis-type-agnostic. Note: the full grid is still composed by
Hydra (only the sampled cells run), and a sampled sweep is intentionally
incomplete (is_completeis False).
Fixed
-
A batch of defensive-handling fixes: corrupt-but-valid-JSON cell-status sidecars now degrade (resume treats a null/
"3"attempt or a wrong-shape record as absent) instead of crashing;load_from_dirraises a realFileNotFoundErrorfor a missing config (not a-O-stripped assert); the resume contextvar no longer leaks when a status write fails;RobustnessCurve.plotno longer leaks a figure when plotting raises; a malformed tuning pin reaches its friendly "delete it or retune" guard;mushin.lightning/benchmark/mcp/testingare reachable as lazy submodules afterimport mushin;compare/compare_llmsreject an unknowncorrection=before any evaluation runs; the MCPget_failures/get_provenancetools tolerate wrong-type sidecar JSON; and avalue_checkmessage typo ("None orof") is fixed. -
A typo'd sweep-axis name is now caught before launch instead of silently
producing a wrong dataset. Passingrun(lrate=multirun(...))when the task
parameter islrpreviously ran to completion and added a phantomlrate
dimension of constant values; it now raises aValueErrorthat names the likely
intended parameter. Only near-misses of a real target are rejected — deliberate
overrides beyond the task's own parameters (values consumed bypre_task,
config groups, interpolations) and tasks declaring**kwargsare unaffected. -
Fixed
trainer.validate()underHydraDDP/HydraFSDP: the child ranks were told to runfit(train) while rank 0 validated — a collective mismatch that hangs or crashes. The launcher now maps Lightning's_validate_implto apl_validatingflag and_pl_maindispatchestrainer.validateaccordingly, alongside the existing test/predict paths. -
Fixed a cluster of silent-wrong-results bugs in the override → grid pipeline: a fixed (non-
multirun) string containing,or=is no longer re-split into an accidental sweep and"true"/"1"stay strings; a duplicated sweep-axis value is rejected instead of silently collapsing two cells into one;combo_keyis now injective (delimiter-containing values can't collide with a different combination); a dotted sweep (model.width) on a config field that already exists no longer crashes; and a batching sweeper's multi-batch job list is flattened instead of failing after every job ran. -
Fixed five defects found by an adversarial review of the exploration features:
cache_diris now resolved to an absolute path (expanduser().resolve())
before launching. A relative or~-prefixedcache_dirwas previously
dereferenced inside each per-cell job dir (Hydra chdirs into it), so
cross-cell/cross-directory reuse silently never hit.resume=Truewithsample=is now rejected with a clear error. Combining
them overwrote already-completed cells (skipped by job index) toskipped/NaN,
silently discarding prior results; resume WITHOUTsamplefills the rest.- The cell count (used by
sample=,confirm_above=/MUSHIN_MAX_CELLS, and
dry_run) now counts sweep axes supplied via the rawoverrides=[...]list,
not onlyparam=multirun(...)kwargs — so those no longer undercount the grid. compare_methods(allow_incomplete=True)now computes each comparison over
only the seeds completed for both methods (dropping NaN cells), fulfilling its
documented promise instead of feeding NaN into the tests and returning all-NaN
statistics.mushin.show(sort=...)places NaN metric values last deterministically
(they no longer break the sort's total order) and raises a clear error when
sortnames a column that isn't present.
-
Fixed metric-value coercion into the JSON sidecar: a task returning a nested-dict metric containing numpy/torch values (or a
datetime/Path) no longer crashes the sidecar write after the task succeeded (which left the cell stuck and aborted the sweep); non-finite metrics (NaN/±Inf) now serialize as valid JSON (null) instead of the non-strictNaN/Infinityliterals that broke strict parsers, and read back as NaN so metric columns stay float dtype on resume; a non-scalar battery metric (numpy array/list, not just a torch tensor) now gives the crafted "must return scalar" error. -
Provenance no longer leaks secrets:
mushin_provenance.jsonrecords the config withresolve=False(so a${oc.env:SECRET}interpolation is kept as an unresolved reference instead of baking the resolved secret into the file), and values under secret-named keys (api_key,token,password, ...) or shaped like provider tokens (sk-...,hf_...) are redacted. This also fixes the MCPget_provenance(include_config=True)tool, which re-served the on-disk record. -
Reloading a sweep offline with
load_from_dir(dir, "mushin_metrics.json")no
longer crashes. The defaultmetric_load_fnnow sniffs the file — it reads the
JSON metrics sidecar (written by any task that returns a dict, including
@mushin.sweep/decorator sweeps) withjsonand falls back totorch.load
only for torch pickle/.ptfiles. Previously it was hard-wired totorch.load,
which raisedUnpicklingErroron the JSON sidecar unless you overrode the loader. -
Resume now guards against stale-code reuse: editing a task body and re-running with
resume=True(same config, same swept params) re-runs the affected cells — with a clear warning — instead of silently returning the previous run’s cached metrics. A per-cell hash of the task source is stored alongside the config fingerprint; a completed cell is reused only when both still match. Legacy sweeps (no recorded code hash) resume as before. -
Resuming a sweep after changing the grid shape (adding or removing an axis) no longer silently reuses the wrong cells: previously, with legacy (pre-fingerprint) sidecars, adding an axis projected every new cell onto the old parameters, reused a stale cell across every new axis value, dropped the new dimension entirely, and reported the sweep complete. The resume now keys reuse on the current swept parameters, so a shape change re-runs the full grid, with a one-time warning.
-
Resuming into a working_dir whose numeric job dirs get reused for different cells no longer leaves stale metrics behind: on a cache hit the reused cell now writes its metrics and status into the current job dir, so the dir stays consistent with the config Hydra wrote there. Previously a reused cell (e.g. a=3) kept the leftover metrics of whatever cell last used that dir (e.g. a=1), so the manifest and an offline
load_from_dirmis-keyed the value. -
compare_methodsnow refuses a sweep that has skipped/never-run cells, not
just failed ones. Previously it keyed only onmushin_failures, so asample=
subset or amax_total_seconds-limited sweep — both of which leave NaN cells —
would compute statistics silently over a partial grid. It now also keys on the
mushin_skippedcompleteness signal and raisesIncompleteSweepError. A new
allow_incomplete=Trueargument bypasses the guard (with a warning) to compute
stats over only the completed cells, for exploratory analysis of a partial
sweep. As before, the guard keys on the completeness signals (attrs), never on
raw NaN values, so a metric that is legitimately NaN for other reasons does not
trigger it.
Misc
- Documentation fixes:
load_from_checkpointdocstring corrected (its three params default to None, not "state_dict"/"model"; noload_moduleparam; returns the passednn.Module, not aLightningModule);run()/wrapper docstrings referencehydra_zen.zen(not the nonexistentmushin.zen);capture_env/resume/load_metrics/workflow_overridesdocstrings match actual behavior; the Python support matrix reads 3.10–3.13 (3.14 is not yet a required CI leg);study.md/concepts.mdlist all seven task batteries; and thecorrection=option is documented in the compare/LLM/index guides. - Documented the sweep-analysis API.
mushin.show,mushin.best,mushin.diff,
andmushin.export.tablenow have an API reference page (reference/analysis),
and the exploration-to-paper guide coversmax_total_seconds,notes=, and
tags=. The workflows guide also gains a "Using mushin alongside a
hyperparameter search" section documenting the Optuna/Ax/Nevergrad → mushin
two-phase pattern (searcher finds configs; mushin runs the reproducible final
grid), with no new dependency. - Fixed several tests that did not test what they claimed: the LLM "clear winner flagged significant" test now uses seed-varying systems and asserts the comparison is actually flagged significant (its old deterministic systems made the comparison masked, so it asserted nothing about significance); the Holm "is monotone" test now pins the exact step-down values and the monotonicity property;
test_on_error_raiseasserts the specificRuntimeError("boom")rather than any exception; a conditional-pass tuning test now visiblypytest.skips instead of silently returning; a workflow test no longer scattersmultirun/into the repo root; and a config-group test cleans up itsConfigStoreentry. - New guide, "From exploration to a paper-ready sweep", documenting the convention
for moving from fast throwaway exploration (sample=,cache_dir=,dry_run,
show/best/diff) to a clean, complete, reproducible paper run — including why
the paper grid must be run fresh (code-era mixing, winner's curse) rather than
grown from the exploration directory. - Repo hygiene & example fixes: the crashing MNIST examples (
compare_classifiers,study_mnist) now name torchvision in their install hint;batteries.pyskips (not fails, exit 0) batteries whose optional extra is missing and drops the false "Run one"; two tracked straymushin_seed*.jsonfiles are removed and gitignored; theomegaconf/typing-extensionsdependency floors are raised to versions that actually resolve;mushin-mcpon a core install gives an actionable "install [mcp]" message instead of a bareModuleNotFoundError; andmake spellnow coversexampleslike CI. - The publish workflow pins pypa/gh-action-pypi-publish to its release tag (a bare commit SHA cannot resolve the Docker-based action's container image, which broke the v0.9.0 publish).