211 lines
7.4 KiB
Markdown
211 lines
7.4 KiB
Markdown
# 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.
|