Throughput economics
The unit economics of serving reduce to one metric — tokens/sec/GPU — turned into $/1M tokens. Batching lowers it to a knee, utilization silently multiplies it, and the self-host vs API decision is a break-even volume you plan under a latency SLO.
Learning objectives
- Compute the one metric that governs serving cost: tokens/sec/GPU.
- Convert a GPU-hour price into $/1M tokens, and compare it to an API's list price.
- Explain why batching lowers $/token up to the memory/latency knee — and why past it you pay for latency.
- Treat utilization as the hidden cost: an idle reserved GPU bills at 100%.
- Find the self-host vs API break-even volume and plan capacity under a p99 latency SLO.
1 · The only cost metric that matters: tokens/sec/GPU advanced
Everything in serving economics reduces to one number: tokens/sec/GPU (also written tok/s/GPU or the aggregate goodput). A GPU costs a fixed number of dollars per hour whether it emits 10 tokens/sec or 10,000. So $/token is just the GPU-hour price divided by how many tokens that GPU produces in an hour. Every other lever in this track — quantization (MS/IC2), the KV-cache (IC3), batching (IC4), tensor/pipeline parallel (MS4) — is worth exactly the tokens/sec/GPU it buys you. Learn to quote everything in this unit.
Order-of-magnitude anchors you should carry in your head (2025-era, single accelerator): an H100 rents for roughly $2–4/GPU-hour on-demand from the big clouds and specialist providers; an A100 is cheaper (~$1–2). A well-batched 7–8B model on one H100 does thousands of output tokens/sec aggregate; a 70B model sharded across GPUs does far fewer per GPU. These are ballparks to reason with — always re-measure your own stack (IC1).
2 · From GPU-hour to $/1M tokens advanced
Here is the whole cost model on one line. Three inputs — the GPU-hour price, the tokens/sec/GPU it sustains, and your utilization (the fraction of paid time the GPU is actually generating billable tokens) — collapse into a single $/1M tokens, which you then hold up against the API's published price.
This diagram is the entire cost model of self-hosting in one row. Read it left to right: three measurable inputs on the left combine into a single $/token number, which you then hold up against the API's list price on the right.
- GPU-hour $ — the fixed price you pay for the accelerator every hour (~$2–4 for an H100), whether it's busy or idle.
- tokens/sec/GPU — the aggregate output tokens that one GPU actually produces per second on your model and batch size. This is measured goodput, not the datasheet.
- × utilization — the fraction of paid time the GPU is really generating billable tokens. Idle hours still cost the full GPU-hour, so this quietly multiplies your $/token.
- $/1M tokens — the three inputs collapse to one number: GPU-hour price ÷ (tokens/sec × 3600 × utilization) × 1,000,000. This is your true unit cost.
- vs API price — the crossover. If your $/1M sits below the API's published price at your real volume, self-host is in the running; if above, the API is simply cheaper.
In short: Everything else in this track — batching, quantization, parallelism — is worth exactly the tokens/sec/GPU (the middle box) it buys. Quote every optimization in this one unit and the comparisons become apples-to-apples.
The arithmetic: one GPU-hour buys tokens_per_sec × 3600 tokens at full utilization. Divide the hourly price by that token count and scale to a million. Then divide by utilization, because tokens you didn't serve still cost you the full hour. If your measured cost sits below the API's $/1M price at your real volume, self-hosting is in the running; if it sits above, the API is simply cheaper and you should stop here.
3 · The $/1M-tokens calculator advanced
This is the model above as code. It is deliberately tiny — the point of throughput economics is that the formula is trivial and the inputs (measured goodput, honest utilization) are where all the judgement lives.
cost_per_mtok.pydef cost_per_million_tokens(gpu_hourly, tokens_per_sec, utilization):
"""$/1M output tokens for a self-hosted GPU.
gpu_hourly : $ per GPU-hour (e.g. 3.00 for an H100 on-demand)
tokens_per_sec : measured aggregate output tokens/sec on that GPU
utilization : fraction of paid time actually generating (0<u<=1)
"""
if tokens_per_sec <= 0 or not (0 < utilization <= 1):
raise ValueError("need tokens_per_sec>0 and 0<utilization<=1")
tokens_per_hour = tokens_per_sec * 3600 * utilization
dollars_per_token = gpu_hourly / tokens_per_hour
return dollars_per_token * 1_000_000
for u in (1.0, 0.5, 0.2):
c = cost_per_million_tokens(gpu_hourly=3.00, tokens_per_sec=2500, utilization=u)
print(f"util {u:>4.0%} -> ${c:6.2f} / 1M tokens")
util 100% -> $ 0.33 / 1M tokens
util 50% -> $ 0.67 / 1M tokens
util 20% -> $ 1.67 / 1M tokens
This is the cost model as code — deliberately tiny. The formula is trivial; the judgement is entirely in the inputs you feed it (measured throughput, honest utilization).
- The guard rejects nonsense inputs — you need a positive token rate and a utilization strictly between 0 and 1 — so a typo can't silently produce a garbage cost.
tokens_per_hour = tokens_per_sec * 3600 * utilizationis the key line: one paid hour buystokens_per_sec × 3600tokens only at full utilization. Multiplying byutilizationdiscounts to what the GPU actually generates.- Dividing the hourly price by those tokens gives $/token; scaling by 1,000,000 gives the industry-standard $/1M-tokens quote.
- The loop runs the same 2,500 tok/s H100 at 100%, 50%, and 20% utilization to isolate one variable: utilization alone.
What the output means: Three lines. At 100% utilization the H100 costs ~$0.33/1M tokens; halve utilization and it doubles to $0.67; drop to 20% and it's $1.67 — the GPU-hour price never changed.
Try this: Change gpu_hourly to 2.00 (a cheaper A100-ish rate) and re-run. Notice the cost scales linearly with the hourly price but is dominated by how full you keep the GPU.
At full utilization an H100 doing 2,500 tok/s costs about $0.33 per million output tokens — cheaper than most frontier APIs. But halve utilization and the cost doubles; drop to 20% and you're at $1.67. The GPU-hour price never changed. Utilization is the entire story, which is why it gets its own section.
4 · Batching: the throughput→cost curve and its knee expert
Decode is memory-bound: every token re-reads the model's weights (IC1/IC3). Batching amortizes that one weight-read across many concurrent requests (IC4), so aggregate tokens/sec/GPU climbs steeply with batch size — and $/token falls in lockstep. But it does not climb forever. Two ceilings bite: the KV-cache runs out of GPU memory (you can't hold more sequences), and per-request latency (TPOT) rises as the batch competes for compute. The knee is where throughput flattens; pushing past it buys almost no cost savings while blowing your latency SLO.
batch_curve.pydef throughput_at_batch(batch, per_stream_toks=55, knee=32, ceiling=3200):
"""Toy model of aggregate tokens/sec vs batch size.
Near-linear early (weight-read amortized), saturating past the knee
where KV-cache/compute cap the GPU. Illustrative shape, not a spec."""
ideal = per_stream_toks * batch # if it scaled forever
saturating = ceiling * (batch / (batch + knee)) # diminishing returns
return round(min(ideal, saturating))
def cost_per_mtok(gpu_hourly, tps):
return gpu_hourly / (tps * 3600) * 1_000_000
print("batch tok/s $/1M marginal $/1M saved")
prev = None
for b in (1, 4, 8, 16, 32, 64, 128):
tps = throughput_at_batch(b)
c = cost_per_mtok(3.00, tps)
delta = "" if prev is None else f"{prev - c:+.3f}"
print(f"{b:>4} {tps:>5} ${c:6.3f} {delta}")
prev = c
batch tok/s $/1M marginal $/1M saved
1 55 $15.152
4 220 $ 3.788 +11.364
8 440 $ 1.894 +1.894
16 880 $ 0.947 +0.947
32 1600 $ 0.521 +0.426
64 2133 $ 0.391 +0.130
128 2560 $ 0.326 +0.065
This models why batching lowers $/token — but only up to a point. Aggregate throughput climbs nearly linearly at first (one weight-read amortized across many requests), then saturates as the KV-cache and compute cap the GPU.
ideal = per_stream_toks * batchis the fantasy where throughput scales forever — each extra request adds a full stream's worth of tokens.saturating = ceiling * (batch / (batch + knee))is the reality: a curve that approaches a hardceilingand bends over near thekneebatch size.min(ideal, saturating)takes whichever is smaller, so early batches are linear and later ones flatten.cost_per_mtokturns each throughput number into $/1M tokens at a $3/hr GPU — higher throughput, lower cost.- The
marginalcolumn subtracts each row's cost from the previous, showing how much each doubling of the batch actually saves.
What the output means: A table from batch 1 to 128. Cost falls fast early (1→4 saves ~$11/1M) then crawls (64→128 saves ~6¢), while every step up the batch adds latency.
Try this: Find the knee in the marginal column — the batch where extra batching saves less than a cent per 1M tokens. That's where you stop batching and start scaling out (adding GPUs) instead.
Read the last column. Going from batch 1→4 saves $11/1M tokens; 32→64 saves 13¢; 64→128 saves 6¢ — and every step up the batch adds queueing and TPOT latency. The economically interesting region is the early, steep part; beyond the knee (~32 here) you are trading real latency for pennies. Batch to the knee, not to the ceiling.
5 · Utilization — the hidden cost expert
A reserved or on-demand GPU bills for wall-clock time, not for tokens. Every second it sits idle — overnight, between traffic bursts, waiting on a slow upstream, or over-provisioned "just in case" — is billed at 100% and produces zero tokens. This is why two teams running the same model on the same GPU can have a 5× difference in $/token: one runs it hot, the other runs it at 20% average utilization. Diurnal traffic (busy 9–5, dead at night) alone can cap your realistic average utilization near 30–40% unless you autoscale, batch offline work into the troughs, or rent by the token.
| Purchasing mode | Rel. price | Commitment | Use for |
|---|---|---|---|
| On-demand | 1.0× (baseline) | none | spiky/unknown load, dev |
| Reserved / committed (1–3yr) | ~0.4–0.6× | you pay even when idle | steady 24/7 baseline |
| Spot / preemptible | ~0.2–0.4× | can be reclaimed anytime | batch, fault-tolerant, checkpointed |
The purchasing mode is a second utilization lever. Reserved capacity is cheap per hour only if you keep it busy — an idle reserved GPU is the worst of both worlds. Spot is the cheapest raw price but can vanish mid-request, so it fits batch/offline (AP1-style) work, not your latency-SLO online path. A common pattern: reserved for the predictable baseline, on-demand to absorb peaks, spot for overnight batch.
6 · Self-host vs API: the break-even volume expert
Self-hosting converts a per-token cost (the API bill, which scales with usage) into a fixed monthly cost (GPUs you rent whether busy or not) plus a small marginal per-token cost. The API is a flat line through the origin; self-host is a line with a big y-intercept and a gentle slope. They cross at one volume — your break-even. Below it the API wins on cost; above it self-host can win, if you actually reach the utilization your fixed cost assumed. This is the GPU-serving version of the general build-vs-buy from LM5.
breakeven.pydef breakeven_volume(api_price_per_mtok, gpu_monthly_fixed, self_host_per_mtok):
"""Monthly volume (in millions of tokens) where self-host total cost
equals the API bill. api = api_price * V ; self = fixed + shp * V.
Returns the crossover V in M-tokens/month, or None if self-host never wins."""
slope_gap = api_price_per_mtok - self_host_per_mtok
if slope_gap <= 0:
return None # API's per-token price already <= our marginal cost
return gpu_monthly_fixed / slope_gap
def monthly(api, fixed, shp, V):
return api * V, fixed + shp * V # (api_bill, self_host_bill) at V M-tokens
# One reserved H100 ~ $1.80/hr committed -> ~$1,314/month fixed.
# Self-host marginal cost ~ $0.33/1M (from section 3). API list ~ $3.00/1M.
be = breakeven_volume(api_price_per_mtok=3.00, gpu_monthly_fixed=1314,
self_host_per_mtok=0.33)
print(f"break-even at ~{be:,.0f} M tokens/month")
for V in (100, be, 1000):
api_bill, sh_bill = monthly(3.00, 1314, 0.33, V)
winner = "API" if api_bill < sh_bill else "self-host"
print(f" V={V:>7,.0f}M API=${api_bill:>8,.0f} self=${sh_bill:>8,.0f} -> {winner}")
break-even at ~492 M tokens/month
V= 100M API=$ 300 self=$ 1,347 -> API
V= 492M API=$ 1,476 self=$ 1,476 -> self-host
V= 1,000M API=$ 3,000 self=$ 1,644 -> self-host
This finds the monthly volume where self-hosting stops being more expensive than the API. The API is a pure per-token cost (a line through the origin); self-host is a big fixed cost plus a small per-token cost (a line with a high y-intercept and gentle slope). They cross once.
slope_gap = api_price_per_mtok - self_host_per_mtokis how much cheaper each million tokens is once you're self-hosting. If it's zero or negative (if slope_gap <= 0), the API's per-token price already beats your marginal cost and self-host can never win — returnNone.- Otherwise the break-even volume is
gpu_monthly_fixed / slope_gap: the fixed GPU cost divided by the per-million savings tells you how many millions of tokens it takes to pay off the fixed cost. monthly(...)returns both bills at a given volume so you can see who wins where.- The example uses a reserved H100 (~$1,314/month fixed), a $0.33/1M marginal cost, and a $3.00/1M API price.
What the output means: Break-even ~492M tokens/month. Below it the API is cheaper (100M: $300 vs $1,347); above it self-host wins (1,000M: $3,000 vs $1,644).
Try this: Add half an engineer's salary (say +$6,000) to gpu_monthly_fixed and re-run — watch the break-even volume jump far to the right. Ops cost is the thing spreadsheets forget.
Below ~492M tokens/month the single reserved GPU's fixed cost isn't amortized and the API is cheaper; above it self-host pulls ahead, and at 1B tokens/month it's nearly 2× cheaper. But note the trap: that $1,314 fixed cost assumes the GPU is busy enough to serve 492M tokens. If diurnal traffic leaves it 30% utilized, your effective marginal cost triples and the break-even volume moves far to the right. Break-even and utilization are the same conversation.
7 · Capacity planning under a latency SLO professional
You cannot simply max out the batch to minimize cost, because p99 latency is a hard constraint (IC4). Capacity planning is: given a peak request rate and a per-GPU throughput that is only valid while you stay under the SLO, how many GPUs do you need — and how much does that fleet cost per million tokens at the utilization the peak/average ratio implies? The planner below sizes the fleet to peak, then reports the honest cost at the resulting average utilization.
capacity.pyimport math
def plan_capacity(peak_tokens_per_sec, per_gpu_tps_at_slo, avg_to_peak_ratio,
gpu_hourly, headroom=1.2):
"""Size a GPU fleet to a peak load while respecting a latency SLO.
per_gpu_tps_at_slo : tokens/sec/GPU you can sustain *without* breaching p99
(well below the throughput ceiling from batch_curve).
avg_to_peak_ratio : average load / peak load (drives real utilization).
headroom : over-provision factor for safety (1.2 = 20% slack).
"""
needed = peak_tokens_per_sec / per_gpu_tps_at_slo * headroom
gpus = math.ceil(needed)
# Average utilization = (avg load) / (provisioned capacity)
provisioned_tps = gpus * per_gpu_tps_at_slo
avg_tps = peak_tokens_per_sec * avg_to_peak_ratio
utilization = avg_tps / provisioned_tps
monthly_fixed = gpus * gpu_hourly * 24 * 30
cost_per_mtok = gpu_hourly / (per_gpu_tps_at_slo * 3600 * utilization) * 1_000_000
return {"gpus": gpus, "avg_util": round(utilization, 3),
"monthly_$": round(monthly_fixed), "$_per_mtok": round(cost_per_mtok, 2)}
plan = plan_capacity(peak_tokens_per_sec=9000, per_gpu_tps_at_slo=1500,
avg_to_peak_ratio=0.4, gpu_hourly=3.00)
print(plan)
{'gpus': 8, 'avg_util': 0.3, 'monthly_$': 17280, '$_per_mtok': 1.85}
This sizes a GPU fleet to a traffic peak while respecting a latency SLO, then reports the honest cost at the utilization that peak-vs-average traffic actually produces — usually far worse than the 100%-utilization demo number.
needed = peak / per_gpu_tps_at_slo * headroomdivides the peak load by the throughput each GPU can sustain without breaching p99 (well below the raw ceiling), times a safety headroom.math.ceilrounds up to whole GPUs.provisioned_tps = gpus * per_gpu_tps_at_slois the capacity you're paying for. Average load is onlyavg_to_peak_ratioof the peak, so realutilization = avg_tps / provisioned_tpsis low — you bought for the peak but mostly run below it.monthly_fixedbills every GPU 24×30 hours regardless of load.cost_per_mtokreuses the section-3 formula but with the real utilization, so the returned $/token reflects the idle hours you're paying for.
What the output means: 8 GPUs, ~30% average utilization, ~$17,280/month, and $1.85/1M tokens — over 5× the $0.33 the full-utilization estimate promised, purely because you size to peak but run at average.
Try this: Raise avg_to_peak_ratio toward 1.0 (flatter traffic) and watch cost fall toward the $0.33 floor. That's the payoff of autoscaling, draining batch work into troughs, or serving peaks on on-demand/spot instead of reserving for them.
Sizing to a 9,000 tok/s peak at a SLO-safe 1,500 tok/s/GPU (well below the batch ceiling) needs 8 GPUs with 20% headroom. But because average load is only 40% of peak and we hold 20% headroom, the fleet averages just 30% utilization — so the real cost is $1.85/1M tokens, more than 5× the $0.33 the full-utilization spreadsheet promised. That gap between the demo number and the SLO-and-diurnal number is exactly what separates a capacity plan that survives contact with production from one that doesn't. To close it: autoscale down at night, drain batch work into the troughs, or serve the peak on on-demand/spot instead of reserving for it.
8 · The tech-lead's serving-cost model tech-lead
A lead owns the whole model end to end: measure real tokens/sec/GPU and real utilization (never trust the datasheet), quote everything in $/1M tokens so batching, quantization, and parallelism can be compared apples-to-apples, compute the API break-even honestly with ops cost folded in, size capacity to the p99 SLO rather than the throughput ceiling, and pick the reserved/on-demand/spot mix that matches the traffic shape. The output is a defensible build-vs-buy recommendation and a $/token you can put in front of finance — with the utilization assumption stated out loud, because that is the number that quietly breaks every naive estimate.
For live pricing you would pull GPU-hour rates from the cloud pricing API rather than hard-coding them. The snippet below sketches that; it is real infrastructure code and is not part of the offline labs above.
live_pricing.py# ▶ needs cloud creds — real infra, NOT run by this lesson's offline labs.
# Requires: pip install boto3, configured AWS credentials, pricing:GetProducts.
import boto3, json
def ondemand_gpu_price(instance_type="p5.48xlarge", region="US East (N. Virginia)"):
pricing = boto3.client("pricing", region_name="us-east-1") # Pricing API lives in us-east-1
resp = pricing.get_products(
ServiceCode="AmazonEC2",
Filters=[
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_type},
{"Type": "TERM_MATCH", "Field": "location", "Value": region},
{"Type": "TERM_MATCH", "Field": "tenancy", "Value": "Shared"},
{"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": "Linux"},
{"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
],
MaxResults=1)
product = json.loads(resp["PriceList"][0])
terms = product["terms"]["OnDemand"]
dim = next(iter(next(iter(terms.values()))["priceDimensions"].values()))
hourly = float(dim["pricePerUnit"]["USD"])
gpus_per_instance = 8 # p5.48xlarge = 8x H100
return hourly / gpus_per_instance # -> feed into cost_per_million_tokens()
# print(round(ondemand_gpu_price(), 2)) # ~ per-GPU-hour, then run the offline model above
Exercise MS5.1 — Quote your stack in $/1M tokens
Context: Quoting a stack honestly means quoting it at the utilization you will actually run at, and marking the batch past which more batching buys almost nothing.
Your task: Measure aggregate output tokens/sec/GPU at a batch that stays under a p99 TPOT SLO, compute $/1M at 100%, 50%, and your honest average utilization, and mark the batching knee.
Requirements:
- Choose a batch that holds a p99 TPOT SLO you set
- Report $/1M at 100%, 50%, and honest-average utilization
- Find the knee: the batch past which batching saves less than a cent per 1M
- Report the three cost numbers and the knee batch
💡 Hint: The three utilization figures show the true cost band; the knee is where the throughput curve flattens.
Exercise MS5.2 — Break-even and a capacity plan
Context: The self-host-vs-API decision moves once you fold in the people cost, and the final fleet size falls out of your peak rate, an SLO-safe throughput, and an honest avg/peak ratio.
Your task: Find the self-host vs API break-even, re-run it with half an engineer's monthly salary folded into the fixed GPU cost, size the fleet from your peak rate, and explain why your $/1M exceeds the 100%-utilization number.
Requirements:
- Compute the break-even volume, then re-compute it with salary in the fixed cost
- Note how far the break-even moves once people cost is included
- Size the fleet from peak rate, SLO-safe per-GPU throughput, and an avg/peak ratio
- Explain why real $/1M beats the 100%-util figure and name the lever you'd pull
💡 Hint: Folding fixed cost in raises the break-even; the gap to the 100%-util price is utilization, and autoscaling / spot / trough-batching are the levers.
An H100 rents for $3/GPU-hour and sustains 2,500 output tokens/sec. Your teammate quotes $0.33/1M tokens. Six months later finance says the real bill is ~$1/1M tokens even though the GPU-hour price and the model never changed. What single number explains the whole gap, and why?
Show answer
You're at batch 32 (near the knee: ~1,600 tok/s, $0.52/1M). A PM asks you to push to batch 128 "to cut cost." Using the curve from section 4, is that a good trade? What does it actually buy and cost?
Show answer
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Serving cost tracks throughput per GPU, not raw speed, so the metric that turns into dollars is tokens/sec/GPU — and by it, a bigger cluster can be the worse deal.
Your task: Write tokens_per_sec_per_gpu(total_tps, gpus) and rank three configs by it.
Requirements:
- Normalize total throughput by GPU count
- Rank the configs best-first on the normalized number
- Show more total tok/s can still be worse per GPU
- Frame tok/s/GPU as the number that becomes cost
💡 Hint: Raw tokens/sec flatters big clusters; divide by GPUs before comparing.
Show solution
Normalize throughput by GPU count so configs are comparable. Runnable:
def tps_per_gpu(total_tps, gpus):
return total_tps / gpus
configs = [("A: 1 GPU", 1200, 1), ("B: 4 GPU", 4000, 4), ("C: 8 GPU", 9600, 8)]
ranked = sorted(configs, key=lambda c: tps_per_gpu(c[1], c[2]), reverse=True)
for name, tps, g in ranked:
print(f"{name:>10}: {tps_per_gpu(tps, g):7.1f} tok/s/GPU")
# C (1200) = A (1200) > B (1000): more total speed can still be worse per GPU
Raw tokens/sec flatters big clusters; tokens/sec/GPU is what turns into dollars.
Context: Self-host economics collapses to one conversion line, and everything else — batching, utilization — just moves the tokens/sec that goes into it.
Your task: Build the $/1M calculator (= gpu_hour / 3600 / tokens_per_sec × 1e6) and compare a self-host config to a $3/1M API price.
Requirements:
- Cost per token = price per GPU-second ÷ tokens per second
- Scale to a million tokens for the quoted figure
- Compare at least one self-host config against the $3/1M API
- Report whether self-host is cheaper or pricier at that throughput
💡 Hint: Only tokens_per_sec varies with your engineering choices; the rest of the line is fixed by the GPU price.
Show solution
Cost per token = price per GPU-second ÷ tokens per second; scale to 1M. Runnable:
def cost_per_million(gpu_hour, tokens_per_sec):
per_sec = gpu_hour / 3600.0
per_token = per_sec / tokens_per_sec
return per_token * 1_000_000
for name, hr, tps in [("A100 @ 900 tok/s", 2.0, 900),
("H100 @ 2500 tok/s", 4.0, 2500)]:
c = cost_per_million(hr, tps)
print(f"{name:>18}: ${c:5.2f}/1M vs API $3.00 -> "
f"{'cheaper' if c < 3 else 'pricier'} to self-host")
The whole self-host economics collapses to this one line — everything else (batching, utilization) just moves tokens_per_sec.
Context: Batching raises throughput by amortizing weight loads, but with diminishing returns up to a memory/latency knee — and the knee, not the max batch, is the operating point.
Your task: Model tokens/sec as a saturating function of batch size, convert to $/1M, and show cost falls then flattens as batch grows.
Requirements:
- Throughput saturates toward a peak as batch grows
- $/1M is inversely proportional to throughput
- Tabulate batch, tok/s, $/1M, and the marginal saving per doubling
- Show the marginal saving shrinks each doubling — that is the knee
💡 Hint: Past the knee you pay latency (bigger batches wait longer) for pennies of savings.
Show solution
Model throughput as saturating in batch; cost is inversely proportional. Runnable:
def throughput_at_batch(b, peak=4000.0, half=8.0):
# saturating curve: approaches `peak` tok/s, half-saturation at batch=half
return peak * b / (b + half)
def cost_per_million(gpu_hour, tps):
return (gpu_hour / 3600.0 / tps) * 1_000_000
print("batch tok/s $/1M marginal $/1M saved")
prev = None
for b in (1, 4, 8, 16, 32, 64, 128):
tps = throughput_at_batch(b)
c = cost_per_million(4.0, tps)
delta = "" if prev is None else f"{prev - c:6.3f}"
print(f"{b:>5} {tps:8.0f} {c:7.3f} {delta}")
prev = c
# savings shrink each doubling -- the knee is where marginal savings stop mattering
Past the knee you pay latency (bigger batches wait longer) for pennies of savings — the knee, not max batch, is the operating point.
Context: A GPU bills for every hour whether or not it is serving tokens, so utilization is the hidden multiplier: effective cost is the raw cost divided by how busy the card actually is.
Your task: Model effective $/1M as raw $/1M ÷ utilization and show that 40% utilization nearly triples the true cost.
Requirements:
- Utilization divides throughput, so it multiplies cost
- Effective cost =
raw_cost / utilization - Tabulate several utilizations from 100% down
- Show ~40% utilization is roughly 2.5× the sticker cost
💡 Hint: Idle GPU-hours are pure loss; this is the number that lets an API's pooled utilization beat a self-host quote.
Show solution
Utilization divides throughput and so multiplies cost. Runnable:
def effective_cost(raw_cost_per_m, utilization):
return raw_cost_per_m / utilization
raw = 1.78 # $/1M at full tilt
for u in (1.0, 0.7, 0.4, 0.2):
print(f"utilization {u:>4.0%}: true cost = ${effective_cost(raw, u):5.2f}/1M")
# 40% util -> 2.5x the sticker cost -- idle GPUs are the silent budget killer
This is why a "cheap on paper" self-host loses to an API: the API bills only used tokens, while your idle GPU-hours are pure loss.
Context: Self-host carries a fixed monthly GPU cost regardless of volume while an API is pure per-token, so there is a break-even volume above which self-host amortizes and below which the API wins.
Your task: Find the monthly token volume where self-host becomes cheaper than a per-token API price.
Requirements:
- Fixed monthly cost =
gpus × gpu_hour × 24 × 30 - Break-even volume =
fixed_cost / api_price_per_token - API is cheaper below break-even, self-host above
- Note low utilization pushes break-even higher (idle hours buy fewer real tokens)
💡 Hint: It is fixed cost divided by API price-per-token; utilization erodes the tokens the fixed bill actually buys.
Show solution
Break-even = fixed monthly GPU cost ÷ API price/token: above that volume the API bill would exceed the fixed fleet cost. Runnable:
def break_even_tokens(gpus, gpu_hour, api_per_million):
# self-host bills a fixed monthly GPU cost regardless of volume;
# the API bills per token. Break-even is where API cost = fixed cost.
fixed_month = gpus * gpu_hour * 24 * 30
api_per_tok = api_per_million / 1_000_000
return fixed_month / api_per_tok, fixed_month
be, fixed = break_even_tokens(gpus=2, gpu_hour=4.0, api_per_million=3.0)
print(f"fixed self-host: ${fixed:,.0f}/mo")
print(f"break-even: ~{be/1e6:,.1f}M tokens/mo (API cheaper below, self-host above)")
Below break-even, the API's pay-per-use wins; above it, the self-host's fixed cost amortizes. Low utilization pushes break-even higher, because idle GPU-hours make the fixed bill buy fewer real tokens.
Context: A real capacity plan puts it all together: a latency SLO caps the batch size, which fixes throughput per GPU, which fixes fleet size and budget — with the curves themselves only trustworthy when measured on the target GPU.
Your task: Build a planner that serves X tokens/day under a p95 latency SLO on the cheapest fleet, using the SLO to cap batch size, and label the real measurement 'needs GPU'.
Requirements:
- Find the largest batch whose modeled latency stays under the p95 SLO
- That batch fixes throughput/GPU and therefore daily capacity per GPU
- GPUs =
ceil(tokens_per_day / per_GPU_daily_capacity); report $/1M and $/mo - Note p95 latency and tok/s must be measured on the real GPU under load (needs GPU)
💡 Hint: The SLO is the binding constraint: it picks the batch, and everything downstream — fleet and bill — follows from it.
Show solution
The SLO caps batch (bigger batch = higher latency); that fixes throughput/GPU and thus fleet size and cost. Runnable:
import math
def throughput_at_batch(b, peak=4000.0, half=8.0):
return peak * b / (b + half)
def latency_ms(b, base=40.0, per_item=6.0):
return base + per_item * b # simple linear batch-latency model
def plan(tokens_per_day, p95_ms=800.0, gpu_hour=4.0):
# largest batch under the SLO
b = 1
while latency_ms(b + 1) <= p95_ms:
b += 1
tps = throughput_at_batch(b)
day_capacity = tps * 86400
gpus = math.ceil(tokens_per_day / day_capacity)
cost_m = (gpu_hour / 3600.0 / tps) * 1_000_000
return (f"SLO-capped batch={b}, {tps:.0f} tok/s/GPU, "
f"gpus={gpus}, ${cost_m:.2f}/1M, ${gpus*gpu_hour*24*30:,.0f}/mo")
print(plan(tokens_per_day=5_000_000_000))
# real p95 & tok/s MUST be measured on the target GPU under load -- needs GPU
The planner picks batch, fleet, and budget from the SLO; the latency and throughput curves themselves are only trustworthy when measured on the real GPU under production load (needs GPU).
✓ Checkpoint — you can move on when you can…
- Quote any serving optimization in tokens/sec/GPU and convert a GPU-hour price to $/1M tokens.
- Explain why batching lowers $/token to a knee, and why pushing past it just buys latency.
- State why utilization is the hidden cost and how purchasing mode (reserved/on-demand/spot) plays in.
- Compute the self-host vs API break-even volume and reason about how ops cost moves it.
- Size a GPU fleet to a p99 latency SLO and report the honest $/1M tokens at real utilization.