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_id for nested runs.
  • TrackingStore — deterministic in-memory backend store: create_experiment, create_run (incrementing run-0001 ids — 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_stage with a real state machine over {"None","Staging","Production", "Archived"} (illegal edges rejected; archive_existing=True demotes 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

ConceptWhat to understand
Run = unit of reproducibilityparams + metrics + artifacts + code/data tags = "what produced this number"
Params are immutablea run's config must be a faithful record; re-logging a different value is a bug, so it raises
Metrics keep step historyyou store the whole learning curve, not just the last value — that's how best_run and plots work
best_run is the sweep's payoffmodel selection = pick the run that max/min's a metric; the soul of any HPO loop
Version auto-incrementsa registered model is an append-only list of immutable versions (1, 2, 3…)
Stage state machineNone → Staging → Production → Archived; Archived can't jump straight to Production
Archive-the-incumbentpromoting v5 to Production while atomically demoting v4 keeps exactly one live model
Lineageversion → run → params/metrics: the answer to "what's in prod and what made it?"

Files

FilePurpose
lab.pyskeleton with # TODO markers — your implementation
solution.pycomplete reference; python solution.py runs a worked example
test_lab.pythe proof — run it red, make it green
requirements.txtpytest 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_raises is a correctness test, not pedantry.
  • You can explain why metrics keep the full (step, value) history and why best_run uses 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=True guarantees (exactly one Production version) and why test_archive_existing_promotes_and_demotes_incumbent is the registry soul test.
  • You can trace get_model_lineage from 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 miniatureThe production mechanismThe real API
TrackingStore.create_run / log_param / log_metric / log_artifactMLflow Tracking — runs, params, metrics, artifactsmlflow.start_run(), mlflow.log_param, mlflow.log_metric(..., step=), mlflow.log_artifact
immutable params, metric step historyexactly MLflow's contract: params single-valued, metrics time-seriesre-logging a different param value errors in MLflow too
best_run / search_runsrun selection / leaderboardsMlflowClient().search_runs(..., order_by=["metrics.auc DESC"])
Run.parent_run_id (nested runs)grouping a sweep's children under a parent runmlflow.start_run(nested=True)
ModelRegistry.register_model / create_model_versionthe MLflow Model Registrymlflow.register_model(model_uri, name)
transition_stage + archive_existingstage promotion with incumbent demotionMlflowClient().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_lineageversion → run → params/metrics reproducibilityMlflowClient().get_model_version(name, v).run_id then get_run(run_id)
inline artifacts dictthe artifact store (S3/GCS/local), separate from the backend store (DB)MLflow splits --backend-store-uri from --default-artifact-root
STAGES / _ALLOWED_TRANSITIONSthe 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_runs still works and survives a restart.

  • Swap stages for aliases: add set_alias(name, alias, version) / get_by_alias and a blue/green champion/challenger pattern (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 .pkl files 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.