상태: 초안 (2026-10-27), 오프로딩 실측, 분리 서빙 미실측. 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0, Qwen3-Reranker-0.6B 로 2026-10-27 에 쟀습니다. LMCache 와 NVIDIA Dynamo 를 이용한 오프로딩·분리 서빙의 원리와 실측은 「LLM KV 캐시 해부」 3·4편에 있어, 이 글은 vLLM 에 기본으로 들어 있는 CPU 오프로딩에 집중합니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. GPU 안의 접두사 캐시는 금방 밀려난다
- 2. vLLM 의 기본 오프로딩
- 3. 직접 재 보기
- 4. 언제 이득이 없나
- 5. 분리 서빙: 같은 커넥터로 KV 를 다른 엔진에 보내기
- 6. 운영에서 가져갈 것
- 7. 확인하지 못한 것
- 참고
1. GPU 안의 접두사 캐시는 금방 밀려난다
2편에서 3,200토큰 접두사를 다시 쓰면 첫 토큰이 174.7ms 에서 18.9ms 가 된다고 쟀습니다. 그런데 그 블록은 GPU KV 의 「빈 자리」에 남아 있을 때만 다시 쓸 수 있습니다. 새 요청이 자리를 달라고 하면 vLLM 은 아무도 쓰지 않는 블록 중 가장 오래된 것부터 지웁니다. 긴 문서 여러 개를 번갈아 묻는 서비스라면 GPU 몇 GB 로는 금방 모자랍니다.
KV 오프로딩은 지우기 전에(정확히는 블록이 다 찼을 때) 그 블록을 더 크고 느린 곳, 곧 CPU 메모리에 복사해 두는 기능입니다. 같은 접두사가 다시 오면 GPU 에서 못 찾은 블록을 CPU 에서 찾아 되돌립니다. 다시 계산하는 것보다 옮기는 것이 빠를 때만 이득이라는 손익 분기는 LMCache 편에서 다뤘습니다.
2. vLLM 의 기본 오프로딩
vLLM 은 KV 를 엔진 밖과 주고받는 일을 KV 커넥터라는 한 인터페이스로 묶습니다. 같은 인터페이스에 여러 구현이 붙습니다.
| 커넥터 | 하는 일 |
|---|---|
OffloadingConnector |
vLLM 기본 CPU 오프로딩. 0.11.0 에 LRU 관리와 함께 들어왔고 0.19.0 에 범용 CPU 오프로딩으로 정리됨 |
LMCacheConnectorV1, LMCacheMPConnector |
LMCache 로 CPU·디스크·원격 저장소까지 |
NixlConnector, MooncakeConnector |
다른 엔진(다른 GPU, 다른 서버)으로 KV 를 보냄. 분리 서빙에 씀 |
MultiConnector |
여러 커넥터를 순서대로 |
기본 오프로딩은 두 줄로 켭니다. --kv-offloading-size 는 GiB 단위의 CPU 버퍼 크기이고, 이 값을 주면 native 백엔드(곧 OffloadingConnector)가 켜집니다.
vllm serve <모델> --kv-offloading-size 0.5 --kv-offloading-backend native
세부 설정은 --kv-transfer-config 의 kv_connector_extra_config 로 줍니다. 0.32.0 문서 기준으로 CPU 하나만 쓰는 CPUOffloadingSpec(기본)과 CPU 아래에 파일 시스템 같은 2차 계층을 두는 TieringOffloadingSpec 이 있고, 지우는 순서는 lru 나 arc 를 고릅니다. 기본으로는 프롬프트(프리필) 블록만 내리고 디코드 블록은 내리지 않습니다(offload_prompt_only). GPU 와 CPU 사이 복사는 cudaMemcpyAsync 로 계산과 겹쳐 돈다고 문서는 적습니다.
3. 직접 재 보기
방법
밀려나는 상황을 일부러 만들었습니다.
- GPU KV 를
--kv-cache-memory-bytes 268435456(256MiB)으로 묶어 2,336토큰만 들어가게 했습니다.--max-model-len 2048. - 한 묶음은 다섯 요청입니다. 접두사 A(약 1,600토큰)를 처음 보내고(차가움), 곧바로 한 번 더(GPU 적중), 다른 접두사 B·C 를 보내 A 를 밀어낸 뒤, A 를 다시 보냅니다.
- 오프로딩 없는 서버와
--kv-offloading-size 0.5(약 4,600토큰분) 서버에서 다섯 묶음씩 돌렸습니다. 첫 묶음의 첫 요청은 서버 예열이 섞여 따로 봅니다. - 그 밖의 환경은 2편과 같습니다(8GB급 노트북 GPU, vLLM 0.31.0, Qwen3-Reranker-0.6B, 컨테이너 메모리 한도 4GiB).
결과
| 단계 | 오프로딩 없음(TTFT 중앙, 2~5번째 묶음) | 오프로딩 0.5GiB(같은 기준) |
|---|---|---|
| A 처음(차가움) | 76.8ms | 77.7ms |
| A 바로 다시(GPU 적중) | 14.0ms | 14.3ms |
| B·C 로 밀어내기 | 77.2ms | 78.1ms |
| A 다시(밀려난 뒤) | 77.3ms(다시 계산) | 35.1ms(CPU 에서 불러옴) |
다섯 묶음 전부를 보면 오프로딩 서버의 「A 다시」는 35.3, 35.4, 34.8, 33.3, 35.6ms 로 모두 34ms 안팎이었고, 오프로딩 없는 서버는 76.3~78.0ms 로 차가운 요청과 같았습니다. 맨 첫 요청은 두 서버 모두 104ms 로 예열 몫이 30ms 가까이 붙었습니다.
서버 지표로 무엇이 옮겨졌는지도 확인했습니다.
지표(/metrics) |
값 | 뜻 |
|---|---|---|
vllm:kv_offload_load_bytes_total |
849.6MB(5회) | CPU→GPU 로 되돌린 양. 한 번에 약 170MB = 약 1,480토큰 × 114,688바이트 |
vllm:kv_offload_load_time_total |
0.0673초 | 다섯 번 되돌리는 데 든 시간. 초당 약 12.6GB, 한 번에 13.5ms |
vllm:kv_offload_store_bytes_total |
2.82GB(25회) | GPU→CPU 로 내린 양. 프롬프트 블록은 모두 내려 둔다 |
vllm:kv_offload_store_time_total |
0.221초 | 초당 약 12.8GB |
vllm:prompt_tokens_by_source_total |
계산 24,771 / GPU 적중 7,936 / 외부 7,408 | 「외부」가 CPU 에서 불러온 토큰 |
읽는 법은 이렇습니다.
- 밀려난 접두사의 첫 토큰이 2.2배 빨라졌습니다(77.3ms → 35.1ms). 그래도 GPU 에 남아 있을 때(14.0ms)보다는 21ms 느립니다. 그 차이의 대부분이 170MB 를 옮기는 13.5ms 입니다.
- 이 GPU 에서 KV 를 옮기는 속도는 초당 약 12.6GB 였습니다. 같은 1,480토큰을 다시 계산하는 데는 약 63ms 가 듭니다(77.3ms 에서 GPU 적중 14.0ms 를 뺀 값). 계산 속도가 초당 약 23,000토큰이고 옮기는 속도는 초당 약 110,000토큰분이라, 이 조합에서는 옮기는 쪽이 다섯 배 가깝게 빠릅니다.
- 내리는 일은 공짜가 아닙니다. 다시 쓰이지 않은 블록까지 모두 2.82GB 를 내렸습니다. 복사는 비동기라 이번 측정의 차가운 요청 TTFT 에는 차이가 거의 보이지 않았지만(76.8 대 77.7ms), PCIe 대역과 CPU 메모리를 계속 씁니다.
- CPU 버퍼는 고정 메모리입니다. 0.5GiB 를 주면 그만큼 페이지가 고정되어 컨테이너 메모리 한도에 그대로 잡힙니다. 이번 시험은 4GiB 한도 안에서 하느라 버퍼를 작게 잡았습니다.
4. 언제 이득이 없나
- 모델이 작고 GPU 계산이 빠를수록 이득이 줄어듭니다. 다시 계산하는 시간이 옮기는 시간에 가까워지기 때문입니다. 반대로 큰 모델일수록 토큰당 계산은 늘고 토큰당 KV 바이트는 GQA·MLA 덕분에 덜 늡니다. 「LLM KV 캐시 해부」 3편에서 같은 GPU 로 잰 LMCache 실측(576ms → 48.6ms)이 이번보다 이득이 컸던 것도 입력이 길었기 때문입니다.
- 재사용이 없으면 비용만 남습니다. 모든 프롬프트 블록을 내리므로, 접두사가 다시 오지 않는 부하에서는 PCIe 대역과 고정 메모리만 씁니다.
store_threshold(몇 번 조회된 블록만 내릴지)로 줄일 수 있습니다(미실측). - GPU 에 자리가 넉넉하면 쓸 일이 없습니다. 2편의 기본 서버는 KV 가 40,208토큰이라 같은 시험에서 A 가 밀려나지 않습니다.
5. 분리 서빙: 같은 커넥터로 KV 를 다른 엔진에 보내기
프리필과 디코드를 다른 GPU 에 두는 분리 서빙(disaggregated prefill)은 위 커넥터 구조 위에서 돕니다. 프리필 엔진이 KV 를 만들어 NixlConnector 로 디코드 엔진에 보내고, 디코드 엔진은 받은 KV 로 이어서 생성합니다. vLLM 문서가 꼽는 이유는 둘입니다.
- TTFT 와 ITL 을 따로 조율하기 위해(프리필과 디코드에 다른 병렬화를 줄 수 있음)
- 꼬리 ITL 을 다스리기 위해. 2편에서 본 「긴 입력이 디코드를 멈추는 시간」은 청크 크기로도 줄일 수 있지만, 알맞은 청크 크기를 고르기 어렵다고 문서는 적습니다.
같은 문서는 「분리 서빙은 처리량을 늘리지 않는다」고 분명히 적고, 기능 전체를 실험적이라고 표시합니다. 원조 논문 DistServe(OSDI 2024)는 TTFT·TPOT 목표를 함께 지키는 GPU 당 최대 요청률로 따져 기존 시스템보다 요청 7.4배, 또는 12.6배 엄격한 목표를 보고했습니다(초록 기준). 0.32.0 에는 분리 서빙에서 디코드 쪽이 프리필이 만든 토큰 ID 를 다시 쓰는 기능(kv_transfer_params 의 prompt_token_ids)도 들어 있습니다.
GPU 가 한 장이라 이번에도 vLLM 단독 분리 서빙은 미실측입니다. 같은 GPU 계열 한 장과 1GbE 로 묶은 두 대에서 NVIDIA Dynamo 로 분리 서빙을 해 본 결과(한 장에서 5배, 두 대 사이에서 20배 넘게 느림)는 Dynamo 편과 「토스 ML/AI Infra 준비」 24편에 있습니다. KV 를 옮기는 길이 NVLink·RDMA 처럼 빠를 때만 의미가 있다는 결론은 이번 오프로딩 실측(PCIe 로 초당 12.6GB)과도 같은 방향입니다.
6. 운영에서 가져갈 것
- 긴 문서 여러 개를 번갈아 묻는 서비스(RAG, 코드 저장소 질의, 긴 대화)라면 CPU 오프로딩은 옵션 한 줄로 밀려난 접두사의 TTFT 를 절반 아래로 줄입니다. GPU KV 사용률(
vllm:kv_cache_usage_perc)이 늘 높고 접두사 적중률이 기대보다 낮을 때 먼저 켜 볼 만합니다. - 켠 뒤에는
vllm:prompt_tokens_by_source_total{source="external_kv_transfer"}와vllm:kv_offload_load_time_total로 실제로 불러오고 있는지, 얼마나 걸리는지 확인합니다. - CPU 버퍼 크기는 컨테이너 메모리 한도에 더해 잡습니다. 고정 메모리는 스왑되지 않고, 한도를 넘으면 엔진이 OOM 으로 죽습니다.
7. 확인하지 못한 것
TieringOffloadingSpec의 파일 시스템 계층,arc정책,store_threshold는 미실측입니다.- 높은 동시성에서 오프로딩의 복사가 디코드 처리량에 주는 영향은 미실측입니다. 이번 시험은 요청을 하나씩 보냈습니다.
- vLLM 단독 분리 서빙(
NixlConnector)은 GPU 가 한 장이라 미실측입니다.
참고
- vLLM 문서, KV Offloading Usage Guide, Disaggregated Prefilling(0.32.0): https://docs.vllm.ai/en/latest/features/kv_offloading_usage.html, https://docs.vllm.ai/en/latest/features/disagg_prefill.html
- vLLM 0.11.0, 0.19.0 릴리스 노트(CPU 오프로딩): https://github.com/vllm-project/vllm/releases
- Zhong 외, 「DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving」, OSDI 2024. https://arxiv.org/abs/2401.09670
- 이전 글: LMCache: KV 캐시를 GPU 밖으로, NVIDIA Dynamo