Prep · System Design

GPU Inference Platform

Three workloads on one fleet, where 5% of the requests spend 93% of the money.

The Prompt

Design a platform that serves three inference workloads — video generation, image generation, and speech synthesis — on a shared GPU fleet.

Ask First

Nothing gets drawn until these are answered. Each one moves a box on the board or removes it.

  1. 01Which of the three answers inside a user's request, and which one is allowed to take minutes? That single answer decides how many queues I draw.
  2. 02What is one request, exactly — a five-second clip, a 1024px image, a minute of audio? Service time is meaningless until the unit is pinned.
  3. 03Do the models fit on one accelerator, or does any of them need to be sharded across cards? Sharding changes which instance family I can buy.
  4. 04Is there a latency SLO with money attached to it, or only a throughput target? A p99 in a contract is a different system than a p99 on a dashboard.
  5. 05Who owns the budget, and does anyone see cost per request today? If nobody does, that is the first thing I am building.
  6. 06Are we in one region or several, and can we get P5 capacity there on demand? The newest accelerator is a scheduling problem before it is a cost problem.
  7. 07Do we control the models, or do tenants bring their own? Arbitrary weights make cold start and memory footprint unbounded.
  8. 08How spiky is it — what is the peak-to-average ratio we have actually measured, and how long does the peak last?

Arrow keys pan the board, plus and minus zoom it, zero frames it on this step again.

  • client
  • edge
  • service
  • queue
  • store
  • worker
  • third party
  • async
  1. 01

    Pin the contract: three classes, three different promises

    Before a box goes up I want three workload classes written down, each with its own SLO, and one place that decides which one you are.

    Speech answers inside the caller's request — first audio in a few hundred milliseconds or the feature is broken. Image is a short wait measured in seconds. Video is minutes and does not answer synchronously at all. Those are three different systems wearing one API, and the classification has to happen at the door, because every downstream decision — queue or no queue, which accelerator, what happens under load — is downstream of the class. The API authenticates, classifies, and admits or rejects. It does not generate anything and it never blocks on a GPU.

  2. 02

    Route by workload class, not by model name

    The router dispatches on the class, so a new model inherits a lane instead of needing new plumbing.

    If routing keys on the model name, every model launch is a deploy of the router, and the day someone ships a fourth model it lands in whichever lane the config happened to default to. Keying on the class means a new video model is a registry entry and a capacity number, not a code change. The class also carries the admission policy: a per-tenant concurrency ceiling on video, which is cheap to enforce here and impossible to enforce once the work is on a card.

  3. 03

    Queue the async work, keep the synchronous lane short

    Video gets a durable queue and a job id. Image gets a short one. Speech gets neither.

    A queue in front of the synchronous lane is latency you cannot get back, so speech does not get one — it gets headroom instead, which is the same insurance bought in capacity rather than in waiting. Video gets a durable queue because a five-second clip is 45 GPU-seconds of work and losing it after 40 of them is unacceptable; the caller gets a job id immediately and fetches the clip from object storage later. Image sits between: a short queue whose depth is a latency budget, not a buffer. Separate queues per lane, always — one shared queue means the head of the line is whatever is slowest, and the slowest thing here is 45 seconds long.

  4. 04

    One node pool per accelerator, and pods that ask for a product

    Three node pools — L4, L40S, H100 — labelled and tainted, with pods requesting a GPU by product, not just by count.

    Every node carries a label naming its accelerator and a taint that keeps non-GPU work off it, and every serving pod carries the matching toleration plus a node selector on the product. A device plugin advertises the cards as a schedulable resource so a pod asks for one GPU and gets one GPU. Requesting a count alone is the classic mistake: a speech replica that lands on an H100 because the scheduler only counted cards is burning $6.88 an hour to do $0.80 of work, and nothing in the system will tell you. Naming the product in the request is what makes the fleet legible.

  5. 05

    Hold a warm pool, and pay for idle on purpose

    Cold start is minutes, so I keep N warm replicas per lane and I am going to tell you exactly how I picked N.

    Instance boot, then a multi-gigabyte image pull, then weights off disk into HBM: minutes, not seconds. Nothing about autoscaling shortens that, which is why warm capacity is the design and autoscaling is the follow-up. N is a floor, not a forecast — the share of the steady lane I refuse to scale below. Speech is three cards at steady state and all three stay warm, because the whole lane is $2.41 an hour and scaling it to zero saves nothing worth a cold start. Video is 54 cards at steady state and four stay warm: $27.52 an hour, roughly $20K a month, held under the quietest part of the day. Then be honest about what four buys. One warm H100 finishes a clip every 22.5 seconds, so across a two-minute cold start four cards absorb about 21 of the 200 video requests that arrive at 1.67 rps. The floor makes the first minutes of a cold ramp degrade rather than fail; it does not make them invisible, and anyone who says a warm pool eliminates cold start has not multiplied it out. Pre-pulling the image onto the node and caching weights on local NVMe cuts the cold start itself, which is worth more than buying idle cards and is the thing to do first.

  6. 06

    Two loops, at two speeds, on queue depth rather than CPU

    Pods scale on queue depth and in-flight requests; nodes scale behind them, and the node loop is the one that bounds recovery.

    GPU utilization is a terrible scaling signal — a card can sit at 100% busy on a batch of one and at 100% busy on a batch of eight, and only one of those is a problem. Queue depth and in-flight count are the honest signals, per lane. The fast loop adds pods against warm nodes and settles in seconds. The slow loop notices unschedulable pods and provisions nodes, and that one is minutes because it is bounded by the cold start above. Recovery time is the slow loop, not the fast one, and quoting the fast one as your recovery time is how a burst gets away from you. Scale down deliberately slower than up, with a floor per lane that is the warm pool.

  7. 07

    Raise utilization before buying a single card

    Batching and partitioning are free capacity, and both come before the purchase order.

    Batching is the cheapest throughput on the board: speech at batch 8 sustains 10 rps on one L4, and dropping to batch 1 costs eight times the fleet for the same traffic. The batch window is the knob — a few tens of milliseconds of waiting buys a full batch and is invisible next to generation time. In the other direction, partitioning is how one card serves more than one thing at once. The video lane is the only place that pays: an H100 cut into MIG slices runs a reduced-resolution variant alongside the full-size model, each with its own memory and its own SMs, instead of standing a second $6.88 card up for the cheaper variant. Two constraints go with it. MIG geometry is static and reconfiguring it drains the node, so partitioning is a capacity plan and not an autoscaling lever. And only the P family has MIG at all — the L40S and L4 lanes are Ada, so what they get is time-slicing, which shares a card without isolating it and is a worse trade under a latency SLO. What partitioning is never for is rescuing a small model that landed on a large card: a speech replica on an H100 is a scheduling bug, and the fix is labels and taints, not a slicer. The rule is the same both ways — fill the card you already own before you rent another one, because the next card costs $6.88 an hour forever and the batch window costs 30 milliseconds once.

  8. 08

    Buy the same capacity three ways, at three risk levels

    Reservations for the floor, on-demand for the working range, spot for the batch lane only.

    The floor — the warm pool and the steady load under it — goes on reservations or capacity blocks, because it is running anyway and committed pricing is the only real discount available. The working range above that is on-demand at list. Spot, at roughly 60-70% off, goes only where preemption is survivable, which here means video: it is already async, already checkpointed, already behind a durable queue. Spot for speech would be trading a two-minute recovery against a sub-second SLO, which is not a trade. And the honest failure: P5 capacity is frequently just unavailable in region. The fallback is written down before it is needed — degrade video to A100 at longer service time and recompute the fleet, or move the lane to another region and eat the transfer, but do not let the answer be an unattended autoscaler retrying a launch that will never succeed.

  9. 09

    Make cost per request a first-class signal

    Every request carries its lane, and the meter multiplies GPU-seconds by the per-GPU-hour rate for the card it ran on.

    The numbers only argue if somebody sees them: $61.92 per thousand video requests against $0.0402 per thousand speech requests, a 1,539x spread, on lanes whose traffic share is 5% and 50%. Attribute at the request, not at the account — an invoice split by instance family tells you what you bought, never who spent it. The ledger puts cost share and traffic share on the same row on purpose, because the gap between those two columns is the whole design. This is also the guardrail on model updates: cost per request is the metric that catches a new checkpoint doubling GPU-seconds, and no latency dashboard will.

  10. 10

    Watch three curves, and write down who degrades first

    GPU utilization, queue depth, and p99 — and when they disagree, the policy already says which lane I protect.

    The three curves are deliberately different questions: utilization says whether the cards are busy, depth says whether the fleet is losing, p99 says whether anyone noticed. Utilization high with depth flat is a healthy fleet. Depth climbing with utilization flat is a scheduling failure, not a capacity one. At 3x traffic the fleet does not exist and cannot be conjured in cold-start time, so the policy is written in advance: protect speech, because it is 50% of requests, 0.6% of cost, and the only lane where late means broken. Image degrades to a longer queue and a smaller resolution. Video sheds first — it stops accepting new jobs, tells the caller a real number instead of accepting work it will not finish, and drains what it already owns. Shedding the lane that is 93% of the bill is also the only lever that moves the bill.

Step 1 of 10. Pin the contract: three classes, three different promises. Before a box goes up I want three workload classes written down, each with its own SLO, and one place that decides which one you are.

Requirements

Functional

  • 01Accept a generation request, classify it into a workload class, and admit or reject it at the door.
  • 02Serve speech synchronously inside the caller's request, streamed, in the same connection.
  • 03Serve image generation from a short queue with a bounded wait, returning the frames when they exist.
  • 04Serve video asynchronously: hand back a job id immediately, produce the clip, and let the caller fetch it later.
  • 05Run each workload on the accelerator it belongs on, and let a new model inherit an existing lane rather than needing new plumbing.
  • 06Hold warm capacity per lane so that a request arriving at an idle moment does not pay a cold start.
  • 07Report cost per request per lane, attributed back to the workload that spent it.
  • 08Degrade on a written policy when traffic exceeds what the fleet can serve — the same way every time, not by whichever lane happens to time out first.

Non-functional

  • 01Speech answers in the call: first audio out in a few hundred milliseconds, or the feature is broken.
  • 02Video has a completion target, not a latency target. Minutes are acceptable; silence is not.
  • 03Isolation between lanes: a video backlog must not lengthen a speech response, at any depth.
  • 0470% target GPU utilization. Higher than that and the queue explodes on the first burst; lower and the bill is a choice nobody made.
  • 05Absorb a 3x burst by degrading the expensive lane first, on a policy written before the burst.
  • 06Every GPU-second attributable to a workload class, because an unattributed fleet is an unmanageable one.
  • 07Survive spot preemption in the batch lane without losing accepted work.
  • 08Survive a region running out of the newest accelerator, on a documented fallback rather than a page at 3am.
The Arithmetic

Do it out loud, round hard, and say the assumption you rounded from. The number matters less than the fact that it was derived.

Pricing basis
on-demand list · us-east-1 · August 2026
Say the region and the date out loud before quoting a single number, because both move. These are list prices before spot, Savings Plans, or Capacity Blocks. Spot runs roughly 60-70% under list and preempts. List prices themselves fall: P4 and P5 were cut by up to a third in June 2025.
Offered load
33.3 rps
2,000 concurrent users x 1 request per user per minute = 2,000/min = 33.3 requests a second. This is the only number I am guessing; everything below is derived from it, so if it is 10x the shape holds and the fleet gets wider.
Mix
5% video · 45% image · 50% speech
1.67 rps of video, 15 rps of image, 16.67 rps of speech. Hold on to those three numbers — the entire argument is that they do not predict the bill.
The method
rps ÷ (batch ÷ GPU-seconds) ÷ 0.70
Little's Law with a utilization target. One GPU sustains batch ÷ GPU-seconds requests per second. Divide offered rps by that and you have GPUs at 100%, which nothing runs at. Divide again by 0.70 and you have GPUs you actually buy. Three lines, and the method is what gets graded, not the constants.
Video lane
54 x p5.4xlarge · $371.52/hr
45 GPU-seconds per 5-second clip at batch 2 → one H100 sustains 2/45 = 0.044 rps. 1.67 ÷ 0.044 = 37.5 GPUs at 100%; ÷ 0.70 = 54 GPUs. The model fits in 80GB, so buy the single-GPU p5.4xlarge at $6.88/hr, not the 8-GPU box. 54 x $6.88 = $371.52/hr = $271,210/month.
Image lane
14 x g6e.xlarge · $26.05/hr
2.5 GPU-seconds per 1024px image at batch 4 → one L40S sustains 4/2.5 = 1.6 rps. 15 ÷ 1.6 = 9.4 GPUs at 100%; ÷ 0.70 = 14 GPUs. 48GB of L40S holds an SDXL-class model with room to batch, at $1.861/hr. 14 x $1.861 = $26.05/hr = $19,019/month.
Speech lane
3 x g6.xlarge · $2.41/hr
0.8 GPU-seconds per minute of audio at batch 8 → one L4 sustains 8/0.8 = 10 rps. 16.67 ÷ 10 = 1.67 GPUs at 100%; ÷ 0.70 = 3 GPUs. An L4 at $0.8048/hr is the cheapest card that streams it. 3 x $0.8048 = $2.41/hr = $1,763/month.
Fleet at target
71 GPUs · ~$400/hr · ~$292K/month
54 + 14 + 3 = 71 GPUs. $371.52 + $26.05 + $2.41 = $399.99/hr, x 730 hours = $291,992/month. That is average load at 70% utilization, on list price, in one region.
Sized for the 3x burst
210 instances · ~$869K/month
Multiply GPUs-at-100% by 3 before dividing by the utilization target: video 161, image 41, speech 8. Standing that up permanently costs $1,190/hr = $869,006/month — three times the bill to serve a peak you are not in most of the time. This is the number that makes autoscaling an architecture decision rather than an optimization.
The inversion
5% of requests · 92.9% of the bill
Video is 5% of traffic and $371.52 of a $399.99 hourly bill. Speech is 50% of traffic and 0.6% of the bill. Image is 45% of traffic and 6.5%. Every architectural choice below follows from this one line: the cheap lane is the loud one, and the expensive lane is nearly invisible in the traffic graph.
Cost per 1,000 requests
$61.92 video · $0.48 image · $0.0402 speech
Hourly lane cost ÷ requests per hour, x 1,000. Video $371.52 ÷ 6,000 req/hr; image $26.05 ÷ 54,000; speech $2.41 ÷ 60,000. Video against speech is a 1,539x spread per request. If you remember one number from this design, remember that one.
Bundle premium
P5 1.00x · G6e 2.02x · G5 2.02x · G6 2.07x
An H100 is $6.88/GPU-hr whether you buy one (p5.4xlarge, $6.88) or eight (p5.48xlarge, $55.04) — P5 prices linearly. The G family does not: g6e.48xlarge is $30.13 for 8 cards, $3.77/GPU-hr, against $1.861 for a g6e.xlarge. You are buying 192 vCPU, 1.5TB of host RAM, and the interconnect. If the model fits on one GPU, the 8-GPU box is a 2x penalty, not a volume discount.
Per-GPU-hour, the only comparable unit
H100 $6.88 · A100-40 $2.75 · L40S $1.86 · L4 $0.80
p4d.24xlarge is $21.96 for 8 A100 40GB = $2.75/GPU-hr. Quoting instance prices across families compares nothing, because the instances hold different numbers of cards. Divide by GPU count before you open your mouth.
Cold start
minutes, not seconds
Instance boot, then a multi-gigabyte container image pull, then loading weights off disk into HBM. Minutes end to end. That single number is the entire reason a warm pool exists, and the reason autoscaling alone does not save you: by the time a new node is serving, the burst that asked for it is over.
Trade-offs
01

Warm idle GPUs vs cold-start latency

ChoseA funded warm pool per lane, sized as a floor under the steady lane
OverScaling from zero and eating the first request's cold start

Cold start is minutes — boot, image pull, weights into HBM — and no amount of autoscaling shortens it. Scale-to-zero means the first user of every quiet period pays the whole thing.

CostIdle cards billed at full rate, forever, and a floor that is only a floor. Four warm H100s is $27.52 an hour, roughly $20K a month, to serve nobody — and across a two-minute cold start they still only absorb about 21 of the 200 video requests that arrive. It is a real line item, it buys a slower failure rather than none, and both halves of that have to be said out loud.
02

Single-GPU instances vs the 8-GPU bundle

Chosep5.4xlarge and g6e.xlarge — one card per instance
Overp5.48xlarge and g6e.48xlarge and packing replicas onto them

Every model here fits on one card. P5 prices linearly at $6.88/GPU-hr either way, so the bundle buys nothing, and in the G family the bundle is a 2.02x per-GPU penalty because you are also buying 192 vCPU and the interconnect you will not use.

CostMore instances to manage, more nodes in the cluster, and worse bin-packing headroom. The day a model needs two cards with fast interconnect between them, this decision has to be revisited rather than tuned.
03

Spot vs on-demand for the batch lane

ChoseSpot for video only, on-demand and reservations everywhere else
OverAll on-demand, or spot across the whole fleet

Video is already async, checkpointed, and behind a durable queue, so a preemption costs partial work and a retry. 60-70% off the most expensive lane is the largest single lever on the bill.

CostPreemption mid-generation wastes GPU-seconds already paid for, tail latency on video gets long and lumpy, and the checkpointing machinery is real code with its own bugs. Spot capacity also disappears exactly when everyone else wants it, which is the burst.
04

Batching for throughput vs latency

ChoseA bounded batch window per lane, wide on speech, near zero on nothing
OverServing every request the moment it arrives

Batch 8 against batch 1 on speech is 8x the raw GPU requirement — 1.7 GPUs at 100% against 13.3 — which after the utilization target and rounding is 3 cards instead of 20. A 30-millisecond window is invisible against generation time and multiplies the fleet's capacity.

CostEvery request pays the window, the tail pays a partial batch, and an oversized batch is an out-of-memory kill that takes the whole batch with it. The window has to be a ceiling on waiting, not a target for filling.
05

One shared cluster vs a cluster per workload

ChoseOne cluster, node pools and taints per accelerator
OverThree clusters with three control planes

One place to schedule, one autoscaler, one cost attribution path, one upgrade. Isolation between lanes is what pools, taints, and quotas are for, and they are cheaper than duplicated control planes.

CostA control-plane failure is now a three-lane failure, noisy-neighbour effects on host resources are real, and quota discipline has to be enforced by policy rather than by a hard boundary.
06

Self-managed Kubernetes vs a managed inference service

ChoseOur own cluster and node pools
OverA managed endpoint service that hides the fleet

Three accelerator types, MIG partitioning, spot handling, and per-request cost attribution are exactly the things managed endpoints abstract away, and at $292K a month the abstraction costs more than the team.

CostWe own the device plugins, the driver and CUDA version matrix, node lifecycle, and every 3am page about a leaked GPU process. Below roughly a rack of cards this is the wrong trade and I would say so.
Failure Modes
When

Spot preemption mid-generation

Symptom

A video job at 40 of its 45 GPU-seconds vanishes with a two-minute warning, and the caller sees a job that was in progress go back to pending.

Mitigation

The two-minute notice is a signal, not a courtesy: drain the node, stop pulling new work, checkpoint the diffusion state to object storage, and return the message to the queue rather than acking it. The job resumes on another card from the last checkpoint. Cap the spot share of the video lane so a correlated reclaim cannot take the whole lane at once.

When

A stuck process holds the card

Symptom

GPU memory reads full, utilization reads zero, the pod looks healthy, and every request routed there times out. The lane's queue climbs while the fleet reports capacity.

Mitigation

A liveness probe that runs an actual tiny inference, not a TCP check — the whole class of failure is a process that is alive and useless. Memory-in-use with zero utilization for a sustained window kills the pod and, if it recurs on the same node, cordons it. Node-level GPU health checks catch the ones a pod restart cannot clear, because the leak is sometimes below us.

When

The queue outruns provisioning

Symptom

Depth climbs monotonically, the autoscaler is already asking for nodes, and the nodes are minutes away. Estimated wait passes anything anyone would accept.

Mitigation

Admission control at the door, not at the card: once projected wait exceeds the lane's promise, reject new video with a real retry-after rather than accepting work that will not finish. The warm pool absorbs the first minutes and the burst policy sheds the expensive lane. The number worth paging on is projected wait, which is depth divided by drain rate, not depth itself.

When

Region capacity exhaustion

Symptom

Launches for the newest accelerator fail with insufficient capacity, the provisioner retries in a loop, and the fleet is stuck at its current size during the exact burst that needed it.

Mitigation

Capacity blocks or reservations for the floor so the base fleet is never subject to this. Above the floor, a written fallback order: a second instance family for the lane at a longer service time and a recomputed GPU count, then a second availability zone, then a second region with the transfer cost accepted. The provisioner gives up after a bounded number of attempts and raises a capacity alert instead of retrying forever.

When

An oversized batch runs out of memory

Symptom

The server dies mid-batch and takes every request in that batch with it, usually on the largest resolution somebody just enabled.

Mitigation

Cap batch size by memory footprint rather than by count, since a 1024px image and a 2048px image are not the same request. Reject anything above the per-request footprint ceiling at admission. On an out-of-memory kill, requeue the batch as individual requests so the retry cannot repeat the failure, and treat OOM rate as an alert rather than a restart counter.

When

A model update doubles GPU-seconds per request

Symptom

Latency drifts up a little, quality metrics look fine, nobody rolls it back, and the bill is 3x at the end of the month.

Mitigation

GPU-seconds per request per model version is a release gate, not a monthly report. Canary the new checkpoint on a slice of the lane, compare cost per thousand requests against the incumbent, and block the rollout on a regression the same way a latency regression would block it. The cost ledger is what makes this a five-minute check instead of a finance conversation four weeks later.

When

Speech lands on the wrong accelerator

Symptom

Nothing breaks. Latency is excellent. The bill for the speech lane is up eight times and no graph says why.

Mitigation

Pods request a GPU by product, and the scheduler enforces it with labels, taints, and tolerations rather than trusting a count. Cost per request per lane catches the ones that slip through, because a lane whose per-request cost jumped an order of magnitude with flat traffic has been rescheduled onto something expensive.

Listening For

What is actually being scored while you talk. None of it is the drawing.

  • 01Whether you quote dollars per GPU-hour rather than per instance. An instance price compares nothing across families, and dividing by GPU count is the tell that you have actually bought this hardware.
  • 02Whether you know cold start is minutes, and that a warm pool exists because of it rather than because someone was cautious.
  • 03Whether you name the accelerators — H100, L40S, L4 — and match each to a workload with a reason, instead of saying GPU as though they were interchangeable.
  • 04Whether you noticed the inversion: 5% of requests and 93% of the bill, and every decision downstream flowing from that one ratio.
  • 05Whether you scale on queue depth and in-flight requests rather than GPU utilization, and can say why utilization is a bad signal for a batched server.
  • 06Whether you separate the pod loop from the node loop and say which one bounds recovery time.
  • 07Whether the 8-GPU box is understood as a 2x penalty in the G family and a wash in P5 — and whether you know that because you divided, not because you memorized it.
  • 08Whether you volunteer the cost of every choice. Warm pools cost idle dollars, spot costs partial work, batching costs latency; an answer with no cost in it is one nobody believes.