npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wavelab

v0.10.0

Published

Wave-based experiment scheduler for AI agents: run hundreds of parameterized experiments (benchmarks, sweeps, paper reproduction) with durable job state, metric extraction, and agent-friendly summaries.

Readme

wavelab

Wave-based experiment scheduler for AI agents.

License: AGPL v3 npm

Coding agents are good at deciding what to try next, and terrible at babysitting hundreds of running experiments. wavelab splits the work: the agent makes decisions between waves; wavelab owns everything inside a wave — launching, retries, timeouts, metric extraction, and durable state.

Built for benchmark sweeps, hyperparameter searches, and reproducing experimental results from ML papers (hundreds of parameterized runs of the same repo).

Design

  • Wave semantics. A wave is a batch of experiments launched together and settled together. The agent gets exactly one summary per wave — top-k results, new global best, failure digest — never a stream of per-run events. Decision points sit between waves.
  • State lives in SQLite, not in agent context. Every experiment (params, status, metrics, log path) is a row. Agents query aggregated views (status, top, failures); raw logs stay on disk.
  • Durable and idempotent. Experiments are deduplicated by parameter set. Re-running a study never repeats completed work — crash, fix, re-run.
  • Logs are handles, not payloads. Each run writes to a known log file from the start; metric extraction happens on completion; failure digests carry a short tail, not the full log.
  • Zero runtime dependencies. Node >= 22.5 (node:sqlite), local executor built in. The Executor interface is the seam for remote backends; a Slurm executor ships built-in (see Executors below).

Install

npm install -g wavelab

Quickstart

Describe the sweep in a spec file:

{
  "name": "my-sweep",
  "command": "python3 train.py --lr {lr} --width {width}",
  "cwd": ".",
  "params": {
    "lr": [0.001, 0.01, 0.1],
    "width": [128, 256, 512]
  },
  "metrics": [
    { "name": "val_accuracy", "pattern": "val_accuracy=([0-9.]+)", "goal": "max" }
  ],
  "primaryMetric": "val_accuracy",
  "concurrency": 8,
  "timeoutSec": 3600,
  "retries": 1,
  "failureAbortRatio": 0.5
}

Run it:

wavelab run spec.json --wave-size 20   # 9 runs here; wave-size matters at scale
wavelab status my-sweep
wavelab report my-sweep --top 10

Everything lands in ./wavelab-runs/<study>/: wavelab.db (state), logs/exp-N.log (one per run).

Executors

The same spec runs locally, in a container, or on a Slurm cluster; only the executor block changes.

Local (default)

Omit executor entirely. Commands run as local child processes with a process-group timeout kill.

Docker

The usual first failure when reproducing a paper is "the author's dependencies do not install on my machine". The docker executor makes the environment part of the spec instead of part of the machine:

{
  "executor": {
    "mode": "docker",
    "docker": {
      "image": "pytorch/pytorch:2.4.0-cuda12.1-cudnn9-runtime",
      "setup": "pip install -q -r requirements.txt",
      "gpus": "all",
      "shmSize": "2g",
      "user": "1000:1000",
      "network": "none",
      "mounts": ["/data/imagenet:/data:ro"]
    }
  }
}
  • One container per experiment, always --rm: no run inherits state from a previous one.
  • The study cwd is bind-mounted at /workspace and used as the working directory, so the same command works on the local and docker backends.
  • Container output streams into the same logs/exp-N.log handle; docker CLI diagnostics (missing image, refused mount) are appended there too, so they show up in failure digests.
  • Timeouts are enforced by wavelab: the CLI is killed and the container is force-removed by name (wavelab-<id>), so a hung image cannot leak a running container.
  • user avoids root-owned output files; network: "none" is worth setting once dependencies are installed, both for hermeticity and to prove the experiment does not phone home.

Slurm

For university / lab GPU clusters. The controller stays on your machine; each experiment is submitted as one Slurm job:

{
  "executor": {
    "mode": "slurm",
    "slurm": {
      "sshHost": "user@login-node",
      "remoteWorkspace": "/nfs/home/user/my-project",
      "partition": "gpu-h200",
      "time": "24:00:00",
      "gpusPerJob": 1,
      "setup": "module load cuda/12.4 && conda activate myenv",
      "sbatchExtra": ["--mem=64G"]
    }
  }
}

Design invariants:

  • Submit-and-exit. The sbatch script is piped over one transient ssh call (cat > script && sbatch --parsable); no process is ever left running on the login node.
  • sacct is the sole liveness authority. A job sacct reports as pending or running is never reaped early; Slurm's --time guarantees termination. Two bounds (consecutive-unknown grace + a --time-derived wall-clock backstop) keep the poll loop finite even if the cluster goes unreachable.
  • Truthful outcomes. FAILED / TIMEOUT / CANCELLED / OOM land in the store as failures with the sacct exit code, never as silent successes.
  • Logs come home. Slurm writes to the shared NFS workspace (<remoteWorkspace>/.wavelab/logs/); on completion the log is copied to the local logs/exp-N.log, so metric extraction, failure digests, and reports work identically across backends.
  • Commands execute with --chdir=<remoteWorkspace>; GPU binding is owned by Slurm via --gres (an inherited CUDA_VISIBLE_DEVICES is stripped).
  • concurrency maps to the number of jobs wavelab keeps in the Slurm queue at once — set it with your cluster's fairness policy in mind.

Auto mode and explicit fallback

mode: "auto" probes the configured remote backend at startup (docker daemon + image, or ssh reachability + sbatch/sacct) and announces the chosen backend. Probe failure is fail-fast by default — wavelab never silently degrades, because a spec written for a container or a cluster usually cannot produce valid results on the bare host (and running training on a login node can get your account banned). Degradation must be whitelisted explicitly:

"executor": { "mode": "auto", "fallback": ["docker", "local"], "docker": { ... } }

With the whitelist, a failed probe drops to the local executor and says so loudly. Typical workflow: debug locally with mode: "local", then flip to "docker" or "slurm" for the full grid — same spec file.

Try the built-in demo (60 mock training runs across 3 seeds, ~1 min):

git clone https://github.com/LKRCharon/wavelab && cd wavelab
npm install --ignore-scripts && npm run demo

Use as a pi extension

wavelab ships an extension for the pi coding agent that turns studies into agent tools:

pi -e node_modules/wavelab/dist/pi-extension.js
  • study_run — start a study; returns immediately. Each settled wave injects one summary message into the agent loop (steer + triggerTurn), so an idle agent wakes up and decides the next wave.
  • study_query — status counts / top-k / failure digests, without reading raw logs into context.
  • venue_save / venue_status / venue_check — the agent researches a CFP page itself, then persists deadlines and requirements here.
  • /wavelab — list studies. /deadlines — venue countdown board.

The intended loop: the agent designs a coarse grid, study_runs it, works on something else, receives the wave summary, narrows the grid around the best region, repeats.

Venue deadlines and CFP tracking

Reproduction and benchmark runs are usually deadline-driven, so wavelab keeps your target venues next to your experiments:

wavelab venue add neurips.json   # {id, name, type, deadlines[], cfp, checklist[]}
wavelab venue list               # all venues, sorted by next deadline
# neurips-2026  NeurIPS 2026  full paper: 42d 07h  (2026-05-15T23:59:59 AoE)  [checklist: 3 open]
wavelab venue show neurips-2026  # all deadlines + CFP notes + checklist
wavelab venue check neurips-2026 "anonymize repo"
  • Deadlines default to AoE (UTC-12), the CS-venue standard; UTC and explicit offsets (+08:00) are also supported.
  • cfp holds the requirements that bite at submission time: page limit, anonymization rules, artifact/reproducibility policy.
  • The checklist tracks concrete submission chores; open items surface in every listing.
  • Venues live in ~/.wavelab/venues.json — plain JSON, no network access. In pi, the agent reads the CFP page itself and saves it via venue_save; /deadlines shows the countdown board anytime.

Research integrity

The reproduction check reads the spread across seeds as the model's run-to-run variance. Two things have to hold for that reading to be honest, and wavelab now checks both instead of assuming them.

Does the seed actually control the randomness?

wavelab determinism my-study            # repeats the best configuration 3 times
wavelab determinism my-study --repeats 5 --params '{"lr":0.01,"seed":0}'
Determinism: NONDETERMINISTIC (3/3 repetitions of {"seed":0})
Runs are not reproducible bit-for-bit at a fixed seed (acc varied by 0.0146
(1.59% of its mean) across identical runs). The spread across seeds therefore
includes implementation noise, and prediction intervals built from it are wider
than the model's own variance.

One configuration is run several times with everything held identical, including the seed, and the metrics are compared for exact equality. Not "close enough": approximate agreement is what the cross-seed interval already measures, while this check exists to find out whether the seed pins the result at all. An unseeded dataloader worker, a nondeterministic cuDNN kernel, or a global RNG consumed elsewhere all show up here.

Nondeterminism does not invalidate a study, but it changes what its intervals mean, so the verdict is stored and quoted in every later reproduction report. Exit code 5 marks a nondeterministic verdict for scripting.

Repetitions carry a reserved __rep parameter. That is what makes them distinct rows despite parameter deduplication, and reserved parameters are excluded from rankings and from seed grouping, so probes never contaminate results.

Which code produced the numbers?

"provenance": {}

Defaults to git rev-parse HEAD and git status --porcelain; pass commands to capture more (python3 -V, pip freeze, nvidia-smi --query-gpu=name --format=csv).

wavelab provenance my-study     # exit 4 when unrecorded or dirty

The most common reason a published number cannot be reproduced is not a missing seed: it is that the numbers came from code that was never committed. A dirty working tree is therefore flagged, and the flag ignores wavelab's own output directories — a warning that fires on every run is a warning nobody reads.

Scope, stated plainly: these commands run on the machine that launched the study. On the docker and slurm backends the experiments run elsewhere, so the snapshot describes the controller. wavelab records the backend identity (image tag, partition) next to it so the gap is visible rather than implied.

Seed axis declaration

"seedAxis": "seed"

Declaring it lets wavelab check the axis exists, lets reproduction checking find it without being told twice, and turns two silent failures into loud ones:

  • A repeated value is now an error. "seed": [0, 0, 0] used to expand to three combinations and store one, because identical parameter sets are deduplicated: the study claimed three repetitions and ran a single one.
  • A missing or thin seed axis is warned about at launch, along with a missing provenance block, because both change how much the resulting numbers can be trusted.

Reproduction checking

Reproducing a published number is not "within 5% of the claim" — it is "inside the run-to-run variance of the original experiment". wavelab uses the same judgement as CORE-Bench: a 95% t-distribution prediction interval over repeated runs.

Add a seed axis to the spec (3+ values), then describe the paper's claims in a rubric file:

{
  "paper": "CTGCN (arXiv:2003.09902)",
  "seedAxis": "seed",
  "claims": [
    {
      "claim": "AUC of Had with CTGCN-C on UCI",
      "metric": "auc",
      "value": 0.9375,
      "tolerance": "auto",
      "where": { "dataset": "uci", "method": "CTGCN-C" }
    }
  ]
}
wavelab reproduce my-study rubric.json    # markdown report; exit 2 if any claim fails
| claim | metric | claimed | measured (mean) | runs | accepted interval | verdict |
|---|---|---|---|---|---|---|
| AUC ... on UCI | auc | 0.9375 | 0.9356 ± 0.0031 | 3 | [0.9200, 0.9512] (prediction) | reproduced |
  • Runs are grouped by parameters excluding the seed axis, then aggregated across seeds (mean, sample sd).
  • tolerance: "auto" (default) uses the prediction interval. {"abs": x} and {"rel": p} give explicit bands when you have a stated tolerance.
  • A single-seed group cannot establish variance: it falls back to a 5% relative band and is reported as low confidence rather than silently claiming reproduction.
  • Nonzero exit code makes it usable as a CI gate on your own artifacts.
  • In pi, study_reproduce returns the same report for the agent to act on.
  • The report ends with integrity caveats drawn from the determinism check and the provenance snapshot. A report that omits them overstates its own strength, so an unchecked determinism state says so explicitly.

GPU scheduling on a shared server

The usual lab situation: the box has 8 GPUs, you may use some of them, and other people's jobs come and go. wavelab leases cards it finds free, waits for the busy ones, and never touches anyone else's process.

"gpu": {
  "devices": [4, 5, 6, 7],
  "perExperiment": 1,
  "freeMemoryMiB": 1024,
  "pollIntervalSec": 30,
  "waitTimeoutSec": 3600,
  "confirmDelaySec": 3
}

Two ways to say what "usable" means

These are different needs and wavelab keeps them apart; setting both is a validation error rather than a silent precedence rule.

| Mode | Field | Meaning | |---|---|---| | Exclusive (default) | freeMemoryMiB: 1024 | Used memory above this means the card is someone else's. Right when a run needs the whole card. | | Capacity | requireFreeMiB: 16384 | Usable while at least this much is free, whoever else is on it. Right for coexisting with a long-lived inference server that only needs part of the card. |

Capacity mode is not blanket sharing: a card holding 70 GiB of 80 GiB is still refused for a 16 GiB request. And a card wavelab has leased is never offered twice even in capacity mode — one wavelab experiment per card, since partial reservations are not tracked.

Indices are the ones nvidia-smi reports and start at 0, so an 8-card box is 0..7.

wavelab gpu spec.json          # devices from the spec
wavelab gpu 4,5,6 --need 16384 # a bare device list, before any spec exists
GPUs wavelab may use: 4, 5, 6 (1 per experiment, capacity mode: usable while 16.0 GiB is free)
  gpu 4: busy — 70.4 GiB of 80.0 GiB used, 9.6 GiB free, 0% util
      70.4 GiB  pid 3001 (colleague)  [inference-server]  python -m vllm.entrypoints.openai.api_server --model Coder-7B
  gpu 5: usable — 57.6 GiB of 80.0 GiB used, 22.4 GiB free, 0% util
      57.6 GiB  pid 3002 (colleague)  [inference-server]  python -m vllm.entrypoints.openai.api_server --model Qwen-32B-AWQ
  gpu 6: usable — 60.0 GiB of 80.0 GiB used, 20.0 GiB free, 0% util
      60.0 GiB  pid 3003 (colleague)  [inference-server]  python -m vllm.entrypoints.openai.api_server --model Qwen-32B-AWQ

Note: 3 inference server process(es) hold 188.0 GiB at 0% utilization. Stopping
them, or capping their memory fraction, returns that capacity to experiments.

The occupant lines answer the question a free/busy summary cannot: who is holding the card, under which user, running what. An inference server that pre-allocated its KV cache and has been idle for hours is the single most common reason experiments cannot start, and it is invisible in --query-gpu output alone, so it gets called out explicitly.

How it decides:

  • Busy means occupied memory, not utilization. A job loading data sits at 0% util while holding 20 GB, and starting next to it would OOM both of you. maxUtilizationPercent adds utilization as an extra gate if you want it.
  • wavelab's own runs hold leases, so its memory is never mistaken for somebody else's occupancy.
  • Leased cards are pinned with CUDA_VISIBLE_DEVICES on the local backend and translated to --gpus device=N on the docker backend (host indices are not leaked into the container, where the cards appear as 0..N-1).
  • concurrency is effectively capped by available cards: extra workers wait rather than pile onto one GPU.
  • Waiting time is not counted as experiment duration, so it never corrupts the budget estimate.

When nothing frees up within waitTimeoutSec, the wave settles and the unstarted experiments stay pending — not failed, not skipped. Run the study again later and they resume, deduplicated as usual:

GPU: stopped waiting for a free GPU after 3600s; 12 experiment(s) left pending
and will resume on the next run

Two honest limitations:

  • Two wavelab processes are kept apart by exclusive lock files under ~/.wavelab/gpu-locks (crossProcessLock: false to disable, lockDir to move them). A lock left behind by a killed run is reclaimed once its process is gone, and a stale lock is never reported as blocking a card that acquire would take anyway. Only wavelab honors these locks: a colleague's launcher or a server started by hand knows nothing about them.
  • Against everything else, detection is still a poll. After leasing, wavelab waits confirmDelaySec and re-reads; if the card is no longer usable, someone else won and the lease is dropped. This narrows the race, it does not remove it. Set confirmDelaySec: 0 on a machine only you use.
  • For real multi-tenant fairness, use a cluster scheduler. That is what the slurm backend is for, and gpu is rejected there because Slurm allocates cards itself via --gres.

Budgets

A competition slot or a shared GPU gives you a hard limit: "one 4090, two hours, produce a submission". Under that constraint the scheduler's job is not just to run the grid but to decide how much of it is affordable, and to stop in time to produce the final artifact.

"budget": { "wallClockSec": 7200, "reserveSec": 900 }
Wave 1 settled: 12/12 done, 0 failed, 0 timeout, in 18s
budget: 18s used of 20s, 0s usable, 5s reserved, ~6s/experiment, ~0 more affordable
Budget stop: budget exhausted: 2s left, all of it reserved for the final run
  • Cost per experiment is the observed median of finished runs, not a guess. Before anything has finished, the first wave runs unrestricted as a probe (or uses perExperimentEstimateSec if you provide one).
  • Waves that do not fit are trimmed, and the reason appears in the wave summary so the agent can react.
  • reserveSec is never planned into: it is the time you keep for the final full run, the submission, or the report.
  • The budget covers one run of the study (one session), not the study's lifetime. Resuming starts a fresh budget, which is the honest reading of "I have two hours right now".

Structured metrics

Log regexes are fragile against progress bars and multi-line output. Any run can instead report metrics as JSON, and wavelab tells it where:

import json, os
json.dump({"val_accuracy": 0.918}, open(os.environ["WAVELAB_METRICS_FILE"], "w"))

File metrics override regex-extracted ones for the same name. Set metricsFile in the spec (supports {param} placeholders) if the script writes to a path you do not control. Works on all three backends: remote backends fetch the file the same way they fetch logs.

Every run also gets these variables, so experiments group correctly in whatever tracker your script already uses (SwanLab, W&B, TensorBoard):

| Variable | Meaning | |---|---| | WAVELAB_STUDY | Study name | | WAVELAB_WAVE | Wave number | | WAVELAB_EXP_ID | Experiment id (matches logs/exp-N.log) | | WAVELAB_METRICS_FILE | Where to write metrics JSON | | WAVELAB_ARTIFACT_DIR | Where to save checkpoints (set when artifacts is configured) |

Settings and feature toggles

wavelab settings show
wavelab settings set defaults.concurrency 8
wavelab settings set features.venueTools off

Settings live in ~/.wavelab/settings.json and are entirely optional. Precedence, highest first: spec field > WAVELAB_* environment variable > settings file > built-in default.

Feature toggles matter most for the pi extension: every registered tool costs schema tokens on every request, so turn off what you do not use.

| Feature | Default | Effect when off | |---|---|---| | injectEnv | on | No WAVELAB_* variables are set | | metricsFile | on | Metrics come from log regexes only | | budgetEnforcement | on | spec.budget is ignored | | gpuScheduling | on | spec.gpu is ignored | | venueTools | on | pi: no venue_* tools, no /deadlines | | reproduceTools | on | pi: no study_reproduce tool |

Each toggle also has an environment override, e.g. WAVELAB_FEATURE_VENUE_TOOLS=0, which is handy in CI.

Data management

The metrics in wavelab.db are the irreplaceable part of a study. Logs are not: they are large, and once metrics are extracted the log of a mediocre configuration is worth little. Everything here follows from that asymmetry.

Log growth is capped automatically

Unbounded logs are the most likely way wavelab fills a disk: a script that prints a progress line per step can produce hundreds of MB, and 600 of those exhaust a lab quota. Each log is therefore trimmed to head + tail when its experiment finishes:

"limits": { "logMaxBytes": 4194304, "logHeadBytes": 1048576 }

The head keeps the config echo and library versions, the tail keeps the error and the final metrics, and the middle is replaced by a marker stating how many bytes went. Cuts land on line boundaries, so no metric regex ever sees a half-written line. Peak usage during a run is still one full log per concurrent experiment, bounded by timeoutSec.

Checkpoint accounting

Checkpoints are two orders of magnitude larger than logs: a 600-run sweep saving a 500 MB checkpoint each holds 300 GB. wavelab records what each run produced, so the weights become searchable and prunable instead of a directory of 600 similarly named .pt files.

Runs write to the directory wavelab hands them:

torch.save(model.state_dict(), f"{os.environ['WAVELAB_ARTIFACT_DIR']}/model.pt")

Scripts whose output path cannot be changed declare it instead:

"artifacts": { "paths": ["checkpoints/lr-{lr}/best.pt"], "hash": true }
wavelab artifacts my-study --keep-best 3
12 experiment(s) with artifacts, 5.8 GiB total, 3 protected by keep-best
  exp #17: 512 MiB in 1 file(s) [protected]
    .wavelab/artifacts/exp-17/model.pt (512 MiB, sha256 118196f406cf...)
  • wavelab never copies or moves artifacts. They are too large, and moving them would break the scripts that wrote them. Only paths, sizes, and optional hashes are recorded.
  • hash: true computes sha256 so you can prove the weights you are serving are the ones that produced the reported number. It is off by default because it reads every byte; files above hashMaxBytes (2 GiB) are skipped.
  • On the slurm backend the weights stay on the cluster: sizes come back from a remote find, hashes from sha256sum, the files themselves never move.
  • A retry replaces an experiment's artifact rows, so a rerun never leaves stale sizes in the accounting.

Storage report

wavelab du            # every study
wavelab du my-study   # one study
| study | database | logs | files | artifacts | artifact files |
|---|---|---|---|---|---|
| ckpt-demo | 40 KiB | 45 B | 3 | 586 KiB | 3 |
| **total** | 40 KiB | 45 B | | 586 KiB | |

Largest logs:
  mock-benchmark/logs/exp-10.log: 52 B

Database size includes the -wal and -shm sidecars, which can be large mid-run.

Backup

wavelab backup my-study [--out DIR]

Uses SQLite's VACUUM INTO, not a file copy. This is not a stylistic choice: the database runs in WAL mode, so copying wavelab.db without its -wal sidecar can yield a stale or torn snapshot. VACUUM INTO writes a fully checkpointed, compacted database while readers stay active, and wavelab then reopens the result and counts its rows so a broken backup is noticed now rather than when you need it. Snapshots are timestamped and never overwritten.

Worth knowing: wavelab-runs/ is gitignored, so git clean -xfd will delete it. Back up before anything that touches the working tree, or set baseDir outside the repository.

Export

wavelab export my-study --format csv --out results.csv
wavelab export my-study --format json

CSV columns are stable and sorted (param.* then metric.*), so diffing two exports is meaningful; values containing commas are quoted. This is the file that goes into a paper appendix or an artifact submission — readable without wavelab or SQLite.

Prune

wavelab prune my-study                      # dry run: shows what would go
wavelab prune my-study --keep-best 5 --yes  # actually delete
wavelab prune my-study --compress --yes     # gzip instead of delete
wavelab prune my-study --older-than 30 --yes
wavelab prune my-study --artifacts --keep-best 3 --yes   # also drop checkpoints

Safety is the default, not a flag:

  • Dry run unless --yes. A mistyped policy cannot cost you logs.
  • The database is never a target. Only log files are ever touched, so metrics, rankings, and reproduction reports survive any prune.
  • Failures and the best results are protected (--keep-best N, default 10; --drop-failed to release failures). A failed run's log is the one you actually need.
  • Refuses while experiments are marked running, so a prune cannot pull files out from under a live study.
  • --compress gzips and repoints the record at the archive, so log tails and failure digests keep working transparently.
  • Artifacts need their own --artifacts flag, even with --yes. Weights are an experiment's most valuable output, so deleting them is deliberately harder than deleting a log; only the --keep-best experiments keep theirs, and the plan says plainly that they are not recoverable without a re-run.

Study comparability is enforced

Changing a study's command, cwd, or metric definitions and re-running it would silently mix incomparable rows: old experiments keep their old command while new ones use the new one, and both end up in the same ranking. wavelab refuses:

Study "sweep" already has results produced with a different command, cwd, or
metric definition. Extending it would rank experiments that were not produced
the same way.
  recorded: python train.py --lr {lr}
  current:  python train_v2.py --lr {lr}
Use a new study name, revert the change, or pass --allow-spec-change if you
know the results stay comparable.

Growing the parameter grid is the normal way to extend a study and is always allowed; so are concurrency, timeoutSec, budget, and gpu changes.

Use as a library

import { loadSpec, runStudy } from "wavelab";

const spec = loadSpec("spec.json");
const summaries = await runStudy(spec, {
  waveSize: 20,
  events: { onWaveSettled: (s) => console.log(s) },
});

Spec reference

| Field | Required | Default | Description | |---|---|---|---| | name | yes | — | Study id, [a-zA-Z0-9_-]+ | | command | yes | — | Template with {param} placeholders | | cwd | no | spec dir | Working directory (relative resolves against the spec file) | | params | yes | — | Axis name -> values; cartesian product is the run set | | metrics[] | yes | — | { name, pattern, goal }; regex with one capture group, last match wins | | primaryMetric | yes | — | Metric used for ranking | | seedAxis | no | — | Which axis holds repetitions (see Research integrity) | | provenance | no | — | { commands?, maxOutputBytes?, timeoutSec? } environment snapshot | | concurrency | no | 4 | Max parallel runs | | timeoutSec | no | 3600 | Per-run wall clock limit (SIGKILL on the process group) | | retries | no | 1 | Re-attempts for nonzero exits, within the same wave | | failureAbortRatio | no | 0.5 | Abort the study when this fraction of a wave fails | | env | no | — | Extra environment variables per run | | executor | no | local | Execution backend config (see Executors) | | budget | no | — | { wallClockSec, reserveSec?, perExperimentEstimateSec? } (see Budgets) | | gpu | no | — | { devices[], perExperiment?, freeMemoryMiB? \| requireFreeMiB?, ... } (see GPU scheduling) | | limits | no | 4 MiB | { logMaxBytes?, logHeadBytes? } log trimming (see Data management) | | metricsFile | no | auto | Path of a JSON metrics file the run writes (see Structured metrics) | | artifacts | no | — | { paths?, hash?, hashMaxBytes? } checkpoint accounting (see Data management) |

Roadmap

  • SkyPilot executor (cloud GPU provisioning)
  • Adaptive wave planning helpers (successive halving, plateau detection)
  • scancel on abort for in-flight Slurm jobs

Contributing

See CONTRIBUTING.md for development setup and the contribution license terms.

License

wavelab is dual-licensed:

  • AGPL-3.0 for open source use. You can use, modify, and redistribute wavelab freely under the terms of the GNU AGPL v3. Running it locally or on your cluster for research — including commercial research — requires nothing from you.
  • Commercial license for embedding wavelab into proprietary products or network services without AGPL obligations. Contact [email protected].

Contributions are welcome; to keep dual licensing possible, external contributions require a simple CLA (asked once, on your first PR).