跳到內容

一種基於點對點通訊、受 Dynamo 部分啟發的、具有動態擴充套件能力的 xPyD 實現。

詳細設計

整體流程

如圖 1 所示,此 PD 解耦 方案的整體流程透過請求流進行描述

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

image1

代理/路由器(演示)

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

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

目前,為了快速驗證 xPyD 是否能工作,採用輪詢方式選擇 1P1D。未來計劃結合例項的負載狀態,使用 trie 樹來選擇合適的 P 和 D。

每個 P/D 例項會定期(目前每 3 秒)向代理/路由器傳送心跳包,以進行註冊(即報告 http_addr -> zmq_addr)並保持連線活躍。如果某個例項崩潰並在一定時間內未能傳送 ping,代理/路由器將移除該超時例項(此功能尚未開發)。

KV Cache 傳輸方法

KVCache 傳輸有三種方法:PUT、GET 和 PUT_ASYNC。這些方法可以透過 --kv-transfer-configkv_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 組。

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 這樣的解決方案就沒有必要了。張量記憶體池充當一個分洪區,通常只有在突發流量激增時才會被使用。在最壞的情況下,我的解決方案的效能不會比帶有快取儲存的正常情況差。

安裝 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)

命令
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

關閉

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

測試資料

場景:1K 輸入 & 200 輸出 token,端到端 P99 延遲約 2 秒

testdata