GPU 推理并发的本质:从第一性原理到工程实践

一、因:GPU 的物理约束(不可违背的客观规律)

一切优化的起点,是两个硬件事实:

事实 1:搬运比计算慢。

以 A100 为例:显存带宽 2TB/s,算力峰值 312 TFLOPS(FP16)。一个 7B 模型的权重约 14GB,单次加载耗时 7ms,但这 14GB 权重理论上可以完成的浮点运算远超 7ms 内 GPU 能"搬完"的量。结论:推理的瓶颈不是算不过来,而是搬不过来。

事实 2:神经网络是串行的。

同一个请求,Layer N 的输入 = Layer N-1 的输出。这是数学结构决定的,任何框架都无法绕过。

这两条构成了推理优化的因果起点

追问:权重不是已经在显存里了吗?为什么每次推理都要"搬运"?

这是一个非常关键的追问。模型加载后,权重确实常驻在显存(HBM/VRAM)里 ,不会每次都从硬盘读取。但问题在于------显存不是计算发生的地方。

GPU 的内部结构分为两层:

复制代码
┌─────────────────────────────────────────┐
│              GPU 芯片                    │
│                                         │
│   ┌───────────┐    ┌───────────┐        │
│   │  SM 0     │    │  SM 1     │  ...   │  ← 计算单元(CUDA Cores 在这里)
│   │ 寄存器     │    │ 寄存器     │        │  ← 每个 SM 有自己的寄存器 + 共享内存
│   │ 共享内存   │    │ 共享内存   │        │     (SRAM,极快,但极小:~256KB/SM)
│   └───────────┘    └───────────┘        │
│          ↑               ↑              │
│          │   数据搬运总线  │              │  ← 这条路就是"带宽瓶颈"
│          ↓               ↓              │
│   ┌─────────────────────────────────┐   │
│   │       显存 HBM(VRAM)          │   │  ← 权重 W 住在这里(大:80GB)
│   │       容量大,但距离远            │   │     但 SM 不能直接在这里算
│   └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

类比 :显存是仓库 ,SM 的寄存器和共享内存是工作台。工人(CUDA Core)只能在工作台上干活,而工作台非常小(每个 SM 只有约 256KB SRAM)。

但问题比"放不下整个模型"更严重------连一层都放不下

以 LLaMA-7B 为例,单层 Transformer 包含 Q/K/V/O 四个投影矩阵加上 FFN 的 up/down/gate 三个矩阵,总权重约 440MB(FP16) 。而单个 SM 的 SRAM 只有 256KB ,就算把 A100 全部 108 个 SM 的 SRAM 加起来也不到 28MB

复制代码
单层权重 ≈ 440MB  >>  全部 SM 的 SRAM ≈ 28MB  >>  单个 SM 的 SRAM ≈ 256KB

那 GPU 是怎么算的?答案是:切瓷砖(Tiling)。

GPU 不会试图把整层权重搬上来,而是把大矩阵切成很多小瓷砖(Tile),每块只有几十 KB,刚好放得进 SRAM。

复制代码
一层的权重矩阵 W(440MB)
┌────┬────┬────┬────┬───┐
│Tile│Tile│Tile│Tile│...│
│ 0  │ 1  │ 2  │ 3  │   │   ← 每个 Tile 几十 KB
├────┼────┼────┼────┼───┤
│Tile│Tile│Tile│Tile│...│
│ 4  │ 5  │ 6  │ 7  │   │
├────┼────┼────┼────┼───┤
│... │... │... │... │...│
└────┴────┴────┴────┴───┘

计算过程(每个 SM 的循环):
  ① 从 HBM 搬一小块 Tile 到 SRAM      ← 等待,带宽瓶颈在这里
  ② 在 SRAM 上完成这小块的乘加运算      ← 极快,纳秒级
  ③ 累加到部分结果
  ④ 丢掉这块 Tile,搬下一块            ← 回到 ①
  ...直到这一层所有 Tile 处理完毕

更直观的类比 :想象你在一张小书桌上做一幅 1000 块的拼图。桌子只能同时放 5 块,所以你必须:从盒子里拿 5 块 → 拼好 → 放回成品区 → 再拿 5 块。你的速度瓶颈不是"拼"的速度,而是"从盒子里翻找和拿取"的速度。GPU 的情况完全一样:算得飞快,但搬得跟不上。

这就解释了一个关键事实:同一层的权重,在每次前向传播时都要重新从 HBM 逐块搬运到 SM。因为 SRAM 太小,Tile 用完即丢,没有任何"缓存"能留住它们到下一次推理。

复制代码
完整因果链:
  SRAM 极小(256KB/SM)
    → 连一层权重都放不下,必须切成 Tile 逐块搬运
    → 每个 Tile 都要走一次 HBM → SM 的搬运路径
    → 搬运速度受限于显存带宽(这就是 Memory-bound 的根源)
    → 搬一次 Tile,只为 1 个输入算一次太浪费
    → 搬一次 Tile,同时为 N 个输入算 → Batching

注意 :这里说的 Tiling 是 GPU 执行矩阵乘法时自动发生的底层行为(由 cuBLAS / CUTLASS 等库实现),不需要框架开发者手动操作。但理解它,才能真正明白为什么"搬运"是逐块反复发生的,而不是"加载一次就够了"。

追问:并发数量取决于什么?SM 越多 = 并发越多?

理解了 Tiling 之后,很容易产生一个误判:既然计算受限于 SRAM 大小,那是不是 SM 大小决定了能支持多少并发?显卡越大(更多 SM)也没用?

恰恰相反。SRAM 不限制并发数量,HBM(显存容量)才是。

原因在于:Tiling 机制对 batch size 是透明的。不管你是 1 个输入还是 50 个输入,Tile 的切法会自动调整,每块 Tile 依然是几十 KB,依然放得进 SRAM。SM 不关心你的 batch 有多大,它只管算眼前这一小块 Tile。

那真正限制并发的瓶颈是什么?是 KV Cache。

每个并发请求在推理过程中都需要维护自己的 KV Cache(Key-Value 缓存),它存储了该请求所有已生成 token 的注意力状态,供后续 token 生成时使用。KV Cache 住在 HBM 里,而且随着序列变长会持续增长。

复制代码
HBM 显存的分配:
┌──────────────────────────────────────────────┐
│                  HBM(如 A100 = 80GB)         │
│                                              │
│  ┌──────────┐  ┌─────────────────────────┐   │
│  │ 模型权重  │  │       KV Cache          │   │
│  │  (固定)   │  │  请求 A: 2MB            │   │
│  │  ~14GB   │  │  请求 B: 5MB            │   │
│  │  (7B模型) │  │  请求 C: 1MB            │   │
│  │          │  │  请求 D: 8MB            │   │
│  │          │  │  ...                    │   │
│  │          │  │  每多一个并发请求,        │   │
│  │          │  │  就多占一份 KV Cache      │   │
│  └──────────┘  └─────────────────────────┘   │
│                                              │
│  剩余空间 = 80GB - 权重 - 所有请求的 KV Cache   │
│  剩余空间归零 → 无法接受新的并发请求             │
└──────────────────────────────────────────────┘

案例 :以 LLaMA-7B(FP16)为例,单个请求生成 2048 token 的 KV Cache 约占 1GB。模型权重约 14GB,那么在 A100-80GB 上:

复制代码
可用于 KV Cache 的空间 ≈ 80GB - 14GB = 66GB
最大并发数 ≈ 66GB / 1GB = ~66 个请求(2048 token 上下文)

如果换成 A100-40GB:

复制代码
可用空间 ≈ 40GB - 14GB = 26GB
最大并发数 ≈ 26GB / 1GB = ~26 个请求

所以显卡越大(HBM 容量越大),能支持的并发越多。 这也是为什么 vLLM 的 PagedAttention 如此重要------它通过类似操作系统虚拟内存的方式管理 KV Cache,减少显存碎片,让同样大小的 HBM 能塞下更多并发请求。

复制代码
因果链:
  每个并发请求需要独占一份 KV Cache
    → KV Cache 住在 HBM 里
    → HBM 总容量 - 模型权重 = KV Cache 可用空间
    → 可用空间 / 单请求 KV Cache = 最大并发数
    → 显存越大 → 并发越多
    → PagedAttention 减少碎片 → 同样显存塞下更多并发

把三个层次理清:

组件 决定了什么 大了有什么好处
SRAM(SM 内部) 单次 Tile 计算的大小 Tile 可以切得更大,减少搬运次数,略微提速
HBM 容量(显存) 最大并发请求数 能同时存放更多请求的 KV Cache
HBM 带宽(显存速度) 单请求延迟 权重 Tile 搬运更快,每个 token 生成更快

二、果:Batching 是唯一的"免费午餐"

从事实 1 出发,因果链条如下:

复制代码
搬运权重 W 是主要开销
  → 搬一次 W,只服务 1 个输入 X,利用率极低
  → 搬一次 W,同时服务 N 个输入 [X₁, X₂, ..., Xₙ],利用率提升 N 倍
  → 这就是 Batching

案例:假设你开了一家快递站,每次出车成本 100 元(= 加载权重的带宽成本)。只送 1 个包裹,每单成本 100 元;装满 50 个包裹,每单成本降到 2 元。油费没变,但吞吐量翻了 50 倍。

本质上,Batching 做的事情是:把矩阵乘法从 W·x(向量)变成 W·X(矩阵),用更大的输入维度"喂饱"带宽。


三、果的演进:从 Static Batching 到 Continuous Batching

3.1 Static Batching 的问题

传统方式:凑齐一批请求,一起跑完,再接下一批。

问题出在哪?请求长短不一。一个 10 token 的请求和一个 500 token 的请求被绑在一起,短请求早就生成完毕,却只能干等长请求跑完,GPU 在为已完成的请求做无用功。

复制代码
因果链:
  请求长度不同 → 短请求等长请求 → GPU 存在"计算气泡" → 吞吐量下降

3.2 Continuous Batching 的解法

vLLM 的核心洞察:把调度粒度从"整个请求"降到"单个 token 生成"。

每一轮前向传播(生成一个 token)结束后,框架都可以:

  • 把已完成的请求踢出 batch,释放显存

  • 把新到的请求加入 batch,填充空位

    因果链:
    调度粒度变细 → 每轮都能动态调整 batch 组成 → 消除等待气泡 → GPU 利用率最大化

关键细节:新请求加入时需要先做 Prefill(处理全部输入 token),这不是零成本的。框架需要决定何时、如何插入 Prefill,这引出了下一个问题。


四、核心澄清:GPU 上的"并行"到底是什么?

这是最容易被误解的地方。必须区分三个层次:

层次 1:Batch 内并行(这是主流做法)

vLLM 把同一 batch 内所有请求的当前 token 拼成一个大矩阵 ,发射一个 kernel,GPU 内部数千个 CUDA Core 同时处理这个大矩阵的不同部分。

复制代码
请求 A 的 token + 请求 B 的 token + 请求 C 的 token
  → 拼成矩阵 X = [xₐ, x_b, x_c]
  → 一次 kernel launch: W · X
  → 所有请求在同一个 kernel 内并行完成

这是 SIMT(单指令多线程)架构的天然并行,不需要多个 CUDA Stream。

层次 2:计算与通信重叠(CUDA Streams 的真正用途)

在多卡推理(Tensor Parallelism)中,Layer N 计算完成后需要跨卡通信(all-reduce)。CUDA Streams 允许:

  • Stream 1:执行 Layer N 的 all-reduce
  • Stream 2:同时开始 Layer N+1 中不依赖 all-reduce 结果的计算

这是同一请求内部的流水线重叠,不是"不同请求跑不同层"。

层次 3:Prefill 与 Decode 的混合调度

当 batch 中同时存在做 Prefill 的新请求和做 Decode 的老请求时,它们被融合在同一个 kernel 中处理,而非分配到不同的 SM 上独立运行。框架层面的区分在于 attention 计算方式不同(Prefill 做全量 attention,Decode 做增量 attention),但底层仍是一次统一的矩阵运算。


五、实战推导:安全研判场景的优化策略

假设你部署了一个安全研判模型,面对两类任务:

任务类型 特征 瓶颈
告警标题判别 高频、短文本(几十 token) 对延迟敏感
攻击溯源分析 低频、长日志(数千 token) Prefill 计算量大

问题的因果链

复制代码
长日志 Prefill 计算量巨大
  → 占据整轮前向传播的大量时间
  → 同 batch 内短文本的 Decode 被迫等待
  → 短文本首 token 延迟(TTFT)剧烈波动

解法:Chunked Prefill

将长日志的 Prefill 切成固定大小的块(如 512 token),每轮只处理一块:

复制代码
第 1 轮:长日志 Prefill [0:512] + 短文本 A Decode + 短文本 B Decode
第 2 轮:长日志 Prefill [512:1024] + 短文本 C Decode + 新请求 D Prefill
第 3 轮:长日志 Prefill [1024:1536] + 短文本 A Decode + ...

代价是长日志的总处理时间略有增加,但短文本的延迟稳定性大幅改善。这就是吞吐量与延迟之间的工程权衡。


六、实战算账:2×4090 部署 Qwen-32B-INT4,最多能扛多少并发?

这是一道可以用前面所有知识精确推算的题目。我们一步步来。

第一步:盘点显存总量

复制代码
RTX 4090 单卡显存 = 24GB
两张卡(Tensor Parallelism) = 24GB × 2 = 48GB 总显存

第二步:算模型权重占多少

Qwen2.5-32B 有约 325 亿参数,INT4 量化后:

复制代码
裸权重 = 32.5B × 4 bit ÷ 8 = ~16.2GB
量化元数据(scales、zeros,GPTQ/AWQ 约 10-15% 开销)≈ +1.8GB
模型权重合计 ≈ 18GB

第三步:扣掉框架开销

CUDA Context、内核代码、中间激活值(Activations)、vLLM 内部缓冲区等:

复制代码
框架开销 ≈ 2~3GB(保守取 3GB)

第四步:算 KV Cache 可用空间

复制代码
可用于 KV Cache 的显存 = 48GB - 18GB - 3GB = 27GB

第五步:算单个请求的 KV Cache 大小(核心)

Qwen2.5-32B 的架构参数:

复制代码
Transformer 层数 = 64
KV Head 数量 = 8(使用了 GQA,不是完整的 40 个头)
Head 维度 = 128
KV Cache 精度 = FP16(2 字节)

每生成 1 个 token,需要存储的 KV Cache:

复制代码
单 token 单层 = 2(K和V) × 8(KV头数) × 128(维度) × 2(字节)
             = 4,096 字节 = 4KB

单 token 全部 64 层 = 4KB × 64 = 256KB

那么不同上下文长度下,单个请求的 KV Cache 总量:

复制代码
 2K 上下文(2048 token):256KB × 2,048 =  512MB
 4K 上下文(4096 token):256KB × 4,096 = 1,024MB = 1GB
 8K 上下文(8192 token):256KB × 8,192 = 2,048MB = 2GB
32K 上下文(32768 token):256KB × 32,768 = 8,192MB = 8GB

第六步:得出最大并发数

复制代码
最大并发 = 可用显存 ÷ 单请求 KV Cache
上下文长度 单请求 KV Cache 最大并发数 适用场景
2K token 512MB ~54 短对话、分类、告警判别
4K token 1GB ~27 普通多轮对话
8K token 2GB ~13 长文档摘要
32K token 8GB ~3 超长上下文分析

第七步:现实还要再打折

以上是理论上限,实际部署还要考虑:

① PCIe 带宽瓶颈 :4090 没有 NVLink,两张卡之间走 PCIe 4.0(~32GB/s 双向)。Tensor Parallelism 每一层都需要跨卡通信(all-reduce),PCIe 的延迟会拖慢每个 token 的生成速度。并发数不受影响,但每个请求会变慢

② PagedAttention 的 Block 粒度:vLLM 以固定大小的 Block 为单位分配 KV Cache(默认 16 token/block),会有少量内部碎片。

③ 实际并发 ≠ 满上下文:不是每个请求都会用满上下文长度。如果平均实际长度是 1K token,那 KV Cache 平均只占 256MB,并发数可以翻倍。

保守估计 :部署 Qwen-32B-INT4 在 2×4090 上,4K 上下文场景下,稳定并发约 20~25 个请求

完整因果链

复制代码
2×4090 = 48GB 总显存
  → 减去模型权重 18GB、框架开销 3GB = 27GB 可用
  → 每个并发请求需要 KV Cache(大小 ∝ 上下文长度)
  → 27GB ÷ 单请求 KV Cache = 理论并发上限
  → 再受 PCIe 带宽、碎片、实际序列长度影响
  → 4K 上下文稳定并发 ≈ 20~25

结论 :想要更多并发,优化方向很明确------压缩 KV Cache (如 FP8 量化 KV Cache、GQA/MQA 架构)、增大显存 (换更大卡或加更多卡)、缩短上下文(业务层面裁剪输入长度)。


七、因果总结:五条推导链

链条 1:为什么要 Batch?

复制代码
SRAM 极小 → 权重必须切成 Tile 逐块从 HBM 搬到 SM → 搬运受限于显存带宽
→ 推理是 Memory-bound → 每搬一块 Tile 要尽量为更多输入服务 → Batching

链条 2:为什么要 Continuous Batching?

复制代码
请求长度不一 → Static Batch 有计算气泡 → 需要逐 token 调度 → Continuous Batching

链条 3:GPU 并行的真相是什么?

复制代码
多请求并行 ≠ 多 kernel 并行 → 而是拼成大矩阵、一个 kernel 内 SIMT 并行
CUDA Streams 的作用 = 计算与通信重叠,不是"不同请求跑不同层"

链条 4:并发上限由什么决定?

复制代码
每个请求独占 KV Cache → KV Cache 住在 HBM 中 → HBM 容量决定并发上限
PagedAttention 减少碎片 → 同样显存容纳更多并发

链条 5:延迟与吞吐的权衡怎么做?

复制代码
Batch 越大 → 吞吐越高,但单请求延迟越大
Chunked Prefill → 牺牲长请求速度,换短请求延迟稳定性
所有优化 = 在物理极限内寻找业务最优平衡点

结语

GPU 推理并发没有魔法。每一个优化策略都能追溯到两条物理约束:带宽慢于计算层间必须串行。理解了这两条,就能理解为什么要 Batch、为什么要 Continuous Batching、为什么 CUDA Streams 不是万能药。

尊重硬件的物理规律,在约束中寻找最优解,这是推理优化的全部秘密。


相关推荐
哎嗨人生公众号5 小时前
手写求导公式,让轨迹优化性能飞升,150ms变成9ms
开发语言·c++·算法·机器人·自动驾驶
foundbug9995 小时前
STM32 内部温度传感器测量程序(标准库函数版)
stm32·单片机·嵌入式硬件·算法
Hello.Reader5 小时前
为什么学线性代数(一)
线性代数·算法·机器学习
_深海凉_5 小时前
LeetCode热题100-找到字符串中所有字母异位词
算法·leetcode·职场和发展
木井巳5 小时前
【递归算法】目标和
java·算法·leetcode·决策树·深度优先
旖-旎5 小时前
哈希表(字母异位次分组)(5)
数据结构·c++·算法·leetcode·哈希算法·散列表
别或许5 小时前
4、高数----一元函数微分学的计算
人工智能·算法·机器学习
_深海凉_5 小时前
LeetCode热题100-最长连续序列
算法·leetcode·职场和发展
这里没有酒5 小时前
[信息安全] AES128 加密/解密 --> state 矩阵
算法