1. 从“decode 阶段”说起:为什么它成了硬件部署的分水岭
如果你最近在折腾大模型推理部署,大概率会遇到一个很割裂的现象:prefill 阶段跑得好好的,一到 decode 阶段显存就爆了,或者吞吐量断崖式下跌。这不是你的错觉,而是由两个阶段的计算本质决定的。prefill 阶段处理的是完整 prompt,所有 token 并行计算,矩阵乘法的维度大、并行度高,GPU 的算力能被充分压榨;而 decode 阶段是自回归逐 token 生成,每次只处理一个 token,计算量小但访存密集,GPU 的算力利用率往往连 10% 都不到,瓶颈从“算力”转移到了“显存带宽”。
这个差异直接决定了硬件部署策略的分野。很多团队在选型时只看峰值算力(TFLOPS),结果上线后发现 decode 阶段根本跑不满,钱花在了用不上的算力上。真正影响 decode 性能的核心指标是显存带宽(GB/s)和显存容量,其次是卡间互联带宽。举个例子,一张标称 1000 TFLOPS 但显存带宽只有 1.5 TB/s 的卡,在 decode 阶段的实际表现可能还不如一张 500 TFLOPS 但带宽 3 TB/s 的卡。这就是为什么 decode 阶段的硬件部署不能简单套用训练或 prefill 的经验。
这篇文章面向的是正在做端侧或服务端大模型推理部署的工程师,无论你是刚接触推理优化,还是已经在调优 decode 吞吐,我都会从硬件选型逻辑、显存规划、并行策略、量化方案到实际踩坑,把 decode 阶段硬件部署这件事拆开讲透。核心关键词 decode 和硬件部署会贯穿全文,但我不打算堆术语,而是尽量用实际场景和可复现的参数来说话。
2. decode 阶段的硬件特性与选型逻辑
2.1 为什么 decode 是“访存瓶颈”而非“算力瓶颈”
要理解 decode 阶段的硬件需求,得先搞清楚它到底在干什么。自回归生成时,模型每次只输入一个新 token,但需要和之前所有 token 的 KV Cache 做注意力计算。这意味着每一步的计算量是 O(d_model × seq_len) 级别的矩阵向量乘,而访存量却是 O(d_model × seq_len × num_layers) 级别的 KV Cache 读取。计算访存比极低,GPU 的 SM 大部分时间在等数据从显存搬过来,而不是在算。
我用一个具体数字来说明。假设一个 7B 模型,FP16 精度,hidden size 4096,32 层,序列长度 2048。每生成一个 token,需要读取的 KV Cache 大约是 2 × 32 × 2048 × 4096 × 2 bytes ≈ 2.1 GB。而对应的计算量大约是 2 × 7B × 2 ≈ 28 GFLOP。如果一张卡的显存带宽是 2 TB/s,读取 2.1 GB 需要约 1.05 ms;如果算力是 300 TFLOPS,算 28 GFLOP 只需要 0.09 ms。访存时间是计算时间的 11 倍以上。这就是 decode 阶段的真实写照:算力大量闲置,显存带宽才是硬约束。
所以选型时的第一原则是:优先看显存带宽,其次看显存容量,最后才看算力。很多国产卡或消费级卡在纸面算力上很好看,但带宽只有 1 TB/s 出头,跑 decode 就会非常吃力。
2.2 显存容量怎么算才不翻车
显存容量决定了你能部署多大的模型、支持多长的上下文、跑多大的 batch。decode 阶段的显存占用主要由四部分组成:模型权重、KV Cache、激活值、框架开销。模型权重是固定的,7B FP16 约 14 GB,INT8 约 7 GB,INT4 约 3.5 GB。KV Cache 是动态的,公式是:
KV Cache 大小 = 2 × num_layers × num_kv_heads × head_dim × seq_len × batch_size × dtype_bytes
以 7B 模型(32 层,32 个 KV head,head_dim 128)为例,FP16 精度下,单条 2048 序列的 KV Cache 是 2 × 32 × 32 × 128 × 2048 × 2 ≈ 1.07 GB。如果 batch_size 是 16,就是 17 GB。加上模型权重 14 GB,光这两项就 31 GB,还没算激活值和框架开销。所以一张 24 GB 的卡跑 7B FP16 加 16 batch 是绝对不够的,必须做量化或者限制 batch。
这里有个经验公式:可用显存 = 总显存 × 0.85(留 15% 给框架和碎片),最大 batch = (可用显存 - 模型权重) / 单条 KV Cache。按这个算,24 GB 卡跑 7B INT4(3.5 GB 权重),可用约 20.4 GB,减去权重剩 16.9 GB,单条 KV Cache 1.07 GB,最大 batch 约 15。但实际部署时还要留余量,建议按 12 来配。
2.3 卡间互联:被低估的 decode 性能变量
如果你用多卡部署,卡间互联带宽对 decode 的影响比 prefill 更大。因为 decode 阶段每生成一个 token 都要做一次 all-reduce 或 all-gather 来同步各卡的输出,通信频率极高。NVLink 的 600 GB/s 和 PCIe 4.0 的 32 GB/s 差距,在 decode 阶段会被放大到几乎不可用。我实测过同样的模型,NVLink 双卡比 PCIe 双卡的 decode 吞吐高 2.5 倍以上,而 prefill 阶段只差 30%。
所以多卡 decode 部署的铁律是:能用 NVLink 就别用 PCIe,能用单卡就别用多卡。如果非要走 PCIe,尽量用 tensor parallelism 而不是 pipeline parallelism,因为 TP 的通信量虽然大但可以 overlap,PP 的 bubble 在 decode 阶段更致命。
3. 显存规划与量化策略的实操细节
3.1 量化精度怎么选:INT8 还是 INT4
量化是 decode 阶段硬件部署的救命稻草,但选 INT8 还是 INT4 不能拍脑袋。INT8 的精度损失通常很小(perplexity 增加 0.1 以内),显存减半,带宽需求也减半,是性价比最高的选择。INT4 显存再减半,但精度损失明显,尤其是小模型(7B 以下)可能出现输出重复、逻辑断裂的问题。
我的建议是:13B 以上模型优先 INT4,7B 以下优先 INT8,13B 左右看场景。如果是对话类应用,INT4 的精度损失用户基本感知不到;如果是代码生成或数学推理,INT4 可能让准确率掉 5-10 个百分点,这时候 INT8 更稳。另外要注意,不是所有层都适合 INT4,embedding 层和 lm_head 层通常保持 FP16 或 INT8,中间 FFN 层可以 INT4,这样混合量化能在显存和精度之间取得更好的平衡。
3.2 KV Cache 量化:省显存的大杀器
很多人只量化模型权重,忽略了 KV Cache 才是 decode 阶段显存的大头。KV Cache 量化到 INT8 可以直接省一半显存,而且对精度的影响比权重量化还小,因为 KV Cache 的数值分布相对集中。现在主流推理框架(如 vLLM、TensorRT-LLM)都支持 KV Cache INT8,开启后 batch 能翻倍。
但 KV Cache 量化有个坑:per-token 量化的 scale 计算会引入额外开销,如果实现不好反而拖慢 decode。建议用 per-channel 或 per-head 的静态 scale,虽然精度略差但速度快。另外,KV Cache 量化后 attention 的数值稳定性会变差,长序列下可能出现 NaN,需要加 clamp 保护。
3.3 显存碎片与预分配策略
decode 阶段是持续生成的过程,显存分配和释放非常频繁,很容易产生碎片。我遇到过好几次“显存明明够但就是分配失败”的情况,最后发现是碎片问题。解决办法是预分配 KV Cache 池,启动时就把最大 batch 和最大序列长度所需的 KV Cache 一次性分配好,运行时不动态申请。vLLM 的 PagedAttention 就是这个思路,把 KV Cache 切成固定大小的 block,按需映射,既避免碎片又支持动态 batch。
如果你自己写推理服务,至少要做到两点:一是用内存池管理 KV Cache,二是设置合理的 max_seq_len 上限,不要让它无限增长。我一般会把 max_seq_len 设成业务实际需要的 1.2 倍,留一点余量但不浪费。
4. 并行策略与部署架构的落地选择
4.1 单卡、TP、PP 到底怎么选
decode 阶段的并行策略和训练阶段完全不同。训练时 PP 很常见,因为可以流水线并行;但 decode 阶段每步只算一个 token,PP 的 bubble 占比极高,基本不可用。所以 decode 阶段只有两个选择:单卡,或者 TP。
单卡最简单,没有通信开销,但受限于单卡显存和带宽。TP 能把模型切到多卡,突破单卡显存限制,但每步都要 all-reduce,通信开销随卡数增加。我的经验是:TP 不超过 4 卡,超过 4 卡后通信开销会吃掉并行带来的收益。如果 4 卡还放不下,优先考虑量化而不是继续加卡。
具体选型可以看这个表:
| 模型规模 | 精度 | 推荐方案 | 显存需求 | 备注 |
|---|---|---|---|---|
| 7B | INT4 | 单卡 24GB | ~8GB | 最稳,无通信开销 |
| 7B | FP16 | 单卡 48GB 或 TP2 | ~20GB | TP2 需 NVLink |
| 13B | INT4 | 单卡 24GB | ~12GB | batch 受限 |
| 13B | INT8 | TP2 或单卡 48GB | ~20GB | TP2 性价比高 |
| 70B | INT4 | TP4 | ~40GB | 必须 NVLink |
4.2 Continuous Batching:吞吐翻倍的关键
decode 阶段如果按请求逐个处理,GPU 利用率极低。Continuous Batching(也叫 iteration-level batching)让不同请求在同一批次里共享计算,新请求可以随时加入,完成的请求随时退出。这个技术能把 decode 吞吐提升 5-10 倍,是现在推理框架的标配。
但 Continuous Batching 对显存管理要求很高,因为 batch 里的序列长度不一,KV Cache 大小不同。PagedAttention 解决了这个问题,把 KV Cache 分页管理,不同序列共享物理页。部署时要注意:block size 不要设太小(建议 16 或 32),太小会导致页表开销大;max_num_seqs 要根据显存和延迟要求调,设太大延迟会飙升,设太小吞吐上不去。
4.3 端侧部署的特殊考量
端侧 decode 部署和服务端完全是两个世界。端侧没有 NVLink,没有大显存,甚至没有独立 GPU。常见方案是手机 NPU、边缘盒子或消费级显卡。端侧的核心约束是功耗和散热,decode 阶段持续运行会让芯片温度快速上升,触发降频后性能断崖下跌。
端侧部署我建议:优先用 INT4 量化,限制 max_seq_len(一般 512-1024 够用),用动态 batch=1(端侧并发低),开启功耗管理策略(比如限制最大频率来换稳定)。另外端侧的内存带宽通常只有几十 GB/s,decode 速度会明显慢于服务端,要做好用户体验预期管理,比如用流式输出让用户感知不到延迟。
5. 常见问题与排查技巧实录
5.1 decode 速度突然变慢的排查思路
decode 速度变慢是最常见的问题,原因可能有很多。我整理了一个排查顺序:
- 先看显存占用:如果显存接近打满,可能是 KV Cache 频繁换入换出,或者触发了 swap。用 nvidia-smi 或框架自带的监控看。
- 再看 GPU 利用率:如果利用率很低(<30%),说明是访存瓶颈,检查 batch 是否太小、KV Cache 是否量化。
- 然后看温度:如果温度超过 80 度,大概率在降频,检查散热。
- 最后看通信:多卡部署时用 NCCL 的 profiling 看 all-reduce 耗时,如果占比超过 30%,说明通信是瓶颈。
我遇到过一次 decode 速度从 50 tokens/s 掉到 5 tokens/s,最后发现是某个请求的序列长度到了 max_seq_len,触发了 KV Cache 的重新分配,导致整个 batch 卡住。后来把 max_seq_len 调大并加了预警就好了。
5.2 显存够但报 OOM 的几种情况
“显存明明够但报 OOM”是另一个高频问题。常见原因有:
- 碎片问题:前面提过,用内存池或 PagedAttention 解决。
- 峰值显存被低估:decode 阶段虽然平均显存低,但某些操作(如 attention 的 softmax)会有峰值,要留余量。
- 框架开销:PyTorch 的 CUDA context 本身占几百 MB,TensorRT 的 engine 也有开销,别忽略。
- 多进程共享:如果多个进程共享一张卡,显存是叠加的,要算总账。
排查时可以用torch.cuda.memory_summary()看详细分配,或者用nvidia-smi --query-gpu=memory.used持续监控。
5.3 量化后精度下降的补救措施
量化后精度下降是必然的,但可以通过一些手段补救:
- 混合精度:关键层保持高精度,非关键层量化。
- 校准集优化:量化校准用的数据集要贴近实际业务分布,不要随便拿个通用数据集。
- KV Cache 不量化:如果精度要求高,KV Cache 保持 FP16,只量化权重。
- 后处理:对输出做重复检测和截断,减少重复生成。
我实测下来,INT8 权重量化 + FP16 KV Cache 的组合,精度损失基本在 1% 以内,显存省了 40%,是性价比最高的方案。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| decode 速度慢 | batch 太小 | 看 GPU 利用率 | 增大 batch 或开 Continuous Batching |
| 显存 OOM | KV Cache 太大 | 算 KV Cache 公式 | 量化 KV Cache 或限制 seq_len |
| 多卡加速比低 | 通信瓶颈 | NCCL profiling | 换 NVLink 或减少 TP 卡数 |
| 温度高降频 | 散热不足 | 看温度和频率 | 改善散热或限制功耗 |
| 输出重复 | 量化精度损失 | 对比 FP16 输出 | 混合精度或换 INT8 |
| 长序列 NaN | KV Cache 量化 | 检查 attention 数值 | 加 clamp 或 KV Cache 不量化 |
6. 一套可复现的 decode 部署配置参考
6.1 服务端 7B 模型部署实例
假设你有一张 24 GB 显存的卡(比如 4090),要部署 7B 模型做对话服务。我的配置是:
- 量化:INT4 权重 + INT8 KV Cache
- 推理框架:vLLM 或 TensorRT-LLM
- max_seq_len:4096
- max_num_seqs:16
- block_size:16
- gpu_memory_utilization:0.9
这套配置下,7B 模型显存占用约 3.5 GB(权重)+ 8 GB(KV Cache,16 条 4096 序列)+ 2 GB(框架开销)≈ 13.5 GB,留了 10 GB 余量。decode 速度实测约 40-60 tokens/s(batch 16 时总吞吐),单请求延迟约 30ms/token,对话场景完全够用。
启动命令(以 vLLM 为例):
python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --quantization awq \ --kv-cache-dtype int8 \ --max-model-len 4096 \ --max-num-seqs 16 \ --block-size 16 \ --gpu-memory-utilization 0.9 \ --port 80006.2 端侧部署实例
端侧以手机 NPU 为例,部署 3B 模型:
- 量化:INT4
- max_seq_len:1024
- batch:1
- 功耗策略:限制最大频率到 80%
端侧 decode 速度约 10-20 tokens/s,虽然慢但流式输出体验尚可。关键是控制温度,我实测不加功耗限制时,连续生成 2 分钟后温度到 75 度,速度掉一半;加了限制后温度稳定在 60 度,速度稳定。
6.3 参数调优的经验法则
最后分享几个调参经验:
- gpu_memory_utilization 不要设 1.0,留 10% 给框架和碎片,设 0.9 最稳。
- block_size 16 是甜点,8 太小页表开销大,32 太大浪费显存。
- max_num_seqs 先设小再调大,从 8 开始,观察延迟和吞吐,找到平衡点。
- KV Cache 量化优先于权重量化,因为 KV Cache 是 decode 阶段的大头。
- TP 卡数不超过 4,超过后通信开销吃掉收益。
这套配置我在多个项目里复现过,7B 模型单卡 24 GB 跑对话服务,13B 模型双卡 NVLink 跑代码生成,70B 模型四卡 NVLink 跑通用推理,都稳定运行。decode 阶段的硬件部署没有银弹,核心就是抓住访存瓶颈这个本质,在显存、带宽、通信之间找平衡。踩过的坑多了,自然就有手感了。