一、因: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 不是万能药。
尊重硬件的物理规律,在约束中寻找最优解,这是推理优化的全部秘密。