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.
Maintainers
Readme
wavelab
Wave-based experiment scheduler for AI agents.
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. TheExecutorinterface is the seam for remote backends; a Slurm executor ships built-in (see Executors below).
Install
npm install -g wavelabQuickstart
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 10Everything 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
cwdis bind-mounted at/workspaceand used as the working directory, so the samecommandworks on the local and docker backends. - Container output streams into the same
logs/exp-N.loghandle; 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. useravoids 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
--timeguarantees 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 locallogs/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 inheritedCUDA_VISIBLE_DEVICESis stripped). concurrencymaps 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 demoUse 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.jsstudy_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;
UTCand explicit offsets (+08:00) are also supported. cfpholds 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 viavenue_save;/deadlinesshows 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 dirtyThe 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_reproducereturns 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 existsGPUs 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.
maxUtilizationPercentadds 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_DEVICESon the local backend and translated to--gpus device=Non the docker backend (host indices are not leaked into the container, where the cards appear as0..N-1). concurrencyis 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 runTwo honest limitations:
- Two wavelab processes are kept apart by exclusive lock files under
~/.wavelab/gpu-locks(crossProcessLock: falseto disable,lockDirto 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 thatacquirewould 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
confirmDelaySecand 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. SetconfirmDelaySec: 0on a machine only you use. - For real multi-tenant fairness, use a cluster scheduler. That is what the
slurm backend is for, and
gpuis 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
perExperimentEstimateSecif you provide one). - Waves that do not fit are trimmed, and the reason appears in the wave summary so the agent can react.
reserveSecis 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 offSettings 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 312 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: truecomputes 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 abovehashMaxBytes(2 GiB) are skipped.- On the slurm backend the weights stay on the cluster: sizes come back from
a remote
find, hashes fromsha256sum, 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 BDatabase 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 jsonCSV 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 checkpointsSafety 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-failedto 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.
--compressgzips and repoints the record at the archive, so log tails and failure digests keep working transparently.- Artifacts need their own
--artifactsflag, even with--yes. Weights are an experiment's most valuable output, so deleting them is deliberately harder than deleting a log; only the--keep-bestexperiments 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)
scancelon 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).
