AI EngineeringZero to ProductionHome·About·Contact
ML Systems Internals · Part 5

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.

⏱️ ~2 hours🧪 5 labs🎯 Advanced→Tech-lead

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.

GPU-hour $ $2-4/hr H100 tokens/sec/GPU measured goodput × utilization idle burns $ $/1M tokens your true cost vs API price the crossover
🗺️ How to read this diagram

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.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · cost_per_million_tokens (runs, stdlib)
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
▶ How this works

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).

  1. 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.
  2. tokens_per_hour = tokens_per_sec * 3600 * utilization is the key line: one paid hour buys tokens_per_sec × 3600 tokens only at full utilization. Multiplying by utilization discounts to what the GPU actually generates.
  3. Dividing the hourly price by those tokens gives $/token; scaling by 1,000,000 gives the industry-standard $/1M-tokens quote.
  4. 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.

Python · batching throughput→cost curve (runs, stdlib)
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
▶ How this works

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.

  1. ideal = per_stream_toks * batch is the fantasy where throughput scales forever — each extra request adds a full stream's worth of tokens.
  2. saturating = ceiling * (batch / (batch + knee)) is the reality: a curve that approaches a hard ceiling and bends over near the knee batch size. min(ideal, saturating) takes whichever is smaller, so early batches are linear and later ones flatten.
  3. cost_per_mtok turns each throughput number into $/1M tokens at a $3/hr GPU — higher throughput, lower cost.
  4. The marginal column 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.

Utilization is the hidden costYour $/token is set less by the GPU-hour price than by how full you keep the GPU. A frontier API is effectively a shared GPU pool running at very high utilization — that pooled efficiency is baked into its price. When you self-host, you inherit the utilization problem: idle GPUs burn money silently, and a 30%-utilized cluster is 3× more expensive per token than the spreadsheet that assumed 100%. Measure real utilization before you trust any self-host cost.
Purchasing modeRel. priceCommitmentUse for
On-demand1.0× (baseline)nonespiky/unknown load, dev
Reserved / committed (1–3yr)~0.4–0.6×you pay even when idlesteady 24/7 baseline
Spot / preemptible~0.2–0.4×can be reclaimed anytimebatch, 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.

Python · breakeven_volume (runs, stdlib)
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
▶ How this works

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.

  1. slope_gap = api_price_per_mtok - self_host_per_mtok is 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 — return None.
  2. 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.
  3. monthly(...) returns both bills at a given volume so you can see who wins where.
  4. 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.

The break-even ignores the two things that actually decide itThis is a cost-only crossover. Two non-cost factors routinely override it: capability (the frontier API may simply be better at your task — MS is about serving open weights, which trail on the hardest work) and ops (a self-hosted stack needs someone to patch, autoscale, and get paged). Fold in a fraction of an engineer's salary as fixed cost and the break-even moves right by hundreds of millions of tokens.

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.

Python · SLO-constrained capacity planner (runs, stdlib)
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}
▶ How this works

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.

  1. needed = peak / per_gpu_tps_at_slo * headroom divides the peak load by the throughput each GPU can sustain without breaching p99 (well below the raw ceiling), times a safety headroom. math.ceil rounds up to whole GPUs.
  2. provisioned_tps = gpus * per_gpu_tps_at_slo is the capacity you're paying for. Average load is only avg_to_peak_ratio of the peak, so real utilization = avg_tps / provisioned_tps is low — you bought for the peak but mostly run below it.
  3. monthly_fixed bills every GPU 24×30 hours regardless of load.
  4. cost_per_mtok reuses 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.

Python · pull live GPU on-demand pricing (illustrative)
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.

✓ Knowledge check

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
Utilization. $0.33/1M assumes the GPU generates 2,500 tok/s every second it's paid for (100% utilization). Real traffic is diurnal and bursty, so the GPU actually averages ~33% utilization — it bills for 24h but only serves tokens for ~8h-equivalent. Because $/token = GPU-hour price ÷ (tokens/sec × 3600 × utilization), cutting utilization to ~1/3 triples the cost to ~$1/1M. The GPU-hour price and tokens/sec are unchanged; the idle hours are the cost. This is why an API's high pooled utilization is baked into a price you can't beat at low volume.
✓ Knowledge check

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
It's a bad trade. Batch 32→128 only drops cost from ~$0.52 to ~$0.33/1M — about 19¢ per million tokens — because you're past the knee where throughput saturates (KV-cache/compute-bound). In exchange, quadrupling the batch sharply raises TPOT and queueing latency, very likely breaching your p99 SLO for every user. Past the knee you pay real latency for pennies. Batch to the knee, then add GPUs (scale out) rather than batching deeper, and let capacity planning — not the raw cost curve — set the ceiling.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · The only metric: tokens/sec/GPUBeginner

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.

Exercise 2 · From GPU-hour to $/1M tokensIntermediate

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.

Exercise 3 · The batching throughput→cost curve and its kneeAdvanced

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.

Exercise 4 · Utilization — the hidden multiplierExpert

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.

Exercise 5 · Self-host vs API break-even volumeProfessional

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.

Exercise 6 · Capacity plan under a latency SLOIndustry scenario

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.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in