Advanced Examples
Examples 51-75 move from "make a service work" to "make it worth trusting in production": quantization evaluated rather than assumed, model parallelism and its failure mode, capacity planning against a real length distribution, GPU-aware autoscaling, deployment packaging, staged rollout with guardrails, serving observability, and the self-hosting-versus-hosted-API decision. Every framework name, GPU price, and quantization figure that shows up here is deliberately illustrative -- see this course's Accuracy notes for exactly which facts are volatile and why none of them are stated as a current market fact.
Example 51: Quantize a Model
ex-51 · exercises co-19
Quantization maps a range of floating-point weights onto a much smaller set of integer codes, trading precision for memory -- this example quantizes a tiny weight vector to INT8 and measures the reconstruction error directly.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["fp16 weights<br/>0.12, -0.45, 0.98..."]:::blue --> B["Compute scale =<br/>max(abs)/127"]:::orange
B --> C["Quantize: round(w/scale)<br/>-> INT8 codes"]:::teal
C --> D["Dequantize: code*scale<br/>-> approximate float"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-51-quantize-a-model/example.py
"""Example 51: Quantize a Model."""
def quantize_int8(weights: list[float], scale: float) -> list[int]: # => co-19: linear symmetric quantization
return [round(w / scale) for w in weights] # => each float mapped to its nearest INT8 code
def dequantize_int8(quantized: list[int], scale: float) -> list[float]: # => co-19: reconstruct an APPROXIMATE float
return [q * scale for q in quantized] # => the inverse mapping -- approximate, never exact
weights = [0.12, -0.45, 0.98, -1.2, 0.03] # => a tiny stand-in for one row of a real weight matrix
scale = max(abs(w) for w in weights) / 127 # => co-19: INT8 spans [-127, 127] -- scale maps the float range onto it
quantized = quantize_int8(weights, scale) # => the compressed, integer-only representation
dequantized = dequantize_int8(quantized, scale) # => reconstructed floats -- close, but NOT identical to the originals
print(quantized) # => Output: [13, -48, 104, -127, 3]
print([round(d, 4) for d in dequantized]) # => Output: [0.1228, -0.4535, 0.9827, -1.2, 0.0283]
max_error = max(abs(w - d) for w, d in zip(weights, dequantized)) # => the worst single reconstruction error
print(round(max_error, 4)) # => Output: 0.0035
assert max_error < scale # => co-19: quantization error is bounded by roughly half a scale step, always
# => real quantizers (GPTQ, AWQ) use calibration data instead of this toy min/max scale -- same idea, tighter fit
print("ex-51 OK") # => a self-check marker confirming reconstruction error stayed within the expected bound
Run: python3 example.py
Output:
[13, -48, 104, -127, 3]
[0.1228, -0.4535, 0.9827, -1.2, 0.0283]
0.0035
ex-51 OKKey takeaway: Quantization is exactly this mechanical, bounded-error operation -- pick a scale, round every value to the nearest integer code, and the reconstruction error is provably bounded by roughly half a scale step; nothing about it is exotic or unpredictable.
Why it matters: This tiny five-value example is the same operation applied to a billion-parameter weight matrix -- the reconstruction error here directly explains why lower-precision formats measurably change a model's output, which is exactly what Example 52 goes on to quantify.
Example 52: Quantization Quality Cost
ex-52 · exercises co-19
Quantization is a real trade, not a free win: lower precision shrinks memory, but it also degrades output quality, and the two move together as precision drops. This example prices both sides of the trade at fp16, INT8, and INT4 using an illustrative, synthetic quality curve.
learning/code/ex-52-quantization-quality-cost/example.py
"""Example 52: Quantization Quality Cost."""
def synthetic_quality_score(precision_bits: int) -> float:
# => co-19: illustrative synthetic degradation curve, NOT a published benchmark number -- see Accuracy notes
baseline = 100.0 # => an arbitrary quality ceiling -- fp16 loses nothing relative to this baseline
degradation = {16: 0.0, 8: 1.5, 4: 6.0}[precision_bits] # => illustrative points, not measured data
return baseline - degradation # => lower precision, more degradation -- monotonic by construction
def memory_bytes_per_param(precision_bits: int) -> float: # => co-19: bytes needed to store ONE parameter
return precision_bits / 8 # => bits divided by 8 -- the direct bits-to-bytes conversion
param_count = 7_000_000_000 # => 7B params, a common "small" open-weights model size, illustrative
results: dict[int, tuple[float, float]] = {} # => precision (bits) -> (quality score, memory in GiB)
for bits in (16, 8, 4): # => the three precisions this example compares
quality = synthetic_quality_score(bits) # => co-19: quality cost, paid for lower precision
memory_gb = param_count * memory_bytes_per_param(bits) / (1024**3) # => co-19: memory bought back
results[bits] = (quality, round(memory_gb, 2)) # => one row of the tradeoff table per precision
print(results) # => Output: {16: (100.0, 13.04), 8: (98.5, 6.52), 4: (94.0, 3.26)}
assert results[4][1] < results[8][1] < results[16][1] # => co-19: memory shrinks monotonically with precision
assert results[4][0] < results[8][0] < results[16][0] # => co-19: quality degrades monotonically -- the trade is real
# => this synthetic curve motivates the tradeoff shape -- production choices need real eval-suite numbers
print("ex-52 OK") # => a self-check marker confirming both monotonic trends held across all three precisions
Run: python3 example.py
Output:
{16: (100.0, 13.04), 8: (98.5, 6.52), 4: (94.0, 3.26)}
ex-52 OKKey takeaway: Every step down in precision buys real memory (13.04 GB to 6.52 GB to 3.26 GB) at a real, monotonically-worsening quality cost -- neither side of the trade is free, and this course's own Accuracy notes flag exactly why the specific numbers here are illustrative rather than a benchmark claim.
Why it matters: This topic's Tensions & trade-offs section is explicit that a published degradation figure does not transfer to your workload -- this example's job is to show the shape of the trade (monotonic, both directions), which is durable, while treating any specific quality number as something you must measure on your own task, never trust from a table.
Example 53: Quantization Decision Record
ex-53 · exercises co-19
Turning a quality floor into an automatic decision -- accept only candidates that clear the floor, then pick the smallest among those -- makes the quantization choice repeatable and auditable rather than a one-off judgment call.
learning/code/ex-53-quantization-decision-record/example.py
"""Example 53: Quantization Decision Record."""
from dataclasses import dataclass # => stdlib only -- a decision record needs no framework
@dataclass
class QuantizationCandidate: # => co-19: one candidate precision level, already measured (Example 52's shape)
name: str # => a label, useful only for reading print output
precision_bits: int # => kept for reference -- not directly used in the decision itself
quality_score: float # => co-19: the gate this candidate must clear to even be considered
memory_gb: float # => co-19: the tiebreaker among candidates that DO clear the gate
def decide(candidates: list[QuantizationCandidate], quality_floor: float) -> dict[str, str]:
# => co-19: accept ONLY candidates clearing the quality floor; among those, prefer the smallest memory
accepted = [c for c in candidates if c.quality_score >= quality_floor] # => co-19: the SAFETY gate, applied first
if not accepted: # => every candidate failed the quality gate -- there is no safe choice
return {"decision": "reject_all", "reason": f"no candidate clears quality floor {quality_floor}"}
winner = min(accepted, key=lambda c: c.memory_gb) # => co-19: among SAFE candidates, smallest wins
# => "smallest memory that is still safe" is the whole decision rule, in one sentence
return {"decision": winner.name, "reason": f"smallest memory ({winner.memory_gb} GB) among candidates >= {quality_floor}"}
candidates = [ # => the SAME three measured candidates from Example 52
QuantizationCandidate("fp16", 16, 100.0, 13.04), # => highest quality, largest memory
QuantizationCandidate("int8", 8, 98.5, 6.52), # => the usual middle-ground choice
QuantizationCandidate("int4", 4, 94.0, 3.26), # => smallest memory, lowest quality
]
record_strict = decide(candidates, quality_floor=97.0) # => a strict floor rules out int4
record_loose = decide(candidates, quality_floor=90.0) # => a loose floor admits everything -- smallest wins
# => the SAME three candidates, the SAME decision function -- only the quality floor input changes
print(record_strict) # => Output: {'decision': 'int8', 'reason': 'smallest memory (6.52 GB) among candidates >= 97.0'}
print(record_loose) # => Output: {'decision': 'int4', 'reason': 'smallest memory (3.26 GB) among candidates >= 90.0'}
# => this IS a decision record -- the "reason" field is what a real quantization ADR would document
assert record_strict["decision"] == "int8" # => co-19: the floor EXCLUDED int4 despite its smaller memory
# => memory alone never wins -- it only breaks ties AMONG candidates that already passed the gate
assert record_loose["decision"] == "int4" # => co-19: with a looser floor, the smallest safe option wins
# => same code path, same gate logic -- the OUTCOME changed only because the threshold moved
print("ex-53 OK") # => a self-check marker confirming the decision flips correctly with the quality floor
Run: python3 example.py
Output:
{'decision': 'int8', 'reason': 'smallest memory (6.52 GB) among candidates >= 97.0'}
{'decision': 'int4', 'reason': 'smallest memory (3.26 GB) among candidates >= 90.0'}
ex-53 OKKey takeaway: The exact same three candidates produce two different, equally-correct decisions depending only on the quality floor -- a decision record makes that dependency explicit instead of hiding it behind "we chose INT8" with no stated reason.
Why it matters: This is the artefact an operator should keep alongside a deployment manifest (Example 62) -- when someone asks "why INT8 and not INT4," the decision record answers with the threshold that was used, not a memory of a conversation.
Example 54: Model-Parallel Split
ex-54 · exercises co-20
A model too large to fit on one device is split across several -- tensor parallelism divides each weight matrix evenly, but every device must then exchange partial results with every other device on every layer, and that exchange traffic grows with device count.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A["14 GB model"]:::blue --> B["Device 0: 3.5 GB shard"]:::teal
A --> C["Device 1: 3.5 GB shard"]:::teal
A --> D["Device 2: 3.5 GB shard"]:::teal
A --> E["Device 3: 3.5 GB shard"]:::teal
B & C & D & E --> F["All-reduce: EVERY device<br/>exchanges partials EVERY layer"]:::orange
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-54-model-parallel-split/example.py
"""Example 54: Model-Parallel Split. [GPU] illustrative reference numbers; simulator runs offline, deterministic."""
def shard_weights_bytes(total_weight_bytes: int, num_devices: int) -> int:
# => co-20: tensor parallelism splits EACH weight matrix evenly across every device
return total_weight_bytes // num_devices # => exact split assumed for this illustration
def all_reduce_bytes_per_step(activation_bytes: int, num_devices: int) -> int:
# => co-20: every device must exchange its partial activations with every OTHER device, once per layer
return activation_bytes * (num_devices - 1) * 2 # => a simplified ring-all-reduce traffic estimate
total_weight_bytes = 14_000_000_000 # => a 7B-parameter model stored in fp16 (2 bytes/param)
shards = {n: shard_weights_bytes(total_weight_bytes, n) for n in (1, 2, 4, 8)} # => co-20: one entry per device count
print(shards) # => Output: {1: 14000000000, 2: 7000000000, 4: 3500000000, 8: 1750000000}
activation_bytes = 4_000_000 # => per-step activation volume that must be exchanged between devices
traffic = {n: all_reduce_bytes_per_step(activation_bytes, n) for n in (1, 2, 4, 8)} # => co-20: traffic per device count
print(traffic) # => Output: {1: 0, 2: 8000000, 4: 24000000, 8: 56000000}
assert shards[8] == total_weight_bytes // 8 # => co-20: each of 8 devices holds exactly one eighth of the weights
assert traffic[8] > traffic[2] # => co-20: more devices means MORE interconnect traffic per step, not less
# => Example 69 measures what happens when this traffic outgrows the interconnect's real bandwidth
print("ex-54 OK") # => a self-check marker confirming the shard-shrinks/traffic-grows tradeoff held
Run: python3 example.py
Output:
{1: 14000000000, 2: 7000000000, 4: 3500000000, 8: 1750000000}
{1: 0, 2: 8000000, 4: 24000000, 8: 56000000}
ex-54 OKKey takeaway: Splitting a model across more devices shrinks each device's weight shard linearly, but grows the per-step exchange traffic linearly too -- adding devices is not a free way to buy more capacity, it trades weight-memory pressure for interconnect-bandwidth pressure.
Why it matters: This is why model parallelism is a "when you have no other choice" tool, reached for only once a model no longer fits on a single device even after quantization (Example 51) -- the interconnect cost this example measures is exactly what Example 69 prices against a real decode-step budget.
Example 55: Parallelism Failure Mode
ex-55 · exercises co-20
Tensor parallelism has no redundancy built in -- if a single device fails partway through a forward pass, every other device's partial result becomes unusable, and the whole request must restart from scratch rather than degrade gracefully.
learning/code/ex-55-parallelism-failure-mode/example.py
"""Example 55: Parallelism Failure Mode."""
def forward_pass_with_device_failure(num_devices: int, failed_device: int | None) -> str:
# => co-20: tensor parallelism has NO redundancy -- every device must complete for the result to be valid
if failed_device is not None and 0 <= failed_device < num_devices: # => a valid device index actually failed
return "request_failed: partial result on other devices is unrecoverable, must restart from scratch"
return "request_succeeded: all devices completed their shard" # => no failure -- every shard finished normally
healthy = forward_pass_with_device_failure(4, None) # => the baseline: no failure anywhere
one_failed = forward_pass_with_device_failure(4, failed_device=2) # => a SINGLE device out of 4 fails
# => contrast a single-instance deployment: a device failure there costs ONE request, not the fleet
print(healthy) # => Output: request_succeeded: all devices completed their shard
print(one_failed) # => Output: request_failed: partial result on other devices is unrecoverable, must restart from scratch
assert healthy.startswith("request_succeeded") # => the no-failure baseline succeeds, as expected
# => co-20: this all-or-nothing coupling is the direct cost of splitting ONE request across devices
assert one_failed.startswith("request_failed") # => co-20: a SINGLE device failure fails the WHOLE request
print("ex-55 OK") # => a self-check marker confirming tensor parallelism's all-or-nothing failure mode held
Run: python3 example.py
Output:
request_succeeded: all devices completed their shard
request_failed: partial result on other devices is unrecoverable, must restart from scratch
ex-55 OKKey takeaway: One failed device out of four is enough to fail the entire request -- tensor parallelism trades away the fault-tolerance an operator might expect from a distributed system, in exchange for the ability to serve a model too large for a single device at all.
Why it matters: This is a real operational fact that must inform capacity planning and SLOs for a model-parallel deployment: the effective failure rate of a request is roughly the SUM of each participating device's failure rate, not the rate of any single device -- more devices in the split means a measurably higher chance any one request fails.
Example 56: Workload Length Distribution
ex-56 · exercises co-21
Real traffic is a distribution of prompt and output lengths, dominated by short requests with a long tail of rare, very expensive ones -- capacity planning must reason about that whole distribution, not a single "average request" number.
learning/code/ex-56-workload-length-distribution/example.py
"""Example 56: Workload Length Distribution."""
# => co-21: a realistic workload is a DISTRIBUTION of lengths, never one fixed number
LENGTH_BUCKETS = [ # => (output_tokens, relative weight) -- weight is a count, not a random draw
(50, 40), # => short replies dominate
(200, 35), # => the second-most-common bucket
(500, 15), # => a mid-length minority
(1500, 8), # => a genuinely long minority
(4000, 2), # => rare, very long generations -- the tail that matters most for capacity
]
def expand_to_workload(buckets: list[tuple[int, int]]) -> list[int]:
# => co-21: materializes the distribution as an explicit, deterministic list -- no random sampling anywhere
workload: list[int] = [] # => accumulates one entry per simulated request
for length, weight in buckets: # => processes every bucket in the fixed table above
workload.extend([length] * weight) # => `weight` copies of this length, deterministically
return workload # => a flat list -- 100 request lengths, drawn from a fixed, known distribution
workload = expand_to_workload(LENGTH_BUCKETS) # => co-21: the whole distribution, expanded into one list
print(len(workload)) # => Output: 100 -- 100 requests total across all buckets
mean_length = sum(workload) / len(workload) # => the single-number summary this example will show is misleading
print(round(mean_length, 1)) # => Output: 365.0
sorted_workload = sorted(workload) # => sorting is required to read off percentiles by index
p50 = sorted_workload[49] # => the median request length
p99 = sorted_workload[98] # => co-21: the tail -- almost the WORST request in this sample
print(p50, p99) # => Output: 200 4000
assert p99 > mean_length # => co-21: the tail sits FAR above the mean -- a single "average length" hides this
# => capacity planning off the mean alone under-provisions for exactly the requests that hurt most
print("ex-56 OK") # => a self-check marker confirming the tail-above-mean relationship held
Run: python3 example.py
Output:
100
365.0
200 4000
ex-56 OKKey takeaway: The median request length (200) sits well below the mean (365.0), and the p99 length (4000) is 20x the mean -- a single "average request" summary erases exactly the tail behavior that determines worst-case capacity.
Why it matters: This distribution -- not a single number -- is the input every capacity model (Example 59), load test (Example 57), and autoscaling policy (Example 61) in this section is actually built against; skipping straight to "the average request needs N" is precisely the mistake Example 58 demonstrates.
Example 57: Realistic Load Test
ex-57 · exercises co-22
A load test built from a real length distribution, driven through the same continuous-batching engine as production, measures how many wall-clock steps that traffic actually costs -- not how many steps a uniform-length assumption would predict.
learning/code/ex-57-realistic-load-test/example.py
"""Example 57: Realistic Load Test."""
from dataclasses import dataclass # => stdlib only -- driving the engine with a real mix needs no framework
@dataclass
class SimRequest: # => one simulated in-flight generation
id: str # => a label, useful only for reading print output
output_tokens: int # => how many decode steps THIS request needs before it is done
tokens_emitted: int = 0 # => mutated in place as the simulated batch runs
@property
def finished(self) -> bool: # => co-12: the per-request check continuous batching relies on
return self.tokens_emitted >= self.output_tokens # => True once this request hits its own target
def run_continuous_batch(requests: list[SimRequest], max_batch_slots: int) -> int:
# => co-12/co-22: the SAME continuous-batching engine from Example 31, now driven by a real length mix
pending = list(requests) # => the admission queue
active: list[SimRequest] = [] # => the currently-running slots -- capped by max_batch_slots
steps = 0 # => the simulated wall-clock -- one tick per decode step, for the WHOLE batch
while pending or active: # => keep going until EVERYTHING has both arrived and finished
while len(active) < max_batch_slots and pending: # => co-12: fill any FREE slot immediately
active.append(pending.pop(0)) # => moves ONE request from pending to active, per free slot
for r in active: # => every active request takes exactly one step this tick
r.tokens_emitted += 1 # => the ONLY mutation each active request undergoes per tick
steps += 1 # => one wall-clock tick has elapsed, regardless of how many slots were full
active = [r for r in active if not r.finished] # => co-12: retire finished requests IMMEDIATELY
return steps # => total wall-clock steps this realistic-mix workload actually took
LENGTH_BUCKETS = [(50, 4), (200, 3), (500, 2)] # => co-21: a small, deterministic stand-in for a real length mix
def expand_to_workload(buckets: list[tuple[int, int]]) -> list[int]:
# => co-21: the SAME expansion logic as Example 56, applied to a different bucket table
out: list[int] = [] # => accumulates one entry per simulated request
for length, weight in buckets: # => processes every bucket in the fixed table above
out.extend([length] * weight) # => `weight` copies of this length, deterministically
return out # => a flat list -- one length per simulated request
lengths = expand_to_workload(LENGTH_BUCKETS) # => co-21: 9 requests, mixed lengths -- not one uniform number
requests = [SimRequest(f"r{i}", length) for i, length in enumerate(lengths)] # => wraps each length as a SimRequest
total_tokens = sum(lengths) # => the total real work this load test represents
steps = run_continuous_batch(requests, max_batch_slots=4) # => the SAME engine, now under a realistic length mix
print(total_tokens, steps) # => Output: 1800 750
# => 1800 total tokens took 750 wall-clock steps -- co-22: NOT one step per token, batching overlaps work
throughput = total_tokens / steps # => co-22: real work delivered per wall-clock step, under this mix
print(round(throughput, 2)) # => Output: 2.4
assert steps < total_tokens # => co-22: WALL-CLOCK steps are far fewer than total tokens -- batching is working
# => this IS the payoff of Example 31's continuous batching, now measured on a realistic length mix
# => Example 58 shows why testing with a SINGLE averaged length instead of this mix hides real behavior
print("ex-57 OK") # => a self-check marker confirming the engine handled a realistic length mix correctly
Run: python3 example.py
Output:
1800 750
2.4
ex-57 OKKey takeaway: Driving the exact same continuous-batching engine that serves production traffic with a realistic, mixed-length workload is what makes a load test's throughput number ("2.4 tokens/step here") mean something -- the number is only as trustworthy as the length mix that produced it.
Why it matters: This is the difference between "load testing" and "load testing that lies" (Example 58): the engine under test must be identical to production, and the workload driving it must be identical to production's real length distribution, or the resulting capacity number describes a service that does not exist.
Example 58: Load Test That Lies
ex-58 · exercises co-22
Substituting the workload's average length for every request -- a common shortcut -- measures a different, easier workload than production actually sends, producing a load-test number that looks clean but is simply wrong.
learning/code/ex-58-load-test-that-lies/example.py
"""Example 58: Load Test That Lies."""
def simulate_steps(lengths: list[int], max_batch_slots: int) -> int:
# => co-22: the same continuous-batching mechanics as Example 57, generalized to a plain list of lengths
pending = list(lengths) # => the admission queue
active: list[int] = [] # => remaining tokens for each currently-active request
steps = 0 # => the simulated wall-clock -- one tick per decode step, for the WHOLE batch
while pending or active: # => keep going until EVERYTHING has both arrived and finished
while len(active) < max_batch_slots and pending: # => co-12: fill any FREE slot immediately
active.append(pending.pop(0)) # => moves ONE request from pending to active, per free slot
active = [r - 1 for r in active] # => every active request takes one step
steps += 1 # => one wall-clock tick has elapsed, regardless of how many slots were full
active = [r for r in active if r > 0] # => co-12: retire finished requests IMMEDIATELY
return steps # => total wall-clock steps this particular length list actually took
real_lengths = [50, 50, 50, 50, 200, 200, 200, 500, 500] # => co-21's realistic mixed workload, 9 requests
mean_length = round(sum(real_lengths) / len(real_lengths)) # => co-22: the WRONG shortcut -- "just use the average"
uniform_lengths = [mean_length] * len(real_lengths) # => a load test that assumes every request costs the SAME
print(mean_length) # => Output: 200
real_steps = simulate_steps(real_lengths, max_batch_slots=4) # => the TRUE mixed-length workload's cost
uniform_steps = simulate_steps(uniform_lengths, max_batch_slots=4) # => the "averaged" workload's cost
print(real_steps, uniform_steps) # => Output: 750 600
assert real_steps != uniform_steps # => co-22: the uniform-length load test measures a DIFFERENT workload than production
# => a load test built on the mean alone can pass in staging and still mislead about production capacity
print("ex-58 OK") # => a self-check marker confirming the averaged load test disagreed with the real one
Run: python3 example.py
Output:
200
750 600
ex-58 OKKey takeaway: The uniform-average load test needs 600 steps to finish the "same" 9 requests, while the real, mixed-length workload needs 750 -- a 25% understatement of the wall-clock cost, purely from replacing a distribution with its mean.
Why it matters: This is precisely the failure mode co-22 warns about: a load test that reports a clean, reassuring number is worse than no load test at all if that number describes traffic that will never actually arrive -- always drive a load test from the real distribution (Example 56), never its summary statistic.
Example 59: Capacity Model
ex-59 · exercises co-21, co-07
Chaining the cache-size formula (Example 11) with the GPU memory budget's remainder (Example 14) turns capacity planning into one division: how many requests' worth of cache fit in the memory actually left over.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["GPU memory budget"]:::blue --> B["- weights - activations<br/>- framework overhead"]:::orange
B --> C["= cache budget<br/>(co-18's remainder)"]:::teal
C --> D["/ per-request cache size<br/>(co-06's formula)"]:::teal
D --> E["= max concurrency"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-59-capacity-model/example.py
"""Example 59: Capacity Model."""
def kv_cache_bytes_per_request(num_layers: int, num_heads: int, head_dim: int, seq_len: int, bytes_per_value: int) -> int:
# => co-06: the SAME cache-size formula from Example 11, reused here as ONE input to capacity planning
return 2 * num_layers * num_heads * head_dim * seq_len * bytes_per_value # => *2 accounts for BOTH K and V
def max_concurrency(cache_budget_bytes: int, bytes_per_request: int) -> int:
# => co-07/co-21: cache budget, not raw compute, is what SETS the concurrency ceiling
return cache_budget_bytes // bytes_per_request # => floor division -- a partial extra request never counts
cache_budget_bytes = 20 * 1024**3 # => 20 GiB left over after weights and activations -- co-18's remainder
per_request_bytes = kv_cache_bytes_per_request(num_layers=32, num_heads=32, head_dim=128, seq_len=2048, bytes_per_value=2)
# => same formula, same shape as Example 11, applied to a larger 32-layer configuration
print(per_request_bytes) # => Output: 1073741824 -- exactly 1 GiB per request, at this configuration
capacity = max_concurrency(cache_budget_bytes, per_request_bytes) # => co-21: budget divided by per-request cost
print(capacity) # => Output: 20 -- 20 GiB of budget divided by 1 GiB per request
assert capacity > 0 # => co-18: this GPU can serve SOME concurrent requests at this configuration
assert capacity == cache_budget_bytes // per_request_bytes # => co-21: capacity planning IS this one division, end to end
# => Example 71 shows what happens when autoscaling reacts to this ceiling too aggressively
print("ex-59 OK") # => a self-check marker confirming the capacity model reduced to one clean division
Run: python3 example.py
Output:
1073741824
20
ex-59 OKKey takeaway: Everything upstream in this course -- the cache formula, the GPU memory budget, the remainder after weights -- converges into one number here: 20 concurrent requests, at this specific model and sequence-length configuration.
Why it matters: This is the number an operator actually needs before writing an autoscaling policy (Example 61) or answering "how many replicas do we need" -- and it is derivable entirely from architecture facts (co-06, co-18), with zero dependency on a specific serving framework's reported metrics.
Example 60: GPU Cold Start
ex-60 · exercises co-23
Starting a new GPU replica means loading many gigabytes of weights from storage before it can serve a single request -- a cold start measured in seconds, not the milliseconds typical of a stateless CPU service.
learning/code/ex-60-gpu-cold-start/example.py
"""Example 60: GPU Cold Start."""
def weights_load_time_seconds(model_size_bytes: int, storage_bandwidth_bytes_per_sec: float) -> float:
# => co-23: loading multi-GB weights from storage into GPU memory dominates a GPU service's cold start
return model_size_bytes / storage_bandwidth_bytes_per_sec # => bytes divided by bandwidth -- pure I/O time
model_size_bytes = 14_000_000_000 # => a 7B-parameter model stored in fp16
storage_bandwidth = 2_000_000_000 # => 2 GB/s, an illustrative, reasonably fast network-attached storage read
cold_start_seconds = weights_load_time_seconds(model_size_bytes, storage_bandwidth) # => co-23: the I/O floor alone
print(round(cold_start_seconds, 1)) # => Output: 7.0
typical_cpu_service_cold_start_seconds = 2.0 # => co-23: a stateless CPU service is usually READY within seconds
ratio = cold_start_seconds / typical_cpu_service_cold_start_seconds # => how many times slower the GPU service is
print(round(ratio, 2)) # => Output: 3.5
assert cold_start_seconds > typical_cpu_service_cold_start_seconds * 2 # => co-23: GPU cold start is FAR slower, not comparable
# => autoscaling policies (Example 71) that assume CPU-service cold-start speed will misbehave on GPU fleets
print("ex-60 OK") # => a self-check marker confirming GPU cold start dwarfed the CPU-service baseline
Run: python3 example.py
Output:
7.0
3.5
ex-60 OKKey takeaway: A 7-second cold start is more than triple a typical CPU service's cold start, and that gap only widens for larger models or slower storage -- GPU autoscaling cannot borrow a CPU service's "scale out and it's ready in a moment" assumption.
Why it matters: This single number is why co-23's proactive-scaling requirement exists at all -- Example 61 shows exactly how a naive, reactive autoscaling policy fails once this cold-start delay is accounted for.
Example 61: Autoscaling Policy
ex-61 · exercises co-23
A naive autoscaling policy waits until the queue is already deep before scaling out -- but by the time a new GPU replica finishes its multi-second cold start, the queue has grown further still. A GPU-aware policy must project that growth and scale out earlier.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["Queue depth: 8<br/>threshold: 20"]:::blue --> B["Naive: 8 < 20<br/>-> hold"]:::orange
A --> C["Proactive: project 7s<br/>cold start ahead"]:::teal
C --> D["8 + 2/sec * 7s = 22<br/>22 > 20 -> scale out NOW"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-61-autoscaling-policy/example.py
"""Example 61: Autoscaling Policy."""
def naive_scale_out_decision(queue_depth: int, threshold: int) -> bool:
# => co-23: naive CPU-style policy -- scale out only AFTER the queue is already deep
return queue_depth > threshold # => a purely reactive check -- no notion of how long relief will take
def proactive_scale_out_decision(queue_depth: int, threshold: int, cold_start_seconds: float, arrival_rate_per_sec: float) -> bool:
# => co-23: GPU-aware policy -- scale out EARLY enough the new replica is ready before the queue overflows
projected_queue_depth_after_cold_start = queue_depth + arrival_rate_per_sec * cold_start_seconds
# => co-23: projects forward by exactly the cold-start delay -- the reactive check's blind spot
return projected_queue_depth_after_cold_start > threshold # => decides on the PROJECTED depth, not today's
queue_depth = 8 # => today's queue depth -- looks fine on its own
threshold = 20 # => the same threshold both policies are checked against
naive = naive_scale_out_decision(queue_depth, threshold) # => only looks at right-now
proactive = proactive_scale_out_decision(queue_depth, threshold, cold_start_seconds=7.0, arrival_rate_per_sec=2.0)
# => co-23: same queue_depth, same threshold -- only the PROJECTION differs between the two calls
print(naive, proactive) # => Output: False True
# => a naive autoscaler here would start scaling only AFTER the queue has already overflowed
assert naive is False # => co-23: naive policy says "not yet" -- queue is under threshold RIGHT NOW
assert proactive is True # => co-23: proactive policy already sees the cold start will let the queue overflow
print("ex-61 OK") # => a self-check marker confirming the reactive-vs-proactive disagreement held
Run: python3 example.py
Output:
False True
ex-61 OKKey takeaway: Both policies look at the identical queue depth of 8, but only the proactive policy accounts for what happens during the 7-second cold start -- the naive policy scales out too late, exactly when the queue has already overflowed.
Why it matters: This is the concrete mechanism behind co-23's claim that "GPU autoscaling behaves unlike stateless CPU autoscaling" -- a policy copied directly from a CPU service's playbook will reliably under-provision a GPU service under real load growth.
Example 62: Package the Deployment
ex-62 · exercises co-24
A deployment manifest bundles weights, runtime, and configuration into one versioned unit -- this example builds one directly, deliberately flagging every field whose real value is volatile rather than guessing at a current framework version or GPU type.
learning/code/ex-62-package-the-deployment/example.py
"""Example 62: Package the Deployment."""
from dataclasses import asdict, dataclass # => stdlib only -- packaging a manifest needs no framework
@dataclass
class DeploymentManifest: # => co-24: the served artefact is weights + runtime + config, versioned as ONE unit
model_id: str # => which model -- half of "what's actually running in production"
model_revision: str # => co-24: pins the EXACT weight snapshot, not just a model family name
framework_version_pin: str # => `[Unverified]` placeholder -- see this course's Accuracy notes
gpu_type: str # => `[Unverified]` placeholder -- see this course's Accuracy notes
replica_count: int # => co-24: how many copies of this EXACT artefact are currently serving traffic
max_batch_slots: int # => co-24: a serving-time knob, versioned alongside the weights it runs against
manifest = DeploymentManifest(
model_id="example-org/example-7b", # => illustrative model ID -- co-24: every field is versioned together
model_revision="a1b2c3d", # => a specific, reproducible weight snapshot -- not "latest"
framework_version_pin="[Unverified]-pin-at-deploy-time", # => co-24: pin the REAL version at actual deploy time
gpu_type="[Unverified]-pin-at-deploy-time", # => co-24: pin the REAL hardware at actual deploy time
replica_count=3, # => an illustrative fleet size for this worked example
max_batch_slots=64, # => an illustrative batching cap for this worked example
)
print(asdict(manifest))
# => Output: {'model_id': 'example-org/example-7b', 'model_revision': 'a1b2c3d',
# => Output: 'framework_version_pin': '[Unverified]-pin-at-deploy-time', 'gpu_type': '[Unverified]-pin-at-deploy-time',
# => Output: 'replica_count': 3, 'max_batch_slots': 64}
# => co-24: every field a rollback needs to reconstruct EXACTLY what was running, in one dict
assert manifest.replica_count == 3 # => the manifest round-trips through asdict() without losing any field
# => co-24: this ONE dict is what an incident responder needs first when a served model misbehaves
assert "Unverified" in manifest.framework_version_pin # => co-24: volatile fields are FLAGGED, never hard-guessed
print("ex-62 OK") # => a self-check marker confirming the manifest holds every field a real deploy needs
Run: python3 example.py
Output:
{'model_id': 'example-org/example-7b', 'model_revision': 'a1b2c3d', 'framework_version_pin': '[Unverified]-pin-at-deploy-time', 'gpu_type': '[Unverified]-pin-at-deploy-time', 'replica_count': 3, 'max_batch_slots': 64}
ex-62 OKKey takeaway: A deployment manifest's whole job is to make the served unit reproducible and auditable -- every field that would otherwise be guessed or assumed (framework version, GPU type) is instead explicitly marked as something the deployer must pin at deploy time, not authoring time.
Why it matters: This shape -- a single versioned record covering model, runtime, and hardware -- is what makes the staged rollout in Example 63 meaningful: you can only roll a version forward or back if every deploy produces one identifiable, complete manifest like this one.
Example 63: Staged Model Rollout
ex-63 · exercises co-25
Changing the served model is a production change with quality consequences -- rolling it out in stages, gated on an error-rate guardrail at each stage, is the same discipline as any staged software release, applied to a model swap.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["Stage 0: 5% traffic"]:::blue --> B{"error_rate <=<br/>guardrail?"}:::orange
B -->|yes| C["Stage 1: 25% traffic"]:::teal
B -->|no| D["Halt -- stay at<br/>current stage"]:::orange
C --> E{"error_rate <=<br/>guardrail?"}:::orange
E -->|yes| F["Stage 2: 100% traffic"]:::teal
E -->|no| D
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-63-staged-model-rollout/example.py
"""Example 63: Staged Model Rollout."""
ROLLOUT_STAGES = [5, 25, 100] # => co-25: percentage of traffic sent to the NEW model version, staged
def next_stage(current_stage_index: int, error_rate: float, error_rate_guardrail: float) -> int | None:
# => co-25: advance ONLY if the current stage is healthy; `None` means "halt right here"
if error_rate > error_rate_guardrail: # => co-25: the guardrail is checked BEFORE any advance decision
return None # => `None` is the explicit "stop, do not proceed" signal
if current_stage_index + 1 < len(ROLLOUT_STAGES): # => is there a wider stage still ahead?
return current_stage_index + 1 # => co-25: advance to the NEXT wider stage
return current_stage_index # => already at 100% -- rollout complete, nothing further to advance to
healthy_progression: list[int | None] = [] # => records the stage index reached after each check
stage = 0 # => rollout always starts at the narrowest stage
for error_rate in [0.001, 0.002, 0.001]: # => healthy at every one of the three stages
stage_result = next_stage(stage, error_rate, error_rate_guardrail=0.01) # => co-25: one gated check per stage
healthy_progression.append(stage_result) # => records whatever this stage's check produced
stage = stage_result if stage_result is not None else stage # => only advances the LIVE stage on success
print(healthy_progression) # => Output: [1, 2, 2]
halted_stage = next_stage(0, error_rate=0.05, error_rate_guardrail=0.01) # => a 5% error rate at 5% traffic
print(halted_stage) # => Output: None
assert healthy_progression[-1] == 2 # => co-25: reached the final stage (index 2 => 100% traffic)
assert halted_stage is None # => co-25: rollout HALTS at 5% traffic rather than propagating a bad model further
# => Example 73 pairs this same guardrail idea with an automatic rollback trigger
print("ex-63 OK") # => a self-check marker confirming both the healthy progression and the halt held
Run: python3 example.py
Output:
[1, 2, 2]
None
ex-63 OKKey takeaway: A healthy new version progresses through every stage automatically, while an unhealthy one is caught and stopped at the very first, cheapest stage (5% traffic) -- the whole point of staging is bounding the blast radius of a bad model to a small fraction of traffic.
Why it matters: The guardrail here checks only error rate; Example 72 extends this exact pattern to also guard on tail latency, because a model swap can regress quality metrics silently while error rate stays perfectly clean.
Example 64: Serving Observability Dashboard
ex-64 · exercises co-26, co-16
Aggregating queue depth, batch occupancy, latency percentiles, and preemption rate from a set of completed requests turns raw traces into the handful of signals that actually explain a serving problem.
learning/code/ex-64-serving-observability-dashboard/example.py
"""Example 64: Serving Observability Dashboard."""
from dataclasses import dataclass # => stdlib only -- aggregating traces needs no framework
@dataclass
class RequestTrace: # => co-26/co-16: one completed request's observed metrics
ttft_ms: float # => co-16: this request's time-to-first-token
itl_ms: float # => co-16: this request's steady-state inter-token latency
preempted: bool # => co-14: whether this request was ever evicted mid-generation
def build_dashboard(traces: list[RequestTrace], queue_depth: int, batch_occupancy: float) -> dict[str, float | int]:
# => co-26: aggregates the signals that EXPLAIN a serving problem -- queue, occupancy, latency, preemption
ttfts = sorted(t.ttft_ms for t in traces) # => sorted so a percentile can be read off by index
itls = sorted(t.itl_ms for t in traces) # => sorted so a percentile can be read off by index
preemption_rate = sum(1 for t in traces if t.preempted) / len(traces) # => co-14: fraction of traces evicted
return { # => co-26: one dashboard snapshot -- every field a real on-call engineer would want
"queue_depth": queue_depth, # => co-13: how many requests are waiting, right now
"batch_occupancy": batch_occupancy, # => co-12: how full the active batch is, right now
"ttft_p50_ms": ttfts[len(ttfts) // 2], # => co-16: the median time-to-first-token
"itl_p50_ms": itls[len(itls) // 2], # => co-16: the median inter-token latency
"preemption_rate": round(preemption_rate, 2), # => co-14: how often eviction is happening
}
traces = [ # => co-26: five completed requests, two of which were preempted mid-generation -- a small fleet snapshot
RequestTrace(120, 18, False), # => a normal, uninterrupted request
RequestTrace(140, 20, False), # => a normal, uninterrupted request
RequestTrace(500, 45, True), # => co-14: this request was PREEMPTED -- its latency reflects that cost
RequestTrace(130, 19, False), # => a normal, uninterrupted request
RequestTrace(600, 50, True), # => co-14: preempted again -- two of five, a signal worth noticing
]
dashboard = build_dashboard(traces, queue_depth=12, batch_occupancy=0.92) # => co-26: one call, five signals
print(dashboard)
# => Output: {'queue_depth': 12, 'batch_occupancy': 0.92, 'ttft_p50_ms': 140, 'itl_p50_ms': 20, 'preemption_rate': 0.4}
# => co-26: Example 74 feeds this EXACT shape into a rule chain to diagnose what's actually wrong
assert dashboard["preemption_rate"] == 0.4 # => co-26: 2 of 5 traces were preempted -- a real, actionable number
# => a single p50 latency number alone would NEVER have surfaced this preemption signal
# => co-26: dashboards built from raw traces beat dashboards built from pre-averaged summaries
print("ex-64 OK") # => a self-check marker confirming the dashboard's preemption-rate arithmetic held
# => this is the last co-26 example -- capacity, batching, and preemption all converge into one view
Run: python3 example.py
Output:
{'queue_depth': 12, 'batch_occupancy': 0.92, 'ttft_p50_ms': 140, 'itl_p50_ms': 20, 'preemption_rate': 0.4}
ex-64 OKKey takeaway: Five completed requests reduce to five numbers -- but the 0.4 preemption rate is the one that immediately points at a specific, actionable cause (cache pressure), which is exactly what a raw list of individual traces does not surface on its own.
Why it matters: This dashboard shape is the input Example 74's incident-diagnosis logic consumes directly -- co-26's whole premise is that these five aggregated signals, read together, are usually enough to explain a serving problem without needing to inspect individual request traces.
Example 65: Build-vs-Buy Calculation
ex-65 · exercises co-27
Comparing self-hosted GPU cost per token against a hosted API's per-token price makes the build-versus-buy decision an explicit calculation rather than a gut feeling -- this example prices both sides using deliberately illustrative, round numbers.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["$2.00/GPU-hour<br/>at 800 tok/sec"]:::blue --> B["= $0.6944<br/>per million tokens"]:::teal
C["$0.000002/token<br/>hosted API"]:::orange --> D["= $2.00<br/>per million tokens"]:::orange
B --> E{"self-hosted <<br/>hosted API?"}:::teal
D --> E
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-65-build-vs-buy-calculation/example.py
"""Example 65: Build-vs-Buy Calculation."""
def self_hosted_cost_per_million_tokens(gpu_hourly_rate: float, tokens_per_second: float) -> float:
# => co-27: `[Unverified]` gpu_hourly_rate is an illustrative placeholder -- see this course's Accuracy notes
tokens_per_hour = tokens_per_second * 3600 # => converts a per-second rate into an hourly one
return (gpu_hourly_rate / tokens_per_hour) * 1_000_000 # => cost per million tokens, at FULL utilization
def hosted_api_cost_per_million_tokens(price_per_token: float) -> float:
# => co-27: `[Unverified]` price_per_token is an illustrative placeholder -- see this course's Accuracy notes
return price_per_token * 1_000_000 # => a simple linear scale -- no utilization assumption needed
gpu_hourly_rate = 2.00 # => `[Unverified]` illustrative placeholder, NOT a current market price
tokens_per_second = 800.0 # => sustained aggregate throughput at a chosen batch size
self_hosted = self_hosted_cost_per_million_tokens(gpu_hourly_rate, tokens_per_second) # => co-27: the "build" side
hosted_api = hosted_api_cost_per_million_tokens(price_per_token=0.000002) # => `[Unverified]` illustrative placeholder
print(round(self_hosted, 4), round(hosted_api, 4)) # => Output: 0.6944 2.0
assert self_hosted < hosted_api # => co-27: at THIS illustrative, FULL utilization, self-hosting is cheaper per token
# => Example 75 revisits this exact comparison under LESS than full utilization -- the answer can flip
print("ex-65 OK") # => a self-check marker confirming the build-vs-buy comparison held under these assumptions
Run: python3 example.py
Output:
0.6944 2.0
ex-65 OKKey takeaway: At full, sustained throughput, self-hosting comes out roughly 3x cheaper per million tokens than the illustrative hosted-API price here -- but that comparison assumes the GPU is running at full utilization every hour it is billed for, which Example 75 shows is rarely true.
Why it matters: This calculation is the one this topic's
Tensions & trade-offs section
explicitly warns is "almost always wrong" when done at full-utilization assumptions -- the calculation
itself is durable and correct, but every input number here is [Unverified] and must be re-sourced
against current pricing and REAL observed utilization before it informs a real decision.
Example 66: Quantization INT8 vs INT4 Memory
ex-66 · exercises co-19
Comparing INT8 and INT4 memory footprints directly, at the same parameter count, quantifies exactly how much additional memory INT4 buys over INT8 -- the second half-precision step is not the same size as the first.
learning/code/ex-66-quantization-int8-vs-int4-memory/example.py
"""Example 66: Quantization INT8 vs INT4 Memory."""
def model_memory_gb(param_count: int, bits_per_param: int) -> float: # => co-19: total storage for ALL parameters
return param_count * bits_per_param / 8 / (1024**3) # => params times bits, converted bits->bytes->GiB
param_count = 7_000_000_000 # => the SAME 7B parameter count used throughout the quantization examples
memory_by_precision = {bits: round(model_memory_gb(param_count, bits), 2) for bits in (16, 8, 4)} # => co-19: three points
print(memory_by_precision) # => Output: {16: 13.04, 8: 6.52, 4: 3.26}
savings_int8_vs_fp16 = memory_by_precision[16] - memory_by_precision[8] # => co-19: the middle-ground precision's savings
savings_int4_vs_fp16 = memory_by_precision[16] - memory_by_precision[4] # => co-19: the aggressive precision's savings
print(round(savings_int8_vs_fp16, 2), round(savings_int4_vs_fp16, 2)) # => Output: 6.52 9.78
assert savings_int4_vs_fp16 > savings_int8_vs_fp16 # => co-19: INT4 saves MORE than INT8, at a steeper quality cost
# => Example 67 shows a middle path -- blending precisions instead of picking one for the whole model
print("ex-66 OK") # => a self-check marker confirming INT4's larger memory saving held over INT8
Run: python3 example.py
Output:
{16: 13.04, 8: 6.52, 4: 3.26}
6.52 9.78
ex-66 OKKey takeaway: Dropping from fp16 to INT8 saves 6.52 GB; dropping further to INT4 saves 9.78 GB total -- each halving of bit width saves a diminishing absolute amount even though it is the same proportional halving, because the baseline it's halving keeps shrinking.
Why it matters: This is the concrete memory side of Example 52's quality/memory trade -- an operator choosing between INT8 and INT4 is choosing between a 6.52 GB saving with modest quality cost and a 9.78 GB saving with a steeper one, and the decision record pattern from Example 53 is exactly how to make that choice auditable.
Example 67: Mixed-Precision Serving
ex-67 · exercises co-19
Keeping a small fraction of parameters -- typically the layers most sensitive to quality -- at full precision while quantizing the rest lands memory usage strictly between the two pure extremes, buying back some quality at a small memory cost.
learning/code/ex-67-mixed-precision-serving/example.py
"""Example 67: Mixed-Precision Serving."""
def blended_memory_gb(param_count: int, fp16_fraction: float, quantized_bits: int) -> float:
# => co-19: keep a FRACTION of params at fp16 (typically attention/output layers) and quantize the rest
fp16_params = param_count * fp16_fraction # => the portion kept at full precision, for quality
quantized_params = param_count * (1 - fp16_fraction) # => the remainder, quantized down for memory
fp16_bytes = fp16_params * 16 / 8 # => full-precision portion's byte cost
quantized_bytes = quantized_params * quantized_bits / 8 # => quantized portion's byte cost
return (fp16_bytes + quantized_bytes) / (1024**3) # => the two portions summed, converted to GiB
param_count = 7_000_000_000 # => the SAME 7B parameter count used throughout the quantization examples
pure_int8 = blended_memory_gb(param_count, fp16_fraction=0.0, quantized_bits=8) # => nothing kept at fp16
mixed_10pct_fp16 = blended_memory_gb(param_count, fp16_fraction=0.10, quantized_bits=8) # => 10% kept for quality
pure_fp16 = blended_memory_gb(param_count, fp16_fraction=1.0, quantized_bits=8) # => everything at full precision
print(round(pure_int8, 2), round(mixed_10pct_fp16, 2), round(pure_fp16, 2)) # => Output: 6.52 7.17 13.04
assert pure_int8 < mixed_10pct_fp16 < pure_fp16 # => co-19: mixed precision sits STRICTLY between the two pure extremes
# => production quantizers pick this fraction per-layer, not as one global knob, for the same reason
print("ex-67 OK") # => a self-check marker confirming mixed precision lands strictly between the two extremes
Run: python3 example.py
Output:
6.52 7.17 13.04
ex-67 OKKey takeaway: Protecting just 10% of parameters at full precision costs only 0.65 GB over pure INT8 (6.52 GB to 7.17 GB) -- a small memory premium for whatever quality the protected layers recover, which is why mixed precision is a common middle ground rather than an all-or-nothing choice.
Why it matters: This is the practical shape a real quantization decision usually takes: not "INT8 or not," but "which small set of layers, if any, are worth protecting" -- the calculation here is exactly what feeds that narrower, more defensible decision.
Example 68: Tensor-Parallel vs Pipeline-Parallel
ex-68 · exercises co-20
Tensor parallelism exchanges activation data on every layer boundary; pipeline parallelism exchanges only at stage boundaries -- the two strategies trade interconnect traffic against pipeline idle time ("bubbles") in opposite directions.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["Tensor Parallel<br/>(4 devices)"]:::blue --> B["Exchange EVERY<br/>layer: 32 exchanges"]:::orange
C["Pipeline Parallel<br/>(4 stages)"]:::blue --> D["Exchange at stage<br/>boundaries: 3 exchanges"]:::teal
B --> E["384,000,000 bytes/step"]:::orange
D --> F["12,000,000 bytes/step<br/>+ pipeline bubble idle time"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-68-tensor-parallel-vs-pipeline-parallel/example.py
"""Example 68: Tensor-Parallel vs Pipeline-Parallel."""
def tensor_parallel_traffic_per_step(activation_bytes: int, num_devices: int) -> int:
# => co-20: TP exchanges activations on EVERY layer -- frequent, and it adds up fast
num_layers = 32 # => a stand-in transformer depth, used only to size the traffic estimate
return activation_bytes * (num_devices - 1) * num_layers # => co-20: traffic scales with BOTH devices AND layers
def pipeline_parallel_traffic_per_step(activation_bytes: int, num_stages: int) -> int:
# => co-20: PP exchanges activations only at STAGE BOUNDARIES -- rarer, but adds pipeline "bubble" idle time
return activation_bytes * (num_stages - 1) # => co-20: traffic scales with stages, NOT with layer count
activation_bytes = 4_000_000 # => per-layer-boundary activation volume that must be exchanged
tp_traffic = tensor_parallel_traffic_per_step(activation_bytes, num_devices=4) # => co-20: the layer-by-layer cost
pp_traffic = pipeline_parallel_traffic_per_step(activation_bytes, num_stages=4) # => co-20: the boundary-only cost
print(tp_traffic, pp_traffic) # => Output: 384000000 12000000
assert tp_traffic > pp_traffic # => co-20: TP moves FAR more data per step -- it needs a fast interconnect to pay off
# => Example 69 converts this traffic gap into actual wall-clock milliseconds on real links
print("ex-68 OK") # => a self-check marker confirming TP's far higher per-step traffic held
Run: python3 example.py
Output:
384000000 12000000
ex-68 OKKey takeaway: Tensor parallelism moves 32x more data per step than pipeline parallelism in this configuration -- TP only pays off with a fast, low-latency interconnect, while PP tolerates a slower one but pays in idle "bubble" time waiting for each stage to hand off.
Why it matters: This is why production model-parallel deployments frequently combine both strategies -- tensor parallelism within a tightly-interconnected node, pipeline parallelism across nodes -- rather than picking one exclusively; Example 69 prices exactly how much TP's traffic volume matters once a real interconnect bandwidth is applied.
Example 69: Interconnect Bottleneck Measurement
ex-69 · exercises co-20
Applying a real interconnect bandwidth to Example 68's tensor-parallel traffic volume shows the all-reduce time can be negligible on a fast link or can single-handedly exceed the entire decode-step latency budget on a slow one.
learning/code/ex-69-interconnect-bottleneck-measurement/example.py
"""Example 69: Interconnect Bottleneck Measurement. [GPU] illustrative reference numbers."""
def all_reduce_time_ms(traffic_bytes: int, interconnect_bandwidth_bytes_per_sec: float) -> float:
# => co-20: converts Example 68's byte count into WALL-CLOCK time, given a link's real bandwidth
return traffic_bytes / interconnect_bandwidth_bytes_per_sec * 1000 # => seconds converted to milliseconds
traffic_bytes = 384_000_000 # => co-20: tensor-parallel traffic per step, from Example 68
fast_interconnect = 300_000_000_000 # => 300 GB/s, illustrative -- e.g. a high-end multi-GPU link
slow_interconnect = 10_000_000_000 # => 10 GB/s, illustrative -- e.g. commodity networking between hosts
fast_time = all_reduce_time_ms(traffic_bytes, fast_interconnect) # => co-20: the SAME traffic, fast link
slow_time = all_reduce_time_ms(traffic_bytes, slow_interconnect) # => co-20: the SAME traffic, slow link
print(round(fast_time, 3), round(slow_time, 2)) # => Output: 1.28 38.4
decode_step_budget_ms = 20.0 # => co-15/co-16: this topic's own decode-step cost model, from Example 33
assert fast_time < decode_step_budget_ms # => co-20: a fast interconnect adds a NEGLIGIBLE fraction of the step budget
assert slow_time > decode_step_budget_ms # => co-20: a slow interconnect ALONE exceeds the entire step budget
# => choosing TP over PP without checking link bandwidth first can turn a compute win into a network loss
print("ex-69 OK") # => a self-check marker confirming the interconnect-speed-vs-step-budget comparison held
Run: python3 example.py
Output:
1.28 38.4
ex-69 OKKey takeaway: The identical 384 MB of traffic costs 1.28ms on a fast interconnect but 38.4ms on a slow one -- against a roughly 20ms decode-step budget, that is the difference between an unnoticeable overhead and a bottleneck that alone doubles every decode step's latency.
Why it matters: This is precisely why interconnect bandwidth -- not just GPU count -- is a first-order consideration when deciding to split a model across devices; a model-parallel deployment on the wrong hardware topology can be slower than a smaller, non-parallel deployment despite having more total compute available.
Example 70: P50/P99 Latency Under Load
ex-70 · exercises co-22, co-16
Applying the batch-size-to-latency cost model (Example 33) to a realistic, fluctuating batch-size trace shows the median latency stays low while a single occupancy spike drags the p99 far above it -- exactly the tail behavior a mean-only latency report would hide.
learning/code/ex-70-p50-p99-latency-under-load/example.py
"""Example 70: P50/P99 Latency Under Load."""
def itl_at_batch(batch_size: int) -> float: # => co-15/co-16: the SAME cost model as Example 33
return 15.0 + 1.0 * batch_size # => the SAME fixed-floor-plus-per-slot formula, reused here
def simulate_latencies_under_load(batch_sizes_over_time: list[int]) -> list[float]:
# => co-22: latency observed by requests DEPENDS on how full the batch was WHEN they were served
return [itl_at_batch(b) for b in batch_sizes_over_time] # => one latency sample per observed batch size
# => co-22: a realistic trace -- batch size fluctuates with arrivals, occasionally spiking
batch_trace = [4, 4, 5, 4, 32, 4, 4, 5, 4, 4] # => one spike to batch=32 among mostly-light load
latencies = simulate_latencies_under_load(batch_trace) # => co-22: the resulting per-request latency samples
sorted_latencies = sorted(latencies) # => sorting is required to read off percentiles by index
p50 = sorted_latencies[len(sorted_latencies) // 2] # => the median observed latency
p99_index = min(len(sorted_latencies) - 1, int(len(sorted_latencies) * 0.99)) # => clamped so it never overflows
p99 = sorted_latencies[p99_index] # => co-22: the tail -- almost the WORST observed latency
print(sorted_latencies) # => Output: [19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 20.0, 20.0, 47.0]
print(p50, p99) # => Output: 19.0 47.0
assert p99 > p50 * 1.5 # => co-22: the tail is dominated by the rare spike -- far above the typical case
print("ex-70 OK") # => a self-check marker confirming the tail-dominated-by-spike relationship held
Run: python3 example.py
Output:
[19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 19.0, 20.0, 20.0, 47.0]
19.0 47.0
ex-70 OKKey takeaway: One request out of ten -- the one served during the batch=32 spike -- sets a p99 latency of 47ms, nearly 2.5x the p50 of 19ms; a report that quotes only the mean or median would call this trace healthy.
Why it matters: This is why co-16's latency vocabulary insists on percentiles rather than averages, and why the observability dashboard (Example 64) tracks p50 alongside occupancy -- a p99 spike traces directly back to a specific, identifiable batch-occupancy event, not a mysterious, unexplainable slowdown.
Example 71: Autoscaling Thrashing
ex-71 · exercises co-23
A single scaling threshold with no gap flips direction the instant queue depth crosses it in either direction, producing constant scale-out/scale-in reversals -- a cooldown window after any scaling action is the standard fix.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph TD
A["21 -> scale_out"]:::orange --> B["19 -> scale_in"]:::orange
B --> C["22 -> scale_out"]:::orange
C --> D["18 -> scale_in"]:::orange
D --> E["21 -> scale_out"]:::orange
F["With cooldown:<br/>21 -> scale_out"]:::teal --> G["19 -> hold_cooldown"]:::teal
G --> H["22 -> scale_out"]:::teal
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-71-autoscaling-thrashing/example.py
"""Example 71: Autoscaling Thrashing."""
def naive_threshold_decision(queue_depth: int, threshold: int) -> str:
# => co-23: a SINGLE threshold with no gap flips direction the instant queue depth crosses it, either way
if queue_depth > threshold: # => any tick above threshold triggers an immediate scale-out
return "scale_out"
if queue_depth < threshold: # => any tick below threshold triggers an immediate scale-in
return "scale_in"
return "hold" # => exactly at threshold -- the only tick that does nothing
def hysteresis_decision(queue_depth: int, threshold: int, cooldown_active: bool) -> str:
# => co-23: fix -- ANY scaling action arms a cooldown that suppresses the very next decision
if cooldown_active: # => co-23: a cooldown from the PREVIOUS tick overrides whatever this tick would decide
return "hold_cooldown"
return naive_threshold_decision(queue_depth, threshold) # => no active cooldown -- decide normally
queue_trace = [21, 19, 22, 18, 21] # => co-23: queue depth oscillating right around threshold=20
# => this is a deliberately adversarial trace -- real traffic rarely bounces this tightly, but it CAN
naive_decisions = [naive_threshold_decision(q, threshold=20) for q in queue_trace] # => co-23: no memory between ticks
print(naive_decisions) # => Output: ['scale_out', 'scale_in', 'scale_out', 'scale_in', 'scale_out']
naive_scaling_events = sum(1 for d in naive_decisions if d != "hold") # => co-23: count of REAL scaling actions
print(naive_scaling_events) # => Output: 5 -- every single tick triggers a REVERSAL, this is thrashing
hysteresis_decisions: list[str] = [] # => records the decision made at each tick, cooldown-aware
cooldown = False # => starts clear -- the first tick is never suppressed
for q in queue_trace: # => processes the SAME queue trace as the naive policy, for a fair comparison
decision = hysteresis_decision(q, threshold=20, cooldown_active=cooldown) # => co-23: cooldown-aware decision
hysteresis_decisions.append(decision) # => records this tick's outcome before the cooldown updates
cooldown = decision in ("scale_out", "scale_in") # => co-23: any REAL action arms the cooldown for next tick
print(hysteresis_decisions) # => Output: ['scale_out', 'hold_cooldown', 'scale_out', 'hold_cooldown', 'scale_out']
hysteresis_scaling_events = sum(1 for d in hysteresis_decisions if d in ("scale_out", "scale_in")) # => co-23: real actions only
print(hysteresis_scaling_events) # => Output: 3
assert naive_scaling_events > hysteresis_scaling_events # => co-23: hysteresis cuts thrashing roughly in half here
# => co-23: the SAME idea that fixes preemption thrashing (Example 46) also fixes scaling thrashing
# => a cooldown window is a general pattern: any policy re-evaluated too fast can start oscillating
# => this trace also stresses cold-start cost (Example 60) -- every reversal pays that penalty again
print("ex-71 OK") # => a self-check marker confirming hysteresis measurably reduced the thrashing
Run: python3 example.py
Output:
['scale_out', 'scale_in', 'scale_out', 'scale_in', 'scale_out']
5
['scale_out', 'hold_cooldown', 'scale_out', 'hold_cooldown', 'scale_out']
3
ex-71 OKKey takeaway: The naive policy reverses direction on every single tick of this five-step trace, while a one-tick cooldown after any action cuts real scaling actions from 5 to 3 -- with zero change to the underlying threshold logic itself.
Why it matters: Thrashing autoscalers are expensive twice over on GPU infrastructure: every spurious scale-out pays Example 60's multi-second cold start for a replica that may be torn down again moments later, and thrashing spuriously churns the very cache state (Example 45) that preemption is already under pressure to protect.
Example 72: Canary Rollout With Metrics Guardrail
ex-72 · exercises co-25
Extending the staged rollout from Example 63 with a second guardrail -- p99 latency alongside error rate -- catches a quality regression that would otherwise slip through a model swap with a perfectly clean error rate.
learning/code/ex-72-canary-rollout-with-metrics-guardrail/example.py
"""Example 72: Canary Rollout With Metrics Guardrail."""
def evaluate_canary_stage(error_rate: float, p99_latency_ms: float, error_guardrail: float, latency_guardrail_ms: float) -> str:
# => co-25: a stage is healthy ONLY if BOTH guardrails hold -- either one alone is not enough
if error_rate > error_guardrail: # => the FIRST gate -- checked before latency is even looked at
return "halt: error_rate_guardrail_breached"
if p99_latency_ms > latency_guardrail_ms: # => co-25: a SECOND, independent gate -- errors alone are not enough
return "halt: latency_guardrail_breached"
return "advance" # => both gates cleared -- the stage is genuinely healthy
stage_results = [ # => co-25: the SAME guardrails, three different simulated health states
evaluate_canary_stage(0.002, 180.0, error_guardrail=0.01, latency_guardrail_ms=250.0), # => healthy on both axes
evaluate_canary_stage(0.002, 400.0, error_guardrail=0.01, latency_guardrail_ms=250.0), # => latency regression only
evaluate_canary_stage(0.05, 180.0, error_guardrail=0.01, latency_guardrail_ms=250.0), # => error spike only
]
print(stage_results)
# => Output: ['advance', 'halt: latency_guardrail_breached', 'halt: error_rate_guardrail_breached']
# => co-25: this contrasts with Example 63, which only checked ONE signal (error rate alone)
assert stage_results[0] == "advance" # => co-25: healthy on BOTH axes -- correctly allowed to proceed
# => "advance" here means the NEXT wider traffic stage becomes eligible, same idea as Example 63
assert stage_results[1] == "halt: latency_guardrail_breached" # => co-25: a latency-ONLY regression is caught too
# => an error-rate-only rollout gate would have MISSED this regression entirely
assert stage_results[2] == "halt: error_rate_guardrail_breached" # => co-25: an error-ONLY spike is caught too
print("ex-72 OK") # => a self-check marker confirming both independent guardrails caught their own failure mode
# => co-25: a real canary would also gate on custom business metrics, not just error rate and latency
Run: python3 example.py
Output:
['advance', 'halt: latency_guardrail_breached', 'halt: error_rate_guardrail_breached']
ex-72 OKKey takeaway: A stage with a perfectly healthy error rate is still halted by this policy the moment its p99 latency crosses the guardrail -- error rate alone is not a sufficient signal that a model swap is safe to keep rolling forward.
Why it matters: Latency-only regressions are common and easy to miss: a new model version might answer correctly every time yet take noticeably longer per token, degrading the user experience while every error-rate dashboard stays green -- this two-guardrail pattern is the minimum bar for catching that class of regression automatically.
Example 73: Rollback on Latency Regression
ex-73 · exercises co-25, co-26
Comparing the new version's p99 latency against the old version's, with an explicit tolerance, turns "did this deploy make things worse" into a deterministic rollback decision rather than a judgment call made under incident pressure.
learning/code/ex-73-rollback-on-latency-regression/example.py
"""Example 73: Rollback on Latency Regression."""
def should_rollback(old_version_p99_ms: float, new_version_p99_ms: float, regression_tolerance: float) -> bool:
# => co-25/co-26: rollback if the NEW version's tail latency regresses beyond a tolerated multiple of the OLD one
return new_version_p99_ms > old_version_p99_ms * (1 + regression_tolerance) # => a simple relative-threshold check
old_p99 = 200.0 # => the previous version's known-good tail latency
new_p99_ok = 220.0 # => 10% worse -- within a 15% tolerance
new_p99_bad = 300.0 # => 50% worse -- breaches a 15% tolerance
decision_ok = should_rollback(old_p99, new_p99_ok, regression_tolerance=0.15) # => co-25: the SAME tolerance, modest regression
decision_bad = should_rollback(old_p99, new_p99_bad, regression_tolerance=0.15) # => co-25: the SAME tolerance, large regression
print(decision_ok, decision_bad) # => Output: False True
assert decision_ok is False # => co-25: a modest regression stays within tolerance -- roll forward
assert decision_bad is True # => co-26: a large regression is caught by the SAME observability signal and rolled back
# => pairing this check with the staged rollout in Example 63 halts a bad model before it reaches 100%
print("ex-73 OK") # => a self-check marker confirming the tolerance correctly separated OK from bad regressions
Run: python3 example.py
Output:
False True
ex-73 OKKey takeaway: A 10% latency regression is treated as acceptable noise, while a 50% regression triggers an automatic rollback -- the explicit tolerance is what separates "normal variance" from "this deploy broke something," instead of leaving that call to whoever is paged during the incident.
Why it matters: This decision function is the natural next step after Example 72's guardrail halts a rollout mid-stage -- once a bad model is caught, this is the mechanical rule that decides whether to simply hold at the current stage or actively roll back to the previous, known-good version.
Example 74: Full Observability Explains an Incident
ex-74 · exercises co-26
Running a simple rule chain over the dashboard shape from Example 64 turns raw metrics into an actionable diagnosis -- demonstrating that co-26's five signals, read together, are usually enough to explain a serving problem without inspecting individual request traces.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A["Dashboard metrics"]:::blue --> B{"preemption_rate<br/>> 0.3?"}:::orange
B -->|yes| C["Cache pressure --<br/>check admission control"]:::teal
B -->|no| D{"queue_depth > 15 AND<br/>occupancy < 0.5?"}:::orange
D -->|yes| E["Undersized replica<br/>count"]:::teal
D -->|no| F{"itl_p50 > 40ms?"}:::orange
F -->|yes| G["Oversized batch for<br/>the configured SLO"]:::teal
F -->|no| H["No anomaly detected"]:::purple
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-74-full-observability-explains-an-incident/example.py
"""Example 74: Full Observability Explains an Incident."""
def diagnose(dashboard: dict[str, float | int]) -> str:
# => co-26: a simple, deterministic rule chain -- real observability tooling automates exactly this reasoning
if dashboard["preemption_rate"] > 0.3: # => co-14: the FIRST, highest-priority signal this rule chain checks
return "likely cause: cache pressure causing frequent preemption -- check admission control / batch size"
if dashboard["queue_depth"] > 15 and dashboard["batch_occupancy"] < 0.5: # => co-13/co-12: queueing WHILE underused
return "likely cause: undersized replica count -- requests queueing while GPU sits underused"
if dashboard["itl_p50_ms"] > 40: # => co-15: the LAST signal checked -- a pure latency/throughput tradeoff symptom
return "likely cause: oversized batch -- trading latency for throughput beyond the configured SLO"
return "no anomaly detected" # => none of the three rules fired -- the dashboard looks healthy
incident_dashboard = {"queue_depth": 12, "batch_occupancy": 0.92, "itl_p50_ms": 20, "preemption_rate": 0.4}
# => co-26: the SAME shape Example 64's build_dashboard() produced -- preemption_rate is the giveaway here
healthy_dashboard = {"queue_depth": 3, "batch_occupancy": 0.6, "itl_p50_ms": 18, "preemption_rate": 0.05}
# => low queue, low occupancy, low preemption -- none of the three rules above have a reason to fire
print(diagnose(incident_dashboard))
# => Output: likely cause: cache pressure causing frequent preemption -- check admission control / batch size
print(diagnose(healthy_dashboard)) # => Output: no anomaly detected
# => the SAME function, the SAME rule chain -- only the input dashboard's numbers changed
assert diagnose(incident_dashboard).startswith("likely cause: cache pressure") # => co-26: correctly diagnosed the incident
# => this is the payoff of everything from co-06 through co-26: one dashboard number resolves to one root cause
assert diagnose(healthy_dashboard) == "no anomaly detected" # => co-26: observability also confirms when NOTHING is wrong
print("ex-74 OK") # => a self-check marker confirming the rule chain diagnosed both dashboards correctly
# => capacity, batching, memory, deployment, and observability all converge into this one function
# => a real on-call runbook is this same if-chain, just with links to the matching dashboard panels
Run: python3 example.py
Output:
likely cause: cache pressure causing frequent preemption -- check admission control / batch size
no anomaly detected
ex-74 OKKey takeaway: The exact same dashboard shape from Example 64 -- queue depth, occupancy, ITL, and preemption rate -- is enough for a deterministic rule chain to correctly separate a genuine incident from a healthy service, with no need to reach for individual request-level traces.
Why it matters: This is the concrete argument for why co-26 lists exactly these five signals rather than "log everything": a small, well-chosen set of aggregated metrics is both cheaper to collect and faster to reason about during an incident than an unbounded pile of raw traces.
Example 75: TCO Sensitivity to Utilization
ex-75 · exercises co-28, co-27
The same self-hosted GPU cost per token varies enormously with utilization -- at 90% utilization self-hosting beats the illustrative hosted-API price from Example 65, but at 10% utilization it costs nearly 3.5x more, because idle GPU-hours still bill in full.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73
graph LR
A["90% utilization<br/>$0.7716/M tokens"]:::teal --> D["vs hosted API<br/>$2.00/M tokens"]:::orange
B["50% utilization<br/>$1.3889/M tokens"]:::orange --> D
C["10% utilization<br/>$6.9444/M tokens"]:::orange --> D
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-75-tco-sensitivity-to-utilization/example.py
"""Example 75: TCO Sensitivity to Utilization."""
def tco_per_million_tokens(gpu_hourly_rate: float, tokens_per_second_at_full_load: float, utilization: float) -> float:
# => co-28: the GPU bills for EVERY hour it's on, but only serves tokens during utilized hours
effective_tokens_per_hour = tokens_per_second_at_full_load * 3600 * utilization # => co-28: idle hours produce ZERO tokens
return (gpu_hourly_rate / effective_tokens_per_hour) * 1_000_000 # => the SAME formula as Example 65, utilization-scaled
gpu_hourly_rate = 2.00 # => `[Unverified]` illustrative placeholder -- see this course's Accuracy notes
tokens_per_second_at_full_load = 800.0 # => the SAME peak throughput used in Example 65
hosted_api_cost = 2.00 # => co-27's hosted-API comparison point from Example 65, per million tokens
tco_by_utilization = {u: round(tco_per_million_tokens(gpu_hourly_rate, tokens_per_second_at_full_load, u), 4) for u in (0.9, 0.5, 0.1)}
# => co-28: the SAME hardware, the SAME peak throughput -- only the UTILIZATION assumption changes here
print(tco_by_utilization) # => Output: {0.9: 0.7716, 0.5: 1.3889, 0.1: 6.9444}
assert tco_by_utilization[0.1] > hosted_api_cost # => co-28: at LOW utilization, self-hosting is WORSE than the API
assert tco_by_utilization[0.9] < hosted_api_cost # => co-28: at HIGH utilization, self-hosting is BETTER
# => the whole course closes on this point: infrastructure decisions are workload-shape decisions, not fixed answers
print("ex-75 OK") # => a self-check marker confirming utilization alone flips the build-vs-buy verdict
Run: python3 example.py
Output:
{0.9: 0.7716, 0.5: 1.3889, 0.1: 6.9444}
ex-75 OKKey takeaway: The exact same GPU, at the exact same hourly rate, swings from cheaper-than-the-API (0.9 utilization) to nearly 3.5x more expensive (0.1 utilization) purely as a function of how much of its billed time is actually spent serving tokens -- utilization is not a footnote to the build-versus-buy calculation, it is the calculation.
Why it matters: This is the honest total-cost-of-ownership comparison this topic's Tensions & trade-offs section insists on: not hourly GPU price against per-token API price at full load, but the same comparison made at the utilization you will realistically achieve -- which, for spiky or unpredictable traffic, is frequently much closer to 10% than 90%.
← Previous: Intermediate Examples · Next: Capstone →
Last updated July 25, 2026