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

CI/CD for ML Pipelines

Automate tests, training checks, and gated promotion into production.

1h
0 problems
Topic Progress 0%

What Actually Runs on Every Merge: The Six Stages of an ML Pipeline

Your fraud-scoring repo passes lint and 214 unit tests, so the pull request merges cleanly. Two days later production degrades: the upstream team renamed card_present to card_present_flag, your pipeline encoded every value as null, and no test caught it because in ML the data is an input, and code tests never see it. Software CI validates one artifact (the binary). ML CI must validate three: code, data, and the model that training produces from their combination. That is why an ML pipeline adds gates on top of ordinary tests instead of replacing them.

The full sequence, with what each stage actually blocks:

Stage What it checks Typical runtime Fails on
1. Lint + unit tests feature code, business logic, config parsing ~2 min assertion or lint errors
2. Data validation schema, ranges, null rates vs reference snapshot ~1 min renamed/missing column, new nulls, out-of-range values
3. Training smoke test tiny subset, one epoch, end to end ~3 min crash, NaN loss, metric below floor
4. Full training complete dataset, fixed hyperparameters 20-90 min convergence failure, missing artifact
5. Evaluation gate candidate vs champion on holdout + slices ~5 min effect-size or slice thresholds unmet
6. Deploy image push, registry promotion, canary ~10 min health check failure; canary SLO breach auto-rolls back

A GitHub Actions skeleton — the shape matters more than the vendor:

name: ml-ci
on: [push, pull_request]
jobs:
  code:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.lock && ruff check . && pytest -q
  data-check:
    needs: code
    steps:
      - uses: actions/checkout@v4
      - uses: iterative/setup-dvc@v2
      - run: dvc pull data/reference   # versioned reference snapshot
      - run: python -m checks.validate_schema data/live --reference data/reference
  smoke-train:
    needs: data-check
    runs-on: ubuntu-latest
    steps:
      - run: python train.py --subset 5000 --epochs 1 --expect-loss-below 0.80

Stage 2 is the highest-leverage minute in the pipeline. A schema or distribution check against the versioned reference snapshot catches the renamed column before you spend a GPU-hour. Express the expectations as code reviewed in pull requests — column presence and types, null_fraction < 0.05, value ranges, allowed category sets, and a population-stability check against the reference for high-impact features. Great Expectations and TFDV-style tools both reduce to exactly this: named expectations with a pass/fail result the pipeline can block on.

The smoke test (stage 3) exists to prove the training entrypoint still runs end to end with the current dependency lock: imports resolve, data loaders iterate, loss decreases, the artifact writes to disk. Its assertions stay deliberately weak — loss moved, no NaN, metric above a generous floor — because its job is catching breakage in minutes, not judging quality. Quality is stage 5's job, and quality judgments need the full dataset.

Full training should not run on every pull request: a 45-minute A10G job at 20 PRs per day burns roughly 15 GPU-hours daily for changes that 99% do not affect training. Run it on merge to main or nightly, cache the data snapshot through DVC so repeated runs skip the download, and gate it behind a run-full-train label for when an author genuinely needs it.

The pattern from Online and Batch Serving tells you what the deploy stage must produce — an endpoint whose latency and freshness guarantees hold — but nothing tells you whether the new model deserves to replace the old one. Stage 5 does, and it is the subject of the next chapter.

Candidate Beats Champion by 0.3%: Defining a Promotion Gate That Means Something

Stage 5 asks one question: does the candidate deserve to replace what is running? Your candidate reports AUC 0.8712 against the champion's 0.8697, and someone on the pull request writes "ship it." That comment is the bug. A 0.0015 gap means nothing until you know the noise floor, and promoting on point estimates alone ships regressions roughly as often as it ships gains. The promotion gate must be executable code, not a person eyeballing a dashboard — dashboards drift with attention, pipelines do not.

Encode four criteria, all versioned in params.yaml so changing a threshold is a reviewed diff:

  1. Minimum effect size. Require a relative improvement of at least +0.3% (a business-set floor). Below that, shipping adds risk for no measurable gain — keep the champion even if the candidate is technically higher.
  2. Statistical confidence. A paired bootstrap over the holdout: resample the same items with replacement 1,000 times, compute the metric difference each round, and require the 95% confidence interval to exclude zero.
import numpy as np

def delta_bootstrap(y, cand, champ, metric, n=1000, seed=42):
    rng = np.random.default_rng(seed)
    idx = np.arange(len(y))
    deltas = np.empty(n)
    for i in range(n):
        s = rng.choice(idx, len(idx), replace=True)
        deltas[i] = metric(y[s], cand[s]) - metric(y[s], champ[s])
    lo, hi = np.percentile(deltas, [2.5, 97.5])
    return float(deltas.mean()), float(lo), float(hi)

# ship only if mean delta >= MIN_EFFECT and lo > 0
  1. Slice guardrails. No critical segment — country, device, merchant category — may regress by more than 1 point. A global gain that hides a 4-point loss on your largest segment is a guaranteed rollback, and aggregate metrics will never show it.
  2. Non-metric gates. p99 latency within the budget from serving design, artifact size within GPU memory, and calibration error within tolerance if scores drive business thresholds.

Once the gate passes, promotion mechanics limit blast radius: the registry moves the version Staging → Production with the gate's evidence attached — metrics JSON, data snapshot ID, git SHA — then rollout proceeds shadow (log-only, zero risk, compare offline) → canary (5-10% of traffic for 30 minutes, automatic rollback if error rate or latency breaches) → full. The registry semantics come from Model Registry and Versioning; the gate is what makes the stages mean something.

None of this is reproducible without versioned inputs, which is where DVC earns its place. Git tracks code and small configuration; DVC tracks the heavy artifacts through pointer files in git and content in object storage:

dvc add data/train.parquet            # .dvc pointer committed, data to S3 remote
dvc stage add train -p params.yaml \
  -d data/train.parquet -d src/train.py -o models/model.pkl python src/train.py
dvc repro train                       # reruns only when inputs actually changed
dvc push                              # artifacts to the shared remote
dvc metrics show --json               # what the gate script reads

Two checkouts of the same commit reconstruct the exact training run: same data snapshot, same parameters, same code. That matters the day a gate result is disputed — "the candidate saw different data" becomes a command, not an argument. It also powers dvc metrics diff across runs, so the candidate-versus-champion comparison is a first-class, reviewable object rather than numbers pasted into a pull request comment.

A subtle failure the gate must absorb: nondeterminism. GPU kernels and shuffled loaders mean two identical runs differ in the last decimal place, so write every threshold as a tolerance (delta >= +0.3%, score within 0.002) — never exact equality — and seed the training run so the variance stays inside the tolerance you wrote. A gate that fails on rounding teaches people to bypass it, and a bypassed gate is worse than no gate.

Versioning the Serving Stack and the Five Ways ML CI Breaks in Practice

Code, data, and models are versioned — and then the environment that runs them is managed by hand, which is where releases quietly rot. Infrastructure as code (Terraform, Pulumi, CloudFormation) versions the training cluster and the serving stack alongside application code: instance types, autoscaling limits, resource requests, ingress rules, and the model-server image digest. A capacity change becomes a reviewed pull request, so "who moved us to a smaller GPU?" is a git log instead of a meeting.

resource "aws_ecs_service" "inference" {
  name            = "fraud-inference"
  task_definition = aws_ecs_task_definition.inference.arn
  desired_count   = 2
  # the task definition pins the image by digest, not :latest —
  # the digest is what actually ran through canary
}

Pair that with a pinned model version from the registry and every deploy is reproducible: same task definition, same image digest, same model artifact. Five failure modes account for most real-world ML CI breakage, and each has a specific fix:

  1. Data not versioned. Training silently pulls a different snapshot than the one the evaluation gate scored, so the gate's verdict describes a model nobody will run. Fix: DVC pointer files in git, the snapshot ID recorded in the registry entry, and a pipeline that fails when the pointer and the training job disagree.
  2. Nondeterminism masquerading as regression. Shuffled loaders, unordered reductions, and GPU kernels make exact reproduction impossible on some hardware. Fix: seed every RNG, set deterministic flags where available, and write gates as tolerances — Δ >= +0.3%, never score == 0.8712. A gate that trips on rounding gets disabled by the first frustrated team, and a disabled gate is theater.
  3. Full training on every pull request. The cost math turns hostile fast: 45 minutes on an A10G at 20 PRs per day is about 15 GPU-hours daily. Fix: PR runs are limited to lint, unit tests, data validation, and the smoke stage; full training fires on merge or nightly; DVC caching means unchanged data is never re-downloaded; the run-full-train label exists for the exceptions.
  4. Floating references everywhere. :latest image tags, unpinned base images, and unpinned inference dependencies mean the artifact that passed canary is not the artifact running next week. Fix: pin image digests, commit a dependency lockfile, and make the lockfile part of what the evaluation gate validated — an untested dependency bump is an unreviewed model change.
  5. No rehearsed rollback. The canary alarm fires at 02:00 and nobody knows the command. Fix: treat (image digest, model version, task definition) as one deployable unit, keep the previous unit one command away, automate the trigger — canary breaches SLO → previous task definition plus a registry stage flip — and rehearse it in a game day. Rollback you have never run is a hypothesis.

Add two operating rules. First, pin desired_count and autoscaling bounds in the same Terraform module so capacity intent is reviewable; a hand-scaled console change disappears from history. Second, deploy-stage health checks must include a prediction probe — send a synthetic request and assert a plausible score — because containers can be healthy while the model fails to deserialize, and only a real inference catches that.

The pipeline's last act is handing the new version to the systems that watch it: post-deploy checks on latency, error rate, and input distribution. That is where the lifecycle continues in Production Monitoring and Drift Detection.

In an interview, compress the whole story to one sentence: four things are versioned (code, data, model, infrastructure), gates run in a fixed order, and promotion means an executable gate plus a canary plus a rehearsed rollback. That sentence is the difference between "we use GitHub Actions" and a system you can defend.

Quiz

1. An upstream team renames a column from `card_present` to `card_present_flag`. Your pipeline starts encoding every value as null, but lint, unit tests, and the smoke test all pass. Which stage catches this before a GPU-hour is spent?

Question 1 options

2. A candidate scores AUC 0.8712 vs the champion's 0.8697 on the holdout. A paired bootstrap gives a 95% interval of [-0.0004, +0.0034], and the required minimum effect is +0.3% relative. What should the gate do?

Question 2 options

3. Two engineers check out the same commit but their training jobs read different dataset contents, producing irreproducible results. Which change fixes this at the source?

Question 3 options

4. Your team's pull-request pipeline spends 45 GPU-minutes per push and reviewers wait an hour for feedback. What change preserves coverage while cutting cost and latency?

Question 4 options

Flashcards

Question

Name the six stages of an ML CI/CD pipeline and what each one blocks.

Answer

1) Lint + unit tests — code defects. 2) Data validation vs a versioned reference — schema/range/null regressions before any compute. 3) Training smoke test — broken entrypoints, NaN loss (minutes, weak floor). 4) Full training — convergence failures (20-90 min, on merge/nightly). 5) Evaluation gate — candidate vs champion by effect size, confidence, slices. 6) Deploy — image push, registry promotion, canary with auto-rollback on SLO breach.

Question

Why is data validation the highest-leverage stage, and what should it check?

Answer

Because it fails the pipeline in ~1 minute before GPU-hours are spent, and it is the only stage that can see the data itself — unit tests only see fixtures. Check: column presence and types, null fraction (e.g. < 5%), value ranges, allowed category sets, and a distribution distance (PSI/KS) against the reference snapshot. Express expectations as code so changes are reviewed in PRs.

Question

What four criteria make up a promotion gate that decides candidate vs champion?

Answer

1) Minimum effect size (e.g. >= +0.3% relative) — below it, keep the champion. 2) Statistical confidence: paired bootstrap over the holdout, 1,000 resamples, 95% CI must exclude zero. 3) Slice guardrails — no critical segment regressing beyond ~1 point. 4) Non-metric gates — p99 latency in budget, artifact fits memory, calibration within tolerance. Encode all four in versioned params.yaml as tolerances, never exact equality.

Question

How do git, DVC, and a model registry divide responsibility for versioning an ML release?

Answer

Git tracks code, params.yaml, dvc.yaml, and DVC pointer files (small). DVC resolves those pointers to the exact dataset/model artifacts in a shared object-storage remote (`dvc repro` reruns only on input change, `dvc metrics show --json` feeds the gate). The registry records the promoted version with its lineage — metrics, data snapshot ID, git SHA — and stages (Staging/Production). Same commit reconstructs the same run.

Question

What four things must be versioned in ML CI, and what is the rollback unit?

Answer

Code (git), data (DVC pointers), model (registry version + lineage), infrastructure (Terraform/Pulumi: instance types, autoscaling, image digests). The rollback unit is the combination — image digest + model version + task definition — kept as one deployable object so a canary SLO breach flips back with a single command, rehearsed in game days. An untested rollback is a hypothesis.

Revision Notes

Key Takeaways

  • 1. ML CI adds gates for data, training, and evaluation on top of software CI because the data is an input that code tests never see
  • 2. Data validation against a versioned reference snapshot is the highest-leverage minute: it catches renamed columns and null explosions before any GPU-hour is spent
  • 3. A promotion gate is code, not a dashboard glance: minimum effect size, paired-bootstrap confidence, slice guardrails, and non-metric budgets, all tolerance-based
  • 4. DVC pointer files + a model registry + Terraform make one commit reconstruct the entire run — code, data, model, and the environment that serves it
  • 5. Rollout is shadow → canary → full, with (image digest, model version, task definition) as a single rehearsed rollback unit

Interview Tips

  • • "Walk me through your ML CI pipeline." — give the six stages in order with the defect each blocks and its runtime; emphasize that data validation runs before training and that full training is on merge/nightly, not every PR
  • • "How do you decide a new model is safe to deploy?" — four criteria: effect-size floor, paired-bootstrap CI excluding zero, slice guardrails, non-metric budgets — then shadow/canary with automatic rollback
  • • "What is the difference between git and DVC?" — git versions code and tiny pointers, DVC resolves pointers to content-addressed data/model artifacts; same commit reproduces the same training run, and changing data is a reviewed diff
  • • "Your canary starts failing at 02:00 — what happens?" — automatic flip to the previous (image digest, model version, task definition) unit and registry stage demotion; mention that you rehearsed it, because a rollback never tested is a hypothesis

Cheat Sheet

CI/CD for ML Pipelines

Stage order and what it blocks:

Stage Blocks Runtime
lint + unit tests code defects ~2 min
data validation vs reference schema/null/range drift ~1 min
training smoke test broken entrypoint, NaN loss ~3 min
full training (merge/nightly) convergence failure 20-90 min
evaluation gate weak candidate, slice regression ~5 min
deploy + canary SLO breach (auto-rollback) ~10 min

Promotion gate (all four, executable): effect size >= +0.3% relative AND paired-bootstrap 95% CI excludes zero (1,000 resamples) AND no slice regresses > 1 pt AND p99/size/calibration within budget. Thresholds live in params.yaml as reviewed, tolerance-based diffs.

Versioning split:

git    → code, params.yaml, dvc.yaml, *.dvc pointers
dvc add data/train.parquet      # content to remote, pointer to git
dvc repro train                 # rerun only on input change
dvc metrics show --json         # what the gate reads
registry → promoted model version + metrics + snapshot id + git SHA
Terraform → image digest, instance types, autoscaling, desired_count

Rollout: shadow (log-only) → canary (5-10% for 30 min, auto-rollback) → full. Rollback unit = image digest + model version + task definition, rehearsed.

Five breakages: unversioned data; nondeterminism (seed everything, gate on tolerances); full training per PR (limit PRs to fast stages, label-trigger exceptions); floating tags/locks (pin digests + lockfile); untested rollback (game days).

Continue Learning