先抛一个现象很多人在做模型推理部署时对着监控面板发愁——GPU利用率只有百分之二三十显存却快占满了业务上并发也不低可算力就是吃不动。第一反应往往是“代码写错了”“batch设太小”“框架有问题”。我一开始也这么怀疑直到把推理链路从头到尾拆了一遍才发现大部分情况下不是代码的问题而是推理场景本身的结构性难题。这篇文章想把“AI模型推理GPU利用率提升”这件事讲透。我会从底层原因开始把动态批处理、显存治理、算子优化、框架参数调优这几条主流路线串起来再附上实际部署中踩过的坑和排查经验。适合正在做推理服务部署、模型优化、量化加速或者对大模型推理性能调优感兴趣的开发者。读懂原理之后再带着监控数据去优化会比盲目改参数高效得多。1. 先搞清楚推理场景的“GPU利用率低”到底卡在哪1.1 训练和推理的本质差异决定了瓶颈不同GPU利用率低首先要理解一个底层差异训练和推理对GPU的使用方式完全不同。训练是密集的批量矩阵运算数据准备好之后计算是一波接一波的GPU大部分时间都在满负荷跑。而推理面对的是真实请求到达时间随机、序列长度不一、输出还要逐token生成天然存在大量碎片化空闲。打个比方训练像一条标准化流水线原料源源不断送进来每个工序都是机器满负荷运转。推理更像一家小餐馆顾客随机进门有人点快餐有人点硬菜后厨不能为了等客人一次性做完全部菜也不能只为一桌客人把整个厨房停下来。GPU就是那个大厨他的利用率低往往不是手艺不行而是“没活可干”或者“活太碎”。知道这一点很重要因为很多人一看到利用率低就想着换更贵的卡、堆更多算力实际上换个GPU只能提升峰值上限解决不了调度和碎片化的浪费。1.2 利用率低的四个典型隐形杀手推理场景下GPU利用率上不去通常逃不出四个原因请求碎片化与batch太小。模型推理的吞吐量随batch size增大而明显提升但一次只推理一个请求时GPU大部分时间花在算子启动和数据搬运上算力本身根本没怎么发挥。自回归解码的串行依赖。生成类模型一次只输出一个token每一步都要等前一步的结果而每一步的前向计算量并不大。这一步的GPU运算时间可能很短但CPU调度、显存读取这些固定开销省不掉。KV Cache把显存吃满batch被迫调小。请求一多、序列一长KV Cache占用迅速膨胀显存不够用batch size就只能降下来GPU自然跑不满。CPU侧成为瓶颈。分词、采样、日志、网络传输都在CPU上执行。CPU忙不过来的时候GPU只能空转等数据。这四类原因经常叠加出现而且不同场景下主导因素不一样。所以在动手优化之前先别急着改代码先搞清楚自己当前系统的瓶颈主因后面的方案才有方向。2. 从“攒批”到“连续批处理”把GPU喂饱的第一板斧2.1 静态批处理为什么喂不饱GPU传统的推理服务普遍采用静态批处理攒够N个请求组成一个batch一次性送进GPU做前向计算等整个batch全部生成完毕再统一返回。这个方案实现简单但有一个很严重的浪费batch内所有序列必须同步推进任何一个序列没生成完整个batch都不能释放。我见过一个典型的例子一个batch里有两个请求一个问“你是谁”三五个token就答完了另一个要求写一千字长文要生成几百个token。两个请求必须一起等短请求早就生成完了却要陪着长请求把时间耗尽。这个阶段GPU在计算那些已经生成结束的token的位置上做的全是无效计算。更麻烦的是攒批的过程本身就在制造等待请求不够一批的时候GPU是空的请求超出一批的时候多出来的请求又在排队。这种“凑满一车才发车”的模式在并发平稳的大流量场景下勉强可用但在请求长短不一、到达波动的场景下浪费非常明显。2.2 连续批处理的核心思路与实现要点后来的方案是连续批处理也叫迭代级批处理。思路一句话就能说清在生成过程的每一步重新组织计算批次。某个请求生成了终止符或者达到长度上限立刻退出batch不等其他请求新请求马上补进来。GPU每一轮都在计算有用的token而不是空转等慢请求。这个方案的名字很好理解不再是固定的一组人坐一辆车而是滚动发车到站就下车新乘客随时上车。对于长短混杂的请求连续批处理能把GPU利用率拉上去好几个档位。但实现起来有几个点要特别注意。第一推理引擎必须支持对不等长序列做有效处理。最笨的做法是每个iteration都做padding把batch内所有序列填充到同样长度。如果batch内有序列长度差异很大padding部分会让收益大打折扣。我自己做过一个实验同一批请求长度从几十到上千token不等如果引擎只支持静态padding连续批处理带来的收益还不如静态批处理。所以选框架时一定要确认它真的支持变长序列推理而不是名字叫“动态batch”实则在背后疯狂padding。第二调度策略不能太简单。连续批处理引入了每步动态调度新请求什么时候插入、老请求是否优先都需要调度器决策。常见的策略有最长等待优先、设置单请求最大等待时间、按照SLA优先级处理。比较实用的做法是“接近超时的请求优先调度”这能让服务端的P99延迟明显改善而不是只提高平均吞吐。第三现在很多推理框架会把prompt解析阶段和token生成阶段分开处理。prompt阶段适合大batch并行处理decode阶段则适合连续批处理的调度模式。这两阶段用不同的策略能进一步吃满GPU。如果你用的是成熟框架优先确认它是否自带连续批处理能力如果是自研引擎改造调度器的工程量不小但在高并发、请求长度差异大的场景下收益比非常高。3. 显存是真正的天花板KV Cache与显存治理3.1 KV Cache到底偷走了多少显存很多人以为模型推理最大的显存开销是权重参数其实在长序列、大并发场景下KV Cache才是真正的显存大户。自回归生成每一步都要读取之前所有token的Key和Value来计算注意力为了不从头重算这些中间结果必须缓存下来。KV Cache的大小估算公式并不复杂KV Cache字节数 2K和V两个矩阵× 层数 × 注意力头数 × 每头维度 × 序列长度 × batch size × 每个元素字节数举个例子某7B级模型假设40层、32头、每头128维FP16存储每个元素2字节同时处理32个请求平均生成序列长度是2000 token2 × 40 × 32 × 128 × 2000 × 32 × 2 约40GB这个数字意味着什么一张常见的24GB显存GPU光KV Cache就远远放不下更别提还有权重参数和其他中间张量。如果序列长度再涨到4000KV Cache直接翻倍。KV Cache决定了batch size能开多大而batch size又直接决定了吞吐上限所以不治理KV CacheGPU利用率就一定上不去。这里要说清楚KV Cache不是可以省掉的省掉它就得每一步都从头对整段序列重算注意力计算量随序列长度平方增长更不可行。优化方向只有两条更省地存或者更高效地复用。3.2 显存碎片与按块分配的KV Cache另一个被低估的问题是显存碎片化。不同请求的序列长度不一样KV Cache动态申请、动态释放长期运行下来显存里到处是碎片。碎片多了明明总计剩余显存不少但是分配不出连续的大块区域导致新请求无法加入batchbatch size被迫下降。这个场景很像一个停车场每辆车需要的车位大小不同车不断进出空位散布在各个角落但一辆大巴需要连续的整片车位停不进去。结果是“看起来有位子实际用不上”。更好的方案借鉴操作系统的分页思想KV Cache不再按“连续大块”分配而是拆成固定大小的块非连续的物理块通过页表映射起来。每个请求开始时先申请几个块不够了再动态追加。这种按块管理能把碎片化影响降到最低显存利用率大幅提升batch size自然能上去GPU也就更容易跑满。很多开源推理框架能在高并发场景下做到高吞吐核心秘密就在这里。自研引擎的话这个改造非常值得做但如果是小规模内部服务并发请求只有个位数收益有限不必一上来就追求这么复杂的架构。3.3 KV Cache量化与序列长度上限KV Cache默认用FP16存储一半的精度其实是冗余的。把它压缩到INT8显存直接砍半压到INT4能再砍一半。这就是KV Cache量化。实操建议是按阶梯来先在INT8档位验证如果生成质量没有明显劣化再考虑INT4。量化对生成质量的影响在不同模型、不同任务上差异很大不能只看BLEU这类指标一定要抽几条真实业务样例人工看一下。压缩之后还需要保留缩放因子有些框架支持自动校准能省不少手工验证的时间。这里还有一个很多人忽视的调参点max_seq_len设太大框架会按最大值预留KV Cache空间这部分显存是固定的“占位”无论用不用都拿不回来。我曾经把序列上限拉到能力的极限值结果显存里大半都是预留空位batch size被压到个位数吞吐反而暴跌。后来把序列上限设置到贴近真实业务分布的百分之九十五分位预留空间大幅缩小batch size明显提升整体吞吐反而涨了一大截。长尾请求可以走“超长序列再分配一块额外空间”的策略而不是一开始就给所有人留最大空间。4. 算子级优化与执行效率让每一毫秒都花在刀刃上4.1 先认清瓶颈是算不动还是搬不动GPU利用率低有时并不是算力不够而是数据搬运太慢。大模型推理尤其是decode阶段每个token只需要一次前向计算但这一次计算要把权重参数从头到尾读一遍还要把当前序列的全部KV Cache读一遍。权重和KV Cache都在显存HBM里HBM的带宽是固定的读这么多数据本身就要花不少时间。计算量相对不大“搬数据”的时间反而占了大头。用一个类比帮助理解你要查一本书翻到指定页看内容可能只要一分钟但从书库把书调出来可能要半小时。GPU每次都像在大型书库里来回跑跑路时间比阅读时间还长。这种问题叫访存受限靠增加计算核心数没用得减少访存次数。4.2 FlashAttention这类算子为什么收益大FlashAttention的思路就是减少显存读写把attention计算切成小块在芯片的片上内存里完成部分计算避免把中间矩阵写到显存再读回来。它没有降低计算量但大幅减少了HBM访问次数对长序列场景收益尤其明显。在推理侧FlashAttention带来的直接影响是KV Cache读取开销降低而KV Cache恰恰是访存瓶颈的大头。现在主流的GPU推理框架基本默认启用这类融合注意力算子使用者不需要手动实现但要理解它在做什么——序列越长收益越大如果你的业务都是短query短回答那这个优化带来的效果不会太显著。4.3 算子融合与减少kernel启动开销除了访存另一个容易被忽略的开销是kernel启动。一个模型前向推理要执行几百上千个GPU算子每一个算子从CPU下发到GPU都有固定延迟。如果算子拆得太碎GPU大部分时间在“接指令”而不是“干活”。算子融合的思路是把相邻的多个算子合并成一个比如RMSNorm、残差相加、激活函数、量化这串操作原本要启动四五个kernel融合后一个kernel即可完成。既减少了启动次数也减少了中间张量写回显存再读出的开销。更强的做法是图捕获技术把一整串kernel调用捕获生成一张计算图一次性提交给GPU执行把CPU和GPU之间的通信往返降到最低。很多框架在小batch场景下能靠这个技术吃到显著的利用率提升。但图捕获有个明显的坑它对输入输出的shape敏感shape一变就得重新捕获。如果你的请求长度动态变化一个静态捕获的图就无法覆盖所有情况。实际工程中一般按序列长度分桶比如128、256、512、1024各捕获一份再根据实际长度选择最接近的图如果长度落在桶外的长尾区间就走普通动态路径。配置不当会让捕获动作反复触发显存和延迟双双崩溃这一点我在后面排查章节会细说。4.4 量化最直接的算力扩张手段权重量化是另一个高性价比优化。把FP16权重转成INT8、FP8甚至INT4显存占用降低batch size可以开更大同时部分GPU对低精度计算有更高吞吐等于变相给GPU“扩容”。量化需要注意的细节不少不是所有层都适合低精度。某些敏感层对精度极其敏感比如归一化层、注意力输出投影保留FP16更稳妥。现在很多方案支持混合精度只对非敏感层做量化。校准集要贴近真实数据分布。校准集应该覆盖业务里最常见的prompt和输出长度分布随便拿一批文本凑数上线后很容易出问题。量化之后必须做质量回归。除了跑指标还要人工抽测实际用例这一点不能省。我见过一个项目追求极致的显存节省把权重压到INT4最终质量指标只掉了零点几个点但实际抽检时几类典型case输出内容明显偏离。后来改成敏感层保留FP16、其余INT4混合精度显存节省了大部分质量也回到了可接受范围。量化不是越激进越好目标是在显存收益和质量损失之间找平衡点。方案显存权重占用相对推理吞吐精度风险建议场景FP16基线1倍1倍无作为对照基线INT8权重约一半提升明显较低缺显存或需提吞吐时优先尝试INT4权重约四分之一更高中高显存极度紧张且有校准数据KV Cache INT8KV Cache减半支持更大batch低长序列、大并发场景KV Cache INT4KV Cache四分之一进一步扩batch中需抽测验证质量权重量化和KV Cache量化可以叠加使用但每一步都要通过质量回归验证不能只看显存数字好看。5. 框架选型与关键参数调优从原理到落地的最后一公里5.1 先站在别人的轮子上再决定要不要自研推理优化领域方案繁多选型时最容易犯的错是“什么都想自己造”。实际上现在的开源推理框架已经内置了连续批处理、按块KV Cache、量化支持和图捕获等能力直接用框架往往一天之内就能看到明显收益。自研引擎只建议在两种情况下考虑一是模型结构高度特殊现成框架无法高效表达二是目标硬件不在框架支持范围内。其他情况下我都会建议先用成熟框架把基线跑起来再针对瓶颈做定向定制。商用推理引擎的优化通常更深但可能带来成本和生态绑定问题小团队可以先不做这个选择。5.2 部署参数里最容易出效果的几个不管用什么框架下面几个参数几乎决定最终性能表现值得逐一精细调整显存利用率上限。通常推荐设置在0.8到0.9之间给运行时和内存碎片留出余量。设到0.95看着很大方但一旦显存抖动就容易OOM反而导致服务不可用。最大序列长度和最大批大小。这两个参数共同决定了KV Cache的预留空间和调度空间。建议以真实业务的长度分布为基准去设置而不是拍脑袋拉满。调度策略和队列策略。设置并发上限、单请求最大等待时间以及SLA优先级后再安下心来调延时会舒服很多。服务化层的并发控制。请求排队时间过长不只延迟高还会拖垮整体吞吐需要配合压测找到并发上限。调参流程我一般是这样做的先用小流量压测记录GPU利用率、P99延迟和显存占用三个指标然后逐步增大并发观察两条曲线——利用率开始明显下降或延迟陡然飙升的位置就是当前参数下的容量拐点再调整max_batch和序列上限重复压测记录不同场景的最佳参数组合。这个过程我会反复做每个模型上线前都会来一轮。5.3 多卡扩展先单卡吃满再考虑堆卡当单卡放不下模型时多卡并行不可避免。但多卡不等于高利用率搞不好只是把一个低利用率复制到多张卡上。常用的多卡方案有三种。张量并行把单层计算拆到多张卡上通信频繁需要高速卡间互联适合延迟敏感的大模型推理。流水线并行按层切分通信量少一些但存在流水线气泡需要配合交错调度来减少空闲。数据并行最简单每张卡放一份完整权重各自处理不同请求吞吐可以线性扩展但每张卡都需要足够的请求才能跑满请求量不够时利用率会很低。我的实操建议是先把单卡利用率吃到80%以上再谈多卡扩展。如果单卡利用率只有30%多卡并行很可能只是把30%复制到四张卡上成本翻倍收益却很低。多卡方案一定要用高速互联否则通信等待会让并行收益大幅缩水。6. 常见问题与排查实战从现象到原因的定位手册6.1 一张可以照着做的排查清单推理服务GPU利用率上不去排查要基于现象分主因。下面是我常用的判断思路现象优先怀疑方向验证手段利用率低、显存接近爆满KV Cache或模型权重占满显存batch上不去看显存占用构成调低序列上限或启用KV Cache量化利用率波动剧烈一会儿100%一会儿0%请求到达不均匀、调度策略导致GPU空转看请求队列长度和调度日志设最长等待时间利用率低、CPU占用高数据预处理、采样、日志成为瓶颈用性能分析工具看CPU热点把CPU侧耗时的逻辑异步化或者精简小batch下利用率低大batch利用率高但延迟飙升算子启动开销或访存瓶颈配合图捕获、算子融合优化利用率低、延迟也低请求本身太少系统在空跑增大并发模拟真实负载再测观测工具方面GPU实时状态用厂商自带的监控命令看利用率和显存长期趋势需要接监控面板。要深入定位算子级的空闲就得用深度学习框架自带的性能分析器看kernel时间轴着重看两个信息kernel自身耗时占比以及kernel之间的空隙大小。空隙大说明调度或数据准备在拖后腿kernel自身耗时长说明算子或访存需要优化。6.2 我实际踩过的几个坑复盘自己踩过的问题有几个特别典型。第一个坑是盲目开图捕获却没有处理动态长度。第一次部署时我一开图捕获推理速度立刻涨了一截正高兴呢过了两分钟显存和延迟一起崩了。查了半天发现请求长度稍微一变框架就触发一次重新捕获捕获期间大量显存被临时占用。正确做法是像我前面说的那样按长度分桶捕获多份图长尾请求走动态路径。第二个坑是日志打印把CPU打穿了。当时为了观察生成效果每个token都打一条日志到标准输出结果GPU利用率从80%直接掉到40%。排查时CPU核心全在输出日志。后来改成按请求汇总、异步落盘只在出现异常时打详细日志吞吐马上恢复了。这个坑很不显眼但非常常见。第三个坑是KV Cache量化激进上线质量崩了才发现。我把KV Cache压到INT4显存省了batch大了看着一切完美结果线上有类case输出内容经常跑偏用户反馈之后才回滚。后来换成INT8同时对长序列场景启用KV Cache量化、短序列保持高精度平衡了质量和性能。量化之后一定要做真实样例抽测这个环节真不能省。第四个坑是max_seq_len设置过度。我当时想“既然要做长文本生成干脆支持到极限”结果这个参数让框架预留了大片KV Cache空间batch size被压到十几GPU利用率一直上不去。把上限调回贴近真实需求的长度后batch size翻了几倍吞吐反而大幅提升。6.3 优化优先级先吃最大的红利再抠细节最后分享一个优先级排序的经验这是我做了多次优化之后总结出来的第一优先级先把连续批处理跑起来。这个方案对GPU利用率的提升幅度最大而且不需要牺牲质量属于“正规军”级别的优化。第二优先级治理KV Cache。按块分配、合理设置序列上限、必要时做KV Cache量化把显存这块天花板抬起来。第三优先级再考虑图捕获和算子融合减少调度开销和访存浪费。第四优先级才是权重量化。量化涉及精度风险需要大量质量验证应该放在最后做。如果你当前的GPU利用率只有百分之十几大概率问题在请求调度和显存管理而不是算子不够快。这时候花大力气优化算子收益远不如先改调度和显存策略。先找到那个最大的瓶颈再动刀。我自己每次做推理优化都会先打印三张曲线GPU利用率、显存占用、P99延迟。三项指标对照着看能快速排除掉一半以上的错误方向。另外一个小技巧在正式压测之前先造一批长度分布和线上业务相似的请求。请求长度分布对KV Cache和调度的压力影响非常大用纯短文本压出来的结果到了线上长文本混流量时往往完全不是一回事。推理优化没有一劳永逸的方案。模型结构换了、业务请求分布变了最优参数都会跟着变。与其照搬别人的“最优配置”不如把自己系统的性能基线和瓶颈定位方法掌握清楚每一次优化都用数据说话自然能稳稳地把GPU利用率从20%一路抬到80%以上。