一種基於點對點通訊、受 Dynamo 部分啟發的、具有動態擴充套件能力的 xPyD 實現。
詳細設計¶
整體流程¶
如圖 1 所示,此 PD 解耦 方案的整體流程透過請求流進行描述
- 客戶端向代理/路由器的
/v1/completions
介面傳送 HTTP 請求。 - 代理/路由器透過輪詢或隨機選擇方式,選擇一個 1P1D(1 個 Prefill 例項 + 1 個 Decode 例項),生成一個
request_id
(規則稍後介紹),將 HTTP 請求訊息中的max_tokens
修改為 1,然後將請求轉發給 P 例項。 - 緊接著,代理/路由器將**原始 HTTP 請求**轉發給 **D 例項**。
- **P 例項** 執行 **Prefill**,然後**主動將生成的 KV cache 傳送**給 D 例項(採用 **PUT_ASYNC** 模式)。D 例項的
zmq_addr
可以透過request_id
解析。 - **D 例項** 有一個**專用執行緒**用於接收 KV cache(以避免阻塞主程序)。接收到的 KV cache 被儲存到 **GPU 記憶體緩衝區**中,其大小由 vLLM 啟動引數
kv_buffer_size
決定。當 GPU 緩衝區滿時,KV cache 將儲存在**本地張量記憶體池**中。 - 在 **Decode** 過程中,D 例項的主程序從 **GPU 緩衝區**或**記憶體池**中檢索 KV cache(由 P 例項傳輸),從而**跳過 Prefill**。
- 完成 **Decode** 後,D 例項將結果返回給**代理/路由器**,代理/路由器再將其轉發給**客戶端**。
代理/路由器(演示)¶
一個簡單的 HTTP 服務作為客戶端請求的入口點,並啟動一個後臺執行緒來監聽 P/D 例項報告其 HTTP IP 和 PORT,以及 ZMQ IP 和 PORT。它維護一個 http_addr -> zmq_addr
的字典。http_addr
是 vLLM 例項請求的 IP:PORT,而 zmq_addr
是用於 KV cache 握手和元資料接收的地址。
代理/路由器負責根據客戶端請求的特性(例如 prompt)選擇 1P1D,並生成相應的 request_id
,例如
目前,為了快速驗證 xPyD 是否能工作,採用輪詢方式選擇 1P1D。未來計劃結合例項的負載狀態,使用 trie 樹來選擇合適的 P 和 D。
每個 P/D 例項會定期(目前每 3 秒)向代理/路由器傳送心跳包,以進行註冊(即報告 http_addr -> zmq_addr
)並保持連線活躍。如果某個例項崩潰並在一定時間內未能傳送 ping,代理/路由器將移除該超時例項(此功能尚未開發)。
KV Cache 傳輸方法¶
KVCache 傳輸有三種方法:PUT、GET 和 PUT_ASYNC。這些方法可以透過 --kv-transfer-config
和 kv_connector_extra_config
引數(特別是透過 send_type
欄位)來指定。PUT 和 PUT_ASYNC 都涉及 P 例項主動將 KVCache 傳送給 D 例項。區別在於 PUT 是一種同步傳輸方法,會阻塞主程序,而 PUT_ASYNC 是一種非同步傳輸方法。PUT_ASYNC 使用專用執行緒傳送 KVCache,這意味著它不會阻塞主程序。相比之下,GET 方法涉及 P 例項在完成 prefill 計算後將 KVCache 儲存到記憶體緩衝區中。然後 D 例項在為 KVCache 分配空間後,主動從 P 例項中檢索計算好的 KVCache。
實驗結果表明,這些方法的效能從高到低依次為:PUT_ASYNC → GET → PUT。
透過 ZMQ & NCCL 進行 P2P 通訊¶
只要知道對方的地址,就可以進行點對點 KV cache 傳輸(使用 NCCL),不受 rank 和 world size 的限制。以支援 PD 解耦例項的動態擴充套件(擴容和縮容)。這意味著新增或移除 P/D 例項不需要完全重啟系統。
每個 P/D 例項只需建立一個 P2pNcclEngine
例項。該例項維護一個 ZMQ 伺服器,執行一個專用執行緒監聽 zmq_addr
地址並接收來自其他例項的控制流請求。這些請求包括建立 NCCL 連線的請求和傳送 KVCache 元資料(如張量形狀和資料型別)的請求。但它不實際傳輸 KVCache 資料本身。
當 P 例項和 D 例項首次傳輸 KVCache 時,需要建立 ZMQ 連線和 NCCL 組。對於後續的 KVCache 傳輸,此 ZMQ 連線和 NCCL 組將被複用。NCCL 組僅包含兩個 rank,這意味著 world size 等於 2。此設計旨在支援動態擴充套件,這意味著新增或刪除 P/D 例項無需完全重啟系統。只要知道對方的地址,就可以進行點對點 KVCache 傳輸,不受 rank 或 world size 的限制。
NCCL 組拓撲¶
目前,KVCache 傳輸僅支援對稱 TP(Tensor Parallelism)方法。未來將支援非對稱 TP 和 PP(Pipeline Parallelism)方法。圖 2 展示了 1P2D 配置,其中每個例項的 TP(Tensor Parallelism)度為 2。總共有 7 個 NCCL 組:三個 vLLM 例項各自有一個 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 這樣的解決方案就沒有必要了。張量記憶體池充當一個分洪區,通常只有在突發流量激增時才會被使用。在最壞的情況下,我的解決方案的效能不會比帶有快取儲存的正常情況差。
安裝 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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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)¶
命令
VLLM_USE_V1=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 \
--disable-log-request \
--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
}'
基準測試¶
命令
python3 benchmark_serving.py \
--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