这篇教程拆解一个真实场景:模型大到单机 8 卡装不下(或者装得下但 AI 词典:KV Cache">KV Cache 不够用),需要把权重切到两台 DGX 上,用 vLLM 的多节点张量并行跑起来,再通过压测把输出吞吐稳定推到 600+ tok/s 这一档。
文中出现的吞吐数字(例如 607.8 tok/s)来自特定测试环境下的实测结果,只作为调优目标参考。你的机器互联带宽、模型精度、输入输出长度分布不同,结果会明显不同——复现思路一致,具体数值以你自己压测为准。模型权重仓库名、镜像 tag、vLLM 版本号请一律以官方文档当前版本为准。
适用场景
手头有两台各 8 卡的 DGX(或同级别 GPU 服务器),要部署一个单机显存放不下的超大规模 MoE / 长上下文模型,并且对首 token 延迟和整体吞吐都有要求。这套方案适合做内网推理服务、模型评测基线、私有化交付前的性能验证。如果你只是想让几个人试用,单机 + 量化就够了,不必上多节点。
环境与前置条件
- 操作系统:Ubuntu 22.04 LTS 或同代服务器发行版,两台机器内核版本保持一致。
- 驱动 / CUDA:GPU 驱动、CUDA 版本两台完全一致,用
nvidia-smi右上角确认。驱动不一致是多节点故障的高发原因。 - GPU 互联:每台机器 8 卡,卡间走 NVLink/NVSwitch;跨机必须有一张 RDMA 网卡(InfiniBand 或 RoCE),千兆/万兆以太网只能跑通、跑不快。
- 运行时:Python 3.10+,vLLM 主版本与 CUDA 编译版本匹配(以官方文档当前版本为准);需要 Ray 作为多节点分布式执行后端。
- 内存:每节点系统内存 ≥ 模型权重体积的 1.5 倍(加载时会先落到内存再分发)。
- 显存:
模型参数量 × 每参数字节数 ÷ 16要显著小于单卡显存。以 FP16/BF16 部署为例,权重总量 800 GB 时,16 卡平均每卡 50 GB 权重,再留出 KV Cache 和激活的空间。 - 磁盘:模型权重放在两台机器都能访问的同一路径下(NFS / 并行文件系统),或者两台各存一份、路径完全一致。预留 ≥ 权重体积 2 倍的磁盘。
- 网络:两台机器同网段,能互相
ping通,防火墙放行 Ray 端口(默认 6379、8265)和 vLLM 通信端口。
分步骤部署
第 1 步:确定并行切分方案
先算清楚怎么切。设模型有 N 个注意力头、L 层、16 张卡:
- 优先用张量并行(TP):
--tensor-parallel-size 16,把每层的权重按头和中间维度切成 16 份。要求注意力头数能被 TP 整除,且单层权重切片后放得进单卡。 - 如果头数不够整除,退一步用 TP × PP 组合:例如
--tensor-parallel-size 8 --pipeline-parallel-size 2,每台机器内部做 8 路张量并行,两台机器之间做流水线并行。代价是多一段流水线气泡,吞吐略降但更容易跑通。 - 跨机 TP 的通信量远大于 PP,如果 RDMA 带宽有限(比如只有单口 100G),先用 TP=8 + PP=2 验证,再考虑上 TP=16。
把结论记下来,后面启动命令直接用。
第 2 步:系统与硬件自检
两台机器分别执行,结果要能对得上。
```bash
驱动与 GPU 数量
nvidia-smi
nvidia-smi topo -m # 确认卡间是 NVLink,不是全部走 SYS
RDMA 网卡状态
ibstat # IB 网卡应显示 State: Active,Rate 为实际速率
ip -br addr # 记下业务网卡名,后面 NCCL_SOCKET_IFNAME 要用
```
成功标志:nvidia-smi 显示 8 张卡且型号一致;nvidia-smi topo -m 里 GPU 之间标 NV#;ibstat 状态 Active。如果 ibstat 看不到设备,先排查驱动和固件,别急着往下走。
第 3 步:写统一的环境变量文件
两台机器都创建 /etc/profile.d/nccl.sh,内容一致,只有 VLLM_HOST_IP 按本机 IP 改:
```bash
RDMA 网卡名,多口用逗号分隔,可用 ibdev2netdev 查询
export NCCL_IB_HCA=mlx5_0,mlx5_1
RoCE v2 一般是 3,IB 环境可用 show_gids 确认
export NCCL_IB_GID_INDEX=3
控制面网卡,写成业务网卡名,不要写 lo
export NCCL_SOCKET_IFNAME=bond0
export GLOO_SOCKET_IFNAME=bond0
允许 GPU 直接读写网卡内存
export NCCL_NET_GDR_LEVEL=SYS
export NCCL_DEBUG=WARN
export NCCL_DEBUG_SUBSYS=INIT,NET
本机 IP,每台机器改成自己的
export VLLM_HOST_IP=<本机业务IP>
```
然后 source /etc/profile.d/nccl.sh 让它生效。
这一步在做的事:告诉 NCCL 走哪张 RDMA 网卡、用哪个 GID、控制面走哪张以太网卡。多节点跑不起来的情况里,一大半是这几个变量没设或设错。
第 4 步:用 nccl-tests 验证跨机通信
在启动大模型之前,先用小工具确认两台机器能高效通信,避免把网络问题误判成模型问题。
```bash
机器 A
./all_reduce_perf -b 8 -e 8G -f 2 -g 8
机器 B(与 A 同时启动)
./all_reduce_perf -b 8 -e 8G -f 2 -g 8
```
成功标志:输出里出现 Avg bus bandwidth,大包(GB 级)的带宽接近网卡标称速率的 70% 以上,且没有 NCCL WARN。带宽只有几十 MB/s,说明流量走了 TCP 回退,回头检查 NCCL_IB_HCA 和 GID。
第 5 步:准备模型权重
在两台机器上使用完全相同的路径,例如 /models/glm:
```bash
ls /models/glm
config.json tokenizer_config.json *.safetensors ...
```
如果是 NFS,确认两台机器 ls 出来的文件列表和大小一致;如果是本地各存一份,用 sha256sum 抽查几个分片确认一致。路径不一致会导致 worker 端起不来。
第 6 步:拉起 Ray 集群
先检查端口:
```bash
ss -lntp | grep -E '6379|8265'
```
头节点(机器 A):
```bash
ray stop --force
ray start --head \
--node-ip-address=<机器A业务IP> \
--port=6379 \
--dashboard-host=0.0.0.0 \
--num-gpus=8 \
--disable-usage-stats
```
工作节点(机器 B):
```bash
ray stop --force
ray start \
--address=<机器A业务IP>:6379 \
--node-ip-address=<机器B业务IP> \
--num-gpus=8 \
--disable-usage-stats
```
回到头节点验证:
```bash
ray status
```
成功标志:输出里 Resources 一段显示 GPU: 16.0,Node status 里有两个节点,状态都是 ALIVE。这时候 Ray 的 Dashboard 也能在 http://<机器A业务IP>:8265 打开。
第 7 步:启动 vLLM 服务
在头节点执行。注意 --distributed-executor-backend ray 不能省,否则 vLLM 会用单机的 multiprocessing 后端,worker 节点被晾在一边。
```bash
vllm serve /models/glm \
--served-model-name glm-5.3 \
--tensor-parallel-size 16 \
--distributed-executor-backend ray \
--gpu-memory-utilization 0.90 \
--max-model-len 32768 \
--max-num-seqs 256 \
--enable-chunked-prefill \
--kv-cache-dtype fp8 \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000
```
几个参数的作用,按需调整:
--gpu-memory-utilization 0.90:显存切分的关键旋钮。vLLM 先按比例吃掉显存做权重和激活,剩下的全部拿来做 KV Cache。压到 0.95 能多出缓存,但留给 CUDA Graph 和碎片的余量变少,容易在压测中途 OOM。--max-model-len:直接决定 KV Cache 单条占用。上下文长度翻倍,单请求缓存近似翻倍,能同时服务的请求数就减半。--max-num-seqs:同时在跑的序列数上限,是吞吐调优的主要手段之一。--enable-chunked-prefill:长输入场景下把 prefill 分块,避免一个超长请求把其他请求的 decode 卡住,对 TPOT 稳定性帮助明显。--kv-cache-dtype fp8:KV Cache 用 FP8 存,缓存容量大约翻倍,代价是极轻微的质量损失,需要你的 GPU 架构支持。
启动过程中会有 1~3 分钟(大模型更久)的权重加载和显存 profiling。日志里看到类似 KV cache blocks: N、Maximum concurrency for ... requests 和 Application startup complete,就说明服务起来了。
验证部署是否成功
1. 健康检查
```bash
curl -s http://<机器A业务IP>:8000/health -o /dev/null -w '%{http_code}\n'
```
预期返回 200。
2. 模型列表
```bash
curl -s http://<机器A业务IP>:8000/v1/models | python3 -m json.tool
```
预期能看到 "id": "glm-5.3"。
3. 跑一次真实推理
```bash
curl -s http://<机器A业务IP>:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "glm-5.3",
"messages": [{"role": "user", "content": "用三句话解释张量并行"}],
"max_tokens": 256,
"temperature": 0.2
}' | python3 -m json.tool
```
预期返回结构完整的 choices[0].message.content,且 usage.completion_tokens 大于 0。
4. 确认两台机器都在干活
服务跑推理时,在两台机器上分别执行:
```bash
nvidia-smi --query-gpu=index,utilization.gpu,memory.used --format=csv
```
预期:两台机器的 8 张卡都有显存占用(每卡几十 GB)且 GPU 利用率同时抬起来。如果只有机器 A 在动,说明请求没分发出去,回看第 7 步的 backend 参数。
5. 压测看吞吐
```bash
vllm bench serve \
--backend openai-chat \
--model glm-5.3 \
--base-url http://<机器A业务IP>:8000 \
--endpoint /v1/chat/completions \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 256 \
--num-prompts 500 \
--max-concurrency 32 \
--request-rate inf
```
预期输出里有 Request throughput、Output token throughput、Mean TTFT、Mean TPOT。要冲 600+ tok/s 这一档,做法是固定输入输出长度,把 --max-concurrency 从 8 开始按 8 / 16 / 32 / 64 / 128 逐档跑,记录每一档的输出吞吐和 TPOT,画出曲线找到拐点——吞吐不再涨、TPOT 明显变差的那一档,就是这台机器的实际工作区间。
调参的几个有效顺序:先加大 --max-num-seqs(配合并发),再看 KV Cache 是否够(日志里的 blocks 数),不够就 --kv-cache-dtype fp8 或降 --max-model-len,最后才动 --gpu-memory-utilization。通信瓶颈明显的,先把 --tensor-parallel-size 降到 8 加 --pipeline-parallel-size 2 对比一下。
常见报错与解决
1. NCCL WARN ... no device found / 跨机通信直接卡住
原因:NCCL_IB_HCA 写的网卡名和实际不符,或者容器的 /dev/infiniband 没挂进去。
解决:
```bash
ibdev2netdev # 查出真实网卡名
export NCCL_IB_HCA=<真实名>
容器启动时确保挂载
docker run --gpus all --network=host --ipc=host \
--device=/dev/infiniband \
-v /etc/profile.d/nccl.sh:/etc/profile.d/nccl.sh ...
```
2. OSError: /models/glm does not exist 或 worker 端加载权重失败
原因:两台机器模型路径不一致,或者用了 NFS 但挂载点没同步。
解决:
```bash
两台机器都执行,输出应完全一致
ls -l /models/glm | md5sum
mount | grep /models
```
统一路径后 ray stop --force 再重新拉起集群。
3. ValueError: Total number of attention heads must be divisible by tensor parallel size
原因:模型的注意力头数不能被 16 整除,TP=16 不成立。
解决:改用组合并行。
```bash
--tensor-parallel-size 8 --pipeline-parallel-size 2
```
4. No available memory for the cache blocks / 启动时 OOM
原因:--gpu-memory-utilization 太高,或者 --max-model-len 太大,权重加载完没有余量做 KV Cache。
解决:依次尝试
```bash
--gpu-memory-utilization 0.85
--max-model-len 16384
--kv-cache-dtype fp8
```
5. Ray worker 一直 PENDING,ray status 里 GPU 数只有 8
原因:worker 节点没连上头节点,或者端口被防火墙拦了。
解决:
```bash
头节点放行
sudo ufw allow 6379/tcp && sudo ufw allow 8265/tcp
worker 上重连
ray stop --force
ray start --address=<头节点IP>:6379 --num-gpus=8 --disable-usage-stats
ray status
```
6. 压测中途 CUDA out of memory,但启动时正常
原因:并发上来后激活内存和 CUDA Graph 占位叠加,启动时的余量被吃掉。
解决:降 --max-num-seqs 或 --max-model-len,同时关掉不必要的图捕获:
```bash
--enforce-eager # 先排除 CUDA Graph 因素,稳定后再开回来
```
后续维护
备份:把 /etc/profile.d/nccl.sh、Ray 启动脚本、vLLM 启动命令(建议写成 serve.sh)、压测脚本和每轮压测的原始输出,一起放进 Git 或配置仓库。压测记录要带日期、模型版本、参数组合和结果数字——只记“跑到过 600+”,下次就复现不出来了。
升级:两台机器必须同时升级,不允许一高一低混跑。升级顺序是先 ray stop --force 停服务,再统一换 vLLM / Ray / 驱动版本,然后重跑第 4 步的 nccl-tests 和第 5 步的压测基线,确认吞吐没有回退再切流量。镜像 tag 和版本号以官方文档当前版本为准,升级前先在测试环境验证。
日志:vLLM 日志按天切割,保留至少两周;NCCL_DEBUG=WARN 常开,出问题时临时调成 INFO 复现一次。Ray 的 worker 日志在 /tmp/ray/session_latest/logs/ 下,多节点故障第一现场就在这里。
监控:vLLM 自带 Prometheus 指标,curl http://<机器A业务IP>:8000/metrics 能看到队列长度、KV Cache 使用率、TTFT 等。建议采集这几个:
vllm:gpu_cache_usage_perc:KV Cache 使用率持续 > 0.9 就该扩容或降并发vllm:num_requests_waiting:等待队列长期非零,说明并发上限设低了vllm:time_to_first_token_seconds:P99 突增通常是长输入把 prefill 挤爆了
再配合 DCGM Exporter 采 GPU 的温度、功耗、XID 错误,两台机器的指标放同一个面板对照看——两边显存使用率或利用率明显不对称,基本就是切分不均或者网络单边拥塞。
日常巡检:每周跑一次小规模压测(比如 100 条请求),和基线对比;每月检查一次 RDMA 网卡的误包计数(ibstat 里的 Symbol errors、PortRcvErrors),网卡劣化是吞吐缓慢下滑的隐形原因。
