TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

KV Offloading and Disaggregated Serving: Reloading Evicted Prefixes from the CPU

Status: Draft (2026-10-27), offloading measured, disaggregated serving unmeasured. Measured on 2026-10-27 using a single 8GB laptop GPU (Ada generation), vLLM 0.31.0, and Qwen3-Reranker-0.6B. The principles and measurements of offloading and disaggregated serving using LMCache and NVIDIA Dynamo are covered in Parts 3 and 4 of "Anatomy of LLM KV Cache", so this post focuses on vLLM's built-in CPU offloading. Writing rules: Measured data takes priority; unmeasured items are marked as "Unmeasured", and unverified items as "Unverified".

Table of Contents

1. Prefix Caches Inside the GPU Are Quickly Evicted

Part 2 showed that reusing a 3,200-token prefix reduces the time to the first token from 174.7ms to 18.9ms. However, that block can only be reused if it remains in the "free space" of the GPU KV cache. When a new request demands space, vLLM evicts the oldest unused blocks first. In a service querying multiple long documents alternately, a few gigabytes of GPU memory will fill up quickly.

KV offloading is a feature that copies blocks to a larger, slower medium—specifically CPU memory—before eviction (strictly speaking, when a block becomes full). When the same prefix appears again, any blocks missing from the GPU are retrieved from the CPU and restored. The break-even point—where transferring is faster than recomputing—was covered in the LMCache post.

2. vLLM's Default Offloading

vLLM bundles the exchange of KV data outside the engine into a single interface called the KV Connector. Multiple implementations attach to this same interface.

Connector Function
OffloadingConnector vLLM default CPU offloading. Introduced with LRU management in 0.11.0 and finalized as general CPU offloading in 0.19.0
LMCacheConnectorV1, LMCacheMPConnector LMCache extending to CPU, disk, and remote storage
NixlConnector, MooncakeConnector Sends KV to another engine (different GPU, different server). Used for disaggregated serving
MultiConnector Chains multiple connectors sequentially

Default offloading is enabled with two lines. --kv-offloading-size specifies the CPU buffer size in GiB, and providing this value activates the native backend (specifically OffloadingConnector).

vllm serve <모델> --kv-offloading-size 0.5 --kv-offloading-backend native

Detailed configurations are passed via --kv-transfer-config 's kv_connector_extra_config . Based on the 0.32.0 documentation, options include CPUOffloadingSpec(default), which uses a single CPU, and TieringOffloadingSpec , which places a secondary tier like a file system beneath the CPU. Eviction order selects either lru or arc . By default, only prompt (prefill) blocks are offloaded, while decode blocks are not (offload_prompt_only). The documentation notes that copying between GPU and CPU overlaps with computation via cudaMemcpyAsync .

3. Measuring It Yourself

Method

An eviction scenario was intentionally simulated.

Results

Stage No Offloading (Median TTFT, 2nd–5th batches) 0.5GiB Offloading (Same criteria)
A First (Cold) 76.8ms 77.7ms
A Immediate Resend (GPU Hit) 14.0ms 14.3ms
Evicted by B & C 77.2ms 78.1ms
A Resent (After Eviction) 77.3ms(Recomputed) 35.1ms(Loaded from CPU)

Looking at all five batches, "A Resent" on the offloading server clocked in at 35.3, 35.4, 34.8, 33.3, and 35.6ms, consistently hovering around 34ms, whereas the server without offloading matched cold requests at 76.3–78.0ms. The very first request on both servers reached 104ms, absorbing nearly 30ms of warmup overhead.

Server metrics were also checked to verify what was transferred.

Metric(/metrics) Value Meaning
vllm:kv_offload_load_bytes_total 849.6MB (5 times) Amount restored from CPU→GPU. Roughly 170MB per instance = approx. 1,480 tokens × 114,688 bytes
vllm:kv_offload_load_time_total 0.0673s Time taken for five restorations. About 12.6GB per second, 13.5ms per instance
vllm:kv_offload_store_bytes_total 2.82GB (25 times) Amount offloaded from GPU→CPU. All prompt blocks are offloaded
vllm:kv_offload_store_time_total 0.221s Roughly 12.8GB per second
vllm:prompt_tokens_by_source_total Computed 24,771 / GPU Hit 7,936 / External 7,408 "External" tokens loaded from the CPU

Here is how to read the data:

  1. The time to the first token for the evicted prefix became 2.2x faster (77.3ms → 35.1ms). However, it is still 21ms slower than when it remains in the GPU (14.0ms). Most of that difference comes from the 13.5ms required to transfer 170MB.
  2. The KV transfer speed on this GPU was approximately 12.6GB per second. Recomputing the same 1,480 tokens takes about 63ms (calculated by subtracting the GPU hit time of 14.0ms from 77.3ms). With a computation speed of roughly 23,000 tokens per second and a transfer speed of roughly 110,000 token equivalents per second, transferring is nearly five times faster in this combination.
  3. Offloading is not free. A total of 2.82GB was offloaded, including blocks that were never reused. Because copying is asynchronous, the difference was barely noticeable in the cold request TTFT of this measurement (76.8 vs 77.7ms), but it continuously consumes PCIe bandwidth and CPU memory.
  4. The CPU buffer uses pinned memory. Allocating 0.5GiB locks those pages, counting directly against the container memory limit. This test used a small buffer to stay within the 4GiB limit.

4. When Is There No Benefit?

5. Disaggregated Serving: Sending KV to Another Engine via the Same Connector

Disaggregated serving, which places prefill and decode on separate GPUs, operates on top of the connector architecture described above. The prefill engine generates KV, sends it to the decode engine via NixlConnector , and the decode engine continues generation using the received KV. The vLLM documentation cites two main reasons for this:

The same documentation explicitly states that "disaggregated serving does not increase throughput" and labels the entire feature as experimental. The original paper, DistServe (OSDI 2024), reported 7.4x to 12.6x stricter request goals compared to traditional systems when evaluated by maximum request rate per GPU while satisfying both TTFT and TPOT targets (based on the abstract). Version 0.32.0 also includes a feature where the decode side reuses token IDs generated by the prefill side (kv_transfer_params 's prompt_token_ids).

Because I only have a single GPU, standalone vLLM disaggregated serving remains unmeasured here. Results from running disaggregated serving with NVIDIA Dynamo across two machines connected via 1GbE and a single GPU family (5x slower on a single card, over 20x slower between two machines) can be found in the Dynamo post and Part 24 of "Toss ML/AI Infra Preparation". The conclusion that KV transfer must be as fast as NVLink or RDMA to be meaningful aligns with this offloading measurement (12.6GB/s over PCIe).

6. Takeaways for Production

7. Unverified Items

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment