2910 字
约 9 分钟
19
S23-X07 · 分布式推理:张量并行与流水并行

单个模型装不进一张卡,是这一层最先撞上的墙。三种走法:量化压小、切分摊开、把一部分权重丢到 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. 自测

  1. 模型装不下时,为什么要先试量化而不是先上 TP?
  2. TP 跨机后哪一阶段先变差,为什么?
  3. P=4、D=8 时气泡占比大概多少?怎么降低?
  4. 8 卡机要跑两个模型实例,TP 和 DP 该怎么配?
  5. 为什么扩容后吞吐不涨,看平均利用率会误判?
  6. 大 TP 单副本与小 TP 多副本,各自适合什么 SLA?
S23-X07 · 分布式推理:张量并行与流水并行
https://www.clxhxhhr.top/posts/5931/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。