Lab 01 — MLflow-style Tracking Store & Model Registry
Phase: 17 — MLOps: The ML Platform (Tracking, Registry, Drift, CI/CD) Difficulty: ⭐⭐⭐☆☆ (the data structures are simple; the state machine and lineage discipline are ⭐⭐⭐⭐⭐) Time: 3–4 hours
Every MLOps platform is, underneath the dashboards, two mechanisms: an experiment-tracking store that records what each training run did (params, metrics with full step history, tags, artifacts) and a model registry that turns the winning run into a versioned, staged, promotable, rollback-able model with lineage back to exactly what produced it. This lab builds both from first principles — a deterministic, in-memory MLflow — so the words run, param immutability, metric step history, best run, version, stage transition, archive-the-incumbent, and lineage stop being dashboard buttons and become code you own.
What you build
Run— one trial:run_id,experiment,params,metrics(key → ordered(step, value)history),tags,artifacts,status(RUNNING/FINISHED/FAILED),parent_run_idfor nested runs.TrackingStore— deterministic in-memory backend store:create_experiment,create_run(incrementingrun-0001ids — no uuid, no clock),log_param(immutable — re-logging a different value raises),log_metric(keeps the full step history),log_artifact/get_artifact,set_tag,end_run,get_run,search_runs(filter + order),best_run(max/min over a metric),compare_runs.ModelRegistry— versions, stages, lineage, backed by the store:register_model,create_model_version(auto-incrementing version bound to a run),transition_stagewith a real state machine over{"None","Staging","Production", "Archived"}(illegal edges rejected;archive_existing=Truedemotes the current Production version to Archived when you promote a new one),get_latest_versions,get_model_lineage(version → run → params/metrics),set_version_tag.
Key concepts
| Concept | What to understand |
|---|---|
| Run = unit of reproducibility | params + metrics + artifacts + code/data tags = "what produced this number" |
| Params are immutable | a run's config must be a faithful record; re-logging a different value is a bug, so it raises |
| Metrics keep step history | you store the whole learning curve, not just the last value — that's how best_run and plots work |
best_run is the sweep's payoff | model selection = pick the run that max/min's a metric; the soul of any HPO loop |
| Version auto-increments | a registered model is an append-only list of immutable versions (1, 2, 3…) |
| Stage state machine | None → Staging → Production → Archived; Archived can't jump straight to Production |
| Archive-the-incumbent | promoting v5 to Production while atomically demoting v4 keeps exactly one live model |
| Lineage | version → run → params/metrics: the answer to "what's in prod and what made it?" |
Files
| File | Purpose |
|---|---|
lab.py | skeleton with # TODO markers — your implementation |
solution.py | complete reference; python solution.py runs a worked example |
test_lab.py | the proof — run it red, make it green |
requirements.txt | pytest only (pure stdlib otherwise) |
Run
pip install -r requirements.txt
pytest test_lab.py -v # against your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # against the reference (must be green)
python solution.py # the worked example
Success criteria
- All tests pass against your implementation (and
LAB_MODULE=solution). - You can explain why params are immutable but tags and metrics are not — and why
test_relog_param_different_value_raisesis a correctness test, not pedantry. - You can explain why metrics keep the full
(step, value)history and whybest_runuses the latest value (test_best_run_uses_latest_value_not_first). - You can draw the stage state machine and say which edges are illegal and why Archived → Production directly is forbidden.
- You can explain what
archive_existing=Trueguarantees (exactly one Production version) and whytest_archive_existing_promotes_and_demotes_incumbentis the registry soul test. - You can trace
get_model_lineagefrom a version back to the params/metrics that produced it and say which 2 a.m. question that answers.
How this maps to the real stack
| The miniature | The production mechanism | The real API |
|---|---|---|
TrackingStore.create_run / log_param / log_metric / log_artifact | MLflow Tracking — runs, params, metrics, artifacts | mlflow.start_run(), mlflow.log_param, mlflow.log_metric(..., step=), mlflow.log_artifact |
| immutable params, metric step history | exactly MLflow's contract: params single-valued, metrics time-series | re-logging a different param value errors in MLflow too |
best_run / search_runs | run selection / leaderboards | MlflowClient().search_runs(..., order_by=["metrics.auc DESC"]) |
Run.parent_run_id (nested runs) | grouping a sweep's children under a parent run | mlflow.start_run(nested=True) |
ModelRegistry.register_model / create_model_version | the MLflow Model Registry | mlflow.register_model(model_uri, name) |
transition_stage + archive_existing | stage promotion with incumbent demotion | MlflowClient().transition_model_version_stage(name, version, "Production", archive_existing_versions=True) |
get_latest_versions(name, stage="Production") | "what model is live?" | MlflowClient().get_latest_versions(name, stages=["Production"]) |
get_model_lineage | version → run → params/metrics reproducibility | MlflowClient().get_model_version(name, v).run_id then get_run(run_id) |
inline artifacts dict | the artifact store (S3/GCS/local), separate from the backend store (DB) | MLflow splits --backend-store-uri from --default-artifact-root |
STAGES / _ALLOWED_TRANSITIONS | the registry lifecycle (newer MLflow uses aliases + tags instead of fixed stages) | set_registered_model_alias(name, "champion", version) |
Limits of the miniature (say these in the interview): it is in-memory and single-process — a
real tracking server has a persistent backend store (Postgres/MySQL/filesystem) plus a
separate artifact store for blobs, and concurrent writers need transactional guarantees we
don't model; our "lineage" is param/metric provenance only — true reproducibility also needs the
code commit and the data/feature version (Git + DVC/Delta), which Lab 02/03 lean on; the stage
state machine here is the classic 4-stage model, while MLflow ≥ 2.9 deprecates fixed stages in
favor of model aliases (@champion, @challenger) and tags for exactly the flexibility our
fixed edges lack; and a real registry adds access control / approvals / webhooks on
transitions (the human gate before Production) that we leave as an extension.
Extensions (build these on real MLflow / your own platform)
-
Run it on real MLflow. The same workflow, against a live server:
import mlflow from mlflow import MlflowClient mlflow.set_experiment("fraud-classifier") with mlflow.start_run() as run: # a Run, auto-ended on exit mlflow.log_param("lr", 0.05) # immutable for step, auc in enumerate([0.83, 0.88, 0.91]): mlflow.log_metric("val_auc", auc, step=step) # step history mlflow.log_artifact("confusion_matrix.png") mlflow.sklearn.log_model(model, "model") # writes an MLmodel flavor # register the winner and promote it, demoting the incumbent mv = mlflow.register_model(f"runs:/{run.info.run_id}/model", "fraud-model") MlflowClient().transition_model_version_stage( name="fraud-model", version=mv.version, stage="Production", archive_existing_versions=True, # archive-the-incumbent ) prod = MlflowClient().get_latest_versions("fraud-model", stages=["Production"])[0] src_run = MlflowClient().get_run(prod.run_id) # lineage: params/metrics that made prod -
Add a real backend/artifact split: persist runs to SQLite and artifacts to a local directory; show that
search_runsstill works and survives a restart. -
Swap stages for aliases: add
set_alias(name, alias, version)/get_by_aliasand a blue/greenchampion/challengerpattern (this is where MLflow is going). -
Add an approval gate: require a
set_version_tag(..., "approved_by", user)before a transition into Production is allowed — the human-in-the-loop release gate. -
Compare against W&B / Neptune: log the same sweep to one of them and note what their data model adds (system metrics, artifact lineage graphs, hosted UI) over this core.
Interview / resume
- Talking points: "Why are params immutable but metrics a time-series?" "Walk me through a
model promotion to Production and how you'd roll back." "What does a model registry give you
that a folder of
.pklfiles doesn't?" "How do you answer what model is in prod and what data trained it at 2 a.m.?" - Resume bullet: Built an MLflow-style experiment-tracking store and model registry — deterministic run tracking with immutable params and full metric step history, search/best-run selection, and a versioned registry with a stage state machine (Staging→Production→Archived), atomic promote-and-archive, and run-level lineage — mapping each mechanism to the production MLflow API.