Tooling Stack/Chapter 14 of 35

Data & model versioning

4 min readEdit on GitHub

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:

  1. "What dataset did I use for the v1.2 model?" → Nobody is sure. The manifest existed in someone's notebook. The .parquet on the share drive has been overwritten twice.
  2. "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

Loading diagram…

Tool roundup

ToolBest forNotes
HF HubPublic datasets and models, public model cardsRevision pinning is a must (revision="<sha>")
DVCPrivate datasets with rich versioning, dataset diffsSlightly heavy; pays off above ~100GB or with frequent edits
S3 + versioningBig private artifacts, simple needsCheapest; "git for blobs" but no diff
Git LFSSmall-to-medium binary assets in your repoErgonomic but quotas bite; avoid for >1GB
lakeFSBranchable data lakes for teamsHeavy; only when you have a data engineering team
Pachyderm / QuiltNiche; specific compliance or pipeline needsSkip unless you have the specific need
Manifest in gitAnything you can hashThe 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.pt should be referenced by name from any model card or release.
  • Keep a registry. MODELS.md with 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.