# 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`](../../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: ```bash # 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, `localhost` traffic 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 ```bash # 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. ```bash # 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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.