C04 — Production Model Optimization¶
Track C · Module 04 · Status: DONE · Est. 7 hours · Depth 3/5
Prerequisites: C03 Training systems, and C01 Problem framing for the metric discipline this module's quality gate depends on. Cross-references B04 Precision & quantization and B06 Inference serving optimizations. Backends: none — this is a design module. The only executables are standard-library Python 3 tools in
tools/. No GPU, no framework, no network required. Terms: GLOSSARY.md · Sources: REFERENCES.md.
1. TL;DR + Layman analogy¶
TL;DR. You have a trained model that is too slow or too expensive to ship. There is an ordering to the fixes, and it is cheapest-and-least-risky first: don't call the model at all (caching, cascades, early exit) → serve it better (batching, runtime, graph capture) → shrink the numbers (quantization) → shrink the model (distillation, pruning, architecture change). Engineers habitually invert this ordering, because the bottom rung is the interesting research problem and the top rung is plumbing. Every rung below the second changes the model's outputs, which means every one of them must pass a task-level evaluation before it ships — and "the loss looked fine" is not that evaluation. The unit of decision throughout is cost per 1,000 requests, compared against the engineering weeks it takes to get there.
Layman analogy — cutting a household energy bill. You turn off the lights in rooms nobody is in. Then you fix the door that doesn't seal. Then you swap the bulbs for LEDs. Only then do you consider tearing out the walls to add insulation. The ordering is not about how much each saves — insulation may well save the most — it is about cost, risk, and reversibility. Switching off a light is free and undoable in a second. Ripping out a wall is expensive, takes weeks, and if you get it wrong you are living in a construction site. So you take the cheap reversible wins first, measure what is left, and only then decide whether the wall is worth it.
Where the analogy breaks — three places worth knowing: 1. A light switch can't damage the house; a model change can damage the product. Every rung below "serve it better" alters what the model outputs. There is no equivalent in the bill analogy of "the lights are now 30% cheaper but occasionally illuminate the wrong room." That asymmetry is why this module spends as much time on the quality gate as on the optimizations. 2. Your bill doesn't fight back. Serving cost depends on traffic, which changes daily, and on cache hit rates that decay as your users' behaviour shifts. Optimizations that depend on traffic shape (caching, cascades) need monitoring, because they silently stop working. 3. The house doesn't get rebuilt every six weeks. Model versions turn over fast. An optimization that took a quarter and is coupled to one specific checkpoint may be obsolete before it lands — which is itself an argument for the cheap, model-agnostic rungs.
By the end of this module you can: order the optimization ladder for a specific situation and justify the ordering; explain why compression ratio is not speedup; design a quality gate with slices, thresholds, canary, and rollback trigger; compute expected cost and quality for a cascade; and build the business case — including the case where the correct answer is "don't do it."
2. First Principles¶
The quantity you are actually minimising¶
monthly_cost = requests_per_month × cost_per_request
cost_per_request = (compute_per_request) / (throughput_per_dollar)
Three separate levers hide in that identity, and they are not equally easy:
| Lever | Mechanism | Changes model outputs? |
|---|---|---|
| Fewer requests reach the model | cache, cascade, early exit, precompute | no (cache) or yes, controllably (cascade) |
| Less compute per request | shorter context, smaller model, fewer tokens | yes |
| More throughput per dollar | batching, better kernels, quantization, cheaper hardware | no (batching) or yes (quantization) |
The first row is the one people skip. It is also the only one that can deliver an unbounded win: a 90% cache hit rate removes 90% of the work no matter how well-optimised the remaining 10% is. You cannot get a 10× speedup out of a kernel; you can get it out of not running the kernel.
Derive the ladder from cost × risk × reversibility¶
Rank every optimization on three axes, and the ordering falls out on its own:
| Rung | Engineering cost | Quality risk | Reversible? | Coupled to the model? |
|---|---|---|---|---|
| 1. Don't call the model | low | none (exact cache) to low (cascade) | yes — flip a flag | no |
| 2. Serve it better | low–medium | none — same weights, same outputs | yes — redeploy | mostly no |
| 3. Shrink the numbers | medium | small, measurable | yes — ship the fp16 weights | yes |
| 4. Shrink the model | high | real — needs retraining + full eval | no — the old model is a rollback, the work is not | entirely |
The ordering is not "biggest win first". It is best expected value per unit of risk-adjusted effort, and that is a defensible principle in a design review even when the interviewer pushes back with "but distillation gives 3×". The reply: "It might. It also costs a quarter, needs a full eval, and is dead the next time we change the base model. Show me the cache hit rate first — if it's 40%, I get most of that win in two weeks with no quality risk at all."
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#F8FAFC',
'primaryTextColor': '#0F172A',
'primaryBorderColor': '#0891B2',
'lineColor': '#64748B',
'secondaryColor': '#E0F2FE',
'tertiaryColor': '#EEF2FF',
'fontFamily': 'Inter, Segoe UI, sans-serif'
}
}}%%
flowchart TB
R1["Rung 1 - Do not call the model<br/>cache, precompute, early exit, cascade"]
R2["Rung 2 - Serve it better<br/>batching, runtime, graph capture"]
R3["Rung 3 - Shrink the numbers<br/>weight, activation and KV-cache quantization"]
R4["Rung 4 - Shrink the model<br/>distillation, pruning, architecture change"]
R1 --> G1{"Cost target met?"}
G1 -->|no| R2
G1 -->|yes| SHIP["Ship. Stop optimizing."]
R2 --> G2{"Cost target met?"}
G2 -->|no| R3
G2 -->|yes| SHIP
R3 --> G3{"Cost target met?"}
G3 -->|no| R4
G3 -->|yes| SHIP
R3 -.->|"must pass"| GATE["Task-level quality gate<br/>slices, canary, rollback"]
R4 -.->|"must pass"| GATE
classDef trackC fill:#F59E0B,stroke:#0F172A,color:#0F172A
classDef neutral fill:#F8FAFC,stroke:#0891B2,color:#0F172A
classDef success fill:#10B981,stroke:#0F172A,color:#fff
classDef warn fill:#FEF3C7,stroke:#F59E0B,color:#0F172A
class R1,R2,R3,R4 trackC
class G1,G2,G3 neutral
class SHIP success
class GATE warn
The load-bearing part of this diagram is the Ship. Stop optimizing. node: at every rung the
default is to stop, and the burden of proof is on continuing — which is the opposite of how these
projects usually run.
The naive version, and where it breaks¶
The naive approach is "the model is too slow, so make the model smaller." It breaks in four ways, each of which names a section below:
| Naive assumption | Reality | Section |
|---|---|---|
| every request needs the model | traffic is skewed; a cache serves the head | §3.1 |
| the model is the cost | often the runtime, batching, or padding is | §3.2 |
| smaller means faster | 90% unstructured sparsity can be no faster at all | §3.4 |
| the loss confirms it still works | loss is not the task; harm hides in slices | §3.7 |
3. Deep Dive¶
3.1 Rung 1 — Don't call the model¶
Exact-match caching. Hash the request (prompt, parameters, model version) and return the stored
response. Costs a lookup; saves the entire inference. Its effectiveness is decided by one property of
your traffic: skew. If request popularity follows something Zipf-like, a cache holding a tiny
fraction of the key space captures a large fraction of traffic. tools/cache_simulator.py makes this
concrete: on a seeded Zipf trace with exponent 1.0 over 20,000 distinct keys, a 2,000-entry cache
(10% of the key space) serves about 70% of requests, while the same cache on uniform traffic serves
about 10%. Same cache, same size — the entire difference is the skew. Measure the exponent of
your real request log before you build anything.
What breaks it: personalisation (per-user keys destroy skew), staleness (the right answer changes), sampling parameters that vary per request (temperature > 0 means the "same" request has many valid answers), and cold start after every deploy. And one security property to state explicitly in a design review: a shared cache keyed carelessly can return one user's answer to another. Key on everything that affects the output, including tenant and permission scope.
Semantic caching — embed the request, return a stored answer for a near match. Higher hit rate, and a genuinely different risk class: a false hit returns a plausible but wrong answer, and nothing in your latency or cost metrics will tell you. It needs the same quality gate as a model change (§3.7). Related and lower-risk: prefix caching in LLM serving, where the shared prefix of many requests is computed once — that is a serving mechanism owned by B06 §3, and it is exact rather than approximate.
Precomputation. If the input space is small and known — the top 10,000 products, the standard report queries — compute offline and serve from a table. Latency becomes a database read. The cost is freshness and storage.
Early exit and cheap filters. Many requests do not need the model at all: a regex, a lookup, a business rule, or a confidence threshold on a tiny classifier can answer them. This is the cheapest possible optimization and the one most often overlooked, because it feels like it is not ML.
3.2 Rung 2 — Serve it better¶
The distinguishing property of this rung: the weights do not change, so the outputs do not change, so there is no quality risk. That makes it the highest-value-per-unit-risk work in the module.
- Batching. GPU inference at batch size 1 leaves most of the hardware idle.
Continuous / in-flight batching keeps the device
fed. The mechanics, the latency-throughput tradeoff, and
KV cache paging belong to
B06 —
this module only cares that batching is a throughput multiplier you can measure and feed into
tools/cost_model.py. - A better runtime. Optimized kernels, fused attention, better memory layout. Same idea: measure the multiplier, don't assume it.
- Graph capture and operator fusion. Capture the execution graph once and replay it, removing per-op launch overhead. This matters most exactly where you would least expect — small models and small batches, where launch overhead is a large fraction of the step. For a big model at a big batch it may do nothing.
- Right-sizing the fleet. Before optimizing anything, check utilization. If your servers run at
40% because of rounding and headroom, you are paying for idle capacity, not for an inefficient
model, and consolidation is a config change.
cost_model.pyprints baseline utilization for exactly this reason.
3.3 Rung 3 — Shrink the numbers¶
Quantization reduces the precision of weights, activations, and (for LLMs) the KV cache. It reduces memory footprint, memory bandwidth, and — if the hardware has fast paths for the target format — compute time.
The numerics belong to B04 §3: formats, scaling, outlier handling, calibration, and the algorithms (LLM.int8(), GPTQ, AWQ). This module owns three decisions that sit above the numerics:
- What to quantize, in order of risk. Weights only is the safest and gives the memory win. Weights + activations gives more compute benefit and more risk. KV-cache quantization is often the highest-leverage choice for long-context LLM serving because the KV cache dominates memory — and it is the one whose quality effect is hardest to see in aggregate metrics, because it degrades long conversations specifically.
- Post-training quantization vs quantization-aware training. PTQ is hours and needs a calibration set; QAT is a training run and a full eval. Start with PTQ; escalate only if the gate fails.
- Whether the speedup is real on your hardware. A format with no hardware fast path gives you the memory saving and none of the compute saving. Verify on the accelerator you will deploy on — this is a place where AMD and NVIDIA support genuinely differ by generation, and where a number from a blog post about someone else's GPU will mislead you.
The "it depends on X" pivot. X is what is scarce. Memory-bound serving (large model, small batch, long context) → quantization helps a lot, because you are moving fewer bytes. Compute-bound serving (large batch, short context) → it helps only if the hardware has a fast path for the target format. Say which regime you are in before quoting an expected win.
3.4 Rung 4a — Distillation¶
Train a small student to reproduce a large teacher's behaviour, using the teacher's soft output distribution rather than only the hard labels (Hinton, Vinyals, Dean 2015).
Why soft targets carry more information. A hard label says "this is a 7". The teacher's full distribution says "this is a 7, and it is somewhat 1-like, and not at all 8-like." That relative structure — Hinton's dark knowledge — is a much richer training signal per example than a one-hot vector, which is why a student can reach accuracy it could not reach training on the labels alone. The temperature parameter softens the distribution to expose more of that structure.
Task-specific distillation is the usual production win. A student that must match the teacher on everything is a hard problem and generally needs to be nearly as large. A student that must match the teacher on your traffic distribution can be dramatically smaller, because it only has to cover the part of the input space you actually serve. Which gives you the practical recipe: log real production traffic, score it with the teacher, and train the student on that — the distillation set is a dataset you already own.
The costs, which must appear in your proposal: a training pipeline, teacher inference over the distillation set (not free), a full task evaluation, and a new model to maintain, monitor, and re-distil every time the teacher changes. That last one is the recurring cost teams forget — it is the ongoing maintenance burden of Sculley et al. 2015 rather than a one-off project cost, and it is what makes distillation a poor fit for a fast-moving base model.
3.5 Rung 4b — Pruning and sparsity, and why compression ≠ speedup¶
This is the single most important lesson in the module, and the one most often gotten wrong.
- Unstructured pruning zeroes individual weights wherever they are smallest. It achieves impressive sparsity ratios and shrinks the model on disk beautifully. On a GPU it typically buys little or no speedup, because dense matrix units do not care that some of the numbers are zero — they multiply them anyway. You changed the model and paid a quality risk for a storage win.
- Structured pruning removes whole units the hardware and the kernel can actually skip: entire channels, attention heads, layers, or blocks. Smaller dense matrices mean genuinely less work. This is where real latency wins come from.
- Hardware-supported patterns. NVIDIA's 2:4 structured sparsity (2 of every 4 contiguous weights are zero) exists precisely because it is a compromise the silicon can exploit — the Sparse Tensor Cores skip the zeros. Support and the achievable benefit vary by architecture and by operator, so verify against NVIDIA's documentation and, more importantly, against a measurement on your own model.
The rule: only patterns the hardware exploits reduce latency. When someone reports "we pruned 90% of the weights", the correct next question is not "what happened to accuracy" — it is "what happened to latency?" Very often the answer is "nothing."
The Lottery Ticket Hypothesis is the reason this area stays interesting: sparse subnetworks that train to full accuracy demonstrably exist. But finding one requires the full training run you were trying to avoid, and the result is unstructured — so it is a statement about what is possible, not a production recipe.
3.6 Cascades and routing¶
A small model handles most traffic; only the cases it is uncertain about escalate to the large one. Expected cost is straightforward, because the small model runs on everything:
cost(e) = small_cost + e × large_cost
accuracy(e) = 1 − [ E_small × (1 − capture(e)) + e × (1 − large_accuracy) ]
where e is the escalation rate and capture(e) is the fraction of the small model's errors that
land inside the escalated slice. tools/cascade_analyzer.py models capture(e) = 1 − (1−e)^g, where
g = 1 means escalating at random and g > 1 means the confidence signal genuinely finds the small
model's mistakes.
Three results from that model worth carrying into an interview:
- The cascade's value is entirely in the confidence signal, not in the models. With
g = 1(random escalation) the tool's default scenario cannot beat simply serving the large model — the cheapest way to hit the quality floor is to escalate everything, which costs more than large-only because you also paid for the small model on every request. Withg = 3the same two models hit a 95% quality floor at ~27% escalation for about a third of the large-only cost. Same models. The difference is knowing which requests to escalate. - Blended accuracy can exceed the large model's own accuracy — and then decline. In the default
scenario it peaks around 65% escalation, above
large_accuracy. Past the peak you are escalating requests the small model already had right, and the large model gets some of them wrong. A good router beats a bigger model. - The cost is tail latency. Escalated requests pay both models. Your p99 is now
small_latency + large_latency, and your escalation rate drifts with traffic — so it must be monitored, not configured once. A cascade tuned in March quietly becomes a different system by September.
3.7 The quality gate — the heart of this module¶
Every optimization from rung 3 downward changes the model's outputs. The gate is what stands between that change and your users. A loss check is not a gate. Loss is a training-time proxy; it can move by an amount that looks negligible while a specific, important behaviour breaks.
A gate has five parts, and missing any one of them makes it decorative:
| Part | Requirement | Why |
|---|---|---|
| Fixed eval set | frozen, content-hashed, versioned; both variants scored on the same one | otherwise you are comparing eval sets, not models (see C03 exercise 3) |
| Task-level metrics | the metric the product is judged on, not loss or perplexity | the thing you optimise is the thing you get |
| Per-slice metrics | by language, user segment, input length, device, rare-but-critical categories | an aggregate that moves −0.2 can be −8 on one slice and +0.5 elsewhere |
| Latency and cost measurement | measured at the target percentile under realistic load | the entire point of the change; if you didn't measure it, you don't have it |
| Rollback plan | a named trigger, a tested procedure, and an owner | a rollback you have never executed is a hope |
Why slices are non-negotiable. Aggregate metrics average harm away. Quantization error is not uniform — it concentrates where activations have unusual dynamic range, which often means unusual inputs, which often means an under-represented group of users. A −0.2 aggregate is a pass on a dashboard and a serious regression for the people it lands on. This is the point of the ML Test Score rubric (Breck et al. 2017), and it is the question a good interviewer will ask.
The rollout. Offline eval is necessary and not sufficient — it cannot see production traffic shift, real latency under load, or interactions with the rest of the system. So: shadow (serve both, compare offline, no user impact) → canary at a small traffic percentage with pre-declared metric thresholds → staged ramp → full. And define the rollback trigger before the canary starts, because after it starts everyone will be motivated to interpret an ambiguous number favourably.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#F8FAFC',
'primaryTextColor': '#0F172A',
'primaryBorderColor': '#0891B2',
'lineColor': '#64748B',
'secondaryColor': '#E0F2FE',
'tertiaryColor': '#EEF2FF',
'fontFamily': 'Inter, Segoe UI, sans-serif'
}
}}%%
flowchart LR
CAND["Optimized candidate"] --> OFF{"Offline eval<br/>frozen set, task metric"}
OFF -->|"aggregate regressed"| REJ["Reject"]
OFF -->|"aggregate OK"| SLICE{"Per-slice check<br/>worst slice within threshold?"}
SLICE -->|"a slice regressed"| REJ
SLICE -->|"all slices OK"| PERF{"Latency and cost<br/>measured at p99 under load?"}
PERF -->|"no real win"| REJ
PERF -->|"win confirmed"| SHADOW["Shadow on live traffic<br/>compare offline"]
SHADOW --> CANARY["Canary at small percent<br/>pre-declared thresholds"]
CANARY -->|"trigger fires"| RB["Roll back<br/>tested procedure, named owner"]
CANARY -->|"clean for the soak period"| RAMP["Staged ramp to 100 percent"]
classDef trackC fill:#F59E0B,stroke:#0F172A,color:#0F172A
classDef neutral fill:#F8FAFC,stroke:#0891B2,color:#0F172A
classDef success fill:#10B981,stroke:#0F172A,color:#fff
classDef warn fill:#FEF3C7,stroke:#F59E0B,color:#0F172A
class CAND,SHADOW,CANARY,RAMP trackC
class OFF,SLICE,PERF neutral
class REJ,RB warn
class RAMP success
Read this as a set of gates that can only reject: nothing here proves the candidate is good, it only gives increasingly expensive chances to discover that it is bad — which is the correct posture for a change whose upside is already known and whose downside is not.
3.8 Compilation and runtime — with the honest costs¶
Ahead-of-time compilation and graph capture (export to a serving runtime, kernel fusion, static shapes) can be a genuine win, particularly for small models where per-operator overhead dominates. The costs that get left out of the proposal:
- Compile time in the deploy path — minutes per shape bucket, and it multiplies by the number of buckets.
- Dynamic shapes. Variable sequence lengths force either recompilation, bucketing with padding (you now pay compute for padding), or falling back to eager. Any of the three can erase the win.
- Debuggability. A fused graph is much harder to inspect than eager execution. When a numerical problem appears in production, you will want the eager path back — keep it runnable.
- Version pinning. Compiled artifacts couple you to a runtime version, which couples you to a driver version, which slows your ability to take security updates.
None of these makes compilation wrong. They make it a rung 2 decision with real operational cost, which should be stated rather than discovered.
4. Hands-On Labs¶
This is a design track: there is no hip/ or cuda/ code, because every decision in this module
is an economic one. The kernels live in Track B. Everything below is pure Python 3.8+ standard
library — no pip install, no GPU, no network.
make help # list targets
make run # run all three tools with their default scenarios
make run-exercises # run the exact scenarios the drills reference
make clean # remove __pycache__
On Windows PowerShell, if python3 is not on your PATH: make run PYTHON=python, or invoke the
tools directly.
The tools are analytic models. Their multipliers are placeholders you must replace with your own measurements — the tools say so on every run. Never carry a number out of this module and into a design doc as though it were measured.
Lab 1 — Price the ladder (tools/cost_model.py)¶
python tools/cost_model.py
python tools/cost_model.py --qps 5 # a small service
python tools/cost_model.py --cache-hit-rate 0.0 --quantize-multiplier 1.2
What to observe. At the default 120 qps, every rung pays back in under two months, so the ladder
ordering looks like a formality. Now run --qps 5. The traffic is 24× smaller, the savings shrink
with it, but the engineering cost does not — and distillation stops making sense entirely. Same
technology, opposite decision, and the only thing that changed was traffic volume. That is the
argument for computing break-even before choosing an optimization, and it is the point of exercise 3.
Also watch the GPU-count rounding. Savings arrive in steps, not smoothly: a 15% throughput improvement that does not remove a whole GPU saves exactly nothing. Real fleets are lumpy.
Lab 2 — Find the cascade's operating point (tools/cascade_analyzer.py)¶
python tools/cascade_analyzer.py
python tools/cascade_analyzer.py --confidence-gain 1.0 # escalate at random
python tools/cascade_analyzer.py --large-hardness 1.8 # hard cases really are harder
What to observe. With a good confidence signal (g = 3) the default cascade clears a 95% quality
floor at ~27% escalation for about a third of the large-only cost. Set --confidence-gain 1.0 and
the whole structure collapses: the tool tells you to escalate everything, which costs more than
just serving the large model, and it says so explicitly. The cascade was never about the models; it
was about the router. Then set --large-hardness 1.8 to relax the optimistic assumption that the
big model is equally good on the hard slice, and watch the achievable quality ceiling drop — that is
the assumption a sharp reviewer will attack, so attack it yourself first.
Lab 3 — Measure the value of skew (tools/cache_simulator.py)¶
python tools/cache_simulator.py
python tools/cache_simulator.py --zipf 0 # uniform traffic
python tools/cache_simulator.py --zipf 1.4 --cache-size 500 # heavier skew, tiny cache
What to observe. At --zipf 1.0, a cache holding 10% of the key space serves ~70% of requests.
At --zipf 0 the same cache serves ~10% — exactly its share of the key space, which is what "no
skew" means. The hit rate is a property of your traffic, not of your cache. This is why the first
question about a caching proposal is always "what does the request-frequency distribution of the
production log look like?" and never "how big should the cache be?"
Design drills¶
The real work is in exercises/ — three written design problems with reference answers
in solutions/.
| # | Drill | Skill |
|---|---|---|
| 1 | 01_optimization_order.md |
Order the ladder for a specific situation; justify not starting with distillation |
| 2 | 02_quality_gate.md |
Design a quantization rollout gate: eval set, slices, thresholds, canary, rollback |
| 3 | 03_build_the_cost_case.md |
Break-even analysis, including when the answer is "don't do it" |
5. Performance Analysis¶
This section is reframed for a design track. There is no kernel to profile. The system-level equivalent of the roofline question ("compute-bound or memory-bound?") is: which term of the cost identity binds, and what is the ceiling on fixing it? The discipline is unchanged — measure, find the binding constraint, compute the ceiling before doing the work — only the units differ.
The binding-constraint table¶
| Binding constraint | How you see it | Ceiling on the fix | Where to work |
|---|---|---|---|
| Requests that need not reach the model | high repeat rate in the request log | the repeat rate itself | §3.1 caching, cascades |
| Fleet utilization | servers below ~60% busy | the idle fraction | §3.2 right-sizing (a config change) |
| Throughput per GPU | utilization high, batch sizes small | measured batching/runtime multiplier | §3.2, and B06 §5 |
| Memory bandwidth / model size | large model, small batch, long context | the byte reduction from quantization | §3.3, and B04 §5 |
| Model capacity you do not need | small model matches on most traffic | the gap between the two models | §3.4 distillation, §3.6 cascade |
Compute the ceiling first. If 30% of your cost is idle capacity from over-provisioning, no model optimization can beat 30%, and the config change is a day's work. This is the same instinct as checking arithmetic intensity before optimising a kernel: know the bound before you do the work.
Worked example: cost per 1,000 requests (analytic model)¶
From tools/cost_model.py defaults — 120 qps, 600 tokens/request, 2,200 tokens/s per GPU at 70%
usable headroom, $3.20/GPU-hour. The placeholder multipliers are illustrative, not published
speedups:
| Configuration | GPUs | $/1k requests | $/month | Saving | Eng weeks | Break-even |
|---|---|---|---|---|---|---|
| Baseline | 47 | 0.3481 | 109,792 | — | — | — |
| Cache (30% hit) | 33 | 0.2444 | 77,088 | 32,704 | 2 | 0.4 mo |
| Batching/runtime (×1.6) | 30 | 0.2222 | 70,080 | 39,712 | 3 | 0.5 mo |
| Quantization (×1.8) | 26 | 0.1926 | 60,736 | 49,056 | 6 | 0.7 mo |
| Distilled student (×3.0) | 16 | 0.1185 | 37,376 | 72,416 | 20 | 1.7 mo |
| Cache + batching + quantization | 12 | 0.0889 | 28,032 | 81,760 | 11 | 0.8 mo |
The before/after delta to internalise: the combination of the three low-risk rungs (cache + batching + quantization) reaches $0.0889 per 1k requests against the distilled student's $0.1185 — cheaper than the highest-risk single option, for half the engineering time and without retraining anything. That is the ladder's whole argument, in one row.
The second observation is subtler and matters more in a real review: the rungs compose multiplicatively, and the risky one is on the wrong side of the multiplication. If you do distillation first, you still want caching and batching afterwards — so the question was never "which one", it was "which one first", and the answer is the one that is cheap, safe, and does not have to be redone when the base model changes.
Reproduce with
python tools/cost_model.py. These are model outputs from stated assumptions, not measurements. On a real system every multiplier in that table is something you benchmark.
What you must measure, not model¶
| Quantity | Why a model will not do |
|---|---|
| Throughput multiplier from batching, quantization, compilation | entirely hardware- and shape-dependent |
| Cache hit rate | a property of your traffic; simulate the shape, measure the value |
| Quality delta per slice | the only number that decides whether you can ship |
| p99 latency under realistic load | means hide the tail that users experience |
| Escalation rate drift | it moves with traffic and will not tell you |
6. Challenges, Drawbacks & Tradeoffs¶
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#F8FAFC',
'primaryTextColor': '#0F172A',
'primaryBorderColor': '#0891B2',
'lineColor': '#64748B',
'secondaryColor': '#E0F2FE',
'tertiaryColor': '#EEF2FF',
'fontFamily': 'Inter, Segoe UI, sans-serif'
}
}}%%
flowchart TD
START{"Serving is too expensive.<br/>Where do you start?"}
START --> U{"Fleet utilization<br/>above 60 percent?"}
U -->|no| RIGHTSIZE["Right-size the fleet first.<br/>Config change, no risk."]
U -->|yes| REPEAT{"Repeat rate in the<br/>request log?"}
REPEAT -->|"high"| CACHE["Cache. Highest value,<br/>no quality risk."]
REPEAT -->|"low"| BATCH{"Batch size at<br/>the latency target?"}
BATCH -->|"small"| SERVE["Batching and runtime.<br/>Same weights, no risk."]
BATCH -->|"already large"| MEM{"Memory-bound?<br/>long context, small batch"}
MEM -->|"yes"| QUANT["Quantize. Small risk,<br/>needs the gate."]
MEM -->|"no"| SMALLER{"Does a smaller model<br/>match on your traffic?"}
SMALLER -->|"yes"| CASCADE["Cascade first - reversible.<br/>Distil only if it holds."]
SMALLER -->|"no"| STOP["Do not optimize the model.<br/>Renegotiate the budget or the SLO."]
classDef trackC fill:#F59E0B,stroke:#0F172A,color:#0F172A
classDef neutral fill:#F8FAFC,stroke:#0891B2,color:#0F172A
classDef success fill:#10B981,stroke:#0F172A,color:#fff
classDef warn fill:#FEF3C7,stroke:#F59E0B,color:#0F172A
class START,U,REPEAT,BATCH,MEM,SMALLER neutral
class RIGHTSIZE,CACHE,SERVE success
class QUANT,CASCADE trackC
class STOP warn
Note the terminal node on the right: "do not optimize the model" is a legitimate, and sometimes correct, output of this decision tree — a design review should be as willing to reach it as any other.
Where this breaks¶
- Compression ratio quoted as speedup. 90% unstructured sparsity, no latency change. The most common false claim in this area (§3.5).
- A quality gate that only checks the aggregate. Harm concentrated in a slice passes cleanly and reaches exactly the users least able to complain about it.
- Caching a non-deterministic endpoint. With temperature > 0, "the same request" has many valid answers; caching one of them silently removes the diversity your product may depend on.
- A cascade whose escalation rate drifts. Tuned once at 27%, drifts to 60% as traffic shifts, and now costs more than the large model alone — while every dashboard says "the cascade is working."
- Semantic cache false hits. A plausible wrong answer, invisible to every latency and cost metric. Needs the full quality gate; it is a model change wearing an infrastructure costume.
- Compiled artifacts pinning your stack. You cannot take a driver security patch without re-validating the whole serving path.
- Optimizing a model that is about to be replaced. A quarter of distillation work delivered the week the base model is deprecated is a total loss. Weigh the model's expected remaining lifetime.
- The optimization that saves money and loses revenue. A 5% cost saving that costs 0.5% of conversions is usually a large net loss. If you cannot measure the product-side effect, you have not finished the analysis.
When not to optimize¶
- The bill is small relative to engineering cost.
cost_model.pyprints the whole serving bill in engineer-weeks per month for exactly this reason. If the entire bill is 3 engineer-weeks per month, a 6-week project cannot pay back inside a year even if it makes serving free. - Traffic is about to change by more than the optimization. Optimize after the launch, not before it.
- The model will be replaced this quarter. Do rung 1 and 2 (model-agnostic); skip 3 and 4.
- You are pre-product-market-fit. Cost per request is not your binding constraint; finding out whether anyone wants the product is.
Tradeoffs to hold in mind¶
| Decision | You gain | You pay |
|---|---|---|
| Exact-match cache | large win on skewed traffic, no quality risk | staleness, key-design and tenancy hazards, cold start |
| Semantic cache | higher hit rate | false hits — a real quality risk, invisible to cost metrics |
| Larger batches | throughput | latency, especially p99 (see B06) |
| Quantization | memory, bandwidth, sometimes compute | quality risk concentrated in unusual inputs; hardware-dependent benefit |
| Unstructured pruning | model size on disk | usually no latency win on GPUs |
| Structured / 2:4 sparsity | real latency win | a larger accuracy cost for the same nominal sparsity; hardware- and operator-dependent |
| Distillation | large, durable cost reduction | a quarter of work, a full eval, and a permanent re-distillation treadmill |
| Cascade | most of the large model's quality at a fraction of the cost | worse tail latency, two models to maintain, a drifting operating point |
| Graph capture / compilation | latency, especially for small models | compile time, dynamic-shape pain, debuggability, version pinning |
7. Real-World Use Cases¶
- Prefix / prompt caching in LLM serving. vLLM's automatic prefix caching and SGLang's RadixAttention (Zheng et al. 2023) reuse the computed KV cache for shared prefixes — rung 1 applied inside the serving engine, and exact rather than approximate.
- DistilBERT (Sanh et al. 2019) is the canonical published distillation result and the standard reference when proposing a student model.
- Post-training quantization for LLM serving — LLM.int8() (Dettmers et al. 2022), GPTQ (Frantar et al. 2022), AWQ (Lin et al. 2023). The numerics are B04's; the decision to adopt one is this module's.
- 2:4 structured sparsity on NVIDIA Ampere and later — the industrial example of "only patterns the hardware exploits reduce latency" (NVIDIA developer blog).
- Cascades and routing — the pattern predates deep learning (Viola–Jones detectors used a cascade of cheap classifiers) and has returned for LLM serving as model routing; FrugalGPT is a published treatment of the cost/quality tradeoff.
- Speculative decoding (Leviathan et al. 2023) is the cascade idea moved inside the decode loop: a small draft model proposes, the large model verifies. It is exactness-preserving, which makes it unusual — a rung-4-shaped mechanism with rung-2 risk.
- Quality gates in practice — the ML Test Score rubric (Breck et al. 2017) formalises the slice, staleness, and rollback tests in §3.7.
Where to go next. C05 picks up where the quality gate leaves off: the shadow, canary and rollback machinery this module specifies has to be built into a serving architecture, and that is C05's subject. C06 is the answer to §3.6's warning that a cascade tuned in March is a different system by September — cache hit rate and escalation rate are drifting quantities, and drift is what C06 teaches you to watch. B04 §3 supplies the one rung this module deliberately does not explain: the numerics that decide whether a quantization scheme survives your gate.
8. Cited References¶
Grounding for the claims above. Sources shared with other modules are cited by anchor into the master bibliography, where the URL and its retrieval date live; sources only this module uses keep their full citation here.
Distillation - Distilling the Knowledge in a Neural Network (Hinton, Vinyals, Dean, 2015) — soft targets and temperature; the origin of §3.4, and the argument for why a student trained on the teacher's distribution beats one trained on the hard labels alone. - Sanh, Debut, Chaumond, Wolf (2019). DistilBERT, a distilled version of BERT. https://arxiv.org/abs/1910.01108
Pruning and sparsity - Frankle & Carbin (2019). The Lottery Ticket Hypothesis. https://arxiv.org/abs/1803.03635 - Han, Pool, Tran, Dally (2015). Learning both Weights and Connections for Efficient Neural Networks — the foundational unstructured-pruning result. https://arxiv.org/abs/1506.02626 - Gale, Elsen, Hooker (2019). The State of Sparsity in Deep Neural Networks — a careful account of what sparsity does and does not deliver. https://arxiv.org/abs/1902.09574 - NVIDIA — Accelerating Inference with Sparsity Using Ampere and TensorRT — 2:4 structured sparsity, the hardware-supported pattern. (retrieved 8 Aug 2026) https://developer.nvidia.com/blog/accelerating-inference-with-sparsity-using-ampere-and-tensorrt/
Quantization (cross-reference to B04) - LLM.int8() (Dettmers, Lewis, Belkada, Zettlemoyer, 2022) — the outlier-aware int8 scheme behind §3.3's "what to quantize, in order of risk" decision. - GPTQ (Frantar, Ashkboos, Hoefler, Alistarh, 2022) — the post-training route §3.3 tells you to start with before escalating to QAT. - AWQ (Lin et al., 2023) — the other post-training option §3.3's second decision weighs.
Caching, cascades, routing (cross-reference to B06) - vLLM — Automatic Prefix Caching — the production form of §3.1's rung-1 argument, inside the serving engine. (retrieved 8 Aug 2026) https://docs.vllm.ai/en/latest/features/automatic_prefix_caching.html - Zheng et al. (2023). SGLang / RadixAttention — Efficiently Programming Large Language Models. https://arxiv.org/abs/2312.07104 - Chen, Zaharia, Zou (2023). FrugalGPT: How to Use Large Language Models While Reducing Cost and Improving Performance. https://arxiv.org/abs/2305.05176 - Fast Inference from Transformers via Speculative Decoding (Leviathan, Kalman, Matias, 2023) — the exactness-preserving cascade of §7: rung-4 shape at rung-2 risk.
Quality gates and production practice - The ML Test Score (Breck, Cai, Nielsen, Salib, Sculley, 2017) — IEEE Big Data. The rubric §3.7's five-part gate is built from, including the slice and rollback tests. - Hidden Technical Debt in Machine Learning Systems (Sculley et al., 2015) — NeurIPS. Why an optimization that adds a second model adds permanent maintenance cost, not a one-off project cost. - Ribeiro, Wu, Guestrin, Singh (2020). Beyond Accuracy: Behavioral Testing of NLP Models with CheckList — behavioural, slice-like testing beyond an aggregate score. https://arxiv.org/abs/2005.04118
9. Self-Assessment & Interview Drills¶
Conceptual (answers below)¶
- A colleague proposes distillation as the first response to a serving cost problem. Give the three-sentence reply you would make in the design review.
- Your team pruned 90% of the weights and the model is the same speed. Explain why, and say what they should have done instead.
- Quantization moved your aggregate eval metric by −0.2 points. Ship it or not? What do you need to know?
- A cascade escalates 25% of traffic to the large model. Write the expressions for expected cost and expected accuracy, and name the parameter that decides whether the cascade is worth building.
- Your cache hit rate is 8%. Give two distinct explanations, and the measurement that distinguishes them.
- Under what conditions is the correct answer to "should we optimize this model?" simply no?
- Explain to a product manager why "the loss barely changed" is not evidence that a quantized model is safe to ship.
Answers
-
"Distillation is the most expensive and riskiest option on the ladder — a quarter of work, a full retraining, a complete eval, and a new model to maintain and re-distil every time the base model changes. Before we commit to that, show me fleet utilization, the repeat rate in the request log, and the batch size we run at the latency target; those three numbers usually contain most of the win for a fraction of the cost and zero quality risk. If we still have a gap after that, I'll support distillation — and it will be a better-scoped project because we'll know exactly how big the gap is."
-
Unstructured pruning zeroes scattered individual weights. Dense matrix hardware multiplies them anyway — the zeros are not skipped — so you get a smaller file and the same latency. Instead: structured pruning (remove whole channels, heads, or layers, which genuinely shrinks the matrices) or a hardware-supported pattern such as 2:4 sparsity on capable NVIDIA parts. Rule: compression ratio is not speedup; only patterns the hardware exploits reduce latency.
-
Not enough information — and that is the correct answer, not a dodge. I need: (a) the per-slice deltas, because −0.2 aggregate can be −8 on one slice; (b) whether the eval set is the frozen one both variants were scored on; (c) the measured latency/cost win, so I know what I am buying; and (d) the pre-declared threshold — if we said in advance that −0.2 is acceptable, this is a pass; if we are deciding now, we are rationalising. Also confirm the worst-affected slice is not a user group we have a duty of care to.
-
cost(e) = small_cost + e × large_cost(the small model runs on everything), andaccuracy(e) = 1 − [E_small × (1 − capture(e)) + e × (1 − large_accuracy)], wherecapture(e)is the fraction of the small model's errors falling inside the escalated slice. The deciding parameter is the quality of the confidence signal — how much bettercapture(e)is thane. If escalation is random,capture(e) = eand the cascade is worse than just serving the large model, because you pay for the small one on every request too. -
(a) Traffic is not skewed — requests are near-unique, so there is nothing to reuse. (b) The cache key is too specific — a timestamp, request ID, or session token in the key makes genuinely repeated requests look distinct. Distinguish them by computing the repeat rate of the normalised request in the raw log: if the log shows 60% repeats but the cache sees 8%, the key is the bug; if the log also shows 8%, the traffic is simply not cacheable and you should stop.
-
When the total serving bill is small relative to the engineering cost (compute break-even; if it is beyond your planning horizon, it is a no); when the model will be replaced before the work lands; when traffic is about to change by more than the optimization would save; when the fleet is under-utilised (fix that first — it is a config change); and when the quality risk threatens revenue worth more than the saving. Say the number, not the instinct: "this saves $4k/month and costs 8 engineer-weeks, so it pays back in a year — that is a no this quarter."
-
Loss is a training-time average over a training-like distribution. It is not the task, and it is not your traffic. A change can leave average loss almost untouched while breaking one specific behaviour that matters commercially — long documents, a particular language, a rare-but-critical category. The evidence we need is the task metric on a frozen eval set, broken out by slice, plus a canary on real traffic with a pre-declared rollback trigger. In one line for the PM: loss tells us the model is still roughly the same on average; it cannot tell us whether it still works for a specific group of our users.
Design drills¶
Do these in exercises/; reference answers with the reasoning in
solutions/. Write them as design docs: assumptions stated, arithmetic shown, tradeoff
named, "it depends on X" pivot given.
- Order the ladder (
exercises/01_optimization_order.md) — for a specific latency/cost problem, produce the ordering for that situation, and justify why you would not start with distillation. - Design the quality gate (
exercises/02_quality_gate.md) — for a quantization rollout: eval set, slices, thresholds, canary plan, rollback trigger, and who decides. - Build the cost case (
exercises/03_build_the_cost_case.md) — compute break-even given engineering cost and monthly saving, and state clearly when the answer is "don't do it."
Stretch¶
- Extend
cascade_analyzer.pyto three tiers and find the cost-optimal pair of thresholds. Does the third tier earn its complexity? (Usually not — show why with numbers.) - Add TTL and staleness to
cache_simulator.py: entries expire afterkseconds and a stale hit counts as an error. Find the TTL that maximises saving subject to a staleness budget. - Take a real serving workload you have access to, measure the repeat rate of normalised requests in the log, and compare it against the hit rate your cache actually achieves. The gap between the two is a bug in your cache key, and it is one query to find.