Data and Model Versioning: Content Addressing, Lineage and Garbage Collection
Hands-on lab · IDE in your browser

Data and Model Versioning: Content Addressing, Lineage and Garbage Collection

Build a content-addressed store for datasets and models, the idea behind tools like DVC.

Time
45 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

The job

A team keeps overwriting model.pkl and cannot say which data trained which model, or roll back safely. You build the version store they should have had: content-addressed blobs, dataset snapshots, lineage from every model back to its data and code, reproducibility checks, and a garbage collector that frees space without breaking a rollback.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Content-address blobs

    Versioning data and models well starts with one idea: address everything by the hash of its content.

  2. 2

    Snapshot a dataset

    A dataset is many files.

  3. 3

    Record lineage

    A model you cannot trace is a liability: when it misbehaves you need to know exactly what data and code produced it.

  4. 4

    Reproduce and verify

    Two things keep a version store honest: knowing when two runs are really the same, and knowing that nothing in the store has been altered under its id.

  5. 5

    Garbage collect

    A content store fills with every intermediate model and dataset.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Content-address blobs

Content-address blobs

Versioning data and models well starts with one idea: address everything by the hash of its content. Then identical data is stored once, and a version id is just the hash, reproducible from the bytes alone with no central counter to trust.

store.py gives you the store: new_store(), get(store, id), manifest_bytes(tree), read_manifest(store, id) and a deterministic train(...). A store holds blobs, runs, tags and pins.

Write two functions in versioning.py:

  • blob_id(data): the content id of some bytes, their sha256 hex digest.
  • put(store, data): store the bytes under their content id and return the id. Putting the same bytes twice keeps a single copy.

Run it: the same content gets the same id, and the store dedupes it.

versioning.py, the file you edit78 lines
"""Your version store. You content-address data and models, snapshot a dataset as a tree, record which data
and code produced each model, tell whether a run reproduces, verify the store's integrity, and garbage
collect what no tagged release still needs."""
import hashlib
import json

import store as S


# ---------- Step 1: content addressing ----------

def blob_id(data):
    """The content id of some bytes: their sha256 hex digest."""
    # TODO (Step 1): sha256 hex digest of the bytes.
    raise NotImplementedError("Step 1: write blob_id()")


def put(store, data):
    """Store bytes under their content id and return the id. Storing the same bytes twice keeps one copy."""
    # TODO (Step 1): store data under blob_id(data) and return the id; the same bytes overwrite
    # the same key, so they are kept once.
    raise NotImplementedError("Step 1: write put()")


# ---------- Step 2: snapshot a dataset ----------

def snapshot(store, files):
    """Store every file's bytes, build a {path: blob_id} tree, store that tree too, and return its id. Two
    datasets that share a file share its blob; two identical datasets get the same snapshot id."""
    raise NotImplementedError("snapshot() arrives in Step 2")


def diff(store, snap_a, snap_b):
    """What changed from snapshot a to b: {"added", "removed", "changed"} lists of paths. A path is changed
    when it exists in both but points at different blobs."""
    raise NotImplementedError("diff() arrives in Step 2")


# ---------- Step 3: record lineage ----------

def record_run(store, name, version, data_snap, code_id, params, model_bytes):
    """Store the model and record the run that made it: its name, version, the data snapshot, the code id
    and params, and the model's id. Returns the run record."""
    raise NotImplementedError("record_run() arrives in Step 3")


def provenance(store, model_id):
    """The inputs that produced a model: {"data_snap", "code_id", "params"} of the run whose model_id this
    is, or None if no run made it."""
    raise NotImplementedError("provenance() arrives in Step 3")


# ---------- Step 4: reproducibility and integrity ----------

def run_fingerprint(run):
    """A hash of a run's INPUTS only (data snapshot, code id, params). Two runs reproduce each other when
    their fingerprints match, whatever their name or version."""
    raise NotImplementedError("run_fingerprint() arrives in Step 4")


def verify_store(store):
    """The ids of any blobs whose stored bytes no longer hash to their id: corruption or tampering. An
    intact store returns an empty list."""
    raise NotImplementedError("verify_store() arrives in Step 4")


# ---------- Step 5: garbage collection ----------

def reachable(store):
    """Every blob id still needed by a tagged release: for each tagged run, its model, its code, its data
    snapshot and every file blob inside that snapshot, plus anything pinned directly."""
    raise NotImplementedError("reachable() arrives in Step 5")


def gc(store):
    """Delete blobs no tagged release needs, and return the ids removed. Reachable and pinned blobs stay,
    so any tagged model remains fully materializable afterwards."""
    raise NotImplementedError("gc() arrives in Step 5")
Provided for you:store.pytry_it.py

Frequently asked questions

What is content-addressed storage?

Storing each object under an id derived from a hash of its bytes. Identical content gets the same id and is stored once, an id is reproducible from the content alone, and any change to the bytes changes the id, which makes tampering detectable. Git and DVC both work this way.

How does data versioning support reproducibility?

By recording, for every model, the exact snapshot of the data and the code and params that produced it. A run's fingerprint over those inputs lets you tell whether a rerun truly reproduces an earlier model or whether an input quietly changed.

Why does a model store need garbage collection?

It accumulates every intermediate dataset and model. Garbage collection frees the blobs no tagged release references, but it has to keep everything a tagged model depends on so an old version can still be rebuilt and rolled back to.

Versioning data and models

Versioning code is solved; versioning the data and models that machine learning depends on is not, and overwriting a file loses the link between a model and what produced it. Content-addressed storage, the idea behind DVC and git's object store, fixes this: address everything by the hash of its content, so data is deduplicated, a version id is reproducible, and lineage is exact. In this lab you build that store: content-addressed blobs, dataset snapshots you can diff, a lineage record from every model to its data and code, a run fingerprint that separates real reproductions from changed inputs, integrity verification, and garbage collection that keeps every tagged release materializable.