8 张 2080Ti 上的两种切法:张量并行生成快,预填充却慢
同一堆卡、同一个模型,把 llama.cpp 的
--split-mode从layer换成tensor,token 生成反而变快了,预填充却慢了好几倍。盯着这张 6 卡对照表看了半天,我一时没想通:卡没变、模型没变,凭什么换个切法,性能曲线就整个反过来了?
这篇文章想讲清楚一件事:layer split(按层切,也就是流水线并行)和 tensor split(张量并行)到底切的是什么、通信差在哪;以及为什么在凑不出 GPU 全互联的消费级多卡上,这个差异会在"生成"和"预填充"两个方向上被放大到肉眼可见。下面的数据都是这套 8× 2080 Ti 机器上自己打的,不是网上抄的。
先看现象
先交代环境,不然数字没处落:
- 硬件:8 张 GeForce RTX 2080 Ti(GPU 0–7 全上),没有 NVLink 桥,卡间走 PCIe(
topo里是 PIX/PXB,没有 NV#);节点对外是 ether 网,不是 InfiniBand。 - 软件:自己编的
llama-server(build b1-192067b,Turing sm_75),不是 vLLM。生产锁的是--split-mode layer。 - 模型:Qwen3.8-27B Q8_0,开 FlashAttn、MTP(
--spec-draft-n-max 4)、KV F16;测速直连10.26.6.3:31808,关 thinking。 - KV:
--kv-unified,总池 384K、3 个并发槽共享,单条请求 cap 在 256K。
现在生产跑的就是这套 8 卡 layer:4K 预填充约 1100–1144 t/s、解码(短代码 MTP)约 44.9 t/s(acc 0.939)、4K 首词约 4 s。
但要说清"为什么 tensor 生成快、预填充慢",需要一个同一套卡、只改变切法的对照。这个对照是在 6 卡上做的(当时 layer 和 tensor 都能装下 256K);换成 8 卡后我只重测了 layer,8 卡的 tensor 没测——切过去得停生产。6 卡 A/B 如下,变量只剩切法:
| 同一套 6 卡(都 256K) | 预填充 | 解码 | 4K 首词 |
|---|---|---|---|
layer(流水线) | ~1119 t/s | ~43 t/s | ~4 s |
tensor(张量并行) | ~200 t/s | ~54 t/s | ~21 s |
结论先摆出来,反直觉但清楚:
- 解码(生成):tensor 赢,54 t/s 对 43 t/s,快约 26%。
- 预填充:layer 赢,而且赢得多——4K 这一档 1119 t/s 对 200 t/s,layer 快约 5.6 倍;上下文越长,tensor 的差距还会继续拉大,我在长 prompt 上的体感是"慢十倍"。
- 4K 首词:layer ~4 s、tensor ~21 s,tensor 慢了约 5 倍——因为首词时间基本由预填充决定。
layer 和 tensor 到底是什么、为什么会有这种"一个快一个慢"的分裂,下面拆。
另外单独记一笔,免得后面混淆:8 卡 layer 的预填充还会随上下文长度衰减(cache_n=0,取 llama.cpp 的 prompt_per_second)——短 prompt / 空 KV 时贴着约 1100–1140 t/s 的平台,到 70K≈988、120K≈781、200K≈584、250K≈510(单条 cap 256K,没打满 261K)。这是 KV 和 attention 随序列变长而变贵的正交效应,跟切法无关,但测速时很容易被误当成"切法的问题"。

layer split 和 tensor split 到底切了什么
先补一块必要的背景:一个 transformer 层里,真正吃算力的是几个大矩阵乘(GEMM)——attention 的 QKV 投影、输出投影,以及 FFN 里那几个大权重矩阵。层间那些 LayerNorm、残差加都是零头。所以下面说"切层"还是"切权重",切的主要就是这几个 GEMM。
layer split(-sm layer,默认)= 按层切,也就是流水线并行。 把模型切成几段连续的层,每段落在一张卡上:GPU0 跑前几层,GPU1 跑中间几层,一路到 GPU7。一个 token 从头到尾像接力一样穿过所有卡。
tensor split(-sm tensor,实验性)= 张量并行。 不切层,而是把每一层里那几个大权重矩阵再横向切开,每张卡只持每层权重的一片。同一个 token、同一层,8 张卡同时算自己那片,算完要把各自的部分结果汇总(all-reduce),才能进入下一步。
两者数据流不一样,画出来最直观:
一句话:layer 是"接力",卡间只在建段的地方传一次激活;tensor 是"齐算",每一层都要做一次跨卡汇总。这个"每层都要汇总",是后面所有故事的根源。
关键差异在通信,不在算力
两种切法并行化算力的方式不同,但真正拉开差距的是通信模式:
| layer(流水线) | tensor(张量并行) | |
|---|---|---|
| 切什么 | 整层,卡间建段 | 层内权重矩阵 |
| 每卡持有 | 一段完整层的权重 | 每层权重的一片(1/8) |
| 卡间通信 | 点对点传激活,每个段边界一次 | 每层 all-reduce,一次 attention 一次 FFN,约 2 次/层 |
| 通信频率 | 低 | 高(每层都来) |
| 单次通信量 | 一段的 hidden 状态 | 一次 all-reduce 的激活张量 |
这里有个后面会反复用到的点:all-reduce 要搬的数据量,跟 batch × seq_len × hidden 成正比。 一个 token 来 all-reduce 是一小坨,一整段长 prompt 来 all-reduce 是几百 MB。记住"量随 token 数涨",是理解"为什么生成扛得住、预填充扛不住"的钥匙。
再补一句 all-reduce 本身:它比"把结果发给别人"复杂,目标是让每张卡都拿到求和后的完整结果。常见的环式(ring)/蝶式(butterfly)做法,为了让每卡凑齐全量,实际在链路上跑的数据不止一份。卡越多、量越大,它越吃带宽。
2080 Ti 的 P2P 陷阱
这一节是全文的关键,也是最有"坑"味的部分。
很多人的第一反应是:2080 Ti 卡上有 NVLink 桥接口,“那 tensor 并行的 all-reduce 应该挺快吧?” 这个直觉在 2080 Ti 上是错的。原因有两层:
- Turing 系 GeForce 的 GPU 间 P2P 只能走 NVLink,不能走 PCIe。 这是 NVIDIA 在官方论坛里明确过的:2080 / 2080 Ti / Titan RTX 想建 P2P,必须两张卡之间插 NVLink 桥;不插桥,
cudaDeviceCanAccessPeer直接返回 No,PCIe 这条路对 P2P 是关着的。 - NVLink 桥一次只能连 2 张卡。 就算你插了桥,也只能凑出"两两 P2P",最多把 8 卡分成 4 对,凑不出一张 8 卡全互联的网。而 tensor 并行的 all-reduce 需要的是"所有卡都能互相够到"。
把这两条叠到我们的机器上:8 张 2080 Ti、没桥、topo 里全是 PIX/PXB——根本不存在一个 P2P 全互联。于是 tensor 并行的 all-reduce 只能退化到"经 host 内存、走 PCIe"的路(NCCL 管这叫 SHM transport,llama.cpp 的 fallback 也是这个意思)。这条路又慢、又吃延迟。
所以关键不在"2080 Ti 支不支持多卡",而在"这套卡之间,all-reduce 到底走哪条路"。答案决定了 tensor 并行是飞快还是崩。
怎么自查(我这次在容器里没能跑成 topo,权限和会话都不允许重启,但下面这几条平时是能用的):
# 卡间拓扑:看卡与卡、卡与 CPU 之间走什么
nvidia-smi topo -m
# P2P 矩阵:p = PCIe,n = NVLink,CNS = 不支持
nvidia-smi topo -p2p
在没全互联的机器上,topo -p2p 基本会是一片 CNS。看到这一片就该有数了:tensor 并行的 all-reduce 走不了快的路。
为什么 tensor 生成更快
先说清楚生成(decode)这个阶段的性质。生成是一个 token 一个 token 往外蹦的:每一步只处理 1 个新 token,要算的量很小,但每一步都得把整个模型的权重从显存里搬一遍进计算单元。所以生成是访存受限——瓶颈在显存带宽,不在算力。一个 token 的时间,粗略就是:
每 token ≈ 全部权重字节数 ÷ 显存带宽
拿这个公式去看两种切法:
- layer(流水线)对单个 token 几乎不加速。 token 得顺序穿过 GPU0 → GPU1 → … → GPU7,每段用的是单卡的显存带宽,后一段还得等前一段。八段加起来 ≈ 单卡读全部权重的时间,甚至更慢(还多了段间延迟)。流水线并行的好处是"多条 token 同时在管里流",可生成时你手上常常就一条序列、一个 token 在飞,流水线是空的,没东西可重叠。
- tensor(张量并行)把"读权重"这件事并行了。 8 张卡同时各读自己那 1/8 的权重,各用各的显存带宽,所以"读全部权重"的时间被摊到 8 张卡上,理论能到约 8×。而生成时每次 all-reduce 的激活只有几十 KB(batch×hidden,很小),就算走 PCIe/host,那点数据也不耽误事。
所以生成的账是:tensor 把主要开销(读权重)并行掉了,通信又便宜 → 快;layer 的主要开销没被并行、只是接力 → 几乎不提速。这跟实测对得上:6 卡上 tensor 解码 54 t/s,layer 43 t/s。
这里还能再补一刀,从矩阵乘的角度看为什么"偏偏是 tensor":生成那一步的 GEMM,行数 M 就是 batch(≈1),非常小,整个乘法是"读权重"主导。把权重维度切小(tensor 干的事)才真正减负;把 M 再切小(layer / 微批干的事)没意义,因为 M 本来就已经小于 GEMM kernel 的 tile 尺寸了,切了也不快。一句话:生成的小 M,只有"切 K(权重)“这条 tensor 的路能救。
为什么 tensor 预填充反而崩
预填充(prefill)是另一个性格。它把整段 prompt 一次吃进去,一个大矩阵乘算出所有位置的表示,再产出第一个 token。这时行数 M = batch × 序列长度,非常大,是算力受限——瓶颈变成 FLOPs,而不是读权重。
关键变化在 all-reduce 的量。生成时每次 all-reduce 是几十 KB;预填充时,每次 all-reduce 搬的是整段 prompt 的激活(batch × seq_len × hidden),是几百 MB。而 tensor 并行是每层都要做这个 all-reduce(约 2 次/层 × 几十层)。几百 MB × 每层 2 次 × 几十层,再叠在"只能走 PCIe/host"这条慢路上——通信直接把计算盖过去了,GPU 大量时间在等 all-reduce。
而 layer 在预填充上是顺的:
- 每卡只对整段序列算自己那一段完整层,算完只传一次 hidden 状态(
batch × hidden,不是每 token 一份)给下一段; - 长 prompt 时,流水线里塞满了 token,前一段在算第 N 块、后一段在算第 N-1 块,流水线的空泡占比很小;
- 通信既少又便宜。
所以预填充的账反过来:layer 通信少、流水线填得满 → 扩展得好;tensor 每层都 all-reduce 大张量、又走慢路 → 崩。 这跟实测对得上:6 卡 4K 预填充 layer 1119 t/s,tensor 只剩 200 t/s。
这一层其实有人专门测过。有篇论文在走 PCIe 的 A100 上用 SGLang 跑 Qwen2.5-32B/14B,结论是同一件事:流水线并行的 TTFT(预填充)更低、张量并行的 TPOT(逐 token 生成)更低——跟我们这套 2080 Ti 的现象方向完全一致。
落到 llama.cpp 的实现层
你如果也在用 llama.cpp,这里有个更具体的点,直接解释"为什么卡数一超过 2、tensor 就比 2 卡惨”。
llama.cpp 在 tensor 模式下,为 2 张卡专门写了一个不走 NCCL 的内部 all-reduce 内核(ggml-cuda/allreduce.cu):小张量(生成那种几十 KB)走"单 kernel + 忙等"的低延迟路径,张大张量(预填充那种)走"copy-engine 分块"路径,都是经 pinned host 内存过 PCIe。但这个内核 dispatch 时明确写着:GPU 数 > 2 就落到通用 fallback。
所以 8 卡——以及任何超过 2 卡的——tensor 并行都用不上那个为 2 卡调好的内核,会退到:
- 如果这个 build 编了 NCCL(
-DGGML_CUDA_NCCL=ON,默认)→ 走 NCCL;在没 P2P 全互联的机器上,NCCL 也是 host-staged 过 PCIe; - 没编 NCCL → 走 meta-backend 的 butterfly fallback,同样 host-staged 过 PCIe。
两条路在 2080 Ti 上殊途同归:all-reduce 都经 host 内存过 PCIe。想确认自己这条 build 到底走了哪条,看启动日志有没有 NCCL not compiled in; falling back to internal AllReduce,或者用 GGML_CUDA_ALLREDUCE=internal|nccl|none 强制、看 first-AR 那行诊断输出。
那生产到底怎么选
回到最初的问题:这套机器上,到底用哪个?我们的答案是锁回 --split-mode layer + 256K,而且这个决定现在看是对的。判断逻辑其实是通用的:
- 卡间没有高速全互联(PCIe、没 NVLink 桥)+ 长上下文 / prefill 重:用 layer。它的通信少、能扛慢路,预填充和首词延迟都占优。这是我们这种场景的最优解。
- 有 NVLink 全互联 + 在意逐 token 延迟 + 短 prompt:tensor 才划算。它的低延迟优势,要在"all-reduce 很便宜"的前提下才兑现。
- 生产大系统通常是混合的:节点内走 NVLink 的地方上 tensor,跨节点走慢网的地方上流水线。vLLM 的默认建议就是
tensor_parallel_size = 每节点卡数、pipeline_parallel_size = 节点数。llama.cpp 里-sm layer就是那条"能扛慢互联"的默认路,-sm tensor官方一直标着 EXPERIMENTAL,不是没有道理。
能缓解但不能根治的手段也有:chunked prefill(把长 prompt 切块,降低单次 all-reduce 的量)、通信和计算重叠。但只要卡间还是 PCIe、没有全互联,tensor 预填充那笔账的根本代价还在。
顺带一句:换成 8 卡后,之前"7 不是 2 的幂、tensor 切不整"的顾虑也没了——8 是 2 的幂,head 数 / hidden 维一般都能整除。现在生产是 8 卡 layer + --kv-unified(384K 总池、3 槽共享、单条 cap 256K),这套组合既扛得住 PCIe 慢互联,又能把 KV 池堆到 384K。说到底,layer 占优是因为它既扛得住慢互联,又装得下大上下文,它并非天生比 tensor 快。
结语
回到开头那张表:同一堆卡,--split-mode 换个值,生成快了、预填充崩了。原因不在卡,也不全在模型,而在三件事叠在一起——两种切法把通信放在了不同的地方、生成和预填充的瓶颈根本不同、而这堆 2080 Ti 之间恰好没有一条快的路。
折腾这套东西最大的体会是:多卡并行从来不是"卡越多越快"这么简单。瓶颈卡在哪、卡与卡之间那根线多贵,往往比卡数本身更决定性能。消费级多卡没有 NVLink 那种快线,就得顺着 PCIe 的脾气来——该用流水线的时候别硬上张量并行,不是它不行,是那根线喂不饱它。
参考资料
- llama.cpp 多卡文档:docs/multi-gpu.md
- llama.cpp 张量并行实现(meta backend,PR #19378):ggml-org/llama.cpp#19378
- llama.cpp 内部 AllReduce 内核(2 卡、host-staged,PR #22299):ggml-org/llama.cpp#22299
- NVIDIA 官方论坛:Turing 系 GeForce 的 P2P 只能走 NVLink、不能走 PCIe:2080 Tis cudaDeviceCanAccessPeer failure without NVLink bridge
- YALIS:多节点 LLM 推理的通信瓶颈(prefill 大消息 / decode 小消息、TP vs PP):arXiv:2511.09557
- 重新审视 LLM 服务中的流水线并行(PCIe A100 上 Qwen 的 TTFT/TPOT 对照):Revisiting Pipeline Parallelism for LLM Serving(USENIX)
版权声明: 本文首发于 指尖魔法屋-8 张 2080Ti 上的两种切法:张量并行生成快,预填充却慢(https://blog.thinkmoon.cn/post/1036-llama-cpp-layer-vs-tensor-split/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。