CPU EC Connector Usage Guide¶
ECCPUConnector extends the GPU-based encoder cache with a CPU tier: it offloads encoder outputs (encoder_cache[mm_hash]) to a shared /dev/shm mmap region so later steps and later requests reuse them instead of recomputing. GPU↔CPU copies run on pooled CUDA streams via swap_blocks_batch, asynchronously with model compute.
Setting ec_enable_nixl: true in ec_connector_extra_config additionally enables peer-to-peer (P2P) transfer: a consumer instance pulls an encoding directly out of a producer instance's CPU tier over NIXL instead of recomputing it locally — for E/PD disaggregation or encoder/decoder-instance sharing.
Prerequisites¶
ECCPUConnectorrequires the V2 model runner:VLLM_USE_V2_MODEL_RUNNER=1. It raisesValueErrorat construction otherwise.- Local CPU-tier offload (
ec_enable_nixlunset orfalse) needs no extra packages — the gate-off code path (cpu/connector.py,cpu/scheduler/,cpu/worker/,cpu/common.py) imports nonixl/zmq/msgspec, enforced by a repo test (tests/v1/ec_connector/unit/test_no_nixl_imports.py). - P2P NIXL mode (
ec_enable_nixl: true) requires thenixlpackage:uv pip install nixl(pinned tonixl==1.3.2inrequirements/kv_connectors.txt, shared withNixlConnector). Refer to the NIXL repository for platform-specific installation. Ifnixlisn't importable, the connector raisesRuntimeError: ec_enable_nixl requires NIXL; install the nixl package or remove ec_enable_nixl from ec_connector_extra_config.
Basic Usage¶
Local CPU-tier offload only, within a single engine instance:
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": the same process offloads to and reloads from the CPU tier.- The tier is one mmap region (
/dev/shm/vllm_ec_{instance_id}_dp{dp_rank}.mmap) shared by every TP/PCP worker of the instance; only TP rank 0 / PCP rank 0 writes on save, since all ranks hold identical encoder output. - Entries are keyed by
mm_hash;EmbeddingCacheevicts ready+unpinned entries FIFO when space is needed. - Each batched save/load runs on a pooled CUDA stream; completion is reported to the scheduler once the transfer's end event fires (
ECCPUWorker.build_connector_worker_meta→ECCPUScheduler.update_connector_output), which marks saved entries ready and unpins loaded ones. - The region is unlinked from
/dev/shminshutdown().
Usage With P2P NIXL¶
Producer — offloads to its CPU tier and serves reads from consumers:
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" alone already enables mm_encoder_only on the multimodal config, so vllm_config.is_mm_encoder_only is True (skips the language model, sampler, and pooler); add --mm-encoder-only only if you need encoder-only execution independent of ec_transfer_config.
Consumer — pulls encodings named in a request's ec_transfer_params before falling back to local encoding:
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¶
- A request finishes on the producer.
ECCPUConnector.request_finished()returns, for eachmm_hashstill resident in its CPU tier:
surfaced to the caller as ec_transfer_params (RequestOutput.ec_transfer_params / EngineCoreOutput.ec_transfer_params). metadata carries the placeholder fields the model declares for the modality, for an orchestrator that rewrites the media into a metadata-only reference; the remaining keys are the connector's own handle on the published encoding.
The two halves are published together or not at all. An mm_hash the producer cannot serve — never saved because the region was full, or evicted since — is reported with empty metadata and no address, so an orchestrator leaves the media on the request and the consumer encodes it locally. 2. The orchestrator issues a follow-up request with the same mm_hash to a consumer instance, passing the producer's ec_transfer_params through SamplingParams.extra_args["ec_transfer_params"]. 3. On the consumer, ECCPUScheduler.ensure_cache_available() reads request.ec_transfer_params. For each mm_hash not already cached locally, it opens a ZMQ session to (peer_host, peer_port), sends an XferReq, and on an OK XferAck issues a NIXL READ that pulls the blocks straight from the producer's mmap into its own. The request is deferred until the READ completes. 4. A NACK_NOT_READY means the producer announced the encoding but its GPU→mmap save has not landed yet. The consumer releases the in-flight entry without recording a failure and re-requests the read on a later step, so a save that lands a few steps late costs latency rather than a recompute. 5. On any other NACK (NACK_MISSING, NACK_INCOMPAT, NACK_VERSION, NACK_INTERNAL), ack timeout, read timeout, or peer disconnect, the consumer discards the in-flight entry and falls back to local encoding for that mm_hash — a P2P failure never blocks the request indefinitely.
Protocol¶
- Control plane: ZMQ. The producer binds a
ROUTERsocket onVLLM_EC_SIDE_CHANNEL_HOST:VLLM_EC_SIDE_CHANNEL_PORT; each consumer opens oneDEALERconnection per producer peer, with ZMQ heartbeating (2s interval, 4s timeout, 8s TTL) to detect a dead peer.XferReq/XferAckaremsgspecmsgpack structs, versioned byEC_CONNECTOR_VERSION(currently1) — a version mismatch is NACKed. - Compatibility check: every
XferReqcarries a SHA-256 hash over(vllm_version, model, dtype, block_size_bytes); the producer NACKs (NACK_INCOMPAT) any peer whose hash differs. - Ack statuses:
OK,NACK_MISSING(the producer no longer holds the encoding),NACK_NOT_READY(held, but its save has not landed),NACK_INCOMPAT,NACK_VERSION,NACK_INTERNAL. OnlyNACK_NOT_READYis retryable —RETRYABLE_NACKSincpu/protocol.pyis what both ends consult, so the classification lives with the wire vocabulary rather than at each call site. - Data plane: NIXL,
UCXbackend, consumer-initiatedREAD— the consumer pulls bytes directly out of the producer's registered mmap region; the producer never pushes. - Producer restart recovery: the
XferAckcarries the producer's NIXL agent metadata, so a consumer can recover a READ against a restarted producer without a fresh handshake round-trip. - Timeouts: consumer XferAck wait 2s; NIXL read 20s (then quarantined — not evicted — for up to 60s to let an unabortable DMA settle); the producer releases an unclaimed pinned grant after a 30s pin lease.
Configuration¶
EC transfer is configured via --ec-transfer-config (CLI) or the ec_transfer_config field of VllmConfig (ECTransferConfig, vllm/config/ec_transfer.py):
| Field | Type | Default | Description |
|---|---|---|---|
ec_connector | str \| None | None | Connector class name. Use "ECCPUConnector". |
ec_role | "ec_producer" \| "ec_consumer" \| "ec_both" \| None | None | Required whenever ec_connector is set. ec_producer offloads GPU→CPU only, ec_consumer reloads CPU→GPU only, ec_both does both in the same process. |
ec_connector_extra_config | dict[str, Any] | {} | Connector-specific settings, including ec_enable_nixl — see ec_connector_extra_config Reference. |
engine_id | str \| None | random UUID4 | Names the NIXL agent when ec_enable_nixl=True. |
ec_connector_module_path | str \| None | None | Python module path to load an out-of-tree connector from, when ec_connector isn't in the built-in registry (ECExampleConnector, ECCPUConnector). |
ec_connector_extra_config Reference¶
| Key | Type | Required | Description |
|---|---|---|---|
ec_enable_nixl | bool | No (default false) | Enables NIXL P2P transfer in addition to local CPU offload. Omitted or false imports no NIXL/ZMQ. Extra config is not type coerced, so a string value is parsed: "true", "1", "yes" enable it, anything else does not. |
consumer_ack_timeout_s | float | No (default 2.0) | How long a consumer waits for an XferAck before giving up on a read. The producer answers XferReqs from its own scheduler step, so its reply latency scales with the encoder's --max-num-batched-tokens: a loaded encoder whose steps run longer than this makes consumers abandon reads the producer is about to grant. Raise it for large encoder batches. |
ec_cpu_bytes | int | Yes | Total size, in bytes, of the shared CPU mmap region. ECCPUConnector raises ValueError if unset. Block count = ec_cpu_bytes // block_size_bytes, where block_size_bytes = hidden_dim * dtype.element_size() (hidden_dim accounts for Qwen3-VL deepstack: out_hidden_size * (1 + num_deepstack_layers)). |
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
VLLM_EC_SIDE_CHANNEL_HOST | localhost | Host the producer's ZMQ ROUTER socket binds to. Set to a routable address (e.g. the pod IP) for multi-instance/multi-node P2P — the default only works when producer and consumer share a host. |
VLLM_EC_SIDE_CHANNEL_PORT | 5601 | Port for the same ZMQ ROUTER socket. |
Both are read only when ec_enable_nixl=True on a producer (ec_role="ec_producer" or "ec_both").
Limitations¶
- No mechanism to notify an orchestrator or peer instance when an encoding is evicted from the CPU tier before it's consumed. A consumer only discovers a miss when its
XferReqis NACKed (NACK_MISSING) and falls back to local recompute. - At the moment, Mmap cleanup on process shutdown is best-effort: if the creating process is
SIGKILLed beforeECSharedRegion.cleanup()runs, the/dev/shm/vllm_ec_*.mmapfile leaks and must be removed manually. NixlDataTransporthardcodes theUCXbackend.- A retried read releases its destination blocks and re-allocates them on the next step, so a producer whose save is slow to land makes the consumer evict ready entries to win the same blocks back, once per engine step.
- Falling back to local encoding requires the media to still be on the request. Announcing an encoding only when it can be served keeps an orchestrator from rewriting the media away for one that cannot, but an encoding lost after it was announced — evicted between the announcement and the consumer's read — leaves a rewritten request with nothing to embed, and it fails in the worker's
sanity_check_mm_encoder_outputs.ensure_cache_available()can only defer a request, not fail it, so reporting this cleanly needs a scheduler-side failure path.