Module 6 — Model Registry and versions
Module 5 produced a winning model as a file in Cloud Storage. That is not enough to serve traffic responsibly. This module puts the file into the Model Registry — Vertex's catalogue of versioned model artifacts with their metadata, evaluation and lineage — so that module 7 can deploy it, and any teammate can answer "what is in production?" in one query.
Why a registry, and not just a file path
A trained model is only useful if you can answer three questions about it later: what data was it trained on, what code produced it, and how good was it on a held-out set. Storing the file at gs://.../models/final.bst and calling it done erases all three answers by the next training run.
The Model Registry solves this by wrapping the file into a Model resource with:
- A version (auto-incrementing when uploaded to the same parent).
- Aliases (movable labels like
default,production,champion). - Attached metadata: training job that produced it, dataset URIs, evaluation metrics, dataset schema.
- A serving container already declared — deployment in module 7 needs only an endpoint.
A model in the registry is also the object that appears in the lineage graph of module 9: pipelines that use it, endpoints that serve it, batch jobs that call it, all connect back through this one resource.
The upload, done properly
There are three routes to get a model into the registry:
- Return it from
job.run()on aCustomTrainingJob(what module 4 already did — the model is uploaded automatically). - Call
aiplatform.Model.upload(...)directly, when the file was produced elsewhere. - Ingest it from a Model Garden fine-tune (module 10).
The direct upload is worth seeing explicitly, because production uploads (from CI, from a KFP pipeline) look like this rather than like the notebook path:
from google.cloud import aiplatform
aiplatform.init(project="fraud-detection-dev", location="europe-west1")
model = aiplatform.Model.upload(
display_name="fraud-xgb",
parent_model="projects/…/models/1234567890", # None on the first upload
artifact_uri="gs://fraud-detection-dev-vertex-eu/artifacts/fraud-xgb/v7/",
serving_container_image_uri=(
"europe-docker.pkg.dev/vertex-ai/prediction/xgboost-cpu.1-7:latest"
),
serving_container_predict_route="/predict",
serving_container_health_route="/health",
is_default_version=True,
version_aliases=["challenger"],
version_description="Vizier trial 47, aucpr=0.86 on 2026-06 validation split.",
labels={"env": "dev", "team": "fraud", "framework": "xgboost"},
)
Two rules that pay off two months later.
Always set parent_model starting from the second upload. Without it, Vertex creates a new model rather than a new version of the existing one, and the console fills with fraud-xgb, fraud-xgb-v2, fraud-xgb-final, fraud-xgb-final-final. Every experiment that shares a prediction target belongs under the same parent_model.
Write a version_description that is useful in a hurry. "Trained by Alice" is not; "Vizier trial 47, aucpr=0.86 on 2026-06 validation split" tells the on-call engineer at 3 a.m. which version is which.
Versions and aliases: which one moves
Versions are immutable: version 7 is version 7 forever, with its artifact URI and its metadata frozen. Aliases are movable pointers: default might point at version 5 today and version 7 tomorrow.
The convention that scales to a team is:
| Alias | Meaning | Moved by |
|---|---|---|
default | The version served by an endpoint that does not name a version | Model.update_version(..., version_aliases=["default"]) |
champion | The version currently serving production traffic | Deployment pipeline (module 9) |
challenger | A candidate under evaluation | A/B test workflow |
previous | The prior champion, kept for rollback | Rotated by the same pipeline |
An endpoint deployment in module 7 that reads model_version="default" benefits from this indirection: promoting a new version is one alias move away, and no endpoint config needs to change.
# Promote v7 to champion, kick v5 back to previous
model.update_version(version="7", version_aliases=["champion"])
model.update_version(version="5", version_aliases=["previous"])
default, latest and any string starting with a digit are reserved or rejected. Team-defined aliases (champion, challenger, holdout-2026-q3) are fine. Attempting to set an invalid alias returns a 400 Bad Request from the API — worth catching in a CI script rather than discovering during a release.
Attaching evaluation to a version
An unlabelled model version is a "trust me" model. Attaching an evaluation makes the numbers auditable in the console and readable by downstream jobs.
from google.cloud.aiplatform import model_evaluation
evaluation = model.upload_evaluation(
display_name="2026-06 validation split",
metric_type="classification",
metrics={
"auRoc": 0.972,
"auPrc": 0.861, # the metric that actually matters here
"logLoss": 0.086,
},
prediction_type="classification",
ground_truth_column="is_fraud",
slice_dimensions=["merchant_category"], # per-category metrics if provided
)
For a class-imbalanced problem like fraud (0.2% positives), PR-AUC is the number to defend, not ROC-AUC — a point module 7 comes back to when choosing the decision threshold. The slice_dimensions field enables per-category evaluation, which surfaces the "great on average, awful on food-delivery merchants" pattern that averages hide.
Lineage: how the registry connects to everything else
Vertex's metadata store records lineage automatically whenever the SDK is involved. A model uploaded by a CustomTrainingJob gets edges to:
- The training job that produced it (with its args, machine type, container tag).
- The dataset URIs the job read (whether GCS or BigQuery).
- Any pipeline run it participated in (module 9).
- Any endpoint it is deployed to and any batch job that called it.
In the Model Registry UI, the Lineage tab draws this as a graph. In code, the same information is available via aiplatform.Metadata:
for e in aiplatform.Execution.list(
filter=f'metadata.output_model="{model.resource_name}"'):
print(e.display_name, e.metadata)
The value of lineage compounds. Answering "which training data produced version 7?" or "which endpoints are on a version older than three months?" becomes a query rather than an archaeological dig through Slack and old scripts.
The pattern of a healthy registry
Three habits keep a Model Registry useful over a year:
- One model per prediction target. All fraud models live under
fraud-xgb, tuning experiments and rewrites included. Not one per author, not one per week. - Aliases are named after roles, not versions.
championmoves;v7does not. Endpoints refer to roles. - Evaluation attached before promotion. A version that has no evaluation cannot become
champion. This one rule prevents the majority of "we shipped the wrong model" incidents.
In summary
- The Model Registry turns a file in Cloud Storage into a versioned, catalogued resource with evaluation, lineage and a declared serving container.
- Use
parent_modelon every upload after the first to accumulate versions under one model; use aliases (champion,challenger,previous) to represent roles that move. - Attach evaluation with the metric that matches the problem — PR-AUC for the class-imbalanced fraud task — and slice it by natural dimensions like merchant category.
- Lineage is automatic when the SDK is used end to end; that graph turns "who trained this model on what data" into a one-line query.
Next module: exposing versions of the model to traffic — endpoints, a 90/10 split between two versions, replica autoscaling and request logging.