Module 22: ML system design and experimentation 50 min18 min quick readAI engineering
ML system design
Use a repeatable ten-step framework to design an ML system in an interview or at work, and apply it end to end to a multi-stage recommendation feed with a latency budget.
Before this: Workflow, splits, and leakage, Classification metrics and thresholds, Random forests and gradient boosting
Quick read: the intuition, interactive visualization, summary, and interview questions. The math, implementation, engineering, and knowledge-check sections are hidden.
Mental model. An ML system is a factory, not a model. Raw events come in, get turned into labels and features, feed a model, and the model's outputs go back out to users, whose reactions become the next day's raw events. Designing the system means designing every station on that loop and the budget each one gets, not just picking the fanciest model.
You will learn to
- Run a ten-step design framework, from clarifying the goal to monitoring, and spend 45 interview minutes on it sensibly.
- Separate business, offline, and online metrics, and explain why they disagree.
- Design a recommendation funnel (two-tower candidate generation with ANN search, a feature-rich ranker, a re-ranker) and size it against a latency budget.
- Spot and prevent training-serving skew, point-in-time leakage, position bias, and feedback loops.
- Correct predicted probabilities after negative downsampling.
Why it matters. "Design a feed / fraud / search system" is the standard ML engineer interview, and the same reasoning decides whether a real project ships. Most ML failures in production are system failures: a feature computed differently online than offline, a label that leaks the future, a metric that rewards the wrong thing. A strong design names those risks before they happen.
1. Intuition
The framework
Every ML design question, whatever the product, can be answered with the same ten steps. Interviewers grade the order as much as the content: jumping to "I'd use a transformer" before you know the goal is the most common way to fail.
| # | Step | The question you answer |
|---|---|---|
| 1 | Clarify goal and constraints | What does the business want? Scale (users, items, QPS)? Latency? Freshness? Fairness, privacy, or regulatory limits? |
| 2 | Frame the ML task | What exactly is predicted, for whom, and when? Classification, regression, ranking, retrieval? |
| 3 | Metrics | Business metric (revenue, retention), offline metric (AUC, nDCG, recall@k), online metric (CTR, watch time, complaints), and guardrails. |
| 4 | Data and labels | Where do labels come from (explicit, implicit, human)? How delayed and noisy are they? Is there bias in how they were collected? |
| 5 | Features | User, item, context, and interaction features. Which are real-time, which are batch? Can they be computed identically offline and online? |
| 6 | Model, baseline first | A heuristic or popularity baseline, then a simple model (logistic regression, GBDT), then deep models only if they earn their cost. |
| 7 | Training pipeline | Data snapshots, point-in-time joins, train/validation split by time, retraining cadence, reproducibility. |
| 8 | Serving | Batch or online? Latency budget per stage, caching, fallbacks, hardware. |
| 9 | Evaluation and launch | Offline evaluation, shadow mode, A/B test with guardrails, ramp-up. |
| 10 | Monitoring and iteration | Data and prediction drift, feature freshness, metric dashboards, alerting, retraining triggers, the next experiment. |
Spending 45 minutes
| Minutes | Steps | What a good answer sounds like |
|---|---|---|
| 0 to 5 | 1 | Asks 3 to 5 clarifying questions, states assumptions out loud ("say 100M daily users, 10k QPS peak, 200 ms p99") |
| 5 to 10 | 2, 3 | Writes down the prediction target and the three metric layers before any model talk |
| 10 to 17 | 4, 5 | Names the label source and its biases; sketches feature groups and which need a feature store |
| 17 to 25 | 6 | Baseline, then the real design; justifies each stage with a number |
| 25 to 32 | 7, 8 | Training data flow; serving path with a latency budget; batch versus online |
| 32 to 40 | 9, 10 | Offline gate, A/B plan, monitoring, what could go wrong |
| 40 to 45 | Deep dive wherever the interviewer pushes; summarize trade-offs |
Engineering noteDraw the diagram early (boxes for data sources, feature pipeline, training, model registry, serving, logging) and keep pointing at it. It turns a ramble into a tour.
The worked case: a recommendation feed
Clarify. A short-video app wants a personalized home feed. Assume 10M items in the catalog, 10,000 requests per second at peak, a 150 ms server-side budget at p99, and 20 items returned per request. Goal: long-term engagement (daily active users), not just clicks.
Frame. For a (user, context) pair, rank the catalog and return the top 20. In practice that is a ranking problem solved as pointwise prediction: estimate for several engagement types (click, watch past 50%, like, share, hide) and combine them into one score with business weights.
Metrics.
| Layer | Examples | Why it exists |
|---|---|---|
| Business | 7-day retention, daily active users, session time | what the company actually wants; slow and noisy |
| Online (A/B) | watch time per user, CTR, hides and reports per 1,000 impressions | measured on real traffic in days |
| Offline | AUC and log loss per head, recall@k for candidate generation, nDCG@20 | measured in minutes on logged data; decides what earns an A/B slot |
| Guardrails | p99 latency, error rate, creator diversity, policy-violating content rate | must not get worse even if the main metric improves |
Data and labels. Labels are implicit: impressions joined with later clicks, watches, likes, and hides. They arrive with a delay (a watch-time label is final only when the session ends), they only exist for items the old system chose to show (selection bias), and they are skewed by where the item was shown (position bias).
Features. User (history embeddings, long-term topic affinities, account age), item (content embedding, creator, age, recent engagement rates), context (time of day, device, network), and cross features (user's past engagement with this creator). Real-time counters such as "views of this item in the last hour" come from a streaming job and a feature store.
Model: a three-stage funnel. You cannot run a rich model over 10M items in 150 ms, so the system narrows the catalog in stages, each one more expensive per item and run on fewer items.
- Candidate generation (retrieval). A two-tower model maps the user and each item to vectors; an approximate nearest neighbor (ANN) index returns the ~1,000 items whose vectors have the highest dot product with the user vector. Several sources usually run in parallel and are merged: two-tower, "more from creators you follow", trending in your region, fresh uploads. Goal: high recall, cheap per item.
- Ranking. A gradient-boosted tree model or a deep network (often multi-task, one head per engagement type) scores each of the ~1,000 candidates using hundreds of features, including cross features the two-tower model cannot express. Goal: precision at the top.
- Re-ranking. Business logic and slate-level objectives on the top ~100: diversity (no five videos from one creator in a row), freshness boosts, deduplication, policy filters, ads insertion. Returns the final 20.
Baseline first: before any of this, "most popular in your region in the last 24 hours" is a real system that ships in a day and gives every later model something to beat.
Training pipeline. Daily (or hourly) jobs build training examples by joining logged impressions with labels and with the feature values as they were at impression time. Split train and validation by time, never randomly. Retrain the ranker daily; refresh the item tower and ANN index more often than the user tower when new items arrive constantly.
Serving. Online, because the feed depends on what the user did a minute ago. Precompute item embeddings in batch, compute the user embedding per request, cache aggressively, and set a latency budget per stage (worked out in section 3). If ranking times out, fall back to the candidate generator's order; if everything fails, serve the popularity baseline.
Evaluation and launch. Offline gate (AUC, log loss, and nDCG on the most recent days), then a shadow deployment to check latency and score distributions, then an A/B test on watch time with guardrails (see Experimentation and A/B testing), then a staged ramp.
Monitoring. Feature freshness and null rates, prediction distribution versus training, calibration per head, per-stage latency, and the online metrics by segment (new users, new items, regions).
Two more cases in one table
| Decision | Feed recommendation | Fraud detection | Search ranking |
|---|---|---|---|
| Prediction | per item, several heads | per transaction | relevance of a document to a query |
| Labels | implicit, plentiful, biased by exposure | chargebacks and analyst reviews; weeks of delay; ~0.1% positive | human relevance ratings plus clicks (position-biased) |
| Key metric | watch time, retention | recall at a fixed false-positive rate, dollars of fraud caught | nDCG@10, query success rate, abandonment |
| Serving | online funnel, ~100 to 200 ms | synchronous scoring in the payment path, often under 50 ms, with a rules engine alongside | retrieval (inverted index plus vectors) then a learned-to-rank model |
| Model | two-tower plus GBDT or DNN ranker | GBDT on aggregates (velocity counts, device and graph features) | BM25 plus dense retrieval, then a cross-encoder or GBDT ranker |
| Special risk | feedback loops, popularity bias | adversaries adapt, so drift is intentional; label delay | queries with no clicks, freshness for news |
2. Visualization
Recommendation funnel
Synthetic 10M-item catalog. Choose how many items each stage passes on and how expensive each stage is per item; watch latency, compute, and recall.
Latency per request (bar) against the budget (dashed line)
Try it (predict first, then check):
- Click Rank 5,000. Predict whether recall goes up a lot or a little, and by how many milliseconds you overshoot the budget.
- Click Starved ranker (only 200 candidates). Latency drops; which stage now loses the most relevant items?
- Click Heavy ranker (0.2 ms per item). Find the largest k1 that still fits 150 ms, and compare its recall with the default.
- Set the budget to 100 ms. Is it cheaper in recall to cut k1 or to cut k2?
3. The math
Symbols
| Symbol | Meaning | Shape / unit |
|---|---|---|
| catalog size | items | |
| items kept by candidate generation, ranking, re-ranking | items | |
| per-item cost of stage | ms per item | |
| fixed cost of stage (feature fetch, RPC) | ms | |
| , | total latency and latency budget | ms |
| user tower output | ||
| item tower outputs for the catalog | ||
| synthetic recall scale of stage | items | |
| fraction of negatives kept when downsampling | scalar in | |
| , | model output after downsampling, calibrated probability | scalars |
Two-tower scoring
Because the score is a dot product, all item vectors can be computed offline and put in an ANN index (graph-based such as HNSW, or quantized such as IVF-PQ). At request time you compute one user vector and ask the index for the top by dot product, in roughly logarithmic rather than linear time in . The price: user and item interact only through one dot product, so the model cannot express cross features like "this user likes this creator, but only at night". That is the ranker's job.
Training typically uses in-batch negatives with a softmax over the batch. Popular items appear as negatives more often than they should, so a common fix subtracts (the item's sampling probability) from its logit during training.
Funnel cost model
where is the number of items stage processes. Candidate generation pays per returned candidate, ranking scores all candidates, and re-ranking scores the top . Compute per request (busy worker-milliseconds) is . This is a one-worker model with no sharding; real systems parallelize, which lowers latency but not compute.
For the lab, each stage keeps relevant items with a synthetic saturating curve, and end-to-end recall is the product:
Worked example 1: the latency budget
Constants (synthetic): overhead 20 ms; candidate generation , ; ranking , ; re-ranking , . Keep , , . Budget ms.
| Stage | Items processed | Latency | Stage recall |
|---|---|---|---|
| Candidate generation | 1,000 returned | ms | |
| Ranking | 1,000 scored | ms | |
| Re-ranking | 100 scored | ms |
Total: ms. Headroom: ms. End-to-end recall: .
Now double the candidates to : candidate generation becomes ms and ranking ms, so ms, 40 ms over budget, while only moves from 0.9817 to .
Worked example 2: why not rank everything?
Ranking the full catalog: ms s per request. The funnel exists because that number is four orders of magnitude over budget.
Worked example 3: capacity
Compute per request: worker-ms. At 10,000 QPS: worker-ms per second worker-seconds per second, so about 750 workers fully busy, before headroom for peaks and failures. Halving the ranker's per-item cost (distillation, fewer features) saves 30 worker-ms per request, which is 300 workers.
Worked example 4: calibration after negative downsampling
Clicks are rare, so training sets often keep all positives and only a fraction of negatives. That multiplies the odds of a positive by , so the model's output is too high. Undo it in odds space:
With (keep 10% of negatives) and : odds , times gives , so . Check with the closed form: . With : .
Ranking order is unchanged by this correction (it is monotonic), so it matters only when the probability itself is used: ad auctions (bid pCTR), combining several heads with weights, or thresholds.
4. Implementation
Two of the system failures named above, point-in-time leakage and training-serving skew, are invisible in a design diagram and obvious in code. This section builds them from scratch on synthetic purchase data, then shows the production version.
Build it from scratch
The script is code/22-ml-system-design/feature_skew_scratch.py (stdlib only, seeded synthetic users). The task: predict whether a user churns (no purchase in the next 30 days).
One feature definition. The function takes as_of and only looks at events on or before it. Every feature in a real feature store has this shape: a definition plus a timestamp. Training and serving both call the same function, which is the whole defense against skew.
def days_since_last(days, as_of):
"""Days since the user's last purchase, seeing only events on or before `as_of`."""
seen = [d for d in days if d <= as_of]
return float(as_of - max(seen)) if seen else 365.0
def purchases_30d(days, as_of):
return float(sum(1 for d in days if as_of - 30 < d <= as_of))Predict: a user with no purchases yet gets 365. What would happen to the model if offline used 365 but the online service used 0 for the same case?
Labels look into the future, on purpose. A label at day t asks about days t+1 to t+30. That is fine for a label. It becomes a leak only if a feature also sees past t.
def make_labels(events, label_days, window=30):
"""Label = 1 (churned) if the user makes no purchase in the 30 days AFTER the label day."""
return [(u, t, 0 if any(t < d <= t + window for d in days) else 1)
for u, days in sorted(events.items()) for t in label_days if any(d <= t for d in days)]Predict: if you computed days_since_last at day t + 30 instead of t, what value would almost every churned user get, and why would accuracy look perfect?
Two ways to join features to labels. The offline feature table holds one snapshot per user per week. The point-in-time join takes, for each label, the latest snapshot at or before the label day (binary search over the snapshot days). The naive join takes the user's latest snapshot, which for old labels is from the future.
def point_in_time_join(labels, table):
"""For each label at day t, take the latest snapshot with day <= t. Never a later one."""
out = []
for u, t, y in labels:
snaps = table[u]
i = bisect.bisect_right([d for d, _ in snaps], t) - 1
out.append((snaps[i][1] if i >= 0 else 365.0, y))
return out
def naive_latest_join(labels, table):
"""The classic leak: join every label to the user's LATEST snapshot, which can be from the future."""
return [(table[u][-1][1], y) for u, t, y in labels]Predict: the naive join's training accuracy is about the same as the honest one here. Why can a leak hurt badly even when it doesn't inflate the training score? (Look at the threshold each one learns in the output below.)
Skew detection. Replay the same (user, time) requests through the offline definition and the online one, and count disagreements. Here the "online" path is a legacy reimplementation with <= instead of <, a 31-day window. Nothing crashes; the numbers are just quietly different.
def legacy_purchases_30d(days, as_of):
return float(sum(1 for d in days if as_of - 30 <= d <= as_of)) # bug: 31-day window (<= vs <)
def skew_rate(events, requests, offline_fn, online_fn, tol=1e-9):
diffs = [abs(offline_fn(events[u], t) - online_fn(events[u], t)) for u, t in requests]
return sum(d > tol for d in diffs) / len(diffs), sum(diffs) / len(diffs)Predict: the mismatch rate is the fraction of requests with a purchase exactly 30 days before. Roughly what rate would you expect if users buy on about 20% of days?
A baseline gate. Step 6 of the framework as code: a model ships only if it beats the best trivial baseline by a margin you set in advance.
def beats_baselines(model_acc, baseline_accs, min_lift=0.02):
best = max(baseline_accs, key=baseline_accs.get)
return best, model_acc - baseline_accs[best], model_acc - baseline_accs[best] >= min_liftPredict: on a dataset that is 95% "no churn", what accuracy does the majority-class baseline get, and why does that make accuracy a poor primary metric there?
Use the library
In production the definitions live in a feature store such as Feast, and the same registry serves both paths. Feast's offline get_historical_features does the point-in-time join against an entity table of (entity, timestamp) rows; get_online_features reads the latest materialized values for serving:
from feast import FeatureStore # pip install feast
store = FeatureStore(repo_path=".")
train_df = store.get_historical_features( # point-in-time join, offline
entity_df=labels_df, # columns: user_id, event_timestamp, label
features=["user_stats:days_since_last", "user_stats:purchases_30d"],
).to_df()
online = store.get_online_features( # low-latency read, serving
features=["user_stats:days_since_last", "user_stats:purchases_30d"],
entity_rows=[{"user_id": "u042"}],
).to_dict()The funnel that consumes these features is a chain of narrowing calls, each with its own timeout and fallback:
def recommend(user_id, ctx, k1=1000, k2=100, k3=20):
u = user_tower(feature_store.get_user(user_id), ctx) # [d]
cands = merge(ann_index.search(u, k1), follows(user_id), trending(ctx.region))
feats = feature_store.get_items([c.id for c in cands], user_id, ctx) # [k1, n_features]
try:
scores = ranker.predict(feats, timeout_ms=80) # [k1, n_heads]
except Timeout:
scores = [c.retrieval_score for c in cands] # fallback: retrieval order
top = top_k(cands, combine(scores, weights=HEAD_WEIGHTS), k2)
return rerank(top, rules=[dedupe, diversify_by_creator, policy_filter], k=k3)The single most important line is feature_store.get_items: the same feature definitions must produce the training data, or the model will see different numbers in production than it learned on. The library does not remove the need for the skew check above. Log the served feature vector with each prediction and compare it with the offline value for the same request.
Run it
From guide/:
uv run code/22-ml-system-design/feature_skew_scratch.pysynthetic: 300 users, 3895 train labels, 1200 test labels
point-in-time join: threshold 18 days, train acc 0.912, test acc 0.911
naive latest join : threshold 97 days, train acc 0.914, test acc 0.646
skew, shared definition : mismatch rate 0.000, mean |diff| 0.000
skew, legacy 31-day window: mismatch rate 0.188, mean |diff| 0.188 -> flag (> 0.01)
baseline gate: best baseline 'majority class' acc 0.617, lift +0.294 -> ship=TrueThe leaky join trains to the same accuracy but learns a 97-day threshold, because its features come from the end of the data where churned users have been idle for months. Scored on honest features, that rule falls to 0.646, barely above the 0.617 baseline. Offline metrics did not warn you; only a time-correct evaluation did.
The latency, capacity, and recall arithmetic from section 3, the calibration correction, and this lesson's figure are in code/22-ml-system-design/recsys_funnel.py.
5. Engineering
Training-serving skew. Training-serving skew is any difference between the features or preprocessing seen at training time and at serving time: a feature computed in SQL offline and in Java online, a different tokenizer version, a missing-value default of 0 offline and -1 online, a counter that is hourly offline and real-time online. Fixes: define each feature once in a feature store that serves both paths; log the exact feature vector served with each prediction and train on those logs; compare online and offline feature distributions continuously.
Point-in-time correctness. A training example for an impression at 10:00 must use feature values as of 10:00. Joining today's "user's total likes" onto last month's impressions leaks the future: the label (did they like it?) is partly inside the feature. Feature stores provide point-in-time ("as-of") joins for exactly this reason.
Label leakage. Beyond time travel, watch for features that are consequences of the label: "session length" when predicting a watch, "refund issued" when predicting fraud, an ID that encodes the outcome. A sudden offline AUC of 0.99 is a data leakage alarm, not a celebration.
Position bias and feedback loops. Users click the top slot partly because it is the top slot. A model trained naively on clicks learns "whatever we showed first is good", and then shows it first again: a feedback loop that entrenches popular items and starves new ones. Mitigations: add position as a training feature and set it to a fixed value at serving time, learn propensities by occasionally randomizing positions (inverse propensity weighting), and reserve a small exploration slice of traffic.
Cold start. Cold start is the lack of interaction history for new users or items. New items: rely on content features (embeddings of the video, title, creator) in the item tower, plus a freshness boost or exploration budget in re-ranking. New users: use context (country, device, sign-up answers) and popular items, then adapt quickly from the first few interactions with real-time features.
Class imbalance and negative sampling. With CTRs near 1%, keep all positives and downsample negatives, then recalibrate (worked example 4). For the two-tower model, "negatives" are usually other items in the batch or random catalog items; hard negatives (items shown but skipped) sharpen the model but must be mixed with easy ones.
Calibration. Multi-task rankers combine heads as ; that sum is only meaningful if each is a real probability. Monitor calibration (predicted versus observed rate per score bucket) per head and per segment, and recalibrate with isotonic or Platt scaling on recent data.
Offline-online mismatch. Offline metrics are computed on logs collected by the old system, so they can only judge items the old system chose to show (selection bias); they ignore position effects, latency, and slate interactions; and they measure short-term clicks, not long-term retention. Expect some offline wins to lose online. Track how well offline deltas have predicted online deltas over past launches, and use that history to decide how much to trust a new offline result.
Batch versus online prediction. If recommendations can be a few hours stale (a weekly email, "items you might like" on a low-traffic page), precompute them in batch: cheaper and simpler. Use online inference when the context changes per request (search queries, the current session, fraud at payment time).
Watch outFailure modes. A feature pipeline silently returns defaults after an upstream schema change; the ANN index is rebuilt with a new item tower but the old user tower (vectors from different models are incompatible); a popularity feedback loop collapses diversity; delayed labels make yesterday's data look like it has fewer positives, so a daily retrain learns a lower base rate.
Common mistakes
- Naming a model before stating the goal, the prediction target, and the metric.
- Splitting train and validation randomly on time-ordered data, which leaks the future.
- Optimizing the offline metric only and calling it done, with no plan for online evaluation or guardrails.
- Forgetting that downsampled training data produces miscalibrated probabilities.
- Ignoring the latency budget: proposing a cross-encoder over 10M items.
- No baseline, so nobody knows whether the complex system is worth its cost.
6. Knowledge check
Quiz: ML system design
Put these ML system design steps in the order you should cover them in an interview.
- 1.Data, labels, and features
- 2.Baseline, then model choice
- 3.Training pipeline and serving
- 4.Evaluation, launch, and monitoring
- 5.Clarify the goal and constraints
- 6.Frame the ML task and choose metrics
Practice exercise. Your feed ranker scores 1,500 candidates at 0.04 ms each with 15 ms fixed cost. Candidate generation costs ms, re-ranking scores 100 items at 0.1 ms each plus 3 ms, and overhead is 20 ms. Does the system fit a 150 ms budget, and how many workers does it need at 10,000 QPS?
Solution
Candidate generation: ms. Ranking: ms. Re-ranking: ms. Total: ms, so it fits with 22.5 ms of headroom. Compute: worker-ms per request; at 10,000 QPS that is busy workers. A cheaper ranker let you pass 50% more candidates for about the same cost.
Implementation challenge. Extend recsys_funnel.py with a fourth stage, a "light ranker" between candidate generation and the heavy ranker (for example, 5,000 candidates in, 500 out, 0.004 ms per item). Find a configuration that raises synthetic recall above the three-stage default while staying under 150 ms.
Solution sketch
Add a stage with its own , let candidate generation return 5,000 items (cost ms), let the light ranker score them ( ms) and keep 500, and let the heavy ranker score 500 instead of 1,000 ( ms). With : ms. Candidate recall rises to ; whether the total rises depends on the light ranker's . The pattern (cascade of ever more expensive models on ever fewer items) is exactly why production systems often have four or more stages.
Summary
- Answer every design question in the same order: goal and constraints, task framing, metrics, data, features, baseline then model, training, serving, launch, monitoring.
- Keep three metric layers apart (business, online, offline) plus guardrails, and expect offline and online results to disagree sometimes.
- Recommendation systems are funnels: cheap high-recall retrieval (two-tower plus ANN), an expensive precise ranker, then re-ranking for diversity and business rules, each sized against a latency budget.
- Most production failures are data failures: training-serving skew, point-in-time leakage, position bias and feedback loops, delayed labels.
- Downsampling negatives requires recalibration: .
Next: Experimentation and A/B testing
Related: ML workflow · Classification metrics · Random forests and boosting · Production architecture · Cheat sheet: ML system design · Model card: two-tower model
Go deeper, code-first (AI Engineering from Scratch): Graph Theory for Machine Learning · Design Success Metrics Before the Result Exists · Model, System, and Dataset Cards
Parts of this lesson are adapted from AI Engineering from Scratch by Rohit Ghumare (MIT license, notice): ML System Design — From Problem to Deployed Model, Feature Stores and Training-Serving Skew.
Interview angle
Why do recommendation systems use a multi-stage funnel instead of one big model?
Because no single model can be both accurate and cheap enough to score millions of items within a ~100 to 200 ms budget. The funnel spends compute where it matters. Candidate generation, usually a two-tower model with an ANN index, cuts 10M items to about 1,000 in a few milliseconds, because item vectors are precomputed and the search is sublinear. Its job is recall. A ranker with hundreds of features and cross features then scores only those 1,000; its job is precision at the top. Re-ranking applies slate-level rules such as diversity, freshness, and policy. In numbers: ranking 10M items at 0.06 ms each would take 600 seconds per request, while ranking 1,000 takes 60 ms. The trade-off is that anything retrieval misses can never be recovered later, so you monitor candidate recall separately.
When would you choose a GBDT ranker over a deep neural ranker?
Choose GBDT when features are mostly tabular and hand-engineered (counts, rates, recency, categorical IDs with modest cardinality), data is in the millions rather than billions of rows, and you need fast iteration, CPU serving, and easy feature importance. Gradient-boosted trees are strong on heterogeneous tabular data with little tuning, and are a common first ranker. Choose a deep ranker when you have huge logs, high-cardinality sparse IDs that benefit from learned embeddings, raw content inputs (text, images, sequences of past actions), or several related objectives you want in one multi-task model with shared layers. The deep model costs more to train and serve, usually on GPUs, and is harder to debug. A common path is GBDT first, then a DNN once the data and team can support it, with the GBDT as the baseline it must beat online.
Your new ranker beats the old one on offline AUC but loses the online A/B test. What do you check?
First, training-serving skew. Log the features served online for a sample of requests and compare them with the offline features for the same impressions; a default value or a stale counter often explains everything. Second, leakage. Check that offline features were point-in-time correct, since a leaked feature inflates offline AUC and vanishes online. Third, latency. A slower model can time out and fall back to retrieval order, or slow the page enough to hurt engagement, so check p99 and fallback rates per arm. Fourth, the metric gap. AUC measures pairwise ordering on items the old system chose to show, not the top-20 slate, and it ignores position bias and long-term effects. Compare nDCG@20 and calibration, and segment the A/B results. Finally, run sanity checks on the experiment itself, such as sample ratio mismatch and guardrails.
You trained a CTR model keeping 5% of negatives. It outputs 0.3 for an ad. What CTR do you use in the auction?
About 2.1%. Keeping a fraction of negatives multiplies the odds of a click by , so you multiply the predicted odds by to undo it. Predicted odds are . Times gives . Converting back, . Equivalently, . Ranking by or gives the same order, so a pure ranking metric would never reveal the problem. But an auction computes bid times pCTR and compares across campaigns and models, so an uncorrected 0.3 would overcharge advertisers by more than ten times. After correcting, verify calibration on fresh unsampled traffic, predicted versus observed CTR per bucket, because the correction assumes the downsampling was uniformly random.