资讯动态

GPU推理性能优化:从调度策略到动态批处理的完整实践

发布时间:2026/10/19 2:57:05 来源:尧图企业网站定制
1. 项目现状高配卡、排队请求、不到三成的利用率做推理性能分析的人最常遇到的怪现象不是“卡到爆”而是“明明资源还有富余吞吐却上不去”。这次我接手的项目就是这样一套给线上AI问答模型服务的推理环境刚换了一张算力强得多的新卡并发请求从早到晚没断过按道理吞吐至少能翻一倍。结果上线第一周监控面板上GPU计算利用率一直徘徊在25%到30%请求超时率不降反升。当时有同事第一反应是“换卡换出问题了”还有人说部署脚本哪里没配好。我把模型推理时间拆开一看模型本身的计算没有异常数据搬运的耗时也正常真正的问题出在“谁先算、谁后算、几批一起算”的调度环节。AI推理和传统批量计算不同它面对的是稀疏到达、文本长度各异的请求流。GPU要保持高利用率必须有调度器把不同输入临时拼成适合计算的批次再根据模型是预填充阶段还是逐字生成阶段来回切换执行顺序。分派逻辑稍有问题GPU就会在等待批次凑齐的过程里漏掉大量时间最终表现为利用率、吞吐量一起拉胯。这次排查前后花了差不多两周过程很折腾但最后把推理吞吐提升了近十倍也把一套怎么找瓶颈、怎么调参的方法沉淀了下来。这篇文章就把完整的调度性能分析过程展开讲一讲读完你可以直接拿去对照自己的线上服务。1.1 这是“算得慢”还是“等得久”我在分析时做的第一件事是分清请求的端到端延迟里有多少花在真正的计算上有多少花在等。方法很简单把单个请求进推理引擎后打上时间戳记录进入排队、开始执行、预填充结束、首个token输出、完全结束这几个节点然后对比同一段时间里GPU实际执行计算内核的时长。结果让我很意外。单个请求的平均端到端延迟大约是120毫秒但它在GPU上的实际计算时间只有20毫秒出头。也就是说接近80%的时间处于“已经准备好却没人管”的状态。这不是计算能力的问题是典型的调度等待问题。举个生活里的类比一家餐厅明明厨师很快但服务员要凑够八个人才肯引座入席结果大厅一半空着门口却排长队。GPU就是这个厨师调度器就是那个凑桌的服务员。这个发现改变了整个排查方向。之前所有人都在查模型算子、查显存大小、查推理框架版本方向全错了。真正该看的是请求从进入到出结果中间那一段调度逻辑。1.2 请求排队数与GPU空闲同时出现第二个关键现象更反直觉高峰期排队请求数很多但GPU内核执行窗口却很空。我在监控里同时挂了两个维度一个是应用层的请求排队长度一个是GPU侧的计算活跃时间。晚高峰时队列里经常堆着三四十个请求但每次内核处理的间隙里GPU能空出好几毫秒。想象一下幼儿园门口三十个孩子等着进门教室里却只有两个老师还在悠闲地喝茶。这完全不是“忙不过来”而是“分配不过来”。为什么会出现这种矛盾因为我们的请求到达时间极不均匀有时候一秒来一个有时候一毫秒来一堆。调度器采用的是固定窗口批处理也就是每隔固定时间收集一次请求不够一个批次就发车结果请求稀疏时让大佬闲等请求密集时又因为批次窗口设置太大而把后到的请求全部堵住。GPU内部的执行管线一会儿被喂满一会儿饿肚子利用率自然被平均得很难看。把这个现象记录下来之后我基本可以确定性能瓶颈不在模型算力也不在显存带宽而在调度策略与请求到达模式的匹配度上。2. 把一次推理拆开看GPU调度到底在哪一环卡住要真正优化调度得先理解一次推理请求在GPU上经历了什么。很多分析报告只给结论不给过程导致很多人对着结果无从下手。我这里把链路拆到“阶段”级别每个阶段对应一个耗时段和可优化点。2.1 请求从进入到出结果的五个阶段一个文本类模型请求从客户端发出到最终返回通常经历五个阶段。第一是接收与解析网关收到HTTP或RPC请求做鉴权、路由在这个阶段主要消耗CPU。第二是预处理把文本切分成token序列加上特殊标记这个阶段同样在CPU上完成但耗时和文本长度强相关。第三是排队任务进入推理引擎的请求队列等待调度器分配批次。第四是GPU执行包含模型的前向计算对于生成式模型又拆成预填充和逐token生成两个子阶段。第五是后处理把输出token拼成完整结果并返回。我用一组线上真实分布数据测算过这五个阶段里阶段三和阶段四的不确定性最大。预处理再慢也就是几毫秒的事可一旦进入排队等待时间可能从几毫秒膨胀到几百毫秒。GPU执行阶段的最大可优化点则在“批次如何组合”而不是“单个请求算得多快”。2.2 三个层面的“调度”分别管什么很多人一听到“GPU调度”以为是显卡底层的那套线程调度。实际上AI推理的调度至少分三层三层职责完全不同。第一层是应用层请求调度负责决定哪些请求进入引擎、哪些请求排队、先处理谁后处理谁。这一层通常由我们自己写的网关或推理引擎的请求调度器完成是优化空间最大的位置。第二层是引擎内迭代级调度比如预填充和生成阶段如何交错一个字一个字的生成阶段能不能和其他请求的预填充并行。这一层决定GPU在细粒度时间片上是否持续有活干。第三层才是硬件级别的线程分配GPU拿到一个计算内核后把线程块分配到各个计算单元上执行。前两层的优化空间大改一个判断逻辑就能提升几个数量级的吞吐。第三层基本由下游库和驱动决定一般不需要应用开发者去碰。我见过有人拿硬件层的线程分配工具去分析整个推理系统忙活半天发现和自己能改的代码完全不搭边。正确做法是从上往下排查先把应用层调度看明白再逐层往下钻。3. 实测数据三种调度策略在同一模型上的表现对比为了验证猜测我搭了一套可复现的实验环境同一个模型、同一张卡、同一份模拟请求数据。请求数据严格按照线上采样得到的到达间隔和文本长度分布生成确保测试场景和真实负载足够接近。然后我只改调度策略其他条件保持不变跑完一轮记录一轮。实验一共比较了四种方式完全串行处理、固定窗口批处理、动态批处理、思路上更进一步的无等待持续批处理。为了让结论可读我在同一个表里记录吞吐量、平均延迟、P99延迟和GPU计算利用率。3.1 完全串行延迟稳定但吞吐感人最朴素的方案是来了一个请求就立刻算一个事件驱动、无队列、无批次。这个方案的优势是单请求体验非常好平均延迟最低因为完全不需要等待凑批次。可GPU是典型的高吞吐并行设备你让它一次只处理一个数据等于让一台货车一趟只送一个包裹。串行方案实测下来每秒完成约2.3个请求GPU计算利用率只有12%左右。模型本身的计算逻辑没什么可指责的问题是每次计算内核启动时只装一点点数据大量性能都耗在了内核启动和权重搬运上。这个数据让我想起很多企业刚上线推理服务时的场景从日志看一切正常就是总吞吐上不去其实是被这种最原始的调度逻辑给卡住了。3.2 固定窗口批处理吞吐上去了抖动也上去了第二种方案是固定窗口批处理每个窗口周期比如100毫秒窗口结束时把队列里积压的请求全部拼成一个批次送进去推理。没有积压就用可能不满的批次发送。这样做的好处是代码简单不需要考虑请求到达率的变化。我测试了窗口大小从50毫秒到200毫秒的多组配置最终发现吞吐量确实比串行翻了四五倍但延迟抖动非常严重。问题在于窗口边缘效应如果一个请求刚好在窗口关闭后1毫秒到达它就得在队列里等完大半个窗口。P99延迟从串行时的42毫秒直接涨到220毫秒。更麻烦的是批内所有请求的输出长度如果不一致较短的请求会被较长的请求拖住形成队列头阻塞。固定窗口批处理适合到达规律、输出长度稳定的传统CV图像分类场景一到文本生成这种时长不定的场景就露馅。3.3 动态批处理把等待时间变成收益率第三种方案是动态批处理也是很多高并发推理服务默认的调度思路不再刻意等固定窗口而是同时维护一个“可立即执行”的判断条件。队列里积压请求达到预设的批次上限就立刻发车没达到上限但已经等了超过最大等待时间也发车。动态批处理的核心价值在于把“等”控制在可接受的范围内。我实测下载入8的批次上限、150毫秒最大等待时间后平均吞吐率达到每秒34.8个请求GPU利用率提升到82%同时P99延迟从220毫秒降到110毫秒。代价是平均延迟比串行高一些但换来的是吞吐近十五倍的提升。绝大多数生产场景要的就是这种取舍。提示动态批处理的两个参数批次上限和最大等待时间不是越大越好。批次过大会让单个请求等太久批次过小又起不到凑批效果。两者需要根据实际到达率计算后面第五章我会给出具体方法。3.4 持续批处理把等待时间变成计算并发第四种方案的思路更加激进。传统批处理在一次批内等所有请求都完成才释放资源而持续批处理允许生成阶段的新请求不断“插队”加入计算。打个比方以前是一条生产线必须所有工件都加工完才能换下一批现在则允许一边加工A工件一边把B工件的毛坯放上另一条空闲工位。我评估后最终在没有引入复杂依赖的前提下用模拟代码模拟了持续批处理的行为。结果显示这一策略能把吞吐拉到每秒45个请求GPU利用率超过90%P99延迟反而进一步降低到96毫秒。原因是生成式推理在逐token输出阶段对算力的需求远低于预填充阶段两者正好可以错峰利用GPU资源。虽然我们生产环境没有立刻全量切换但从数据上看这是所有策略里空间最大的一个。4. 围绕调度的核心指标不能只盯“利用率”性能分析做到这里我发现一个更值得写下来的经验很多人做分析时只盯着一两个指标看尤其是“GPU利用率”。但这个指标单独拿出来看非常容易误导人。4.1 五种指标怎么搭配着看我建议把五个指标放在同一张时间线里观察请求吞吐量每秒完成请求数、平均延迟、P99延迟、GPU计算利用率、GPU显存利用率。每个指标单独看都说明不了全部问题组合在一起才能反映调度质量。拿GPU显存利用率举例。显存高可能只是模型权重占得多和当前计算负载关系不大。计算利用率高则说明GPU有活干但有可能是在算一堆无效请求。吞吐量高也不意味着体验好如果P99延迟已经超出业务可接受范围高吞吐毫无意义。我在分析表里专门加了“排队时间占比”这个衍生指标用它和计算利用率做对比。如果排队时间占比高而计算利用率低基本可以断定是调度批次策略太保守如果两者都高说明确实是请求量超过了算力上限。指标组合表指标反映问题组合判读方法请求吞吐量系统整体产能与P99延迟一起看避免唯吞吐论平均延迟单请求体验基准结合排队时间判断是计算慢还是等待慢P99延迟尾部体验与抖动远高于均值时查批次窗口和队头阻塞GPU计算利用率计算资源饱和度单独高不代表好需配合排队占比看排队时间占比调度效率占比高而利用率低是调度策略失配的信号4.2 从指标反推调度问题的三步判断这组指标还能反推问题的类型。我的经验是按三步走。第一步如果平均延迟高但排队时间占比不高说明瓶颈在模型计算本身要么模型太大要么算子效率低。第二步如果排队时间占比高且计算利用率低那问题几乎一定在调度策略比如批次太小、窗口太短、并发上限卡太死。第三步如果P99延迟远远高于平均值需要重点检查系统里有没有“一个慢请求拖住一批快请求”的情况这在文本生成模型里尤其常见一个长输出请求会把整批请求都锁住。我在这次排查中就是靠这个顺序一步步定位的。一开始所有人都认为是模型问题但排除了计算耗时之后排队占比高、计算利用率低这两个信号已经直接把矛头指向调度层后面所有优化动作几乎是一打一个准。5. 参数调优实录batch size、并发上限、超时阈值该怎么配调度策略光有方向不够还得落到具体参数。这一章我不给通用默认值而是用这次项目的真实计算过程来说明参数是怎么定出来的。你们照着这套流程算一遍就能得到适合自己服务的配置。5.1 batch size 与请求到达率的匹配动态批处理里批次上限和最大等待时间本来就应该是一对互相牵制的参数。它们的关系可以用一个简单公式估算批次等待时间大约等于批次上限除以请求到达率。假设线上到达率是每秒20个请求你设置了批次上限为8那么凑满一个批次平均需要8除以20等于0.4秒也就是400毫秒。这个等待时间必须小于业务的延迟上限一般建议控制在延迟上限的一半以内。如果延迟上限是400毫秒那么批次上限就别超过4到8如果延迟上限很宽比如后台报表任务可以等两秒那么批次上限设32甚至64都合理。我这次的业务线上问答场景延迟上限是800毫秒于是我选择批次上限8、最大等待时间150毫秒的组合。这样平均凑批时间约为400毫秒加上推理本身的约150毫秒整体延迟还有余量。这个计算过程看起来简单但很多人直接照抄网上默认值抄过来发现要么吞吐上不去要么延迟超标。你不根据自己到达率算一遍永远是拍脑袋。5.2 并发上限别让请求无限堆积第二个容易被忽视的参数是最大并发请求数也叫最大在执行请求数。它控制同时有多少批次在GPU管线里流转。从直觉看并发越大吞吐越高。但当并发达到一定程度时新请求根本进不了GPU只是在内存队列里越堆越多队列越长P99延迟越难看最后还可能把网关内存打爆。我把这次实验的并发上限从1调到4分别记录指标发现并发为1时P99稳定在96毫秒并发为2时吞吐提高约三成P99还在合理范围。并发到4时吞吐没有明显提升P99却逼近400毫秒原因是多个批次同时抢占计算资源导致不稳定。最终我采用的是一种带反馈的限流方式当队列积压超过预设水位时不再接收新请求让上游重试或降级。这比单纯调大并发上限更安全。调度器伪代码如下def schedule(): while True: batch collect_batch(max_batch8, max_wait_ms150) if inflight max_inflight: reject_or_backpressure(batch) else: submit_to_gpu(batch)5.3 我最终采用的配置和效果组合以上分析这次项目最后的生产配置是批次上限8最大等待时间150毫秒最大并发执行请求数2单请求超时3秒队列高水位建议控制在64。这个配置跑了一周线上流量结果平均每秒完成请求数稳定在45左右P99延迟压在300毫秒以内GPU计算利用率从28%提升到85%以上。这里要说明一点这个配置只适用于我这次的模型和流量特征。如果你做的是图像分类模型推理时间几乎固定批次上限可以放开到32或更高如果你做的是超长文档生成单请求推理时间很长批次上限和并发都必须降低。参数没有银弹只有“按你的到达率、按你的延迟上限”去算出来的配置才靠谱。6. 最容易让分析结果失真的三个坑调度分析做到后面真正难的不是优化而是保证分析结果可信。这一章是我这次排查里踩过最深的三个坑每一个都直接导致过错误结论。6.1 用监控面板默认的“GPU利用率”代表一切很多监控面板里的GPU利用率默认统计的是“这个采样周期内有没有计算任务在跑”而不是计算单元真正忙了多少。假如一个内核只启动了1毫秒循环执行100次面板上很可能显示利用率接近100%但其中有大量时间花在启动和切换上实际计算效率很低。我在实验里就吃过这个亏。第一次优化后看到面板利用率90%以为已经压榨到底了。后来用细粒度的内核耗时分析工具一看真正的有效计算时间只占60%多其余都是短小内核反复启动带来的开销。之后我都是同时看“活跃时间占比”和“累计内核时长”两个维度前者看GPU有没有活干后者看活干得有没有效率。6.2 只在理想负载下测试忽略了突发与长尾第二个坑是测试负载设计得太“乖”。我当时第一轮实验用的是均匀等间隔的请求流延迟、吞吐指标都很好看。但把线上真实请求回放进去之后性能立刻露馅因为真实流量往往是“大量请求在几十毫秒内同时到达然后安静几百毫秒”。调度分析必须包含突发场景。我是这样设计的取线上连续5分钟真实请求时间戳作为压测的输入序列同时额外模拟一个“瞬时双倍峰值”的注入场景。只有在这种负载下稳定通过配置才敢上线。均匀负载通过只能证明系统没有大毛病不能证明调度策略配得好。6.3 忽略CPU预处理时间GPU的空闲可能是“假空闲”最后一个坑最隐蔽。我开始定位时一度以为GPU利用率低的瓶颈全在调度器后来把日志细分才发现部分请求在CPU预处理阶段就堵住了。文本很长时分词、特化标记、张量搬运的CPU耗时能达到几十毫秒和GPU单次推理时间相当。GPU窗口没活干不一定是调度器犯懒也可能是活还在CPU那儿没送过来。解决办法是让CPU预处理服务和推理调度解耦请求先进入一个预处理队列完成后再进入推理队列两个队列分别统计延迟。这样如果瓶颈在CPU侧推理队列一直空着你不会误判成调度问题。我这轮分析把这个因素隔离之后调度参数的调整才真正起效。7. 从这次分析里真正值得带走的事文章写到这里不再展开新实验了只分享几个最想让你带走的判断。性能分析的价值不在于找到一个“神奇配置”而在于建立一套可信的测量方法。先拆阶段再分层次再定指标最后才有资格调参数。任何一个步骤跳过去后面的数字都可能是在自我安慰。另外一点调度器和模型的配合程度远超你想象。同一个模型换一种调度策略吞吐可以差二十倍。这不是模型本身的差距而是你让GPU在工作时做了多少无用等待。排查AI推理性能时至少留一半精力给调度千万别一上来就怀疑模型算不动。最后再给一个小建议把所有调度实验都做成可回放的脚本包括请求到达序列、批次参数、延迟采集方式。这样每次线上出现问题你都能用同一把尺子量出新老版本的差距而不是凭感觉猜。这套脚本我至今还在维护已经帮我把排查时间从几天压缩到半天以内。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑