Beautiful, local-first experiment tracking. A lightweight alternative to wandb / tensorboard.
Project description
pandm
pandm tracks ML experiments locally. The Python SDK writes metrics and images straight to a .pandm/ directory next to your code — no account, no daemon, no cloud — and pandm ui serves a dashboard to compare runs. Unlike wandb there is nothing to sign up for, and unlike tensorboard the data is plain SQLite + PNG files you can query yourself. The same scripts report to a shared server over HTTP when you set one env var.
Install
pip install pandm
Quick start
import pandm
run = pandm.init(project="mnist", config={"lr": 1e-3, "batch_size": 64}, description="baseline sweep")
run.define_metric("train/acc", unit="percent", goal="max") # fixed 0–100% axis, leading run marked
for step in range(1000):
loss, acc = train_step()
run.log({"train/loss": loss, "train/acc": acc}, step=step)
if step % 100 == 0:
run.log_image("samples", sample_grid, step=step) # PIL / numpy / torch / path
run.summary({"best/acc": 0.99, "best/epoch": 7}) # the chosen checkpoint's self-consistent row
run.finish()
pandm ui # opens http://127.0.0.1:7878
The dashboard overlays selected runs per metric, with smoothing, log scale, step/time axes, an image browser with a step slider, and a config/summary comparison table. It polls while runs are alive, so curves grow during training.
Usage
step is optional (an internal counter is used). Runs end as finished or crashed: uncaught exceptions are detected via sys.excepthook (and the context manager), and hard-killed processes (kill -9, OOM) are presumed crashed once their 15s heartbeat goes quiet for 60s — self-healing if the process was merely suspended.
with pandm.init(project="mnist") as run:
run.log({"loss": 0.5})
Inspect, export or delete runs from the terminal:
pandm ls # list runs
pandm show <run_id> # config, summary, logged metrics
pandm export <run_id> > data.csv # full series as CSV (or --json, -k <key>)
pandm delete <run_id> -y # delete a run (local + cloud); -y skips the prompt
Data lives in ./.pandm by default; override with --dir or PANDM_DIR.
Resuming a run
Give a run a stable id and pass resume=True to continue it after a crash, a
preemption (spot/OOM), or a manual restart — the run flips back to running and
its step counter picks up past the last logged step instead of starting a second,
disconnected run. Its original config is kept.
run = pandm.init(project="mnist", id="exp-42", resume=True) # continue if it exists, else start fresh
# resume="must" errors if exp-42 is missing; a fresh id that already exists errors unless resume is set
pandm show reports MIN/MAX per metric next to the last value (and the read
API carries a stats field — {min, max, last, count} per key — so the
dashboard and pandm-inspect can pick the best run, not just the latest value).
Hugging Face Accelerate
Pass a PandmTracker instance to Accelerator (Accelerate only resolves strings for its built-in trackers) — accelerator.log then reports to pandm, and end_training finishes the run:
from accelerate import Accelerator
from pandm.integrations.accelerate import PandmTracker
accelerator = Accelerator(log_with=PandmTracker(project="mnist", name="baseline"))
accelerator.init_trackers("mnist", config={"lr": 1e-3})
accelerator.log({"loss": 0.42}, step=10)
accelerator.end_training()
For images, unwrap the raw run: accelerator.get_tracker("pandm", unwrap=True).log_image("samples", img, step=step, caption=prompt).
Cloud mode
Training scripts never change — sign in once per machine and pandm.init() dual-writes: local stays the source of truth, a background thread syncs to the server, and anything logged offline is backfilled on reconnect. Delivery is exact-once (re-pushes are deduped server-side). Sync never stalls training: every network step is time-bounded (PANDM_SYNC_TIMEOUT, default 10s) and finish() flushes the tail under a hard budget (PANDM_FINISH_TIMEOUT, default 4s) before leaving the rest to pandm sync.
pandm login # hosted cloud (pandm.jannchie.com); pass a URL for self-hosted
python train.py # local + cloud
pandm sync # backfill runs whose process already exited
pandm login uses device-flow approval (like gh auth login): it prints a URL
to open in any browser and polls until you approve — so it works over ssh, where
it won't try to open a browser on the remote host. Until you're signed in, the
first pandm.init() offers to log in on an interactive terminal, or prints a
one-line hint on a non-interactive one (CI, nohup, ssh) — it never blocks a
run. PANDM_SILENT=1 silences the hint for good, as does logging in or choosing
keep local.
Each user signs in with GitHub and sees only their own runs. Two interchangeable server implementations speak the same protocol — the full walkthrough (OAuth App, custom domain, backups, troubleshooting) is in docs/deploy.md:
Cloudflare Workers (serverless: D1 for metrics, R2 for media — workers/):
cd workers && pnpm install
npx wrangler d1 create pandm # paste the database_id into wrangler.jsonc
npx wrangler secret put GITHUB_CLIENT_ID # OAuth App callback: https://<domain>/api/auth/callback
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put PANDM_SECRET_KEY # e.g. `openssl rand -hex 32`
npx wrangler d1 migrations apply pandm --remote
pnpm run deploy
Note: D1 bills per row written (100k/day free). Logging ~10 metrics/sec around the clock lands in the paid tier — a few dollars a month.
Self-hosted Python server (same binary as pandm ui):
GITHUB_CLIENT_ID=… GITHUB_CLIENT_SECRET=… docker compose up -d # multi-user mode
Without OAuth env vars the server falls back to single-tenant mode — pandm server --api-key my-secret plus PANDM_REMOTE/PANDM_API_KEY on the client (remote-only, no local copy, no accounts).
API
pandm.init(project, name=None, config=None, *, description=None, id=None, resume=False, total_steps=None, directory=None, remote=None, api_key=None) |
start (or resume) a run; description is a one-line subtitle |
run.log(metrics, step=None) |
log scalar metrics |
run.log_image(key, image, step=None, caption=None) |
log an image |
run.summary(values) |
record run-level scalars (the chosen checkpoint's metric row); merges across calls |
run.define_metric(key, *, min=None, max=None, unit=None, goal=None, baseline=None, description=None) |
declare a metric's display: fixed axis, unit="percent", baseline line, goal for the leading run, description subtitle |
run.finish(status="finished") |
end the run (also via atexit) |
run.delete() |
delete the run + media, local and cloud — for throwaway smoke-test runs |
GET /api/docs |
REST API reference on any running server |
Agent skills
LLM/agent harnesses can drive pandm through two Agent Skills: one to
record runs, one to read them back as JSON. Install them with
npx skills:
npx skills add Jannchie/pandm --skill pandm-track --skill pandm-inspect
-g installs at the user level, -a claude-code targets one agent. See
skills/README.md for what each skill does.
Development
uv sync && uv run pytest # python sdk + server
cd web && pnpm install && pnpm dev # dashboard dev server (proxies to :7878)
pnpm build # bundles the dashboard into src/pandm/static
cd workers && pnpm install && pnpm test # cloudflare workers server (contract tests)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pandm-0.15.0.tar.gz.
File metadata
- Download URL: pandm-0.15.0.tar.gz
- Upload date:
- Size: 728.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30ef30aaa1b62a64260069f299d40427cf9658d843179e5ec239b48769a43bde
|
|
| MD5 |
a3bdf6acd2c38504bbf5d22e3a478d93
|
|
| BLAKE2b-256 |
f9f71b733bd7f73067095aad65e8527480d3206419ee1ac4f1443f5c5e719701
|
Provenance
The following attestation bundles were made for pandm-0.15.0.tar.gz:
Publisher:
release.yml on Jannchie/pandm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pandm-0.15.0.tar.gz -
Subject digest:
30ef30aaa1b62a64260069f299d40427cf9658d843179e5ec239b48769a43bde - Sigstore transparency entry: 1870081896
- Sigstore integration time:
-
Permalink:
Jannchie/pandm@ba9c0812f7b646efdd75e9f9487e1c16beab33d0 -
Branch / Tag:
refs/tags/v0.15.0 - Owner: https://github.com/Jannchie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ba9c0812f7b646efdd75e9f9487e1c16beab33d0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pandm-0.15.0-py3-none-any.whl.
File metadata
- Download URL: pandm-0.15.0-py3-none-any.whl
- Upload date:
- Size: 294.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0356ea0a461206644653513eac2448414f0f495b50deff15e6684b8b4fe9b7d1
|
|
| MD5 |
a84dc05f696a7afd4296ba1e0b9e8305
|
|
| BLAKE2b-256 |
4a27c6e051b9cfe055abd8e7903f49e2efddc4c7827813926a626459a66b4400
|
Provenance
The following attestation bundles were made for pandm-0.15.0-py3-none-any.whl:
Publisher:
release.yml on Jannchie/pandm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pandm-0.15.0-py3-none-any.whl -
Subject digest:
0356ea0a461206644653513eac2448414f0f495b50deff15e6684b8b4fe9b7d1 - Sigstore transparency entry: 1870081906
- Sigstore integration time:
-
Permalink:
Jannchie/pandm@ba9c0812f7b646efdd75e9f9487e1c16beab33d0 -
Branch / Tag:
refs/tags/v0.15.0 - Owner: https://github.com/Jannchie
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ba9c0812f7b646efdd75e9f9487e1c16beab33d0 -
Trigger Event:
push
-
Statement type: