MLOps and CI/CD for ML Systems
Implement production CI/CD for ML with testing, lineage, and rollback guarantees.
Continuous integration and continuous deployment are standard practice in software engineering. In ML, the same principles apply - but the pipeline is more complex because there are two additional artifacts to version and validate: data and trained models. A real ML CI/CD pipeline does not just test code; it validates data, trains a model, evaluates its quality, registers the artifact, and deploys it.
What a Real ML CI Pipeline Does
A typical ML CI pipeline runs in this order:
- Data validation - schema check, null rate check, distribution snapshot
- Training - reproducible training run with logged hyperparameters and metrics
- Quality gate - compare new model against current production model on holdout data
- Artifact registration - push model to registry if quality gate passes
- Deploy to staging - run integration tests and latency benchmarks
- Promote to production - canary rollout or blue-green cutover
yaml# .github/workflows/ml-pipeline.yml name: ML Training and Deployment on: push: paths: - 'src/training/**' - 'configs/**' jobs: validate-data: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: python scripts/validate_data.py --config configs/data_validation.yml train: needs: validate-data runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: python scripts/train.py --config configs/training_config.yml - uses: actions/upload-artifact@v4 with: name: model-artifact path: outputs/model/ quality-gate: needs: train runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: model-artifact path: outputs/model/ - run: python scripts/quality_gate.py --challenger outputs/model/ --champion-alias production deploy-staging: needs: quality-gate runs-on: ubuntu-latest steps: - run: ./scripts/deploy.sh staging - run: python scripts/integration_tests.py --env staging
DVC for Data Versioning
DVC (Data Version Control) versions data files alongside code using Git-tracked pointer files. Each training run references a specific, immutable data snapshot.
bash# Initialize DVC in your repo dvc init git add .dvc .dvcignore git commit -m "Initialize DVC" # Track a data file dvc add data/training_set.parquet git add data/training_set.parquet.dvc .gitignore git commit -m "Track training data with DVC" # Push data to remote storage dvc remote add -d myremote s3://my-bucket/dvc-cache dvc push
The .dvc file is a tiny YAML file containing a hash and the remote path. Check it into git. Now git checkout + dvc pull reproduces any historical training data exactly.
yaml# data/training_set.parquet.dvc outs: - md5: e3b0c44298fc1c149afb size: 142857600 path: training_set.parquet
MLflow for Experiment Tracking
MLflow logs hyperparameters, metrics, and artifacts for each training run. Every training job writes to MLflow before pushing its artifact anywhere.
pythonimport mlflow def train_and_log(config: dict): with mlflow.start_run(): mlflow.log_params(config) model, metrics = train_model(config) mlflow.log_metrics(metrics) # {"auc": 0.91, "f1": 0.78} mlflow.sklearn.log_model(model, artifact_path="model", registered_model_name="churn-prediction") # Tag the run for promotion filtering mlflow.set_tags({"data_version": config["data_hash"], "env": "ci"})
The quality gate script queries MLflow to compare the new run against the champion:
pythondef quality_gate(challenger_run_id: str, metric: str = "auc", min_delta: float = -0.005): client = mlflow.tracking.MlflowClient() challenger_metric = client.get_run(challenger_run_id).data.metrics[metric] champion = client.get_model_version_by_alias("churn-prediction", "production") champion_metric = float(champion.tags.get(f"metric.{metric}", 0)) delta = challenger_metric - champion_metric passed = delta >= min_delta print(f"Challenger {metric}: {challenger_metric:.4f}, Champion: {champion_metric:.4f}, delta: {delta:+.4f}") if not passed: raise SystemExit(f"Quality gate failed: {metric} delta {delta:+.4f} below threshold {min_delta}")
Canary and Shadow Deployments
Shadow deployment sends a copy of live traffic to the new model without affecting the response returned to the user. You collect real predictions and compare them to the production model offline before any user-facing impact.
Canary deployment routes a small fraction of live traffic (1–5%) to the new model with real user impact. Monitor error rate and prediction distribution for 24 hours before scaling to 100%.
python# Feature flag - based canary in your serving layer import random def route_request(request, canary_fraction: float = 0.05): if random.random() < canary_fraction: return new_model.predict(request) return production_model.predict(request)
In Kubernetes, canary routing can be implemented with two Deployments and traffic splitting at the ingress level using weights, without requiring application-layer changes.
Common Mistakes
Training in CI without data pinning. If your CI training job pulls "the latest data," two runs on the same commit produce different models. Always pin the data version in your training config and verify the hash at the start of each run.
Skipping the quality gate. Deploying every artifact that builds without comparing it to the current production model means you will eventually ship a regression silently. The quality gate is the ML equivalent of a passing test suite.
One-shot promotion. Going straight from training to 100% traffic with no canary phase eliminates your ability to catch issues before they affect all users. Always route a small percentage first.
Where to Go Next
mlops-deployment-serving- the deployment step in this pipeline - container building, serving patterns, and rollbackmonitoring-drift-llm-evaluation- the monitoring step that feeds back into the retraining triggermilestone-gate-2-production-readiness- use this pipeline as the reference for your gate 2 self-assessment
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsOpen-Weight and Small Models in 2026: When to Self-Host
Open-weight models are competitive, small models run on a phone, and the API-for-everything default is no longer obviously right. Here is a decision framework for self-hosting versus API, where small models win, what mixture-of-experts changes about the parameter count, and the hybrid most teams end up with.
ML Model to Production: A Complete Walkthrough
Most ML models die in notebooks. Walk through the full path from trained model to live API endpoint serving real traffic - packaging, containerizing, deploying, and monitoring.
Model Versioning with MLflow: Practical Guide
Without model versioning, you cannot reproduce results, roll back broken deployments, or compare experiments. MLflow gives you a practical registry - here is how to use it well.