P2P KV Cache Sharing (multiprocess mode)
In a multi-node deployment, every node runs its own LMCache server, and each one only caches the KV of the requests it served. P2P KV cache sharing turns those per-node caches into one logical cache: when a node looks up a prefix it does not have locally, it reads that prefix's KV directly from the memory of the peer that holds it, over the datacenter network using RDMA — instead of recomputing the prefix.
This example shows how to run it both on a single node (for testing/debugging) and across multiple nodes, and what logs to expect.
See also the full reference:
docs/source/mp/p2p.rst.
How it works
- Coordinator — one small HTTP service per deployment that tracks which LMCache servers are alive. It only manages membership; it never sees KV data.
- LMCache server — each server polls the coordinator for live peers and, for every peer, opens a connection used to look up and RDMA-read that peer's KV.
- vLLM talks only to its local LMCache server (via
LMCacheMPConnector). The P2P fetch happens inside the LMCache server, transparently to vLLM.
Requirements
- LMCache (full install, with CUDA) + vLLM, on every node.
- A coordinator URL reachable by all nodes (P2P refuses to start without
--coordinator-url). - An RDMA-capable network (InfiniBand / RoCE) for production performance.
- A single, contiguous L1 region — P2P is incompatible with the GDS
(
--gds-l1-path) and Device-DAX (--l1-devdax-path) L1 tiers; the server refuses to start in those configurations. - Recommended:
--l1-align-bytes 65536(64 KB) on P2P servers for larger, better-aligned RDMA reads.
Single-node setup (testing & debugging)
Run two LMCache servers and two vLLM servers over localhost, plus one
coordinator, on a 2-GPU machine. This exercises the entire P2P path without a
real network.
Open five terminals:
# Terminal 1 — coordinator
lmcache coordinator --host 0.0.0.0 --port 9300
# Terminal 2 — node A: LMCache server
lmcache server \
--host 127.0.0.1 --port 6555 --http-port 7555 \
--l1-size-gb 50 --eviction-policy LRU \
--l1-align-bytes 65536 \
--instance-id node-a \
--coordinator-url http://127.0.0.1:9300 \
--coordinator-advertise-ip 127.0.0.1 \
--p2p-advertise-url 127.0.0.1:8555
# Terminal 3 — node A: vLLM on GPU 0, connector -> local LMCache (port 6555)
CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen3-14B --port 8000 \
--kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"lmcache.mp.port":6555}}'
# Terminal 4 — node B: LMCache server
lmcache server \
--host 127.0.0.1 --port 6556 --http-port 7556 \
--l1-size-gb 50 --eviction-policy LRU \
--l1-align-bytes 65536 \
--instance-id node-b \
--coordinator-url http://127.0.0.1:9300 \
--coordinator-advertise-ip 127.0.0.1 \
--p2p-advertise-url 127.0.0.1:8556
# Terminal 5 — node B: vLLM on GPU 1, connector -> local LMCache (port 6556)
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-14B --port 8001 \
--kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"lmcache.mp.port":6556}}'
The two LMCache servers must differ in every port: ZMQ (--port), HTTP
(--http-port), and the P2P transfer endpoint (--p2p-advertise-url). Give
each a distinct --instance-id.
Note: on a single host,
localhosttraffic uses the loopback/TCP path, not RDMA, so latencies are not representative of a real fabric. Single-node mode is for functional testing — benchmark on a real multi-node RDMA deployment.
Test it
# 1. Populate node A's cache (cold — expect ~0 LMCache hits).
python send_request.py --port 8000
# 2. Send the SAME prompt to node B. B never served it and its cache is empty,
# so any LMCache hit must have been read from A over P2P.
python send_request.py --port 8001
The second call should print a non-zero num_lmcache_cached_tokens.
Multi-node setup
Start the coordinator on a host all nodes can reach (here 10.0.0.1), then run
one LMCache server + one vLLM per node. Adding more nodes is just more copies of
the per-node block, all pointing at the same coordinator.
# Coordinator host (10.0.0.1)
lmcache coordinator --host 0.0.0.0 --port 9300
On each node, set NODE_IP to that node's routable address and run:
NODE_IP=10.0.0.2 # this node's address (change per node)
COORDINATOR=10.0.0.1
# RDMA tuning for UCX (adjust transports/rails to your fabric).
export UCX_TLS=rc,sm,self
export UCX_MAX_RMA_RAILS=8
# 1. LMCache server with P2P enabled. Bind 0.0.0.0, advertise NODE_IP.
lmcache server \
--host 0.0.0.0 --port 6555 --http-port 7555 \
--l1-size-gb 100 --eviction-policy LRU \
--l1-align-bytes 65536 \
--instance-id "lmcache-${NODE_IP}" \
--coordinator-url "http://${COORDINATOR}:9300" \
--coordinator-advertise-ip "${NODE_IP}" \
--p2p-advertise-url "${NODE_IP}:8555" \
--p2p-listen-url 0.0.0.0:8555
# 2. vLLM, connector pointed at the LOCAL LMCache server (port 6555).
vllm serve Qwen/Qwen3-14B --port 8000 \
--kv-transfer-config '{"kv_connector":"LMCacheMPConnector","kv_role":"kv_both","kv_load_failure_policy":"recompute","kv_connector_extra_config":{"lmcache.mp.port":6555}}'
--coordinator-advertise-ip is the address peers reach this node's control
plane at; --p2p-advertise-url is its RDMA transfer endpoint. Binding 0.0.0.0
while advertising NODE_IP (via --p2p-listen-url) is the usual pattern when a
node has multiple interfaces.
Once two nodes are up, send a long prompt to one node and the same prompt to another — the second serves it from the first over RDMA:
python send_request.py --host 10.0.0.2 --port 8000
python send_request.py --host 10.0.0.3 --port 8000
What logs to expect
Coordinator logs each instance as it registers:
Registered instance node-a at 127.0.0.1:7555
Registered instance node-b at 127.0.0.1:7556
Each LMCache server at startup and as it discovers its peer (INFO level):
Started PeriodicThread: p2p-controller-thread (level=medium, interval=5.0s, init_wait=0.0s)
Registered with coordinator as node-a
Added L2 adapter 0 (p2p) # created when the peer is discovered
With LMCACHE_LOG_LEVEL=DEBUG, the P2P controller also names the peer:
Added P2P adapter 0 for peer node-b (127.0.0.1:8556)
When a peer leaves (or after ~3 missed discovery polls), you'll see the adapter torn down:
Deleted L2 adapter 0
Removed P2P adapter for peer node-b # DEBUG
Verifying P2P is working
Query a server's status endpoint for its P2P state and connected peers:
curl -s http://127.0.0.1:7555/status | python3 -m json.tool
# look for: "p2p_state": "registered", "p2p_peer_count": 1, "p2p_peers": ["node-b"]
List the fleet from the coordinator:
curl -s http://127.0.0.1:9300/instances | python3 -m json.tool
A successful P2P read shows up as a non-zero num_lmcache_cached_tokens from
send_request.py on a node that never served the prompt (see the test steps
above).
Notes
- Read-only: a node reads KV from its peers; it never writes into a peer's memory.
- One hop: a node reads directly from the peer that holds the prefix; reads are not chained across multiple peers.