适用版本 :vLLM 0.18.0、Ray 2.40+、PyTorch 2.10、Python 3.12
实战模型 :Qwen3.5-72B(双节点 A100 80G × 8)/ DeepSeek-V3(四节点 H100 × 8)
网络互联:InfiniBand HDR 200Gb/s(节点间)/ NVLink(节点内)
前言
单卡跑通了,多卡也跑通了,但当模型大到单节点放不下,你就要踏入多机多卡的领地。
这一步的跨度比从单卡到多卡大得多。节点间通信、NCCL 网络配置、Ray 集群管理、环境一致性......任何一个环节出问题,表现都是同一个症状:卡住不动,没有报错,等一会儿 timeout。
本文记录从双节点跑 Qwen3.5-72B 到四节点跑 DeepSeek-V3 的完整踩坑路径,10 个坑,每个坑都给出可直接用的诊断命令和修复方案。
一、先搞清楚两种并行的本质区别
在动手之前,必须先把 Tensor Parallel(TP)和 Pipeline Parallel(PP)的本质区别理解透------选错并行策略是最大的性能浪费,比踩任何技术坑都代价高。
Tensor Parallel(张量并行)
把单层的权重矩阵横向切片,分布到多块 GPU 上,每张卡计算一部分,再通过 AllReduce 合并结果。
python
一个 Attention 层的 W_q 矩阵(维度 d × d):
GPU 0: W_q 的前 d/4 列
GPU 1: W_q 的第 2 个 d/4 列
GPU 2: W_q 的第 3 个 d/4 列
GPU 3: W_q 的后 d/4 列
→ 每个 Token 的每一层,四张卡都要参与计算并同步
核心特征:每个 Token 都需要所有 GPU 协同计算,通信发生在每一层。通信延迟直接影响 TTFT,对网络带宽要求极高(需要 NVLink 或 InfiniBand)。
Pipeline Parallel(流水线并行)
把模型的层切分给不同 GPU(或节点),每个 GPU 只负责若干层,前一段的输出(激活值)传给下一段。
python
Qwen3.5-72B(80 层 Transformer)横跨 2 个节点:
节点 0:第 0 ~ 39 层
节点 1:第 40 ~ 79 层
→ 节点 0 处理完前 40 层,把激活值传给节点 1
→ 节点 1 接着处理后 40 层
核心特征:节点间只传递激活值(远小于权重),通信量小;但单个请求要顺序经过两个节点,存在流水线气泡。
如何选择
vLLM 官方给出了明确的决策规则:
python
决策树:
模型能放进单 GPU?
→ 是:不需要并行,直接单卡跑
→ 否 ↓
模型能放进单节点(多 GPU)?
→ 是:用 Tensor Parallel(单节点内 NVLink 带宽高)
tp_size = 节点内 GPU 数
→ 否 ↓
需要跨节点?
→ 标准做法:TP 处理节点内,PP 处理节点间
tp_size = 每节点 GPU 数
pp_size = 节点数
特殊情况:单节点内 GPU 数量不能整除模型层数?
→ 用 PP(PP 支持不均等分层)
tp_size = 1,pp_size = GPU 总数
一句话记住 :TP 用于节点内(高带宽互联),PP 用于节点间(带宽较低的跨节点)。
二、环境准备:多机配置的前提检查清单
动手部署之前,先在所有节点上逐项检查这个清单。跳过任何一项,大概率后面要花数倍时间排查。
python
# ============ 在每个节点上分别执行 ============
# 1. 验证 GPU 可见性
nvidia-smi -L
# 期望:列出所有 GPU,确认数量和型号一致
# 2. 验证 CUDA 版本一致(版本不一致会有奇怪的行为)
nvcc --version
python3 -c "import torch; print(torch.version.cuda)"
# 3. 验证节点间网络连通性(替换为实际 IP)
ping -c 4 <另一节点 IP>
# 4. 验证 InfiniBand / RDMA(如果有 IB 网卡)
ibstat | grep -E "State|Rate"
# 期望:State: Active,Rate: 200(HDR)或 100(EDR)
# 5. 验证 NCCL 能跨节点通信
NCCL_DEBUG=INFO python3 -c "
import torch, torch.distributed as dist, os
os.environ['MASTER_ADDR'] = '<头节点 IP>'
os.environ['MASTER_PORT'] = '29500'
os.environ['RANK'] = '0' # 工作节点改为 1
os.environ['WORLD_SIZE'] = '2'
dist.init_process_group('nccl', timeout=__import__('datetime').timedelta(seconds=30))
print('NCCL 初始化成功')
"
# 6. 验证 SSH 无密码互访(Ray 需要)
ssh <另一节点 IP> "hostname"
# 期望:直接返回主机名,不需要输密码
# 7. 验证模型文件路径一致(每个节点必须能访问同一路径的模型权重)
ls /data/models/Qwen3.5-72B-Instruct/
# 期望:所有节点输出相同的文件列表
三、Ray 集群启动
多机部署强依赖 Ray,单节点多卡可以用 multiprocessing,但跨节点只能用 Ray。
标准启动流程
python
# ==================== 头节点 ====================
# 设置网络接口(必须是节点间互通的那张网卡)
export NCCL_SOCKET_IFNAME=ib0 # InfiniBand 网卡
# export NCCL_SOCKET_IFNAME=eth0 # 以太网备选
export GLOO_SOCKET_IFNAME=ib0
export RAY_NETWORK_INTERFACE=ib0
ray start --head \
--node-ip-address=<头节点 IP> \
--port=6379 \
--num-gpus=8
# ==================== 所有工作节点 ====================
export NCCL_SOCKET_IFNAME=ib0
export GLOO_SOCKET_IFNAME=ib0
export RAY_NETWORK_INTERFACE=ib0
ray start \
--address="<头节点 IP>:6379" \
--node-ip-address=<本节点 IP> \
--num-gpus=8
# ==================== 在头节点验证集群状态 ====================
ray status
# 期望输出(2 节点 × 8 GPU 示例):
# Active: 2 nodes
# Total Usage: 0.0/16.0 GPU
启动 vLLM(在头节点执行)
python
# 2 节点 × 8 GPU = 16 GPU 总计
# Qwen3.5-72B:TP=8(节点内),PP=2(节点间)
export VLLM_HOST_IP=<头节点 IP>
vllm serve /data/models/Qwen3.5-72B-Instruct \
--served-model-name qwen3.5-72b \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray \
--dtype bfloat16 \
--max-model-len 32768 \
--gpu-memory-utilization 0.88 \
--trust-remote-code
四、10 个真实踩坑
坑1:NCCL 卡住不动,没有错误日志,等 300 秒后 timeout
现象:启动命令一切正常,日志显示各个 Worker 初始化中,然后在某个位置停住,几分钟后出现:
java
ncclCommInitRank: timeout waiting for ...
torch.distributed.DistBackendError: NCCL error ...
根本原因 :NCCL 在 ncclCommInitRank 阶段需要所有节点的 GPU 互相握手,如果节点间网络配置不对(防火墙、网卡未指定、端口被占),握手永远无法完成。
诊断步骤:
python
# 第一步:开启 NCCL 详细日志,看卡在哪里
NCCL_DEBUG=TRACE vllm serve ... 2>&1 | head -200
# 如果看到 "NCCL INFO Net/IB" 但没有 "Connected" → IB 网卡问题
# 如果看到 "NCCL INFO NET/Socket" → 没走 InfiniBand,走的是以太网
# 第二步:检查防火墙(NCCL 需要开放 29400~29500 范围的端口)
sudo ufw status
# 如果防火墙开着,临时关闭测试
sudo ufw disable
# 第三步:确认 NCCL_SOCKET_IFNAME 指向了正确的网卡
ip addr show
# 找出节点间互通的网卡名(通常是 ib0 或 eth0)
修复:
python
# 必须在所有节点上设置相同的环境变量,然后重启 Ray
export NCCL_SOCKET_IFNAME=ib0 # 替换为你的实际网卡名
export NCCL_IB_DISABLE=0 # 有 IB 时确保启用
export NCCL_IB_GID_INDEX=3 # IB 网卡的 GID 索引,视配置调整
# 增大 NCCL 初始化超时(慢速下载环境)
# 在 vllm serve 命令里加
--distributed-timeout 1800 # 默认 600s,下载大模型权重时可能不够
坑2:Ray 头节点自动占用 GPU,工作节点 GPU 数量统计错误
现象 :ray status 显示总 GPU 数少了 8 块,或者 vLLM 启动时报 Not enough GPUs,但 nvidia-smi 上明明有足够的 GPU。
原因 :Ray 的 Head Node 默认也会声明 GPU 资源(num-gpus=8),这导致 Ray 把头节点的 GPU 同时分配给了 Ray 的调度任务和 vLLM 的推理任务,产生资源竞争。
修复:头节点的 Ray 进程不分配 GPU:
python
# ✅ 头节点:num-gpus=0,不占用 GPU
ray start --head \
--node-ip-address=<头节点 IP> \
--port=6379 \
--num-gpus=0 # 关键:头节点不持有 GPU 资源
# ✅ 工作节点(包括头节点所在机器):单独启动 worker
ray start \
--address="localhost:6379" \
--node-ip-address=<头节点 IP> \
--num-gpus=8
坑3:每个节点的模型路径不一致,Worker 加载失败
现象 :头节点日志正常,但工作节点的 Ray Worker 报 FileNotFoundError: model path not found。
原因 :vLLM 的每个 Worker 进程都要独立加载模型权重,路径必须在所有节点上完全一致。这个问题在容器和裸机部署时表现不同:容器里很容易用 -v 挂载一致路径,裸机环境往往模型放在不同位置。
修复方案一(推荐):NFS 共享存储,所有节点挂载同一路径:
python
# 在头节点建立 NFS 共享
sudo apt install nfs-kernel-server
echo "/data/models *(rw,sync,no_subtree_check)" >> /etc/exports
sudo exportfs -a
# 在所有工作节点挂载
sudo mount <头节点 IP>:/data/models /data/models
修复方案二 :使用 vLLM 官方提供的 run_cluster.sh,用 Docker 统一镜像和挂载点:
python
# 官方脚本:vllm/examples/online_serving/run_cluster.sh
# 头节点
bash run_cluster.sh \
vllm/vllm-openai:v0.18.0 \ # 相同镜像
<头节点 IP> \
--head \
/data/models:/data/models # 统一挂载路径
# 工作节点
bash run_cluster.sh \
vllm/vllm-openai:v0.18.0 \
<头节点 IP> \
--worker \
/data/models:/data/models
坑4:PP 模式下 pipeline_parallel_size 和实际节点数不匹配
现象 :设置了 --pipeline-parallel-size 2 但只有 1 个节点,或者 Ray 集群有 3 个节点但只用了 2 个,剩余节点资源空置。
原因理解 :vLLM 的 PP 把模型按层分成 pp_size 段,每段分配到一个独立的 GPU 组。如果 Ray 集群的实际节点数多于 pp_size,多余节点的 GPU 不会被使用。
python
# 典型正确配置(2 节点 × 8 GPU)
vllm serve ... \
--tensor-parallel-size 8 \ # 节点内 8 卡 TP
--pipeline-parallel-size 2 # 2 个节点 PP
# 等价的纯 TP 配置(需要节点间高带宽 IB)
vllm serve ... \
--tensor-parallel-size 16 # 跨节点 16 卡 TP,不用 PP
# 何时选哪个?
# IB 带宽充足(HDR 200Gb)→ 纯 TP,延迟更低
# IB 带宽一般(25GbE 以太网)→ TP+PP 组合,通信量少
坑5:PP 模式流水线气泡导致 GPU 利用率低
现象 :开启 PP 后,nvidia-smi 显示工作节点的 GPU 利用率只有 50~60%,第 0 段节点在计算,第 1 段节点在等待。
原因:这是流水线并行的固有问题。当 Micro-batch 不够多时,后段的 GPU 要等前段计算完才能工作------这段等待时间叫"流水线气泡"。
python
时间轴:
节点0(层 0-39): [计算 req1] [计算 req2] [计算 req3] ...
节点1(层40-79): [计算 req1] [计算 req2] ...
↑ 节点1 在等节点0,这段时间是"气泡"
缓解方法:
python
# 方法一:增大并发请求数,让流水线更满
vllm serve ... \
--max-num-seqs 256 # 增大并发,让流水线持续有数据
# 方法二:启用 Chunked Prefill,让 Prefill 和 Decode 交替执行
vllm serve ... \
--enable-chunked-prefill \
--max-num-batched-tokens 2048
# 方法三(vLLM 0.18 新特性):使用 --performance-mode throughput
# 这个模式内置了批次优化策略,自动减少气泡
vllm serve ... \
--performance-mode throughput \
--pipeline-parallel-size 2
坑6:Ray Worker 在请求过程中崩溃,Socket closed 错误
现象:服务启动正常,能处理一段时间的请求,但在处理了 100~5000 个请求后崩溃,日志出现:
(RayWorkerWrapper pid=xxx) Error pushing mutable object:
RPC Error message: Socket closed; rpc_code: 14
原因:这是 vLLM + Ray Compiled DAG 在高并发多节点场景下的已知稳定性问题(GitHub Ray #59404),在 A100 和 H100 集群上均有复现。
临时缓解:
python
# 方法一:关闭 Ray Compiled DAG,改用标准通信
export VLLM_USE_RAY_COMPILED_DAG=0
# 方法二:改用共享内存通信(可以服务更多请求再崩溃)
export VLLM_USE_RAY_COMPILED_DAG_CHANNEL_TYPE=shm
# 方法三:限制最大并发数,降低通信压力
vllm serve ... \
--max-num-seqs 64 # 保守值,视机型调整
# 方法四:加进程保活脚本
自动重启保活(生产环境必备):
python
# restart_vllm.sh
#!/bin/bash
while true; do
echo "[$(date)] 启动 vLLM..."
vllm serve /data/models/Qwen3.5-72B-Instruct \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray \
# ... 其他参数
EXIT_CODE=$?
echo "[$(date)] vLLM 退出,退出码=${EXIT_CODE},5 秒后重启..."
sleep 5
done
坑7:RDMA / InfiniBand 配置后回退到 Gloo,性能极差
现象 :明明配置了 InfiniBand,但 NCCL 日志里显示 Using Gloo,节点间通信带宽只有几 Gb/s,推理延迟极高。
原因:NCCL 会在 IB 初始化失败时静默回退到 Gloo(以太网慢速通信),而不是报错退出。常见原因是 GID Index 配置不对或者 IB 驱动未正确安装。
诊断和修复:
python
# 确认 IB 网卡状态
ibstat
ibv_devinfo | grep -E "port_state|active_mtu|gid"
# 找到正确的 GID Index
for i in $(seq 0 10); do
echo -n "GID $i: "
show_gids | grep "mlx5_0" | awk "NR==$((i+1)) {print}"
done
# 找到 RoCE v2 类型的 GID,记下 Index
# 设置正确的 GID Index(通常是 3,但每台机器可能不同)
export NCCL_IB_GID_INDEX=3
export NCCL_IB_DISABLE=0
export NCCL_NET_GDR_LEVEL=2 # 开启 GPU Direct RDMA
# 验证是否真正走了 IB
NCCL_DEBUG=INFO vllm serve ... 2>&1 | grep "NCCL INFO NET"
# 期望看到:NCCL INFO NET/IB : Using [mlx5_0:1]
# 如果看到:NCCL INFO NET/Socket → 没走 IB
坑8:多节点模型内存计算错误,实际显存不够
现象:理论上显存够(72B × 2 bytes ÷ 16 GPU ≈ 9GB/卡),但启动时仍然 OOM:
python
torch.cuda.OutOfMemoryError: CUDA out of memory
原因:vLLM 的显存预算包含三部分:模型权重 + KV Cache + 激活值缓冲。当混合使用 TP 和 PP 时,由于 PP 的中间激活值需要在节点间传输,每个节点实际需要的显存比纯理论值高 10~20%。
另外,MeluXina 的实测数据显示:在 TP+PP 混合部署 Llama-405B 时,显存利用率并不是在所有 GPU 间完全均衡的,某些节点可能比理论值多用 15% 显存。
修复:
python
# 降低 gpu-memory-utilization,给激活值和缓冲留出空间
vllm serve ... \
--gpu-memory-utilization 0.80 # 从默认 0.90 降到 0.80
# 或者降低最大上下文长度,减少 KV Cache 占用
vllm serve ... \
--max-model-len 8192 # 从 32768 降到 8192
# 查看实际显存分配
# 启动后在日志里搜索:
# INFO: # GPU blocks: xxx → 可用 KV Cache 块数
# INFO: Memory profiling results: xxx → 各部分显存分配详情
坑9:环境变量在 Ray Worker 里不生效
现象 :在头节点设置了 NCCL_SOCKET_IFNAME=ib0,但 NCCL 日志显示 Worker 进程用的是 eth0。
原因:Ray Worker 是子进程,不会自动继承所有父进程的环境变量,尤其是 Ray 的 Worker 是在 Ray 启动时就 fork 的,后来设的环境变量不会传进去。
正确做法 :必须在 ray start 之前设置环境变量,或者通过 Ray 的运行时环境传递:
python
# ❌ 错误:ray start 之后再设 env,Worker 看不到
ray start --head ...
export NCCL_SOCKET_IFNAME=ib0 # 太晚了!
# ✅ 正确方法一:ray start 前设置
export NCCL_SOCKET_IFNAME=ib0
export NCCL_IB_GID_INDEX=3
ray start --head ...
# ✅ 正确方法二:通过 --runtime-env 传递(推荐,显式可见)
ray start --head \
--runtime-env='{"env_vars": {"NCCL_SOCKET_IFNAME": "ib0", "NCCL_IB_GID_INDEX": "3"}}' \
...
坑10:TP=16 跨节点纯 Tensor Parallel,首 Token 延迟反而比 TP=8+PP=2 更高
现象:把 TP 从节点内 8 卡改成跨节点 16 卡(纯 TP,不用 PP),理论上并行度更高,实际上 p95 TTFT 从 1.2s 上升到 2.8s。
原因:TP 在每一层都需要 AllReduce 同步,TP=16 意味着跨节点的 AllReduce 要走节点间网络。即使是 InfiniBand HDR(200Gb/s),跨节点 AllReduce 的延迟也是节点内 NVLink AllReduce 的 5~10 倍,这个延迟在每一层都累积。
以 Qwen3.5-72B(80 层)为例:
- 节点内 NVLink AllReduce:~0.1ms/层 × 80 层 = 8ms
- 跨节点 IB AllReduce:~0.8ms/层 × 80 层 = 64ms
这就是为什么 Red Hat 的实践文档明确指出:当节点间互联带宽有限时,用 PP 处理节点间通信,而不是让 TP 跨节点。
python
推荐配置(2 节点 × 8 GPU,IB HDR):
TP=8(节点内 NVLink),PP=2(节点间 IB)→ TTFT ~1.2s
不推荐:
TP=16(跨节点 IB AllReduce 每层)→ TTFT ~2.8s
五、完整启动脚本(2 节点生产环境)
python
#!/bin/bash
# start_distributed_qwen35_72b.sh
# 在头节点执行,工作节点已提前加入 Ray 集群
set -e
# ========== 节点配置 ==========
HEAD_IP="10.0.0.1"
WORKER_IPS=("10.0.0.2")
MODEL_PATH="/data/models/Qwen3.5-72B-Instruct"
LOG_DIR="/var/log/vllm"
# ========== 网络配置 ==========
export NCCL_SOCKET_IFNAME=ib0
export NCCL_IB_DISABLE=0
export NCCL_IB_GID_INDEX=3
export NCCL_NET_GDR_LEVEL=2
export GLOO_SOCKET_IFNAME=ib0
export RAY_NETWORK_INTERFACE=ib0
export VLLM_HOST_IP=$HEAD_IP
mkdir -p $LOG_DIR
# ========== 启动 Ray 集群 ==========
echo "启动 Ray 集群..."
# 头节点 Ray(不持有 GPU)
ray start \
--head \
--node-ip-address=$HEAD_IP \
--port=6379 \
--num-gpus=0 \
--runtime-env="{\"env_vars\": {
\"NCCL_SOCKET_IFNAME\": \"ib0\",
\"NCCL_IB_GID_INDEX\": \"3\"
}}"
# 头节点的 GPU Worker
ray start \
--address="localhost:6379" \
--node-ip-address=$HEAD_IP \
--num-gpus=8
# 等待工作节点加入(工作节点需手动提前执行 ray start --address=...)
echo "等待工作节点加入集群(30s)..."
sleep 30
# 验证集群状态
ray status
TOTAL_GPUS=$(ray status | grep "GPU" | grep "Total" | awk '{print $2}' | cut -d'/' -f2)
echo "集群总 GPU 数:$TOTAL_GPUS"
if [ "$TOTAL_GPUS" != "16" ]; then
echo "❌ GPU 数量不对,期望 16,实际 $TOTAL_GPUS,请检查工作节点"
exit 1
fi
# ========== 启动 vLLM ==========
echo "启动 vLLM 服务..."
exec vllm serve $MODEL_PATH \
--served-model-name qwen3.5-72b \
--host 0.0.0.0 \
--port 8000 \
--dtype bfloat16 \
--tensor-parallel-size 8 \
--pipeline-parallel-size 2 \
--distributed-executor-backend ray \
--distributed-timeout 1800 \
--max-model-len 32768 \
--gpu-memory-utilization 0.85 \
--max-num-seqs 128 \
--enable-prefix-caching \
--performance-mode balanced \
--trust-remote-code \
2>&1 | tee -a $LOG_DIR/vllm_distributed.log
六、踩坑速查表
| # | 坑 | 表现 | 关键修复 |
|---|---|---|---|
| 1 | NCCL 握手超时 | 卡住 300s 后 timeout | NCCL_SOCKET_IFNAME 指向正确网卡 |
| 2 | 头节点占用 GPU | Ray GPU 数少 8 | 头节点 --num-gpus=0 |
| 3 | 模型路径不一致 | Worker 找不到模型 | NFS 共享或 Docker -v 统一挂载 |
| 4 | PP size 配置错 | 节点资源浪费 | pp_size = 节点数,tp_size = 节点内 GPU 数 |
| 5 | PP 气泡利用率低 | GPU 利用率 50~60% | --max-num-seqs 256 + --enable-chunked-prefill |
| 6 | Ray Compiled DAG 崩溃 | 100~5000 请求后 Socket closed |
VLLM_USE_RAY_COMPILED_DAG=0 + 保活脚本 |
| 7 | IB 回退到 Gloo | 带宽极低,延迟高 | 检查 NCCL_IB_GID_INDEX |
| 8 | 实际显存超出预估 | OOM | --gpu-memory-utilization 0.80 |
| 9 | 环境变量 Worker 不生效 | NCCL 用错网卡 | ray start 前设置或用 --runtime-env |
| 10 | 跨节点纯 TP 反而慢 | TTFT 比 TP+PP 高 2~3 倍 | 节点间走 PP,节点内走 TP |
参考资料
- vLLM 分布式推理官方文档
- Distributed Inference with vLLM(Red Hat Developer)
- vLLM Multi-Node Production Deployment 2026(Spheron)
- The vLLM MoE Playbook: TP, DP, PP and Expert Parallelism(ROCm Blogs)
- KubeRay Troubleshoot Multi-Node GPU Serving(Ray 官方)
- vLLM GitHub Issue #19684:多节点 NCCL 错误
- Ray GitHub Issue #59404:Ray Compiled DAG 多节点崩溃
- vLLM Parallel Config API 文档
多机分布式部署是 LLM 运维里最复杂的一关。踩坑的时候请记住:先排网络,再排 Ray,最后排 vLLM。90% 的多机问题都出在网络和环境一致性上,不是 vLLM 本身的 bug。如果遇到了本文没有覆盖的坑,欢迎评论区补充。