GPU Inference Platform
Three workloads on one fleet, where 5% of the requests spend 93% of the money.
“Design a platform that serves three inference workloads — video generation, image generation, and speech synthesis — on a shared GPU fleet.”
Nothing gets drawn until these are answered. Each one moves a box on the board or removes it.
- 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.
- 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.
- 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.
- 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.
- 05Who owns the budget, and does anyone see cost per request today? If nobody does, that is the first thing I am building.
- 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.
- 07Do we control the models, or do tenants bring their own? Arbitrary weights make cold start and memory footprint unbounded.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
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.
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.
Warm idle GPUs vs cold-start latency
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.
Single-GPU instances vs the 8-GPU bundle
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.
Spot vs on-demand for the batch lane
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.
Batching for throughput vs latency
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.
One shared cluster vs a cluster per workload
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.
Self-managed Kubernetes vs a managed inference service
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.
Spot preemption mid-generation
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.
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.
A stuck process holds the card
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.
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.
The queue outruns provisioning
Depth climbs monotonically, the autoscaler is already asking for nodes, and the nodes are minutes away. Estimated wait passes anything anyone would accept.
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.
Region capacity exhaustion
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.
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.
An oversized batch runs out of memory
The server dies mid-batch and takes every request in that batch with it, usually on the largest resolution somebody just enabled.
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.
A model update doubles GPU-seconds per request
Latency drifts up a little, quality metrics look fine, nobody rolls it back, and the bill is 3x at the end of the month.
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.
Speech lands on the wrong accelerator
Nothing breaks. Latency is excellent. The bill for the speech lane is up eight times and no graph says why.
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.
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.