首页 / 资讯中心 / 文章详情

AI推理GPU调度优化实战:从连续批处理到KV Cache管理

AI推理GPU调度优化实战:从连续批处理到KV Cache管理 ★ FEATURED ARTICLE
模型能跑起来和跑得好是两回事这句话做推理服务的朋友应该都深有体会。模型部署上线后GPU利用率盯着看并不低但请求一多响应就开始排队首token延迟直线往上飙调大并发显存又扛不住动不动就OOM好不容易打完一轮压测吞吐忽高忽低。这些问题八成不是模型本身的问题而是AI模型推理阶段的GPU调度机制没有打磨到位。调度在推理系统里是个“隐形杠杆”它不改变算力总量却决定了算力被用得好不好。这篇文章把我做GPU推理调度优化的经验整体沉淀一遍从机制原理、核心维度、框架选型到实测调参和问题排查尽量讲透。1. 推理调度到底在调度什么1.1 为什么推理调度的难度比训练只高不低很多人一开始会把推理调度理解成“简化版训练调度”这是错的。训练过程是稳态负载数据先切好batch再喂进去每个batch大小固定、计算量基本均匀GPU从头到尾都在满负荷运转调度器只需要保证数据按顺序送进去几乎不需要做复杂决策。推理完全不同。线上服务收到的请求到达时间没有规律用户可能在深夜突然发来一个3000字符的长提示词也可能三秒内涌进来五十个短请求。每个请求的长度、期望输出长度都不一样最麻烦的是长尾请求——一个慢慢吞吞输出的对话请求会一直占着算力把后面几十个快速请求全部堵住。训练可以离线把数据重排顺序、重切尺寸推理必须在毫秒级响应在线流量没有机会“洗数据”。调度频率的差异更关键。训练里调度器的决策周期是以迭代为单位的一次决策可以管好几分钟推理的调度周期直接落到每一次前向计算每一次forward之前都要重新决策这一轮让哪些请求参与计算、哪些请求继续等待、哪些请求的资源要回收。决策频率高一个数量级决策失误的代价也陡增——训练里某个batch调度慢一点无所谓推理里一次错误调度就会让一批请求集体超时。1.2 调度驱动的是什么请求、Token与显存块如果把推理引擎的速度层拆开看调度器其实同时管理着三个层面的事。第一层是请求级调度。新请求进来之后调度器要决定立刻接受、排队等待还是按某种优先级直接插队。这个决策决定的是请求的“入场顺序”直接影响用户的等待体感。第二层是Token级调度。模型解码是一个自回归过程每个请求在当前这一步生成一个Token下一步要不要让这个请求继续生成、要不要把刚到达的新请求的prefill阶段塞进当前batch全都要在每步forward之前决定。这一步是连续批处理的核心也是推理吞吐能拉开倍数差距的关键。第三层是显存块调度。推理过程中每个请求的KV Cache都住在显存里调度器要负责给新请求分配Cache块、在请求结束时回收、在显存吃紧时决定哪些请求的Cache先挪出去。这一层最容易被忽略可一旦这层出事表现就是OOM和全盘崩溃。这三层彼此耦合请求级调度的结果影响Token级调度Token级调度直接决定显存块的需求量显存块调度反过来约束前两层能放进多少请求。做优化最难的点就在这儿——调一个层级很快就能看到效果但往往只是把压力转嫁给了另一个层级。2. 推理GPU调度的核心维度2.1 先想清楚你要优化的是吞吐还是延迟GPU调度优化的大前提是明确目标但很多团队在这里就没想清楚。吞吐和延迟不是同一根刻度尺想要同时拿到极致的吞吐和极低的延迟在有限显存里几乎不可能。先把几个指标定义对不然后面所有调参都是盲调。首Token延迟TTFT衡量的是从请求发出到收到第一个输出Token的时间它是用户感知最敏感的指标之一。单Token生成间隔TPOT衡量的是相邻输出Token的平均间隔直接反映生成速度。这两个指标和吞吐是互斥的——把批次塞得越满总吞吐越高但单个请求的排队时间变长TTFT和TPOT都会恶化。我做了这么久调度优化最大的心得是先给业务分清楚优先级。对话助手类业务会把“延迟满意度”放在第一位调度器的目标函数是“在保证90%请求TTFT不超1秒的前提下尽量提升吞吐”离线批量生成类任务则反过来允许单个请求慢一点只求总吞吐最大化。目标定好后面所有参数调整才有依据。实际操作中我会用“延迟约束下的有效吞吐”来评估不计入那些已经超时的请求只统计达标请求的总token生成量。很多看似吞吐很高的配置把超时请求剔除后实际收益很小。2.2 连续批处理把GPU喂饱的核心机制早期的推理服务用的是静态批处理把一个时间段内到达的请求攒成一个batchbatch里所有请求必须全部生成完毕才能整体结束、一起释放。这种模式有一个致命问题batch里只要有一个请求的序列特别长其余短请求完成之后也得干等着GPU空闲了一大半这就是经典的“木桶效应”。后来行业转向了连续批处理思路完全不同不再等整个batch完成而是在每一次迭代前重新组batch。某个请求生成完了立刻把它移出去剩余空间马上从队列里拉新请求补上甚至正在prefill的长请求和正在decode的短请求可以在同一个迭代里混合计算。调度器每一次forward都是一个动态拼装的过程GPU的每个计算周期都在处理有效Token而不是等慢请求。这个机制的收益非常直观我见过同样的模型、同样的GPU只是把静态批处理切到连续批处理吞吐直接翻倍。但它也带来了新的调度复杂度不同请求的KV Cache必须在显存里同时共存互相之间不能地址冲突这就要靠底层的分页显存管理来支撑。没有连续批处理上层调度策略再花哨都是白搭。2.3 KV Cache管理显存调度的灵魂KV Cache是调度真正绕不开的资源瓶颈。每一个在跑请求的中间状态都缓存在显存的KV Cache里请求越多Cache占用的显存越大。长上下文的对话请求里KV Cache的显存占用甚至能超过模型本身的权重。调度器管理KV Cache的方式直接决定了能同时跑多少请求。当前主流的做法是分页式管理把KV Cache切成固定大小的显存块按需分配给请求而不是给请求预先分配一整块连续显存。这个思路和操作系统里的虚拟内存分页如出一辙。请求变长时动态追加显存块请求结束时立刻回收释放这样一来显存利用率大幅提升长请求和短请求可以更灵活地共存。分页式管理对调度器提出了额外要求它必须时刻维护每个请求的显存块映射知道哪块Cache对应哪个请求的哪个Token。调度器做抢占时要么把这些块整体转移到CPU内存要么直接标记丢弃、之后重新计算。我见过有的团队调优时只关注Batch大小显存配额不管结果连续批处理每前进几步就要触发一次Cache回收和重分配性能反而大幅下滑。调度优化的第一步永远是先给KV Cache划出合理的地盘。2.4 抢占与优先级资源不够时的取舍艺术显存再大也有穷尽的时候。当KV Cache分配不出新块、又有高优先级请求必须现在处理调度器必须决定牺牲谁、保留谁这就是抢占机制。抢占有两种整理方式一种是交换把低优先级请求的KV Cache挪到CPU内存保存腾出显存给高优先级请求等显存有空再搬回来。另一种是重算直接把低优先级请求的Cache丢掉之后需要时从头重新prefill。交换保留中间状态、恢复快但受限于CPU和内存带宽重算省CPU传输却要重新算一遍前置Token恢复成本也不低。调度器一般会根据剩余显存大小和CPU繁忙程度动态选择。优先级调度本身也是个精细活。维护一个纯粹的高优先级队列很简单但低优先级请求可能被活活饿死。我给系统设计了老化优先级——请求的等待时间越久它的动态优先级逐步提高这样即使低优先级请求也保证在可接受的时间内有进展。抢占和优先级如果设计得当高峰期系统整体满意度反而比“一碗水端平”更高。3. 主流推理引擎与调度参数选型3.1 不同推理引擎的调度机制对比同一个调度思路放到不同推理引擎里落地细节差异很大。我做选型时会把几个主流的路线都摸一遍因为它们背后的调度哲学不完全一样。对比维度开源生态常用框架A硬件厂商优化栈B前沿调度强化框架C批处理模式连续批处理Token级动态调度飞行中批处理CUDA Graph深度优化连续批处理带Cache复用能力KV Cache管理分页式管理抢占机制完善静态缓冲与分页混合分页式管理优化多轮复用率适合场景通用在线服务调试成本低单卡高吞吐、极致性能压榨多轮对话、超长上下文调度策略可配置性高参数丰富中偏向黑盒高定制能力强框架A最明显的优点是调度器完全开源可见参数都能自己控制排查问题方便适合我们这种喜欢手动调参的团队。框架B的优势是深度绑定了硬件底层优化同样的模型推理吞吐上限更高但调度逻辑封装得更黑盒出了问题能调的手段有限。框架C在多轮对话场景表现突出它把长文本中间的重复前缀复用起来省掉了大量重复prefill代价是显存管理逻辑更复杂部署门槛更高。选型我的建议很简单如果团队主要做通用服务、需要快速上线和灵活调优优先考虑框架A如果模型固定、场景单一愿意吃厂商优化红利可以上框架B如果业务特点是多轮长对话、上下文复用比例很高框架C会很香。别盲目追最炫的要匹配业务流量模型。3.2 几个关键调度参数的真实含义参数名在不同框架里略有差异但核心逻辑相通。我把真正影响调度行为的几个拎出来讲这些是不调不行、调了见效最快的关键项。max_num_seqs表示调度器同一时刻最多活跃的请求数相当于“同时服务的并发席位”。调小了GPU明明有能力处理更多请求却饿着调大了显存爆掉是早晚的事。max_num_batched_tokens表示每一次前向迭代里最多可处理的Token总数它限制的是单轮计算的上限。这个参数直接决定批处理的最大尺寸设太大单轮计算时间变长TPOT也跟着变长短请求被拖累设太小又发挥不出连续批处理的优势。gpu_memory_utilization是给KV Cache预留显存的配额预留越多能同时驻留的请求越多但留给权重和计算的空间就越少。一般我会从0.85到0.95之间试不断监控KV Cache实际占用率再微调。enable_chunked_prefill控制长Prompt的prefill是否切片处理。不开时一个超长prompt的prefill会独占一整个迭代短请求全部得等开启后长prompt的prefill被切成小块与短请求的decode混跑能大幅拉低TTFT的尖峰。3.3 调度策略的适用场景选择调度策略不是越复杂越好核心是匹配请求到达模式。FCFS先来先服务适合请求量平稳、差异不大的场景实现简单问题也少。短作业优先LSFS变体适合单个请求计算量差异很大的场景让短请求优先跑完长请求稍微多等一会整体吞吐和平均延迟都有收益。基于优先级的加权调度则适合有明确业务等级的在线服务——比如付费用户请求优先、后台批量任务走低优先级。我踩过一个坑一开始为了榨吞吐一律采用短作业优先结果一个长对话请求足足等了平时十倍的时间用户体验直接崩了。后来改成“优先级分层层内短作业优先”给了长请求基础保障才算稳住局面。调度策略必须有兜底任何让某类请求长期得不到服务的策略早晚会反馈成线上事故。4. 一次完整的调度参数优化压测实录4.1 压测环境与请求特征定义拿一次实战调优来演示。当时的环境单张80G显存的GPU部署7B规模对话模型权重加中间表示约占18G显存剩余约62G可用于KV Cache。压测请求特征模拟线上对话场景prompt长度在500到2000 Token之间分布期望输出在200到800 Token之间同时并发数在50到200之间波动整体流量形状是“平峰带尖峰”。为了真实还原线上情况压测脚本用了多路并发模拟请求间隔按泊松分布采样避免齐步走造成虚假峰值。我自己写了个小工具把TTFT、TPOT、吞吐、超时次数逐条记录下来。这一步很关键数据采集口径不一致的话后面调完根本分不清是优化生效了还是压测波动。基线是这样跑的用引擎的默认参数直接压测不碰任何配置。4.2 瓶颈发现与分步调整过程先跑第一轮基线问题立刻暴露。吞吐约1500 Token/sP95 TTFT达到1.8秒明显超标。GPU利用率看着有80%左右但KV Cache利用率只有37%说明大量显存还空着没用上调度器根本没有把它们转化为并发能力。进一步看日志发现prefill和decode互相阻塞严重——长prompt的prefill一旦进场整个迭代的其他请求全部卡住。第一步先调显存配额把gpu_memory_utilization从默认的0.85提到0.92让KV Cache可用的显存从大约50G增到55G。这步操作没有任何风险只是把空置的显存解放出来但配额加大的同时并发请求上限也自然增高吞吐小幅提升到1900。第二步调调度窗口。把max_num_seqs从64提到128max_num_batched_tokens从2048提到4096。这一步效果最明显连续批处理终于有足够的“库存”做token级重组。本轮吞吐到了2900但P95 TTFT还在1.1秒左右说明prefill和decode的互相干扰问题仍然存在。第三步开启chunked_prefill把长prompt切成256 Token的切片插在decode迭代里混跑。这个动作对TTFT有立竿见影的效果P95 TTFT从1.1秒掉到0.6秒而且因为GPU不再被大块prefill占死整体吞吐继续升到3200。最后把max_num_seqs再微调成96效果没有明显回升说明调度窗口已经接近硬件上限。4.3 优化后的整体收益这一轮调整做完最直观的对比数据如下指标基线配置优化后变化P95 TTFT1.8秒0.6秒降低67%吞吐量1500 Token/s3200 Token/s提升113%P95 TPOT波动较大峰值超100ms稳定在40~55ms抖动显著下降KV Cache利用率37%82%提升45个百分点压测超时率2.3%0.2%降低约10倍有个容易被忽略但非常重要的变化是TPOT稳定性优化后TPOT不再大幅抖动。原因很简单chunked prefill把长任务切细之后每个迭代的Token组合都相对均匀GPU的算力不被某一个庞大请求独占短请求的生成节奏自然稳定下来。这次调优也让我意识到一个问题参数之间不是独立起作用的必须先给显存配额留足空间再调调度窗口最后再处理prefill与decode的混合策略顺序搞反很容易陷入“调高一个参数导致另一个瓶颈”的死循环。5. 常见问题与排查技巧实录5.1 显存看着还有为什么会OOM这是出现频率最高也最容易误判的问题。显存的“还剩”和“可分配”是两回事当gpu_memory_utilization设为0.92时引擎已经把92%的显存统一切给KV Cache托管这部分显存在调度器的视角里是被占用的即使当前没有一个请求在跑新请求来了也只能在剩下的8%里找空间。不了解这层机制的团队看到系统监控显示还有20G显存却频繁OOM怎么都想不通。排查步骤推荐这样走先看KV Cache的实际占用率再看当前活跃请求数和等待队列长度最后确认是不是调度器正在做大量preemption。如果KV Cache占用在95%以上且大量请求在等待那就是配额不够了。处理方式一般是上调gpu_memory_utilization或者调低max_num_seqs限制活跃请求数。5.2 GPU利用率高响应却慢得离谱GPU利用率高不一定代表干的是“有效工作”也可能是大批请求卡在prefill阶段空耗算力。我遇到过利用率冲到90%以上但P95 TPOT超过300毫秒的情况第一反应是硬件算力不够最后定位出来是prefill在反复抢占。这种问题重点看两个指标一是平均batch的有效Token数如果每个迭代都在处理大量重复计算的前置Token说明Cache复用没生效二是调度器日志里的swap和recompute计数如果这两个计数在持续增长说明系统在做大量无效的Cache迁移工作。解决办法通常是两个方向——限制并发窗口减小抢占压力或者打开chunked prefill让任务粒度变细。我在压测实录里用的就是后者效果已经很明确。5.3 并发升高后TTFT突然恶化正常情况并发升高TTFT会缓慢增长如果某个点之后突然恶化大概率是触发了资源悬崖。原因一般是请求到达速率超过调度器的处理能力队列开始无限积压每条新请求都必须在前面的长队里等完一轮才有机会进场。这个场景里有效手段是引入优先级插队把短请求、重试请求直接推到队首长请求放到队尾允许长请求多等一会。再加上超时熔断保护排队超过阈值的请求直接返回提示而不是继续堆积。请注意一点优先级策略需要搭配老化机制否则低优先级请求会被饿到死——具体做法我在前文提到过等待时间会提升动态优先级保证每类请求最终都有出路。5.4 调度器日志与监控要点调优和排障都离不开监控数据。除了常规的GPU利用率和显存使用我把真正和调度相关的指标整理成一张速查表排查时照着看即可。指标名称观察要点异常含义KV Cache利用率长期低于50%调度窗口太小并发能力没释放排队请求平均时长持续上升请求到达率超过处理上限Preemption计数快速累积显存紧缺频繁抢占Swap流量大小明显偏高Cache交换太频繁CPU传输成瓶颈P95 TPOT抖动大Batch组合不均匀或prefill干扰超时请求占比超过业务容忍线调度策略需要调整优先级日志层面我习惯开启调度器的细粒度Trace重点观察每个迭代里prefill和decode的Token比例。这个比例失衡往往是大多数问题的根源。另外监控要拉到请求级能够定位到“某个长请求占用了大量Cache块导致一批短请求被卡死”时才能判断是流量问题还是调度策略问题。做过几轮完整的调度优化之后我个人的体感是GPU调度机制优化没有“一次到位”的说法业务流量模型在变、模型在迭代每换一个模型或每改一次请求分布之前调好的参数都可能要重调。最有效的做法是梳理好监控指标、沉淀一套参数基线把每次调整都量化记录成对比数据让后续优化有据可依。到最后你会发现调度优化的本质不是把某个参数调到极致而是不断寻找在吞吐、时延和资源三个约束下的动态平衡点。
阅读完成 · 觉得有帮助?
咨询建站