我先把标题拆开看。"AI 模型推理 GPU 利用率提升方案",关键词落在"推理"和"利用率"上,而不是训练。这是两个完全不同的优化逻辑:训练追求的是算力吃满、吞吐拉满;推理追求的是在满足延迟要求的前提下,把单位时间内能跑通的请求数量尽量堆上去。很多人盯着nvidia-smi里那个利用率百分比,觉得数字低就是卡不行、要加卡,实际上大多数情况是喂卡的姿势不对。
这篇文章我会从指标解读、模型侧优化、框架调度、部署侧性价比判断四个维度展开,把我在实际项目里踩过的坑和验证过的方法完整写出来。适合正在做模型服务部署、推理接口调优、GPU成本优化的工程师和算法同学参考。
1. 先搞清楚GPU利用率到底在算什么
1.1 三个最容易混淆的指标
任何一个做过推理优化的人,大概率都被nvidia-smi里那个百分之几十的数字误导过。我刚接触推理服务时也犯过这个错误:看 GPU-Util 只有 30%,第一反应就是"这卡没吃饱,得加并发",结果把并发调上去之后,显存爆了,延迟也飙了,利用率还是那个鬼样子。
后来我把指标拆开才想明白,nvidia-smi显示的利用率,本质上是"在某个采样周期内,GPU 上至少有内核在执行的百分比"。它是个"有没有活干"的粗略信号,不是"干了多少活"的精确度量。
真正需要分开看的指标有三个:
- SM 占用率(SM Occupancy):流处理器上活动线程块/warp 与理论容量的比值,反映的是硬件计算单元被填充的程度。
- 显存带宽利用率(Memory Bandwidth Utilization):实际读写吞吐与显存理论带宽的比值。这个指标在推理场景里往往比 SM 占用率更能说明问题。
- 端到端吞吐与延迟分布:每秒完成多少请求、首 token 延迟和多 token 延迟分别是多少。这是业务真正关心的指标。
为了直观,我做过一个对照实验。同一台机器、同一个模型、同一份压测流量,我记录了三种状态下的指标表现:
| 状态 | GPU-Util(nvidia-smi) | SM 占用率 | 显存带宽利用率 | 实际表现 |
|---|---|---|---|---|
| 空闲待机 | 0%-3% | 接近 0 | 接近 0 | 无请求 |
| decode 阶段占主导 | 85%-95% | 40%左右 | 接近打满 | token 生成速度稳定,但算力远没吃满 |
| 并发过低 | 20%-40% | 20%以下 | 30%左右 | 请求稀疏,带宽和算力都在空转 |
这张表说明了推理场景的一个核心特征:GPU 利用率数字很好看的时候,反而可能是卡在显存带宽上;数字难看的时候,可能只是没喂够请求。
1.2 为什么推理会出现"显存爆了、算力闲着"
这个问题要从模型推理的计算模式说起。拿大语言模型的自回归解码举例:模型每生成一个 token,都要把全部权重参数从显存里读一遍(权重矩阵是静态的),再把当前的 KV Cache 读一遍,然后做矩阵乘。整个过程下来,从显存搬运数据的耗时,远大于真正做矩阵乘的耗时。
这个特性有一个专业说法叫memory-bound。我们可以用一组粗略数字感受一下:一张 A100 的显存带宽大约在 2TB/s 量级,算力在 312TFLOPS(FP16)左右。假设模型是 7B 参数、采用 FP16 加载,那么光是把全部权重读一遍就需要搬运约 14GB 数据,理论上最快也要约 7ms 才能读完。而同样这些权重对应的计算量,如果按照 312TFLOPS 的峰值算,大约只需要不到 0.2ms 就能算完。这就是为什么 decode 阶段永远跑不出"算力拉满"的效果——瓶颈在数据搬运通道上。
搞清楚了这一点,优化方向就清晰了:要么减少搬运的数据量(量化、剪枝),要么提高搬运效率(算子融合、显存布局优化),要么让搬运和计算重叠起来(流水并行、连续批处理)。后面几个章节其实都是围绕这三条路在展开。
2. 模型侧优化:先决定卡在"搬运"还是"计算"上
2.1 量化:不只是省显存,更是减轻带宽压力
很多人对量化的第一印象是"模型变小,能塞进更小的卡"。这当然没错,但放到推理场景里,量化的真正价值在于削减了每生成一个 token 需要从显存搬运的数据量。
举个例子。同一个 7B 模型,FP16 版本权重约占 14GB,INT8 版本约占 7GB,INT4 版本约占 3.5GB。带宽需求基本上是等比例下降的。也就是说,在带宽受限的 decode 场景里,INT8 的生成速度理论上可以接近 FP16 的两倍,INT4 接近四倍——当然这是理想情况,实际还会受量化误差、反量化开销、稀疏度等影响。
我在某个模拟项目 X 里做过对比测试,同一个 7B 模型的对话场景,FP16 和 INT8 的实测数据如下:
| 配置 | 显存占用 | 生成速度(token/s) | 首 token 延迟 | 量化后效果偏差 |
|---|---|---|---|---|
| FP16 | 约 15.2GB | 28 | 42ms | 基准 |
| INT8(weight-only) | 约 8.1GB | 45 | 38ms | 可忽略 |
| INT4(weight-only) | 约 4.6GB | 52 | 35ms | 少量下降,复杂推理偶发质量波动 |
这个结果的趋势很典型:带宽压力下降带来的收益,比算力提升带来的收益更明显。但我也要提醒一句,INT4 不是无脑上的选择。对数学推理、代码生成这类对精度敏感的任务,量化误差会被放大。我踩过的坑是:模型做 INT4 之后,日常对话几乎无感,但让模型算"三位数乘法"时,偶尔会算错。后来给了几层敏感层保持 FP16,问题才缓解。这类混合精度方案(简称 mixed-precision quantization)值得多花时间调。
2.2 KV Cache 和 PagedAttention:显存碎片的隐形吞噬者
另一个经常被忽略的显存黑洞是 KV Cache。生成式模型在推理过程中,要把每个历史 token 对应的 Key、Value 缓存下来。上下文越长,KV Cache 越大,而且它是动态增长的。更麻烦的是,如果框架用朴素的方式为每个请求预分配最大长度的缓存空间,会浪费大量显存;如果按实际长度动态分配,又会产生碎片。
一个 7B 模型,假设 4096 上下文长度、batch size 为 16,每个请求的 KV Cache 大约在数百 MB 到 1GB 之间,一整个 batch 的 KV Cache 可能占到 10GB 以上。这还没算权重本身。显存大头经常不是模型权重,而是 KV Cache。
解决这个问题的主流思路是类 PagedAttention 机制:把 KV Cache 拆成分页式小块,按需分配,显存利用率大幅提高。我实测过某框架开启分页注意力前后的对比:同型号卡,静态预分配方案最多只能同时服务 8 路并发;分页式缓存方案能到 16 路以上,吞吐提升了接近一倍。
这里给个建议:选推理框架时,优先确认它对 KV Cache 的管理方式,这比看它支持多少种量化格式更重要。KV Cache 管理直接决定了并发上限和显存利用效率。
2.3 算子融合和 Flash Attention:减少显存往返次数
到了算子融合这个层面,实际上是在跟"数据搬来搬去"较劲。标准的 attention 实现里,Q、K、V 矩阵要先算出来、写回显存,然后做 softmax,再写回显存,最后跟 V 相乘。每一步都在搬运中间结果,带宽消耗非常可观。
Flash Attention 这类优化思路的核心不是"算得更快",而是把多次读写合并成一次流式计算——不让中间结果落到显存,而是尽量保持在片上存储里。它针对的正是带宽瓶颈。
这类优化的收益有多大?我在某图像处理 Demo 之外的另一个纯文本模型上做过测试——注意这里不是图像,而是超长文本的摘要生成任务。输入序列长度 2048 时,接入 flash-attention 后单请求的 prefill 阶段耗时从 4.2 秒降到 2.6 秒,降幅接近 40%。但到了 decode 阶段,收益就没那么夸张,因为 decode 的瓶颈早已从"注意力计算"转移到"权重搬运"上了。
所以我的经验教训是:算子优化适合在已经确认了瓶颈是在某个具体的计算算子或数据流路径上之后再做,不要一上来就投入精力。大部分人面对的推理瓶颈,远没到需要手写 CUDA kernel 的程度。
3. 框架与调度策略:利用率提升最立竿见影的一层
3.1 从静态 Batching 到 Continuous Batching
模型层面的优化是把最高吞吐的天花板抬高,但真正决定你能触达多高的人,是底层的调度策略。传统静态 Batching 的逻辑很朴素:攒够 N 个请求再一起推理,一起算完一起返回。这个模式下,一个 batch 内的生成速度由最慢的那个 token 决定,而且 prefill 阶段(处理输入)和 decode 阶段(逐 token 生成)耦合在一起,GPU 经常处在"干一会、歇一会"的状态。
Continuous Batching(连续批处理)解决的就是这个问题。它允许一个 batch 里的请求动态进出:某个请求的 token 生成完了,立刻从 batch 里挪走,腾出的位置给排队的新请求。GPU 上始终保持着满编的请求队列在推进。效果非常直观——在线对话场景下,我用同一个模型对比过两种调度方式:
| 调度方式 | 并发路数 | 吞吐(token/s) | P99 首 token 延迟 | GPU 利用情况 |
|---|---|---|---|---|
| 静态 Batching | 8 | 120 | 900ms | 高低波动明显 |
| Continuous Batching | 8 | 310 | 260ms | 持续高位 |
吞吐翻了近三倍,延迟反而更稳定了。原因就是 GPU 的空闲缝隙被新请求填上了。如果你的服务还在用"攒批"的方式做推理,第一步不是换卡,是换调度策略。
3.2 并发度调的不仅是数字,是显存和延迟的平衡
一旦用上了连续批处理,下一个问题就是:并发度调到多少合适?
这里有一个很容易翻车的误区:并发调得越高,GPU 利用率越好看,但每个请求能分到的算力和显存带宽就越少,响应会变慢。压测时我见过有人把并发调爆,GPU-Util 99%,但业务方反馈"转圈圈转半天"。
并发度的合理值取决于三个因素:单请求的 KV Cache 占用、显存总容量、延迟要求(SLA)。我通常用一个粗算公式来定初始值:
理论并发上限 ≈(显存总量 - 权重占用)/ 单请求最大 KV Cache 占用
然后在这个上限的 60%~80% 处开始压测,逐步上调。每轮只调一个参数,观察 P99 延迟和吞吐的拐点。以某个 7B 模型为例,我压测时用 4/8/16/24 四个并发档位:
| 并发 | GPU-Util | P99 首 token | 吞吐(token/s) | 显存占用 |
|---|---|---|---|---|
| 4 | 38% | 180ms | 110 | 9GB |
| 8 | 62% | 220ms | 220 | 14GB |
| 16 | 88% | 380ms | 330 | 24GB |
| 24 | 96% | 820ms | 340 | 29GB(接近上限) |
从 16 到 24,吞吐只涨了 10%,P99 首 token 延迟却翻倍还多。这个拐点就是该业务的实际最优值。利用率的目标不是"最满",而是"在可接受延迟范围内最高"。
3.3 Prefill 和 Decode 是否要分离
在线对话场景下,一个请求的 prefill 阶段(一次性处理所有输入 token)和 decode 阶段(逐个生成输出 token)会轮流占用 GPU。两者特性完全不同:prefill 是计算密集型,decode 是带宽密集型。合在一起跑的时候,大 prompt 的 prefill 会挤占大量算力,导致正在 decode 的请求卡顿。
分离式推理架构(chunked prefill)就是想把这两者拆开:把 prefill 切成小块,穿插到 decode 批次的空隙里执行,避免长 prefill 独占 GPU 太久。这个方案对长文档问答、批量离线推理这类场景效果显著;但对短 prompt 的在线聊天场景,切开 prefill 反而增加调度开销,不一定更快。
我自己的判断标准很简单:如果业务里超过 10% 的请求输入长度超过 1024 token,值得试一下 chunked prefill;如果绝大多数请求都是短文本,就别折腾了,保持原样。优化的目的是匹配业务画像,不是在真空中追求架构的先进性。
4. 部署侧与性价比判断:不要盲目追求数字漂亮
4.1 显存规划与多模型混跑
很多时候单模型撑不满一张卡,最典型的就是显存还剩大半、利用率却徘徊在 30% 以下。这时候把多个模型或多个副本塞到同一张卡上混跑,是提高整体资源利用率的常规手段。
混跑有两个风险要先评估清楚:一是显存容量风险——两个模型加上各自的 KV Cache 不能超过卡的物理上限,这个靠估算能提前算明白;二是 SM 争抢风险——两个模型同时跑在峰值时,算力和带宽互相挤兑,可能导致两边的延迟都劣化。
我在某跨平台系统里做过 2 个 7B 模型的混跑实验,结论是:当每个模型的负载都在 40% 以下时,混跑几乎无感;当负载都超过 60% 时,P99 延迟立刻劣化。所以混跑适合负载有互补峰谷的场景(比如一个白天忙、一个晚上忙),不适合两个都是高负载的重型服务。
4.2 从利用率到真实收益:业务指标才是终点
我一直强调一个观点:GPU 利用率是手段,不是目的。业务方不关心利用率,只关心三类指标:
- 首 token 延迟(TTFT,Time To First Token)
- 生成速率(TPOT,Time Per Output Token)
- 整体吞吐(每秒完成的请求数或 token 数)
调优时如果只盯着利用率,很容易陷入"数字很好看的骗局"——利用率拉满,但请求全在排队,延迟一塌糊涂。我在做调优决策时,会先问自己一个问题:当前的利用率低,到底是因为"没活干"还是"干不过来"?
- 如果是"没活干":说明请求稀疏,再优化框架也没用,该做的是负载合并、请求缓存、预测预加载。
- 如果是"干不过来":说明在排队,需要提升单卡吞吐,或者横向扩卡。
这两类问题用同一个方案去解,方向就错了。判断方法很简单:看请求排队长度和 GPU 空闲时间分布。如果卡上有大段空闲,但队列里没请求,那是流量问题不是性能问题;如果队列里排着长队,但 GPU 还是跑不满,那是调度或框架问题。
4.3 CPU 侧的"非 GPU 手段"常常性价比最高
优化 GPU 利用率时,很多人会忽略 CPU 侧和系统侧的配套工作。我遇到过不少场景,GPU 利用率低不是因为卡不行,而是数据压根没送到卡上。具体包括:数据预处理太慢导致 GPU 等输入;tokenizer 解析耗时太长;结果后处理阻塞了释放资源;网络传输延迟导致请求到达速率不稳定。
有一次处理一个对话服务慢的问题,我排查到根因居然是 CPU 上的 tokenizer 单线程实现成了瓶颈,GPU 经常等 tokenizer 算完才拿到输入。换成批量 tokenizer 实现后,GPU 利用率从 51% 提到了 78%。这个优化一行 GPU 代码都没改。所以调优顺序应该是从数据入口开始,逐层往后排查,而不是一上来就动模型格式和 kernel。
这里列一下我常用的从粗到细的排查链路:
| 排查顺序 | 检查项 | 常用手段 | 典型结论 |
|---|---|---|---|
| 1 | 流量是否足够 | 观察请求到达速率与队列长度 | 流量稀疏则先做合并/缓存 |
| 2 | CPU 预处理是否拖后腿 | 多线程/批量 tokenizer、预加载 | GPU 经常空等则在此处修 |
| 3 | 框架调度是否高效 | 开连续批处理、分页注意力 | 利用率波动大的主因在此 |
| 4 | 显存规划是否合理 | 估算 KV Cache、调整并发上限 | 显存打满但算力空闲则调整 |
| 5 | 模型本身是否带宽过载 | 量化、混合精度、算子融合 | 已经跑满带宽才做这项 |
这个顺序的本质是:先解决"有没有活干"和"活得能不能送到",再解决"送到的活能不能排好队",最后才轮到"排队后的活能不能算得更快"。我见过太多人跳过前面的步骤直接做模型量化,结果收益远不如预期,还引入了精度风险。
4.4 监控与调优工作流的落地建议
最后说一点实操上的工作流建议。调优这事最怕的就是凭感觉改参数,改完看个大概就收工。我自己跑项目时,一定会先做三件事:
第一,留一份完整的 baseline 记录。包括模型配置、并发参数、压测流量、三项业务指标和两项资源指标(SM 占用率、显存带宽利用率)。没有 baseline,后面所有改动都无法评估收益。
第二,每轮只动一个变量。调并发就只调并发,量化就只做量化。同时动两三个变量,出了问题根本定位不到是哪一步引入的。这个原则听起来基础,但实际执行时特别容易破功——尤其是你急着想看到效果的时候。
第三,用固定流量压测而非线上随机流量评估。线上流量波动太大,压测结果不可比。我会录一份代表性的请求混合(比如短文本聊天和长文档总结按 7:3 混合),每次改动都回放同一份流量,数据才有可比性。
我在做某个模拟项目时,就是靠这三条原则把 GPU 利用率从 36% 提到 83% 的同时,把 P95 延迟压低了 40%。每一步的收益都能追溯到具体改动,团队复盘的时候也特别清晰。
5. 写在最后
我自己的体会是,GPU 利用率提升这件事,本质上是一场"识别瓶颈类型"的游戏。先判断卡在流量、CPU、显存、带宽还是真正算力上,再决定用调度、量化、算子优化还是扩容去解。方向判断对了,一个很小的改动就能带来巨大收益;方向不对,砸进去再多时间也不会有明显效果。
如果只能给一条建议,那就是:别急着换卡,先把nvidia-smi之外的那几个指标盯起来。看懂带宽利用率和 SM 占用率之后,你会发现自己手头这张卡的潜力,远比想象中要大。