Data & model versioning
Code without versioning is unsupportable; data and weights are no different. This chapter is about the practical minimum.
The problem
Two scenarios that play out in every ML repo within six months:
- "What dataset did I use for the v1.2 model?" → Nobody is sure. The
manifest existed in someone's notebook. The
.parqueton the share drive has been overwritten twice. - "Can you re-run the experiment from March?" → The HF dataset has been updated upstream. Your code still works, but the numbers are different. You cannot tell whether the model regressed or the data shifted.
Both are preventable.
A decision tree
Tool roundup
| Tool | Best for | Notes |
|---|---|---|
| HF Hub | Public datasets and models, public model cards | Revision pinning is a must (revision="<sha>") |
| DVC | Private datasets with rich versioning, dataset diffs | Slightly heavy; pays off above ~100GB or with frequent edits |
| S3 + versioning | Big private artifacts, simple needs | Cheapest; "git for blobs" but no diff |
| Git LFS | Small-to-medium binary assets in your repo | Ergonomic but quotas bite; avoid for >1GB |
| lakeFS | Branchable data lakes for teams | Heavy; only when you have a data engineering team |
| Pachyderm / Quilt | Niche; specific compliance or pipeline needs | Skip unless you have the specific need |
| Manifest in git | Anything you can hash | The lowest-tech option that actually works |
The "manifest" pattern
The smallest thing that gives you reproducibility:
{
"version": "train_v3",
"created_at": "2026-04-22",
"row_count": 248391,
"files": [
{ "path": "s3://my-bucket/raw/2026-q1.parquet", "sha256": "8e2c..." },
{ "path": "s3://my-bucket/raw/2026-q2.parquet", "sha256": "f9a1..." }
],
"derivation": {
"script": "scripts/build_train_v3.py",
"git_rev": "a1b2c3d",
"args": ["--min-len", "8", "--dedupe", "minhash"]
}
}
Commit this JSON. Now every experiment has an exact, hashable description of its training data. If a file changes silently, the hash mismatch crashes the job — which is what you want.
Pinning HF Hub artifacts
Anti-pattern:
ds = load_dataset("organization/dataset-name")
model = AutoModel.from_pretrained("organization/model-name")
These will silently change when upstream pushes. Use revision pins:
ds = load_dataset("organization/dataset-name", revision="abc123def")
model = AutoModel.from_pretrained("organization/model-name", revision="abc123def")
The agent will, by default, write the unpinned version. Add a hard rule in
CLAUDE.md: "Always pin HF Hub revisions when loading. No bare repo IDs in
production code."
Model weights are part of your artifacts
Treat checkpoints as first-class versioned artifacts:
- Tag the run that produced them.
runs/2026-04-22-charngram/checkpoint-best.ptshould be referenced by name from any model card or release. - Keep a registry.
MODELS.mdwith one row per shipped model: name, version, training run, eval results, license. - Never overwrite. If you need to re-run, write to a new path. Treat checkpoints as immutable.
DVC in 30 seconds
If you decide DVC is right for you:
dvc init
dvc add data/raw/big-corpus.parquet
git add data/raw/big-corpus.parquet.dvc.gitignore
git commit -m "data: add raw corpus v1"
dvc remote add -d s3 s3://my-bucket/dvc
dvc push
Now data/raw/big-corpus.parquet is stored in S3, but a tiny .dvc pointer
file is in git. git checkout <old sha> && dvc pull retrieves the exact
historical bytes.
DVC also has a pipeline DAG (dvc.yaml). If you don't need it, skip it — the
versioning piece is enough on its own.
What the agent typically gets wrong
- Loads the latest revision when the doc says "use v1.2."
- Writes to a static path (
data/processed.parquet) instead of a versioned one. - Recomputes a hash on every run instead of comparing to the committed manifest.
- Strips the data versioning when refactoring "for cleanliness."
Catch these in /review and codify them in CLAUDE.md.