KV connectors
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
A KV connector moves KV blocks in and out of vLLM; its scheduler half plans each step and its worker half moves the bytes.
The idea#
The KV cache keeps the keys and values of earlier tokens, so decode does not compute them again (The KV cache). Prefix caching reuses these blocks inside one vLLM instance (Prefix caching in the engine core: hashes, lookup, caching, eviction and KV events). A KV connector goes further: it moves blocks to or from something outside the instance. Examples are a second vLLM instance (disaggregated prefill), CPU memory, a local disk or a distributed store.
The work has two parts that run in different processes. The scheduler must know, before it allocates blocks, how many prompt tokens it can load instead of compute. The workers must copy the bytes into or out of the GPU. So one connector class is made twice, with a different connector role. The SCHEDULER object lives in the EngineCore process. The WORKER object lives in each worker. Each step, the scheduler side writes a metadata object; the worker side reads it.
You turn a connector on with --kv-transfer-config, a KVTransferConfig. Its field kv_connector names the class. KVConnectorFactory maps names to classes. vLLM registers 17 built-in connectors, and a plugin can register more, or name a module path.
An everyday comparison. Think of a restaurant kitchen with an external store room. Before each round of orders, the head chef checks the store list for the sauces of each order. If they are in the store, she reserves a shelf and writes a pick list. The kitchen porters read the list, bring the items to the shelf, and take finished items back to the store. The comparison stops here. In vLLM the porters (workers) can also carry items layer by layer during cooking, and the chef can postpone an order until its items arrive.
The picture#
Follow the numbers. Steps 1-3 run on the CPU in the engine core, before the step goes to the workers. Steps 4-6 run in every worker around the forward pass. Steps 7-9 run in the engine core when the outputs come back, and in the next schedule.
In the code#
Step 1: the configuration#
@config
class KVTransferConfig:
"""Configuration for distributed KV cache transfer."""
kv_connector: str | None = None
"""The KV connector for vLLM to transmit KV caches between vLLM instances.
"""
engine_id: str | None = None
"""The engine id for KV transfers."""
kv_buffer_device: str = field(default_factory=kv_buffer_device_default_factory)
"""The device used by kv connector to buffer the KV cache. Choices are
'cuda', 'cpu' and 'xpu'."""
kv_role: KVRole | None = None
"""Whether this vLLM instance produces, consumes KV cache, or both. Choices
are 'kv_producer', 'kv_consumer', and 'kv_both'."""
kv_port: int = 14579
"""The KV connector port, used to build distributed connection."""
kv_connector_extra_config: dict[str, Any] = field(default_factory=dict)
"""any extra config that the connector may need."""
kv_connector_module_path: str | None = None
"""The Python module path to dynamically load the KV connector from.
Only supported in V1."""
enable_permute_local_kv: bool = False
"""Experiment feature flag to enable HND to NHD KV Transfer"""
kv_load_failure_policy: Literal["recompute", "fail"] = "fail"
"""Policy for handling KV cache load failures.
'recompute': reschedule the request to recompute failed blocks
'fail': immediately fail the request with an error finish reason (default)"""kv_connectoris the registered name or the class name.kv_roleiskv_producer,kv_consumerorkv_both; a connector without a role raises (vllm/config/kv_transfer.py:120-124).kv_connector_extra_configis a free dict for the connector, for examplecpu_bytes_to_use.kv_load_failure_policydefaults to"fail": a failed load fails the request.engine_idbecomes a random UUID when unset (vllm/config/kv_transfer.py:111-112).
The producer and consumer properties decide some scheduler behaviour (vllm/config/kv_transfer.py:126-136). A typical disaggregated setup runs one instance with kv_producer and one with kv_consumer; the page Disaggregated prefill: prefill and decode on different instances explains it. This command is adapted from the docs (docs/features/disagg_prefill.md:41):
vllm serve Qwen/Qwen3-8B --kv-transfer-config \
'{"kv_connector":"OffloadingConnector","kv_role":"kv_both","kv_connector_extra_config":{"cpu_bytes_to_use":1000000000}}'
Step 2: the factory finds the class#
@classmethod
def get_connector_class(
cls, kv_transfer_config: "KVTransferConfig"
) -> type[KVConnectorBaseType]:
connector_name = kv_transfer_config.kv_connector
if connector_name is None:
raise ValueError("Connector name is not set in KVTransferConfig")
connector_module_path = kv_transfer_config.kv_connector_module_path
if connector_module_path is not None and not connector_module_path:
raise ValueError("kv_connector_module_path cannot be an empty string.")
if connector_module_path:
# External module path takes priority over internal registry.
connector_module = importlib.import_module(connector_module_path)
try:
connector_cls = getattr(connector_module, connector_name)
except AttributeError as e:
raise AttributeError(
f"Class {connector_name} not found in {connector_module_path}"
) from e
connector_cls = cast(type[KVConnectorBaseType], connector_cls)
if not supports_kw(connector_cls, "kv_cache_config"):
msg = (
f"Connector {connector_cls.__name__} uses deprecated "
"2-argument constructor signature. External v1 KV "
"connectors must accept kv_cache_config as the third "
"constructor argument and pass it to super().__init__()."
)
logger.error(msg)
raise ValueError(msg)
elif connector_name in cls._registry:
connector_cls = cls._registry[connector_name]()
else:
raise ValueError(f"Unsupported connector type: {connector_name}")
return connector_cls- Lines 106-124. When
kv_connector_module_pathis set, it wins over the registry. vLLM imports the module and takes the class namedkv_connector. A class without thekv_cache_configconstructor argument raises. - Lines 125-126. Else the registry: it calls the stored loader, which imports the module only now.
- Lines 127-128. An unknown name raises "Unsupported connector type".
A plugin registers a name with register_connector(name, module_path, class_name). A name that exists raises ValueError (vllm/distributed/kv_transfer/kv_connector/factory.py:30-40). The built-in names are registered when the factory module is imported (lines 149-249). The page distributed/kv_transfer/kv_connector/factory.py: the KV connector registry reads the file line by line.
Step 3: one class, two objects#
The scheduler makes the SCHEDULER object in its constructor:
kv_transfer_config = self.vllm_config.kv_transfer_config
if kv_transfer_config is not None:
assert not self.is_encoder_decoder, (
"Encoder-decoder models are not currently supported with KV connectors"
)
self.connector = KVConnectorFactory.create_connector(
config=self.vllm_config,
role=KVConnectorRole.SCHEDULER,
kv_cache_config=self.kv_cache_config,
)
if self.log_stats:
self.connector_prefix_cache_stats = PrefixCacheStats()
kv_load_failure_policy = kv_transfer_config.kv_load_failure_policy
self.recompute_kv_load_failures = kv_load_failure_policy == "recompute"
# With overlapping batches (async scheduling or PP), a step may
# still be writing a freed request's KV blocks. A consumer KV
# Connector can reallocate and fill those blocks via a load that
# isn't ordered against that write, so defer freeing them.
multiple_inflight_batches = self.vllm_config.max_concurrent_batches > 1
if multiple_inflight_batches and kv_transfer_config.is_kv_consumer:
self.defer_block_free = True
self.requires_kv_delivery = self.connector.requires_kv_deliveryEach worker makes the WORKER object after it knows the KV cache config (vllm/v1/worker/gpu_worker.py:873-878, vllm/distributed/kv_transfer/kv_transfer_state.py:73-92). The roles are an enum with two values (vllm/distributed/kv_transfer/kv_connector/v1/base.py:139-144). The factory checks the hybrid KV cache manager: a connector that does not support it raises when the manager is on (vllm/distributed/kv_transfer/kv_connector/factory.py:54-60). VllmConfig turns the manager off for such a connector when the user did not choose, with a warning (vllm/config/vllm.py:2236-2259).
The base class constructor stores the config and logs a warning that the API "is experimental and subject to change" (vllm/distributed/kv_transfer/kv_connector/v1/base.py:214-232).
Step 4: the scheduler-side methods#
Three methods are abstract on the scheduler side (vllm/distributed/kv_transfer/kv_connector/v1/base.py:515-611):
| Method | Called | Returns, and what the scheduler does |
|---|---|---|
get_num_new_matched_tokens(request, num_computed_tokens) | once for a waiting request with no computed tokens, after the local prefix lookup (vllm/v1/core/sched/scheduler.py:1027-1039) | (n, async): n more tokens can be loaded. None means "ask again": the request is skipped this step (vllm/v1/core/sched/scheduler.py:1043-1048). |
update_state_after_alloc(request, blocks, num_external_tokens) | after allocate_slots succeeds (vllm/v1/core/sched/scheduler.py:1352-1357) | nothing; the connector notes which blocks to fill |
build_connector_meta(scheduler_output) | at the end of schedule() (vllm/v1/core/sched/scheduler.py:1615-1617) | the metadata object that goes to the workers inside SchedulerOutput; the call also resets the connector state |
Other scheduler-side methods have defaults: request_finished (no delay), update_connector_output, on_new_request, take_events and more (vllm/distributed/kv_transfer/kv_connector/v1/base.py:613-680).
Step 5: the worker-side methods#
Four methods are abstract on the worker side (vllm/distributed/kv_transfer/kv_connector/v1/base.py:332-396): start_load_kv, wait_for_layer_load, save_kv_layer and wait_for_save. Model Runner V2 wraps them in a small class:
def pre_forward(
self,
scheduler_output: "SchedulerOutput",
input_batch: "InputBatch | None" = None,
attn_metadata: dict[str, Any] | None = None,
) -> None:
if self._disabled:
return
kv_connector_metadata = scheduler_output.kv_connector_metadata
assert kv_connector_metadata is not None
self.kv_connector.handle_preemptions(kv_connector_metadata)
self.kv_connector.bind_connector_metadata(kv_connector_metadata)
self._pending_load_kwargs = self._build_load_kwargs(
scheduler_output, input_batch, attn_metadata
)
if scheduler_output.has_sync_kv_loads:
# Sync loads need to run before this step's forward.
self._start_load_kv()
# Otherwise start the async load after forward to keep submission
# off the critical path.A synchronous load must finish before the forward reads the blocks, so it starts in pre_forward. An asynchronous load starts after the forward, "to keep submission off the critical path". The layer-by-layer calls wrap the attention custom op:
@wraps(func)
def wrapper(*args, **kwargs):
if not has_kv_transfer_group() or not is_v1_kv_transfer_group():
return func(*args, **kwargs)
layer_name = _resolve_layer_name(args[layer_name_index])
# Extract attention context (metadata, layer, kv_cache, layer_slot_mapping)
attn_metadata, _, kv_cache, _ = get_attention_context(layer_name)
connector = get_kv_transfer_group()
if attn_metadata is None or not connector.has_connector_metadata():
return func(*args, **kwargs)
# Wait for KV layer on entry
connector.wait_for_layer_load(layer_name)
# Execute the function
result = func(*args, **kwargs)
# Save KV cache layer on exit
connector.save_kv_layer(layer_name, kv_cache, attn_metadata)
return result
return wrapperAfter the forward, post_forward calls wait_for_save and collects the finished transfers into a KVConnectorOutput (vllm/v1/worker/gpu/kv_connector.py:136-160). With speculative decoding, the save finishes later, after the drafter runs (vllm/v1/worker/gpu/model_runner.py:2220-2224). The model runner calls it on every pipeline rank (vllm/v1/worker/gpu/model_runner.py:2150, vllm/v1/worker/gpu/model_runner.py:2222-2224).
Step 6: async loads, delayed caching and deferred frees#
The scheduler integrates the connector in four places:
- External prefix hits. The connector sees a block-aligned local hit and returns more tokens. The scheduler counts them as computed (
vllm/v1/core/sched/scheduler.py:1027-1039). - Async loads. For an async load,
allocate_slotsgetsdelay_cache_blocks=True(vllm/v1/core/sched/scheduler.py:1327). The block hashes are not committed yet, because the bytes are not there (vllm/v1/core/kv_cache_manager.py:602-605). The request goes toWAITING_FOR_REMOTE_KVSand is skipped for this step (vllm/v1/core/sched/scheduler.py:1384-1411). - Completion. When a worker reports
finished_recving, the request is marked ready (vllm/v1/core/sched/scheduler.py:3325-3334). The next schedule caches the loaded blocks and moves it back toWAITING(vllm/v1/core/sched/scheduler.py:3275-3289,vllm/v1/core/sched/scheduler.py:3254-3257). - Frees.
request_finishedcan returnTrue(a connector withSupportsHMAgetsrequest_finished_all_groups,vllm/v1/core/sched/scheduler.py:3145-3157): the blocks stay until the worker reportsfinished_sending(vllm/v1/core/sched/scheduler.py:2840-2842,vllm/v1/core/sched/scheduler.py:3335-3338). Separately,defer_block_freeholds freed blocks while an in-flight step may still write them. It is on only with more than one batch in flight and a consumer role (vllm/v1/core/sched/scheduler.py:187-193).
Step 7: which processes read the registry#
The registry is a class attribute in each process. Three processes read it, for different reasons:
| Process | Reads | Where |
|---|---|---|
| front end | the class only, for config checks: the hybrid manager, sleep mode, the CUDA graph mode, and the metrics loggers | vllm/config/vllm.py:2243, vllm/config/vllm.py:1242, vllm/config/vllm.py:1966, vllm/v1/metrics/loggers.py:132 → vllm/distributed/kv_transfer/kv_connector/v1/metrics.py:57, vllm/distributed/kv_transfer/kv_connector/v1/metrics.py:161 |
| engine core | makes the SCHEDULER object | vllm/v1/core/sched/scheduler.py:177-181 |
| each worker | makes the WORKER object | vllm/distributed/kv_transfer/kv_transfer_state.py:88-92 |
So a connector registered by a plugin must be registered in all three processes. A general plugin does that. kv_connector_module_path needs no registration, because each process imports the module itself.
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveThe 17 built-in connectors
Registered at vllm/distributed/kv_transfer/kv_connector/factory.py:149-249. The descriptions come from the docstrings and comments of each module.
| Name | What it does | Source |
|---|---|---|
ExampleConnector | "Simple debug implementation"; saves and loads KV to and from disk (shared_storage_path) | vllm/distributed/kv_transfer/kv_connector/v1/example_connector.py:85-88 |
ExampleHiddenStatesConnector | debug connector that stores hidden states to disk, for the extract_hidden_states method | vllm/distributed/kv_transfer/kv_connector/v1/example_hidden_states_connector.py:99-104 |
LMCacheConnectorV1, LMCacheMPConnector | the external LMCache library; the MP form talks to a separate lmcache server | docs/features/disagg_prefill.md:23 |
NixlConnector, NixlPullConnector, NixlPushConnector | NIXL transfer for disaggregated prefill; pull (READ) or push (WRITE); NixlConnector is an alias of the pull form | vllm/distributed/kv_transfer/kv_connector/v1/nixl/connector.py:350-351, vllm/distributed/kv_transfer/kv_connector/v1/nixl/connector.py:391-392, vllm/distributed/kv_transfer/kv_connector/v1/nixl/connector.py:432 |
MultiConnector | wraps several connectors: loads from the first that reports tokens, saves to all | vllm/distributed/kv_transfer/kv_connector/v1/multi_connector.py:133-140 |
HiSparseConnector | connector boundary for HiSparse sparse-MLA host offloading | vllm/distributed/kv_transfer/kv_connector/v1/hisparse/connector.py:3, vllm/config/attention.py:122-126 |
MoRIIOConnector | MoRI-IO transfer, ROCm only | docs/features/disagg_prefill.md:31 |
OffloadingConnector | vLLM's native offloading to CPU (and more tiers) through vllm/v1/kv_offload | vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py:79-100 |
DecodeBenchConnector | fills the KV cache with dummy values to test a decode instance | vllm/distributed/kv_transfer/kv_connector/v1/decode_bench_connector.py:3-7 |
MooncakeConnector | direct peer-to-peer transfer with Mooncake | vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/connector.py:6-10 |
MooncakeStoreConnector | a shared KV pool in MooncakeDistributedStore | vllm/distributed/kv_transfer/kv_connector/v1/mooncake/store/connector.py:6-10 |
FlexKVConnectorV1 | offload to FlexKV: CPU memory, SSD and remote storage | vllm/distributed/kv_transfer/kv_connector/v1/flexkv_connector.py:36-40 |
SimpleCPUOffloadConnector | "minimal CPU KV cache offloading" | vllm/distributed/kv_transfer/kv_connector/v1/simple_cpu_offload_connector.py:3 |
HF3FSKVConnector | stores KV in the 3FS file system | vllm/distributed/kv_transfer/kv_connector/v1/hf3fs/hf3fs_connector.py:3-6 |
There is no P2pNcclConnector at this commit (git grep finds no such name in vllm/). The docs say "Now supports 9 types of connectors" and name nine in eight bullets (docs/features/disagg_prefill.md:20-49). LMCacheMPConnector is inside the LMCache bullet. The registry has 17 names.
Deep diveThe metadata contract between the two roles
- Scheduler → worker.
KVConnectorMetadatais an empty abstract class; each connector defines its own (vllm/distributed/kv_transfer/kv_connector/v1/base.py:155-160). It travels insideSchedulerOutputto every worker. The worker binds it before the forward and clears it after (vllm/distributed/kv_transfer/kv_connector/v1/base.py:249-268). - Worker → scheduler. Each worker returns a
KVConnectorOutputwithfinished_sending,finished_recving, failed loads, invalid block ids, stats, events and optional worker metadata (vllm/v1/worker/gpu/kv_connector.py:145-157). Worker metadata objects must implementaggregate, because vLLM merges the objects of all workers (vllm/distributed/kv_transfer/kv_connector/v1/base.py:163-178). - All workers answer. With a KV connector, the engine core sets up an output aggregator (
vllm/v1/engine/core.py:191-192). The multiprocess executor then collects the outputs of all workers, not only of the output rank, and merges them (vllm/v1/executor/multiproc_executor.py:405-419). - Request outputs.
request_finishedmay returnkv_transfer_params; vLLM puts them into the request output, so a router can pass them to the next instance (vllm/distributed/kv_transfer/kv_connector/v1/base.py:631-650).
Deep diveWorked example: an async load of a 3,000-token prompt
A consumer instance with block_size 16 gets a request with a 3,000-token prompt. The local prefix cache has the first 62 full blocks, 992 tokens; only full blocks can hit (vllm/v1/core/kv_cache_manager.py:264-266). The connector can load 1,984 more tokens asynchronously. (The numbers are an example; the steps follow the cited lines.)
- The local hit is 992 tokens. 992 mod 16 = 0, so the connector sees all 992 tokens (
vllm/v1/core/sched/scheduler.py:1031-1034). get_num_new_matched_tokens(request, 992)returns(1984, True). There is no partial tail, so all 1,984 tokens are external (vllm/v1/core/sched/scheduler.py:1083-1084). (A local hit that ends inside a block loses its partial block when the remote hit is longer, lines 1064-1075.)allocate_slotsgets 992 local and 1,984 external computed tokens, anddelay_cache_blocks=True(vllm/v1/core/sched/scheduler.py:1320-1337). 2,976 tokens fill 186 blocks of 16.update_state_after_alloctells the connector the block ids.- The request goes to
WAITING_FOR_REMOTE_KVS, withnum_computed_tokensset already (vllm/v1/core/sched/scheduler.py:1393-1407). It runs no prefill this step. - The workers start the load after their forward. Some steps later a worker reports
finished_recving. - The next
schedule()caches the loaded blocks and puts the request back toWAITING. The remaining 24 prompt tokens are prefilled as usual.
If the whole prompt was loaded, vLLM sets num_computed_tokens to the prompt length minus one. So one token runs, and the model samples the first output token (vllm/v1/core/sched/scheduler.py:3271-3272).
Deep diveEC connectors: the same pattern for encoder outputs
A multimodal model runs an encoder (for example a vision tower) before the language model. An EC connector moves the encoder outputs between instances, so one instance can run only the encoder. It has its own config, --ec-transfer-config, with ec_connector, ec_role (ec_producer, ec_consumer or ec_both) and ec_connector_module_path (vllm/config/ec_transfer.py:10-54).
The factory is a copy of the KV factory, with one difference: the registry wins, and the module path is only the fallback (vllm/distributed/ec_transfer/ec_connector/factory.py:58-74). Three connectors are built in: ECExampleConnector, ECCPUConnector and ECMooncakeConnector (vllm/distributed/ec_transfer/ec_connector/factory.py:81-97). The scheduler makes the scheduler-side object (vllm/v1/core/sched/scheduler.py:201-205); each worker makes the worker-side object (vllm/distributed/ec_transfer/ec_transfer_state.py:26-39). The base class has start_load_caches, save_caches, has_cache_item, update_state_after_alloc and build_connector_meta as abstract methods (vllm/distributed/ec_transfer/ec_connector/base.py:164-312). The scheduler calls request_finished on it next to the KV connector (vllm/v1/core/sched/scheduler.py:2829-2832).
Deep diveKV offloading: vllm/v1/kv_offload
KV offloading copies blocks to CPU memory or another tier, so a later request with the same prefix can load them. The easy switch is --kv-offloading-size in GiB. VllmConfig then makes or changes the KV transfer config:
# KV offloading is only activated when kv_offloading_size is set.
if (kv_offloading_size := self.cache_config.kv_offloading_size) is None:
return
kv_offloading_backend = self.cache_config.kv_offloading_backend
# If no KVTransferConfig is provided, create a default one.
if self.kv_transfer_config is None:
self.kv_transfer_config = KVTransferConfig()
if kv_offloading_backend == "native":
if envs.VLLM_USE_SIMPLE_KV_OFFLOAD:
config_connector = "SimpleCPUOffloadConnector"
else:
config_connector = "OffloadingConnector"
self.kv_transfer_config.kv_connector = config_connector
self.kv_transfer_config.kv_connector_extra_config.update(
{"cpu_bytes_to_use": kv_offloading_size * (1 << 30)}
)
elif kv_offloading_backend == "lmcache":
# Default to LMCache multi-process (MP) mode. The actual KV
# storage capacity is managed by the standalone LMCache server
# process, so ``kv_offloading_size`` is not propagated here.
# ``LMCacheMPConnector`` falls back to ``tcp://localhost:5555``
# when host/port are not provided via extra_config.
self.kv_transfer_config.kv_connector = "LMCacheMPConnector"
# This is the same for all backends
self.kv_transfer_config.kv_role = "kv_both"The default backend native selects OffloadingConnector, or SimpleCPUOffloadConnector with VLLM_USE_SIMPLE_KV_OFFLOAD=1 (default 0, vllm/envs.py:2239-2241). The size becomes cpu_bytes_to_use. The docstring says that with TP > 1 the size is "the total buffer size summed across all TP ranks" (vllm/config/cache.py:380-389). Note that lines 1154 and 1167 overwrite kv_connector and kv_role of a config that you also passed. (Derived.)
The native code has its own small plugin point. OffloadingConnector builds an OffloadingSpec through OffloadingSpecFactory (vllm/distributed/kv_transfer/kv_connector/v1/offloading_connector.py:87-89). The spec gives a scheduler-side OffloadingManager and a worker-side OffloadingWorker (vllm/v1/kv_offload/base.py:724-743). Two specs are built in, CPUOffloadingSpec (the default) and TieringOffloadingSpec. An out-of-tree spec is named with spec_name and spec_module_path in kv_connector_extra_config, with an "experimental" warning (vllm/v1/kv_offload/factory.py:30-69).
What can go wrong#
- The front end does not know your connector. The front end reads the class for config checks (
vllm/config/vllm.py:2243). If only the engine core and the workers register it,VllmConfigor the metrics loggers fail with "Unsupported connector type". Use a general plugin orkv_connector_module_path. - Old constructor. A module-path connector must accept
kv_cache_configas the third argument, elseValueError(vllm/distributed/kv_transfer/kv_connector/factory.py:116-124). - Hybrid models. A connector without
SupportsHMAturns the hybrid KV cache manager off, and the warning says that hybrid SSM models "will fail at startup without it" (vllm/config/vllm.py:2245-2256). - Encoder-decoder models. The scheduler asserts "Encoder-decoder models are not currently supported with KV connectors" (
vllm/v1/core/sched/scheduler.py:174-176). - Backends that refuse a connector. An attention backend can reject a KV connector in its validation;
ROCM_ATTNdoes (vllm/v1/attention/backends/rocm_attn.py:184-188). See How vLLM chooses an attention backend. - Load failures. With the default policy
"fail", a failed load fails the request;"recompute"computes the missing blocks instead (vllm/config/kv_transfer.py:88-91,vllm/v1/core/sched/scheduler.py:184-185).
Why it matters. KV connectors are the base of disaggregated prefill and of KV offloading, two of the main ways to scale online serving. Most connector problems are process problems: which process registered the class, and which role runs where.
Check yourself#
- 1Which object of a KV connector runs in the engine core, and which runs in the workers?easy
Show answer
The
SCHEDULER-role object runs in the engine core, made by the scheduler (vllm/v1/core/sched/scheduler.py:177-181). Each worker makes aWORKER-role object (vllm/distributed/kv_transfer/kv_transfer_state.py:88-92). - 2What doeseasy
get_num_new_matched_tokensreturn, and what doesNonemean?Show answer
A pair: the number of extra tokens that can be loaded, and whether the load is asynchronous.
Noneas the count means that the connector needs more time; the scheduler skips the request this step and asks again (vllm/distributed/kv_transfer/kv_connector/v1/base.py:529-538,vllm/v1/core/sched/scheduler.py:1043-1048). - 3You set bothmedium
kv_connector_module_pathand a registry entry for the same name. Which class runs? And for an EC connector?Show answer
For KV connectors the module path wins (
vllm/distributed/kv_transfer/kv_connector/factory.py:106-108). For EC connectors the registry wins and the module path is the fallback (vllm/distributed/ec_transfer/ec_connector/factory.py:66-73). - 4Why does the scheduler passmedium
delay_cache_blocks=Truefor an async load?Show answer
The blocks are allocated, but the bytes arrive later. Committing their hashes now would let another request hit blocks that hold no data.
allocate_slotsreturns beforecache_blocksin that case (vllm/v1/core/kv_cache_manager.py:602-605), and the blocks are cached when the load finishes (vllm/v1/core/sched/scheduler.py:3254-3257). - 5You start withhard
--kv-offloading-size 8 --kv-transfer-config '{"kv_connector":"NixlConnector","kv_role":"kv_producer"}'. Which connector and role run?Show answer
OffloadingConnectorwithkv_both. Whenkv_offloading_sizeis set and the backend isnative,_post_init_kv_transfer_configoverwriteskv_connectorand setskv_role = "kv_both"on the existing config (vllm/config/vllm.py:1149-1167). To combine both, useMultiConnector. (Derived from these lines.)