vLLM Disaggregated Prefill: Connectors and the Proxy
By the end of this lesson you will be able to start two vLLM instances with different key-value roles, choose a connector your network can actually carry, put the project’s own example proxy in front of the pair, set the environment variables that decide which interface the transfer uses, read the logs and metrics that show whether a cache actually moved, and state the feature’s support status with the date you checked it.
This is the implementation the first lab builds. Read it with Part 18’s measured link numbers and the previous lesson’s transfer arithmetic beside you, because they decide which of the connectors below is worth your evening.
The status line, first
Section titled “The status line, first”Take this in before anything else, because it changes how you should treat everything after it.
The page gives two benefits and they are both about latency rather than volume. The first is tuning time to first token and inter-token latency separately, by giving each phase its own instance with its own parallel strategy. The second is controlling tail inter-token latency, on the grounds that a separated prefill cannot interrupt a decode in progress, which the page presents as more reliable than chunked prefill for that specific purpose.
Two instances and three roles
Section titled “Two instances and three roles”A disaggregated vLLM deployment is two ordinary vllm serve processes. Both load the whole model.
What distinguishes them is one extra argument, --kv-transfer-config, whose value is a JSON object.
Two of its fields decide the topology.
kv_connector names the plug-in that moves the blocks. kv_role says what this instance does with
them: kv_producer for the prefill instance, kv_consumer for the decode instance, or kv_both for
an instance that does both, which is what you use when a single instance is talking to a store
rather than to a partner.
The LMCache documentation’s two-node example, read on 2026-09-09, shows the shape clearly: the
prefill server is started with "kv_role":"kv_producer" and the decode server with
"kv_role":"kv_consumer", both naming the same connector.
One request through a disaggregated pair
- The client sends a request to the proxyAn ordinary OpenAI-compatible chat or completion request. The client knows nothing about the split.
- The proxy asks the prefill instance for one tokenThe producer reads the whole prompt, writes the key-value blocks out through its connector, and returns almost immediately. Its answer is discarded.
- The connector moves the blocksPoint to point over NIXL or Mooncake, or through a store, or into a shared directory. This is the transfer whose size the previous lesson computed.
- The proxy sends the same request to the decode instanceThe consumer finds the blocks already present, skips the prefill it would otherwise have done, and starts generating.
- The proxy streams the decode instance's answer backThe client sees one response from one endpoint, as it would from a single server.
The page also documents the client-side form of the same handshake, for programs that would rather
drive the two instances themselves than run a proxy. The prefill call passes
{"return_token_ids": True, "kv_transfer_params": {"do_remote_decode": True}} in the request body
and the decode call passes {"kv_transfer_params": {"do_remote_prefill": True, "prompt_token_ids": ids}} with the token ids the first call returned. Worth knowing that this exists; the proxy is the
easier path and the one the lab uses.
Choosing a connector for the network you have
Section titled “Choosing a connector for the network you have”The page lists nine connectors. Four of them matter for a home lab, and the choice is decided by the link, not by preference.
| Connector | What it moves bytes with | What it needs | Where it fits at home |
|---|---|---|---|
ExampleConnector |
A shared directory, named by shared_storage_path in the extra configuration |
A filesystem both instances can read and write | Any network at all, including a slow one. The Part 18 NFS mount is exactly this. |
NixlConnector |
NIXL over UCX | UCX; RDMA to be worth doing, TCP to merely work | Track S over the ConnectX-7 cable; Track M over Thunderbolt 5 from macOS 26.2 |
MooncakeConnector |
Mooncake’s Transfer Engine | The mooncake-transfer-engine package; TCP is a supported transport |
The plain-Ethernet point-to-point option, if you want one |
OffloadingConnector |
Host memory on the same machine | Nothing on the network | One machine, extending the cache into host memory. The second lab. |
The one to reach for first on a house network is the shared-directory connector, and the reason is
the same reason Part 18 built the NFS export before it built any cluster: it is the path whose
failures you can already diagnose. vLLM’s own example for it, examples/disaggregated/example_connector/
in the repository, builds a KVTransferConfig with kv_connector="ExampleConnector",
kv_role="kv_both" and kv_connector_extra_config={"shared_storage_path": "local_storage"}, then
runs a prefill script and a decode script in sequence against that directory. Change the directory
to a mount both machines see and the same mechanism spans two machines.
Fragment — not complete on its own
# The connector argument on the prefill instance. The path must be visible to BOTH# instances; on the reference cluster that means the shared mount from Part 18.--kv-transfer-config '{"kv_connector":"ExampleConnector","kv_role":"kv_producer","kv_connector_extra_config":{"shared_storage_path":"/mnt/models/kv-store"}}'The point-to-point connectors are what the documentation is really built around, and on a link with
RDMA they are the right answer. LMCache’s page shows a NixlConnector wrapped in a MultiConnector
alongside an LMCache connector, so that the same deployment does the request handoff over NIXL and
keeps a longer-lived tier behind it. That is a production shape; the lab uses the simpler form.
The proxy
Section titled “The proxy”Something has to call the prefill instance and then the decode instance for every request. vLLM ships a demonstration of that, and the lab uses it rather than inventing one.
The file is examples/disaggregated/disaggregated_serving/disagg_proxy_demo.py in the vLLM
repository, and the directory’s README describes it as demonstrating “XpYd (X prefill instances, Y
decode instances)”. Its module docstring calls it “a disaggregated prefilling proxy demo to
demonstrate an example usage of XpYd disaggregated prefilling”. It takes four arguments: --model,
--prefill with one or more host:port values, --decode with one or more, and --port for the
proxy’s own listener, which defaults to 8000. The README’s usage example is:
Fragment — not complete on its own
# From the vLLM repository README for examples/disaggregated/disaggregated_serving,# read 2026-09-09. Take the current file from the repository at the tag you installed.python3 examples/disaggregated/disaggregated_serving/disagg_proxy_demo.py \ --model $model_name \ --prefill localhost:8100 localhost:8101 \ --decode localhost:8200 localhost:8201 \ --port 8000Two practical notes about obtaining it. It is not installed by pip install vllm; it lives in the
source tree, so you need the repository checked out at the same tag as the wheel you installed, or
at least the single file downloaded from that tag. And it is a demonstration, which the file’s own
docstring says: it is the right thing to measure with and the wrong thing to leave running in front
of anything that matters. Part 23 is where a front door gets built properly.
Configuration and environment
Section titled “Configuration and environment”Beyond the connector JSON, four environment variables decide whether the transfer works at all, and three of them are about naming the right interface. The values below come from LMCache’s disaggregated prefill documentation, read on 2026-09-09.
VLLM_NIXL_SIDE_CHANNEL_HOST is the address this instance advertises for the handshake between
producer and consumer. It must be an address the other machine can reach, which on a cluster with
a direct cable means the address on the cable and not the one on the house switch. This is the same
mistake Part 20 warns about for Ray, in a different costume.
VLLM_NIXL_SIDE_CHANNEL_PORT is the port for that handshake, and the documentation notes that two
instances sharing a host need distinct ones. On the single-machine path, that is the line people
miss.
UCX_NET_DEVICES restricts UCX to the interfaces you name. Part 20’s TensorRT-LLM lesson sets the
same variable for the same reason: without it, a library will cheerfully choose the management
interface and give you a slow cluster while reporting nothing at all.
NCCL_CUMEM_ENABLE appears in the documented examples alongside the others; set it as the
documentation for your version shows rather than reasoning about it.
None of these applies to the shared-directory connector, which has no handshake and no transport. That is a third reason to start there.
Fragment — not complete on its own
# Shape only. The interface name and the addresses come from your own .env,# never from a page: a name that is right in one house is wrong in every other.export VLLM_NIXL_SIDE_CHANNEL_HOST="${PREFILL_LINK_ADDR}"export VLLM_NIXL_SIDE_CHANNEL_PORT=5600export UCX_NET_DEVICES="${CLUSTER_IFACE}"Reading the logs and the metrics
Section titled “Reading the logs and the metrics”A disaggregated pair has three processes and four places a request can go wrong. These are the things to look at, in order.
The startup log of each instance. Both should report the model loaded and the key-value cache size the engine settled on, exactly as in Part 9. If the two instances disagree about the model, the quantisation or the block size, the blocks one produces are not the blocks the other expects, and the symptom is a silent miss rather than an error.
The proxy’s own output. It shows which instance each request went to and in what order. A proxy that is sending everything to the decode instance and nothing to the prefill instance is a working single-machine deployment wearing a disaggregated hat, and it will look fine.
The metrics endpoint on each instance. Both serve /metrics in the Prometheus text format.
vLLM’s production metrics page documents the four that matter here, and the interesting readings are
comparisons between the two instances rather than absolute values:
| Metric | Documented as | What it tells you here |
|---|---|---|
vllm:time_to_first_token_seconds |
“Histogram of time to first token in seconds.” | On the decode instance, this is the number the split is trying to improve |
vllm:request_time_per_output_token_seconds |
“Histogram of time_per_output_token_seconds per request.” | Should be essentially unchanged by the split; if it got worse, something is wrong |
vllm:prefix_cache_hits |
“Prefix cache hits, in terms of number of cached tokens.” | On the decode instance, a hit rate near zero when it should be high means the transfer is not landing |
vllm:kv_cache_usage_perc |
“KV-cache usage. 1 means 100 percent usage.” | On the decode instance, this is the occupancy that decides how many conversations fit |
The interface counters. The engine cannot tell you how many bytes crossed the cable; the operating system can. Part 19’s approach of reading the interface byte counters either side of a request is exactly the right instrument here, and the first lab reuses it. A split that is working shows a large read on the decode machine’s cluster interface at the moment each request starts, of roughly the size the previous lesson’s arithmetic predicted. A split that is not working shows nothing, and the arithmetic tells you what “nothing” would have looked like.
Per-track status
Section titled “Per-track status”Track S — NVIDIA DGX Spark
Primary path. vLLM runs on DGX Spark, as Part 9 established, and a pair of Sparks joined by
a ConnectX-7 QSFP cable is the only configuration in this course where the transfer arithmetic
from the previous lesson comes out comfortably in favour of a split. Use NixlConnector over
the cable, with UCX_NET_DEVICES naming the QSFP interface and
VLLM_NIXL_SIDE_CHANNEL_HOST set to each node’s address on that cable. Start with the shared
directory anyway: proving the mechanism on the cheap connector first turns a two-variable
problem into two one-variable problems.
Track X — AMD Ryzen AI Max+ 395Partial
vLLM's GPU installation page lists Ryzen AI MAX and AI 300 (gfx1151/1150) among its ROCm targets with pre-built wheels, but this course has not exercised that path and no ROCm-specific disaggregation guidance exists in the vLLM disaggregated prefilling documentation.
vLLM is listed for this hardware and this course has not run it there, which is the same status
Part 9 recorded. If your installation works, the shared-directory connector is the one to try:
it does not touch UCX or RDMA, and 2.5 gigabit Ethernet is what these machines usually have, so
the arithmetic already says the point-to-point path is not going to win. The page’s list of
connectors does include a ROCm-only MoRIIOConnector; this course has not tested it and does
not describe it further.
Track M — Apple siliconNot supported
vLLM's mainline GPU path does not cover macOS, so neither instance in a disaggregated pair can run on Apple silicon with GPU acceleration.
Take the single-machine path with llama-server instead. It cannot disaggregate, but it can do
the thing disaggregation is trying to protect: run several conversations at once without a long
prompt stalling the others, with each conversation’s cache kept warm in its own slot. Start it
with --parallel set to the number of concurrent conversations you want and prompt caching left
at its default, and use the slot save and restore endpoints from Part 17 to keep an expensive
prompt across restarts. Part 21 is where a two-Mac cluster is built; this particular feature is
not part of it.
RunnableTrack M · Apple silicon
llama-server \ --model ~/models/unsloth/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf \ --alias local-chat \ --host 127.0.0.1 \ --port 8080 \ --ctx-size 32768 \ --parallel 4 \ --n-gpu-layers 999 \ --cache-prompt \ --slot-save-path ./slot-cache \ --metrics \ --slotsTrack N — NVIDIA desktop or laptop
Primary path. Two desktops or a desktop and a laptop, both running vLLM as installed in
Part 9. Everything in this lesson applies unchanged. The thing to check before you start is the
link: unless you have put a card in a spare slot, this is the track most likely to be on
ordinary Ethernet, and the previous lesson’s table is the prediction your measurement should
match. One machine with two GPUs is also a legitimate configuration here, with one instance
pinned to each card through CUDA_VISIBLE_DEVICES and the transfer happening over the host
rather than the network.
Trace the request through producer, transfer and consumer
Section titled “Trace the request through producer, transfer and consumer”Assign a request identifier and follow it through the entry proxy, prefill producer, connector and decode consumer. Record which process owns each phase and where errors are returned. A healthy producer and consumer tested separately do not prove that the handoff works.
Use the connector example corresponding to your installed version and transport. Configuration keys and topology assumptions are part of the implementation contract; do not mix a current example with a different pinned engine without reviewing the differences. Start with a small checkpoint and one request before attempting load.
Measure transfer latency, first-token latency and output cadence alongside failures. Test a producer restart, a consumer restart and a cancelled request in the isolated lab. Observe whether buffers are reclaimed and whether a later request succeeds. If a connector silently falls back or recomputes, document it rather than describing the result as successful cache transfer. The service-level acceptance test must establish both correct outputs and the intended division of work.
A disaggregated vLLM deployment is two ordinary servers with one extra argument.
--kv-transfer-config carries a JSON object whose kv_connector names the plug-in and whose
kv_role is kv_producer on the prefill instance and kv_consumer on the decode instance. The
prompt goes to both; only the first prefills it.
The connector choice is a network decision. ExampleConnector with a shared_storage_path needs
only a directory both machines can see, which the Part 18 NFS mount already provides;
NixlConnector goes point to point over UCX and wants RDMA to be worth doing; MooncakeConnector
names TCP among its transports; OffloadingConnector moves blocks into host memory on one machine
and touches no network at all.
The proxy is disagg_proxy_demo.py in the vLLM repository’s disaggregated serving examples, taking
--model, --prefill, --decode and --port. It is a demonstration, it is not installed by the
wheel, and it has no authentication.
Four environment variables decide whether a point-to-point transfer uses the right cable, and
VLLM_NIXL_SIDE_CHANNEL_HOST set to a management address instead of a cluster address is the
classic way to build a split that works and is slow. The shared-directory connector has none of
them.
Proof of life is the decode instance’s time to first token against the same instance without a producer, the prefix cache counters on the decode instance, and the interface byte counters showing a payload the size the arithmetic predicted. And the feature is marked experimental on its own page as of 2026-09-09, so pin the version and write it down.
Next: SGLang’s version of the same split, which uses different words and a bootstrap server, and NVIDIA Dynamo, which puts a scheduler above all of it.
Check your understanding
Sources for this lesson
8 verified · checked 2026-09-09
- 01vLLM — Disaggregated Prefilling (experimental)§ Why; usage example; connectors; developmentdocs.vllm.ai/en/latest/features/disagg_prefill.html2026-09-09
- 02vLLM — disaggregated serving examples§ README; disagg_proxy_demo.pygithub.com/vllm-project/vllm/tree/main/examples/disaggregated/disaggregated_serving2026-09-09
- 03vLLM — example connector, run.sh§ KVTransferConfig; shared_storage_pathgithub.com/vllm-project/vllm/blob/main/examples/disaggregated/example_connector/prefill_example.py2026-09-09
- 04vLLM — vllm serve CLI reference§ Optionsdocs.vllm.ai/en/latest/cli/serve.html2026-09-09
- 05vLLM — Engine arguments§ cpu-offload-gb; kv-cache-dtypedocs.vllm.ai/en/latest/configuration/engine_args.html2026-09-09
- 06vLLM — Production metrics§ Metric names; endpointdocs.vllm.ai/en/latest/usage/metrics.html2026-09-09
- 07LMCache — Disaggregated prefill§ Two-node setup; single-node note; environment variablesdocs.lmcache.ai/mp/disaggregated_prefill.html2026-09-09
- 08llama.cpp — llama-server README§ Prompt caching; slots; parallelgithub.com/ggml-org/llama.cpp/blob/master/tools/server/README.md2026-09-09
Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.