Getting started¶
Install¶
Only click, loguru and pydantic are required. Everything else is an extra:
| Extra | Adds |
|---|---|
wandb |
mirror metrics to Weights & Biases |
trackio |
mirror metrics to trackio |
pandas / polars |
history(output_type=...) frames |
gpu |
GpuStats span plugin, via NVML |
all |
all of the above |
Alert channels need nothing: every one of them, Lark included, is built on the standard library.
A missing extra is reported with the exact install command; it never crashes a run.
A complete run¶
import expr_tracker as et
et.init(
project="mnist",
name="baseline",
config={"lr": 3e-4, "batch_size": 64},
alert_rules=["isnan(loss) => critical: loss diverged"],
)
for step in range(1000):
loss = train_one_step()
et.log({"loss": loss, "lr": scheduler.get_last_lr()[0]})
if step % 100 == 0:
et.log({"eval/acc": evaluate()}, step=step)
et.summary()["best_acc"] = best
et.log_artifact("checkpoints/final.pt", name="model", type="model")
et.finish()
This writes:
./tracker/jsonl/ # the root, from dir=
└── mnist/ # the project
├── baseline/ # this run
│ ├── metrics.jsonl # one JSON object per step
│ ├── metrics.meta.json # index sidecar, for fast resume and seeks
│ ├── config.json
│ ├── summary.json
│ └── artifacts.jsonl # lineage: what this run produced and consumed
└── artifacts/ # shared by every run of the project
dir is a root, so a run lands in <dir>/<project>/<name>. That is what
lets a project's runs share and deduplicate artifacts, and what lets a resume
find its files from the project and name alone. The path is logged at init(),
and et.get_run().dir returns it.
et.init(project="mnist", name="baseline", dir="/data/runs")
# -> /data/runs/mnist/baseline
et.init(project="mnist", name="baseline", run_dir="/data/runs/exp-42")
# -> /data/runs/exp-42, exactly
Use run_dir when something else already chose the path — a scheduler's output
directory, say. Artifacts then live in <run_dir>/artifacts and are no longer
shared with the project's other runs, which is the price of the flat layout.
Passing both dir and run_dir is an error.
Reading it back¶
While the run is live, or from another process afterwards:
et.history(50) # last 50 steps
et.history(-1) # everything
et.history(-1, metrics=["loss"]) # one metric
et.history(-1, step_range=(100, 200)) # a slice, end exclusive
et.history(100, output_type="pandas") # a DataFrame
et.history(50, run="tracker/jsonl/mnist/baseline") # no init() needed
Rows are plain dicts with _step and _time plus whatever you logged:
Resuming¶
Re-running init() with the same project and name continues the same file:
et.init(project="mnist", name="baseline")
et.get_run().step # 1000 - the cursor picked up where it left off
len(et.history(-1)) # 1000 - the old rows are still there
This is unconditional: resume is passed on to wandb and trackio, but the local
file always continues. Use a different name to start clean.
A process that dies without finish() still leaves a complete, valid file: the open
row is committed and the summary saved by an exit hook, and a torn trailing line is
repaired on the next run.
Next¶
- Examples — six runnable programs, all offline.
- Logging metrics for commit semantics and out-of-order steps.
- Alerts to get notified instead of watching curves.
- CLI to inspect runs without writing code.