事情是这样的:我以前一直以为 一张卡放得下的话,tp1 肯定比 tp>1 快,因为卡间通讯需要时间

可是 A100, sglang 部署 1.7B 和 4B 的模型,QPS=1 的情况下都是 tp2 比较快

问了 gpt 发现通讯没打满的情况下,tp 还可能增大 tensor core 的利用率

这个现象本身是正常的,但前提是:你现在的瓶颈不在通信,而在单卡算力/带宽利用率。否则 tp=2 理论上只会更慢。

我直接把机制拆开讲清楚。


一、先把问题抽象成计算模型

4B 模型,单 query,并发=1,本质是:

单条自回归 decode:逐 token 推理

每一步是:

[
h_{t+1} = \text{Transformer}(h_t)
]

关键在于:

每一步都是小 batch、低并行度计算


二、tp=1 时发生了什么

单卡跑 4B:

  • 参数全在一张卡
  • 每层 GEMM 规模是固定的(比如 hidden 4096 × 4096)

问题在于:

👉 GPU 并没有被吃满

原因是:

1. GEMM 尺寸不够“理想”

对于 GPU(尤其是 Tensor Core)来说:

  • 最优是大矩阵(比如 batch 大)
  • 但你现在是:
    • batch = 1
    • seq 很短(decode阶段)

👉 导致:

算子 launch overhead + memory latency 占比很高


2. memory-bound 明显

Transformer decode 很多操作是:

  • KV cache 读取
  • 小矩阵乘

这些是典型:

memory bandwidth bound,而不是 compute bound


三、tp=2 发生了什么(关键)

tp=2 本质是:

把权重按 hidden 维切成两半,分布到两张卡

每一步变成:

  • 每张卡算一半 GEMM
  • 然后 all-reduce / all-gather

表面看:多了通信,应该更慢

但你现在变快,说明一个事实:

通信开销 < 单卡未利用的算力浪费


真正的加速来源是这几个点

1. 提高了 SM 利用率

原来:

  • 一个大 kernel 没吃满 SM

现在:

  • 拆成两个更“适配硬件”的 kernel
  • 两张卡各自跑

👉 等价于:

把低利用率 → 两个中等利用率


2. L2 / HBM 带宽压力被分摊

原来:

  • KV cache / 权重 全压在一张卡
  • bandwidth saturation

现在:

  • 一半数据在卡1
  • 一半在卡2

👉 实际效果:

memory bandwidth ×2(近似)

这点在 decode 阶段非常关键。


3. kernel launch / pipeline overlap 更好

现代推理框架(比如 vLLM / TensorRT-LLM)会:

  • overlap compute 和通信
  • stream pipeline

所以实际不是:

compute → 通信 → compute

而是:

compute 和 communication overlap


4. hidden size 切分后更“Tensor Core friendly”

有些维度在 tp=1 时:

  • 不能很好对齐 Tensor Core tile(比如 128/256)

tp=2 后:

  • 子矩阵尺寸更规整

👉 提高了:

FLOPs utilization


四、什么时候 tp=2 会变慢?

这个现象不是普适的,它成立有前提。

如果出现下面情况,tp=2 会输:


1. batch size 提高

当:

  • batch > 8 或 16

👉 单卡已经吃满:

compute-bound

这时:

通信 = 纯负担


2. 模型更大(比如 13B+)

这时:

  • 单卡已经 compute heavy

tp=2:

  • 通信比例变大

如果是:

  • PCIe
  • 或者跨节点

👉 带宽骤降

tp=2 会直接掉速


五、你这个 case 的本质一句话

你现在在“单卡低利用率区间”,tp=2 帮你把计算从 memory-bound 拉向更均衡状态。


六、一个更底层的理解(很关键)

把 latency 拆开:

[
T = T_{compute} + T_{memory} + T_{launch}
]

tp=1:

  • T_memory 很大
  • T_compute 没吃满

tp=2:

  • T_memory ↓(分摊)
  • T_compute ↑(利用率提升)
  • T_comm ↑(但被 overlap)

最终:

[
T_{total} ↓
]


七、工程上你可以验证的点

如果你想确认我说的是对的,可以直接看 profile:

  • SM occupancy
  • dram utilization
  • nvlink utilization
  • kernel time vs memcpy time

你会看到:

👉 tp=1:

  • dram 很高
  • sm 不满

👉 tp=2:

  • dram 降
  • sm 升
  • nvlink 有占用但没成为瓶颈

收束

这个现象不是“tp=2更强”,而是:

你的当前负载让单卡处在低效区,而 tp=2 恰好把它推回高效区。

如果把 batch 拉起来,你会看到结论反转。


如果继续往下挖,这里其实有个更有意思的问题:

为什么 decode 阶段几乎永远是 memory-bound,而 prefill 更偏 compute-bound?