跳到內容

P2P NCCL 聯結器

xPyD 的一種實現,基於點對點通訊實現動態擴充套件,部分靈感來源於 Dynamo。

詳細設計

整體流程

如圖 1 所示,該 **PD 分離** 解決方案的整體流程透過請求流進行描述。

  1. 客戶端向 Proxy/Router 的 /v1/completions 介面傳送 HTTP 請求。
  2. Proxy/Router 透過輪詢或隨機選擇的方式選擇一個 **1P1D (1 Prefill 例項 + 1 Decode 例項)**,生成一個 request_id (規則稍後介紹),將 HTTP 請求訊息中的 max_tokens 修改為 **1**,然後將請求轉發給 **P 例項**。
  3. 緊接著,Proxy/Router 將 **原始 HTTP 請求** 轉發給 **D 例項**。
  4. **P 例項** 執行 **Prefill**,然後 **主動將生成的 KV 快取** 傳送給 D 例項 (使用 **PUT_ASYNC** 模式)。D 例項的 zmq_addr 可以透過 request_id 解析。
  5. **D 例項** 設有 **專用執行緒** 用於接收 KV 快取 (以避免阻塞主程序)。接收到的 KV 快取儲存在 **GPU 記憶體緩衝區** 中,其大小由 vLLM 啟動引數 kv_buffer_size 決定。當 GPU 緩衝區已滿時,KV 快取將儲存在 **本地張量記憶體池** 中。
  6. 在 **Decode** 過程中,D 例項的主程序從 **GPU 緩衝區** 或 **記憶體池** 中檢索 KV 快取 (由 P 例項傳輸),從而 **跳過 Prefill**。
  7. 完成 **Decode** 後,D 例項將結果返回給 **Proxy/Router**,然後 Proxy/Router 將其轉發給 **客戶端**。

image1

代理/路由器 (演示)

一個簡單的 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,例如:

cmpl-___prefill_addr_10.0.1.2:21001___decode_addr_10.0.1.3:22001_93923d63113b4b338973f24d19d4bf11-0

目前,為了快速驗證 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-configkv_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 組。

image2

每個 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

pip install "vllm>=0.9.2"

執行 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_sizeport (如果存在衝突)。
  • PUT_ASYNC 提供了最佳效能,應優先使用。
  • --port 必須與 --kv-transfer-config 中的 http_port 一致。
  • disagg_proxy_p2p_nccl_xpyd.py 指令碼將使用埠 10001 (用於接收客戶端請求) 和埠 30001 (用於接收來自 P 和 D 例項的服務發現)。
  • 執行代理的節點必須安裝 quart
  • 支援多節點;您只需要在 --kv-transfer-config 中修改 proxy_ipproxy_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

關閉

pgrep python | xargs kill -9 && pkill -f python

測試資料

**場景**:1K 輸入 & 200 輸出 token,端到端 P99 延遲 ~2s

testdata