1. 先搞清楚 Tensor Parallel 到底在并行什么
1.1 模型为什么放不进一张卡 —— 显存墙
做推理部署的人,迟早会撞上显存这堵墙。以 70B 参数量的大模型为例,哪怕用 FP8 量化,光权重就得占 70GB 左右。现在消费级显卡 24GB 是常态,数据中心里 A100/H100 大多也就 80GB,一张卡纯放权重都紧张,更别说推理过程中还得留出 KV Cache、激活值、临时缓冲区的空间。
我见过不少团队一开始的方案是“既然一张卡放不下,那就拆到多张卡上呗”。这句话听上去没毛病,但拆法有讲究。最常见的三种并行方式:数据并行(Data Parallel, DP)、流水线并行(Pipeline Parallel, PP)、张量并行(Tensor Parallel, TP)。DP 是每张卡放一份完整模型,处理不同的 batch,它解决的是吞吐问题,不解决显存问题;PP 是把模型按层切开,前一层算完传给后一层,每张卡只负责其中几层,显存压力确实下降不少,但它天然有流水线气泡,而且层与层之间是串行依赖的。
TP 的思路更激进:把某一层内部的矩阵运算切开,多张卡一起算这一层。也就是说,权重矩阵被横七竖八地拆到了多张卡上,每张卡只持有完整矩阵的一个分片。前向计算时,多张卡协同完成一次矩阵乘法,然后再把结果拼起来或者叠加起来。
听起来 TP 是最能解决显存问题的方案,也确实如此。但这里埋着一个大坑:矩阵被切开之后,卡与卡之间需要频繁同步中间结果,这个同步开销会随卡数增加而暴涨。很多人第一次跑 TP 推理时都会懵:我加了卡,速度怎么不升反降?这篇文章就把这个反直觉的现象掰开揉碎讲清楚。
1.2 TP 的两种切法:按行切与按列切
要说清楚 TP 为什么有通信开销,得先看它到底怎么切矩阵。一次典型的前向计算可以简化为 Y = X × W,X 是输入激活矩阵,W 是权重矩阵。TP 的切法有两种:按行切 W 和按列切 W。
按列切 W 比较简单。把 W 按列切成两块 W1 和 W2,分别放在 GPU 0 和 GPU 1 上。输入 X 需要完整地发给两张卡,各自计算 Y1 = X × W1,Y2 = X × W2。因为 W1 和 W2 是 W 的左右两半,所以 Y1 和 Y2 拼接起来就是完整的 Y。这个拼接动作在 All-Gather 类通信中完成,通信量等于 Y 的完整大小。
按行切 W 稍微绕一点。把 W 按行切成上下两块 W1 和 W2,分别放在两张卡上。输入 X 也要按列切成 X1 和 X2,分别送到对应的卡上。GPU 0 算 Z1 = X1 × W1,GPU 1 算 Z2 = X2 × W2。注意,Z1 和 Z2 是部分和,要得到最终结果 Y = Z1 + Z2,必须把两张卡的结果加起来。这个加法在 All-Reduce 通信中完成,通信量也等于 Y 的完整大小。
注意:现代大模型普遍采用“按行切 + All-Reduce”的方式,因为它在后续的激活函数、Dropout 等操作上更自然。但无论哪种切法,每一层 Transformer Block 里至少有一次跨卡同步,一次 All-Reduce 意味着每张卡既要发数据也要收数据,通信量随卡数线性打满。
1.3 为什么 TP 需要“全局同步”
Transformer Block 里有两个主要的矩阵运算块:Attention 的 QKV 投影和输出投影,MLP 的 up/gate 投影和 down 投影。每一处投影如果做了 TP,就需要一次跨卡同步。
以 MLP 为例:up 投影先按列切,各卡独立算完;激活函数逐元素操作,不需要通信;但 down 投影按行切,这时候必须做 All-Reduce,把两张卡的部分结果累加起来。也就是说,一个 MLP 块至少一次 All-Reduce,Attention 块至少两次。算下来,一层 Transformer 至少三次跨卡同步。
有人可能会想,All-Reduce 不就等所有卡算完,然后互相发一下数据吗?听起来很简单,但实际开销远不止“发数据”本身。All-Reduce 需要经过 Reduce-Scatter 和 All-Gather 两个阶段,每一阶段都是两两通信,还有一个隐式的全局同步屏障——所有卡必须等最慢的那张卡算完才能进入通信。
这就是问题的核心:模型被拆到多张卡上之后,计算从“单卡串行”变成了“多卡并行 + 跨卡同步”。如果计算时间降幅小于通信时间增幅,总耗时反而上升。多加 GPU,推理可能更慢——这句话不是玩笑,是实测里经常发生的事。
2. 多加 GPU 反而更慢的三个核心原因
2.1 通信才是真正的瓶颈:All-Reduce 的代价
为了说清楚这个问题,我拿一个具体例子做估算。假设我们有一个 13B 参数的模型,每层 hidden size 为 5120,MLP 的中间维度为 13824。用 FP16 推理时,一个 Token 在 MLP down 投影上的激活矩阵大小大约是 5120 × 1(单 Token 推理时序列长度视为 1)。
听起来很小对吧?5120 个元素,FP16 下才 10KB。但实际推理不会只处理一个 Token——生产环境里通常是连续批处理(continuous batching),一个 batch 可能包含几十甚至上百个序列,每个序列又有几百个 Token。假设 batch size × 序列长度 = 4096 个 Token,那么激活矩阵就是 5120 × 4096,FP16 下约 40MB。
在 8 卡 TP 配置里,down 投影的 All-Reduce 需要将这 40MB 在 8 张卡之间做归约和广播。通信量不能简单算 40MB,实际是每张卡发送约 7/8 的数据再接收 7/8 的数据,总数据量接近 70MB 级别。
拿数据说话:单张 H100 的 NVLink 双向带宽约 900GB/s,看起来很高,但 70MB 在这种带宽下也要约 78 微秒。而 13B 模型在单卡上算一个 layer 的前向,在 H100 上大概也就 100 到 200 微秒。也就是说,一次 All-Reduce 的通信时间几乎相当于一次完整计算的时间——翻一倍开销。
这里还没算通信发起时的延迟(latency)。NVLink 的点对点延迟通常在 1 到 2 微秒,看似可以忽略,但 All-Reduce 包含多个阶段,并且每层都要做,累计起来非常可观。一个 80 层的模型,每层 3 次 All-Reduce,总共 240 次,每一次 80 微秒——光通信就要 19.2 毫秒,这还不包括计算时间。
2.2 同步屏障:木桶效应被放大
TP 天然带全局同步屏障。All-Reduce 的语义是所有卡先把本地结果算完,然后才能开始归约。这意味着,如果一个 batch 里某些 Token 的 attention 计算路径特别长,或者某张卡因为显存带宽争抢而变慢,所有卡都得等它。
单卡推理时,不同 Token 之间的计算差异会被 GPU 内部的并行调度消化掉一部分。但 TP 多卡环境下,最慢的那张卡决定了整次迭代的耗时。这个现象在混合 batch 大小不均、或者序列长度差异很大的推理场景里特别明显。
我实测过一个场景:输入里有几个特别长的序列,它们需要更大的 KV Cache,attention 计算量也更大。在单卡上,有长序列的请求只是让整体延迟高一些,但如果这些长序列恰好被分配到了同一张卡上(TP 切分是按矩阵维度切的,不是按序列维度切的,所以理论上长序列的计算同样被切分到所有卡,但不同卡上 KV Cache 的显存占用、内存访问模式可能有差异),那张卡就变成瓶颈卡,整个 TP 组的耗时被拉高。
同步屏障还有一个容易被忽视的副作用:它限制了批处理带来的吞吐提升。TP 的 batch size 变大时,虽然计算时间增长,但通信量也成比例增长。在通信占比已经很高的情况下,增大 batch 带来的算力收益会被通信开销吃掉不少,这也是为什么大 batch 下 TP 扩展性更差的原因之一。
2.3 计算量没变,通信量却叠加了
关于 TP 效率,一个常见的误解是:8 张卡一起算,计算时间不是应该变成 1/8 吗?理论上确实如此。8 张卡各算 1/8 的矩阵乘法,计算时间可以接近 1/8。但总耗时是“计算时间 + 通信时间 + 调度时间”,通信时间不会因为卡变多而减少,反而会增加。
更扎心的是,计算时间不太可能真的变成 1/8。因为矩阵被切小之后,每张卡上的矩阵乘法规模变小,GPU 的利用率会下降。矩阵乘法要跑得满,需要足够的计算强度(compute intensity)。一个小矩阵的 GEMM 可能连 GPU 的 Tensor Core 都喂不饱,算力利用率只能到 40% 甚至更低。
这相当于:计算时间没有变成 1/8,可能是 1/4 或 1/5;通信时间却从 0 变成了完整的一份甚至更多。整体自然不划算。
实操心得:把模型从单卡换到 2 卡 TP,很多场景下速度是不会翻倍的,甚至下降。我见过最极端的案例:一个 7B 模型在单张 A100 上延迟 45ms,换到 8 卡 TP 后,延迟反而变成 68ms。8 张卡跑不过 1 张卡,这就是通信开销压过算力收益的典型例子。
3. 什么场景适合 TP,什么场景碰都不该碰
3.1 适合 TP 的场景特征
说了半天 TP 的坏话,但它绝不是一无是处。TP 是唯一能解决“单卡显存放不下”的并行方式,在模型大到必须跨卡时是绕不开的。
适合 TP 的第一类场景:模型大到单卡放不下,且没有量化空间。比如 70B 模型在 8 张 A100 上用 FP16 推理,一张卡 80GB 放不下完整权重,就只能用 TP 把权重分片。此时不做 TP 就没法做,属于“没得选”。
适合 TP 的第二类场景:卡间互联带宽极高,且节点内拓扑友好。8 张 H100 通过 NVLink 全互联,通信开销相对可控。相比之下,跨节点走 InfiniBand 做 TP 就非常痛苦,带宽和延迟都差一个数量级,强烈不推荐。
适合 TP 的第三类场景:单请求延迟敏感,且 batch 比较小。TP 能降低单请求的计算延迟,因为矩阵乘法被并行到多卡上。如果 batch 小到通信量相对可控,2 卡或 4 卡 TP 确实能压低延迟。但要记住边界:TP 卡数越多,边际收益越低,通常超过 4 卡就要非常谨慎。
3.2 不适合 TP 的场景特征
最不适合 TP 的场景有两个:一是模型本身单卡就能放下,二是对吞吐要求极高而延迟要求没那么苛刻。
模型单卡能放下时,TP 几乎总是纯亏。原因很简单:单卡推理没有任何跨卡通信,矩阵乘法全额利用 GPU 算力。TP 不但引入通信开销,还因为矩阵变小导致算力利用率下降,双重损失。很多人看网上说“多卡一定比单卡快”,结果是给自己找麻烦。
对吞吐要求高的场景,数据并行(DP)或张量并行 + 数据并行的混合方案通常更好。DP 的处理方式是每张卡独立跑完整模型,处理不同 batch,卡之间只需要在反向传播时同步梯度;推理时 DP 根本不需要同步,天然可扩展。如果在 DP 之上加一点 TP 来分摊显存压力,也可以,但要注意别让 TP 的通信开销反噬吞吐。
我见过一个典型案例:某个团队用 8 卡 A100 做 13B 模型的推理服务,先上了 TP=8,吞吐只有 120 tokens/s;后来改了 DP=8(每卡放一份量化后的模型),虽然显存紧张了些,但吞吐直接跳到 380 tokens/s。这就是选型带来的数量级差距。
3.3 TP、PP、DP 怎么选
三种并行方式各有适用面,合理的方案通常是组合使用,而非只用一种。
| 并行方式 | 解决什么问题 | 通信开销 | 典型卡数 | 适用场景 |
|---|---|---|---|---|
| DP | 吞吐扩展 | 推理无通信 | 8 到 64 | 模型单卡放得下,追求吞吐 |
| PP | 显存扩展 | 流水线气泡 | 2 到 8 | 模型按层切分,层间显存均衡 |
| TP | 显存扩展 + 降低单请求延迟 | 每层高频通信 | 2 到 8(单节点) | 超大模型,卡间互联极好 |
实践中的通用配方:先 DP 横向扩展吞吐,再 PP 纵向切层分担显存,最后在单机内部用 TP 把单层切成更小块。比如 70B 模型做 8 卡部署,常见方案是 DP=2 × PP=2 × TP=2,既保证吞吐,又让卡间通信尽量收敛在 NVLink 域内。
需要特别提醒:TP 的卡数不要跨节点。跨节点的网络带宽再高,也远不如 NVLink,TP 的通信频率注定了它必须在物理上紧耦合的卡之间跑。
4. 实操优化:让多卡推理真正做到 1+1>1
4.1 通算重叠:把 All-Reduce 拆开
既然通信不可避免,那就想办法让通信和计算同时发生,而不是串行等待。这是 TP 性能优化里收益最大的一招。
标准 All-Reduce 的朴素实现是:全部算完 → 统一归约 → 全部广播。但 All-Reduce 本身可以分解成 Reduce-Scatter 和 All-Gather 两个阶段。Reduce-Scatter 是每张卡把本地结果按目标卡切块,发给对应卡做归约;All-Gather 是归约完成后,每张卡再把完整结果广播给所有卡。
聪明的做法是把矩阵乘法的行切分与 Reduce-Scatter 融合。比如 down 投影按行切成了 8 份,每张卡算完自己的那份后,不需要等到所有卡都算完才通信。可以先算完自己负责的那部分行,立刻把这部分结果发给目标卡,同时开始算下一个 Attention 块的 QKV 投影——这就是通信与计算的重叠。
在 PyTorch 生态里,这个优化通常由框架层完成。比如 Megatron-LM 的 Sequence Parallel 和 vLLM 的 TP 实现,都花了大量精力做通算重叠。自己做推理框架的话,最简单的切入点是检查通信是否被同步阻塞:如果 All-Reduce 是阻塞式的,后面所有计算都在等它,那就考虑把矩阵乘法切成更细粒度的 chunk,计算一个 chunk 后立刻通信这个 chunk,流水线式地重叠。
4.2 从 kernel 与框架层压开销
通信优化的另一条线是把通信次数本身变少。Transformer Block 里有几次独立的 All-Reduce,如果能把它们合并成一次,通信延迟直接减半,这就是 kernel fusion 的核心收益。
具体来说,Attention 输出投影和 MLP 的 down 投影之间,可以尝试融合。只要中间没有必须的同步操作,一次 All-Reduce 可以同时归约两个张量,代价只是通信数据量略增,但通信次数和同步屏障次数大幅下降。
vLLM 里做得更绝:它对连续的多个层做了通信合并,把跨层通信合并成更少次数的批量操作。这不是简单的框架参数调整,而是对底层通信原语的重新设计。自己做优化时,至少要做到的一点是:避免在每层内部引入多余的同步,比如不要在矩阵乘法之后立刻做 All-Reduce,再接一个只需要局部数据的操作——应该把 All-Reduce 尽量延迟到真正需要全局数据的那一刻。
框架层还有一个容易被忽视的点:通信后端的选择。NCCL 是首选,但 NCCL 的环境变量对性能影响很大。比如 NCCL_P2P_LEVEL 决定是否走 NVLink P2P,NCCL_BUFFSIZE 决定通信缓冲区大小,这些参数在 TP 场景下直接决定通信效率。用默认配置跑 8 卡 TP,和调过参数的跑,延迟可以差 20% 到 30%。
注意:NCCL 参数调优不是无脑改大就好。NCCL_BUFFSIZE 改得太大可能挤占显存,影响 KV Cache;改小了则通信吞吐上不去。我的习惯是先跑一轮基准测试,用 nvbandwidth 和 nccl-tests 摸清卡间实际带宽和延迟,再反向调参数。
4.3 实测流程与基准测试要点
做 TP 性能调优,第一步不是改代码,而是建立可复现的基准测试流程。我自己固定使用的流程分三层:
第一层,硬件摸底。用 nccl-tests 测 All-Reduce 在不同数据量下的带宽和延迟,从 1KB 测到 512MB。这一步能直观看到当前环境的通信天花板。比如数据量 40MB 时 All-Reduce 延迟 80 微秒,那后续预估推理延迟就按这个数据来,不凭感觉。
第二层,单层前向测试。构造一个 Transformer Block 的单层模型,分别测单卡和 TP 多卡下的前向时间。通过逐层分析定位是通信占比高,还是计算切分导致利用率低。我建议用 PyTorch 的 profiler 导出 kernel 级别的时间线,观察 All-Reduce 旁边是否有空白等待段。
第三层,端到端推理测试。跑真实模型和真实请求分布,记录 P50、P99 延迟和吞吐。如果 P99 明显高于 P50,大概率是某个 batch 里出现了长序列,触发了同步屏障等待。
基准测试最容易犯的错误是只看总延迟,不拆解时间组成。总延迟变差,到底是通信变差、计算变慢、还是负载不均衡?拆开看才能对症下药。我踩过一次坑:某次调 TP 参数,延迟稳中有降,但吞吐也降了,后来发现是批处理大小被悄悄改了,根本没对比到同一条基准线。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| TP 卡数增加,延迟反而上升 | 通信占比过高 / 卡间拓扑不佳 | nccl-tests 测带宽,确认是否走 NVLink |
| 某张卡显存占用明显高于其他卡 | 权重切分不均 / KV Cache 不均 | profiler 查看各卡显存分配,检查切分逻辑 |
| P99 延迟波动剧烈 | 长序列请求触发同步屏障等待 | 按序列长度分组压测,看延迟分布 |
| 吞吐随 batch 增大不涨反降 | 通信量与计算量同比例增长 | 对比不同 batch 下通信时间占比 |
| 8 卡 TP 比 4 卡 TP 更慢 | 通信量线性增长,计算不再线性加速 | 逐卡数测试(1/2/4/8),找拐点 |
具体到排查动作,推理性问题用 profiler 最快。PyTorch 的 torch.profiler 能导出每个通信算子的耗时,NCCL 的通信算子会标记为 ncclKernel_AllReduce。如果看到大量 ncclKernel 且旁边没有计算 kernel 重叠,说明通算没有重叠,需要做算子融合。
如果通信时间本身很长,先用 nvidia-smi topo 查看卡间拓扑。有些服务器是两两 NVLink 但不是全互联,TP 卡选错会导致通信走 PCIe,性能直接掉一个数量级。选卡时尽量选同一 NUMA 节点下、拓扑图上直连的卡。
5.2 容易被忽略的显存分配坑
TP 切分权重时,大部分框架按维度均分。但 Attention 里的 KV Cache 是按层分配的,不同层之间的 Cache 显存占用在不同卡上可能不均。有些模型的 embedding 和 lm_head 是共享参数的(tied embedding),TP 切分时共享权重如果没有正确复制到所有卡,会触发通信时额外的 All-Gather,拖慢速度。
还有个坑是框架层对中间缓冲区的分配。TP 推理时,每层前向都需要额外的通信缓冲区,这部分显存是隐藏开销。有些框架默认分配 2 到 4 倍模型大小的中间缓冲区,直接把 KV Cache 的空间挤掉了。遇到显存明明够但 OOM 的情况,优先检查是不是通信缓冲区吃掉了太多显存,把 NCCL_BUFFSIZE 调小试试。
5.3 我踩过的三个典型坑
第一个坑:只测吞吐不测延迟。有一次优化后吞吐涨了 15%,我正高兴,压测才发现 P99 延迟翻倍。原因是吞吐按 token 数计算,优化让并发变高,但单个请求的前向延迟因为通信串行反而变长了。推理服务既要看吞吐,也要看延迟分位数,两个指标一起看才靠谱。
第二个坑:忽略 CPU 侧的调度开销。TP 多卡推理需要 CPU 频繁发起通信原语,每个 All-Reduce 都涉及 CPU 的 launch。如果 CPU 绑定不合理,或者 numactl 没配好,GPU 的 kernel launch 跟不上,GPU 就在空转等 CPU 下发指令。后来把 CPU 绑核、设置 CUDA_LAUNCH_BLOCKING=0(默认就是异步,一般不需要改),再把 CPU 侧的 busy-loop 改成 blocking 等待,性能立刻改善。
第三个坑:序列长度对 TP 性能的放大效应。短序列时 TP 通信占比高得离谱,因为计算太小,通信延迟占主导;长序列时通信占比下降,TP 效果更好。如果生产请求大多数是短文本,TP 可能不仅没收益,还是负资产。这时候不如干脆用 DP,甚至单卡推理。
收尾:一点个人体会
装机时总有人追求“卡越多越好”,但做推理部署这几年,我最大的体会是:分布式不是灵丹妙药,它只是在你不得不跨卡时,帮你把损失降到最低的手段。TP 尤其如此——它解决了显存放不下的问题,但代价是通信和同步开销。合理评估你的模型大小、卡间互联、请求分布,再决定上不上 TP、上几卡,比盲目堆卡重要得多。
如果你正打算给推理服务加卡,我的建议是先跑一轮 nccl-tests 和单层前向基准,把通信成本和计算收益量化清楚。如果模型单卡放得下,别碰 TP;如果必须跨卡,先从 2 卡 TP 起步测试,逐步加卡找拐点,别一步到位上 8 卡。最后,所有优化都要以 P50/P99 延迟和吞吐的联合基准为准绳,任何单指标优化都可能是陷阱。