PagedAttention & Continuous Batching Physics

vLLM Continuous Batching & KV Cache GPU Concurrency

Size multi-GPU clusters for high-concurrency LLM serving. Calculate exact PagedAttention KV cache pools, model weight VRAM footprints, memory bandwidth saturation, and maximum simultaneous streaming users before OOM.

Quick Setups:
Model & Hardware Cluster Config
Tune architecture, quantization, context length, and GPU count
LLM Base Architecture
Weight Quantization Precision
KV Cache Precision (`--kv-cache-dtype`)
GPU Cluster & Tensor Parallelism (TP)
Average Active Context Window (Prompt + Gen) 4,096 tokens
512 tokens 8,192 tokens 32,768 tokens (Long)
GPU Memory Target (`--gpu-memory-utilization`) 0.95 (95%)
0.80 (Conservative) 0.95 (Production Recommended) 0.98 (Aggressive)
Max Simultaneous Streams
138 Streams
Zero OOM Queueing Capacity
Total Cluster Throughput
2,840 tps
~20.6 tokens/sec per stream
Available KV Cache VRAM
89.2 GB
660 MB per 4k user stream
Cluster VRAM Allocation & Headroom
Static weights vs PagedAttention KV pool vs CUDA buffer
Model Weights
Active KV Cache
CUDA Safety Buffer
Weights: 70.0 GB KV Pool: 82.0 GB 0 GB 80 GB 160 GB Total VRAM Optimal Sizing: Supports 138 concurrent users @ 4,096 tokens without OOM
Hardware Physics & VRAM Allocation Breakdown
Allocation Category Value / Size Percentage of VRAM Technical Context
Total Cluster Physical VRAM 160.0 GB (2x H100) 100.0% Combined VRAM across NVLink / PCIe
Static Model Weights 70.0 GB 43.8% Sharded evenly via Tensor Parallelism
CUDA Runtime & Activation Buffer 8.0 GB 5.0% PyTorch context, NCCL communication
Available PagedAttention KV Cache Pool 82.0 GB 51.2% Divided into non-contiguous 16-token pages
KV Cache Memory Per User Stream 655.4 MB 0.41% GQA 8 heads × 80 layers × 4k tokens (FP8)
Simultaneous Concurrency Limit 138 Streams Max Capacity Continuous batching saturation point
run_vllm_cluster.sh — High-Throughput Production Flags
#!/usr/bin/env bash
# Production vLLM Deployment with PagedAttention and FP8 KV Cache Optimization

python3 -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.3-70B-Instruct \
  --tensor-parallel-size 2 \
  --pipeline-parallel-size 1 \
  --gpu-memory-utilization 0.95 \
  --max-model-len 8192 \
  --max-num-seqs 256 \
  --kv-cache-dtype fp8_e5m2 \
  --dtype float16 \
  --block-size 16 \
  --enable-chunked-prefill \
  --disable-log-requests \
  --host 0.0.0.0 \
  --port 8000 \
  --served-model-name "llama-3.3-70b-instruct"
docker-compose.yml — 2x/4x/8x NVIDIA GPU Cluster Setup
version: '3.8'

services:
  vllm-engine:
    image: vllm/vllm-openai:latest
    container_name: vllm_prod_engine
    restart: always
    runtime: nvidia
    ports:
      - "8000:8000"
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
      - NCCL_DEBUG=INFO
      - VLLM_LOGGING_LEVEL=INFO
    volumes:
      - /mnt/fast_nvme/hf_cache:/root/.cache/huggingface
      - /dev/shm:/dev/shm # Critical: Allocate host shared memory for PyTorch IPC
    shm_size: '32gb'
    command: >
      --model meta-llama/Llama-3.3-70B-Instruct
      --tensor-parallel-size 2
      --gpu-memory-utilization 0.95
      --max-model-len 8192
      --max-num-seqs 256
      --kv-cache-dtype fp8
      --port 8000
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [gpu]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
benchmark_concurrency.py — Async Load Generator for vLLM
import asyncio
import aiohttp
import time

VLLM_URL = "http://localhost:8000/v1/chat/completions"
CONCURRENT_USERS = 64
MAX_TOKENS = 512

PROMPT = "Explain the difference between continuous batching and static batching in 3 paragraphs."

async def send_user_stream(session, user_id):
    payload = {
        "model": "llama-3.3-70b-instruct",
        "messages": [{"role": "user", "content": PROMPT}],
        "max_tokens": MAX_TOKENS,
        "temperature": 0.7,
        "stream": True,
    }
    
    start_time = time.time()
    ttft = None
    token_count = 0
    
    async with session.post(VLLM_URL, json=payload) as resp:
        async for chunk in resp.content:
            if not ttft:
                ttft = time.time() - start_time
            token_count += 1
            
    total_time = time.time() - start_time
    return {"user_id": user_id, "ttft": ttft, "total_time": total_time, "tokens": token_count}

async def main():
    print(f"Firing {CONCURRENT_USERS} simultaneous user streams into vLLM cluster...")
    async with aiohttp.ClientSession() as session:
        tasks = [send_user_stream(session, i) for i in range(CONCURRENT_USERS)]
        results = await asyncio.gather(*tasks)
        
    avg_ttft = sum(r["ttft"] for r in results) / len(results)
    total_tokens = sum(r["tokens"] for r in results)
    total_wall_time = max(r["total_time"] for r in results)
    
    print(f"\n--- Benchmark Results ---")
    print(f"Total Tokens Generated: {total_tokens}")
    print(f"Aggregate Throughput: {total_tokens / total_wall_time:.2f} tokens/sec")
    print(f"Average TTFT (Time-To-First-Token): {avg_ttft * 1000:.1f} ms")

if __name__ == "__main__":
    asyncio.run(main())
Model Architecture Total / Active Parameters Layers Attention Type KV Heads KV Cache per 1k tokens (FP16)
Llama 3.3 70B 70.6 Billion (Dense) 80 GQA (Grouped Query) 8 Heads 320 KB
DeepSeek R1 / V3 671B MoE (37B active) 61 MLA (Multi-head Latent) Compressed 128 KB
Qwen 2.5 72B 72.7 Billion (Dense) 80 GQA (Grouped Query) 8 Heads 320 KB
Mistral Small 24B 24.0 Billion (Dense) 56 GQA (Grouped Query) 8 Heads 224 KB
Llama 3.1 8B 8.0 Billion (Dense) 32 GQA (Grouped Query) 8 Heads 128 KB

The Engineering Physics of vLLM Continuous Batching

In traditional deep learning pipelines, batching works synchronously: a server waits for 16 or 32 requests to arrive, packages them into a static matrix, passes them through the GPU, and waits until the longest generation finishes before releasing the batch. If one user asks for 50 tokens and another asks for 1,000 tokens, the GPU sits idle on 15 rows while waiting for that single user to finish.

Continuous Batching (Iteration-Level Scheduling) changes this paradigm completely. Instead of waiting for a batch to complete, the vLLM engine operates at the individual token generation step. As soon as a request emits its `<|endoftext|>` token, that sequence is immediately evicted from the iteration and a newly arrived request is injected into the next forward pass without waiting.

The PagedAttention Breakthrough: Continuous batching was historically bottlenecked by memory fragmentation. Because sequence lengths are unpredictable, naive memory managers reserved contiguous VRAM blocks for each user's maximum potential context window (e.g. 8,192 tokens), wasting up to 80% of VRAM on unallocated space. PagedAttention solves this by managing KV cache in non-contiguous 16-token memory pages, increasing effective concurrency by 200% to 400% on the exact same physical GPUs.

GQA (Grouped Query Attention)

In older models like Llama 1 (Multi-Head Attention), every query head had its own key and value head, resulting in massive KV cache sizes. Modern architectures like Llama 3 and Qwen 2.5 use GQA, grouping 8 query heads to share a single key-value head. This slashes KV cache memory overhead by 87.5% with near-zero accuracy loss.

Memory Bandwidth vs Compute Bound

During the initial prompt prefill phase, inference is compute-bound (matrix multiplication of large prompt chunks). During the autoregressive token decode phase, inference is memory-bandwidth bound: the entire model weight footprint must be read from HBM3 memory into the GPU cores for every single token generated across all concurrent users.

FP8 KV Cache Halving

By launching vLLM with `--kv-cache-dtype fp8_e5m2`, the KV cache values are dynamically quantized from 16-bit to 8-bit. This directly halves the per-token memory footprint from 320 KB down to 160 KB for 70B models, doubling the number of concurrent streaming users that can fit inside the VRAM pool.

Preemption & Swapping Safeguards

If a sudden burst of users submit massive 16k context prompts, available KV cache pages can be exhausted. Rather than crashing the GPU with a CUDA Out-Of-Memory error, vLLM dynamically pauses and preempts sequences, evicting pages to CPU memory until GPU headroom is freed up.

Frequently Asked Questions: vLLM GPU Cluster Sizing

Traditional transformer inference allocates contiguous memory blocks for each request's maximum potential context window upfront, causing 60% to 80% VRAM memory waste due to internal and external fragmentation. PagedAttention divides the KV cache into non-contiguous physical memory blocks (similar to virtual memory pages in an operating system), enabling near-zero memory waste and allowing 2x to 4x higher concurrent user throughput on identical GPU hardware.
The memory consumed by the Key-Value (KV) cache per token is given by the formula: 2 × Number of Layers × Number of KV Heads × Head Dimension × Precision Bytes. For Llama 3.3 70B (80 layers, 8 KV heads with Grouped Query Attention, 128 head dim, FP16 2 bytes), each token consumes exactly 327,680 bytes (320 KB). A 4,000-token conversation consumes 1.28 GB of VRAM solely for that single user's KV cache.
In standard Multi-Head Attention (MHA), there is one Key head and one Value head for every Query head (e.g. 64 Q heads, 64 KV heads). Grouped Query Attention (GQA) shares a single Key-Value head across multiple Query heads (e.g. 64 Q heads sharing 8 KV heads in Llama 3 70B). This reduces KV cache size by an astounding 8x with minimal impact on model reasoning quality, massively expanding concurrent user capacity.
Yes! Passing `--kv-cache-dtype fp8` (or `fp8_e5m2`) to vLLM halves KV cache memory consumption from 2 bytes per element to 1 byte per element. On modern NVIDIA Ada Lovelace, Hopper, and Blackwell GPUs, FP8 KV cache enables up to 2x higher concurrent user streams without noticeable perplexity degradation.
During token generation, an LLM generates one token at a time autoregressively. For every single generated token, the entire model parameter weight matrix must be transferred from GPU VRAM into the compute tensor cores. The theoretical token generation speed across the batch is directly constrained by the GPU's memory bandwidth (e.g. 3.35 TB/s on H100 SXM5 vs 864 GB/s on L40S).
When sudden traffic spikes exhaust the PagedAttention memory pool, vLLM triggers request preemption: it pauses decoding for selected lower-priority sequences and temporarily evicts or swaps their KV cache blocks to CPU RAM. Once active sequences finish generating, the evicted sequences are recomputed or swapped back. This guarantees the server never crashes with CUDA OOM, though preemption introduces slight latency jitter.
DeepSeek R1 and V3 are 671B Mixture-of-Experts (MoE) architectures with 37B active parameters per token. In native FP8 quantization, the weights require ~350 GB of VRAM. A production deployment requires at least 8x NVIDIA H100 80GB GPUs (Tensor Parallelism = 8) or 4x NVIDIA H200 141GB GPUs to provide sufficient headroom for large concurrent KV caches.
The recommended setting is 0.90 to 0.95. Leaving 5% to 10% of VRAM free prevents catastrophic CUDA driver out-of-memory errors caused by dynamic PyTorch allocations, intermediate activation tensors, and NCCL inter-GPU communication buffers during Tensor Parallelism.
4.9 out of 5
★★★★★
Developer Community Benchmark Rating
Verified across 167 community production token & cost audits
Rate this benchmark: