一张卡装不下时多卡怎么切:张量并行和流水并行怎么选
服务起不来,日志里一行 OOM,或者是「not enough KV cache space」。绝大多数人的第一反应是「卡不够,再加一张」。
加卡这个动作本身没错,错的是很多人加完卡就直接开跑,压根没想过「加进来的卡要怎么用」。而 vLLM 里给你的不是一个「多卡开关」,是两个参数:tensor_parallel_size 和 pipeline_parallel_size。它们都能让原来装不下的模型装下,但它们解决的是不同的问题,付出的代价也完全不同。选错了,显存确实省下来了,服务也确实起来了,但延迟表现可能比你预期的差一大截——而且你还找不到原因,因为「它明明跑起来了」。
这篇就把这两种切法的机制掰开,然后给一个能落地的选型流程。
两个参数,两种完全不同的切法
先把官方对这两个参数的定位摆出来,这是后面所有判断的地基:
| 参数 | 官方说明要点 |
|---|---|
tensor_parallel_size | 把模型权重切分到多张 GPU,使每张卡有更多显存留给 KV cache |
pipeline_parallel_size | 把模型层分布到多张 GPU,降低每张卡上模型权重所需显存 |
两句话都在说「省显存」,看起来很像。差别藏在「切什么」上:一个切的是权重矩阵本身,一个切的是层的归属。这一字之差,决定了它们在运行时的通信模式截然不同。
张量并行:横着切开每一层
张量并行是把每一层内部的权重矩阵沿某个维度拆开,分给多张卡。也就是说,模型的第 1 层不再完整地待在某张卡上,而是每张卡各持有它的一部分。
这带来的直接后果是:计算一层,需要所有参与的卡协同完成。每张卡算出自己那一片的部分结果,然后要把结果汇总(或者交换),才能得到这一层完整的输出,才能进入下一层。模型有多少层,这个「算一片—汇总—再算一片」的循环就要跑多少轮。
所以张量并行的代价是同步开销:卡与卡之间要极其频繁地通信,而且是阻塞式的——没等到别人的结果,自己就不能往下走。
这意味着张量并行对卡间互联带宽极度敏感,这是它最重要的工程属性:
- 同机多卡:卡插在同一台机器上,走机内的高速互联或 PCIe。通信路径短,是张量并行最舒服的场景。
- 跨机多卡:通信要走网络。张量并行那种「每层都要同步一次」的模式,在网络延迟面前会被急剧放大——单次同步的开销本来不大,但乘以层数之后就不是小数了。
所以有一条经验值得记死:张量并行尽量别跨机。如果你手上是两台各插了若干卡的机器,先把张量并行限制在单机内部,跨机那一层用别的方式解决,而不是让张量并行直接横跨网络。这不是性能调优的细枝末节,是会直接决定服务能不能用的结构性选择。
流水并行:竖着切开层的归属
流水并行是把模型的层按顺序分段,第一段的层放卡 A,第二段放卡 B,依此类推。每一层还是完整的,只是住在不同的卡上。
请求进来之后,先在卡 A 上把前面几层算完,把中间结果传给卡 B,卡 B 接着算,再传给卡 C……像流水线上的工件一样,依次穿过每一张卡。
通信量小得多——卡之间只在分段的接缝处传一次中间结果,不像张量并行那样每层都要同步。这是流水并行的优势。
但代价是延迟:一个请求要串行地经过所有分段。分段越多,这条串行链路越长。更关键的是,在流水线没被填满的时候,同一时刻只有一段在干活,其他段都在等——卡 A 算的时候卡 B 空着,卡 B 算的时候卡 A 空着。
这就带出流水并行的关键特性:它的表现强烈依赖并发量。
- 高吞吐场景:请求源源不断地进来,卡 A 处理完请求 1 交给卡 B 之后,马上可以接手请求 2。流水线被填满,各段都在忙,硬件利用率上得去。
- 低并发场景:一次就来一两个请求,流水线填不满,大部分卡在空转,而单个请求还实打实地要串行走完全程。这时候你既没换来吞吐,还额外付出了延迟。
我见过不少人在测试环境里单请求跑一下,觉得「怎么加了卡反而更慢了」,就是撞在这里——测试方式(单请求)恰好是流水并行最不擅长的场景。
怎么选:先问自己要什么
把上面两段压缩成一句话:张量并行拿通信换延迟,流水并行拿延迟换通信。
所以选型的第一个问题不是「我有几张卡」,而是「我这个服务的目标是什么」。
目标是「装下 + 保低延迟」
优先张量并行,并且尽量把它约束在同一台机器内部。
典型场景:对话式产品、代码补全、任何用户坐在屏幕前等结果的场景。这些场景对首字延迟和整体响应时间敏感,用户能直接感觉到。流水并行那种「串行穿过多张卡」的结构在这里是净损失,尤其是流量还没起来、并发不高的早期阶段。
目标是「装下 + 吞吐优先,延迟可放宽」
流水并行是可以接受的。
典型场景:批量文档处理、离线数据清洗、夜间跑的评测任务、内容批量生成。这些场景的验收标准是「一晚上跑完多少条」,单条慢一点没人在意。流水线能被持续的请求流填满,正好发挥它通信量小的优势。
目标是「模型太大,单一种切法不够」
两者可以组合——比如机内用张量并行,跨机用流水并行。这在结构上是合理的:让通信密集的张量并行待在带宽好的机内,让通信稀疏的流水并行去承担跨机那一跳。
但要清楚代价:复杂度和调试成本会明显上升。出问题时你要判断是切分配置的问题、拓扑的问题、还是网络的问题,排查面一下子变宽。参数怎么组合、有没有额外约束,以官方文档当次版本为准——不同版本对组合方式的支持程度可能不一样,别照抄网上的旧配置。
一个可以直接用的判断流程:
模型单卡装不下
│
├─ 先别急着加卡 → 回到 gpu_memory_utilization / max_num_seqs 等旋钮(见下一节)
│
└─ 确认必须加卡
│
├─ 延迟敏感(用户在等)?
│ └─ 是 → tensor_parallel_size,且尽量同机
│
├─ 吞吐优先、延迟可放宽(批处理/离线)?
│ └─ 是 → pipeline_parallel_size 可接受
│
└─ 单机切不下,必须跨机?
└─ 机内 tensor_parallel_size + 跨机 pipeline_parallel_size
(组合方式以官方文档为准,调试成本预留出来)
多卡在缓解顺序里排在后面,这不是偶然
官方针对 OOM 和「not enough KV cache space」给出的缓解顺序是这样的:
- 提高
gpu_memory_utilization - 降低
max_num_seqs或max_num_batched_tokens - 提高
tensor_parallel_size(代价:同步开销) - 提高
pipeline_parallel_size(代价:延迟)
注意多卡这两步排在第三第四。这个顺序背后是很朴素的经济学:
前两步不需要加硬件,也不损失模型质量。 提高 gpu_memory_utilization 是让 vLLM 更充分地使用你已经买下的显存——官方对它的说明就是提高利用率可以给 KV cache 更多空间。这一步的边际成本接近零,你只是把本来闲置的显存用起来了。
降低 max_num_seqs / max_num_batched_tokens 也不花钱,但开始有代价了——它减少一批里的并发请求数,从而需要更少的 KV cache 空间。代价是并发能力下降。不过这个代价是可度量、可回退的:你调小了,观测到吞吐掉了多少,觉得不值就调回去,几分钟的事。
到第三步才开始动硬件。 加卡意味着真金白银的采购或租用成本,还意味着运维面积扩大:多一张卡就多一个故障点,多一份驱动和拓扑要操心。所以它排在后面——不是因为它效果差,是因为它贵。
第四步排最后,因为它的代价最难回退。 延迟变差不像并发下降那样是个可以随手调回来的旋钮,它是结构性的:只要请求还得串行穿过那几段,延迟就在那里。
这个顺序真正的价值在于它教你怎么花钱:先榨干免费的,再动便宜的,最后才动贵的和难回退的。我见过团队在第一步都没做的情况下直接申请扩容,批下来之后发现瓶颈根本不在卡数上,钱花了,问题还在。关于旋钮和显存之间的账怎么算,可以先看显存估算怎么做,把需求量心里有数了再谈加卡。
卡加倍不等于吞吐加倍
这是加卡前必须先建立的心理预期:多卡的收益不是线性的。三个原因:
通信开销。前面已经讲透了——张量并行每层都要同步,流水并行每段接缝都要传中间结果。这部分开销是新增的,卡越多通常越大。它不产生任何计算价值,纯粹是切分的税。
负载不均。流水并行按层分段,但各层的计算量并不总是均匀的;分段边界一旦切得不好,某一段就会成为整条流水线的节拍瓶颈——最慢的那段决定整体节奏,其他段再快也只能等它。
显存碎片与余量。切分之后每张卡上的显存布局变了,权重、KV cache、激活值、临时缓冲的分布都要重新平衡。不是「原来一张卡装不下,切成两份就正好各装一半」这么干净——切分本身也有额外开销,每张卡还得留出自己的余量。
所以给一条务实的建议:加卡前,先实测出单卡的拐点在哪里。
具体做法是在单卡上逐步加压——把并发从低往高推,记录吞吐随并发变化的曲线。你会看到吞吐先上升,然后增速放缓,最后压到某个点开始掉头或者延迟急剧恶化。那个点就是单卡拐点。
知道拐点有两个用处:一是你能判断当前离天花板还有多远,说不定调调批处理相关的旋钮就够用了,根本不用加卡;二是加卡之后你有了对照基准——新配置的吞吐相比单卡拐点提升了多少,这个比值才是你评估「这卡加得值不值」的依据。没有基准的话,加完卡看到一个数字,你根本不知道它是好是坏。
怎么测:把配置和结果记成表
选型这件事,讲机制能帮你缩小候选范围,但最终拍板得靠自己环境里的实测数。别人的经验、包括这篇文章里的判断,只能帮你少走弯路,替代不了测量。
测的时候有三个前提必须固定住,否则数据没有可比性:
- 固定模型:同一个模型、同一份权重、同一种量化方案。中途换量化会让所有数字失去可比性(量化怎么选是另一个话题,见量化方案怎么选)。
- 固定请求分布:用你真实业务的输入输出长度分布,不要用清一色的短请求。长短请求混合会显著改变 KV cache 的占用形态,用均匀短请求测出来的结论,上线后往往对不上。
- 固定压测方式:同样的并发爬升方式、同样的持续时长、同样的预热流程。
每个配置至少记这几列:
| 切分配置 | 卡数/拓扑 | 吞吐 | p95 延迟 | 首字延迟 | 显存峰值 | 备注 |
|---|---|---|---|---|---|---|
| 单卡基线 | 1,同机 | 拐点在哪 | ||||
| TP=2 | 2,同机 | |||||
| TP=4 | 4,同机 | |||||
| PP=2 | 2,同机 | 注意低并发下的表现 | ||||
| TP=2 × PP=2 | 4,跨机 | 组合,调试成本高 |
几个填表要点:
必须有单卡基线那一行。 没有基线,后面所有数字都是孤立的。
p95 而不是平均值。 平均延迟会把长尾抹平,而用户感知到的恰恰是长尾。流水并行在低并发下的问题,平均值里可能看不太出来,p95 会诚实地告诉你。
首字延迟单独记一列。 对话式场景里,首字延迟和整体完成时间是两回事,用户对前者更敏感。
显存峰值要记,不是记「起来了没有」。 刚好卡在边缘起得来的配置,上线遇到长请求就会 OOM。峰值离上限还有多少余量,是判断这个配置能不能扛住真实流量的关键。
备注列别省。 记下你在那次测试里观察到的异常——某张卡利用率明显偏低、日志里的告警、压到某个并发就不稳。这些定性观察在后面复盘时的价值经常超过数字本身。
表格填完,选型的判断就从「听说 TP 更好」变成了「在我这套硬件、我这个模型、我这个请求分布下,这个配置的 p95 是多少」。这才是能拿去跟人争论的依据。
至于把服务真正跑起来的命令与参数细节,参数名和取值以官方文档当次版本为准,可以配合vLLM 起服务一起看;选卡阶段的考量则见GPU 怎么选。
动手前的自查清单
- 确认前两步旋钮已经用尽——
gpu_memory_utilization提过了吗?max_num_seqs/max_num_batched_tokens试着降过吗?没做完这两步就加卡,很可能白花钱。 - 写下这个服务的首要目标——是低延迟还是高吞吐?只能选一个作为首要。选不出来,说明需求没聊清楚,先别配。
- 测出单卡拐点并记录下来——作为所有多卡配置的对照基准,没有它后面无法判断收益。
- 确认卡间拓扑——这几张卡是同机还是跨机?跨机的话,张量并行不要横跨网络。
- 按目标选切法——延迟敏感走
tensor_parallel_size且尽量同机;吞吐优先、延迟可放宽再考虑pipeline_parallel_size。 - 组合方案预留调试预算——TP × PP 组合能解决大模型跨机的问题,但排查成本会上升,别在上线前一天才开始配。
- 用真实请求分布压测,把表填完——吞吐、p95 延迟、首字延迟、显存峰值四列都要有,别只看「起来了没有」。
- 显存峰值留足余量——刚好起得来的配置扛不住真实流量里的长请求,上线必炸。
- 参数名与可用组合以官方文档当次版本为准——版本更新可能改变支持范围,别照搬网上的旧配置。