TT Lab
블로그

한국어

시작하기
블로그

블로그

KV 오프로딩과 분리 서빙: 밀려난 접두사를 CPU 에서 다시 불러오기

상태: 초안 (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편에서 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. 직접 재 보기

방법

밀려나는 상황을 일부러 만들었습니다.

결과

단계 오프로딩 없음(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 에서 불러온 토큰

읽는 법은 이렇습니다.

  1. 밀려난 접두사의 첫 토큰이 2.2배 빨라졌습니다(77.3ms → 35.1ms). 그래도 GPU 에 남아 있을 때(14.0ms)보다는 21ms 느립니다. 그 차이의 대부분이 170MB 를 옮기는 13.5ms 입니다.
  2. 이 GPU 에서 KV 를 옮기는 속도는 초당 약 12.6GB 였습니다. 같은 1,480토큰을 다시 계산하는 데는 약 63ms 가 듭니다(77.3ms 에서 GPU 적중 14.0ms 를 뺀 값). 계산 속도가 초당 약 23,000토큰이고 옮기는 속도는 초당 약 110,000토큰분이라, 이 조합에서는 옮기는 쪽이 다섯 배 가깝게 빠릅니다.
  3. 내리는 일은 공짜가 아닙니다. 다시 쓰이지 않은 블록까지 모두 2.82GB 를 내렸습니다. 복사는 비동기라 이번 측정의 차가운 요청 TTFT 에는 차이가 거의 보이지 않았지만(76.8 대 77.7ms), PCIe 대역과 CPU 메모리를 계속 씁니다.
  4. CPU 버퍼는 고정 메모리입니다. 0.5GiB 를 주면 그만큼 페이지가 고정되어 컨테이너 메모리 한도에 그대로 잡힙니다. 이번 시험은 4GiB 한도 안에서 하느라 버퍼를 작게 잡았습니다.

4. 언제 이득이 없나

5. 분리 서빙: 같은 커넥터로 KV 를 다른 엔진에 보내기

프리필과 디코드를 다른 GPU 에 두는 분리 서빙(disaggregated prefill)은 위 커넥터 구조 위에서 돕니다. 프리필 엔진이 KV 를 만들어 NixlConnector 로 디코드 엔진에 보내고, 디코드 엔진은 받은 KV 로 이어서 생성합니다. vLLM 문서가 꼽는 이유는 둘입니다.

같은 문서는 「분리 서빙은 처리량을 늘리지 않는다」고 분명히 적고, 기능 전체를 실험적이라고 표시합니다. 원조 논문 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. 운영에서 가져갈 것

7. 확인하지 못한 것

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다