CPU EC 커넥터 사용 가이드
CPU EC 커넥터 사용 가이드
인코더 캐시(EC)를 다룰 때, GPU에만 얹어 두면 메모리가 부족해질 때가 있어요. ECCPUConnector는 GPU 기반 인코더 캐시에 CPU 티어(tier) 를 더해서, 인코더 출력(encoder_cache[mm_hash])을 공유 /dev/shm mmap 영역으로 내려(offload) 두고, 이후 단계나 이후 요청이 다시 계산하지 않고 재사용하게 해줘요. GPU↔CPU 복사는 풀링된 CUDA 스트림에서 swap_blocks_batch로 모델 계산과 비동기로 일어나요.
ec_connector_extra_config에 ec_enable_nixl: true를 설정하면 여기에 더해 피어투피어(P2P) 전송이 켜져요. 소비자 인스턴스가 생산자 인스턴스의 CPU 티어에서 인코딩을 직접 꺼내서, 로컬에서 다시 계산하지 않아요 — E/PD 분리나 인코더/디코더 인스턴스 공유에 유용하죠.
출처: 공식문서
사전 준비 (Prerequisites)
ECCPUConnector는 V2 모델 러너가 필요해요:VLLM_USE_V2_MODEL_RUNNER=1. 그렇지 않으면 생성 시점에ValueError가 나요.- 로컬 CPU 티어 오프로드(
ec_enable_nixl을 안 두거나false)는 추가 패키지가 필요 없어요. 게이트 꺼진 코드 경로(cpu/connector.py,cpu/scheduler/,cpu/worker/,cpu/common.py)는nixl/zmq/msgspec를 import 하지 않고, 이는 저장소 테스트(tests/v1/ec_connector/unit/test_no_nixl_imports.py)로 강제돼요. - P2P NIXL 모드(
ec_enable_nixl: true)는nixl패키지가 필요해요:uv pip install nixl(requirements/kv_connectors.txt에서nixl==1.3.2로 고정,NixlConnector와 공유). 플랫폼별 설치는 NIXL 저장소를 참고하세요.nixl을 import 할 수 없으면 커넥터가RuntimeError: ec_enable_nixl requires NIXL; install the nixl package or remove ec_enable_nixl from ec_connector_extra_config.를 던져요.
기본 사용법
단일 엔진 인스턴스 안에서 로컬 CPU 티어 오프로드만 쓰는 경우예요.
vllm serve <model> --ec-transfer-config '{
"ec_connector": "ECCPUConnector",
"ec_role": "ec_both",
"ec_connector_extra_config": {"ec_cpu_bytes": 1073741824}
}'
ec_role="ec_both": 같은 프로세스가 CPU 티어로 내리고 다시 불러와요.- 티어는 하나의 mmap 영역(
/dev/shm/vllm_ec_{instance_id}_dp{dp_rank}.mmap)이고, 인스턴스의 모든 TP/PCP 워커가 공유해요. 모든 랭크가 동일한 인코더 출력을 갖고 있으므로 저장(save) 시에는 TP rank 0 / PCP rank 0만 써요. - 항목은
mm_hash로 키잉되고,EmbeddingCache는 공간이 필요해지면 준비 완료되고 핀되지 않은(ready+unpinned) 항목을 FIFO로 퇴출해요. - 배치 저장/로드 각각은 풀링된 CUDA 스트림에서 실행되고, 전송의 end 이벤트가 발생하면(
ECCPUWorker.build_connector_worker_meta→ECCPUScheduler.update_connector_output) 완료가 스케줄러에 보고돼요. 그러면 저장된 항목이 ready로 표시되고 로드된 항목은 unpin 돼요. - 영역은
shutdown()에서/dev/shm에서 unlink 돼요.
P2P NIXL과 함께 쓰기
생산자(Producer) — CPU 티어로 내려 두고 소비자의 읽기를 제공해요.
vllm serve <model> --ec-transfer-config '{
"ec_connector": "ECCPUConnector",
"ec_role": "ec_producer",
"ec_connector_extra_config": {"ec_enable_nixl": true, "ec_cpu_bytes": 1073741824}
}'
ec_role="ec_producer"만으로도 멀티모달 설정에서 mm_encoder_only가 켜져서 vllm_config.is_mm_encoder_only가 True가 돼요(언어 모델, 샘플러, 풀러를 건너뜀). ec_transfer_config와 무관하게 인코더 전용 실행이 필요할 때만 --mm-encoder-only를 추가하세요.
소비자(Consumer) — 요청의 ec_transfer_params에 이름이 있는 인코딩을 먼저 꺼내 오고, 없으면 로컬 인코딩으로 폴백해요.
vllm serve <model> --ec-transfer-config '{
"ec_connector": "ECCPUConnector",
"ec_role": "ec_consumer",
"ec_connector_extra_config": {"ec_enable_nixl": true, "ec_cpu_bytes": 1073741824}
}'
오케스트레이션 흐름 (Orchestration flow)
-
요청이 생산자에서 끝나요.
ECCPUConnector.request_finished()는 CPU 티어에 여전히 상주하는 각mm_hash에 대해 다음을 돌려줘요.{mm_hash: {"metadata": {...}, "peer_host": str, "peer_port": int, "size_bytes": int}}이게
ec_transfer_params(RequestOutput.ec_transfer_params/EngineCoreOutput.ec_transfer_params)로 호출자에게 전달돼요.metadata는 모델이 해당 모달리티에 대해 선언한 플레이스홀더 필드를 담고 있어서, 오케스트레이터가 미디어를 메타데이터 전용 참조로 다시 쓸 수 있어요. 나머지 키는 게시된 인코딩에 대한 커넥터 자신의 핸들이에요.두 절반(metadata와 주소)은 함께 게시되거나 아예 안 게시돼요. 생산자가 서빙할 수 없는
mm_hash— 영역이 꽉 차서 저장되지 않았거나, 저장된 뒤 퇴출된 경우 — 는 빈metadata와 주소 없이 보고돼서, 오케스트레이터가 요청에 미디어를 남겨 두고 소비자가 로컬로 인코딩하게 해요. -
오케스트레이터가 같은
mm_hash로 소비자 인스턴스에 후속 요청을 내고, 생산자의ec_transfer_params를SamplingParams.extra_args["ec_transfer_params"]로 전달해요. -
소비자 쪽에서
ECCPUScheduler.ensure_cache_available()가request.ec_transfer_params를 읽어요. 로컬에 아직 캐시되지 않은 각mm_hash에 대해(peer_host, peer_port)로 ZMQ 세션을 열고XferReq를 보내요.OKXferAck를 받으면 NIXL READ를 내보내서 생산자의 mmap에서 자기 것으로 블록을 직접 끌어와요. READ가 끝날 때까지 요청은 연기(defer)돼요. -
NACK_NOT_READY는 생산자가 인코딩을 선언했지만 GPU→mmap 저장이 아직 안 끝났다는 뜻이에요. 소비자는 실패로 기록하지 않고 in-flight 항목을 해제한 뒤, 다음 스텝에서 read를 다시 요청해요. 그러니 저장이 몇 스텝 늦게 도착해도 재계산이 아니라 지연(latency) 비용만 들게 돼요. -
그 외의 모든 NACK(
NACK_MISSING,NACK_INCOMPAT,NACK_VERSION,NACK_INTERNAL), ack 타임아웃, read 타임아웃, 또는 피어 연결 끊김에서는 소비자가 in-flight 항목을 버리고 그mm_hash에 대해 로컬 인코딩으로 폴백해요 — P2P 실패가 요청을 무기한 막지는 않아요.
프로토콜
- 제어 평면(Control plane): ZMQ. 생산자가
VLLM_EC_SIDE_CHANNEL_HOST:VLLM_EC_SIDE_CHANNEL_PORT에ROUTER소켓을 바인딩하고, 각 소비자는 생산자 피어마다DEALER연결 하나를 열어요. 죽은 피어를 감지하기 위해 ZMQ 하트비팅(2초 간격, 4초 타임아웃, 8초 TTL)을 써요.XferReq/XferAck는msgspecmsgpack 구조체이고,EC_CONNECTOR_VERSION(현재1)으로 버전을 매겨요 — 버전이 다르면 NACK돼요. - 호환성 확인: 모든
XferReq는(vllm_version, model, dtype, block_size_bytes)에 대한 SHA-256 해시를 담아요. 생산자는 해시가 다른 피어를 NACK(NACK_INCOMPAT) 처리해요. - Ack 상태:
OK,NACK_MISSING(생산자가 더는 인코딩을 보유하지 않음),NACK_NOT_READY(보유하지만 저장이 아직 안 끝남),NACK_INCOMPAT,NACK_VERSION,NACK_INTERNAL. 오직NACK_NOT_READY만 재시도 가능해요 —cpu/protocol.py의RETRYABLE_NACKS가 양쪽에서 참조되므로, 분류는 각 호출 지점이 아니라 와이어 어휘와 함께 존재해요. - 데이터 평면(Data plane): NIXL,
UCX백엔드, 소비자 주도의READ— 소비자가 생산자의 등록된 mmap 영역에서 바이트를 직접 끌어오고, 생산자가 밀어주지는 않아요. - 생산자 재시작 복구:
XferAck가 생산자의 NIXL 에이전트 메타데이터를 담아서, 소비자는 새 핸드셰이크 왕복 없이 재시작된 생산자에 대한 READ를 복구할 수 있어요. - 타임아웃: 소비자 XferAck 대기 2초, NIXL read 20초(그 다음에는 퇴출이 아니라 격리(quarantine)되어 최대 60초 동안 중단 불가능한 DMA가 정리되도록 둠). 생산자는 30초 핀 임대 후에 클레임되지 않은 핀 그랜트를 해제해요.
설정 (Configuration)
EC 전송은 --ec-transfer-config(CLI) 또는 VllmConfig의 ec_transfer_config 필드(ECTransferConfig, vllm/config/ec_transfer.py)로 설정해요.
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
ec_connector |
str | None |
None |
커넥터 클래스 이름. "ECCPUConnector"를 쓰세요. |
ec_role |
"ec_producer" | "ec_consumer" | "ec_both" | None |
None |
ec_connector가 설정될 때마다 필요해요. ec_producer는 GPU→CPU 오프로드만, ec_consumer는 CPU→GPU 복원만, ec_both는 같은 프로세스에서 둘 다 해요. |
ec_connector_extra_config |
dict[str, Any] |
{} |
커넥터 전용 설정. ec_enable_nixl 포함 — 아래 ec_connector_extra_config 참조를 보세요. |
engine_id |
str | None |
랜덤 UUID4 | ec_enable_nixl=True일 때 NIXL 에이전트 이름을 정해요. |
ec_connector_module_path |
str | None |
None |
ec_connector가 내장 레지스트리(ECExampleConnector, ECCPUConnector)에 없을 때 아웃오브트리 커넥터를 로드할 파이썬 모듈 경로. |
ec_connector_extra_config 참조
| 키 | 타입 | 필수 | 설명 |
|---|---|---|---|
ec_enable_nixl |
bool |
아니오 (기본 false) |
로컬 CPU 오프로드에 더해 NIXL P2P 전송을 켜요. 생략하거나 false면 NIXL/ZMQ를 import 하지 않아요. 추가 설정은 타입 강제가 없어서 문자열 값도 파싱돼요: "true", "1", "yes"면 켜지고, 그 외에는 켜지지 않아요. |
consumer_ack_timeout_s |
float |
아니오 (기본 2.0) |
소비자가 read를 포기하기 전에 XferAck를 기다리는 시간. 생산자가 자기 스케줄러 스텝에서 XferReq에 응답하므로, 응답 지연은 인코더의 --max-num-batched-tokens에 따라 달라져요. 로드가 많은 인코더에서 이보다 긴 스텝이 돌면, 소비자는 생산자가 곧 승인할 read를 포기하게 돼요. 큰 인코더 배치에서는 이 값을 올리세요. |
ec_cpu_bytes |
int |
예 | 공유 CPU mmap 영역의 총 크기(바이트). 설정하지 않으면 ECCPUConnector가 ValueError를 던져요. 블록 수 = ec_cpu_bytes // block_size_bytes이고, block_size_bytes = hidden_dim * dtype.element_size()예요(hidden_dim은 Qwen3-VL deepstack을 반영: out_hidden_size * (1 + num_deepstack_layers)). |
환경변수
| 변수 | 기본값 | 설명 |
|---|---|---|
VLLM_EC_SIDE_CHANNEL_HOST |
localhost |
생산자의 ZMQ ROUTER 소켓이 바인딩할 호스트. 다중 인스턴스/다중 노드 P2P에서는 라우팅 가능한 주소(예: pod IP)로 설정하세요 — 기본값은 생산자와 소비자가 같은 호스트를 공유할 때만 동작해요. |
VLLM_EC_SIDE_CHANNEL_PORT |
5601 |
같은 ZMQ ROUTER 소켓의 포트. |
둘 다 생산자(ec_role="ec_producer" 또는 "ec_both")에서 ec_enable_nixl=True일 때만 읽어요.
제약 (Limitations)
- 인코딩이 소비되기 전에 CPU 티어에서 퇴출됐을 때 이를 오케스트레이터나 피어 인스턴스에 알릴 메커니즘이 없어요. 소비자는 자기
XferReq가 NACK(NACK_MISSING)된 뒤에야 miss를 발견하고 로컬 재계산으로 폴백해요. - 현재 프로세스 종료 시 mmap 정리는 최선(best-effort)이에요. 생성 프로세스가
ECSharedRegion.cleanup()이 실행되기 전에SIGKILL되면/dev/shm/vllm_ec_*.mmap파일이 새어 나가서 수동으로 제거해야 해요. NixlDataTransport는UCX백엔드를 하드코딩해요.- 재시도된 read는 대상 블록을 해제하고 다음 스텝에서 다시 할당해요. 그래서 생산자의 저장이 느리게 도착하면 소비자는 엔진 스텝마다 준비 완료 항목을 퇴출해서 같은 블록을 되찾아야 해요.
- 로컬 인코딩으로의 폴백은 미디어가 여전히 요청에 있어야 해요. 서빙될 수 있을 때만 인코딩을 선언하면 오케스트레이터가 서빙할 수 없는 것으로 미디어를 다시 쓰지 않게 막아 주지만, 선언 이후 인코딩을 잃은 경우 — 선언과 소비자 read 사이에 퇴출된 경우 — 는 재작성된 요청에 임베딩할 것이 없어서 워커의
sanity_check_mm_encoder_outputs에서 실패해요.ensure_cache_available()은 요청을 연기할 수는 있어도 실패시킬 수는 없으므로, 이걸 깔끔하게 보고하려면 스케줄러 쪽 실패 경로가 필요해요.