The model’s output is a price, not a rank: each P(click) is multiplied by a bid, so calibration is a correctness requirement, not metric hygiene.
The one idea
- Ranking only cares about score order; an auction multiplies
pby a bid and compares it to a fixed floor and a budget controller. - Ranking is invariant to any monotone rescaling of
p. Pricing and the floor check are not, and both cost money. - Calibration: among all times the model says 0.02, ~2 in 100 really click. Separate from ranking.
- COPC = observed clicks / sum of predicted probabilities. 1.00 = calibrated, >1 under-predicts, <1 over-predicts.
The stack
- Light ranker: sparse LR, ~30 kFLOP, 1,000 candidates → top 100.
- Heavy ranker: wide-and-deep (780→1024→512→256→1), 1.45M dense params + 1.15 GB embeddings → one raw score in the downsampled world.
- Calibration layer (between ranker and money):
ln(w)offset + per-segment isotonic map → true-base-rateP(click). - Four bias-correctors: conversion+delay head, examination model (position), ad-level shrinkage prior, quality model.
Numbers to know
- Volume: 500M DAU × 40 impr = 2.0e10 requests/day; 231k QPS avg, ~700k peak.
- CTR ≈ 1%; CPC $0.45 → $90M/day, $32.9B/yr. Every “X% of revenue” = $32.9B × X%.
- Latency: ~20 ms scoring, inside 60 ms bidder, inside 100 ms exchange auction.
- Funnel: ~1,000 targeted → 100 heavy-scored → 1–5 shown.
eCPM = 1000 · p · bid— the currency the auction compares CPC and CPM bids in.
Why calibration bites
- Naive “wrong ad wins” fails: a uniform multiplicative bias
kcancels in GSP (price is a ratio,kon top and bottom cancels). - It doesn’t cancel where a modeled number meets a non-modeled one: (a) the fixed reserve/floor, (b) CPM bids that skip the model, (c) the auto-bidder (
bid = target_CPA · pCVR) and pacer, which usepas a number in its own right. - Real error is segment-structured (per traffic cell), not uniform. The auction takes the max of 100 → optimizer’s curse: the winner’s error is disproportionately positive.
- Systematic error > random error: a hot segment wins auctions persistently, so losses accumulate one direction.
Log-odds error s_e | Revenue vs optimal | Annual cost |
|---|---|---|
| 0.00 | 100.0% | — |
| 0.10 | 98.4% | $0.54B |
| 0.25 | 91.0% | $2.97B |
| 0.40 | 81.2% | $6.19B |
- Halving
s_efrom 0.25 → 0.10 recovers ~$2.42B/yr. Gate on per-segment COPC dispersion (a directs_eestimate), never global COPC (errors cancel to ~1.003).
Objective and the two on-purpose biases
- Loss: pointwise log loss, a strictly proper scoring rule (unique minimizer = true probability). A pairwise ranking loss is disqualified: its minimizer is any monotone transform of truth.
- Negative downsampling (compute only): keep positives, keep negatives at
w = 0.05. Correction is one constant:logit(p) = logit(p') + ln(w), withln(0.05) = -2.9957. Odds off by exactly 1/w = 20×; AUC unchanged. Costs only ~9% wider standard errors (Fisher info:p(1-p)is ~14× larger at 17% than at 1%). - Delayed conversions: a 24 h window mislabels 21% of positives as negative (
ln F(24h) = ln 0.79). Same algebra, but non-uniform by vertical (−4% games to −66% auto/finance). Fix: conversion head + delay head, or per-verticalln F(window)offset. - Then a per-segment isotonic map (PAVA, order-preserving) on each cell’s own un-downsampled, offset-corrected holdout. Collapses ~2× COPC spread to [0.98, 1.03], dispersion ~17× smaller.
Gotchas: forgetting the offset (invisible until revenue drops), double-correcting, and a segment-varying w under a global offset (manufactures the expensive segment error). Downsampling correction is exact only if negatives are dropped uniformly at random.
Model size from latency (before quality)
20 ms is a residual of the 100 ms wall clock, not a choice.
FLOP budget = 0.020 s × 60 GFLOP/s = 1.2 GFLOP/request
per candidate = 1.2 GFLOP / 100 = 12 MFLOP
params ceiling = 12 MFLOP / 2 FLOP = 6M parameters, and not one more
- Ships at 1.45M params = 2.91 MFLOP/candidate = 4.85 ms for 100 (¼ of budget). A 100M model is 333 ms = 17× over a hard external deadline (unbuildable here).
- Indexed vs traversed: the 1.15 GB embedding table is read at ~288 KB/request (indexed → free); the 5.8 MB MLP is traversed 100% every batch. “Make the model bigger” = make the embedding bigger.
- Batch the 100 into one matrix multiply, else weights stream from memory 100× and the system is memory-bound.
Features and failure modes
- High-cardinality categoricals → embeddings (16 dims). Don’t embed user id (128 GB, 86% of footprint; median user has 0 clicks so the vector decays to the prior) — use aggregate history features. Hash the rest into 2^24 buckets; collisions land in the Zipfian tail where they don’t matter (only 2.9% of head ads collide).
- The click log is not a sample of the world — only shown ads, only in their slots, only with arrived outcomes, only against gamed creatives:
| Failure | Detection | Control |
|---|---|---|
Missing ln(w) offset | COPC on un-downsampled holdout | Apply before auction; validate on true prior |
| Segment miscalibration | Per-segment COPC dispersion | Per-segment isotonic, grid fixed ahead |
| Position bias (click = examined AND relevant) | COPC by slot | Position feature held at serve + IPW |
| Cold-ad starvation | Share under 700 lifetime impr | 1.5% exploration (~0.73% of revenue) |
| Delayed conversions | pCVR COPC by vertical | Delayed-feedback likelihood or ln F(t) |
| Clickbait optimum | Post-click conversion / hide by CTR decile | pCTR · pCVR · value + quality multiplier |
| Online-training poisoning | Input-batch guardrails at 5 sd | 15-min checkpoints + batch fallback |
- AUC is structurally blind to all of this (invariant to every monotone transform). Report RIG, gate on COPC dispersion. Use grouped (per-auction) AUC 0.594, not pooled 0.812.