P2P NCCL 聯結器¶
xPyD 的一種實現,基於點對點通訊實現動態擴充套件,部分靈感來源於 Dynamo。
詳細設計¶
整體流程¶
如圖 1 所示,該 **PD 分離** 解決方案的整體流程透過請求流進行描述。
- 客戶端向 Proxy/Router 的
/v1/completions介面傳送 HTTP 請求。 - Proxy/Router 透過輪詢或隨機選擇的方式選擇一個 **1P1D (1 Prefill 例項 + 1 Decode 例項)**,生成一個
request_id(規則稍後介紹),將 HTTP 請求訊息中的max_tokens修改為 **1**,然後將請求轉發給 **P 例項**。 - 緊接著,Proxy/Router 將 **原始 HTTP 請求** 轉發給 **D 例項**。
- **P 例項** 執行 **Prefill**,然後 **主動將生成的 KV 快取** 傳送給 D 例項 (使用 **PUT_ASYNC** 模式)。D 例項的
zmq_addr可以透過request_id解析。 - **D 例項** 設有 **專用執行緒** 用於接收 KV 快取 (以避免阻塞主程序)。接收到的 KV 快取儲存在 **GPU 記憶體緩衝區** 中,其大小由 vLLM 啟動引數
kv_buffer_size決定。當 GPU 緩衝區已滿時,KV 快取將儲存在 **本地張量記憶體池** 中。 - 在 **Decode** 過程中,D 例項的主程序從 **GPU 緩衝區** 或 **記憶體池** 中檢索 KV 快取 (由 P 例項傳輸),從而 **跳過 Prefill**。
- 完成 **Decode** 後,D 例項將結果返回給 **Proxy/Router**,然後 Proxy/Router 將其轉發給 **客戶端**。
代理/路由器 (演示)¶
一個簡單的 HTTP 服務作為客戶端請求的入口點,並啟動一個後臺執行緒監聽 P/D 例項報告的 HTTP IP 和 PORT,以及 ZMQ IP 和 PORT。它維護一個 http_addr -> zmq_addr 的字典。http_addr 是 vLLM 例項請求的 IP:PORT,而 zmq_addr 是用於 KV 快取握手和元資料接收的地址。
Proxy/Router 負責根據客戶端請求的特性(例如提示詞)選擇 1P1D,並生成相應的 request_id,例如:
目前,為了快速驗證 xPyD 是否可行,採用了 1P1D 的輪詢選擇。未來計劃結合 Trie 和例項負載狀態來選擇合適的 P 和 D。
每個 P/D 例項定期向 Proxy/Router 傳送心跳包 (目前每 3 秒一次),用於註冊 (即報告 http_addr -> zmq_addr) 和保持連線。如果一個例項崩潰且在一定時間內未能傳送 ping,Proxy/Router 將移除超時例項 (此功能尚未開發)。
KV 快取傳輸方法¶
KVCache 傳輸有三種方法:PUT、GET 和 PUT_ASYNC。這些方法可以透過 --kv-transfer-config 和 kv_connector_extra_config 引數指定,具體透過 send_type 欄位。PUT 和 PUT_ASYNC 都涉及 P 例項主動向 D 例項傳送 KVCache。區別在於,PUT 是一種同步傳輸方法,會阻塞主程序,而 PUT_ASYNC 是一種非同步傳輸方法。PUT_ASYNC 使用專用執行緒傳送 KVCache,這意味著它不會阻塞主程序。相比之下,GET 方法涉及 P 例項在計算完 prefill 後將 KVCache 儲存到記憶體緩衝區。一旦 D 例項分配了 KVCache 的空間,它就會主動從 P 例項檢索計算好的 KVCache。
實驗結果表明,這些方法的效能從高到低排序為:PUT_ASYNC → GET → PUT。
透過 ZMQ & NCCL 進行 P2P 通訊¶
只要知道對方的地址,就可以進行點對點 KV 快取傳輸 (使用 NCCL),不受 rank 和 world size 的限制。以支援 PD 分離的例項的動態擴充套件 (增減)。這意味著新增或刪除 P/D 例項不需要完全重啟系統。
每個 P/D 例項只需要建立一個 P2pNcclEngine 例項。該例項維護一個 ZMQ 伺服器,執行一個專用執行緒在 zmq_addr 地址上監聽,並接收來自其他例項的控制流請求。這些請求包括建立 NCCL 連線的請求和傳送 KV 快取元資料 (如張量形狀和資料型別) 的請求。但是,它不實際傳輸 KV 快取資料本身。
當 P 例項和 D 例項第一次傳輸 KV 快取時,它們需要建立一個 ZMQ 連線和一個 NCCL 組。對於後續的 KV 快取傳輸,這個 ZMQ 連線和 NCCL 組會被重用。NCCL 組只包含兩個 rank,即 world size 為 2。此設計旨在支援動態擴充套件,這意味著新增或刪除 P/D 例項不需要完全重啟系統。只要知道對方的地址,就可以進行點對點 KV 快取傳輸,而不受 rank 或 world size 的限制。
NCCL 組拓撲¶
目前,KVCache 傳輸僅支援對稱 TP (張量並行) 方法。未來將支援非對稱 TP 和 PP (流水線並行) 方法。圖 2 展示了 1P2D 的設定,其中每個例項的 TP (張量並行) 度為 2。總共有 7 個 NCCL 組:三個 vLLM 例項各有 1 個 TP=2 的 NCCL 組。此外,P 例項的第 0 個 GPU 卡與每個 D 例項的第 0 個 GPU 卡建立一個 NCCL 組。類似地,P 例項的第 1 個 GPU 卡與每個 D 例項的第 1 個 GPU 卡建立一個 NCCL 組。
每個 NCCL 組佔用一定的 GPU 記憶體緩衝區用於通訊,其大小主要受 NCCL_MAX_NCHANNELS 環境變數的影響。當 NCCL_MAX_NCHANNELS=16 時,一個 NCCL 組通常佔用 100MB,而當 NCCL_MAX_NCHANNELS=8 時,它通常佔用 52MB。對於大規模的 xPyD 配置——例如 DeepSeek 的 96P144D——此實現目前不可行。未來,我們正考慮使用 RDMA 進行點對點通訊,並且也在關注 UCCL。
GPU 記憶體緩衝區和張量記憶體池¶
記憶體緩衝區大小的權衡如下:對於 P 例項,在 PUT 和 PUT_ASYNC 模式下不需要記憶體緩衝區,但在 GET 模式下是必需的。對於 D 例項,在所有三種模式下都需要記憶體緩衝區。D 例項的記憶體緩衝區不應過大。同樣,對於處於 GET 模式下的 P 例項,記憶體緩衝區也不應過大。D 例項的記憶體緩衝區用於臨時儲存 P 例項傳送的 KVCache。如果太大,會減少 D 例項正常推理可用的 KVCache 空間,從而降低推理批次大小,最終導致輸出吞吐量下降。記憶體緩衝區的大小由引數 kv_buffer_size 配置,以位元組為單位,通常設定為記憶體大小的 5%~10%。
如果 P 例項的 --max-num-seqs 引數設定為較大的值,由於批次大小較大,P 例項會同時生成大量的 KVCache。這可能會超出 D 例項記憶體緩衝區的容量,導致 KVCache 丟失。一旦 KVCache 丟失,D 例項需要重新計算 Prefill,這相當於執行兩次 Prefill。因此,首次 token 時間 (TTFT) 將顯著增加,導致效能下降。
為了解決上述問題,我設計並開發了一個本地張量記憶體池用於儲存 KVCache,靈感來源於 Linux 記憶體模組中使用的夥伴系統。由於記憶體足夠大,通常在伺服器上是 TB 級別,因此無需考慮字首快取或使用基於塊的設計來重用記憶體,從而節省空間。當記憶體緩衝區不足時,KVCache 可以直接儲存在張量記憶體池中,D 例項隨後可以從中檢索 KVCache。讀寫速度是 PCIe 的速度,PCIe 4.0 的速度約為 21 GB/s,通常比 Prefill 速度快。否則,Mooncake 和 lmcache 等解決方案就沒有必要了。張量記憶體池充當一個洩洪區,通常在突然的流量高峰期才使用。在最壞的情況下,我的解決方案效能不比帶有 Cache 儲存的正常情況差。
安裝 vLLM¶
執行 xPyD¶
說明¶
- 以下示例在 A800 (80GB) 裝置上執行,使用 Meta-Llama-3.1-8B-Instruct 模型。
- 請注意
kv_buffer_size(以位元組為單位) 的設定。經驗值是 GPU 記憶體大小的 10%。這與 kvcache 的大小有關。如果太小,用於臨時儲存接收到的 kvcache 的 GPU 記憶體緩衝區將溢位,導致 kvcache 儲存在張量記憶體池中,從而增加延遲。如果太大,可用於推理的 kvcache 將減少,導致批次大小減小,吞吐量下降。 - 對於 Prefill 例項,在使用非 GET 模式時,
kv_buffer_size可設定為 1,因為 Prefill 目前不需要接收 kvcache。但是,在使用 GET 模式時,需要更大的kv_buffer_size,因為它需要儲存傳送給 D 例項的 kvcache。 - 您可能需要修改以下命令中的
kv_buffer_size和port(如果存在衝突)。 PUT_ASYNC提供了最佳效能,應優先使用。--port必須與--kv-transfer-config中的http_port一致。disagg_proxy_p2p_nccl_xpyd.py指令碼將使用埠 10001 (用於接收客戶端請求) 和埠 30001 (用於接收來自 P 和 D 例項的服務發現)。- 執行代理的節點必須安裝
quart。 - 支援多節點;您只需要在
--kv-transfer-config中修改proxy_ip和proxy_port。 - 在以下示例中,假設 **代理的 IP 地址為 10.0.1.1**。
執行 1P3D¶
代理 (例如. 10.0.1.1)¶
cd {your vllm directory}/examples/online_serving/disaggregated_serving_p2p_nccl_xpyd/
python3 disagg_proxy_p2p_nccl_xpyd.py &
Prefill1 (例如. 10.0.1.2 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=0 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20001 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.9 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_producer","kv_buffer_size":"1e1","kv_port":"21001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20001"}}' > /var/vllm.log 2>&1 &
Decode1 (例如. 10.0.1.3 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=1 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20002 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.7 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_consumer","kv_buffer_size":"8e9","kv_port":"22001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20002"}}' > /var/vllm.log 2>&1 &
Decode2 (例如. 10.0.1.4 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=2 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20003 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.7 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_consumer","kv_buffer_size":"8e9","kv_port":"23001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20003"}}' > /var/vllm.log 2>&1 &
Decode3 (例如. 10.0.1.5 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=3 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20004 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.7 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_consumer","kv_buffer_size":"8e9","kv_port":"24001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20004"}}' > /var/vllm.log 2>&1 &
執行 3P1D¶
代理 (例如. 10.0.1.1)¶
cd {your vllm directory}/examples/online_serving/disaggregated_serving_p2p_nccl_xpyd/
python3 disagg_proxy_p2p_nccl_xpyd.py &
Prefill1 (例如. 10.0.1.2 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=0 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20001 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.9 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_producer","kv_buffer_size":"1e1","kv_port":"21001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20001"}}' > /var/vllm.log 2>&1 &
Prefill2 (例如. 10.0.1.3 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=1 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20002 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.9 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_producer","kv_buffer_size":"1e1","kv_port":"22001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20002"}}' > /var/vllm.log 2>&1 &
Prefill3 (例如. 10.0.1.4 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=2 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20003 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.9 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_producer","kv_buffer_size":"1e1","kv_port":"23001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20003"}}' > /var/vllm.log 2>&1 &
Decode1 (例如. 10.0.1.5 或 10.0.1.1)¶
命令
CUDA_VISIBLE_DEVICES=3 vllm serve {your model directory} \
--host 0.0.0.0 \
--port 20004 \
--tensor-parallel-size 1 \
--seed 1024 \
--served-model-name base_model \
--dtype float16 \
--max-model-len 10000 \
--max-num-batched-tokens 10000 \
--max-num-seqs 256 \
--trust-remote-code \
--gpu-memory-utilization 0.7 \
--kv-transfer-config \
'{"kv_connector":"P2pNcclConnector","kv_role":"kv_consumer","kv_buffer_size":"8e9","kv_port":"24001","kv_connector_extra_config":{"proxy_ip":"10.0.1.1","proxy_port":"30001","http_port":"20004"}}' > /var/vllm.log 2>&1 &
單個請求¶
curl -X POST -s http://10.0.1.1:10001/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "base_model",
"prompt": "San Francisco is a",
"max_tokens": 10,
"temperature": 0
}'
基準測試¶
命令
vllm bench serve \
--backend vllm \
--model base_model \
--tokenizer meta-llama/Llama-3.1-8B-Instruct \
--dataset-name "random" \
--host 10.0.1.1 \
--port 10001 \
--random-input-len 1024 \
--random-output-len 1024 \
--ignore-eos \
--burstiness 100 \
--percentile-metrics "ttft,tpot,itl,e2el" \
--metric-percentiles "90,95,99" \
--seed $(date +%s) \
--trust-remote-code \
--request-rate 3 \
--num-prompts 1000