1. 为什么推理场景的GPU调度是单独的难题
1.1 推理和训练的资源需求差异
先对齐一个基本认知:训练场景的GPU调度和推理场景的GPU调度,根本不是一回事。
训练任务的特点是“长跑型”。一次训练要跑两三天,显存几乎打满,算力长时间处于高位占用状态,调度器给他一块完整卡,一周不用管。对训练做调度,本质上是分配卡和等待卡,慢一点没关系,只要别抢。
推理任务恰好相反,它是“短跑型”加“冲刺型”。一个请求进来,模型过一遍前向,几十毫秒到几百毫秒结束,但同一个服务可能同时面对几百上千路并发。这里真正困难的是,请求在时间轴上是一阵一阵的,有波峰也有低谷,而GPU资源不是按毫秒粒度的可抢占资源,你不可能像CPU线程一样说抢占就抢占。
在训练里你关心的是算力利用得够不够;在推理里你关心的是响应延迟是否达标、单位时间能扛多少请求、以及扩容缩容是否跟得上流量。这三个指标互相牵扯,任何一个调优都可能导致另外两个恶化。
另外一个很容易被忽视的点是显存绑定。训练任务占的显存是学习过程本身的物理需要,推理任务占的显存主要是模型权重加中间激活和KV Cache缓存。显存不等于算力,却能限制算力发挥,一张卡能塞多少个推理副本、能撑起多少并发,全部由显存先框定范围。所以推理调度比训练调度多做的一件关键事,就是精细地盘算显存这块最紧张的资源,看看能不能再挤一点利用率出来。
1.2 在线推理调度的三个核心矛盾
真实做推理服务的团队,基本每天都被这三个矛盾支配着。
第一个矛盾是延迟与吞吐的拉锯。对单个请求来说,把批处理窗口放大,摊薄单卡上的计算开销是最优的,但批处理窗口的放大意味着请求在线等的时间增加,P99延迟直接破线。反过来,为了极致延迟把每个请求单条处理,GPU的利用率和吞吐量又会明显下滑。这个矛盾没有“正确答案”,只有针对业务时效性的折中区间。
第二个矛盾是隔离与共享的对立。多模型或多服务共享同一块GPU才能摊薄资源成本,但要共享就得有细粒度的显存隔离和算力配额,隔离做得不好,某个模型显存暴涨,隔壁模型的延迟立刻跟着遭殃。很多团队第一版方案就是卡一顿乱分,直到某天一个模型在线上把整卡显存占满拖垮了其他所有服务,才知道隔离设计是调度方案的“地基”而不是“配菜”。
第三个矛盾是扩容的速度追不上流量波峰。GPU实例从申请到拉起通常需要几十秒甚至几分钟,加上模型加载和初始化,冷启动时间又加了一截。流量若是蜂拥式上涨,调度器就算感知到了,新实例也未必来得及接住峰值。这时候需要在“提前多囤算力”和“准备抢占式冗余”之间做布局,本质上是拿钱换延迟,花钱买命。
这三个矛盾决定了推理场景的GPU调度,不能只解决“把请求发给谁”的问题,它需要从设备分配、显存管理、请求排队、自动伸缩等多个层级统一设计,才是一个能够稳定跑住线上流量的完整方案。
2. 调度方案的整体设计与核心思路
2.1 目标函数先定清楚
任何方案都比不过目标定义。推理调度的目标函数一般可以拆成三层:服务质量(SLO)、资源利用率、成本约束。服务质量是底线,通常由一个或多个延迟指标表征,比如P99小于200ms、错误率低于0.1%;资源利用率是优化目标,越大越好;成本约束是硬性天花板,决定你敢不敢在波峰期放开打。
我自己在做方案评审时,通常要求团队先回答一个问题:目标SLO的哪一侧是绝对不可突破的?有些业务P99延迟就是命根子,那批处理窗口、排队策略、扩容阈值都要围绕延迟来设计;有些业务只要正确率达标,请求慢两倍也能忍,那就可以放开手脚追吞吐。目标不同,调度策略的选择会走向针锋相对的两套方案。
定完目标,再聊经验数据。每个模型、每种业务流量,都应该有历史曲线,用于推导并发分布、请求大小的分位数、波峰出现的周期性。调度方案相当于在已知流量统计规律的条件下做一个实时可控的资源分配,没有数据先凑合做,也先把监控指标埋好,否则后续所有调优都是在猜。
2.2 两个方向:单卡优化与跨卡编排
GPU调度优化大致分两路。
一路是单卡内优化,重点在把一张卡的显存和算力尽量榨干。常规做法包括模型副本常驻、连续批处理、上下文缓存复用、显存池化、并发控制。这一层的优化是性价比最高的,因为不增加硬件成本,纯粹靠调度软件把卡的颗粒度细化。你可以类比成出租车拼车,同样是跑一趟,一辆车多拉几个顺路乘客,单个乘客的成本就降下来了。
另一路是跨卡编排,重点在集群维度怎么把模型副本放置到合适的卡上,处理扩容缩容、故障转移、多卡并发切分。跨卡编排基于容器化平台或GPU资源调度器来做,把GPU设备抽象成可调度资源,按显存、算力分片给不同服务使用。这里涉及的就不只是“分”,还有“排队”“抢占”“重调度”,复杂度比单卡高一个量级。
两路优化不是替代关系,而是叠加关系。先保证单卡内的利用率不掉链子,再在集群层面做均衡调度,两层都做好了,整体的资源效率才会是真的高。现实中我看到不少团队跑反了,集群层面对设备分配用得很精细,但单卡内每个副本只跑单请求,GPU整体利用率只有十几个百分点,这种场景优先优化单卡内部往往效果更明显。
2.3 决定成败的几个关键取舍
方案落地前通常有几个取舍需要拍板,而且这些取舍会影响后续所有开发路径。
显存分片方式的取舍。固定分配还是动态共享?固定分配实现简单,每个副本的显存配额固定,隔离性也好,但可能有较大闲置空间;动态共享利用率高,但需要处理好显存超卖与回收。在线推理场景我倾向于“以动态为主、以固定为辅”,核心模型用固定配额保住SLO,试验性和周期性模型用动态池共享。
批处理策略的取舍。等到多少请求开始推理?最朴素的实现是设置阈值和最大等待时间,请求积攒到阈值或超时即触发批推理,两个条件哪个先到就执行哪个。再往上走,是生成式模型最常用的连续批处理,允许批内的请求按照各自生成进度动态腾退,新请求可以插队进入空闲位置。这种策略对大语言模型这类解码时长较长的模型效果极为显著。
抢占还是不抢占。队列真的很不显眼,但对调度质量影响巨大。请求排队太多会拉长尾部延迟,排队太少又会放过尖峰。一般要配置最大队列长度、超时淘汰策略、优先级队列,还要有提前量反馈:当队列深度超过预期,直接触发扩容而不是等请求全面积压后再被动加卡。
这些取舍没有放之四海而皆准的答案,只能回到2.1的目标函数去推导。SLO严格,就宁可多留冗余;成本敏感,就接受偶发的排队。
3. 核心实操:从集群管理到请求分发的完整链路
3.1 GPU设备池化管理与可见性
先把物理资源抽象开。在容器编排平台里,最普遍的做法是使用设备插件机制,把GPU作为可计数资源暴露给调度器,容器申请时携带GPU数量或显存大小,调度器据此为工作负载寻找节点。这个层面通常还会再加一层显存调度插件,按显存字节数而非卡数量分配,因为推理场景很多需求是“半个卡”“三分之一卡”的粒度。
设备插件标准化解决了资源可见性的问题,但实际光有可见性还不够。节点上的GPU物理拓扑要感知,否则模型跨卡通信时走了慢速链路,通信延迟就可能拖到不能接受的地步。较好的实践是给节点打上GPU拓扑标签,调度时尽量把多卡任务限制在同机架或者同交换域内。
还有一类容易被忽略的环节是GPU占用的维度。有的服务声明占用整卡GPU,但实际只用到10%的算力和40%显存,这种“名义占用”会造成大量隐性浪费。要解决这个,就得让调度器认定的资源粒度更细,比如按显存大小加按算力百分比双重申请,并且让监控系统在运行时回写实际占用,形成资源画像,逐步校准申请值。画像校准做好后,超卖才有底。
3.2 实例级别调度:显存隔离与并发控制
到了实例层,调度要回答一个直接的问题:这块卡上能放几个模型副本?怎么放?
推理框架加载模型副本时,显存中有三个大头:模型权重、上下文缓存、框架运行开销。目标是在保证隔离的前提下让多个副本合理共用一张卡的显存和算力。实例调度器可以先为每个副本计算预估显存占用,再根据卡的剩余空间决定放置数量。空闲了很多显存的卡并不一定要塞满副本,得看算力余量,否则两个副本同时拉高算力,两个任务的延迟都会受影响。
显存隔离有两种常用手段。一个是框架级显存上限设置,例如用计算图级配置限制进程最大显存占用,这种方案灵活但隔离能力一般;另一个是底层切分方案,隔离力更强,可以把一块物理卡切成多个独立分区,各自拥有独立的显存与算力平面。但切分粒度有限且配置切换成本高,在线场景下灵活性受限;框架级形态灵活,但隔离强度又弱一些。实践里很多团队混合使用:核心大模型用强隔离保底,长尾模型放共享池。
并发控制则是实例层调度的最后一公里。GPU计算大多基于流式执行,同一个进程内并发多个推理请求,需要限制并发进入的请求数,避免算力占用过高导致尾延迟飙升。一般做法是为每个模型副本设置最大并发数,多余的请求在排队层等待。并发数的最优值通常通过压测获得,跟显存占用不一定成比例。早期我见过有人把并发数设置成显存容量除以单请求显存的直接除法,结果并发请求一多,算力排队严重,利用率看着不错,延迟却一路爆表。
3.3 请求级调度:批处理、抢占与优先级
实例调度排布好了,剩下的就是请求怎么进、怎么排队、怎么离开,这一层直接决定SLO走势。
批处理层我强烈推荐连续批处理而不是固定批处理。固定批处理是凑一批、算一批、清一批,最直观但浪费很多空隙;连续批处理是只要批里有一个请求完成解码,就把这个位置腾出来给队列里的新请求,整个批的动态生命周期被拉长,吞吐能提升一到两个数量级。实现时要处理好批动作与解码进度的对齐,好在主流推理引擎都自带这个能力,我们要做的是把外部请求与引擎内部的连续批处理队列接好。
优先级队列需要区分业务级与模型级。业务级上,付费请求或者实时交互请求应该进入高优先级队列,普通异步任务可以放低优先级,甚至允许被抢占。模型级上,不同模型对延迟敏感度不同,不能一刀切。调度器维护多个队列,各自有独立的并发预算,队列间的资源竞争按权重分配。注意权重分配不是静态数字,要在流量过载时根据实时表现做动态调节,比如某个模型P99已经超线,就降低它的分配权重,把资源让出去。
请求级的抢占与淘汰也很关键。虽然GPU没法像CPU一样在真正意义上抢占正在执行的计算,但可以在排队阶段提前处理:低优先级请求等待超过阈值就降级或直接返回错误,高优先级请求可以插入队列头部。实现时可让低优先级请求在队列里“让步”,避免让高优先级请求长时间等待。
3.4 多模型混合部署与排队策略
多模型混合部署现在越来越普遍。每个模型有自己的SLO要求与流量曲线,混在同一批GPU资源池里能摊薄成本,但调度复杂度也上来了。
多模型情境下,第一个要做的是资源水位隔离。每个模型副本都设置显存和算力水位,超水位则触发告警和限流,防止一个模型拖垮整卡。混合部署时,算力冲突的模型最好不要同卡:一个高算力需求、一个低算力常驻,同卡时高算力模型运行时低算力模型延迟会大幅跳动。尽量把同质性请求模型放同卡,把互补型请求模型放同卡但错峰排。
队列策略在多模型时会变成多级队列。一级队列按服务入口划分,二级队列按模型划分,三级队列按优先级划分,调度引擎逐级转发。排队时要注意排队时长记录是分段的,端到端延迟与模型排队延迟要分开监控,否则排错问题时根本看不清是哪一级拖了后腿。我曾遇到一个P99迟迟压不下的事件,第一反应找推理引擎,调了几轮都没用,最后查链路才发现是某级队列超时配置得太长,白白积压了高优请求。
4. 真正让方案落地的几项工程配套
4.1 监控心跳与资源画像
调度方案上线只是开始,持续运行的可靠性依赖监控。GPU监控要覆盖三个层面:节点层看卡的温度、功耗、利用率、显存占用;实例层看单副本的显存曲线、算力曲线、并发数;请求层看每级队列深度、排队延迟、批处理窗口大小、完成请求延迟分布。这三层缺一不可,任何一个指标缺失,线上问题定位就得靠猜。
资源画像是提高调度精度的核心。长期记录每个模型副本的请求量、显存均值与峰值、算力占用趋势,用这些数据定期校准调度器的资源预估值。画像是动态的,不能假设一个模型从上线到退场的资源需求恒定不变。业务迭代可能让模型体量变大,请求分布可能让峰值偏移,调度器应该每轮迭代吸收监控数据做重算。
心跳机制不只是“活着”的意思,要能上报当前队列深度与卡内饱和度。调度器拿到这些反馈后可以做负载均衡,避免某张卡堵成一团而旁边卡还闲着。我曾经做过一个简单的均衡策略:每15秒收集一次各模型的负载水位,水位差异超过阈值就把新请求优先调度到低位卡,效果立竿见影,整体P99延迟下降了三成左右。
4.2 弹性伸缩策略怎么定
推理服务的扩缩容是调度方案的“刹车和油门”。扩容触发条件必须基于排队深度或请求延迟,而不是简单的资源利用率,因为在推理服务里CPU利用率常常是失真的:模型计算走GPU,CPU主要处理数据搬移与序列化,CPU飙升往往说明到了瓶颈,但CPU很低不代表GPU不吃力。
扩容速度需要多级递进。常规流量上涨,按既定步长加副本;突发流量,直接一次性拉出一批副本;还要准备兜底方案,例如提前预热好若干个“影子副本”,平时只加载模型但不接流量,突发时秒切流量。影子副本的成本能控制在可接受范围时,这是应对冷启动最好的办法。
缩容则要克制。GPU实例缩容的代价不止是删除进程,还要考虑请求断开、状态丢失、以及新流量涌入时无卡可用的风险。建议缩容时先摘流量、延迟观察一段时间再释放实例,不要看到一个低谷就立马砍卡。我经历过一次草率缩容,夜间低谷期把卡从十张降到两张,结果早上八点半一波突发流量直接把两张卡打满,后续扩容排队等了六分钟,整整损失了一个小时的可用率。
4.3 成本核算与配额管理
调度方案最终都要挂到成本上。GPU成本通常占在线推理业务的硬件支出大头,做技术方案不能只管好用不管花钱。
成本核算的粒度要细。每个模型每小时的GPU算力成本可以按卡价格与占用系数折算,使用率差异导致的浪费要单独统计。团队要有比较口径:同样流量规模下,上一版本方案和本版本方案的单位请求成本下降多少。没有这个值,后续优化很难向决策层证明价值。
配额管理要防止“资源囤积”。有的模型负责人习惯申请大额度资源来保SLO,导致大量算力闲置。团队配额建议采用预算制加申报制:每个业务线有总预算,超预算必须先审批,调度器对配额内资源有权做弹性共享,闲置额度会被其他服务临时借用,但借用的算力不得超过高优先级服务的SLO容忍区间。这种模式让资源在业务线之间流动起来,成本反而更容易控制。
5. 常见问题与排查实录
5.1 显存泄漏与利用率虚高
推理服务跑久了显存占用持续上涨,最后把卡占满——这是排查频率最高的问题。根因通常是缓存没有及时清理:请求级缓存、上下文缓存、批处理引擎的显存池,都可能随着请求不断累积。
遇到显存持续上升,正确排查路径是:先区分是单调上涨还是阶梯式上涨。单调上涨一般指向泄漏,大概率是某处缓存引用忘记释放;阶梯式上涨通常是池化引擎动态扩容显存池,增长到阈值就暂停。后者只要把池上限设置合理就不会频繁打满。排查时建议先开显存事件监控,记录申请和释放的时间线与调用位置,再用两个异常请求做对照实验,很快能定位到泄漏点。
利用率虚高是另一个迷惑人的坑。GPU利用率显示90%以上,但实际有效计算吞吐很低,原因多半是显存带宽瓶颈或过度频繁的内核启动。这时候只看利用率指标就会被误导,要结合实际带宽、有效算力以及请求平均延迟综合判断,否则你费劲调了半天并发数,结果瓶颈在数据搬运上,白折腾。
5.2 冷启动延迟与预热
模型加载到推理实例一般要经历参数读取、权重反序列化、计算图优化、首次预热请求,整个流程可能从几十秒到数分钟。线上冷启动会导致两种典型问题:扩容后流量已过峰,实例白扩了;流量已经到了,新实例还没就绪。
解法上,首选数据与应用分离,把模型参数放到共享存储或分布式缓存,实例启动时只挂载映射,不拷贝权重,减少启动时间。其次做模型预热,主进程启动后先跑几轮基础请求,确保初始化完成后再对外接受流量,避免首批请求被慢速初始化的冷环境拖到超时。
更彻底的做法是建“常驻模型服务”:核心模型固定驻留几个实例常开,只做横向动态伸缩,不做实例重建。模型更新走双副本流,新版本实例先拉起并在灰度流量下热身,稳定后再把老实例平滑下线。这套做法需要多投入一点闲置算力,但稳定性收益远高于节省的那点成本。
5.3 调度器抖动与多卡并行瓶颈
调度器抖动通常表现为流量正常但各节点负载分布剧烈变化,忽高忽低。根因往往是调度周期设置过短或评分策略太灵敏,导致请求频繁在不同节点间迁移。将调度周期适当拉长,并引入粘性调度,让同一模型副本优先接受相同节点的请求,可以显著减少抖动。
多卡并行瓶颈则是大模型推理服务发展后的常见问题。模型太大一张卡放不下,并行计算把模型拆到多卡,每多一张卡就多一分通信开销。经常看到的现象是“两卡延迟比四卡好,四卡延迟反而不如两卡”,根源多半是通信拓扑不成比例,跨节点或者跨交换域通信把收益吃掉了。排查时先用性能分析工具看通信占比,若通信超过总耗时一定比例就要考虑换通信加速链路或者减少并行度。
另一个多卡细节是负载分配不均。多卡并行时请求路由要保证各卡上的算子负载接近,否则某张卡的算力占用几乎满,其他卡空转,模型吞吐照样上不去。建议在推理框架层开启负载均衡路由,并定期检查各卡算力利用率分布曲线,偏差较大时就要人工介入分析了。
最后再分享一点个人经验
我做GPU推理调度这几年,最大的感受是不要指望一套方案从上线走到成熟,每个阶段都有不同的重心。第一个阶段先把设备和实例层的隔离做好,保证集群别乱、别崩;第二个阶段再优化请求级批处理和队列,把单卡吞吐做上去;第三个阶段才谈跨集群成本和弹性优化。跳过前两个阶段直接谈成本优化,很可能在某个流量尖峰直接翻车。
还有一个至今受用的习惯:每次线上变更,无论多小,先记录资源基线数据再动手。很多次性能回退问题找不到原因,最后回看基线数据才意识到是某次“顺手”调整拖的。调度系统的复杂度在于牵一发动全身,保留好基线,就是在给自己留一条后路。