TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Measuring vLLM directly with a small model: Load, KV cache capacity, prefix cache, chunk size

Status: Draft (2026-10-07), empirical measurement post. All numbers are directly measured by the author in the environment below; calculations are marked as 「Calculated」 and unmeasured items as 「Unmeasured」. Since this is a single environment, a single model, and a single batch of tests, please focus more on the shapes and principlesrather than absolute values. Writing rule: Empirical data is the baseline, and unmeasured items are marked as 「Unmeasured」.

Table of Contents

1. What I wanted to know

The vLLM documentation has many options, and in interviews and operations, people ask 「What changes and by how much when you turn on this option?」 Part 4 summarized the principles (KV cache formula, prefix cache, scheduler) through documentation, and this post answers the same questions with numbers.

  1. Where do throughput and latency start to degrade as concurrent requests increase?
  2. Does the KV cache capacity match the calculated formula, and how does that capacity connect to the point of degradation?
  3. What does each of the following change: prefix cache, KV 8-bit, CUDA graphs, chunk size, and max concurrent sequences?

2. Environment and methodology

Item Value
GPU One 8GB-class laptop GPU (~7.6GiB memory), no other workloads
Server vLLM 0.31.0 official image, PyTorch 2.13 (CUDA 13)
Model Qwen3-1.7B (bf16, server loaded weights 3.22GiB), model config: 28 layers, 8 KV heads, head_dim 128
Common options --max-model-len 4096, --gpu-memory-utilization 0.85, --max-num-seqs 64(16 for comparison tests only)
Client Async client inside the same container as the server, recording timestamps per token via streaming to calculate Time-to-First-Token (TTFT) and Inter-Token Latency (ITL)
Input Random word sequence per request (to prevent accidental prefix cache hits), 128 output tokens (ignore_eos), temperature 0
Repetition 3c requests for concurrency c (2c requests for long input tests), discarding the first two requests as warm-up

The server was restarted for each test (KV cache capacity differs between initial startup and restart, refer to Section 3). Values in the tables were read from a single batch, and repetition variance was measured separately. Repeating the same test 5 times on the same server(concurrency 32, input 512, output 128) yielded throughputs of 831, 833, 831, 832, and 831 tok/s, a TTFT median of 666~670ms, a TTFT p95 of 1,606~1,608ms, and an ITL p99 of 223~224ms, remaining within 0.5%. Restarting the server 3 times also showed identical results: 66 tok/s for concurrency 1 and 831~832 tok/s for concurrency 32. Prefix cache (3 runs) and preemption/interrupts (3 runs, max ITL 227, 226, 226ms) were also identical. This means that in this environment (single GPU, no other workloads), test variance itself is small, though it may change if moved to different hardware or an environment with neighbors.

3. KV cache capacity matches the formula

The KV size per token is 2 × 층 수 × KV 헤드 수 × head_dim × 자료형 바이트. For this model with 28 layers, 8 KV heads, head_dim 128, and bf16 (2 bytes) KV, it is as follows (calculated).

2 × 28 × 8 × 128 × 2 = 114,688 바이트/토큰 (112KiB)
Configuration KV cache memory in server logs Token count in server logs Bytes per token (calculated) Formula value
bf16 KV (default) 2.36GiB 22,048 ~114,900 114,688
fp8 KV (--kv-cache-dtype fp8) 2.24GiB 41,920 ~57,400 57,344

4. Increasing concurrent requests: Throughput slows down, then the queue explodes at KV capacity limit

Input 512, output 128, --max-num-seqs 64:

Concurrent requests Throughput (tok/s) TTFT median TTFT p95 ITL median ITL p99
1 66 73ms 73ms 15ms 16ms
8 383 351ms 474ms 17ms 19ms
32 831 666ms 1.6s 24ms 223ms
64 787 5.7s 6.7s 25ms 228ms

Increasing the input to 2,048 tokens means a single request uses 2,176 tokens, allowing the cache to hold about 10 requests (calculated).

Concurrent requests Throughput (tok/s) TTFT median TTFT p95
4 151 0.73s 1.0s
16 207 4.0s 6.7s
32 213 12.5s 16.7s

Multiplying concurrency by 4 from 4 to 16 increased throughput by only 37% while TTFT became 5.4x. With long inputs, the concurrency level where throughput plateaus drops significantly. This is why in operations, concurrency limits should be determined not by 「what the GPU can endure」, but by 「the number of requests KV can hold given the average input length」.

When KV is insufficient: Do they just wait, or are they evicted?

Preemption counts were read using server metrics (vllm:num_preemptions_total). During 5 repetitions of input 512 and concurrency 32 (within the ~34 request limit of KV capacity), preemption count was 0. Sending input 2,048 and concurrency 32 (64 requests) yielded 7 counts, and subsequently sending input 1,500, output 256, and concurrency 64 (128 requests) resulted in a cumulative 29 counts(22 counts under this specific load). Median TTFTs during these tests were 12.5s and 35.4s, respectively. When KV is insufficient, requests do not merely wait in the queue; running requests are evicted, relinquish their KV cache, and are processed all over again from scratch(preemption recomputes the rolled-back request, incurring an extra input processing step, per documentation). Since the region where preemption count is non-zero exhibits stagnant throughput and surging latency, it serves as a useful alerting metric.

Max concurrent sequences (--max-num-seqs) only shifts the queue location

Concurrency 64, input 512:

Configuration Throughput TTFT median
--max-num-seqs 64 787 tok/s 5.7s (initial startup server, above repeated value is 842 tok/s, 2.1~2.2s)
--max-num-seqs 16 608 tok/s 10.3s

Lowering the upper limit to 16 decreased throughput by 23% and made TTFT 1.8x. If the upper limit is lower than the capacity (about 34), a queue forms even when capacity remains, which is a loss; even if it is higher than the capacity, there is no benefit if KV is lacking. The upper limit is a value set around the number of requests the KV capacity can hold.

5. KV 8-bit: More capacity reduces latency for long inputs

Test bf16 KV fp8 KV
Concurrency 1, Input 512 (Throughput, ITL) 66 tok/s, 15ms 66 tok/s, 15ms
Concurrency 16, Input 2048 (Throughput) 207 tok/s 303 tok/s (+46%)
Concurrency 16, Input 2048 (Median TTFT) 4.0s 0.94s (−76%)
Concurrency 32, Input 2048 (Throughput) 213 tok/s 299 tok/s (+40%)
Concurrency 32, Input 2048 (Median TTFT) 12.5s 7.0s (−44%)
KV Precision Attention Backend Perplexity vs. bf16
bf16 Auto-select (FLASH_ATTN) 17.7775 Baseline
bf16 FLASH_ATTN (fixed, 2 iterations) 17.7796, 17.7796 +0.01%
bf16 FLASHINFER 17.7750 -0.01%
bf16 TRITON_ATTN 17.7732 -0.02%
fp8 FLASHINFER 17.4228 -2.0%
fp8 TRITON_ATTN 17.0340 -4.2%

6. Prefix Caching: Having a common prefix makes the first token 14x faster

We attached a different 32-token tail to a 3,000-token common prefix for each request and sent them one by one in order (1 output token).

Configuration First Request TTFT Subsequent Requests Median TTFT 3,032 tokens with no prefix at all
Prefix cache enabled (default) 394ms 28ms 395ms
--no-enable-prefix-caching 395ms 393ms 393ms

7. Chunked Prefill: Chunk size determines how long long inputs pause decoding

While a single decoding request (300 tokens) was running, 3 requests for 3,500-token inputs were interleaved, and the token intervals in the interleaved section were measured.

--max-num-batched-tokens Max ITL in Interleaved Section Median ITL in Interleaved Section TTFT of Long Input Itself
512 69ms 53ms 481–494ms
Default (Verified value 2,048) 227ms (3 iterations: 227, 226, 226) 14ms 466–480ms
8192 (Input is 3,500 tokens, so effective chunk is 3,500) 445ms 14ms 461–474ms

8. Disabling CUDA graphs (--enforce-eager)

Configuration Server Startup Time Concurrency 1 Throughput Concurrency 32 Throughput Median ITL (Concurrency 1)
Default (CUDA graphs) 71s (first boot, including 19s compilation), subsequent reboots 35–55s 66 tok/s 831 tok/s 15ms
--enforce-eager 30s 64 tok/s 814 tok/s 15ms

For a model of this size, the speed benefit of CUDA graphs was as small as about 3% (66 vs. 64 at concurrency 1), while server preparation was about 25–40 seconds faster. This is likely because, for models taking 14–15ms per token, the kernel launch interval accounts for a small proportion of the total (estimated). We noted that the benefit could be larger for smaller models, and repeating the same test 3 times with Qwen3-0.6B showed that it actually was. At concurrency 1, CUDA graph default 168 tok/s (ITL 6ms) vs. --enforce-eager 108 tok/s (ITL 9ms), graphs were 55% faster, and at concurrency 32, it was about 3.6% at 1,557 vs. 1,503 tok/s. Server preparation was 55s vs. 30s. This is likely because, with a smaller model where time per token is around 6ms, the kernel launch interval constitutes a large share of the total (estimated). Faster GPUs were not tested (not measured). In Qwen3-4B-FP8, an even larger model, the graph default at concurrency 1 was 49 tok/s vs. --enforce-eager 47 tok/s, a benefit of about 4% (ITL 19ms vs. 20ms). The benefit of graphs was large only in very small models where a single token is short, and plateaued at 3–4% for 1.7B and above.

9. Time taken to boot the server for the first time

Values read from server log timestamps.

Step Value
Process startup and module loading (until engine initialization begins) About 15–17s
Weight loading (from disk to GPU) 4–6s (when model files already exist)
Engine initialization (compilation, profiling, KV cache allocation, CUDA graph capture, warmup) First boot 29s (including 19s compilation), 7.5s if compilation cache exists
Total until ready First boot 71s, reboot 35–55s (70s for servers with LoRA enabled)

Model download time is excluded from the above (separate). Since the time for a new pod to receive traffic in autoscaling adds image downloading and model downloading to these figures, pre-warmed replicasand model cache volumesbecome numerically necessary.

10. Takeaways for Operations

  1. Concurrency limit = KV capacity ÷ tokens per request. The formula is from Section 3 and the rationale is from Section 4. Exceeding the limit does not increase throughput, only TTFT.
  2. Look at TTFT p95 and ITL p99 separately. Even if the median is fine, the tail is large (ITL p99 223ms).
  3. Design common prefixes intentionally. Placing system prompts and documents at the beginning and request-specific content at the end allows the prefix cache to provide a 14x benefit.
  4. KV 8-bit is a capacity option, not a throughput option. It is highly effective when queues are long with long inputs. Perform quality evaluations first.
  5. Chunk size is a trade-off between interruptions and input processing speed. Small chunks for conversational services, large chunks for batch jobs.
  6. Performance tests should vary inputs every time and be checked against hardware limits.

11. Potential Interview Questions

External Evidence: Verified with Papers and Official Materials

All figures in this article are measured directly. We reviewed external materials reporting the same phenomenon to check the numbers and conditions, and described their relationship with our results. Since external figures differ in model, GPU, and load, we did not make direct comparisons but only judged agreement in direction and structure.

Topic Material and Verified Location Verified Content and Conditions Relationship with This Article
KV Size Formula Kwon et al., PagedAttention, SOSP 2023, arXiv 2309.06180, Section 3 and Table 1 KV per token for OPT-13B is 2 (key and value) × 5,120 (hidden size) × 40 (layers) × 2 (bytes) = 800KB. On A100 40GB, subtracting 26GB for weights and using 12GB for KV yields 15.7K tokens, matching the arithmetic of 12GiB ÷ 800KiB. Supports. Same formula as in this article (Section 3)
KV Waste in Existing Systems Same paper, Section 1 and Figure 2 The ratio of KV used as actual token states is 20.4–38.2%. This is the average of three Orca variants implemented directly by the authors, excluding FasterTransformer. Related. Managing in units of 16-token blocks (the default verified in this article) is a way to reduce this waste.
Chunk Size and Inter-Token Latency Agrawal et al., Sarathi-Serve, OSDI 2024, arXiv 2403.02310, Section 4.3, Figure 6, and Section 5.4.1 Smaller chunks (token budgets) result in shorter iteration times, reducing inter-token latency. In the compute-bound regime, iteration time is linear with the number of tokens. At chunk 512, total input processing time increases by up to about 25% (Yi-34B, tensor parallelism 2), while at 2,048 it is almost negligible. Supports. Same direction as Section 7. Our TTFT increase (3–5%) cannot be directly compared in magnitude due to different models and GPUs.
Shape of Concurrency and Throughput Same paper, Section 3.1 and Figure 3 (Mistral-7B, single A100, input 1,024) Batching increases decoding throughput almost linearly and has almost no effect on input processing. Partially supports. Same shape as short contexts in Part 21 (14–15x up to concurrency 16). The input 512 table in Section 4 saturates earlier.
Latency Explosion Beyond Capacity PagedAttention, Section 6.2 When the request rate exceeds system capacity, the queue grows endlessly, causing latency to explode. Supports. Same phenomenon as the 8.6x TTFT increase in Section 4.
Cases Where Throughput Plateaus Before Capacity Recasens et al., "Mind the Memory Gap", arXiv 2503.08311, Sections 3–5 On H100 with models like OPT-2.7B, memory limits are reached even at large batches, and 225 tok/s at batch 1 stalls at 7,607 tok/s at batch 256 (about 33.8x, not the expected 256x). Partially supports. Evidence that the saturation in Section 4 may not be solely due to KV capacity.
Quality of KV 8-bit vLLM Official Blog, "The State of FP8 KV-Cache and Attention Quantization in vLLM" (2026-04-22) Uncompensated FP8 (scale 1.0) shows a drop in inference task accuracy of up to 1–2 points on Qwen3-30B-A3B-Thinking-2507 and up to 0.7 points on Qwen3.5-27B (H100). Supports. Changes in literature are small, but depend on the model and calibration. Refer to Section 5 for our model (1.7B) and our evaluation.
KV 8-bit (Integer) LMDeploy Documentation (INT8 KV, OpenCompass) llama2-7b-chat MMLU from 35.64 to 35.58, internlm2.5-chat-7b MMLU from 72.30 to 72.27. Since this is INT8 rather than FP8, it cannot be directly transferred. Related. A different method from vLLM's --kv-cache-dtype fp8 .
Preemption Method vLLM Official Documentation (Optimization page), v0.30.0 scheduler.py, PagedAttention Sections 4.5 and 7.3 Preemption in V1 defaults to recalculation rather than swapping, returning all blocks of the victim request, resetting the calculated token count to 0, and placing it at the front of the queue. The paper explains that recalculation is efficient with small blocks. Supports. Same as the preemption description in Section 4 (prefix cache hit after preemption was not verified).
Defaults v0.30.0 config/cache.py Prefix cache enabled by default, block size 16 Supports. Matches values read directly in 0.31.0 (enabled, 16, chunk budget 2,048).

Figures from vendor and project blogs (vLLM, LMDeploy) were transcribed only when conditions were stated in the text. ACM's PagedAttention page could not be accessed, so we followed the notation on the arXiv abstract page.

Unverified Items

Verified Versions and Dates

As of 2026-10-07. vLLM 0.31.0, PyTorch 2.13 (CUDA 13), Qwen3-1.7B (bf16).

Sign in to like

Comments

No comments yet.

Sign in to leave a comment