P2P NCCL 连接器¶
一个基于点对点通信实现动态扩展的xPyD实现,部分灵感来自Dynamo。
详细设计¶
整体流程¶
如图1所示,这个PD分解解决方案的整体流程通过请求流进行描述:
- 客户端向Proxy/Router的
/v1/completions接口发送HTTP请求。 - 代理/路由器通过轮询或随机选择方式选定一个1P1D(1个预填充实例 + 1个解码实例),生成
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缓存会被存储到本地Tensor内存池中。 - 在解码(Decode)阶段,D实例的主进程会从GPU缓冲区或内存池中获取KV缓存(由P实例传输),从而跳过预填充(Prefill)阶段。
- 完成解码后,D实例将结果返回给代理/路由器,然后由其转发给客户端。
代理/路由器 (演示)¶
一个简单的HTTP服务作为客户端请求的入口点,并启动一个后台线程来监听P/D实例上报的HTTP IP和PORT,以及ZMQ IP和PORT。它维护一个http_addr -> zmq_addr的字典。http_addr是vLLM实例请求的IP:PORT,而zmq_addr是用于KV缓存握手和元数据接收的地址。
代理/路由器负责根据客户端请求的特征(如提示词)选择1P1D,并生成对应的request_id,例如:
目前,为了快速验证xPyD是否能正常工作,采用了1P1D的轮询选择方式。未来计划结合实例负载状态使用字典树来选择合适的P和D。
每个P/D实例会定期向Proxy/Router发送心跳包(当前设置为每3秒一次),用于注册(即上报http_addr -> zmq_addr)并保持连接活跃。如果某个实例崩溃且在一定时间内未能发送心跳信号,Proxy/Router将移除该超时实例(此功能尚未开发完成)。
KV缓存传输方法¶
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实现点对点通信¶
只要知道对端的地址,就可以执行点对点的KV缓存传输(使用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,意味着全局大小为2。这种设计旨在支持动态扩展,即添加或移除P/D实例无需重启整个系统。只要知道对端地址,就可以执行点对点的KVCache传输,不受rank或全局大小的限制。
NCCL 组拓扑¶
目前仅支持对称TP(张量并行)方法进行KVCache传输。非对称TP和PP(流水线并行)方法将在未来得到支持。图2展示了1P2D设置,其中每个实例的TP(张量并行)度为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。因此,首令牌时间(TTFT)将显著增加,导致性能下降。
To address the above issues, I have designed and developed a local Tensor memory pool for storing KVCache, inspired by the buddy system used in Linux memory modules. Since the memory is sufficiently large, typically in the TB range on servers, there is no need to consider prefix caching or using block-based designs to reuse memory, thereby saving space. When the memory buffer is insufficient, KVCache can be directly stored in the Tensor memory pool, and D instances can subsequently retrieve KVCache from it. The read and write speed is that of PCIe, with PCIe 4.0 having a speed of approximately 21 GB/s, which is usually faster than the Prefill speed. Otherwise, solutions like Mooncake and lmcache would not be necessary. The Tensor memory pool acts as a flood diversion area, typically unused except during sudden traffic surges. In the worst-case scenario, my solution performs no worse than the normal situation with a Cache store.
安装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 &
预填充1 (例如 10.0.1.2 或 10.0.1.1)¶
Command
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 &
解码1 (例如 10.0.1.3 或 10.0.1.1)¶
Command
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)¶
Command
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)¶
Command
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 &
预填充1 (例如 10.0.1.2 或 10.0.1.1)¶
Command
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 &
预填充2 (例如 10.0.1.3 或 10.0.1.1)¶
Command
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 &
预填充3 (例如 10.0.1.4 或 10.0.1.1)¶
Command
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 &
解码1 (例如 10.0.1.5 或 10.0.1.1)¶
Command
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
}'
基准测试¶
Command
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