单个模型装不进一张卡,是这一层最先撞上的墙。三种走法:量化压小、切分摊开、把一部分权重丢到 CPU/磁盘。顺序不该颠倒——先量化,再并行,最后才 offload。量化是零通信的收益,并行要付 NCCL 通信与故障域扩大的代价,offload 则直接把单 token 延迟拉到不可接受。
下面只谈切分这一条路,以及为什么"能切"不等于"该切"。
1. 三条路线的取舍
| 路线 | 收益 | 代价 | 何时优先 |
|---|---|---|---|
| 量化(FP8/INT8/AWQ) | 显存降 1/2~1/4,无额外通信 | 精度损失、部分算子回退 | 永远先试 |
| 张量并行 TP | 单层内切权重,单请求就能变快 | 每层一次 all-reduce,强依赖互联 | 单卡装不下且互联好 |
| 流水并行 PP | 按层切,跨机也能用 | 有流水气泡,需多微批填满 | 跨机、卡多、吞吐优先 |
| offload 到 CPU/磁盘 | 显存够用了 | 单 token 延迟量级恶化 | 只做兜底或离线批处理 |
一张 80GB 卡跑 70B 级模型:FP16 权重约 140GB,两个数量级地装不下;量化到 FP8/INT8 约 70GB,仍要留 KV cache 与激活的余量,于是必须切。先算权重 + KV cache + 激活三笔账,再决定切不切,别拿参数量直接除显存。
2. 张量并行:切权重,每层一次通信
TP 把单个矩阵按行或列切开。列切(ColumnParallelLinear)后接行切(RowParallelLinear),两层之间的中间结果保持切分状态,从而在整层里只需要一次 all-reduce 而不是两次。
| 维度 | 切分对象 | 通信点 | 备注 |
|---|---|---|---|
| 列切 | 输出维度 | 无(各卡独立算一部分输出) | 常配在 QKV、gate_up 投影 |
| 行切 | 输入维度 | 求和需 all-reduce | 常配在 o_proj、down_proj |
| 词表切 | lm_head / embedding |
输出 gather | 省显存,末层有通信 |
# vLLM 单机 8 卡 TP=8,按 vLLM 0.6.x 参数名核对
vllm serve /models/Qwen2.5-72B-Instruct \
--tensor-parallel-size 8 \
--pipeline-parallel-size 1 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--enable-prefix-caching
TP 对互联极度敏感。同机 NVLink 的 all-reduce 带宽是 PCIe 的数倍到十几倍量级,跨机走 IB/RoCE 又是另一档;同样的 TP 配置,跨机后 decode 阶段 TPOT 会明显变差,因为 decode 每 token 都要跑一遍全部层的 all-reduce。经验判断:TP 尽量不跨机,超过一台 8 卡机的规模,用 PP 或副本横向扩,而不是把 TP 拉到 16。
3. 流水并行:切层,用微批填气泡
PP 把模型按层段切开,每段一张卡。空转来自"下游在等上游":设备数 P、微批数 D 时,稳定期每轮有 (P-1) 个微批在管道里,气泡占比约 (P-1)/(D+P-1)。
| 变量 | 影响 | 调法 |
|---|---|---|
P(流水段数) |
段多则单卡显存省,气泡也多 | 跨机时用,P 不超过机数 |
D(微批数) |
D 越大气泡越小,但每批变小 |
让 D ≥ 4P 量级,按实测调 |
| 微批大小 | 太小则单批算力利用率低 | 单微批 × D ≈ 目标并发 |
| 层数整除 | 层数必须能被 P 整除或近似均分 |
按模型实际层数核对 |
# 两台 8 卡机:TP=8 机内,PP=2 跨机,共 16 卡
vllm serve /models/Llama-3.1-70B-Instruct \
--tensor-parallel-size 8 --pipeline-parallel-size 2 \
--max-num-seqs 256 --max-num-batched-tokens 8192
--max-num-seqs 太小会让 D 不足,气泡填不满;太大则每微批变小、单批效率下降。这是 PP 最主要的调参矛盾,必须实测 TTFT 与 TPOT 两个指标一起看。
4. 副本(DP):加吞吐,不加单请求速度
同一个模型起多份副本,前面的网关做负载均衡。它解决的是"并发上不去",不是"单请求太慢"。
| 目标 | 该加什么 | 不该加什么 |
|---|---|---|
| 单请求更快 | 更大的 TP(互联允许时) | 副本数 |
| 总吞吐更高 | 副本数、批处理 | 更大的 TP(反而降低利用率) |
| 更长上下文 | KV cache 显存、prefix caching | 副本数 |
副本数是运维上最省事的一维:无通信、故障隔离、可滚动升级。先横向加副本,再考虑切得更细——除非单请求延迟已经是瓶颈。
5. 专家并行(EP)与 MoE 的负载不均
MoE 每 token 只激活少数专家,显存按"全部专家"算,算力按"激活专家"算,于是显存与算力解耦。代价在路由不均:热门专家排队,冷门专家空转。
| 现象 | 原因 | 处理 |
|---|---|---|
| 部分 rank 显存/利用率打满 | 路由倾斜,专家集中 | 看每 rank 指标,不看平均 |
| 平均利用率不高但延迟抖动大 | 少数请求踩到热点专家 | 加 capacity factor、开专家负载均衡 |
| 扩容后吞吐没涨 | 瓶颈在 all-to-all 通信而非算力 | 检查是否该改 TP/EP 配比 |
EP 的通信是 all-to-all,比 all-reduce 更难预测。看平均数会得出错误结论:8 个 rank 里 1 个打满、7 个空闲,平均 12.5%,但整体延迟由那个打满的 rank 决定。
# 按 rank 打散看,而不是看聚合值
curl -s localhost:8000/metrics | grep -E 'gpu_cache_usage|num_requests_running'
watch -n1 'nvidia-smi --query-gpu=index,utilization.gpu --format=csv,noheader'
6. 组合约束:能整除才谈得上并行
assert heads % TP == 0 # 注意力头数要能按 TP 均分
hidden_size % TP == 0
num_layers % PP == 0 # 层数按流水段均分
TP × PP × DP == 可用 GPU 数 # 不能留半张卡
assert micro_batch * DP <= TARGET_CONCURRENCY # 否则队列堆在网关
assert max_num_seqs >= 4 * PP # 否则流水气泡填不满
任何一条不满足,框架要么报错,要么静默退化到更小的并行度。最常见的失败是 TP 设成了不能被头数整除的值(如 64 头配 TP=6),启动时才发现。
还有一条软约束:TP × PP 越大,单个请求跨越的卡越多,故障域越大,长尾越重。
7. 通信成本的量级判断
不用精确建模,只需判断"这次通信是否可能吃掉收益":
| 并行方式 | 每 token 通信量级 | 判断 |
|---|---|---|
| TP | 每层 2 次 all-reduce,量与 hidden_size × batch 成正比 |
层数 × 每层开销,decode 阶段最敏感 |
| PP | 每段边界传一次激活 | 只有段数级,量小 |
| EP | all-to-all,量与激活专家数相关 | 最难预测,网络差时收益为负 |
| DP | 无(仅网关分发) | 可忽略 |
decode 每生成一个 token 都要跑完整网络,TP 的 all-reduce 次数与"层数 × token 数"同阶;prefill 批量大,通信被计算掩盖更多。这就是长上下文 prefill 场景 TP 尚可、短请求 decode 场景 TP 拖后腿的原因。
# 判断是算力瓶颈还是通信瓶颈:decode 期间看卡利用率与 NCCL 带宽
nvidia-smi --query-gpu=index,utilization.gpu,memory.used --format=csv -l 1
nvidia-smi topo -m # 确认 NVLink / PCIe / SYS 拓扑
8. 部署形态的连带影响
| 变化 | 连带影响 | 应对 |
|---|---|---|
| 一个 Pod 占多卡 | 故障域放大,一卡掉全 Pod 挂 | 用 nodeAffinity 绑同机;做多副本 |
| 单请求跨卡 | 尾延迟变重,P99 明显高于 P50 | 按 P99 而不是均值定容量 |
| 启动要加载并切分权重 | 冷启动从秒级到分钟级 | startupProbe + initialDelaySeconds |
| 卡型号不一致 | NCCL 降速、甚至启动失败 | 节点池按卡型分组,禁止混部 |
startupProbe: # 权重加载 + 切分需要时间,先别判死
httpGet: { path: /health, port: 8000 }
periodSeconds: 10
failureThreshold: 60 # 给到约 10 分钟
readinessProbe:
httpGet: { path: /health, port: 8000 }
periodSeconds: 5
failureThreshold: 2
livenessProbe: # 只探进程存活,别探模型可用性
httpGet: { path: /health, port: 8000 }
periodSeconds: 20
failureThreshold: 3
startupProbe 存在期间 k8s 不跑 livenessProbe,这条机制正好用来吸收权重加载的分钟级空窗;不设就会在启动中被反复 kill。
9. 容量与成本的取舍
大规模 TP 让单请求更快,但把卡焊死在一个大 Pod 里:请求少时这些卡也不能拆开给别人用。所以有两种截然不同的优化方向:
- 延迟优先:大 TP + 单副本,追求 TTFT/TPOT,利用率注定不高。
- 成本优先:小 TP/单卡 + 多副本 + 批处理,追求 token/s/卡,延迟靠排队吸收。
同一个模型不可能同时最优化两头。先定 SLA,再选切法;SLA 只写了"平均延迟"就等于没定,P99 才是容量依据。
10. 常见坑
| 坑 | 现象 | 改法 |
|---|---|---|
| 把 TP 拉到跨机 | decode 变慢,卡越多越慢 | TP 限机内,跨机用 PP |
| PP 段数大于微批数 | 有段始终空转 | 提高 max_num_seqs 或降 PP |
| 头数不能被 TP 整除 | 启动报错或静默退化 | 先算 heads % TP |
| 只看平均利用率 | 扩容无效却找不到原因 | 看每 rank 指标 |
| 不同卡型混部 | 吞吐差异大或 NCCL 报错 | 节点池按卡型隔离 |
| 用均值定容量 | 线上 P99 超 SLA | 按 P99 + 冗余系数 |
| 大 Pod 单副本 | 一次重启掉整个服务 | 至少 2 副本跨节点 |
11. 排错
| 现象 | 排查方向 |
|---|---|
| 加卡后吞吐没涨 | 瓶颈在通信;看 NCCL 带宽与 TP 是否跨机 |
| 启动卡在加载权重 | 存储带宽不足;量化后仍按 FP16 读盘会翻倍 |
| P99 远高于 P50 | 单请求跨卡 + 批处理排队;拆小并行度加副本 |
| 只有部分 rank OOM | KV cache 分配不均或 EP 路由倾斜 |
偶发 NCCL timeout |
卡间链路抖动或某卡降速;查 nvidia-smi 与拓扑 |
| 滚动升级期间大量 5xx | 大 Pod 重启慢,副本数太少 |
12. 边界
| 场景 | 更适合的做法 |
|---|---|
| 模型能装进单卡 | 不并行,直接加副本 |
| 延迟不敏感、只求吞吐 | 大 PP + 大批处理,甚至离线批跑 |
| 卡间只有 PCIe 无 NVLink | TP 限制在 2~4,其余靠副本 |
| 多租户共享小模型 | 副本 + 网关路由,不做并行 |
| 显存差一点点 | 量化或缩 max-model-len,别急着切 |
13. 自测
- 模型装不下时,为什么要先试量化而不是先上 TP?
- TP 跨机后哪一阶段先变差,为什么?
P=4、D=8时气泡占比大概多少?怎么降低?- 8 卡机要跑两个模型实例,TP 和 DP 该怎么配?
- 为什么扩容后吞吐不涨,看平均利用率会误判?
- 大 TP 单副本与小 TP 多副本,各自适合什么 SLA?