Skip to content
intermediate Phase 17 · MLOps: Packaging, Serving, and Monitoring

Model Registry and Versioning

Track model versions, promotion stages, and their lineage metadata.

50m
0 problems
Topic Progress 0%

Three Candidates, One Champion: Registry Stages and the Promotion Policy That Enforces Them

A retrain finished overnight and improved AUC by 0.004. Two more candidates from last week sit in a models/ folder on the training box named xgb_v3_final_FINAL2.pkl. Nobody knows which file the serving pods load, and last month's rollback took ninety minutes of archaeology. A model registry fixes this by making promotion a tracked state transition instead of a file copy — and the first thing to design is not the tool, it is the policy.

The vocabulary, using MLflow (the de-facto standard; SageMaker Model Registry and Vertex AI Model Registry express the same states):

Stage Meaning Who writes it Who reads it
None Registered but unvetted training job (auto) nobody
Staging Candidate under evaluation training job or author eval harness, QA
Production Serving traffic reads this CI/CD gate only serving pods
Archived Retired, kept for lineage/rollback promotion or decay policy audits, rollback

Registering a run's model is one call at logging time:

import mlflow
from mlflow.tracking import MlflowClient

mlflow.set_experiment('churn-classifier')
with mlflow.start_run(run_name='xgb-2025-10-14') as run:
    mlflow.log_params({'max_depth': 6, 'lr': 0.05, 'n_estimators': 400})
    mlflow.log_metrics({'auc': 0.9132, 'logloss': 0.318})
    mlflow.log_tag('git_sha', git_sha())
    mlflow.log_artifact('feature_schema.json')
    info = mlflow.sklearn.log_model(model, artifact_path='model',
                                    registered_model_name='churn-classifier')
    run_id = run.info.run_id

client = MlflowClient()
client.transition_model_version_stage(
    name='churn-classifier', version=4, stage='Staging')

Newer MLflow steers toward aliases instead of stages — a named pointer that does not occupy a lifecycle slot:

client.set_registered_model_alias('churn-classifier', 'champion', version=4)
client.set_registered_model_alias('churn-classifier', 'challenger', version=5)
# serving never loads a version number:
model = mlflow.pyfunc.load_model('models:/churn-classifier@champion')

Serving must resolve by alias, never by hardcoded version. The consequence is your rollback story: a bad release is fixed by pointing champion back to the previous version — seconds, no redeploy, no archaeology. An alias is mutable; a version is immutable. That pairing (append-only versions, mutable pointers) is exactly the design Git uses, and it is why models:/churn-classifier@champion is the only model reference that belongs in serving code.

The promotion policy is where interviews separate people who used a registry from people who designed one. Four rules, each with a reason:

  1. Only automation may move a model into Production. Humans (and training jobs) may register and stage; the gate that flips champion is the CI job that re-ran the evaluation harness and compared against the incumbent. This makes promotion reproducible and auditable — the same reason only bots push to main.
  2. Promotion requires beating the incumbent on a fixed eval suite, not just on a training metric: golden datasets, slice metrics (the fraud class you cannot afford to lose), and fairness/regression checks. A +0.004 average AUC that costs −0.03 on your priority segment is a loss.
  3. Never two producers of truth. transition_model_version_stage(..., archive_existing_versions=True) archives whatever was in Production so exactly one version holds the stage (or one alias value per name). Drift between "the Production model" and "the model actually serving" is the failure this prevents.
  4. Every transition carries a note and an actor — who, when, why, tied to a ticket or run id. client.set_model_version_tag(v, 'approval', 'PR-482') is cheap, and the audit trail is the deliverable your future self and your auditors share.

What the registry deliberately does not do: hold training code, retrain models, or validate them. It holds versions of artifacts with metadata and a lineage pointer. The version numbers themselves need a semantics — when does a new version merit a new major number, versus a patch — which is the next chapter's question.

Is the New Model a Major, Minor, or Patch? Version Semantics for Artifacts

The API team asks whether they can deploy model version 7. Without a versioning contract, the answer is a shrug — and a shrug means every consumer must read your release notes to know whether their client code still works. Models are not source code, but their version numbers carry the same question: what did this change break for whoever depends on the artifact?

Borrow semantic versioning and re-define it for models, in terms of the inference contract (feature schema: names, types, order, ranges) and behavior:

Bump Trigger for a model Consumer impact Example
MAJOR (x) Inference contract breaks clients must change before upgrading features added/removed, dtype change, categorical encoding change, output meaning change (probability → score)
MINOR (x.y) New model, same contract drop-in; metrics improve scheduled retrain on fresh data, same features and target
PATCH (x.y.z) No behavior change at all none metadata/description fix, re-export to a new format, docstring, model-card update

Concrete cases that decide real debates:

  • Retrained nightly on the same pipeline, AUC 0.9132 → 0.9151: MINOR. The contract is identical; consumers upgrade by flipping the alias.
  • Added a device_type feature so the model can score new rows: MAJOR. Old clients do not send it; loading this model behind their requests produces silent wrongness, which is exactly what a major bump exists to prevent.
  • Re-exported the same weights from joblib to ONNX with verified parity (the golden-batch check from Model Serialization and Packaging): PATCH — same behavior, different bytes.
  • Changed the positive class from churned_30d to churned_7d — output means something else now: MAJOR, even though every input feature is unchanged.

Two alternative schemes deserve knowing because you will meet them. Calendar versioning (2025-10-14) suits scheduled retrains: the number states provenance rather than compatibility, and it works only while retrains never break contracts. Content-addressed versions (sha256 of the artifact, or a runs:/ URI) give absolute immutability — no ambiguity about which bytes a name refers to — and are ideal as the underlying identity over which human-readable aliases float.

Whatever the top-level scheme, two invariants hold underneath it:

  1. Registry versions are immutable and append-only. If the bytes at version 4 change, every historical claim ("version 4 served in Q2 with AUC 0.9132") becomes a lie. Fix forward with a new version; never overwrite an artifact — MLflow does not let you, and that constraint is a feature of the design, not a limitation.
  2. The version number is not the deployment handle. Consumers pin an alias (champion) or a stage; versions record history. Code that hardcodes models:/churn-classifier/4 couples a deployable service to a run of numbers and bypasses the alias layer that made rollback cheap.

The mapping between schemes and systems, since teams routinely mix them:

git commit sha        -> code that produced the model     (via run tags)
data snapshot hash    -> the training data                (DVC / lakehouse version)
run id                -> hyperparameters, metrics, logs   (experiment tracker)
registry version      -> the artifact + stage/alias       (model registry)
semantic tag          -> what changed for consumers       (your convention)

That five-line ledger is the whole answer to "how do you version ML models?" — each layer names a different dependency, and a registry entry that links all five is what makes the next chapter's lineage questions answerable.

Audit Question: Which Run, Which Data, Which Metrics Produced This Model?

Eight months from now someone asks the only questions that matter: the Production model is degrading — which training run produced it, on what data snapshot, with which hyperparameters, and what were its acceptance metrics when it shipped? If those answers require searching Slack, the registry is decoration. A registry entry is only as good as the lineage it carries, and lineage is something you attach at training time, not something you reconstruct during an incident.

The minimum viable lineage per registered version:

with mlflow.start_run() as run:
    mlflow.log_param('max_depth', 6)
    mlflow.log_metric('auc', 0.9132)
    mlflow.log_tag('git_sha', subprocess.check_output(
        ['git', 'rev-parse', 'HEAD']).decode().strip())
    mlflow.log_tag('data_snapshot', 'lake://churn_features@2025-10-13')
    mlflow.log_artifact('feature_schema.json')     # the inference contract
    mlflow.sklearn.log_model(pipe, artifact_path='model',
                             registered_model_name='churn-classifier',
                             signature=mlflow.models.infer_signature(X_demo, preds),
                             input_example=X_demo.head(3))

# later, during an incident — one call from the running model's metadata:
version = client.get_model_version('churn-classifier', '4')
run = mlflow.get_run(version.run_id)
print(run.data.params, run.data.metrics, run.data.tags['git_sha'])
print(version.source)            # runs:/<run_id>/model — the artifact's exact bytes

Read that round trip as the acceptance test for your registry setup: from a version number alone you can name the run, the code revision, the data snapshot, the schema, and the bytes (runs:/<run_id>/model — immutable, referenced by URI rather than copied). signature and input_example matter more than they look: they make the registry self-describing, and MLflow serving validates incoming requests against the logged signature, turning a schema mismatch into an explicit error instead of a silent bad prediction.

The lifecycle of an entry, and the hygiene rules that keep the system trustworthy:

Event Rule
Training finishes register the model automatically with run id, sha, data tag — no manual uploads
Enters Staging eval harness writes its report as an artifact and tags the version
Promotes to champion CI re-runs the harness against the incumbent; only CI writes the alias, with a PR/ticket tag
Served serving logs the resolved version per prediction — you must be able to say which model answered last Tuesday
Superseded old alias value overwritten; versions never deleted, older stages archived
Incident champion alias re-pointed to the previous version — rollback is a pointer move

Three failure modes the rules counter directly. Manual artifact uploads ("I'll register the file from my laptop") sever the run-to-version link — the artifact is now unattributable, and the next person cannot reproduce it. A latest-style alias that floats to whatever was registered most recently means a retrain by one engineer silently becomes everyone's Production candidate; aliases must be explicit, deliberate pointers. And evaluating at Staging but not re-evaluating at promotion lets a model that failed a new data slice slip through, because the world moved between staging and promotion — the gate must run against current data.

For the interview, compress the whole topic into the sentence a senior engineer wants to hear: every model version is an immutable artifact linked to the run that produced it, the data snapshot it saw, the code SHA that trained it, and the metrics that justified its stage — and serving resolves models by alias so promotion and rollback are pointer moves, audited by the same CI that gates the release. With versions tracked and lineage attached, the remaining question is operational: how the chosen version gets in front of requests with a latency budget — the subject of Online and Batch Serving.

Quiz

1. Why should serving code load models via models:/churn-classifier@champion instead of models:/churn-classifier/4?

Question 1 options

2. A new model version adds a device_type feature to the input schema but leaves all other features and the target unchanged. What version bump does the contract-based scheme demand?

Question 2 options

3. An engineer registers a model by uploading model.pkl from their laptop instead of logging it from the training run. What is the core damage?

Question 3 options

4. What does the transition policy require before a model moves into Production (the champion alias)?

Question 4 options

Flashcards

Question

What do the registry stages None/Staging/Production/Archived mean, and how do MLflow aliases relate to them?

Answer

None: registered, unvetted. Staging: under evaluation by the harness. Production: the version serving traffic. Archived: retired but retained for lineage and rollback. Aliases (champion/challenger) are newer, movable named pointers that express the same roles without occupying lifecycle slots — often preferred because several aliases can coexist and alias transitions are lightweight and unambiguous.

Question

State the four rules of a model promotion policy and the reason for each.

Answer

1) Only automation moves models into Production — reproducible and auditable. 2) Promotion must beat the incumbent on a fixed evaluation suite including slice/regression checks — a small average gain can hide a segment loss. 3) Never two producers of truth — archive the previous Production version so exactly one holds the stage/alias. 4) Every transition records actor, reason, and ticket/run — the audit trail is the deliverable.

Question

Apply the model semver contract to three cases: nightly retrain (AUC 0.913 -> 0.915); added one input feature; same weights re-exported to ONNX with verified parity.

Answer

Nightly retrain: MINOR — same inference contract, better metrics, drop-in for consumers. Added feature: MAJOR — old clients no longer match the schema and would silently degrade. Re-export with parity: PATCH — same behavior in new bytes, no consumer impact. The axis is always the consumer's contract, never the metric delta.

Question

What five identifiers form complete model lineage, and which system owns each?

Answer

Git commit SHA → the training code (tagged on the run); data snapshot hash/URI → the training data (lakehouse version or DVC); run id → hyperparameters, metrics, logs (experiment tracker); registry version → the immutable artifact plus stage/alias (model registry); semantic tag (major/minor/patch) → what changed for consumers (team convention). A registry version linking all five makes any incident question answerable in one call.

Question

Why log a model signature and input example at registration time?

Answer

They make the registry entry self-describing: consumers see the expected feature names, types, and shapes without reading code, and MLflow serving validates incoming requests against the signature — a schema mismatch becomes an explicit error instead of a silently wrong prediction. The input example also documents the preprocessing contract alongside the artifact it applies to.

Revision Notes

Key Takeaways

  • 1. The registry tracks versions, stages, and metadata — and only automation may promote into Production, so every promotion is reproducible and auditable
  • 2. Serving resolves models by alias (champion), making promotion and rollback pointer moves; hardcoded version numbers in service code bypass the safety layer
  • 3. Model semver is judged by the inference contract: broken schema = MAJOR, retrain with same schema = MINOR, no behavior change = PATCH
  • 4. Lineage is captured at registration time — run id, git SHA, data snapshot, signature — because it cannot be reconstructed during an incident
  • 5. Versions are immutable and append-only; a registry entry whose artifact can be replaced makes every historical metric claim meaningless

Interview Tips

  • • "How do you roll back a bad model?" — the serving path resolves the champion alias, so rollback is repointing the alias to the previous version, seconds with no redeploy; add that the previous version was never deleted and the eval report justified its original promotion
  • • "How do you decide major vs minor for a model release?" — name the axis (the consumer's inference contract), give one example of each bump, and mention that metric improvements alone never justify a major
  • • "What belongs in a registry entry?" — the artifact (immutable), stage/alias, run id linking params/metrics/logs, git SHA, data snapshot, signature and input example, plus who promoted it and why; state that all of it is attached at training time
  • • "How do you know the Production model is the one you approved?" — alias-based resolution plus per-request logging of the resolved version, and a CI gate as the only writer of the champion pointer; this closes the gap between what is registered and what is serving

Cheat Sheet

Model Registry and Versioning

Stages / aliases:

Stage Meaning Written by
None registered, unvetted training job
Staging under evaluation author / job
Production serving traffic CI gate only
Archived retired, kept for lineage/rollback promotion or policy
mlflow.sklearn.log_model(pipe, artifact_path='model',
                         registered_model_name='churn-classifier',
                         signature=sig, input_example=X_demo.head(3))
client.transition_model_version_stage('churn-classifier', 4, 'Staging')
client.set_registered_model_alias('churn-classifier', 'champion', 4)
model = mlflow.pyfunc.load_model('models:/churn-classifier@champion')  # serving path

Promotion policy: CI-only into Production · beat the incumbent on a fixed eval suite (slices + regressions) · archive_existing_versions=True / one alias value · every transition tagged with actor, reason, ticket.

Version contract (axis = consumer's inference contract):

  • MAJOR: feature add/remove/type change, output meaning change
  • MINOR: same contract, new retrain, better metrics
  • PATCH: no behavior change (metadata, re-export with verified parity)

Registry versions are immutable and append-only; aliases are the mutable pointers serving resolves. Rollback = re-point the alias.

Lineage per version: git SHA + data snapshot URI + run id (params/metrics/artifacts) + signature/input example + semantic tag — attached at training time, verified by reading it back from the version alone.

Continue Learning