Skip to main content

Capacity Planning

Agentium includes a built-in capacity planning library for modeling LLM inference infrastructure. It answers questions like:
  • How many GPUs do I need for N concurrent users?
  • What happens to latency when I add NAND SSD offloading?
  • What’s the KV cache pressure for my workload mix?
  • Where is the TTFT SLA breach point?
The system has three tiers:

Quick Start


Glossary

Every term used in the capacity planning system, explained in detail.

KV Cache (Key-Value Cache)

During autoregressive generation, each transformer layer computes Key and Value projections for every token. Without caching, generating token N would require recomputing K and V for all N-1 prior tokens — quadratic cost per step. The KV cache stores these projections so each decode step only reads them — reducing cost to linear per step. The KV cache is the single largest consumer of GPU memory during inference. For Llama 3.1 70B at 128K context, the KV cache alone is ~40 GB in bf16 — larger than many GPUs.

KV Bytes Per Token

The memory required to store one token’s KV cache entry across all layers:
The accounts for both the Key tensor and the Value tensor. Each layer has its own independent set of K and V vectors, and each KV head stores a vector of head_dim floating-point values. Example — Llama 3.1 70B in bf16: 2 × 80 layers × 8 kv_heads × 128 head_dim × 2 bytes = 327,680 bytes (~320 KB per token) This means a single 128K-context session consumes 128,000 × 320 KB = 40 GB of KV cache.

Attention Types

How a model organizes its attention heads directly determines KV cache size: Why it matters: Llama 3.1 70B uses GQA with 64 query heads but only 8 KV heads — an 8× reduction in KV cache compared to MHA. Falcon 7B uses MQA with just 1 KV head — KV cache is only 8 KB/token vs 320 KB for Llama 70B.

Layers

The number of transformer blocks stacked sequentially in the model. Each layer has its own independent attention weights and stores its own KV cache. More layers = deeper model = more KV memory per token.

Head Dimension

The size of each attention vector (both Q/K/V). Determined by hidden_dim / num_attention_heads. Larger head dimensions store more information per attention head but increase KV cache proportionally. Most modern models use 128 (Llama, Mistral, Mixtral). Falcon uses 64. Gemma 2 9B uses 256.

Hidden Dimension

The width of the model’s internal representation — the size of the vector that represents each token as it flows through the network. Determines the model’s capacity to represent complex patterns. Related to head_dim via hidden_dim = attention_heads × head_dim.

FFN Dimension

The intermediate size of the feed-forward network inside each transformer layer. Typically 3-4× the hidden dimension. Affects prefill compute cost because FFN operations scale linearly with N per layer.

HBM (High Bandwidth Memory)

The GPU’s on-chip memory (often called VRAM). This is where model weights, KV cache, and activations must reside for active inference. HBM is fast (~2-3.35 TB/s on modern GPUs) but limited in capacity (24-80 GB per GPU). The entire capacity planning problem reduces to: what fits in HBM?

HBM Slots

The number of concurrent sessions that can have their full KV cache resident in GPU HBM. These sessions can generate tokens at full speed with no restore penalty. When HBM is full, new sessions must either wait or be served from NAND (with restore latency).

Weight Memory

The GPU memory consumed by the model’s parameters (weights). This is a fixed cost that must be paid regardless of how many users are served.

NAND SSD Offloading

Using NVMe solid-state drives attached to each GPU server to store KV cache for inactive (parked) sessions. When a parked session becomes active, its KV cache is loaded from NAND back into HBM. NAND expands the total number of sessions the system can manage but does not help active inference speed — decoding still requires KV data in HBM.

NAND Slots

The number of sessions that can be parked on NAND SSD while inactive. Computed as total_nand_gb / kv_per_session_gb. These sessions can be restored to HBM when they become active, at the cost of restore latency.

Restore Latency

The time required to load a parked session’s KV cache from NAND SSD back into GPU HBM. This is the “wake-up cost” for a cold session.
Example: 5 GB KV cache on Gen4 NVMe (7 GB/s) = 714ms restore time. When multiple sessions restore simultaneously, they share the SSD bandwidth pipe, increasing individual restore time: effective_bw = nand_bw / parallel_streams.

Cold Ratio

The percentage of total sessions that are parked on NAND at any given moment (inactive, not generating tokens). Typical values:
  • 20-30% — most sessions are active (interactive chat)
  • 50% — half parked (async agent workloads with tool waits)
  • 70-80% — most parked (background research agents)
concurrent_active = total_sessions × (1 - cold_ratio)

TPOT (Time Per Output Token)

The latency for each decode step — generating one output token. Decoding is memory-bandwidth-bound because each step must stream the entire KV cache for all active sequences through HBM.
TPOT scales linearly with context length and batch size. A user perceives this as the streaming speed — lower TPOT = faster text output. Interactive applications target < 50ms TPOT (~20 tokens/sec streaming).

TTFT (Time To First Token)

The latency from when a user submits their prompt to when the first output token arrives. TTFT is dominated by prefill — processing the entire input prompt through every layer to build the KV cache. Prefill is compute-bound (not memory-bound like decode) because attention scales quadratically with prompt length. Under concurrent load, prefills are serialized on the GPU compute path. With C concurrent users, a random user waits for C/2 prefills ahead of them:
Interactive applications target < 1-5 seconds TTFT.

TTFT Breach Point

The maximum number of concurrent users before average TTFT exceeds the configured SLA threshold. Computed by solving:
Adding more GPUs increases TFLOPS, which reduces single prefill time, which pushes the breach point out. Adding NAND does not move the breach point — NAND doesn’t help prefill compute.

Single Prefill Time

The time to process one prompt through all layers with no queue contention. This is the atomic unit that TTFT is built from.
Where efficiency is ~35% (real-world vs peak TFLOPS). The quadratic attention term dominates at long contexts — a 32K prompt takes ~64x longer than a 4K prompt, not 8x.

Prefix Caching / Prefix Hit Rate

When multiple requests share the same prefix (system prompt, RAG context, few-shot examples), the KV cache for that prefix can be computed once and reused. A prefix cache hit skips the expensive prefill entirely for the shared portion. A 60% hit rate can effectively double throughput — the biggest “free” optimization in production inference.

Tensor Parallelism

Splitting a model across multiple GPUs within the same node. Each GPU holds a shard of the weights and a shard of each KV cache. GPUs communicate via NVLink during each forward pass.
  • Increases total HBM (more GPUs = more memory)
  • Increases aggregate bandwidth (faster TPOT)
  • Increases aggregate TFLOPS (faster prefill, lower TTFT)
  • Adds ~5-15% communication overhead via NVLink

Workload Mix

The distribution of session types by token intensity: The workload mix determines the weighted average context length and drives the capacity plan. A mix of { extreme: 1, heavy: 2, medium: 3, light: 4 } (10 users) produces a weighted average context of ~197K tokens.

Session Category Thresholds

The token boundaries used by the SessionProfiler to classify live sessions:

Overhead

A fixed 5 GB budget for activations, CUDA contexts, framework metadata, and vLLM’s internal data structures (page tables, scheduling state). This is subtracted from total HBM before computing KV capacity.

Precision Options

KV cache and model weights can be quantized independently:

KV Precision

fp8 KV is standard practice — it halves memory and bandwidth usage with negligible quality loss.

Weight Precision

The standard production setup is fp8 KV + bf16 weights for cloud GPUs, or fp8 KV + int4 weights for cost-sensitive self-hosted deployments.

Model Architectures

15 models included out of the box, with specs sourced from HuggingFace config.json: Custom architectures can be passed to any function:

GPU Specs

Key metrics explained:
  • HBM — Total GPU memory. Determines how much fits (weights + KV + overhead).
  • Bandwidth — How fast data streams from HBM. Determines TPOT (decode speed).
  • bf16 TFLOPS — Peak compute throughput. Determines prefill speed and TTFT.
  • NVLink — GPU-to-GPU interconnect bandwidth. Only matters for tensor parallelism across multiple GPUs in the same node. GPUs without NVLink communicate over PCIe (~64 GB/s), which adds latency for multi-GPU setups.

How the Math Works — Step by Step

This section walks through every calculation the capacity planner performs, with a worked example using Llama 3.1 70B on 8× RTX A5000 with int4 AWQ weights and fp8 KV cache.

Step 1: KV Bytes Per Token

What: How many bytes does one token cost in the KV cache? Formula:
Why each term:
  • 2 — one Key vector + one Value vector per layer
  • layers (80) — each of the 80 transformer blocks stores its own K and V
  • kv_heads (8) — GQA means only 8 KV heads (not all 64 query heads)
  • head_dim (128) — each head stores a 128-dimensional vector
  • precision_bytes (1 for fp8) — bytes per floating-point element
Calculation:
If we used bf16 instead of fp8, it would be × 2 bytes = 327,680 bytes = 320 KB/token — double. Code: kvBytesPerToken(arch, "fp8") in kv-estimator.ts

Step 2: KV Cache Per Session

What: Total KV memory for one session at a given average context length. Formula:
Calculation (16K context):
Calculation (128K full context):
Code: kvCacheForContext(arch, 16384, "fp8") in kv-estimator.ts

Step 3: Weight Memory

What: GPU memory consumed by the model’s parameters. Formula:
Precision ratios: bf16 = 1.0, int8 = 0.5, int4 = 0.25 Calculation (int4 AWQ):
Without quantization (bf16), weights would be 140 GB — needing 2× H100s just for weights. With int4, they fit on a single GPU with room to spare. Code: weightMemory(arch, "int4") in kv-estimator.ts

Step 4: Free HBM for KV Cache

What: How much GPU memory is available for KV cache after weights and overhead. Formula:
Calculation (8× RTX A5000):
The 5 GB overhead covers CUDA contexts, vLLM paging metadata, activation buffers, and framework state. Code: Lines 92-94 in capacity-planner.ts

Step 5: HBM Slots (Active Sessions)

What: How many concurrent sessions fit in free HBM. Formula:
Calculation (16K avg context, fp8):
Calculation (4K avg context, fp8):
Notice how context length dominates: 4× shorter context = 4× more sessions. Code: maxConcurrentSessions() in capacity-planner.ts

Step 6: NAND Slots (Parked Sessions)

What: How many additional sessions can be parked on SSD. Formula:
Calculation (4 TB NAND per GPU, 16K context, fp8):
NAND massively expands capacity. But those 12,800 sessions are parked — they need restore latency to become active. Code: Lines 32-36 in capacity-planner.ts

Step 7: TPOT (Decode Latency)

What: How long each output token takes to generate. Why bandwidth-bound: Each decode step must read the entire KV cache for all active sequences from HBM. The GPU compute is idle waiting for memory. Formula:
Calculation (16K context, 1 user, 8× RTX A5000):
With 10 concurrent users (batch=10):
TPOT scales linearly with batch size. At 50ms SLA, you breach at ~114 concurrent users. Code: estimateTpot() in latency-estimator.ts

Step 8: Single Prefill Time

What: Time to process one prompt through all layers (no queue). Why compute-bound: Prefill runs the full attention computation (quadratic in prompt length) plus FFN (linear). The GPU compute units are saturated, not memory. Formula:
The efficiency factor is 0.35 (35%) — real-world GPU utilization vs peak spec. This accounts for memory stalls, kernel launch overhead, and tensor parallelism communication. Calculation (4K prompt, 8× RTX A5000):
Why 32K prompt is ~64× slower than 4K (not 8×): The quadratic attention term dominates. When N grows 8x, the attention cost grows 64x. This is why long-context prefill is so expensive. Code: singlePrefillMs() in latency-estimator.ts

Step 9: TTFT Under Load

What: How long a user waits for the first token when other users are also submitting prompts. Why it degrades: Prefills are serialized on the GPU compute path. With C concurrent users, each user’s prefill waits behind the others in a queue. Formula:
The (C+1)/2 is the average queue position — if C users arrive simultaneously, a random user is at position 1 to C uniformly, so the average wait is (C+1)/2 prefills. Calculation (10 concurrent users, 4K prompt):
Calculation (100 concurrent users):
Code: estimateTtft() in latency-estimator.ts

Step 10: TTFT Breach Point

What: Maximum concurrent users before average TTFT exceeds the SLA. Formula (solving Step 9 for C):
Calculation (5 second SLA, 4K prompt):
At 41 concurrent users, the average TTFT hits 5 seconds. The 42nd user will experience over 5s wait. Important: Adding NAND does NOT change this number. NAND parks cold sessions but doesn’t add TFLOPS — the prefill queue bottleneck is compute, not memory. Code: ttftBreachPoint() in latency-estimator.ts

Step 11: Restore Latency

What: Time to wake up a cold session from NAND SSD. Formula:
Each GPU restores its own shard in parallel, so the KV per session is divided by GPU count. Calculation (16K context fp8, 8× GPU, Gen4 NVMe 7 GB/s):
With parallel restore streams (4 sessions restoring simultaneously):
Code: restoreLatency() in latency-estimator.ts

Step 12: Monthly GPU Cost

What: Infrastructure cost estimate. Formula:
Calculation (8× RTX A5000 on-demand at $1.10/hr):
Per-slot cost:
Code: monthlyGpuCost() in infra-cost.ts

Step 13: Weighted Average Context (Workload Mix)

What: Converts the session type distribution into a single average context length. Formula:
Midpoints: light=35K, medium=130K, heavy=325K, extreme=1,250K Calculation (extreme=1, heavy=2, medium=3, light=4):
This weighted average drives all the session slot calculations in planCapacity(). Code: weightedAvgContext() in capacity-planner.ts

Full Worked Example Summary

Config: Llama 3.1 70B, 8× RTX A5000, int4 weights, fp8 KV, 16K avg context

The CapacityPlan Object

The planCapacity() function returns a complete CapacityPlan with every metric:

Interactive Dashboard

The apps/capacity-planner/ Next.js app provides a full interactive UI with:
  • Model selector (all 15 architectures)
  • GPU type, count, NAND per GPU sliders
  • KV/weight precision selectors
  • Workload controls (avg context, cold ratio, SLA thresholds)
  • Per-GPU breakdown panel (shows free HBM + NAND per card)
  • 6 interactive charts:
    • Users vs GPUs — session capacity scaling with GPU count
    • Users vs Context — how capacity drops as context grows
    • TPOT vs Users — decode latency at different context sizes
    • TTFT vs Users — prefill queue congestion with SLA breach markers
    • Restore Budget — NAND restore time at Gen4/Gen5 bandwidth
    • GPU vs NAND — total sessions across NAND sizes