Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Continuous batching can refill decode slots as soon as a request finishes. A full queue can still hide a capacity failure: once the scheduler is willing to admit more work, what runs out first on the GPU serving a large language model (LLM)?
Picture a code-assistant pool during a release freeze. Every answer still arrives one token at a time, while short completions and long patch explanations share one replica.
Each decode step rereads model weights from GPU high-bandwidth memory (HBM) and consults a growing KV cache. Those bytes, rather than peak FLOPs, usually decide how many streams fit and what each token costs.
The scheduler can refill slots, but a replica still has finite budget. We will follow one mixed-length assistant: an 8B-class replica that fits on one 80 GB GPU, a 70B-class KV cache that overruns that card, and a throughput-latency-cost choice that works for both.
Why isn't "keep the batch full" enough to scale LLM serving?
Answer
A full batch still has to fit in HBM and still has to move weights plus KV state every decode step. If bandwidth or KV residency is the ceiling, extra admitted requests queue or stall instead of raising useful tokens per second.
Decode sits under a bandwidth roof
You already know the two phases from inference mechanics. Prefill reads known prompt tokens in parallel and is often compute-heavy.
Decode emits one new token per request step. This dependency is why a request can have plenty of prompt work behind it but still need many sequential steps to finish.
Users feel prefill as time to first token (TTFT). They feel decode as inter-token latency (ITL), also called time between tokens (TBT). DistServe defines time per output token (TPOT) as average time to generate each output token after first, so mean TPOT can look fine while p95 ITL is ugly.[1]
The scaling fact is the intensity split. Arithmetic intensity is FLOPs per byte loaded from HBM.
Prefill reuses each weight matrix across many prompt positions. Small-batch decode reuses those same weights for one new position per request, so each step looks more like a thin matrix-vector read.
The roofline model names the resulting ceiling: attainable work is the minimum of peak compute and memory bandwidth times intensity. Below the ridge, bandwidth is the limit. Above it, compute is the limit.[2][3]
Before doing hardware arithmetic, predict where each phase lands. One-token decode should sit on the rising bandwidth slope; a large batch or long prefill should move right, toward the flat compute roof.

Name hardware before doing arithmetic. We use NVIDIA H100 SXM as a reference point: it has 80 GB of HBM and peak HBM bandwidth of 3.35 TB/s (3,350 GB/s in decimal units).[4]
Llama 3 8B is an 8B-parameter dense model.[5] An FP16 copy of those weights is about 16 GB (8e9 * 2 bytes).
If one uncached decode step streamed that whole footprint for one active token, the weight-read upper bound on H100 SXM would be 3350 / 16 ≈ 209 steps per second. That is a bandwidth thought experiment, not a promise about a runtime.
Real kernels also read KV state, miss peak bandwidth, and pay runtime overhead, so observed tokens per second are lower. Use the bound as a diagnosis: if a trace saturates HBM while tensor cores sit idle, buying more FLOPs will not fix decode.
1parameters = 8_000_000_000 # Llama 3 8B-class dense count
2bytes_per_parameter = 2 # FP16
3h100_sxm_hbm_gb_s = 3_350 # decimal GB/s, NVIDIA H100 SXM
4
5weight_gb = parameters * bytes_per_parameter / 1_000_000_000
6ideal_steps_per_second = h100_sxm_hbm_gb_s / weight_gb
7
8print(f"FP16 weight footprint: {weight_gb:.1f} GB")
9print(f"H100 SXM weight-read upper bound: {ideal_steps_per_second:.1f} single-token steps/s")
10print("Observed TPS is lower once KV reads and runtime overhead are included.")1FP16 weight footprint: 16.0 GB
2H100 SXM weight-read upper bound: 209.4 single-token steps/s
3Observed TPS is lower once KV reads and runtime overhead are included.You profile decode and see tensor cores idle while HBM bandwidth is near saturation. Should you first buy more FLOPs or reduce memory traffic?
Answer
Reduce memory traffic first. The profile says the bottleneck is bandwidth, so extra FLOPs will sit idle too. Better first moves are a larger effective decode batch, weight quantization, GQA/MQA, KV compression, or better cache packing.
KV bytes cap the batch you can form
Batching only helps if extra requests fit. The KV cache lesson derived the payload formula: each token stores a Key and a Value at every layer and KV head.
Llama 3 uses grouped-query attention with 8 KV heads at 8B, 70B, and 405B.[5][6] That head count is why KV can be much smaller than a full set of attention heads, but it still grows with layers, context, and concurrency.
For Llama 3 70B in FP16, one token is 2 * 80 * 8 * 128 * 2 = 327,680 bytes, or 320 KiB. Do a rough check before reading on: multiplying by 8,192 should land near a few GiB.
The exact estimate is 2.5 GiB per 8,192-token request. Sixty-four concurrent 8K requests need 160 GiB of KV alone, before weights. An 80 GB H100 can't hold that batch, so a plan to keep raising batch size until the roofline moves hits residency first.

The 8B replica in the same family is the one that fits. Llama 3 8B has 32 layers, 8 KV heads, and d_head = 128, so one token is 128 KiB and a 4K request is 0.5 GiB.[5]
That is the budget we will size later. The 70B numbers show the same formula crossing a GPU before FLOPs do.
1def kv_cache_gib(
2 batch_size: int,
3 seq_len: int,
4 num_layers: int,
5 num_kv_heads: int,
6 head_dim: int,
7 dtype_bytes: int = 2,
8) -> float:
9 total_bytes = (
10 2 * batch_size * seq_len * num_layers * num_kv_heads * head_dim * dtype_bytes
11 )
12 return total_bytes / (1024 ** 3)
13
14one_8k = kv_cache_gib(1, 8_192, 80, 8, 128)
15batch_64 = kv_cache_gib(64, 8_192, 80, 8, 128)
16
17print(f"Llama 3 70B, one 8K request: {one_8k:.1f} GiB")
18print(f"64 active 8K requests: {batch_64:.0f} GiB")
19print("single-request estimate correct:", one_8k == 2.5)
20print("64-request estimate correct:", batch_64 == 160.0)1Llama 3 70B, one 8K request: 2.5 GiB
264 active 8K requests: 160 GiB
3single-request estimate correct: True
464-request estimate correct: TrueThat 160 GiB figure assumes every request occupies a live 8K payload. A max-context slab reservation is worse: it pays for tokens that never arrive.
PagedAttention allocates fixed-size blocks on demand and maps logical token order through a block table, so finished requests return pages to a shared pool.[7] Paging doesn't shrink bytes per live token. It stops the allocator from pinning unused tail slots.

In the KV-cache formula, which terms can the serving stack change without shortening user context?
Answer
Grouped-query or multi-query attention reduces num_kv_heads. KV-cache quantization reduces dtype_bytes. Batch size is a workload and admission choice. Layer count and head dimension stay fixed for a chosen checkpoint.
The throughput, latency, cost triangle
Once bandwidth and KV residency are in view, "maximize tokens per second" is not a complete objective. Start with product promise: interactive users care about TTFT and p95 ITL, while offline jobs care more about completion rate.
Larger batches amortize weight reads and usually raise aggregate tokens per second (TPS). They also make requests compete for HBM, which can stretch TTFT and p95 ITL.
Cost per token follows sustained throughput, not the sticker GPU price. A slower, latency-protected replica can therefore be more expensive per token and still be correct for its workload.

| Metric | Raised by | What you usually give up |
|---|---|---|
| Throughput (tokens/s across the replica) | Larger effective decode batches | TTFT or ITL once HBM, queues, or kernels are pressured |
| Latency (TTFT, p95 ITL, p99) | Smaller admitted batches, decode-first budgets | Unused FLOPs and a higher cost per token |
| Cost per token | Higher sustained tokens per GPU-hour | Headroom against latency SLOs |
Cost per token is hourly GPU spend divided by tokens actually produced:
Treat $3.00/hour as a worked fixture for August 2026, not a live cloud quote. Suppose a well-batched replica sustains 2,500 decode tokens/s across active requests:
- Tokens per hour = 2,500 * 3,600 = 9,000,000
- Cost per million tokens = $3.00 / 9,000,000 * 1,000,000 = $0.33
Starve the batch to 250 tokens/s on the same GPU-hour and cost per million jumps to $3.33. Utilization is a 10x multiplier. Batching is not only a latency knob; it moves the cost corner of the triangle.
1def cost_per_million(hourly_cost: float, sustained_tps: int) -> float:
2 return hourly_cost / (sustained_tps * 3600) * 1_000_000
3
4well_batched = cost_per_million(3.00, 2_500)
5starved = cost_per_million(3.00, 250)
6
7print(f"2,500 tokens/s: ${well_batched:.2f} per million tokens")
8print(f"250 tokens/s: ${starved:.2f} per million tokens")
9print(f"cost multiplier: {starved / well_batched:.0f}x")12,500 tokens/s: $0.33 per million tokens
2250 tokens/s: $3.33 per million tokens
3cost multiplier: 10xThe triangle has a blunt rule: push two corners hard and the third drifts. Cap batch size for a 500 ms TTFT chat SLO, and cost per token climbs because the GPU is underused.
Push batch size for overnight summarization, and tail latency rises because no human is waiting on each token. There is no universal best point, only one that meets the product latency SLO at acceptable cost.
| Operating point | Batch pressure | Cost per token | Latency | Typical fit |
|---|---|---|---|---|
| Latency-first | Small | High | Low | Interactive chat, inline completion |
| Balanced | Medium | Medium | Medium | General assistants |
| Throughput-first | Large | Low | High | Offline summarization, eval sweeps |
Production tip: Pick the operating point from the product SLO, then size hardware to it. Watch GPU KV usage, prefill backlog, and decode queue depth together. High KV usage plus rising TTFT usually means residency is capping concurrency. Low KV usage with idle compute means you're leaving throughput, and cheap tokens, on the table.
Two teams serve the same 8B model on the same GPU SKU but report a 5x gap in cost per million tokens. The interactive team is more expensive. Why isn't that automatically a misconfiguration?
Answer
Interactive serving caps batch size to protect TTFT and ITL, so each GPU-hour spreads over fewer tokens. The batch team pushes until throughput saturates. Both can be correct operating points for their SLOs.
Batching raises arithmetic intensity
The previous chapter treated continuous batching as slot reuse: when request A finishes, D can enter at the next decode iteration instead of waiting for C's long answer.[8] Slot reuse is only half the story. One decode step still streams the weight tensor roughly once for the whole batch. More active tokens on that step means more useful FLOPs per byte, which is how serving climbs the roofline without changing the model.
Static batching fights that climb. If three assistant requests need 2, 2, and 5 remaining tokens, a request-level batch of size 3 spends later steps on padding or idle slots. Iteration-level scheduling keeps the weight read attached to live work.
Predict the comparison before running it: same weight-read steps should produce more tokens when finished slots refill immediately. This sketch counts weight-read steps against emitted tokens. It isn't a production scheduler: no token budget, no prefill chunking, no KV admission check. It isolates why membership changes raise work per read.
1from collections import deque
2
3static_remaining = [2, 2, 5]
4static_steps = max(static_remaining)
5static_tokens = sum(static_remaining)
6
7queue = deque([2, 2, 5, 3, 1])
8active: list[int] = []
9max_batch = 3
10weight_reads = 0
11emitted = 0
12
13while queue or active:
14 while queue and len(active) < max_batch:
15 active.append(queue.popleft())
16 if not active:
17 break
18 weight_reads += 1
19 nxt: list[int] = []
20 for remaining in active:
21 remaining -= 1
22 emitted += 1
23 if remaining > 0:
24 nxt.append(remaining)
25 active = nxt
26
27print(f"static batch: {static_tokens} tokens in {static_steps} weight reads")
28print(f"continuous batch: {emitted} tokens in {weight_reads} weight reads")
29print(f"tokens per weight read (static): {static_tokens / static_steps:.2f}")
30print(f"tokens per weight read (continuous): {emitted / weight_reads:.2f}")1static batch: 9 tokens in 5 weight reads
2continuous batch: 13 tokens in 5 weight reads
3tokens per weight read (static): 1.80
4tokens per weight read (continuous): 2.60Same five weight-read steps, more tokens, because D and E filled slots that static batching would have padded. The intensity gain still stops where KV residency stops. A scheduler that admits past the cache budget just swaps bandwidth headroom for eviction and preemption.
Why does batching help decode even though every request still needs its own next token?
Answer
The model weights can be read once for a larger set of active tokens, so that memory movement is amortized across requests. Batching doesn't remove the sequential dependency inside each request. It raises useful work per weight read until KV memory or latency SLOs say stop.
Sharing, chunking, and phase isolation
Three more levers change the same budget without training a new model. Each targets a different source of pressure: repeated prompt work, prompt interference, or movement between phase-specific workers.
Prefix sharing. Many assistant requests start from the same system prompt. Prefix caching is the lookup that finds reuse across independent requests.
Copy-on-write is what happens after sharing exists: continuations keep the same physical blocks until one has to write, then runtime clones only the dirty block.[7] For Llama 3 8B, a shared 4K system prompt is 0.5 GiB the first time and nearly free for each later session that hits the cache, until someone diverges.

Chunked prefill. A 10K RAG prompt on the same worker as live streams can stall ITL. Sarathi-Serve and current vLLM V1 scheduling bound that interference by giving leftover max_num_batched_tokens to prefill after decode and splitting a prompt that doesn't fit.[9][10]
Chunking doesn't add HBM. It trades TTFT on a long prompt for smoother ITL on streams already admitted. Details live in the scheduling chapter.
Prefill-decode disaggregation. Splitwise and DistServe move the two phases onto separate pools when avoided interference pays for KV transfer.[11][1] This changes worker placement, not payload size.
For a Llama 3 70B 8K request, payload is 2.5 GiB. Over a 200 GiB/s link, the one-way floor is 12.5 ms before protocol and scheduling. That is a different break-even from the smaller Qwen handoff in the scheduling chapter: long 70B prefixes make transfer expensive, so disaggregation must save more queueing than 12.5 ms plus overhead.
1kv_cache_gib = 2.5 # Llama 3 70B, one 8K request
2interconnect_gib_s = 200 # binary GiB/s
3
4ideal_transfer_ms = kv_cache_gib / interconnect_gib_s * 1000
5mixed_ms = (kv_cache_gib * 1024**3) / (200 * 1_000_000_000) * 1000
6
7print(f"KV state to transfer: {kv_cache_gib:.1f} GiB")
8print(f"ideal one-way floor at {interconnect_gib_s} GiB/s: {ideal_transfer_ms:.1f} ms")
9print(f"mixed GiB / decimal-GB/s floor (for comparison): {mixed_ms:.1f} ms")
10print("Queueing saved must exceed transfer plus protocol and scheduling overhead.")1KV state to transfer: 2.5 GiB
2ideal one-way floor at 200 GiB/s: 12.5 ms
3mixed GiB / decimal-GB/s floor (for comparison): 13.4 ms
4Queueing saved must exceed transfer plus protocol and scheduling overhead.
Bandwidth sets how hard you can push the batch. KV residency sets how many requests that batch can contain. The operating point is where those two ceilings meet the product's latency SLO.
When can prefill-decode disaggregation make latency worse?
Answer
When KV-cache transfer, coordination, or extra scheduling cost more than the head-of-line blocking they remove. Short prompts, light traffic, or a slow interconnect often favor a colocated worker.
Speculative decoding buys tokens per weight read
Batching amortizes one target-model weight read across requests. Speculative decoding applies the same idea within one request: one target read can score several future tokens.
A small draft model proposes k tokens. The large target model scores that span in one forward pass. Accepted prefix tokens plus a correction can replace several ordinary target decode steps.[12] The dedicated speculative-decoding lesson goes deeper; capacity reading here is simple: fewer target-weight streams per emitted token, if the draft is right often enough.

The accept/reject rule from Leviathan et al. can keep the target model's distribution. On a mismatch it resamples from the residual max(p - q, 0).
If all k drafts survive, it samples one extra token from the target distribution at the last draft position. Exactness doesn't imply speedup. Low acceptance, an expensive draft, or a clumsy kernel can lose to ordinary decoding.
This stdlib step uses a two-token vocabulary so you can check the residual path without a model runtime. p is the target distribution, q is the draft distribution, and the draft proposed token 0 then token 1.
1import random
2
3def residual_sample(p: list[float], q: list[float], rng: random.Random) -> int:
4 leftover = [max(pt - qt, 0.0) for pt, qt in zip(p, q)]
5 total = sum(leftover)
6 if total <= 0:
7 return max(range(len(p)), key=lambda i: p[i])
8 cutoff = rng.random() * total
9 running = 0.0
10 for i, mass in enumerate(leftover):
11 running += mass
12 if cutoff <= running:
13 return i
14 return len(p) - 1
15
16def speculative_step(
17 draft_tokens: list[int],
18 draft_dists: list[list[float]],
19 target_dists: list[list[float]],
20 rng: random.Random,
21) -> list[int]:
22 accepted: list[int] = []
23 for i, token in enumerate(draft_tokens):
24 p = target_dists[i]
25 q = draft_dists[i]
26 if rng.random() < min(1.0, p[token] / q[token]):
27 accepted.append(token)
28 continue
29 return accepted + [residual_sample(p, q, rng)]
30 extra_dist = target_dists[len(draft_tokens)]
31 cutoff = rng.random()
32 running = 0.0
33 extra = len(extra_dist) - 1
34 for i, mass in enumerate(extra_dist):
35 running += mass
36 if cutoff <= running:
37 extra = i
38 break
39 return accepted + [extra]
40
41rng = random.Random(0)
42draft_tokens = [0, 1]
43draft_dists = [[0.8, 0.2], [0.3, 0.7]]
44target_dists = [[0.7, 0.3], [0.2, 0.8], [0.4, 0.6]]
45emitted = speculative_step(draft_tokens, draft_dists, target_dists, rng)
46
47print(f"emitted tokens: {emitted}")
48print(f"target forwards paid in this sketch: 1")
49print(f"draft proposals paid: {len(draft_tokens)}")1emitted tokens: [0, 1, 1]
2target forwards paid in this sketch: 1
3draft proposals paid: 2With this seed both drafts survive, so the step emits two accepted tokens plus one extra from the target. Follow-on methods such as EAGLE draft from the target model's hidden states instead of a separately trained small model.[13]
Serving question stays the same: did accepted tokens repay the extra draft and verification work?
Why can speculative decoding be exact yet still slower than normal decoding?
Answer
The accept/reject rule can preserve the target distribution, but runtime depends on acceptance and overhead. If the draft is wrong too often, or verification is expensive, saved target passes don't cover the extra work.
Lower precision cuts the bytes in that read
If decode rereads weights, fewer bytes per weight is a direct bandwidth lever. Start by asking whether the profile points to weight traffic or KV traffic.
Many stacks start from BF16/FP16, then move to FP8 when hardware and kernels exist, or to INT8/INT4-style weight-only quantization when the goal is a thinner decode stream.[14][15]
KV-cache quantization is a different knob: it targets residency and long-context batch size once the cache, not the weights, is the cap.[16][17]

On the 8B replica, an FP16 full-weight read is 16 GB. INT4 is 4 GB of weight payload before scales and overhead.
That can move the single-stream ceiling, but only if the kernel doesn't spend savings on dequantization and quality still clears your eval. The next lesson, model quantization, is where those codecs live.
1parameters = 8_000_000_000
2bytes_per_weight = {"FP16": 2.0, "INT8": 1.0, "INT4": 0.5}
3
4for precision, width in bytes_per_weight.items():
5 traffic_gb = parameters * width / 1_000_000_000
6 print(f"{precision}: {traffic_gb:.1f} GB of weights per full read")
7
8print("INT4 weight traffic is 0.25x FP16 before kernel overhead.")1FP16: 16.0 GB of weights per full read
2INT8: 8.0 GB of weights per full read
3INT4: 4.0 GB of weights per full read
4INT4 weight traffic is 0.25x FP16 before kernel overhead.Common mistake: 4-bit weights aren't mainly a disk-size trick. The serving win is fewer bytes reread during decode. Whether that becomes a latency win still depends on kernels, dequant overhead, and quality.
Why does weight-only INT4 often help decode latency more directly than prefill latency?
Answer
Decode repeatedly streams model weights for small per-token work, so fewer bytes per weight cut bandwidth pressure. Prefill uses larger matrix operations and can be compute-heavy, so weight compression isn't always the dominant lever there.
When one request no longer fits
The 8B replica was a fit-and-batch problem. Some traffic is not. Before choosing hardware, test long-context demand: a 1,000,000-token prompt on Llama 3 70B needs 2 * 1_000_000 * 80 * 8 * 128 * 2 bytes of KV, about 305 GiB.
That isn't a "buy one more H100" situation. One request has crossed the single-device residency boundary.
Context parallelism (CP) splits the sequence dimension across devices, not the layer stack or the batch. Each GPU holds a shard of the prompt and its KV.
Attention still needs cross-shard Keys and Values, so implementations use ring-style exchange: while a device computes blockwise attention on local queries, K/V blocks rotate around the ring.[18] Context length can scale roughly with device count until communication becomes the next roof.
That isn't tensor parallelism, which shards weights inside a layer, or pipeline parallelism, which shards layers. Those appear when weights don't fit.
Llama 3 70B in FP16 is about 140 GB of parameters, so it can't reside on one 80 GB GPU even at batch 1.[5] The next chapter, model parallelism, makes that split its main subject.
1def kv_gib(sequence_tokens: int) -> float:
2 total_bytes = 2 * sequence_tokens * 80 * 8 * 128 * 2
3 return total_bytes / 1024**3
4
5total_kv = kv_gib(1_000_000)
6devices = 4
7print(f"one 1M-token Llama 3 70B request KV: {total_kv:.1f} GiB")
8print(f"evenly sharded over {devices} devices: {total_kv / devices:.1f} GiB/device")
9print("Communication and runtime buffers still add overhead.")1one 1M-token Llama 3 70B request KV: 305.2 GiB
2evenly sharded over 4 devices: 76.3 GiB/device
3Communication and runtime buffers still add overhead.What bottleneck does context parallelism target, and what bottleneck can it introduce?
Answer
It targets per-request context and KV that no longer fit on one GPU. It can introduce communication overhead because devices must exchange partial attention state across sequence shards.
When the budget lies
These traces show up when formulas look fine on paper but users or profiles disagree. Start with the symptom, name the ceiling it points to, then test one targeted fix.
"I bought a higher-FLOP GPU and decode barely moved"
Peak compute went up, but tokens per second didn't. Decode was already bandwidth-bound, so extra tensor cores wait on the same HBM reads. Profile HBM first. If it's saturated, raise effective batch, quantize weights, shrink KV, or choose a higher-bandwidth SKU.
"Throughput is up and chat feels worse"
Aggregate TPS rose after you lifted max batch, but users report sluggish streams. You optimized the cost corner and spent the latency corner; mean TPOT can still look acceptable. Gate on p95 ITL/TBT and TTFT, not replica tokens/s alone. Split interactive traffic from offline batch.
"Speculative decoding added latency"
A draft model is in the path, and p50 decode got worse. Acceptance may be too low, or draft-plus-verify overhead may exceed the saved target passes. Measure accepted tokens per draft span before rollout. If most proposals die early, keep ordinary decode.
"HBM climbs even though every session shares a system prompt"
KV usage looks close to batch * full_prompt despite a shared prefix. Copy-on-write preserves blocks only after a prefix-cache hit. Without a lookup and a tenant isolation policy, each request builds its own prompt KV. Enable prefix caching with an explicit sharing policy, then confirm block reference counts in the allocator.
A dashboard shows high TTFT, a rising prefill backlog, stable TPOT, and moderate KV memory. Which subsystem should you inspect first?
Answer
Inspect prefill scheduling and admission first. Stable TPOT suggests decode isn't currently degrading, while high TTFT plus a prefill backlog points to prompt work or queueing before the first token.
Size the 8B replica
Put pieces on one GPU. We want to serve Llama 3 8B FP16 on a single 80 GiB H100: about 16 GiB for weights, 8 GiB reserved for activations, allocator slack, and runtime, and average assistant context of 4,096 tokens. Before calculating, predict whether arithmetic-only KV ceiling is dozens or hundreds of requests.
One 4K request works out as:
1KV bytes = 2 * 1 * 4096 * 32 * 8 * 128 * 2
2 = 536,870,912 bytes
3 = 0.5 GiBKV budget = 80 - 16 - 8 = 56 GiB
Maximum batch = 56 / 0.5 = 112 concurrent 4K requests
That's a payload ceiling, not a production setting. Long-context bursts, fragmentation, CUDA graphs, and latency SLOs all eat headroom. A serving engineer caps well below 112 and watches measured HBM plus p95 ITL.
1import math
2
3kv_bytes = 2 * 1 * 4_096 * 32 * 8 * 128 * 2
4kv_per_request_gib = kv_bytes / 1024**3
5weight_gib = 16
6runtime_gib = 8
7kv_budget_gib = 80 - weight_gib - runtime_gib
8arithmetic_ceiling = math.floor(kv_budget_gib / kv_per_request_gib)
9
10print(f"KV per 4K request: {kv_per_request_gib:.2f} GiB")
11print(f"KV budget after weights and runtime: {kv_budget_gib} GiB")
12print(f"arithmetic-only batch ceiling: {arithmetic_ceiling}")1KV per 4K request: 0.50 GiB
2KV budget after weights and runtime: 56 GiB
3arithmetic-only batch ceiling: 112Why isn't that 112-request ceiling a safe production max concurrency?
Answer
It ignores fragmentation, temporary tensors, prefix-cache behavior, traffic with longer contexts, and latency SLOs. Production capacity should come from measured headroom, not from a closed-form KV quotient alone.