search_ads_recsys / 18 · deployment & serving lesson 18 / 39

Deployment & serving

A model that wins offline has done nothing yet. It earns its keep only when it answers a live request correctly, inside a latency SLA, on traffic it has never seen — and keeps doing so after you stop watching.

The latency budget is the real spec

A ranker answers under a hard wall-clock deadline — typically 100–300 ms end-to-end for the whole funnel, of which the ranking serving call gets maybe 20–80 ms. That budget is not advice; it is the spec. A model that is 0.3% more accurate but 2× slower is, in production, a worse model: it either misses the deadline (and a degraded fallback gets served instead) or forces you to shrink the candidate set, losing more accuracy than you gained.

So before you touch the model, decompose where the milliseconds go. A single ranking request is not "the forward pass" — it is a chain, and the surprise is which link dominates.

StageTypical shareWhat it isLever to cut it
Feature fetch30–60%Look up user/item/context features from the store — often the dominant cost, hundreds of small key-value readsBatch reads, cache hot embeddings in RAM/GPU, co-locate store with serving
Pre / post-process5–15%Encode, hash, bucketize, assemble the input tensor; build the responseVectorize; precompute static features offline
Model forward20–50%Inference over 10²–10³ candidatesBatching, quantization, distillation (19 · Large-scale optimization)
Network + serialization5–20%RPC hops, protobuf encode/decode, queueingCo-location, fewer hops, compact wire format
The counter-intuitive headline: it's usually not the model
Engineers reach for quantization first because the model is the part they understand. But on a real ranking service the feature fetch — hundreds of small reads against a feature store — frequently eats more wall-clock than the matmuls. Profile before you optimize. If 55% of P99 is feature I/O, distilling the model to half its size buys you ~20% of the budget back; caching the hot embeddings buys you 40%.

Why P99, not the mean

Your SLA is written on a tail percentile (P99, sometimes P999), not the average, because a user is one request, not a distribution. With a 200 ms budget and a fan-out of, say, 30 parallel sub-calls, the request finishes when the slowest sub-call returns — so even a 1-in-100 slow feature read becomes a near-certain slow request. This is the tail-at-scale effect: as fan-out grows, the request's latency tracks the tail of the component latency, not its mean. Mitigations: hedged requests (issue a duplicate after a delay, take the first), tight per-call timeouts with a fallback, and trimming the number of dependencies on the critical path.

Batching — the one free lever

A ranking request already scores 100–1000 candidates; that is a batch, and it is what makes GPU serving efficient — the weights are read once and reused across candidates (the arithmetic-intensity argument). Across requests, dynamic batching (Triton, TF-Serving) groups concurrent requests arriving within a few-ms window into one forward pass, trading a tiny latency add for a large throughput gain. The tuning knob is the max queue delay: too small and you lose the throughput; too large and you blow the tail.

Export & the serving runtime

The training artifact — a PyTorch .pt graph, a TF SavedModel — is built for flexibility (autograd, eager Python). The serving artifact wants the opposite: a frozen, optimized, language-agnostic graph that a high-throughput C++ runtime executes with no Python in the loop. That is what export buys you.

Format / runtimeWhat it gives youTypical use
ONNXFramework-neutral graph; train in PyTorch, serve via ONNX Runtime / TensorRT. Decouples training stack from serving stackCross-platform, multi-team orgs
TorchScriptFrozen PyTorch graph, runnable from C++, supports hot model replacementPyTorch-native shops
TF SavedModel + TF-ServingVersioned model dir + a server with batching & version management built inTF-native shops
TensorRTOperator fusion, kernel selection, INT8/FP16 — squeezes GPU latency hardGPU-bound ranking, last-mile latency
Triton Inference ServerMulti-framework server: dynamic batching, concurrent model execution, version controlHeterogeneous model zoo on shared GPUs

Two optimizations happen during export and recur in 19 · Large-scale optimization: operator fusion (collapse matmul → bias-add → activation into one kernel, killing memory round-trips) and quantization (FP32 → INT8 cuts memory ~4× and speeds up integer math — but is also the first place numerical results drift from training, which is exactly the next section's problem).

What each optimization actually buys — with numbers

"Quantize the model" is not one lever; it is a family, and the headline you quote in a design review should be the measured one, not the hopeful one. The numbers below are representative of a multi-head ranker (an MMoE-style model) served on a commodity inference GPU.

TechniqueMechanism (one line)Typical payoffWhat it costs you
INT8 quantizationQ = round(R / scale) + zero_point; weights stored at 8 bits, integer matmul on Tensor Cores75% VRAM cut vs FP32; faster integer mathTruncation error on long-tail embeddings; needs a calibration set
TensorRT (fusion + kernel select)Fuses ops, picks the fastest kernel per shape, folds constants−40% latency, +20% GPU utilBuild step per shape/version; operator-coverage gaps (see ONNX check below)
Knowledge distillationA small student is trained to match a big teacher's outputskeeps ≈ 95% of the effect while cutting ≈ 70% of paramsExtra training pipeline; student can lag the teacher on rare slices
Mixed precision (by module)INT8 the body, keep attention / prediction heads in FP16AUC drop < 0.5% vs full INT8's larger dropA mixed graph is fiddlier to export and profile
Why the heads stay FP16 — and why two-tower recall needs QAT
A multi-head ranker's per-task heads emit scores whose absolute scale matters (they feed a weighted blend or a calibrated probability). INT8 rounds the head's outputs coarsely, and a small per-head bias becomes a ranking change — so you spend the bits where they buy AUC and quantize only the wide, redundant body. Separately, a two-tower retrieval model's embeddings are compared by dot product, so INT8 truncation on a cold-start item's vector distorts its similarity to everything — that is the case for quantization-aware training (QAT), which simulates the rounding during training so the model learns weights that survive it. Post-training INT8 is fine for the redundant ranker body; QAT earns its complexity exactly where one vector's small error changes a retrieval result.

Serving a billion-row embedding table

The ranker's parameters are dominated by one object: the embedding table — one learned row per categorical id (every user, item, ad, query token). At industrial scale this table alone is hundreds of GB to terabytes, far past what one box holds in RAM, and it is read on every request and written continuously by online learning. The dense MLP on top is a rounding error by comparison. So "serve the model" is really "serve a TB-scale, constantly-mutating key-value store under a few-ms read SLA" — a storage problem wearing a model's clothes. (The sharding and ANN side of this lives in 19 · Large-scale optimization and 03 · Embeddings & ANN; here we cover the serving mechanics.)

Worked numbers — why the naive table doesn't fit, and what shrinks it
Take 10⁹ ids at 64-d embeddings in FP32 (4 B each):
10⁹ × 64 × 4 B = 256 GB
That is one table, one model version — and you need the old version live during a swap, so double it to 512 GB resident. Two levers, multiplicatively:
int8 (1 B not 4 B): 256 GB → 64 GB  ·  feature admission keeping ~20% of ids: 64 GB → ~13 GB
A 256 GB elephant becomes a ~13 GB object that fits in one machine's RAM with headroom — a ~20× reduction before you shard. The lesson: you attack the table with admission and quantization first; sharding is for what remains.

Hot/cold storage tiering

Access to embedding rows is brutally Zipfian — a few popular items and active users are read constantly; the long tail almost never. So you tier the storage to match: hot rows in memory (or GPU VRAM for the hottest), cold rows on local SSD, full table in a distributed KV store (the Angel / BytePS class of system). A read checks RAM first, falls to SSD on a miss. The win is that the few-ms read SLA only has to hold for the hot set; the tail tolerates an SSD hop. Prefetching the embeddings for a user's recent-history items off the critical path hides most of the remaining latency.

Consistent-hash sharding with locality

What doesn't fit on one box is sharded across a parameter-server cluster, with each id mapped to a shard by consistent hashing — chosen over plain modulo because adding or removing a node remaps only ~1/N of keys instead of nearly all of them, so a scale-up doesn't trigger a table-wide reshuffle (and a cold cache stampede) mid-traffic. Locality-aware scheduling then co-locates a request's likely ids on as few shards as possible, because a lookup that fans out to 30 shards pays the tail-at-scale penalty from 17 · Real-time & streaming: the request waits on the slowest shard.

Double-buffer atomic swap — updating a table you're reading

The table is read on every request and written by retraining/online learning. Lock a row for a write and reads queue behind it; the tail latency blows. The fix is a double buffer: keep two copies of the table (or shard), serve all reads from buffer A while the update lands in buffer B, then flip a single pointer atomically. In-flight reads drain on A, new reads hit B — no read ever waits on a write. It is the same atomic-pointer-flip discipline as the model hot-swap below, applied to the parameter store, and it is what makes "update embeddings every few minutes without a latency glitch" possible. The cost is the obvious one: 2× memory for the buffered table (the same factor the worked numbers above already budgeted for).

Feature admission and eviction — don't store a row you can't learn

A brand-new id has been seen a handful of times; its embedding is essentially noise, yet a naive table allocates a full row for it the instant it appears. Across a long tail of millions of one-shot ids this is pure waste — memory spent on rows the model can't actually fit. Feature admission (特征准入) is the gate: admit an id into the table only after it has accumulated N exposures (e.g. seen ≥ 10 times), so you pay for a row only once there's enough signal to train it. On the other end, eviction reclaims the tail — LRU drops embeddings unread for a long window. Together they hold the table at a working set of ids that earn their memory; the worked numbers above used a 20%-admitted figure, and that one policy did more than the quantization did.

Per-feature 8-bit quantization

The last lever is to store low-frequency feature embeddings at int8 (1 byte/dim) rather than FP32 (4 bytes/dim) — a 4× shrink on those rows. It is applied per feature, not globally, on purpose: head features that drive most predictions keep their precision; the long-tail rows, which are noisy anyway, lose little from coarse quantization. This is the storage-side cousin of the model-weight quantization above, and it composes with admission — admit fewer rows, and store the rows you admit more cheaply.

REQUEST │ ids → consistent hash → shard(s) ┌──────────────────────────┐ ▼ │ feature ADMISSION gate │ ┌─────────────┐ miss ┌──────────────┐ │ (seen ≥ N times?) ──no──▶│ skip, use default │ RAM (hot) │────────▶│ SSD (cold) │ └──────────┬───────────────┘ │ hottest in │◀────────│ full shard │ │ yes │ GPU VRAM │ fetch └──────┬───────┘ ▼ └──────┬──────┘ │ admit row (int8 if long-tail) │ read SLA: few ms │ tail tolerates hop ▼ ▼ ┌─── UPDATE (retrain / online) ───┐ serve embedding (LRU evicts cold, │ write buffer B, then atomic │ unread long-tail rows) │ pointer flip A→B (no read lock)│ └─────────────────────────────────┘

Train–serve skew — the silent killer

This is the single most important idea in the lesson, and the one most likely to cost you a launch. Train–serve skew is when a feature is computed one way during training and a subtly different way at serving time. The model was fit to the training distribution; at serving it receives a shifted distribution and silently produces worse predictions. Nothing crashes. No alert fires by default. The offline AUC was great. The online metric is flat or down, and you have no idea why.

Why skew is so dangerous — it is invisible to every offline check
Your offline evaluation runs on the offline feature pipeline, so it can never see a discrepancy with the online pipeline. The bug lives precisely in the gap between two code paths your tests run one at a time. "It was great offline but flat online" is the canonical symptom of skew, and it is the default explanation for the offline–online gap that haunts 08 · Evaluation.

Where skew creeps in:

The fix: one pipeline, not two

The structural cure is to not have two implementations. A feature store serves the same materialized feature values to both training (via point-in-time correct historical reads) and serving (via low-latency online reads), computed by shared transformation code. The same streaming job that updates the online store also writes the offline feature logs, so the window logic is identical by construction. Where you can't fully unify, you add a guard: log the exact feature vector the model saw online, periodically replay those same entities through the offline pipeline, and diff.

skew check: compare online_log(f) vs offline_pipeline(f) on the same entities → alert if Δmean, Δp50, Δp99, or PSI exceeds threshold

The distance metric of choice is the Population Stability Index — the same statistic that monitors drift in production, just applied to a different baseline. Skew and drift are one measurement against two reference distributions.

PSI = Σᵢ (online_pctᵢ − ref_pctᵢ) · ln(online_pctᵢ / ref_pctᵢ)

Rule of thumb: PSI < 0.1 stable · 0.1–0.25 watch · > 0.25 significant shift, investigate. Data-validation libraries (TFDV, Great Expectations, Deequ) automate the distribution checks; the log-and-replay loop is the gold standard.

Worked numbers — computing one PSI
Bucket a feature into 3 bins. The training (reference) distribution is [0.50, 0.30, 0.20]; the live online distribution drifted to [0.35, 0.30, 0.35] (bin 1 lost mass to bin 3). Term by term, (online − ref)·ln(online/ref):
(0.35−0.50)·ln(0.35/0.50) + (0.30−0.30)·ln(1) + (0.35−0.20)·ln(0.35/0.20)
= (−0.15)(−0.357) + 0 + (0.15)(0.560) = 0.054 + 0 + 0.084 = 0.138
PSI ≈ 0.14 — in the "watch" band: a real shift worth an alert, not yet an emergency. Notice the structure: a bin that didn't move contributes exactly zero, and the metric is symmetric-ish in the log term, so a feature that gained mass somewhere and lost it elsewhere accumulates from both. This is the same number whether the reference is the training distribution (then it measures skew) or yesterday's live distribution (then it measures drift) — one formula, two baselines.

The consistency toolchain — what to actually wire up

"One pipeline plus PSI" is the principle; the question an interviewer follows with is "with what tools?" Each tool below targets a specific way the train and serve paths diverge, and the point is the mechanism each one catches, not the brand name.

Tool / checkCatches which divergenceHow
Feature store + shared codeTwo implementations of one featureThe structural cure above: training and serving read the same materialized values via the same transform code
Protobuf / Parquet encodingCross-language encoding drift (Spark trainer ↔ Java server)A language-neutral wire/columnar format so the bytes mean the same thing on both sides
ONNX operator-compatibility checkAn op the serving runtime can't execute (or executes differently)Verify at export that every operator in the graph is supported by the target runtime — before, not after, it silently falls back
FP16/INT8 consistency testGPU float / quantization rounding errorRun the same inputs FP32 vs FP16/INT8, assert the output delta is within tolerance
TFDV distribution diffTrain-vs-serve feature distributions drifting apartCompute and diff per-feature distributions on both pipelines; flags schema/range anomalies automatically
KS test in shadow modeNew-vs-old model output distributions differingWhile shadowing (below), run a Kolmogorov–Smirnov test on the two score distributions — a significant statistic says the export changed behavior
Online calibration (per head)Multi-head score-scale drift after exportA thin post-hoc layer re-aligns each head's output scale, because INT8/FP16 export shifts per-head scales unevenly (the "numerical drift" skew above)
Feature contract / SLA (特征契约)An upstream producer silently changing a featureProducers commit to a stability SLA with mandatory change-notification, so a feature redefinition can't ship under you unannounced

The distance metric these checks lean on — PSI — is the one already derived above; TFDV and the KS test are just two ways of computing a distribution diff (PSI is the recsys default; cross-link, don't re-derive). The KS test answers a slightly different question than PSI: it asks "are these two samples drawn from the same distribution?" and gives a p-value, which is exactly what you want when comparing the new model's scores to the old model's in shadow mode.

Safe rollout — shadow → canary → ramp → rollback

You never flip 0% → 100%. The whole point of rollout is to bound the blast radius: at every stage the worst case is that a small, recoverable slice of traffic saw a bad model for a short time. Four stages, each answering a different question.

1 · SHADOW 2 · CANARY 3 · RAMP / A/B 4 · FULL "does it run?" "is it harmful?" "is it better?" "keep old hot 24h" ───────────── ───────────── ───────────── ───────────── 100% traffic mirrored 1–5% real traffic 5 → 20 → 50% real 100% new new model scores it, user-ID hash bucket, powered A/B test on old model retained output LOGGED not watch guardrails + core metric; advance as hot standby; returned to user first metric read only if significant one-click ROLLBACK ───────────── ───────────── ───────────── ───────────── └───── zero user risk ─────┘ └── significance before each step up ──┘

Rollback is a feature, and it must be automatic

The single most valuable piece of deployment infrastructure is a one-click — better, automatic — rollback. Define tripwires in advance and wire them to the traffic router.

Example auto-rollback tripwires
Roll back to the last stable version if any of: core metric (avg watch time) down ≥ 3–5% with significance · service error rate > 0.5% for 5 min · P99 latency over SLA for 5 min · crash/OOM detected. Pre-registering these turns a 2 a.m. incident into a non-event — the system reverts before a human reads the page. Model + feature-pipeline + experiment-config versions roll back together; a partial rollback (new model, old features) is its own outage.

The statistics of a canary

A canary at 1% traffic is also a statistical object: with little traffic you can only detect a large regression quickly. The minimum detectable effect scales like MDE ∝ σ / √n, so a 1% slice needs a bigger true regression — or more time — to flag than a 50% ramp does. That is by design: the canary's job is to catch catastrophes fast (large and obvious), and to hand the subtle "is it 0.5% better?" question off to the powered A/B at higher traffic. Don't ask a canary to prove a small win.

The incident playbook — when it's already on fire

Auto-rollback handles the cases you pre-registered. Real incidents are messier, and at 2 a.m. you want a decision tree, not improvisation. The discipline is always the same order: stop the bleeding first, diagnose second — isolate the bad version and switch to the stable backup before you start root-causing, because every minute spent debugging in production is a minute users see the bad model.

Playbook — CTR drops 30%
1 · Detect (tiered). A 15% drop fires a warning; 30% fires critical. 2 · Stop the bleed. Isolate the current model version, switch traffic to the last stable backup. 3 · RCA decision tree — walk it in this order:
CTR −30% │ ├─ recent model / feature change shipped? ──yes──▶ ROLL BACK that change (two-stage, below) │ │ no ├─ feature PSI > 0.25 AND GAUC down? ──yes──▶ DATA problem: pipeline broke │ │ (lost 埋点 / logging, JOIN failure, │ │ real-time feature stale) → fix pipeline, retrain │ │ no ├─ GAUC down but feature distributions OK? ──yes──▶ MODEL problem: bad version / calibration drift │ │ no └─ external: holiday / hot event / spam? ──yes──▶ not your bug — confirm with a segment drill-down
The branch order matters: a version that shipped right before the drop is the cheapest hypothesis to confirm and reverse, so you test it first. The PSI-vs-GAUC fork is the same data-vs-model localization from the monitoring section — data problems cluster in a segment, model problems are global.

Two-stage rollback — features first, then model

A model and its feature-engineering version are coupled: the model was fit to features produced a specific way. So a rollback is not one switch — you roll back in two stages, feature-engineering version first, then the model architecture. The reason is the partial-rollback trap from the auto-rollback tripwires: revert the model but leave new features (or vice-versa) and you've created a new untested combination — its own outage. Staging features-then-model lets you check after stage one whether the feature change alone caused the regression (often it did — a failure knowledge base will tell you feature issues like leakage are a large share of rollbacks) before you also touch the heavier model artifact.

Memory-leak diagnosis — the slow-burn incident

Not every incident is a sharp metric drop; a memory leak is a slow climb in RSS that ends in an OOM kill and a latency cliff. Same discipline, stop-then-diagnose:

Model versioning & hot updates

Three things must version together, because a mismatch between any two is an outage: the model weights, the feature-pipeline code that produces its inputs, and the serving/API contract. A model registry (MLflow Models, TF-Serving's versioned dirs, a custom registry) tracks this triple and is what rollback rewinds to.

Hot-swap matters because recommenders retrain constantly — sometimes hourly, with online/incremental learning (17 · Real-time) updating embeddings continuously. You can't take the service down to load a new model. The pattern: load the new version into memory alongside the old, validate it (health check, a few shadow requests), then atomically flip a pointer so in-flight requests drain on the old graph and new ones hit the new graph. Done wrong, the flip causes a latency glitch — a spike while the new graph's caches warm — which is why you warm it up with synthetic requests before flipping, and ramp rather than slam.

Online / nearline / offline — three update tiers, not one

"Retrain constantly" hides a real architectural decision: different parts of the model update at different speeds, because freshness and model complexity trade off against each other. You cannot fully retrain a heavy DNN every second, and you don't need to — most of its knowledge is stable. So production splits the update into tiers, each with its own cadence, model class, and transport. The rule of thumb: the faster the tier, the lighter the model.

TierCadenceModel classTransportJob it does
Onlinesecond-levellightweight (LR / FTRL)Flink streamingReact to breaking hotspots — a video going viral now
Nearlinehour-leveldeep multi-task (MMoE)mini-batch / SparkFuse long- and short-term interest as it shifts through the day
OfflineT+1 (daily)full DNNbatchThe heavy, stable retrain — the base the faster tiers correct

Feeding all three is a feature pipeline held to a < 1 s end-to-end SLO (Kafka → Flink → online store), because a second-level model tier is pointless if the features it reads are minutes stale — the same window-alignment discipline as 17 · Real-time & streaming. A concrete real-time feature: the user's short-term interest vector as the mean of their last few interaction embeddings, blended with the long-term vector.

The online tier is the dangerous one — guard it hard
A second-level update loop can chase noise into a bad state between human glances, so it carries its own tripwire: auto-rollback when CTR fluctuates beyond ±2% — tighter than the 3–5% tripwire for a full model launch, because the online tier moves faster and a runaway has no human in the loop. Two more guards from the online-learning loop: (1) swap new-into-old by parameter interpolation, θ = α·θ_new + (1−α)·θ_old with α ramped 0→1, so recommendations don't jump at the swap; and (2) periodically force a full retrain (the offline tier) to escape the local optimum an online loop can settle into from its own feedback. Fast and unsupervised is exactly the combination that needs the tightest rollback.

Monitoring — the metrics that predict an incident

Shipping is not the end; the model now lives in a non-stationary world — user behavior drifts, content changes, upstream pipelines break. Monitoring is layered, and the art is watching the upstream signals that move before the business metric, so you get a warning instead of an autopsy.

LayerMetricsWhat a move tells you
BusinessAvg watch time, CTR, CVR, completion rate, retention; diversity (category entropy)The verdict — but a lagging indicator. By the time CTR drops 30%, the damage is done
ModelOnline AUC/GAUC, prediction-score distribution (KL vs baseline), calibrationScore distribution shifting → the model is "seeing" a different world; precedes business impact
Feature / dataFeature drift (PSI), coverage / null rate, fetch latency, online-vs-offline skew gapThe earliest warning. A feature going null upstream shows here hours before the metric tanks
SystemQPS, P99 latency, error rate, CPU/GPU util, memory, fallback trigger rateHealth of the box; rising fallback rate = the model is silently being bypassed
The diagnostic question: model problem or data problem?
When a metric moves, the layered view localizes it fast. Feature PSI > 0.25 and GAUC down → data problem (a feature/pipeline broke). GAUC down but feature distributions normal → model problem (bad version, calibration drift). Regression appears the instant a version shipped and concentrates in one segment → deploy bug; gradual, global decay → concept drift, retrain. Drill down by segment (new vs returning, device, geo): data problems cluster in a dimension, model problems are global.

Alerting without crying wolf

The failure mode of monitoring is too many alerts — on-call fatigue makes a noisy system worse than none. The levers: dynamic thresholds (a baseline from a time-series forecast like Prophet, not a static line, so weekend/holiday cycles don't trip it), windowed triggers (fire only if the condition holds N minutes, not on one spiky sample), correlated suppression (one root cause → one alert, not fifty), and severity tiers (P0 page-now for "service down" vs P3 next-day for "minor wobble"). Audit rules periodically and retire any whose precision drops below ~80%.

Interactive · latency-budget allocator

You have a fixed serving budget. Drag the per-stage shares and pick the cuts you'd ship. The widget recomputes a toy P99 against the SLA and diagnoses what you optimized — and whether you optimized the right stage. The numbers are illustrative; the diagnoses are the point.

Spend the 80 ms ranking budget
Sliders set each stage's baseline cost (ms). The optimizations apply multiplicatively to the stage they affect. Watch which lever actually moves P99 — and read the diagnosis before you trust a number.

mean serving cost
est. P99 vs 80 ms SLA
dominant stage
verdict
Reading

Interview prompts you should be ready for

  1. "Your ranker is 30 ms over its P99 SLA. Where do you look first?" (Profile the chain, not the model. Feature fetch usually dominates — hundreds of small store reads. Cache hot embeddings / batch reads before you touch the matmuls. Optimizing the model when 55% of P99 is feature I/O is malpractice.)
  2. "Offline AUC was great, online metric is flat. What's your first hypothesis?" (Train–serve skew: a feature computed differently in the online path than the offline path. It is invisible to offline eval because both your tests use the offline pipeline. Confirm by logging the online feature vector and replaying it through the offline pipeline, then diff with PSI.)
  3. "Why P99 and not the mean, and what does fan-out do to it?" (A user is one request; the SLA is a tail promise. With parallel fan-out the request waits on the slowest sub-call, so a 1-in-100 slow read becomes a near-certain slow request — tail-at-scale. Hedged requests, tight timeouts + fallback, fewer critical-path deps.)
  4. "Walk me through a safe rollout." (Shadow at 100% mirrored, output logged not served — tests correctness/latency/skew at zero risk. Canary 1–5% real traffic for harm. Powered A/B ramp 5→20→50% for "is it better". Full with old model as 24 h hot standby. Pre-registered automatic rollback; model + features + config version together.)
  5. "Why can't a 1% canary tell you the new model is 0.5% better?" (MDE ∝ σ/√n — at 1% traffic you can only detect large regressions quickly. The canary catches catastrophes; the small-lift question goes to the powered A/B at higher traffic. Asking a canary to prove a small win is underpowered.)
  6. "A metric dropped. Model problem or data problem?" (Layered localization: PSI > 0.25 and GAUC down → data/feature pipeline broke; GAUC down but features normal → model/calibration; regression at the instant of a version ship, in one segment → deploy bug; gradual global decay → concept drift, retrain. Data problems cluster in a dimension; model problems are global.)
  7. "How do you hot-swap a model without a latency glitch?" (Load new version alongside old, health-check + a few shadow requests, warm the caches with synthetic traffic, then atomically flip a pointer — in-flight requests drain on the old graph, new ones hit the new graph. Skipping the warm-up causes a spike while the new graph's caches fill.)
  8. "Your embedding table is 256 GB and won't fit in RAM. How do you serve it?" (It's a TB-scale mutating KV store, not "the model." Shrink first: feature admission — admit a row only after N exposures — plus per-feature int8 takes 256 GB → ~13 GB. Tier hot rows in RAM/VRAM, cold on SSD; shard the rest by consistent hashing (only ~1/N keys remap on scale-up) with locality so a lookup doesn't fan out to every shard. Update via double-buffer atomic pointer flip so reads never wait on a write. Evict the unread long tail by LRU.)
  9. "How does a real-time recommender actually update — surely you don't retrain the DNN every second?" (Three tiers at different speeds: online second-level (lightweight LR/FTRL on Flink) for breaking hotspots; nearline hour-level (deep MMoE) for shifting interest; offline T+1 (full DNN) as the stable base. Faster tier ⇒ lighter model. Feature pipeline held to <1 s. The online tier is guarded hardest: auto-rollback at CTR ±2%, parameter-interpolation swap θ=α·θ_new+(1−α)·θ_old to avoid jumps, and a periodic full retrain to escape feedback-loop local optima.)
Takeaway
Deployment is a discipline whose failures are invisible to offline evaluation. The latency budget is the spec — profile the chain (feature fetch usually dominates), defend P99 not the mean. Train–serve skew is the default explanation for "great offline, flat online" — kill it with one shared feature pipeline plus log-and-replay PSI checks. Roll out shadow → canary → powered A/B ramp with pre-registered automatic rollback, versioning model + features + config as one unit. Then monitor in layers, watching the upstream drift signals that move before the business metric — that lead time is the difference between a warning and an autopsy.