GPU资源调度实战:从显存分片到动态批处理与自动扩缩容 一个老话题但真正把它做好的人真不多。我最早做 AI 模型推理 部署时习惯非常简单粗暴一个模型占一张 GPU 卡互相隔离谁也不碍谁。后来线上服务越来越复杂模型数量一多问题就全出来了——白天流量高的模型卡着不动晚上没人用的模型照样吃满显存有些模型请求不大但显存需求巨大整卡分配之后算力利用率长期在20%出头。这时候才意识到GPU 资源调度 不是一个“多占少吃”的分配问题而是在延迟、吞吐、成本和使用体验之间找平衡的系统工程。这篇文章就是我基于几条真实推理服务线上经验整理出的一套完整思路从需求拆解、选型对比、实操落地到问题排查一次讲透。无论你是刚接触推理部署的工程师还是已经在用容器化GPU但被利用率搞得头疼的运维同学这套方案应该都能帮上忙。我先从负载特征和量化指标讲起这是判断后续一切选型正确性的地基。1. 为什么推理阶段的资源调度比训练阶段更难做1.1 推理负载的三个“古怪脾气”训练任务的资源需求相对稳定跑起来就是单一大任务时间长、弹性差调度只需要按整卡分配。推理则完全不同我先总结成三个特点。第一个是明显的波峰波谷。线上真实请求是用户行为驱动的和太阳升起、饭点、下班高峰期强相关一个典型的业务曲线可能有3到5倍的昼夜峰谷差。白天可能每秒几百请求凌晨几乎为零。这种潮汐式负载如果用静态分配只能在“高峰期勉强够用”和“平时大量闲置”之间二选一。第二个是多模型混合部署。实际生产环境里往往同时存在十几个甚至几十个模型有轻量文本分类、有重型Diffusion生成、有端到端大语言模型。它们对GPU算力和显存的需求完全不同有些是算力瓶颈有些是显存瓶颈不可能用同一套部署规格。第三个是显存消耗不完全跟着请求数走。推理的高峰占用不仅取决于模型权重还取决于并发请求在内存里占用的临时缓冲区、KV Cache等。请求数变多显存占用不是线性增长而是和并发窗口强相关。如果不对服务配置做细粒度控制很容易出现“某个时刻突然OOM”的情况。1.2 把目标拆成三个可量化指标调度方案能不能用得先看指标。我给推理资源调度定下的目标从来只有三个但排序有讲究。第一个是延迟。这是业务的硬上限通常用P95或者P99来衡量。调度方案不能牺牲这个值来换利用率否则线上反馈立马爆发。第二个是吞吐单位时间能处理的请求数。在满足延迟约束的前提下吞吐越高越说明单位GPU能创造的价值越大。这也是一般做资源调度时真正优化的对象。第三个才是资源利用率。很多人一上来就盯着GPU利用率其实这个指标容易被误导。显存利用率、算力利用率、排队耗时、闲置时间都要看单看任何一个都说明不了问题。打个比方资源调度就像开餐厅你不能因为今天中午客人多就把所有桌子都拆了换成站票也不能因为晚上没人就直接关店。目标是在客人排队时间不超过10分钟的前提下尽量让每张桌子都坐满。推理系统里的“客人”就是请求“桌子”就是GPU的显存和算力。1.3 为什么不能靠离线“计划分配”一劳永逸刚接触这块的时候我也想过统计一下线上各个模型的调用量平均值然后按比例分配显存和卡数排一个固定的部署计划这不就完了实践以后发现完全不行。第一请求流量本身有突发性比如一次活动、一次特殊事件都能让某个模型流量瞬间翻数倍第二新模型上线时会调整流量原来模拟好的“最佳分配方案”可能一周后就失效了第三模型升级后显存占用、计算时长会有变化没有动态感知的能力就不可能在合适的时机触发调整。所以真正合理思路一定是调度器不只做一个“初始放置”还要能随着流量变化做副本伸缩、在节点间迁移、自动重分配队列优先级。这已经不是一个离线规划问题而是一个持续决策问题。2. 方案选型调度器、GPU切分、推理框架怎么搭配2.1 从“整卡分配”到“分片共享”的几种路线整个选型过程本质上是在回答一个问题GPU 怎么分给多个推理任务同时用。目前主流的路线大致有四类我直接做成对比表格方便你按自己场景选择。方案粒度优点缺点适用场景整卡独占1张卡隔离性强、部署简单、性能稳定浪费严重小模型也占满整卡大模型、强隔离需求的场景显存分片共享按显存MB划分利用率高、支持细粒度分配算力隔离弱可能互相干扰轻量模型、多模型混部推理引擎动态批处理请求级极大提升吞吐压满算力不适合延迟敏感型请求高并发、可容忍一定排队基于流量的自动扩缩容副本级贴近真实负载削峰填谷冷启动期间有性能毛刺流量波动复杂的生产服务整卡独占是最省心的但绝大多数时候也是浪费最严重的。我们某一个文本模型单卡部署时算力只能用到20%白白占住整张A系列卡。后来切成了分片共享一张卡同时放3个模型高峰期才勉强打满。显存分片共享的做法也有风险。它只是把一张物理卡在驱动层面切成多块并不等于给每一块提供了独立的算力隔离。某个切块里如果突然来了大批量请求很可能会占掉所有SM其他切块的延迟就会跟着飙高。这种问题在低算力需求、高显存需求的场景比如大模型推理里尤其明显。2.2 动态批处理被很多人忽略的隐藏杠杆资源调度如果只停留在“分配卡”那充其量解决了一半问题。另一半在于推理引擎内部的请求合并机制。举个例子。模型一次推理如果只处理一个请求很多算力都浪费在矩阵加载、中间结果缓存上但如果把并发等待的几个请求拼成一个批次一起算吞吐可以提升好几倍。这就是动态批处理最基本的原理。用模拟数据做个对比Batch大小单个请求完成时间总吞吐请求/秒平均每请求延迟140ms2540ms455ms7255ms870ms11470ms16110ms145110ms可以看到batch从1调到8吞吐翻了将近5倍但单请求延迟只增加不到一倍。如果业务允许P95在80ms以内那batch8就是这台机器最舒服的配置。这个决策不是靠拍脑袋而是要靠压测拿到数据再定。不过要注意动态批处理不是越大越好。batch过大会导致尾延迟增加、显存峰值暴涨还可能因为等待填满批次而增加每个请求的“排队时间”。我在实际使用中通常把最大batch控制在一个合理值上再用一个不超过几十毫秒的批处理窗口来平衡吞吐和延迟。2.3 调度器三层联动卡、进程、路由各管一摊一个成熟的GPU调度方案实际上是三层同时协作的结果。第一层是节点级调度决定某个模型实例放在哪台机器、用哪张卡。常用的是容器编排平台加设备插件按显存大小申请资源。第二层是进程内调度由推理框架来控制。它决定一个模型实例同时处理多少请求、要不要合并成batch、批处理窗口有多长。这一层直接决定了单实例的吞吐上限。第三层是流量路由比如负载均衡、路由策略。它能把请求均匀分发到不同副本或者根据业务优先级把请求送去不同的处理队列。这是一个有点类似于物流系统的结构第一层决定货物从哪个仓库发第二层决定仓库里怎么打包才能效率最高第三层决定快递员往哪条线路送。三层互为上下游任何一层出问题整体表现都会打折。做资源调度方案必须把这三层看成一个整体而不是单独去调某一层。3. 实操落地从容量评估到自动扩缩容的完整过程3.1 第一步用实验算出单卡容量而不是拍脑袋每接到一个新模型我都会先跑一轮容量实验而不是直接看模型参数量估算。跑实验的过程其实非常简单核心就是测量“不同并发和batch下的吞吐、延迟、显存占用”。具体做法可以这样准备一批有代表性的压测样本从batch1开始逐渐加大并发记录每个配置下的P95延迟、吞吐和显存峰值。数据收集完以后再画三条曲线——延迟曲线、吞吐曲线、显存曲线找出满足延迟约束的“拐点”。我实际测过一个参数量约7B的模型FP16权重约14GB做量化后降到7GB。如果按之前的习惯整卡部署一张24GB的卡只能放一个实例但用分片调度后这张卡可以放两个实例每个实例限制显存10GB同时开启动态批处理总吞吐提升了近2倍。这就是先测数据带来的直接收益。测试时要用稳定的请求源不要直接用线上流量。我通常会写一个压测脚本循环发送固定比例的真实请求把测试时长拉到5分钟以上因为短时间压测根本反映不出显存累积和缓存策略的真实表现。3.2 第二步容器化部署时怎么写资源申请把模型封装成容器后资源申请写法会直接决定调度器有没有“发挥空间”。我见过不少团队在这里踩坑要么显存请求写得比实际大得多要么忘了设置共享内存导致推理框架一启动就报错。先给一个通用的配置片段apiVersion: apps/v1 kind: Deployment metadata: name: inference-service spec: replicas: 2 template: metadata: labels: app: inference-service spec: containers: - name: inference image: registry.example.com/inference:v2 resources: requests: nvidia.com/gpu: 1 limits: nvidia.com/gpu: 1 env: - name: SHM_SIZE value: 1Gi volumeMounts: - name: dshm mountPath: /dev/shm volumes: - name: dshm emptyDir: medium: Memory这里有两个细节值得注意。第一个GPU资源申请尽量让requests等于limits。这样做是想告诉调度器“这块资源是排他的”避免被其他任务超卖、挤占。如果你用的是显存分片插件那就不能写整卡需要按插件提供的扩展资源来申请比如按显存MB为单位申请。注意不同插件有不同的资源命名方式配置前一定要看对应文档不要套模板。第二个/dev/shm共享内存经常被忽略。PyTorch的多进程数据加载、以及很多推理引擎的令牌分发机制都会用到共享内存。默认只有64MB大并发时直接卡死。我在容器里挂载一个内存盘作为 /dev/shm 之后很多莫名奇妙的性能问题就消失了。3.3 第三步让它自己扩容而不是人工救火流量是会变的所以资源调度方案一定要有自动扩缩容能力。最朴素的思路是看CPU和内存但推理服务的瓶颈往往在GPU吞吐和队列长度CPU和内存根本反映不出来。我建议用自定义指标来做HPA比如“当前排队请求数除以实例数”。只要这个值超过阈值就说明实例忙不过来持续一段时间后就触发扩容。反过来如果实例长时间处于低利用率再触发缩容。下面是HPA配置的一个简化示例apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: inference-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: inference-service minReplicas: 2 maxReplicas: 10 metrics: - type: Object object: metric: name: inference_queue_depth describedObject: apiVersion: v1 kind: Service name: inference-service-metrics target: type: AverageValue averageValue: 8 behavior: scaleDown: stabilizationWindowSeconds: 600扩容策略我建议保守一点缩容冷静期拉长到10分钟以上避免因单次抖动导致的反复伸缩。扩容可以快缩容一定要慢尤其是推理服务一个实例的销毁意味着它上面的队列要重新路由其他实例会短暂压力倍增。我踩过最坑的一次就是缩容策略太激进结果前一天晚上流量回落时把实例全缩了第二天早上高峰请求直接把缓冲队列打爆。另外如果你的环境里没有自定义指标API需要先部署一个指标采集服务比如Prometheus Adapter再把推理框架暴露的内部指标排队长度、每秒请求数透传过去。没有这部分数据自动扩缩容基本上就是盲人摸象。3.4 第四步监控大盘怎么搭该看哪几个数字一套推理服务监控我的核心指标清单固定是这几项GPU使用率SM利用率和显存使用率按实例粒度区分服务吞吐QPS和请求排队深度延迟均值、P95、P99延迟分布图扩容事件和缩容事件记录便于复盘调度决策容器启动时间、模型加载时间、冷启动次数。GPU指标采集通常用DCGM Exporter它能把利用率、显存温度、功耗等数据导成Prometheus格式。应用层面的延迟和排队数据则需要在推理服务里主动埋点输出。两张数据来源缺一不可——只看GPU指标你看不到排队在发生只看应用指标你又不知道是不是硬件已经跑满了。监控搭好之后再回去做容量评估和HPA阈值调整决策质量会有质的飞跃。我现在每一次调整调度策略之前都会先看过去一周的数据而不是凭印象拍板。4. 常见问题与排查技巧实录4.1 现象、原因和处理速查表做调度方案这么久我发现很多问题表面上看起来完全不一样其实底层原因是共通的。这里把我遇到的高频问题整理成一张速查表建议收藏。故障现象可能原因处理思路GPU利用率低但接口P99很高batch太小、请求排队等待增大批处理窗口或最大batch检查路由是否均匀显存OOM但显卡总占用不高显存过期缓存未释放、碎片化限制KV Cache上限增加碎片整理机制容器设置合适limit扩容后延迟反而上升冷启动期间模型加载慢、流量倾斜启用预热接口调整负载均衡策略为最少请求数某一切块延迟飙升其他正常分片算力隔离不足相邻任务抢占SM算力敏感任务单独用整卡或使用带算力隔离的共享方案请求积压但扩缩容没生效指标没接全或HPA阈值不合理检查自定义指标是否上报调整触发阈值和冷却时间缩容后出现大量请求超时缩容太快实例拥挤在同一时间下线增加缩容稳定窗口设置单次缩容比例上限模型更新后显存占用暴涨新版模型权重或缓存策略变化重新执行容量实验按新数据调整资源申请这张表不是标准答案但每条都是从真实故障里总结出来的覆盖了包括调度策略、推理框架、负载均衡、扩容时机在内的多个维度排查时能帮你收敛方向。4.2 三个“慢请求”的实战排查示例举一个我接手过的实际案例。现象是线上有一个模型推理服务GPU利用率看着只有40%但接口延迟P99经常超过1秒。一开始我怀疑是GPU不够但把卡换大之后几乎没有改善。后来用性能分析工具逐层看才发现瓶颈在CPU预处理环节——请求进来要先做分词和历史拼接这段逻辑是单线程的吞吐直接卡死在CPU而不是GPU。这是典型的第一层误区只看GPU指标就把锅甩给调度。推理性能分析一定要把CPU、内存、GPU、网络整体看一遍尤其是文本模型预处理频率太高时CPU同样是瓶颈。另一个案例是显存超过额定值但nvidia-smi看到的占用总是30%。后来发现原因有二一是显存分配器有缓存机制释放的显存不会立刻还给系统二是不同请求的KV Cache分配位置不连续形成了大量碎片。解决办法是把模型部署里的最大缓存上限调低同时定期对服务做滚动重启强制释放碎片。这个问题不做干预的话哪怕理论显存充足也迟早会触发OOM。第三个案例是自动扩容失效。系统在高峰期明明已经排队很深但副本一个都没增加。检查发现监控指标里的“排队请求数”没有被正确导出HPA读取的始终是0。把指标暴露逻辑修好之后扩容响应才恢复正常。这个小问题排查了三小时让我后来对所有“指标看起来没问题”的环节都多留了一个心眼指标本身也可能出错。4.3 独家避坑心得调度配置的三个“不要”第一个不要不要把GPU的requests和limits设置成不同值。很多云环境默认允许配额放大requests5卡、limits10卡看起来能容纳更多实例一旦流量上来超出物理资源的部分只会让调度器手忙脚乱还可能触发整节点驱逐。第二个不要不要在推理实例上混跑在线和离线任务。在线任务要求低延迟离线任务为了吞吐又会无限制造batch两类任务放在一起离线任务会把批处理窗口和显存吃满个位数的在线请求也要跟着排队。合理的做法是用调度队列或者命名空间隔离让不同SLA的任务互不干扰。第三个不要不要忽略模型加载时长。很多推理框架启动时需要把权重从磁盘加载到显存耗时几十秒到几分钟不等。如果没有做“预热”调度器认为实例已经Ready就把流量打过来了结果前几十秒全部超时。建议每次扩容后主动调用一次“预热请求”等模型真正可用后再放流量进来。5. 进阶玩法队列优先级、模型预热与成本优化5.1 把“排队”从敌人变成工具既然推理请求天然有波动那与其硬扛流量还不如把等待变成可管理的队列。我现在的方案是给请求引入优先级比如线上用户请求优先于后台批量任务紧急任务有独立通道。队列里放不下时调度器可以先拒绝低优先级请求保高优先级不降级。做了这个改造之后一个意外的收益是GPU利用率更稳定了。因为后台任务可以把排队的“空隙”填满又不影响线上体验显存和算力都被榨得更干净。这个思路其实和操作系统的进程调度很像资源不足时调度策略比资源总量更重要。5.2 模型预热与权重缓存让冷启动不再是瓶颈自动扩缩容最大的敌人就是冷启动。我试过把模型权重挂到本地高速缓存再把容器启动阶段编排成“下载权重、加载模型、预热、接流量”四步利用生命周期钩子确保流量在模型可用后才开始进入。这样扩容确实快了很多不过对存储和带宽有额外要求。另一个思路是跨副本共享模型权重和缓存。同一台节点上的多个推理实例如果权重相同可以考虑用共享内存直接映射同一份参数文件减少重复加载时间也省节点内存。这个方法不是所有框架都支持需要先确认框架是否具备共享显存参数的能力别一上来就改造优先级不高。5.3 从成本看调度夜间缩容和碎片合并调度方案最终都要算一笔经济账。我见过太多团队花了很多精力做GPU共享但从来没看过账单夜里空闲的卡完全没释放月底成本凭空多出一截。所以现在做资源调度的思路会加入时间维度。夜间低峰期我会直接缩掉一批实例把流量合并到少数几个节点上同时把空出来的机器关机或者释放第二天早上高峰前再通过定时任务提前扩容。这种“预期式调度”和“反应式调度”配合能比纯HPA再节省一部分成本。另外一个省钱细节是显存碎片整理。长期运行的推理服务如果反复加载不同大小模型显存碎片会越来越多最后导致明明总量够用却放不下一个大模型。定期对节点做一次“有损重排”——把运行中的实例重新调度到更紧凑的位置腾出整卡资源——是我目前认为最有效的碎片整理方式。这个操作比较重千万不能放在流量高峰期做而且要设定迁移超时机制。有人问我什么时候开始做资源调度最合适我的回答是从你手上拥有的GPU开始变紧张、开始需要为成本报表负责的那一刻开始。做调度方案没有必须照抄的标准答案你需要先清楚地回答自己三个问题当前延迟瓶颈在哪、流量波动有多大、瓶颈究竟在算力还是显存。想清楚这三件事剩下的选型和参数本质上都是水到渠成的事。