LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 158 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/23
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BMixed Precision TrainingModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Production Agent
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/16
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight InterpolationPrompt Optimization with DSPyRecursive Language Models (RLM)
🤖Advanced Agents & Retrieval0/14
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryMulti-Agent Orchestration
⚡Inference & Production Scale0/20
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementContext EngineeringMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning & Test-Time Compute
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
Back to Topics
LearnProduction ML SystemsMonitoring Predictive Models
⚙️MediumMLOps & Deployment

Monitoring Predictive Models

Monitor predictive models from feature freshness through delayed labels, then gate retraining, promotion, and rollback.

16 min read
Learning path
Step 47 of 158 in the full curriculum
Forecasting and Anomaly DetectionThe Bitter Lesson & Compute

A deployed model isn't done when its endpoint responds. A system that scores job-SLA risk, ranks alerts, or forecasts runner-pool load can still fail after it reaches live traffic: inputs break now, labels arrive days later, and a new model can regress under real usage.

Monitoring answers separate questions on separate clocks: can this request be scored safely, has traffic changed, did eventual outcomes remain good enough, and should a candidate replace the current release?

Predictive-model monitoring dashboard separating request-time safety, delayed outcome quality, and reversible release rollback. Predictive-model monitoring dashboard separating request-time safety, delayed outcome quality, and reversible release rollback.
Request-time checks score 4/6 safe rows and expose parity and drift problems before labels arrive. Later quality uses 5/5 label coverage in the mature cohort, and delayed review cost 8 > 5 proves the rollback pointer restores v3.

Start with a served release and feature traces

Use a job-SLA model as the running example. Each job request carries two model inputs:

FeatureMeaningWhy it can fail
queue_backlogjobs waiting in the runner poolfeed can become missing or use wrong units
hours_since_last_heartbeatage of latest scheduler heartbeatheartbeat pipeline can become stale

Store a feature trace with each prediction. A feature trace records the values and versions that produced one score. It lets an engineer reproduce the decision after traffic, code, or source data changes.

Build six incoming requests against deployed release job-risk-v3. Two rows contain failures that should be caught before delayed SLA labels exist.

build-live-feature-traces.py
1from datetime import datetime, timedelta, timezone 2from hashlib import sha256 3from json import dumps 4from math import log 5 6now = datetime(2026, 2, 4, 12, 0, tzinfo=timezone.utc) 7deployed = { 8 "model": "job-risk-v3", 9 "feature_contract": "features-v3", 10 "threshold": 0.50, 11} 12requests = [ 13 {"job_id": "s-100", "runner_pool": "fc-a", "queue_backlog": 18, "hours_since_last_heartbeat": 2, "feature_as_of": now - timedelta(minutes=10), "risk_score": 0.18}, 14 {"job_id": "s-101", "runner_pool": "fc-a", "queue_backlog": 27, "hours_since_last_heartbeat": 8, "feature_as_of": now - timedelta(minutes=20), "risk_score": 0.44}, 15 {"job_id": "s-102", "runner_pool": "fc-b", "queue_backlog": 35, "hours_since_last_heartbeat": 20, "feature_as_of": now - timedelta(minutes=25), "risk_score": 0.76}, 16 {"job_id": "s-103", "runner_pool": "fc-b", "queue_backlog": None, "hours_since_last_heartbeat": 26, "feature_as_of": now - timedelta(minutes=15), "risk_score": None}, 17 {"job_id": "s-104", "runner_pool": "fc-a", "queue_backlog": 22, "hours_since_last_heartbeat": 6, "feature_as_of": now - timedelta(minutes=180), "risk_score": 0.52}, 18 {"job_id": "s-105", "runner_pool": "fc-b", "queue_backlog": 40, "hours_since_last_heartbeat": 30, "feature_as_of": now - timedelta(minutes=5), "risk_score": 0.91}, 19] 20feature_traces = [ 21 { 22 **request, 23 "model": deployed["model"], 24 "feature_contract": deployed["feature_contract"], 25 } 26 for request in requests 27] 28 29print("deployed:", deployed["model"]) 30print("feature contract:", deployed["feature_contract"]) 31print("feature traces:", len(feature_traces)) 32print("label status: pending")
Output
1deployed: job-risk-v3 2feature contract: features-v3 3feature traces: 6 4label status: pending

At request time, you don't know which jobs will miss their SLA. You do know whether required inputs exist and whether their timestamps are fresh enough to trust. Each trace binds those values to the model and feature-contract versions that consumed them.

Block invalid inputs before waiting for labels

Define two request-time invariants:

  1. Required features can't be missing.
  2. Feature snapshots can't be older than 60 minutes.

An invariant is a condition that must stay true for the system to operate safely. Check each trace and block unsafe rows instead of sending a plausible-looking score downstream.

check-request-time-input-health.py
1required_features = ["queue_backlog", "hours_since_last_heartbeat"] 2max_feature_age_minutes = 60 3 4def request_health(row): 5 missing = [ 6 name 7 for name in required_features 8 if row[name] is None 9 ] 10 feature_age = int((now - row["feature_as_of"]).total_seconds() / 60) 11 reasons = [] 12 if missing: 13 reasons.append("missing=" + ",".join(missing)) 14 if feature_age > max_feature_age_minutes: 15 reasons.append(f"stale={feature_age}m") 16 return reasons 17 18healthy_requests = [] 19for row in feature_traces: 20 reasons = request_health(row) 21 if reasons: 22 print(row["job_id"], "-> block", ";".join(reasons)) 23 else: 24 healthy_requests.append(row) 25 print(row["job_id"], "-> score") 26 27print("scored:", len(healthy_requests)) 28print("blocked:", len(feature_traces) - len(healthy_requests))
Output
1s-100 -> score 2s-101 -> score 3s-102 -> score 4s-103 -> block missing=queue_backlog 5s-104 -> block stale=180m 6s-105 -> score 7scored: 4 8blocked: 2

Job s-103 lacks backlog data. Job s-104 carries a three-hour-old snapshot. A label-based accuracy report would discover the unsafe scoring path too late.

Sculley et al. describe unstable data dependencies, input-data testing, and live monitoring as central production ML concerns.[1]Reference 1Hidden Technical Debt in Machine Learning Systems.https://research.google/pubs/hidden-technical-debt-in-machine-learning-systems/ Their point is practical: models depend on surrounding data systems, not model code alone.

Measure drift without calling it failure

Healthy rows can still look different from training traffic. Data drift means an input distribution changed over time. It's a reason to inspect, not proof that accuracy fell.[2]Reference 2Monitoring Machine Learning Models in Production.https://arxiv.org/abs/2007.06299

Compare the historical and current distributions for hours_since_last_heartbeat:

BucketTraining windowCurrent windowChange
0-4h50%30%-20 points
4-12h30%25%-5 points
12-24h15%25%+10 points
24h+5%20%+15 points

Older heartbeats are more common now. A scheduler outage could cause that shift, but so could a holiday or runner-pool mix change.

This lab summarizes the bucket changes with Population Stability Index (PSI):

PSI=∑i(ai−ei)log⁡(aiei)\text{PSI} = \sum_i (a_i - e_i)\log\left(\frac{a_i}{e_i}\right)PSI=i∑​(ai​−ei​)log(ei​ai​​)

Here eie_iei​ is the historical share for bucket iii, and aia_iai​ is its current share. This exercise uses non-zero shares in every bucket so the logarithm is defined. Real monitoring code must choose an explicit policy for empty buckets.

Calculate PSI and apply a local investigation threshold of 0.20.

calculate-input-drift-diagnostic.py
1bucket_names = ["0-4h", "4-12h", "12-24h", "24h+"] 2reference_counts = [50, 30, 15, 5] 3current_counts = [30, 25, 25, 20] 4 5def normalize(counts): 6 total = sum(counts) 7 return [count / total for count in counts] 8 9def population_stability_index(reference, current): 10 return sum( 11 (actual - expected) * log(actual / expected) 12 for expected, actual in zip(reference, current) 13 ) 14 15reference = normalize(reference_counts) 16current = normalize(current_counts) 17psi = population_stability_index(reference, current) 18investigate_at = 0.20 19 20for name, before, after in zip(bucket_names, reference, current): 21 print(f"{name}: reference={before:.2f} current={after:.2f} delta={after - before:+.2f}") 22print("PSI:", round(psi, 3)) 23print("local action:", "inspect shift" if psi >= investigate_at else "continue")
Output
10-4h: reference=0.50 current=0.30 delta=-0.20 24-12h: reference=0.30 current=0.25 delta=-0.05 312-24h: reference=0.15 current=0.25 delta=+0.10 424h+: reference=0.05 current=0.20 delta=+0.15 5PSI: 0.37 6local action: inspect shift

PSI is a compact diagnostic here, not a universal truth threshold. The output says heartbeat age changed enough to inspect. It doesn't say release v3 is inaccurate, and it doesn't say retraining is the first fix.

Reproduce training-serving skew

Training-serving skew means production computes an input differently from training. Suppose training divided backlog count by staffed runner-pool capacity, while a serving change divides by a fixed 100.

Probe the same raw row through both transformations.

reproduce-training-serving-skew.py
1probe = {"queue_backlog": 20, "staffed_capacity": 50} 2 3def training_transform(row): 4 return round(row["queue_backlog"] / row["staffed_capacity"], 3) 5 6def buggy_serving_transform(row): 7 return round(row["queue_backlog"] / 100, 3) 8 9training_value = training_transform(probe) 10serving_value = buggy_serving_transform(probe) 11print("training backlog ratio:", training_value) 12print("serving backlog ratio:", serving_value) 13print("parity:", training_value == serving_value)
Output
1training backlog ratio: 0.4 2serving backlog ratio: 0.2 3parity: False

The same runner pool becomes 0.4 in training and 0.2 online. A model retrained on the existing training function won't repair that serving bug. Fix the shared transformation contract and keep a parity test in the release gate.

Google Cloud's MLOps guidance includes unit tests for feature engineering, prediction-service tests with expected inputs, data validation, and predictive-performance validation before deployment.[3]Reference 3MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning

Join delayed labels back to stored predictions

Request-time checks move fast because they don't need outcomes. Accuracy moves slower. An SLA-miss label may arrive only after the promised job window closes.

Store model ID, score, job ID, decision context, and the time when each outcome becomes mature. Later, join outcomes by immutable prediction ID. Compute quality on cohorts whose outcome windows have closed, and report label coverage inside those cohorts. Labels that happen to arrive early can be a biased subset; a missing label in a mature cohort is a coverage gap, not an on-schedule outcome.

join-delayed-job-SLA-labels.py
1predictions = [ 2 {"prediction_id": "p-001", "job_id": "s-200", "risk_score": 0.15, "priority_job": False, "label_due_at": now - timedelta(hours=5)}, 3 {"prediction_id": "p-002", "job_id": "s-201", "risk_score": 0.32, "priority_job": False, "label_due_at": now - timedelta(hours=4)}, 4 {"prediction_id": "p-003", "job_id": "s-202", "risk_score": 0.68, "priority_job": True, "label_due_at": now - timedelta(hours=3)}, 5 {"prediction_id": "p-004", "job_id": "s-203", "risk_score": 0.78, "priority_job": False, "label_due_at": now - timedelta(hours=2)}, 6 {"prediction_id": "p-005", "job_id": "s-204", "risk_score": 0.91, "priority_job": True, "label_due_at": now - timedelta(hours=1)}, 7 {"prediction_id": "p-006", "job_id": "s-205", "risk_score": 0.84, "priority_job": True, "label_due_at": now + timedelta(hours=1)}, 8] 9decision_policy = "missed-sla-threshold-050-v1" 10predictions = [ 11 { 12 **prediction, 13 "model": deployed["model"], 14 "feature_contract": deployed["feature_contract"], 15 "decision_policy": decision_policy, 16 "decision_threshold": deployed["threshold"], 17 } 18 for prediction in predictions 19] 20labels = { 21 "p-001": False, 22 "p-002": False, 23 "p-003": True, 24 "p-004": False, 25 "p-005": True, 26} 27 28matured = [prediction for prediction in predictions if prediction["label_due_at"] <= now] 29immature = [prediction for prediction in predictions if prediction["label_due_at"] > now] 30matured_labeled = [] 31missing_matured_labels = [] 32for prediction in matured: 33 prediction_id = prediction["prediction_id"] 34 if prediction_id in labels: 35 matured_labeled.append({**prediction, "missed_sla": labels[prediction_id]}) 36 else: 37 missing_matured_labels.append(prediction_id) 38 39label_coverage = len(matured_labeled) / len(matured) if matured else 0.0 40 41print("stored predictions:", len(predictions)) 42print("matured predictions:", len(matured)) 43print("joined matured labels:", len(matured_labeled)) 44print(f"matured label coverage: {label_coverage:.3f}") 45print("missing matured labels:", missing_matured_labels) 46print("immature predictions:", [row["prediction_id"] for row in immature])
Output
1stored predictions: 6 2matured predictions: 5 3joined matured labels: 5 4matured label coverage: 1.000 5missing matured labels: [] 6immature predictions: ['p-006']

Prediction p-006 is excluded because its SLA outcome window hasn't closed. Label arrival doesn't define cohort maturity. The five-row mature cohort has 100% label coverage. If a mature row were still unlabeled, the report would expose the coverage gap and withhold the quality decision rather than assume the observed subset is representative.

Evaluate decisions and calibration

A binary decision uses deployed threshold 0.50: scores at or above the threshold enter job-SLA review. Require complete label coverage for this mature lab cohort, then compute precision, recall, priority-job misses, and a small review-cost receipt.

measure-delayed-quality-window.py
1threshold = deployed["threshold"] 2 3if missing_matured_labels: 4 raise RuntimeError("quality metrics unavailable: mature cohort has missing labels") 5 6def predicted_missed_sla(row): 7 return row["risk_score"] >= threshold 8 9true_positives = sum(predicted_missed_sla(row) and row["missed_sla"] for row in matured_labeled) 10false_positives = sum(predicted_missed_sla(row) and not row["missed_sla"] for row in matured_labeled) 11false_negatives = sum(not predicted_missed_sla(row) and row["missed_sla"] for row in matured_labeled) 12 13def rate_or_none(numerator, denominator): 14 return round(numerator / denominator, 3) if denominator else None 15 16precision = rate_or_none(true_positives, true_positives + false_positives) 17recall = rate_or_none(true_positives, true_positives + false_negatives) 18missed_priority_job = sum( 19 row["priority_job"] and row["missed_sla"] and not predicted_missed_sla(row) 20 for row in matured_labeled 21) 22review_cost = false_positives * 2 + false_negatives * 10 23 24print("precision:", precision) 25print("recall:", recall) 26print("missed priority jobs:", missed_priority_job) 27print("review cost:", review_cost)
Output
1precision: 0.667 2recall: 1.0 3missed priority jobs: 0 4review cost: 2

The local cost policy assigns 2 units to an unnecessary review and 10 to a missed SLA. It's an example decision policy, not a universal business value. If a denominator is zero, report None: missing evidence isn't the same as measured failure.

Scores also need a calibration check. A score near 0.80 shouldn't be interpreted as an 80% risk unless outcomes from adequately covered mature cohorts support that use.[4]Reference 4On Calibration of Modern Neural Networkshttps://arxiv.org/abs/1706.04599 Compare average score with observed job-SLA rate inside two tiny buckets.

measure-risk-score-calibration.py
1score_buckets = [ 2 ("low", [row for row in matured_labeled if row["risk_score"] < 0.50]), 3 ("high", [row for row in matured_labeled if row["risk_score"] >= 0.50]), 4] 5 6for name, rows_in_bucket in score_buckets: 7 if not rows_in_bucket: 8 print(f"{name}: n=0 average_score=None observed_missed_sla_rate=None") 9 continue 10 average_score = sum(row["risk_score"] for row in rows_in_bucket) / len(rows_in_bucket) 11 observed_rate = sum(row["missed_sla"] for row in rows_in_bucket) / len(rows_in_bucket) 12 print( 13 f"{name}: n={len(rows_in_bucket)} " 14 f"average_score={average_score:.3f} " 15 f"observed_missed_sla_rate={observed_rate:.3f}" 16 )
Output
1low: n=2 average_score=0.235 observed_missed_sla_rate=0.000 2high: n=3 average_score=0.790 observed_missed_sla_rate=0.667

Five labels with full mature-cohort coverage are enough to explain the receipt, not enough to approve probability quality. An empty bucket should report None instead of pretending evidence exists. A production report needs larger mature windows, explicit overall and slice coverage, and slices such as runner pool, service tier, geography, and forecast age.

Triage repair, inspection, and retraining separately

Drift, skew, and delayed quality degradation can appear together. They don't imply the same action:

EvidenceFirst action
missing or stale online featuresrepair data path
parity probe failsrepair training-serving contract
distribution shiftsinspect source health and business context
delayed decision cost regresses after inputs are validtrain frozen-snapshot candidate

Encode that order. The first scenario has broken data and parity. The second has clean inputs plus delayed quality regression.

triage-monitoring-signals.py
1def triage(schema_failures, stale_features, parity_ok, psi, cost_regressed): 2 actions = [] 3 if schema_failures or stale_features: 4 actions.append("repair online data path") 5 if not parity_ok: 6 actions.append("repair training-serving parity") 7 if psi >= investigate_at: 8 actions.append("inspect distribution shift") 9 if not schema_failures and not stale_features and parity_ok and cost_regressed: 10 actions.append("train frozen-snapshot candidate") 11 return actions 12 13print("dirty window:", triage(1, 1, False, psi, True)) 14print("clean changed window:", triage(0, 0, True, psi, True))
Output
1dirty window: ['repair online data path', 'repair training-serving parity', 'inspect distribution shift'] 2clean changed window: ['inspect distribution shift', 'train frozen-snapshot candidate']

Continuous training doesn't mean every alert immediately overwrites production. Google Cloud documents scheduled, new-data, performance-degradation, and distribution-change triggers for retraining pipelines, with metadata and model-validation stages around those pipelines.[3]Reference 3MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning

Diagram showing Production requests feature traces, Fast checks schema + freshness, Predictions release v3, and Delayed labels + quality window cost + slices. Diagram showing Production requests feature traces, Fast checks schema + freshness, Predictions release v3, and Delayed labels + quality window cost + slices.
Production requests feature traces, Fast checks schema + freshness, Predictions release v3, and Delayed labels + quality window cost + slices.

Freeze an immutable candidate bundle

When clean evidence justifies retraining, freeze the snapshot, training pipeline, feature contract, decision policy, evaluation policy, and previous model pointer. The resulting candidate bundle is an immutable release proposal. Promotion moves an alias; it doesn't rewrite the bundle.

Create a deterministic bundle ID from the candidate configuration.

publish-immutable-candidate-bundle.py
1candidate_config = { 2 "model": "job-risk-v4", 3 "snapshot": "labels-through-2026-02-03", 4 "training_pipeline": "job-risk-train-v4", 5 "feature_contract": "features-v3", 6 "decision_policy": decision_policy, 7 "threshold": 0.50, 8 "evaluation_policy": "job-risk-eval-v1", 9 "previous_model": deployed["model"], 10} 11payload = dumps(candidate_config, sort_keys=True, separators=(",", ":")) 12bundle_id = "job-risk-v4-" + sha256(payload.encode()).hexdigest()[:10] 13candidate_bundle = {**candidate_config, "bundle_id": bundle_id} 14 15print("candidate bundle:", candidate_bundle["bundle_id"]) 16print("rollback pointer:", candidate_bundle["previous_model"])
Output
1candidate bundle: job-risk-v4-edde9c2176 2rollback pointer: job-risk-v3

Google Cloud's MLOps guidance recommends recording pipeline versions, arguments, produced artifacts, evaluation metrics, and a pointer to the previous trained model for rollback or comparison.[3]Reference 3MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning

Gate offline metrics and canary traffic

Compare candidate v4 with current v3 before changing live traffic. These thresholds are explicit local policies:

check-offline-promotion-gates.py
1current_metrics = { 2 "recall": 0.80, 3 "priority_misses": 1, 4 "review_cost": 6, 5 "p95_latency_ms": 40, 6} 7candidate_metrics = { 8 "recall": 0.90, 9 "priority_misses": 0, 10 "review_cost": 4, 11 "p95_latency_ms": 42, 12} 13evaluation_cohort = { 14 "maturity_cutoff": "2026-02-03T12:00:00Z", 15 "matured_predictions": 1000, 16 "joined_labels": 1000, 17} 18evaluation_cohort["label_coverage"] = ( 19 evaluation_cohort["joined_labels"] / evaluation_cohort["matured_predictions"] 20) 21offline_evaluation = { 22 "policy": candidate_bundle["evaluation_policy"], 23 "label_snapshot": candidate_bundle["snapshot"], 24 "cohort": evaluation_cohort, 25 "current": current_metrics, 26 "candidate": candidate_metrics, 27} 28offline_gates = { 29 "mature_label_coverage": evaluation_cohort["label_coverage"] == 1.0, 30 "recall": candidate_metrics["recall"] >= current_metrics["recall"], 31 "priority_misses": candidate_metrics["priority_misses"] == 0, 32 "review_cost": candidate_metrics["review_cost"] <= current_metrics["review_cost"], 33 "latency_budget": candidate_metrics["p95_latency_ms"] <= 50, 34} 35 36for name, passed in offline_gates.items(): 37 print(f"{name}: {passed}") 38print("offline gate:", all(offline_gates.values()))
Output
1mature_label_coverage: True 2recall: True 3priority_misses: True 4review_cost: True 5latency_budget: True 6offline gate: True

Offline evidence is necessary but incomplete. A canary sends limited live traffic to a candidate before full promotion. Gate fast signals such as service errors, latency, request count, and immediate review-queue pressure. Those signals can stop a bad rollout quickly, but they don't replace delayed-label quality.

check-canary-promotion-gates.py
1canary_metrics = { 2 "requests": 500, 3 "error_rate": 0.002, 4 "p95_latency_ms": 44, 5 "review_queue_load_ratio": 0.72, 6} 7canary_gates = { 8 "enough_requests": canary_metrics["requests"] >= 500, 9 "error_rate": canary_metrics["error_rate"] <= 0.005, 10 "p95_latency_ms": canary_metrics["p95_latency_ms"] <= 50, 11 "review_queue_load_ratio": canary_metrics["review_queue_load_ratio"] <= 0.80, 12} 13canary_evaluation = { 14 "policy": "job-risk-canary-v1", 15 "metrics": canary_metrics, 16 "checks": canary_gates, 17} 18 19for name, passed in canary_gates.items(): 20 print(f"{name}: {passed}") 21print("canary gate:", all(canary_gates.values()))
Output
1enough_requests: True 2error_rate: True 3p95_latency_ms: True 4review_queue_load_ratio: True 5canary gate: True

Argo Rollouts documents canary steps, metric analysis, unsuccessful-analysis aborts, and post-promotion analysis that can switch traffic back to a previous stable release.[5]Reference 5Argo Rollouts - Kubernetes Progressive Delivery Controllerhttps://argoproj.github.io/argo-rollouts/ The exact deployment tool may differ, but promotion needs measured conditions and a rollback path.

Promote an alias, then rehearse rollback

Keep immutable bundle IDs separate from a movable production alias. If both gate sets pass, move the alias from v3 to the candidate. Then simulate a slower delayed-label guardrail arriving after promotion. The cost threshold is evaluated only after the declared mature cohort reaches full label coverage; an incomplete cohort would report insufficient evidence rather than treat early labels as representative.

promote-and-rollback-model-alias.py
1alias = {"production": deployed["model"]} 2rollback_pointer = alias["production"] 3 4if all(offline_gates.values()) and all(canary_gates.values()): 5 alias["production"] = candidate_bundle["bundle_id"] 6 7print("production alias:", alias["production"]) 8print("rollback pointer:", rollback_pointer) 9 10post_promotion = { 11 "matured_predictions": 500, 12 "joined_labels": 500, 13 "delayed_review_cost": 8, 14} 15post_promotion["label_coverage"] = ( 16 post_promotion["joined_labels"] / post_promotion["matured_predictions"] 17) 18rollback_limit = 5 19rollback_reason = None 20if ( 21 post_promotion["label_coverage"] == 1.0 22 and post_promotion["delayed_review_cost"] > rollback_limit 23): 24 rollback_reason = ( 25 f"delayed review cost {post_promotion['delayed_review_cost']} " 26 f"> {rollback_limit}" 27 ) 28 alias["production"] = rollback_pointer 29 30print(f"post-promotion label coverage: {post_promotion['label_coverage']:.3f}") 31print("delayed review cost:", post_promotion["delayed_review_cost"]) 32print("final alias:", alias["production"]) 33 34receipt = { 35 "candidate_bundle": candidate_bundle, 36 "previous": rollback_pointer, 37 "offline_evaluation": {**offline_evaluation, "checks": offline_gates}, 38 "canary_evaluation": canary_evaluation, 39 "post_promotion": {**post_promotion, "rollback_limit": rollback_limit}, 40 "rollback_reason": rollback_reason, 41 "release_action": "rolled_back" if rollback_reason else "keep_candidate", 42 "final_alias": alias["production"], 43} 44receipt_json = dumps(receipt, sort_keys=True, separators=(",", ":")) 45print("release action:", receipt["release_action"]) 46print("receipt sha256:", sha256(receipt_json.encode()).hexdigest()[:12])
Output
1production alias: job-risk-v4-edde9c2176 2rollback pointer: job-risk-v3 3post-promotion label coverage: 1.000 4delayed review cost: 8 5final alias: job-risk-v3 6release action: rolled_back 7receipt sha256: 54de7af4a622

The rehearsal promotes v4, waits for a fully labeled 500-row mature cohort, observes delayed review cost 8 above local limit 5, and restores alias v3. Its receipt binds candidate lineage, offline evidence, canary evidence, delayed-label coverage, and the derived rollback reason. A rollback drill is stronger than a diagram: it proves the pointer and policy work together.

Explain the release loop without looking back

Before continuing, explain the full path in your own words:

  1. Which checks protect a job before delayed labels exist?
  2. Why is drift evidence different from quality evidence?
  3. Why can't retraining fix a training-serving skew bug?
  4. Why must quality metrics use mature cohorts and report label coverage?
  5. Which artifacts make a candidate reproducible?
  6. Why do offline gates, canary gates, and rollback drills solve different problems?

Practice

  1. Change s-104 feature age from 180 to 30 minutes. Predict how many requests score before rerunning the lab.
  2. Change current 24h+ heartbeat share from 20 to 5 and move the removed 15 counts into 0-4h. Predict whether PSI rises or falls.
  3. Fix buggy_serving_transform() to divide by staffed_capacity. Explain which triage action disappears.
  4. Advance now past p-006's label_due_at, add label p-006: True, and compute the new precision, recall, and mature-cohort coverage.
  5. Change canary p95_latency_ms from 44 to 65. Explain whether production alias ever moves to v4.
  6. Remove rollback_pointer assignment. Explain why a model registry entry alone doesn't prove rollback works.

Practice answer sketches

PromptReasoning check
Freshen s-104Five requests score. Only s-103 remains blocked for missing backlog.
Restore recent-heartbeat mixPSI falls because current traffic moves closer to reference traffic.
Fix serving transformTraining-serving parity becomes True, so repair training-serving parity disappears.
Resolve p-006 as missed_slaTrue positives rise from 2 to 3; precision rises to 3 / 4 = 0.75; recall remains 1.0.
Slow canaryp95_latency_ms gate fails. Alias remains job-risk-v3.
Delete rollback pointerCandidate may still be reproducible, but rollback target is no longer proven by execution.

What strong answers show

EvidenceA strong explanation demonstrates
fast controlsblocks missing or stale features before delayed labels arrive
drift reasoningtreats PSI as a local investigation diagnostic, not an accuracy verdict
parityreproduces skew with one raw row and fixes contract before retraining
delayed qualityjoins immutable prediction IDs, gates on mature cohorts and label coverage, and checks slices
candidate controlfreezes snapshot, training pipeline, feature contract, decision policy, evaluation policy, and previous model pointer
rolloutseparates offline gate, limited live canary, promotion alias, and rollback receipt

When monitoring breaks

SymptomCauseFix
Accuracy report arrives after days of bad inputsonly label metrics are monitoredadd schema and freshness alarms
Every drift alert starts retrainingdrift is confused with failureinspect data path, context, and delayed outcomes
New training run repeats wrong scoresserving transform differs from trainingadd parity probes before retraining
Report looks good while labels are still arrivingmetrics use whichever labels arrived firstdefine mature cohorts, report label coverage, and block or qualify incomplete windows
New model can't be rolled back cleanlybundle and alias aren't versioned separatelypublish immutable candidate plus rollback pointer
Canary passes but delayed cost later risesonly fast rollout metrics were checkedkeep post-promotion guardrails and rehearse rollback
Complete the lesson

Mastery Check

Answer every question, then check your score. Score above 75% to mark this lesson complete.

1.At noon, a job-risk request has all required feature values, but its feature snapshot is from 09:00. The request-time contract requires snapshots no older than 60 minutes. What should the monitor do before SLA labels arrive?
2.A heartbeat-age drift report compares reference shares [50, 30, 15, 5] with current shares [30, 25, 25, 20] and computes PSI 0.37 against a local investigation threshold of 0.20. What action is justified first?
3.Training computes backlog_ratio = queue_backlog / staffed_capacity, but serving was changed to compute queue_backlog / 100. For a raw row with queue_backlog = 20 and staffed_capacity = 50, what should the parity probe conclude?
4.A delayed quality window stored six predictions. Outcome windows have closed for p-001 through p-005, and all five labels arrived. The outcome window for p-006 closes next hour. What should the report do?
5.A fully labeled mature cohort uses threshold 0.50 and has (score, missed_sla) values: (0.15, False), (0.32, False), (0.68, True), (0.78, False), and (0.91, True). False positives cost 2 and false negatives cost 10. Which receipt is correct?
6.A risk report groups a fully labeled mature cohort by score. In the high bucket, three rows have average score 0.790 and observed missed_sla rate 0.667; an empty bucket would report average_score=None and observed_missed_sla_rate=None. Which interpretation is correct?
7.A triage rule adds actions as follows: repair the online data path for schema or staleness failures, repair parity when the parity probe fails, inspect when PSI is at least 0.20, and train a candidate only when inputs are clean, parity passes, and cost regressed. A window has schema_failures=1, stale_features=1, parity_ok=False, PSI=0.37, and cost_regressed=True. Which actions should it return?
8.A clean quality regression justifies retraining. Which candidate package supports reproducibility and rollback before promotion?
9.Offline gates pass for a candidate. Canary metrics are requests=500, error_rate=0.002, p95_latency_ms=65, and review_queue_load_ratio=0.72. Canary gates require requests at least 500, error rate at most 0.005, p95 latency at most 50, and queue load at most 0.80. What happens to the production alias?
10.Offline and canary gates pass, so the production alias moves from job-risk-v3 to immutable bundle job-risk-v4-edde9c2176. Later, a 500-row mature cohort reaches 500/500 label coverage, delayed review cost is 8, the rollback limit is 5, and the rollback pointer is job-risk-v3. What should the release controller do?

10 questions remaining.

Next Step
Continue to The Bitter Lesson & Compute

You can build, serve, monitor, promote, and roll back conventional predictive models with explicit evidence. Next, apply the same measurement discipline to language models whose capabilities grow with data and computation.

PreviousForecasting and Anomaly Detection
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Hidden Technical Debt in Machine Learning Systems.

Sculley et al. · 2015

Monitoring Machine Learning Models in Production.

Klaise, J., et al. · 2020

MLOps: Continuous Delivery and Automation Pipelines in Machine Learning.

Google Cloud. · 2026 · Official documentation

On Calibration of Modern Neural Networks

Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. · 2017

Argo Rollouts - Kubernetes Progressive Delivery Controller

Argo Project · 2026

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.