ML Data Versioning: lakeFS vs DVC vs Delta Lake Time Travel
Every ML team eventually gets the same message: “The model from March was better. Can you retrain it?” Code is in Git, hyperparameters are in the tracker, the container image is pinned, and yet nobody can say which rows the March model actually saw. The training table has been appended to, compacted, back-filled, and partially deleted for a GDPR request. The honest answer is “not exactly,” and that answer quietly destroys auditability, debugging, and trust.
ML data versioning is the discipline of making the dataset a first-class, addressable input, exactly like a Git SHA. Three tool families dominate in 2026: DVC, which tracks data with Git-committed pointer files; lakeFS, which gives an object store Git-style branches, commits, and merges; and Delta Lake time travel, which versions a single table through its transaction log.
You will leave with a precise mental model of how each mechanism works, where each one silently loses history, a decision matrix, runnable code for pinning a dataset to a training run, and a short checklist for making retraining actually reproducible.
What this covers: the reproducibility contract, the internals of DVC, lakeFS, and Delta time travel, retention and garbage-collection traps, a cost model with illustrative numbers, a decision flow, and an adoption checklist.
Context and Background
Reproducibility in ML has four independent inputs: code, configuration, environment, and data. The first three have mature answers. Git pins code, a tracked config file pins hyperparameters, and a container image digest pins the environment. Data is the odd one out because it is large, mutable, and usually owned by a different team than the one training the model. Treating the dataset as “whatever is in the bucket today” is the single most common reason a retraining run does not reproduce.
The problem has gotten sharper for three reasons. First, training sets are increasingly assembled from many sources: feature tables, raw event logs, labelled images, and synthetic augmentations. Second, regulation and internal governance now ask for lineage, so “which data trained this model” is a compliance question as well as a debugging one. Third, pipelines retrain continuously, which means the training set is a moving target by design. If you have not read our piece on feature store architecture and online/offline parity, it covers the sibling problem: keeping the features served at inference consistent with those used at training. Data versioning is the upstream half of the same guarantee.
Three mechanisms have emerged, and they are not interchangeable. The pointer-file approach (DVC) keeps data in ordinary storage and puts a tiny content-addressed manifest in Git. The versioned object store approach (lakeFS) places a Git-like metadata layer between your tools and the bucket. The table log approach (Delta Lake, and similar formats such as Apache Iceberg) records every change to a table as an ordered log of actions and lets you query an older state. Our comparison of Iceberg catalogs including Polaris, Nessie, and Unity is relevant here because catalog-level branching is the table-format world’s answer to what lakeFS does at the object level.
A frequent mistake is to ask “which tool is best” before asking “what is the unit of versioning.” The unit might be a directory of images, a single Parquet table, or a coordinated set of twenty tables that must be consistent with each other. The right tool depends on that unit far more than on feature checklists. The rest of this post is organised around that idea.
What “reproducible” actually means
It is worth separating three levels of reproducibility, because vendors blur them. Data reproducibility means you can retrieve byte-identical inputs. Pipeline reproducibility means the same code run on those inputs yields the same intermediate artifacts. Model reproducibility means the final weights match, which additionally depends on random seeds, nondeterministic GPU kernels, and library versions. Data versioning only solves the first level, but the first level is the prerequisite for the other two. If the inputs drift, nothing downstream can be compared.
For most teams, the realistic target is not bit-exact weights. It is that a retrained model on the pinned data lands within normal seed variance of the original metrics, and that any difference can be attributed to a code or config change rather than to unknown data drift. That is a much cheaper goal, and it is the one the tools below can actually deliver.
The Reproducibility Contract: Pin the Data Like You Pin the Code
The core idea of this post is simple: every training run should write a small manifest that names an immutable data reference, and every consumer should resolve data through that reference only. The reference might be a Git revision of a .dvc file, a lakeFS commit ID, or a Delta table version number. What matters is that it is immutable, recorded next to the run, and sufficient to retrieve the data later.

Figure 1: The run manifest. Code, data reference, config, and environment image digest are pinned together, so any past run can be audited or replayed.
The diagram shows the manifest as the join point. The experiment tracker stores it alongside metrics and the model artifact, which lets a later engineer start from a model and walk back to exact inputs, or start from a manifest and rerun training. The data reference is the only field that differs by tool, which is why the three approaches can coexist in one organisation.
Direct answer: how do you version data for ML training? Record an immutable reference to the dataset, such as a DVC-tracked Git revision, a lakeFS commit ID, or a Delta Lake table version, in every training run’s metadata, and make training read data only through that reference. The tool choice depends on whether the unit of versioning is files, an object-store repository, or a single table.
Why mutable names break reproducibility
A path like s3://bucket/train/latest/ is a mutable name. A branch name such as main is also mutable, as is VERSION AS OF omitted from a Delta read, which resolves to the newest version. Reproducible systems resolve a mutable name to an immutable identifier exactly once, at run start, and then record the identifier. In lakeFS terms, that means resolving main to a commit ID. In Delta terms, it means reading the table’s current version and then passing it explicitly. In DVC terms, it means the Git SHA of the repository that contains the pointer file.
This resolve-once pattern also protects you from a subtle race: two workers in a distributed training job that each read “latest” a few seconds apart can see different data. Resolving once in the launcher and passing the identifier to every worker removes that class of bug entirely.
The three mechanisms in one table
| Property | DVC | lakeFS | Delta Lake time travel |
|---|---|---|---|
| Unit of versioning | File or directory | Whole repository of objects | One table |
| Version identifier | Git commit of .dvc / dvc.lock |
lakeFS commit ID, tag, branch | Table version number or timestamp |
| Where history lives | Git plus a content-addressed cache and remote | lakeFS metadata layer over the object store | Table’s _delta_log |
| Branching | Via Git branches | Native, zero-copy | Not a native table concept in open-source Delta (verify against your engine) |
| Cross-dataset atomicity | Only through one Git commit | Atomic commits and merges across paths | Single-table transactions |
| Typical failure | Forgetting dvc push |
Garbage collection removing needed data | VACUUM or log expiry removing needed versions |
The last row is the one that matters operationally. Each tool has a retention mechanism that can silently delete the very history you thought you were keeping, and each one fails differently. We return to this in the trade-offs section.
Choosing the unit of versioning
If your training set is a folder of images plus a labels CSV, the natural unit is a directory, and a pointer file that hashes the directory is enough. If your training set is a lakehouse with raw, cleaned, and feature layers that must stay mutually consistent, you need atomic multi-path commits, and a table-level log cannot give you that across tables. If your training set is one very large Delta table that you already maintain, time travel may be all you need, provided you control retention.
The practical heuristic: version at the smallest unit that still guarantees consistency. Going finer than that gives you a dataset whose parts can be mismatched. Going coarser than that gives you storage and governance overhead for no benefit.
Inside the Three Mechanisms: DVC, lakeFS, and Delta Lake Time Travel
DVC: content-addressed pointers committed to Git
DVC (Data Version Control) keeps large files out of Git while keeping their identity in Git. When you run dvc add data/train, DVC hashes the content, moves it into a local cache, and writes a small .dvc file. Per the DVC documentation, files ending in .dvc are data placeholders that can be versioned with Git and contain the information needed to track the target data over time; the example in the docs shows an outs entry carrying an md5 hash and size. You commit that pointer. The data itself travels separately to a configured remote such as S3, GCS, or Azure Blob via dvc push.

Figure 2: The DVC loop. Data is hashed into a cache, the pointer file goes to Git, and the bytes go to remote storage. Checking out an old Git revision restores the old pointer, and dvc pull or dvc checkout restores the matching bytes.
The elegance of this design is that Git remains the single source of truth for history, branching, and review. A data change becomes a pull request that modifies a pointer file, and reviewers can see which hash changed. DVC also supports pipelines through dvc.yaml and a dvc.lock file that records the exact hashes of dependencies and outputs for each stage, so a dvc repro can skip stages whose inputs have not changed. For sharing datasets across repositories, dvc import brings a file or directory tracked by another DVC or Git repository into the workspace and records where it came from, with a --rev option to pin a specific revision of the source.
The failure modes follow directly from the architecture. If someone commits a pointer but forgets dvc push, the Git history references bytes that exist only on one laptop. DVC can tell you (dvc status --cloud), but nothing enforces it unless you add a CI check. Second, DVC hashes whole files, so changing one row in a large single file produces a brand-new object, and storage grows by the full file size per version. DVC does handle directories with per-file hashing, so many small files dedupe well; a single multi-gigabyte Parquet file does not. Third, DVC tracks files, not tables, so it has no notion of a schema or a partition; it simply sees changed bytes.
DVC’s sweet spot is therefore a team that already lives in Git, with datasets in the megabytes-to-low-terabytes range, mostly files or directories, and a need for cheap local experimentation. It is a poor fit for a shared lakehouse where dozens of producers write continuously, because Git is not a good coordination point for high-frequency writers.
lakeFS: Git-style branches over the object store
lakeFS places a versioning layer between your clients and an object store. According to the lakeFS documentation, the data itself is not stored inside lakeFS but in the underlying object store, while lakeFS manages pointers and additional metadata. A repository is a logical namespace grouping objects, branches, and commits. Commits are immutable checkpoints containing all the contents of the repository at that moment, identified by a commit ID that is a digest of the commit’s contents. Tags give a human-friendly name to a specific commit.
The mechanism that makes this practical is zero-copy branching. The docs describe a branch as a pointer to a commit plus a set of uncommitted changes, and creating one does not duplicate data. Physically, lakeFS never overwrites an object: uploaded objects land at generated physical addresses under a data/ prefix, and commit metadata (the range and meta-range files) lives under a _lakefs/ prefix in the same storage namespace. Because a path unchanged across versions maps to the same physical object, a new branch of a 100 TB repository costs a metadata pointer rather than 100 TB.
Merging follows Git semantics. lakeFS finds the merge base, then performs a three-way merge over the presence and identity of files. The documentation includes the full truth table: a file changed on only one side takes that side, identical changes on both sides are accepted, and a file changed differently on both sides, or changed on one side and deleted on the other, is a conflict. Merges operate on whole files, because lakeFS is format-agnostic. The source-wins and dest-wins strategies resolve conflicts for the whole merge at once, not per object.

Figure 3: Write-audit-publish with lakeFS. An ingest job works on an isolated branch, a pre-merge hook validates it, and only a passing merge reaches main. Training resolves a commit ID and tags it.
The feature that matters most for ML is the combination of branches, hooks, and protection rules. Hooks are declared in an actions file and can fire on events such as pre-commit and pre-merge; if any validation fails, the operation (for example a merge into main) is blocked. The documentation’s example enforces that only Parquet and Delta Lake files may be written under a given prefix on merges to main. Branch protection rules go further: on a protected branch, direct object writes and commits fail, so the only way in is a merge, which then triggers the pre-merge hooks. The docs note that reverting a previous commit is still allowed on a protected branch.
For reproducible training, this yields a clean pattern. Ingestion writes to a branch. Validation hooks gate the merge. A training job resolves main to a commit ID, reads via lakefs://repo/<commit-id>/path, and tags the commit so the reference has a durable human-readable name. Check your garbage collection retention rules to confirm that tagged or referenced commits are protected in your version, because we did not verify that behaviour. The lakeFS URI format is lakefs://<REPO>/<REF>/<KEY>, and ref expressions such as dev~3 mirror Git’s syntax, so you can read the dataset as it was three commits ago.
The costs are operational. You run a service (or use the managed offering), your jobs access data through the lakeFS endpoint or S3-compatible gateway, and you must manage garbage collection, which is where history is genuinely deleted. We cover that in the trade-offs section.
Delta Lake time travel: the transaction log as a version history
Delta Lake records every change to a table as a numbered commit in the _delta_log directory. Time travel lets you query a previous table state by version number or by timestamp. In SQL that is SELECT * FROM events VERSION AS OF 42 or TIMESTAMP AS OF '2026-03-01'; in the Python reader it is the versionAsOf or timestampAsOf option. DESCRIBE HISTORY lists operations, and RESTORE can roll a table back to an earlier version.
The important detail for reproducibility is that time travel is bounded by two retention properties, both documented in the Delta Lake docs. delta.logRetentionDuration controls how long history is kept, with a default of 30 days. delta.deletedFileRetentionDuration controls how long ago a data file must have been deleted before it becomes a candidate for VACUUM, with a default of 7 days. The docs explicitly say that by default you can time travel up to 30 days unless you ran VACUUM or changed those properties, and that to access 30 days of history even if you run VACUUM you must set the file retention to 30 days, which may increase storage cost.
There is a second, subtler limit that the docs also call out: because log entries are cleaned up on checkpoints, you can end up unable to time travel to a version that is younger than the retention interval. Delta needs all consecutive log entries since the previous checkpoint to reconstruct a version, so if a checkpoint exists at version 10 and the entry for version 0 is cleaned up, versions 1 through 9 become unreachable. The documented mitigation is to increase delta.logRetentionDuration.
Here is a minimal, runnable pattern for pinning a Delta version at the start of a training run:
from delta.tables import DeltaTable
from pyspark.sql import SparkSession
import json, subprocess, time
spark = SparkSession.builder.getOrCreate()
path = "s3://lake/curated/click_events"
# 1. Resolve the mutable name ("current") to an immutable version, once.
dt = DeltaTable.forPath(spark, path)
latest = dt.history(1).collect()[0]
pinned_version = int(latest["version"])
# 2. Read through the pin, never through "current".
train_df = (spark.read.format("delta")
.option("versionAsOf", pinned_version)
.load(path))
# 3. Record the pin next to the code SHA.
manifest = {
"data_kind": "delta",
"table": path,
"version": pinned_version,
"row_count": train_df.count(),
"git_sha": subprocess.check_output(
["git", "rev-parse", "HEAD"]).decode().strip(),
"pinned_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
print(json.dumps(manifest, indent=2))
The manifest is then logged to the experiment tracker. Our walkthrough of MLflow 3 experiment tracking, model registry, and GenAI tracing shows where such parameters and tags belong so they travel with the registered model.
Delta time travel is excellent when the training set is one table and you control table maintenance. It is not a branching system, it does not span tables, and its guarantee is only as long as your retention settings. A table that is VACUUMed aggressively to save money has, in effect, opted out of reproducibility.
Putting It Together: Pinning Data in a Real Training Run
A lakeFS-based training launcher
The following sketch resolves a branch to a commit once, reads through the commit, and records the reference. It uses the lakefs Python SDK; treat method names as illustrative of the pattern and check the SDK reference for your installed version before copying.
import lakefs
import pandas as pd
repo = lakefs.repository("ml-datasets")
# 1. Resolve the mutable branch to an immutable commit ID, once.
main = repo.branch("main")
commit_id = main.get_commit().id
# 2. Tag it so the run has a durable human-readable handle.
repo.tag("train-v42").create(commit_id)
# 3. Read only through the commit, never through "main".
uri = f"lakefs://ml-datasets/{commit_id}/curated/clicks.parquet"
df = pd.read_parquet(uri, storage_options={"anon": False}) # via lakefs-spec/fsspec
run_meta = {"data_kind": "lakefs", "repo": "ml-datasets",
"commit": commit_id, "tag": "train-v42",
"path": "curated/clicks.parquet"}
Notice that nothing in this launcher changes the existing training code beyond a different URI. That is the lakeFS value proposition for ML teams: version-aware reads without rewriting the pipeline.
A DVC-based equivalent
With DVC, the reference is a Git revision. A launcher can call dvc.api to open a file at a specific revision without checking out the workspace:
import dvc.api, subprocess
rev = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
with dvc.api.open("data/train.csv", rev=rev, mode="r") as f:
train_text = f.read()
run_meta = {"data_kind": "dvc", "git_rev": rev, "path": "data/train.csv"}
For stage-based projects, dvc repro reads dvc.lock and recomputes only stages whose dependency hashes changed. That is the DVC counterpart to the manifest in Figure 1, and it is checked into Git automatically, so the data pin and the code pin are literally the same commit.
A unified manifest
Regardless of tool, we recommend one small, tool-agnostic manifest schema. The fields are the same across tools; only ref changes meaning.
run_id: 2026-11-07-ranker-017
code: {git_sha: 9f3c1ab, dirty: false}
data:
kind: lakefs # lakefs | dvc | delta
ref: c6a0...e91 # commit id | git rev | table version
uri: lakefs://ml-datasets/c6a0...e91/curated/clicks/
rows: 184220331
checksum: sha256 of a sorted sample of row hashes
env: {image: registry/train@sha256:...}
config: {path: conf/ranker.yaml, sha: 41be7c2}
seeds: {python: 17, numpy: 17, torch: 17}
The checksum field deserves a note. Even with an immutable reference, you want a cheap integrity check that detects the case where garbage collection or a retention job has modified what the reference resolves to. A row count plus a hash over a sorted sample of row hashes takes seconds and catches silent corruption early. Validate it at the start of every rerun, and fail loudly on mismatch.
Verifying a rerun
Reproducibility claims need a test. A practical approach is a weekly job that picks a random production model from the registry, loads its manifest, resolves the data reference, validates the checksum, and retrains with a reduced step budget. It then compares the loss curve prefix to the logged one. If the data is retrievable and the first few hundred steps match within seed noise, the contract holds. If the reference fails to resolve, you have found a retention bug while it is still cheap to fix.
Cost, Retention, and Garbage Collection: Where History Quietly Dies
All three tools store old versions, and old versions cost money. The interesting question is what each tool’s cleanup does and who triggers it.
A simple cost model
For a dataset of size S gigabytes with a daily change fraction f (the share of bytes rewritten per day) and retention of R days, the extra storage held for history is approximately S × f × R for tools that dedupe unchanged data at object granularity. The monthly cost is that figure times the object-store price per GB-month. Using purely illustrative numbers: with S = 10,000 GB, f = 2 percent, and R = 90 days, extra history is about 10,000 × 0.02 × 90 = 18,000 GB. At an illustrative $0.023 per GB-month, that is roughly $414 per month. Change any input and the answer scales linearly, which is exactly why the formula is useful. These figures are an illustration of the arithmetic, not a benchmark of any product.
The factor that breaks the linear model is rewrite amplification. A compaction job on a Delta table that rewrites every file produces a new copy of the table’s bytes, so f jumps from 2 percent to near 100 percent on compaction days. Likewise, DVC versions a single large file as a whole object, so a one-row edit costs the full file. Object-level deduplication (lakeFS) and file-level deduplication (Delta) are only as good as your write patterns. Measure f from your actual write logs before choosing a retention window.
How each tool deletes history
| Tool | What deletes data | Default behaviour (documented) | What to configure |
|---|---|---|---|
| DVC | dvc gc, run manually or in CI |
Nothing is removed unless you run it | Scope flags so you keep revisions that matter; gate in CI |
| lakeFS | Garbage collection, based on retention rules | Driven by retention rules you define (confirm defaults for your version) | Retention per branch; protect release commits |
| Delta Lake | VACUUM, plus log cleanup at checkpoints |
Log history 30 days, deleted files 7 days | Raise both properties to match your audit window |
The pattern is consistent. DVC and lakeFS require you to run an explicit cleanup, so their failure mode is a cleanup that is configured too aggressively. Delta applies a default retention automatically, so its failure mode is an audit window that is shorter than anyone realised. In our experience, Delta’s quiet default is the more common cause of “we cannot reproduce last quarter’s model” tickets, because nobody ever made a deliberate decision.
Legal deletion versus reproducibility
There is a genuine tension between reproducibility and the right to erasure. If a user’s record must be deleted from all copies, then every historical version containing it must be purged too, which means old training sets become unreproducible by design. There is no technical trick that resolves this; it is a policy decision. A defensible approach is to keep reproducible snapshots for a bounded window, record in the manifest that a snapshot was subject to erasure, and treat models trained on erased snapshots as reproducible only up to the erasure event. Document this in your model cards. Do not promise immutability that your legal obligations contradict.
Decision Matrix and Flow

Figure 4: Decision flow. Start from the unit of versioning and the team’s workflow, then record the pin in the run manifest whichever branch you land on.
The flow encodes three observations. If everything is in Delta tables and you do not need branching or multi-table atomicity, native time travel plus an explicit pin is the cheapest path. If you have mixed files and tables at scale, or you need isolated branches and quality gates, a versioned object store earns its operational cost. If you are a Git-native team with moderate datasets, DVC has the lowest adoption friction.
Use-case matrix
| Use case | Best fit | Why |
|---|---|---|
| Computer vision, folders of images plus labels, small team | DVC | Directory hashing, Git review, cheap local experiments |
| Lakehouse with raw, cleaned, feature layers that must stay consistent | lakeFS | Atomic multi-path commits, branches, pre-merge hooks |
| Single large fact table feeding a tabular model | Delta time travel | No new infrastructure, as long as retention is set |
| Experiment sandboxes cloned from production | lakeFS | Zero-copy branches avoid duplicating the dataset |
| Regulated audit trail of training data | lakeFS or Delta, plus manifest | Immutable references and retention you control |
| Sharing curated datasets across repositories | DVC import or lakeFS tags |
Both record the source and a pinned revision |
They compose
Nothing prevents combining them. A common and sensible stack is Delta tables stored inside a lakeFS repository: lakeFS provides the branch-and-merge workflow and cross-table atomicity, while Delta provides table semantics and query performance. The lakeFS hook example in the documentation, which allows only Parquet and Delta Lake files under a prefix, hints at exactly this combination. DVC can then track small derived artifacts such as label files and evaluation sets in Git alongside the code. The manifest records both references. The cost of composing is conceptual overhead: engineers must know which layer’s retention applies to which bytes.
If you run table formats with catalog-level branching instead, the Polaris, Nessie, and Unity comparison explains how Nessie-style catalog commits deliver a similar multi-table guarantee at the catalog layer rather than the object layer. That is a legitimate alternative when your estate is already Iceberg.
Trade-offs, Gotchas, and What Goes Wrong
Pointers that point at nothing
The most common DVC failure is a Git commit whose .dvc pointer references a hash that was never pushed. The repository looks healthy, CI passes, and the dataset is unrecoverable the day the laptop dies. The fix is mechanical: a CI step that runs a remote status check on every pull request, and a branch rule that refuses merges when pointers lack remote objects. The same class of failure appears in dvc import chains, where an upstream team garbage-collects an old revision that downstream projects still pin.
Garbage collection racing the training queue
With lakeFS, a retention rule that is correct for dashboards may be wrong for ML. A weekly training job that references a commit on a feature branch deleted two weeks ago will fail to resolve its data. Protect the references you promise to keep, tag release commits, and write retention rules from the model registry’s perspective, not the data team’s. Before relying on any retention behaviour, test it in a staging repository by running cleanup and then reading a tagged commit.
Time travel that ends at the checkpoint
Delta’s version-reachability caveat is easy to miss. You can set the retention to 30 days and still lose access to a recent version if log cleanup removed the entries needed to reconstruct it from the nearest checkpoint, as the documentation warns. Raising the log retention is the documented remedy. Treat any version you intend to reproduce as something to verify periodically by reading it, rather than something to assume.
Anti-patterns worth naming
- Versioning the model but not the data. A registered model with a code SHA and no data reference is half a record.
- Resolving “latest” inside the training loop. Resolve once in the launcher and pass the identifier to every worker.
- Copying datasets into per-experiment folders. This reinvents versioning badly, costs full storage per copy, and leaves no lineage.
- Treating a branch name as a version. Branches move. Commit IDs, tags, and table versions do not.
- Hashing everything on every run. Full-dataset checksums on petabyte data are impractical. Use a manifest of counts and a sampled row hash instead.
Format-agnostic merges are coarse
lakeFS merges whole files and, per its documentation, a conflict strategy applies to the entire merge rather than object by object. Two jobs that both rewrite the same Parquet file on different branches will conflict, even if they touched different rows. Plan ownership by prefix, so each branch writes disjoint paths, and you rarely see conflicts at all.
Performance and operational overhead
Adding a versioning layer adds a hop and an operational dependency. For lakeFS, the metadata store and the service must be sized and monitored; the project publishes a sizing guide. For DVC, large dvc pull operations on cold caches can dominate job start-up time, so cache the DVC cache on fast local or shared disk. For Delta, long histories make log replay more expensive until checkpoints catch up. None of these are disqualifying; all of them should be measured on your own data rather than assumed negligible.
What none of them solve
Versioning data does not make the data correct. A faithfully reproducible training set can still contain label noise, leakage, or a skewed sample. Nor does it version the logic that produced the data. If a transformation job changes, the output changes, and the pin only tells you that the bytes differ. Pair data versioning with data quality gates, and version the transformation code in the same Git commit you record in the manifest.
Practical Recommendations
Start by deciding the unit of versioning, then pick the least powerful tool that guarantees consistency for that unit. For a single Delta table, use time travel with explicit pins and retention that matches your audit window. For file-based projects owned by a Git-fluent team, use DVC. For a shared lakehouse with multiple producers, a need for isolated experiments, or validation gates before data reaches consumers, use lakeFS, and consider keeping Delta tables inside it.
Then make the pin non-optional. A training launcher should refuse to start without a resolved immutable reference, and the experiment tracker should refuse to register a model whose run lacks one. Cultural enforcement fails; tooling enforcement works. Finally, test the contract on a schedule, because retention bugs are invisible until the day you need the history.
A checklist you can adopt this week:
- Choose the unit of versioning for each training dataset and write it down.
- Resolve every mutable name (branch,
latest, current table) to an immutable ID in the launcher. - Log a manifest with code SHA, data reference, config hash, image digest, row count, and a sampled checksum.
- Set Delta
delta.logRetentionDurationanddelta.deletedFileRetentionDurationto cover your longest audit window, and budget the storage. - Add a CI check that every DVC pointer has its objects in the remote.
- Protect release commits and tags from lakeFS garbage collection, and test it in staging.
- Run a monthly rerun drill on a random registered model.
- Write the erasure policy: how long snapshots live and how the manifest records deletions.
- Estimate your change fraction f from write logs before choosing a retention window.
Frequently Asked Questions
What is ML data versioning and why does it matter?
ML data versioning means recording an immutable reference to the exact dataset used for each training run, so you can retrieve or audit it later. It matters because code, config, and environment are easy to pin but data is large and mutable. Without a data pin, retraining a past model produces uncontrolled differences, debugging regressions becomes guesswork, and compliance questions about what a model was trained on have no reliable answer.
Is lakeFS better than DVC?
Neither is universally better; they version different units. DVC stores content-addressed pointer files in Git and suits file or directory datasets with a Git-centred workflow. lakeFS adds Git-style branches, commits, merges, and hooks over an object store, which suits shared lakehouses with many writers and a need for atomic changes across many paths. Teams with small to moderate datasets often prefer DVC’s simplicity, while larger shared estates tend to need lakeFS.
How long does Delta Lake time travel keep history?
According to the Delta Lake documentation, you can time travel up to 30 days by default, because delta.logRetentionDuration defaults to 30 days, while delta.deletedFileRetentionDuration defaults to 7 days for files eligible for VACUUM. Running VACUUM or shortening either property reduces what you can read. Log cleanup at checkpoints can also make some recent versions unreachable, so raise retention and verify a version by reading it.
Can I use Delta Lake time travel instead of a versioning tool?
Often yes, when your training data is a single Delta table you control. Pin a version number in the manifest, read it with versionAsOf, and set retention to cover your audit window. It falls short when you need branches, atomic consistency across several tables, or versioning of non-tabular files such as images. In those cases combine it with lakeFS or track the unstructured assets with DVC.
Do these tools work together?
Yes. A common design stores Delta tables inside a lakeFS repository, so lakeFS supplies branching, hooks, and cross-table atomic commits while Delta supplies table semantics and performance. DVC can track small derived artifacts alongside the code in Git. The key is a single run manifest that records each reference, and a clear understanding of which layer’s retention governs which bytes.
How do I make a retrained model reproducible?
Pin the data reference, code SHA, config, and container image digest in one manifest, fix random seeds, and validate a row count and sampled checksum before training. Expect results within normal seed variance rather than bit-exact weights, since GPU kernels and library versions can introduce nondeterminism. Schedule a recurring rerun drill so retention or garbage-collection problems surface while the history still exists.
Further Reading
Internal:
- Feature store architecture: online and offline parity for keeping training and serving features consistent.
- MLflow 3 experiment tracking, model registry, and GenAI tracing for where run manifests and data pins are stored.
- Iceberg catalogs: Polaris vs Nessie vs Unity for catalog-level branching in table formats.
External:
- lakeFS documentation: internals, hooks, and branch protection
- Delta Lake documentation: table batch reads and writes, time travel and data retention
- DVC documentation: .dvc files and the import command
By Riju — about
