KV 오프로딩 사용 가이드
KV 오프로딩 사용 가이드 (KV Offloading Usage Guide)
이 가이드는 프리픽스 캐시를 확장하는 OffloadingConnector 의 설정 방법을 다룹니다. 완료된 KV 블록을 생성되는 대로 더 느리지만 더 큰 계층(CPU 호스트 메모리 + 선택적인 보조 계층)으로 오프로드하고, 오프로드 계층에서 적중한 블록은 요청 시 GPU로 다시 승격시킵니다. GPU와 CPU 간 전송은 DMA(cudaMemcpyAsync)를 사용하며 모델 계산과 함께 비동기로 실행되므로, 오프로딩은 CPU·GPU 코어 오버헤드를 거의 추가하지 않습니다.
참고:
OffloadingConnector는 현재 CUDA, ROCm, XPU만 지원합니다.
출처: 문서
본문
개요 (Overview)
kv_connector_extra_config 의 spec_name 키로 선택할 수 있는 두 가지 스펙이 있습니다.
CPUOffloadingSpec(기본값): 단일 CPU 계층. 완료된 GPU 블록을 고정(pinned) 호스트 메모리에 복사합니다.TieringOffloadingSpec: 다중 계층. CPU 기본 계층 + 하나 이상의 보조 계층을 사용합니다.
GPU에 직접 접근할 수 있는 계층은 CPU 기본 계층뿐입니다. 보조 계층은 GPU 메모리를 읽거나 쓸 수 없으며, 모든 GPU↔보조 계층 전송은 CPU 기본 계층을 경유해 스테이징됩니다.
flowchart LR
GPU <--> CPU["CPU primary tier"]
CPU <--> S0["Secondary tier 0"]
CPU <--> S1["Secondary tier 1"]
CPU <--> SN["..."]
요청별 로드 제어 (Per-request load control)
개별 요청은 kv_transfer_params 에서 max_load_tokens 를 설정해 오프로드된 저장소에서 로드할 토큰 수를 제한할 수 있어요. 이 상한은 GPU 프리픽스 캐시에 이미 있는 토큰을 제외한 나머지 토큰에 적용됩니다. 0 으로 설정하면 해당 요청의 외부 로딩을 비활성화합니다.
{
"kv_transfer_params": {
"max_load_tokens": 0
}
}
GPU 프리픽스 캐시 재사용은 계속 활성화되며, 정렬된 로드 상한을 넘는 토큰은 재계산됩니다. 필드를 생략하면 로딩에 상한이 없어요. 값은 0 이상의 정수여야 하며, 잘못된 값은 무시됩니다. 양수 상한은 구성된 KV 캐시 그룹이 지원하는 경계로 내림 처리됩니다. 오프로딩은 별도로 max_offload_tokens 로 제어하지 않는 한 계속 활성화됩니다.
경고:
max_load_tokens는 실험적이며 변경될 수 있습니다.
kv_load_tiers 는 계속해서 보조 계층을 선택합니다. CPU는 항상 포함됩니다. CPU는 상주 적중을 직접 충족할 수 있고 보조 계층→GPU 로드를 위한 필수 스테이징 계층이기 때문이에요. 따라서 빈 계층 목록은 CPU 적중을 허용하지만 어떤 보조 계층도 조회하지 않습니다.
용어: 청크 (Chunks)
작동 단위는 청크(chunk) 입니다. 토큰 그룹을 다루는 고정 크기의 KV 데이터 조각이에요. 기본적으로 청크는 단일 가속기 블록에 매핑됩니다. 구성 가능한 blocks_per_chunk 파라미터로 더 큰 청크를 만들 수 있고, 이는 호스트 및 보조 계층에 더 큰 I/O를 보냅니다.
단일 계층 설정 (CPU 전용)
vllm serve <model> \
--kv-transfer-config '{
"kv_connector": "OffloadingConnector",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"block_size": 64,
"cpu_bytes_to_use": 1000000000
}
}'
다중 계층 설정 (Multi-Tier)
spec_name 을 "TieringOffloadingSpec" 으로 설정하고 secondary_tiers 목록을 제공하세요. 각 항목은 필수 type 키와 계층별 필드(그리고 트리 밖 계층용 선택적 module_path)가 있는 딕셔너리입니다. 목록은 순서가 중요해요. tier 0을 tier 1보다 먼저 조회합니다. 계층별 키는 보조 계층 을 참고하세요.
vllm serve <model> \
--kv-transfer-config '{
"kv_connector": "OffloadingConnector",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"spec_name": "TieringOffloadingSpec",
"cpu_bytes_to_use": 10737418240,
"block_size": 16,
"eviction_policy": "lru",
"secondary_tiers": [
{
"type": "fs",
"root_dir": "/mnt/kv_cache",
"n_read_threads": 32,
"n_write_threads": 16
}
]
}
}'
kv_connector_extra_config 참조
| 키 | 필수 | 기본값 | 범위 | 참고 |
|---|---|---|---|---|
spec_name |
아니오 | CPUOffloadingSpec |
both | 다중 계층에는 TieringOffloadingSpec 으로 설정합니다. |
cpu_bytes_to_use |
예 | — | both | 모든 워커에 걸쳐(워커별이 아니라) CPU 계층에 예약하는 호스트 메모리 총 바이트입니다. |
block_size |
아니오 | GPU 블록 크기 | both | 오프로드 블록 크기(토큰 단위)입니다. GPU 블록 크기의 배수여야 합니다. blocks_per_chunk 와 상호 배타적입니다. |
blocks_per_chunk |
아니오 | 1 |
both | 오프로드 청크 크기(GPU 블록 단위)입니다. 0보다 커야 합니다. KV 캐시 그룹의 블록 크기가 서로 다른 모델에서 block_size 의 대안으로 사용합니다. |
eviction_policy |
아니오 | lru |
both | 기본 계층 정책: 내장 lru / arc, 또는 커스텀 CachePolicy 이름(커스텀 퇴출 정책 참고). |
cache_policy_module_path |
아니오 | — | both | 내장 레지스트리에 없는 커스텀 CachePolicy 의 Python import 경로입니다. eviction_policy 가 내장이 아니고 CachePolicyFactory 로 미리 등록되지 않은 경우에만 필요합니다(고급). |
store_threshold |
아니오 | 0 |
single-tier | 블록이 오프로드되기 전 최소 조회 횟수입니다. ≥ 2 값은 TieringOffloadingSpec 에서 거부됩니다. |
max_tracker_size |
아니오 | 64000 |
single-tier | 조회 트래커의 최대 항목 수입니다. |
secondary_tiers |
아니오 | [] |
multi-tier | 보조 계층 구성 목록입니다(아래 참고). |
offload_prompt_only |
아니오 | true |
both | true 이면 프롬프트(prefill) 블록만 오프로드하고 decode 블록은 건너뜁니다. |
self_describing_kv_events |
아니오 | false |
both | 옵트인(opt-in)입니다. true 이면서 KV 캐시 이벤트가 활성화된 경우(--kv-events-config 와 enable_kv_cache_events), 커넥터는 자체 기술(self-describing) 블록 단위 BlockStored / BlockRemoved 페이로드(구성 블록 해시, 전체 청크 token_ids, 블록별 block_size, 부모 해시, LoRA + 그룹/캐시 스펙 메타데이터)를 플레이스홀더 폴백 대신 발행하므로 외부 KV 이벤트 소비자가 오프로드된 블록을 인덱싱할 수 있습니다. 이벤트가 활성화되지 않으면 무효(inert)입니다. TieringOffloadingSpec 을 사용하면, 로컬 요청이 이벤트 변환 전에 기본 계층 HIT 을 관찰할 때 CPU 승격이 자체 기술됩니다. 그렇지 않으면 저장된 이벤트가 플레이스홀더를 유지할 수 있고, 이후 HIT 가 제거용 메타데이터를 백필(backfill)할 수 있어요. 제거 대기/재승격 경쟁과 외부 시작 승격도 플레이스홀더를 만들 수 있으므로, 소비자는 알 수 없는 해시의 제거를 무시해야 합니다. 부분적인 반복 꼬리(partial recurrent tail)는 물리 블록 시작부터 꼬리 경계까지 해시 정렬된 부분을 발행합니다. 다른 슬라이딩 윈도우/SSM 청크는 플레이스홀더 폴백을 유지합니다. 청크 모드(block_size > GPU 블록 크기, 또는 blocks_per_chunk > 1)에서는 겹치는 청크가 공유 per-block 해시를 다시 알리므로, 소비자는 반복되는 store/remove 알림을 참조 카운트(deduplicate)해야 합니다. |
spec_module_path |
아니오 | — | both | 내장 레지스트리에 없는 커스텀 OffloadingSpec 의 Python import 경로입니다. spec_name 이 내장이 아닌 경우에만 필요합니다(고급). |
커스텀 퇴출 정책 (Custom Eviction Policies)
eviction_policy 는 내장 lru 와 arc 정책을 미리 등록하는 CachePolicyFactory(vllm/v1/kv_offload/cpu/policies/factory.py)를 통해 해석됩니다.
트리 밖 (권장, Out-of-tree)
자체 패키지에 CachePolicy(vllm/v1/kv_offload/cpu/policies/base.py)를 구현하세요. vLLM 포크나 패치가 필요 없어요. 그리고 kv_connector_extra_config 에서 직접 가리키면 됩니다.
{
"cpu_bytes_to_use": 10737418240,
"eviction_policy": "MyCachePolicy",
"cache_policy_module_path": "my_package.my_module"
}
eviction_policy 는 먼저 내장 레지스트리에서 검사합니다. 등록된 이름이 아니면 vLLM은 cache_policy_module_path 를 import하고 eviction_policy 를 해당 모듈의 클래스 이름으로 조회합니다. 이는 커스텀 OffloadingSpec 에 spec_module_path 가 제공하는 것과 같은 폴백 방식입니다. 서버 시작 전에 import나 등록 호출이 실행될 필요가 없습니다.
친숙한 짧은 이름 등록 (프로세스 내 전용)
vLLM 엔진을 구성하는 프로세스(예: 임베딩 애플리케이션)를 제어한다면, 매 설정마다 모듈 경로를 반복하지 않고 시작 시 한 번 짧은 이름을 등록할 수 있어요.
from vllm.v1.kv_offload.cpu.policies.factory import CachePolicyFactory
CachePolicyFactory.register_cache_policy("my_policy", "my_package.my_module", "MyCachePolicy")
그 다음 kv_connector_extra_config 에서 "eviction_policy": "my_policy" 를 "lru" / "arc" 와 똑같이 설정하면 됩니다. 이는 register_cache_policy 호출을 실행한 프로세스 내에서만 적용됩니다. 서버가 별도 프로세스로 실행되는 경우(예: vllm serve CLI)에는 도움이 되지 않아요. 그 경우 위의 트리 밖 cache_policy_module_path 구성이 유일한 방법입니다.
보조 계층 (Secondary Tiers)
secondary_tiers 의 각 항목은 필수 type 필드와 계층별 필드를 가진 딕셔너리입니다.
파일시스템 및 객체 스토어 계층은 성공적으로 저장한 블록에 대해 해시만 포함한 BlockStored KV 이벤트를 발행할 수 있어요. 두 계층 모두 조대한(granular) 유선 매체 값 STORAGE 를 사용합니다. 매체가 파일시스템과 객체 스토어 저장을 구분하지는 않아요. 위치 의미를 복구하려면 계층 항목에 선택적 locality 필드(LOCAL / REMOTE)를 설정하세요. 소비자에게 그 계층의 블록이 발행 vLLM 인스턴스에 로컬인지 여부를 알려주는 것은 매체가 아니라 이 필드입니다. 계층 항목에서 enable_kv_events: true 를 설정해 옵트인하고, 이벤트는 KV 캐시 이벤트가 --kv-events-config 로 전역 활성화된 경우에만 발행됩니다.
선택적 locality 계층 필드를 LOCAL 또는 REMOTE 로 설정해 발행 vLLM 인스턴스 기준 계층의 저장 위치를 설명하세요. LOCAL 은 해당 인스턴스에 로컬인 스토리지를, REMOTE 는 로컬이 아닌 스토리지를 나타냅니다. 설정을 생략하면 locality는 미지정입니다. vLLM은 계층 유형에서 이를 추론하지 않으므로 obj 계층이 암시적으로 REMOTE 가 되지는 않아요. KV 이벤트는 계층이 명시적으로 구성한 경우에만 locality 를 포함합니다. 이 메타데이터는 계층 속성을 설명하는 것이며, 소비자가 이미 그 블록으로 요청을 라우팅할 수 있음을 의미하지는 않습니다.
파일시스템 (FS)
파일시스템 계층(type: "fs")은 블록을 파일시스템 디렉터리에 씁니다.
| 키 | 필수 | 기본값 | 참고 |
|---|---|---|---|
type |
예 | — | fs 여야 합니다. |
root_dir |
예 | — | 기본 디렉터리입니다. vLLM은 그 아래에 하위 디렉터리를 생성합니다(디스크 레이아웃 참고). |
n_read_threads |
아니오 | 16 |
읽기 우선 I/O 스레드(로드 경로)입니다. |
n_write_threads |
아니오 | 16 |
쓰기 우선 I/O 스레드(저장 경로)입니다. |
enable_kv_events |
아니오 | false |
성공적으로 저장한 블록에 대해 BlockStored KV 이벤트(매체 STORAGE)를 발행합니다. KV 캐시 이벤트가 전역 활성화되어 있어야 합니다. |
locality |
아니오 | unspecified | 발행 vLLM 인스턴스 기준 LOCAL 또는 REMOTE 입니다. 명시적으로 구성된 경우에만 계층의 KV 이벤트에 포함됩니다. |
각 스레드 그룹은 자체 큐를 선호하지만 기본 큐가 비어 있으면 다른 큐에서 가져옵니다. 따라서 쓰기 또는 읽기 폭주가 있어도 우선순위가 낮은 큐가 대기 상태로 방치되지 않습니다. 총합을 스토리지의 유효 동시성에 맞게 조정하세요.
디스크 레이아웃 (On-Disk Layout)
root_dir 아래에서 vLLM은 <model>_<digest> 하위 디렉터리를 생성합니다. 여기서 <model> 은 / 를 _ 로 바꾼 모델 이름이고(따라서 meta-llama/Llama-3-8B 같은 HuggingFace ID가 중첩되지 않음), <digest> 는 실행 구성(모델, 블록 크기, 병렬성, dtype 등)에서 파생된 짧은 SHA256 접두사입니다. 같은 구성으로 실행한 경우 같은 하위 디렉터리를 공유하고, 다른 구성의 실행은 같은 root_dir 아래에 충돌 없이 나란히 존재합니다.
그 하위 디렉터리 안에서 블록은 해시-접두사 하위 디렉터리로 샤딩되어 디렉터리 fan-out을 제한합니다.
<root_dir>/
<model>_<digest>/
config.json
<model>_<digest>_r<rank>/
<hhh>/ # first 3 hex chars of the block hash
<hh>_g<group_idx>/ # next 2 hex chars + KV cache group index
<hash_hex>.bin # full block hash (in hex)
config.json 은 실행(블록 크기, KV 그룹 수 등)을 기록하며 첫 시작 시 작성됩니다. 각 랭크는 자신의 _r<rank> 형제 디렉터리에 블록을 쓰므로 여러 랭크가 안전하게 같은 root_dir 을 공유할 수 있습니다.
프로세스 간 공유 (Cross-Process Sharing)
같은 root_dir 을 사용하는 여러 vLLM 인스턴스 간 KV 캐시 공유(예: 공유 PVC를 통한)는 기본적으로 동작합니다. NONE_HASH(블록 콘텐츠 해시의 체인-해시 시드)가 고정 기본 시드에서 파생되므로, 동일한 토큰 콘텐츠는 인스턴스 간에 동일한 블록 파일명을 만듭니다. 대신 커스텀 공유 시드를 사용하려면 모든 인스턴스에 PYTHONHASHSEED 환경 변수를 같은 값으로 설정하세요.
예외는 --prefix-caching-hash-algo 의 비암호화 xxhash 및 xxhash_cbor 값입니다. 이들은 프로세스별로 NONE_HASH 를 무작위로 시드해 시드를 예측 불가능하게 유지합니다. 이 알고리즘들로 인스턴스 간 캐시를 공유하려면 모든 인스턴스에 같은 PYTHONHASHSEED 를 설정해야 합니다.
PYTHONHASHSEED=<shared-value> vllm serve ...
객체 스토어 (OBJ)
객체 스토어 계층(type: "obj")은 NIXL OBJ 백엔드를 통해 S3 호환 객체 스토어로 블록을 오프로드합니다.
| 키 | 필수 | 기본값 | 참고 |
|---|---|---|---|
type |
예 | — | obj 여야 합니다. |
store_config |
예 | — | 객체 스토어 연결 파라미터입니다(아래 참고). |
prefix |
아니오 | "" |
모든 객체 키에 앞에 붙는 키 접두사입니다. |
io_threads |
아니오 | 4 |
NIXL OBJ 백엔드 I/O 스레드 수입니다. |
enable_kv_events |
아니오 | false |
성공적으로 저장한 블록에 대해 BlockStored KV 이벤트(매체 STORAGE)를 발행합니다. KV 캐시 이벤트가 전역 활성화되어 있어야 합니다. |
locality |
아니오 | unspecified | 발행 vLLM 인스턴스 기준 LOCAL 또는 REMOTE 입니다. 명시적으로 구성된 경우에만 계층의 KV 이벤트에 포함됩니다. |
store_config 필드:
| 키 | 필수 | 기본값 | 참고 |
|---|---|---|---|
bucket |
예 | — | 버킷 이름입니다. |
endpoint_override |
예 | — | 객체 스토어 엔드포인트 호스트입니다. URL scheme는 scheme 로 별도 설정합니다. |
scheme |
아니오 | http |
http 또는 https 입니다. |
access_key, secret_key, session_token |
아니오 | "" |
명시적 자격 증명입니다. 비워두면 NIXL OBJ 플러그인은 AWS SDK 기본 자격 증명 제공자 체인(IAM 역할, 환경 변수, 자격 증명 파일)을 폴백하며, 이는 Kubernetes에서 워크로드 아이덴티티 auth를 가능하게 합니다. |
region |
아니오 | "" |
엔드포인트가 필요하면 버킷 리전입니다. |
ca_bundle |
아니오 | "" |
TLS 검증용 CA 번들 경로입니다. |
객체 키는 파일시스템 계층과 같은 실행 구성 다이제스트 스킴(디스크 레이아웃 참고)을 따르며 선택적 prefix 아래에 저장됩니다. 프로세스 간 공유 동작이 공유 버킷에도 적용되므로, 버킷을 공유하는 인스턴스는 동일 콘텐츠에 동일 키를 만듭니다. 커스텀 시드를 원하면 공유 PYTHONHASHSEED 를 설정하세요. 시작 시 계층은 객체 스토어 연결을 탐색하고, 버킷에 연결할 수 없으면 구성 오류로 빠르게 실패합니다.
P2P (P/D 포함)
P2P 계층(type: "p2p")은 NIXL을 통해 RDMA로 vLLM 인스턴스 간에 완료된 KV 블록을 공유합니다. 각 인스턴스는 host:port 에 제어 소켓을 바인딩하고 피어와 직접 블록을 교환합니다. 공유 파일시스템이 필요 없어요.
피어가 블록을 교환하려면 블록 콘텐츠 해시가 인스턴스 간에 일치해야 합니다(프로세스 간 공유 참고). 이는 결정적 NONE_HASH 시드 덕분에 기본적으로 동작하므로 PYTHONHASHSEED 설정은 선택 사항입니다. 설정한다면 모든 노드에서 같은 값이어야 해요. 각 피어의 유효 시드는 커넥트 핸드셰이크 중 검증됩니다. 다른 시드를 알리는 피어는 거부됩니다. xxhash / xxhash_cbor 알고리즘에서는 시드가 프로세스별로 무작위이므로 모든 피어에 PYTHONHASHSEED 를 설정해야 하며, 그렇지 않으면 핸드셰이크가 거부합니다.
| 키 | 필수 | 기본값 | 참고 |
|---|---|---|---|
type |
예 | — | p2p 여야 합니다. |
host |
아니오 | $VLLM_P2P_SIDE_CHANNEL_HOST (localhost) |
제어 소켓이 바인딩하는 주소이며, 피어가 다시 다이얼하는 아이덴티티로 그대로 사용됩니다. 생략하면 아래 환경 변수에서 해석됩니다. localhost 기본값은 루프백만 바인딩합니다. 크로스-호스트 P2P를 위해서는 반드시 노드의 라우팅 가능한 IP로 설정해야 합니다(아래 참고). |
port |
아니오 | $VLLM_P2P_SIDE_CHANNEL_PORT (5710) |
제어 소켓의 기본 포트입니다. 피어에서 도달 가능해야 합니다. 실제 바인딩 포트는 base + data_parallel_index 입니다(DP 복제당 하나의 소켓). 생략하면 아래 환경 변수에서 기본값을 해석합니다. |
backends |
아니오 | ["UCX"] |
NIXL 전송 백엔드입니다. 사용 가능한 백엔드와 선택 지침은 NixlConnector 사용 가이드 를 참고하세요. |
num_threads |
아니오 | 4 |
NIXL 에이전트 워커 스레드입니다. backends 가 UCX 전용일 때만 사용되며, 비-UCX 백엔드가 요청되면 무시됩니다. |
unbound_store_timeout_s |
아니오 | 60 |
프로듀서가 아직 가져가지 않은 컨슈머를 위해 저장된 블록을 보유하는 시간(초)입니다. 프리필이 기본값보다 오래 걸리는 배포에서는 이 값을 올리세요. 블록은 이 기간 내내 기본 계층 CPU 슬롯을 고정합니다. 만료되면 늦은 fetch는 단일 왕복에서 거부되므로 컨슈머는 즉시 로컬 프리필로 폴백합니다. |
backends 와 num_threads 옵션은 NixlConnector 에서 사용하는 조건부 로직을 반영합니다. 비-UCX 백엔드가 구성되면 NIXL은 backends=... 로 초기화되고, 그렇지 않으면 구성된 num_threads 를 가진 UCX 전용 에이전트로 폴백합니다. 이렇게 하면 P2P 계층이 같은 프로세스에서 실행되는 메인 NixlConnector 와 다른 전송(예: MOONCAKE, GDS_MT, LIBFABRIC)을 사용할 수 있습니다.
프로듀서는 컨슈머의 FetchMsg 가 도착할 때까지 요청의 블록을 주차시키고, unbound_store_timeout_s 내에 도착하지 않으면 블록을 해제해 기본 계층 슬롯을 계속 고정하지 않도록 합니다. 프리필이 기본값보다 합법적으로 오래 걸릴 때 올리세요.
vllm serve <model> \
--kv-transfer-config '{
"kv_connector": "OffloadingConnector",
"kv_role": "kv_both",
"kv_connector_extra_config": {
"spec_name": "TieringOffloadingSpec",
"secondary_tiers": [
{
"type": "p2p",
"unbound_store_timeout_s": 180
}
]
}
}'
환경 변수 (Environment Variables)
각 secondary_tiers 항목에 host / port 를 넣는 대신, 배포 시점에 환경 변수로 한 번 설정하세요(VLLM_NIXL_SIDE_CHANNEL_HOST / VLLM_NIXL_SIDE_CHANNEL_PORT 를 미러링). 명시적 host / port 구성 키가 있으면 우선합니다.
VLLM_P2P_SIDE_CHANNEL_HOST(기본localhost): P2P 제어 소켓이 바인딩하는 주소입니다. 바인딩 주소와 피어가 다시 다이얼하는 아이덴티티로 그대로 사용됩니다. 자동 감지가 없어요(VLLM_NIXL_SIDE_CHANNEL_HOST를 미러링). 기본값은 루프백 인터페이스만 바인딩하므로 다른 호스트의 피어가 도달할 수 없습니다. 크로스-호스트 P2P 배포에서는vllm serve를 실행하기 전에 반드시 노드의 라우팅 가능한 IP(예: pod IP)로 명시적으로 설정하세요. 그렇지 않으면 원격 피어가 연결에 실패합니다. NIXL 에이전트 이름은 별도의 프로세스별 식별자이므로 같은host:port를 공유하는 피어는 결코 충돌하지 않습니다.VLLM_P2P_SIDE_CHANNEL_PORT(기본5710): P2P 제어 소켓의 기본 포트입니다. 실제 바인딩 포트는VLLM_P2P_SIDE_CHANNEL_PORT + data_parallel_index입니다. DP 복제당 하나의 소켓으로 NIXL과 일치합니다(DP=1이면 오프셋 0). 피어의 포트는kv_transfer_params의remote_port로 전달됩니다. DP 랭크를 선택하는 라우터/EPP(예:X-data-parallel-rank헤더)는remote_port = base + rank를 계산합니다. DP-인덱스 오프셋은 한 배포 내에서 복제를 구분합니다. 같은 호스트에 함께 있는 두 배포(프리필러와 디코더)는 바인드 충돌을 피하기 위해 여전히 서로 다른 기본 포트(예: 디코더 기본5711)가 필요합니다.
오케스트레이션 계층 프로토콜 (Orchestration-Layer Protocol)
P2P 계층은 어떤 피어에서 가져올지 결정하지 않습니다. 그것은 오케스트레이션 계층(라우터/EPP와 그 스케줄러)의 몫이에요. 오케스트레이터는 요청의 kv_transfer_params 딕셔너리를 통해 모든 전송을 주도합니다. 요청의 역할을 선택하고 고유 트랜잭션 ID를 할당하며 원격 피어의 주소를 제공합니다. 모든 블록 조회, 해시 매칭, NIXL 전송은 아래 계층 수준에서 일어나고, 오케스트레이터는 올바른 역할 키를 설정하고 허용된 조합을 강제하기만 합니다.
모든 vLLM 인스턴스는 대칭적인 피어 입니다. 요청별로 컨슈머(로컬 계산 대신 원격 피어의 CPU 캐시에서 KV 블록을 가져옴) 또는 프로듀서(자체 CPU 캐시에서 원격 컨슈머에게 블록 제공), 또는 같은 세션에서 서로 다른 요청에 대해 둘 다로 동작합니다. 역할은 아래 키에 의해 요청별로 선택되며, 고정된 프리필러/디코더 프로세스는 없습니다.
세 가지 역할 키가 정의되며 각각 하위 딕셔너리에 매핑됩니다. 모두 선택 사항이며, 아무 것도 없는 요청은 계층을 로컬 CPU 캐시로만 사용합니다.
각 키는 이 피어가 전송하는 원격 상대방 을 나타냅니다(이 피어 자신의 역할이 아니라). 따라서 이름은 "내가 전송하는 원격 ___" 로 읽힙니다.
| 키 | 설정 대상 | 값 필드 | 의미 |
|---|---|---|---|
remote_decoder |
prefill 프로듀서 요청 | kv_request_id |
피어가 KV를 계산해 원격 디코더가 가져갈 수 있도록 CPU 캐시에 유지합니다. |
remote_prefiller |
decode 컨슈머 요청 | kv_request_id, remote_host, remote_port |
피어가 주어진 주소의 원격 프리필러에서 KV를 가져옵니다(고전적 P/D 분리). |
remote_kv_source |
P2P 컨슈머 요청 | kv_request_id, remote_host, remote_port |
피어가 원격 소스가 현재 CPU 캐시에 보유한 블록을 조회해 가져옵니다. |
필드 의미:
kv_request_id(str): 오케스트레이터가 할당하고 전송에 관여하는 모든 피어에 푸시하는 고유 트랜잭션 ID입니다. 조회, fetch, transfer-done 메시지를 연관 짓는 데 사용됩니다. 프로듀서는 암시적입니다. 해당 ID에 대해 자체 CPU 캐시에 현재 보유한 블록 해시를 제공합니다.remote_host(str): 조회할 원격 피어 제어 소켓의 IP/호스트명입니다. 반드시 피어의 라우팅 가능한 노드 IP여야 합니다(환경 변수 참고).remote_port(int): 선택한 DP 랭크에 대해 피어가 바인딩한 제어 소켓 포트, 즉base + data_parallel_index입니다.
허용 및 금지 조합:
remote_decoder+remote_kv_source는 유일하게 허용되는 다중 키 조합입니다. prefill 프로듀서가 같은 요청에 대해 P2P 컨슈머로도 동작할 수 있어요. 소스에서 캐시된 블록을 가져와 프리픽스 프리필을 건너뛰면서도, 자체 계산 블록을 다운스트림 디코더에 계속 제공하는 방식입니다.- 금지:
remote_prefiller+remote_decoder(모순된 역할),remote_prefiller+remote_kv_source(경쟁하는 두 fetch 소스), 그리고 세 키 전부.
최소 예시(요청의 kv_transfer_params 에 나타나는 값):
# Prefill producer — compute and keep KV for a remote decoder to pull
kv_transfer_params = {"remote_decoder": {"kv_request_id": "<unique-transfer-id>"}}
# Decode consumer — pull KV from a specific prefiller (classic P/D)
kv_transfer_params = {
"remote_prefiller": {
"kv_request_id": "<unique-transfer-id>",
"remote_host": "<prefiller-node-ip>",
"remote_port": 5710,
}
}
# P2P consumer — pull whatever the source already has cached
kv_transfer_params = {
"remote_kv_source": {
"kv_request_id": "<unique-transfer-id>",
"remote_host": "<source-node-ip>",
"remote_port": 5710,
}
}
오케스트레이터가 위 키를 설정한 후 P2P(또는 P/D) 풀의 런타임 핸드셰이크:
- 양쪽 피어는 이미 제어 소켓에 리스너 스레드를 갖고 있습니다(환경 변수 참고).
- Lookup. 컨슈머의 티어링 매니저가 블록별 조회를 수행합니다. P2P 모드에서 티어는
None을 반환하고 키를 등록합니다.on_schedule_end에서 컨슈머는 요청별, 스텝별로 피어에게 하나의 LookupMsg(LookupMsg,kv_request_id+ 블록 해시)를 보냅니다. - 프로듀서는 그 해시를 자체 로컬 CPU 캐시와 매칭하고 적중 블록 해시를 실은 LookupRespMsg(
LookupRespMsg)로 응답합니다. - Resolve. 재시도된 조회는 이제 hit / miss / in-flight를 반환합니다. 컨슈머는 적중에 대해서만
submit_load를 호출하고, 적중에 대해서만 CPU 슬롯을 할당합니다. - 컨슈머는 FetchMsg(
FetchMsg,kv_request_id, 블록 해시, 대상 블록 인덱스)를 보냅니다. - 프로듀서는 NIXL WRITE 전송을 수행하고 성공 상태를 실은 TransferDone 을 보냅니다.
get_finished에서 적중은 일반 캐시 적중으로 GPU에 로드되고, 미스는 엔진이 재계산합니다.
고전적 P/D 모드(remote_prefiller 설정, remote_kv_source 없음)에서는 조회 단계(2~4)가 생략됩니다. decode 컨슈머는 프리필러가 요청의 모든 블록을 보유한다고 가정하므로 모든 블록 lookup() 이 즉시 hit를 반환하고, 컨슈머는 5단계의 FetchMsg 로 바로 점프합니다. LookupMsg / LookupRespMsg 왕복은 피어가 어떤 블록을 캐시했는지 미리 알지 못하는 P2P 모드에서만 일어납니다.
트리 밖 보조 계층 (Out-of-Tree Secondary Tiers)
자체 패키지에 SecondaryTierManager(vllm/v1/kv_offload/tiering/base.py)를 구현하세요. vLLM 포크나 패치가 필요 없습니다. 그리고 티어 구성을 직접 가리키면 됩니다.
{
"spec_name": "TieringOffloadingSpec",
"cpu_bytes_to_use": 10737418240,
"secondary_tiers": [
{
"type": "MyCustomTier",
"module_path": "my_package.my_module",
"custom_param": "value"
}
]
}
type 은 먼저 내장 레지스트리에서 검사합니다. 등록된 이름이 아니면 vLLM은 module_path 를 import하고 type 을 해당 모듈의 클래스 이름으로 조회합니다.
튜닝 팁 (Tuning Tips)
cpu_bytes_to_use: 더 큰 CPU 계층은 느린 보조 계층으로 가는 왕복을 줄이고 적중률을 높입니다. 값은 워커 전체 합계이며 워커별이 아닙니다. 호스트의 나머지 워크로드를 위해 여유를 남겨 두세요.- 단일 계층(CPU 전용) 설정에서는
cpu_bytes_to_use를 집계 GPU KV 캐시보다 크게 설정하세요. 오프로딩이 즉시 이루어지므로 더 작은 CPU 계층은 GPU가 이미 보유한 것을 그대로 미러링할 뿐 적중률을 높이지 않습니다. block_size/blocks_per_chunk: 더 큰 오프로드 청크는 블록별 부기 오버헤드를 줄이지만 조회의 세분성을 높입니다.- FS 스레드 수:
n_read_threads와n_write_threads를 스토리지가 견딜 수 있는 병렬성에 맞게 조정하세요. 읽기는 프리필 경로에서 지연 시간에 민감하므로 프리필 적중률이 높으면 읽기 스레드를 더 많이 두는 것이 좋습니다. - 실행 간
root_dir공유: 같은 모델,block_size, 병렬성 배치, dtype을 가진 실행은 같은<digest>하위 디렉터리의 파일을 공유합니다. 이 중 하나라도 바뀌면 새 하위 디렉터리가 생기고, 이전 것은 고아가 되지만 무해합니다. 디스크를 되찾으려면 삭제하세요.
요청별 선택적 오프로드 (Per-Request Selective Offload)
개별 요청은 요청의 kv_transfer_params 에서 max_offload_tokens 를 설정해 오프로드 가능한 토큰 수에 상한을 둘 수 있어요. 요청의 처음 max_offload_tokens 토큰만 오프로드되고, 그 이후의 블록은 저장 경로에서 건너뜁니다. 알려진 프리픽스(예: 시스템 프롬프트나 공유 컨텍스트)는 캐시할 가치가 있지만 이후의 요청별 토큰은 그렇지 않을 때 유용합니다.
| 키 | 타입 | 참고 |
|---|---|---|
max_offload_tokens |
0 이상의 int |
이 요청에 오프로드할 토큰의 상한입니다. 0 은 요청의 오프로드를 완전히 비활성화합니다. 키를 생략하거나 None 으로 설정하면 상한이 없습니다. int 가 아니거나, 음수이거나, bool 값은 경고와 함께 거부되고 상한 없음으로 처리됩니다. |
참고:
max_offload_tokens는 실험적이며 변경될 수 있습니다.
예시(OpenAI 호환 completions 요청):
{
"model": "<model>",
"prompt": "...",
"kv_transfer_params": {
"max_offload_tokens": 1024
}
}
추가 자료 (Further Reading)
- vLLM 블로그: KV Offloading Connector — 동기(motivation), 아키텍처(DMA 기반 비동기 전송), 벤치마크(TTFT 및 처리량)를 다룹니다.