ML Metadata
Every model is built from a dataset, a code commit, and a set of parameters. ML metadata writes that recipe down for each run, so you can trace any model back to exactly what made it.
Why Does This Exist?
A model fails in production and you open the code. The code looks fine, because the code is only one third of the recipe. The model also came from a dataset snapshot, a parameter choice, and an environment, and none of those live in the source file. Without a record, the question "which data trained this model" has no answer, only guesses across spreadsheets and filenames like model_final_v7_REAL.pkl.
ML metadata is that record. Each run logs its inputs, its code version, its parameters, and its outputs into a queryable graph. When predictions turn strange, you walk the graph backwards from the model to the training run to the dataset. When an auditor asks how a decision was made, the same walk is the proof. It replaces scattered notes with one ledger every run writes to.
This page covers the ledger itself: artifacts, executions, and the links between them. Logging parameters and metrics per run is experiment tracking. Versioning the datasets those runs read is data versioning. Staging finished models for release is the model registry. If the lifecycle is new, start with the MLOps overview.
Basics first: an artifact is an immutable file plus its address, like s3://ml-bucket/data_v2.csv with 12,400 rows. An execution is one run of code, like training job run_42 from commit a3f9c1. Metadata stores the addresses and the links, never the file bytes. Everything below assumes that split.
Think of It Like This
The kitchen ticket rail
A busy kitchen hangs a ticket for every dish: table 6, salmon, no salt, fired at 7:04 by chef Ana, from delivery lot 12. The plate carries no history, but the rail does. When a customer complains, the chef reads the ticket backwards from plate to cook to ingredients and finds the bad lot in seconds.
ML metadata is the ticket rail. The model is the plate. The training run is the ticket, naming the dataset lot, the code commit, and the parameter choices. The lineage links are the clips holding ticket to plate.
Where it stops: tickets only work when every dish gets one. A cook who plates off ticket breaks the chain, and no rail can reconstruct it. Manual edits outside the pipeline break lineage the same way.
How It Actually Works
Three node kinds and one edge kind do the work. Call an input or output file , which means one artifact with a URI and a hash. Call one run , which means one execution with a code commit, an image tag, and parameters. Call a link , which means one event of type INPUT or OUTPUT joining an execution to an artifact. A context groups nodes that belong together, like all runs of pipeline run 42.
Artifacts carry addresses, not bytes
An artifact record holds a URI, a hash, and small properties: row counts, schema version, size. The store never holds the 8.2 MB model file or the 12,400-row CSV, only pointers to them. That keeps queries fast and leaves bytes in the bucket where they belong. Two artifacts with the same hash are the same content even when filenames differ, which is how the ledger survives renames.
Executions name the code that ran
An execution record holds the commit hash, the container image, the parameters, and start and end times. Training job run_42 records commit a3f9c1, image trainer:1.4, learning rate 0.01, and 20 epochs. Because the execution is a node rather than a log line, many artifacts can point at it and one query returns them all.
Events make lineage a two-hop walk
Each event joins one execution to one artifact with a direction. run_42 INPUT data_v2 means the run read that dataset. run_42 OUTPUT model_v7 means it wrote that model. Given a model, find its writing execution, then find that execution INPUT artifacts. Two hops answer "which data trained this model" and the reverse walk answers "which models used this broken dataset". When drift fires in production, this walk names the exact training snapshot to compare against.
Worked example
Dataset data_v2 holds 12,400 rows: 10,400 for training and 2,000 held out, since . Training job run_42 reads it with learning rate 0.01 for 20 epochs and writes model v7 plus a metrics artifact. The metrics say 1,660 of 2,000 held-out rows are correct, so accuracy is .
Count the graph: 3 artifacts (dataset, model, metrics), 1 execution (the training job), 3 events (1 INPUT plus 2 OUTPUT). Context pipeline-run-42 groups all 4 nodes. The lineage answer for model_v7 is 2 hops: model to run_42 to data_v2. No filename search, one query.
Code
The point is the graph shape, not a server. This stdlib-only store logs addresses and links, then walks them backwards the way a real metadata query does.
store = {"artifacts": {}, "executions": {}, "events": []}
store["artifacts"]["data_v2"] = {"uri": "s3://ml-bucket/data_v2.csv", "rows": 12400}store["executions"]["run_42"] = {"code": "a3f9c1", "image": "trainer:1.4", "lr": 0.01, "epochs": 20}store["artifacts"]["model_v7"] = {"uri": "s3://ml-bucket/model_v7.pkl", "mb": 8.2}store["artifacts"]["metrics_42"] = {"correct": 1660, "total": 2000}
store["events"].append(("run_42", "INPUT", "data_v2"))store["events"].append(("run_42", "OUTPUT", "model_v7"))store["events"].append(("run_42", "OUTPUT", "metrics_42"))
acc = store["artifacts"]["metrics_42"]["correct"] / store["artifacts"]["metrics_42"]["total"]print(round(acc, 4)) # -> 0.83print(len(store["artifacts"]), len(store["executions"]), len(store["events"])) # -> 3 1 3
writer = [e[0] for e in store["events"] if e[1] == "OUTPUT" and e[2] == "model_v7"][0]inputs = [e[2] for e in store["events"] if e[0] == writer and e[1] == "INPUT"]print(writer, inputs) # -> run_42 ['data_v2']Three artifacts, one execution, three events, and the last line is the two-hop answer: model v7 came from run_42 which read data_v2.
Watch Out For
Storing file bytes inside the metadata store
The symptom is a metadata database that grows by gigabytes and queries that slow to a crawl. Someone logged the CSV or the model weights as a field value instead of a URI, so every run copies megabytes into a relational row built for small facts.
Keep bytes in the bucket and addresses in the store. Log the URI, the hash, the row count, and the size, and fetch the file through the URI when you need it. If the store already holds bytes, migrate the blobs out and leave pointers behind.
A manual edit that severs the chain
A scientist downloads a CSV, fixes labels on a laptop, re-uploads it, and trains without logging the edit. The model looks tracked, but its true parent is a file no event names. The symptom appears later: lineage says data_v2 while the model actually saw data_v2-fixed-by-hand, and a drift investigation compares against the wrong baseline.
Treat the pipeline as the only writer. Every transform that touches data or models runs as a logged execution, even a five-line fix. No logged execution means it did not happen.
The Quick Version
- ML metadata is the per-run recipe: which dataset, which code commit, which parameters, which outputs.
- Artifacts store URIs and hashes, never file bytes, so queries stay fast.
- Executions name the code commit, image, and parameters behind one run.
- Events link runs to files as INPUT or OUTPUT, so lineage is a two-hop walk.
- One run reading 12,400 rows and scoring 1,660 of 2,000 gives accuracy 0.83 with 3 artifacts, 1 execution, and 3 events.
- Contexts group related nodes, like every artifact and run inside pipeline run 42.
- Manual edits outside the pipeline silently break the chain that audits and debugging depend on.