
1. 推理容器为什么需要一套专门的GPU调度机制先讲一个我最近实际遇到的场景。有个团队把训练好的大模型部署成线上推理服务容器编排用的是KubernetesGPU资源用的是NVIDIA官方的device plugin看起来一切正常。结果上线没几天就出了怪问题请求高峰时部分副本响应变慢P99延迟从80毫秒一路飙到1.2秒但一看GPU监控显存没满利用率也不算高两台GPU服务器其中一台已经塞满了实例另一台却空着一大半调度器说什么都不肯把新Pod调度到空闲节点上。拆开排查问题出在“整卡调度”这个默认逻辑上。默认情况下一个容器只要声明了nvidia.com/gpu: 1就会独占一整张GPU卡不管这个模型实际只需要4GB显存还是16GB显存。这就导致两种极端要么一张卡只跑一个轻量模型资源大量闲置要么一张卡被塞进多个容器互相抢算力谁也别想好好跑。说白了默认的GPU调度机制是给“一个任务占一张卡”的粗放场景设计的但AI模型推理的实际情况根本不是这样。这篇文章想讲清楚的就是GPU调度机制在推理容器场景下的完整链路。包括GPU资源是怎么被隔离和切分的、Kubernetes之类的容器编排平台是怎么把GPU分配给容器的、推理场景特有的调度策略比如显存配额、算力切分、排队机制该怎么设计以及线上最容易踩的那些坑。适合正在做模型推理服务部署、容器化改造或者被GPU利用率折腾得睡不好的同学参考。先说一个核心认知训练和推理的GPU调度需求完全不一样。训练任务是长稳型负载一个任务占一张卡跑几个小时甚至几天调度频率低资源粒度粗追求的是单卡算力最大化。而推理任务是延迟敏感型负载并发波动大模型常驻显存单次请求消耗的算力很小如果每个推理服务都独占整卡GPU利用率能做到10%都算不错了。所以推理场景的GPU调度重点不是“把任务塞到哪张卡上”而是“一张卡上的算力和显存怎么安全地分给多个服务用”。这个思路转过来后面的方案就顺了。2. GPU资源在容器里的隔离与切分方式2.1 从硬件到容器GPU隔离的四个层级要理解GPU调度先得知道GPU资源在容器里到底是怎么被“圈出来”的。从底层往上数大致有四个隔离层级。第一层是硬件层的SR-IOV和GPU虚拟化方案。这一层直接把一张物理GPU虚拟成多个虚拟GPU实例每个实例有独立的显存和计算单元隔离性最强但成本和复杂度也最高且对硬件型号有要求一般用在云厂商的GPU实例产品里普通团队很少直接碰。第二层是驱动和运行时层的MIGMulti-Instance GPU和MPSMulti-Process Service。MIG是NVIDIA A100、H100这些高端卡上才有的硬件级切分能力能把一张卡物理切成多个独立的GPU实例每个实例有专属的显存、SM和带宽隔离性接近硬件级。MPS则是软件层面的方案允许多个进程共享同一张GPU并对算力使用做一定程度的切分和控制。第三层是用户态层面的CUDA_VISIBLE_DEVICES置空和显存占位。最常见的做法是给容器设置环境变量让容器只能“看见”指定的GPU编号。这个方法只能隔离可见性不能隔离算力一个容器如果想抢算力完全可以占满整张卡的计算资源。第四层是容器运行时层面的NVIDIA Container Toolkit。它负责把宿主机的GPU驱动和CUDA库注入到容器里让容器内的进程能正常调用GPU。2.2 显存隔离和算力隔离要分开看动手做调度方案之前务必要先想清楚一个问题你到底需要隔离的是“显存”还是“算力”。如果只是显存隔离那你只要保证每个容器拿到的显存配额加起来不超过物理卡总显存即可。实现手段很多给容器设置CUDA_VISIBLE_DEVICES或用device plugin限制显存额度都能做到。但显存隔离有个天然的坑显存够用并不代表算力够用。两个模型各占半张卡的显存但它们的推理请求同时达到高峰时计算单元会被抢烂延迟飙升一个服务的波动会直接污染隔壁服务。如果要做算力隔离就需要MPS或者MIG这一类支持计算资源切分的方案。MPS的思路是在GPU驱动层加一个代理进程把多个进程的CUDA计算请求合并转发给GPU并允许你配置每个进程能使用的最大计算资源比例。MIG更粗暴直接在硬件层面把一部分SM和显存切给某个实例物理隔离互不干扰。我自己的体会是团队规模小、GPU型号杂、又没有专门做基础设施的人优先做好显存隔离和基于副本数的负载分散就够了。等到出现明显的“邻居干扰”问题再上MPS也不迟。一上来就追求完美的算力隔离往往把自己绕进去了。2.3 CUDA上下文和推理引擎的资源特性还有一个很多人忽略的细节GPU上的资源不只有显存和算力还有CUDA上下文、显存带宽、PCIe带宽、拷贝引擎这些东西。每个进程第一次调用CUDA时runtime会创建一个CUDA context这个context在显存里要占几十MB到几百MB的空间。如果调度器把太多小容器塞到一张卡上光是CUDA context和推理引擎的CUDA缓存就能吃掉几个GB显存还没算模型权重。推理引擎比如TensorRT、vLLM、Triton加载模型时也会预留一部分显存作为KV cache或工作缓冲区这部分显存往往是动态伸缩的。以vLLM为例它默认会用掉GPU可用显存的90%左右来准备KV cache。这就意味着你给容器设置了显存上限引擎就会在这个上限范围内尽可能多地预留显存如果调度器又把多个这种容器塞进同一张卡显存很容易爆。所以做调度策略时一定要给每个推理容器预留足够的“呼吸空间”不能按模型权重大小精确到小数点地分显存否则上线就OOM。3. 基于Kubernetes的GPU调度实践方案3.1 全链路组件从驱动到device plugin容器编排平台上的GPU调度本质上是一套链路协作的结果。以Kubernetes为例全链路大致长这样宿主机上的NVIDIA驱动、NVIDIA Container Toolkit负责让容器能用GPU、NVIDIA device plugin负责把GPU资源上报给kubelet、调度器根据资源请求把Pod绑定到节点、容器运行时启动容器时通过CDI或环境变量注入GPU设备。其中最容易出问题的是版本匹配。NVIDIA Container Toolkit和驱动版本不匹配时容器内会报could not select device driver with capabilities: [[gpu]]一类的错误。device plugin版本和Kubernetes版本不匹配时GPU资源可能根本不会被上报。这些坑后面会展开讲。3.2 整卡调度和共享调度的配置对比最基础的配置是用NVIDIA官方device plugin配合nvidia.com/gpu这个扩展资源做整卡调度。Deployment里这样声明resources: limits: nvidia.com/gpu: 1这个配置的意思是这个Pod需要一张完整的GPU卡。调度器会找一个有空闲GPU的节点把Pod放上去并给容器分配一整张卡的访问权。配置很简单缺点也很明显一张24GB的卡只用了4GB剩下的20GB全部闲置而且其他Pod永远别想用这20GB。要做共享调度官方device plugin提供了shared模式可以按显存和算力比例进行切分。部署时给device plugin加一个参数比如用--device-list-strategyenv和--pass-device-specstrue然后启用shared配置通过下面这种方式声明resources: limits: nvidia.com/gpu: 1 nvidia.com/gpu.memory: 4096 nvidia.com/gpu.cuda-cores: 20nvidia.com/gpu.memory表示给这个容器分配4GB显存nvidia.com/gpu.cuda-cores表示占一张卡20%的计算资源。这样一张24GB的卡理论上可以塞下5个4GB显存配20%算力的容器。这套方案的优点是部署简单不需要额外安装组件底层依赖NVIDIA Container Toolkit的MPS能力来实现算力限制。缺点是官方长期把shared标注为实验特性功能迭代慢而且一旦节点上GPU型号混杂比如一台机器上有A10也有V100不同型号的卡显存和算力模型不一样配置shared后调度器很容易算出不合理的分配结果。3.3 资源计算一个8B模型的实例该申请多少显存聊完机制来做一个具体的计算。假设要部署一个Llama-3-8B模型的量化推理服务模型权重是FP16精度总共约160亿参数每个参数占2字节所以模型权重本身需要16GB左右显存。如果用INT8量化模型权重降到8GB左右但推理速度可能会有损失。除了模型权重还要算KV cache。KV cache的大小取决于并发数和上下文长度粗略估算公式是2 * num_layers * num_kv_heads * head_dim * seq_len * batch_size * 每个元素字节数。对8B模型来说如果配置最大上下文长度4096、batch size开到32KV cache可能要占到2GB到4GB。再加上CUDA context、推理引擎的工作缓冲区、碎片化预留单副本至少需要按14GB到18GB来申请。按实测经验我通常会在模型权重加KV cache的基础上再乘1.2到1.3的系数作为安全余量。也就是说一个INT8量化、batch 32、上下文4096的8B模型推理服务我倾向于在容器里申请20GB显存。如果把一张24GB卡的剩余空间全部申请走而没有预留上线后稍微加长上下文或者加大并发很容易OOM。算力方面如果单实例实测能支撑120 QPS业务峰值预计300 QPS那至少需要3个副本。但节点的GPU闲置算力单卡可能只有70%实际可以支撑的QPS要重新压测不能靠理论值硬算。调度器不会帮你判断算力够不够它只知道你怎么声明、它怎么分配。3.4 节点分配和拓扑亲和性的处理还有一个调度细节模型推理服务跨节点通信的场景要特别注意节点和GPU的拓扑关系。一张物理机上如果有8张GPU卡它们之间通过PCIe Switch连接卡和卡之间的通信带宽和延迟有明显差异。如果推理服务是多卡并行比如一个模型切成4张卡部署尽量让调度器把Pod调度到同一节点或同一个PCIe Switch组下否则跨节点通信延迟会拖垮推理性能。在Kubernetes里可以用节点亲和性nodeAffinity和Pod间亲和性podAffinity来实现这个约束。比如给GPU节点打上gpu-typea10和gpu-switchswitch-1的标签然后声明Pod必须被调度到同一个节点的不同GPU上。简单粗暴的做法是声明podAffinity让同一个推理服务的所有副本尽量跑在同一节点上。缺点是容灾性变差节点一挂全部副本一起挂。线上如果对容灾有要求就要在“性能最优”和“可用性优先”之间做个动态平衡。我的选择是单卡能装下模型的绝不跨卡必须跨卡时才启用拓扑亲和约束。4. 推理场景的专属调度策略4.1 模型预热与常驻推理进程推理容器和普通Web容器的一个显著区别模型加载很慢。一个8B模型从磁盘加载到显存冷启动可能要30秒到1分钟如果还要做权重反序列化和图优化几分钟都有可能。调度器通常只负责把Pod调度到节点上并在容器启动后开始拉取镜像、启动进程、做健康检查。如果健康检查用的是/health接口而这个接口在模型加载完才能返回200那调度器就会一直等直到Pod达到CrashLoopBackOff状态。实操经验是推理容器的启动探针startupProbe要把失败阈值设大一点间隔设短一点比如initialDelaySeconds: 10、periodSeconds: 10、failureThreshold: 30给模型加载留足时间。同时liveness探针不要直接用模型推理接口否则请求一多、延迟一高探针超时会误杀容器引发连锁崩溃。这两个坑我踩过不下三次。更高级一点的做法是做模型预热。在容器启动命令里先执行一个预热请求把权重加载进显存、构建好CUDA计算图再启动对外服务。这样第一个真实请求的延迟就不会因为“现加载模型”而爆炸。具体代码逻辑不复杂启动脚本里先调用模型跑一次空输入然后才启动gRPC或HTTP服务。4.2 自适应并发与排队机制推理服务对外暴露的是HTTP或gRPC接口但内部的GPU计算是串行或半并行的。当一个请求进入推理引擎时它会占用GPU算力。GPU对并发请求的处理能力是有限的超过这个限制请求会在引擎内部排队延迟指数级上升。很多团队做调度只关注“怎么把容器放到GPU上”忽略了“容器内部的请求并发怎么控制”。这两个层面是串联的。我见过一个典型案例某个服务配置了50个并发worker每个worker都能向GPU提交计算任务结果GPU算力被50个任务争抢P99延迟直接翻了10倍。后来把并发数调成4P99延迟反而下降了80%。推理服务的并发不是越大越好每个模型和GPU组合都存在一个最优并发区间超过这个区间吞吐和延迟双双恶化。这个最优值只能通过压测来找没有统一公式。如果并发控制已经做到位了但请求量还在增长这时调度器要做的是“排队”而不是“硬扛”。可以在网关层或推理服务入口处加一层请求队列队列满时直接把请求拒绝让客户端走重试或降级逻辑。这比让请求在GPU引擎里挤成一团要优雅得多也更容易监控。4.3 GPU共享、时间片切分与优先级混部当一张卡被多个推理服务共享时谁优先用算力谁可以等待需要有明确的策略。MPS模式下可以通过环境变量CUDA_MPS_ACTIVE_THREAD_PERCENTAGE控制一个进程最多占用多少比例的GPU算力。比如两个推理服务共享一张卡A设成60%B设成40%两个进程都会受到硬性限制谁也别想把对方饿死。如果某个服务是低优先级的短期任务比如数据分析、模型评估可以通过Kubernetes的PriorityClass把它标记为低优先级在节点资源吃紧时调度器会优先驱逐这些Pod保证核心推理服务的稳定性。混部场景更复杂一些。生产环境经常出现“GPU推理服务要求低延迟训练任务要求高吞吐”并存的情况。我的经验是如果混部的节点上推理服务的延迟敏感度很高就不要把训练任务和推理服务放在同一张卡上。物理隔离永远比逻辑隔离可靠。如果实在需要混部把训练任务限制在MPS的较低算力配额内并设置CPU、内存的上限防止训练任务的数据加载和预处理把节点CPU打满连累推理服务。4.4 调度策略上要注意的背压和故障转移最后补充一点和调度相关但很容易被忽略的故障转移。GPU节点的故障处理比普通CPU节点要麻烦得多。GPU卡可能因为温度过高、驱动崩溃、显存ECC错误等原因进入异常状态而这种异常不一定能在节点层面被检测到。处理思路是三层第一层是节点级监控采集GPU的温度、功耗、Xid错误日志第二层是Kubernetes层面的自愈通过自定义探针检测GPU是否还正常响应CUDA调用如果异常就把Pod驱逐到其他节点第三层是推理服务副本数下限保障核心服务至少保留两个副本分布在不同的物理节点上避免单点故障。有了这个基础调度策略的配置才有意义。否则调度机制再精巧节点一崩全白搭。5. 监控、观测与问题排查实录5.1 推理容器GPU监控的核心指标有哪些做GPU调度优化没有数据支撑就是在盲调。监控指标选对了很多问题一眼就能定位。最基础的指标是GPU利用率和显存使用量。但注意nvidia-smi里显示的GPU利用率指的是SM流式多处理器的活动占比它表意很粗。一个模型在推理时如果SM利用率是30%不代表GPU只用了三成的能力可能是并行度不够也可能是访存密集型任务在等显存带宽。所以还需要额外关注显存带宽利用率、GPU时钟频率、温度、功耗、PCIe读写带宽这些指标。推理侧的业务指标同样关键QPS、平均延迟、P99延迟、请求排队长度、batch size分布等。卡上的监控和业务侧的监控要放在一个面板上对比看。比如某个时刻延迟飙升同时SM利用率降到了个位数那多半是CPU侧预处理或者网络IO出瓶颈了GPU在干等数据。如果SM利用率拉满延迟飙升那是算力不够了考虑扩容或降低并发。如果SM利用率不高、显存快满了那要看是不是KV cache配置得太大、显存预算分配得不合理。5.2 常见问题定位思路与解决手段把我在实际运维中遇到的GPU调度相关典型问题整理成一个速查表方便排查时对照。现象可能的根因排查手段解决方向Pod一直Pending调度不上去节点没有足够显存额度nodeSelector约束不匹配GPU资源被占满但调度器无法区分碎片查看kubectl describe pod的事件kubectl get nodes --show-labels检查标签开启共享调度合理设置资源请求检查亲和性规则容器内nvidia-smi看不到GPU容器运行时未注入设备NVIDIA Container Toolkit未安装或版本不匹配CDI配置缺失查看容器内ls /dev/nvidia*检查容器运行时日志重新安装/升级Container Toolkit配置好CDI或环境变量注入GPU利用率打满QPS却很低并发设置太高导致任务争抢单请求batch size为1CPU预处理瓶颈对比SM利用率和请求延迟检查batch配置用nvidia-smi dmon看CPU和GPU的空闲占比调低并发数开启动态batching优化数据预处理流水线显存没满但新容器申请失败调度器按整卡分配碎片显存不可用device plugin资源统计没有实时更新查看节点上nvidia.com/gpu资源分配情况升级device plugin切换shared模式手动排空节点回收资源多个容器共享一张卡延迟抖动算力隔离没有做MPS未启用某个容器计算突发把所有SM占满检查是否启用了MPS用nvidia-smi观察多个进程的算力占比启用MPS为共享容器配置算力上限高优先级服务独占物理卡推理服务频繁OOM容器显存配额太小KV cache动态扩展导致超限显存碎片化查看容器OOMKilled事件在容器内监控显存用量变化曲线调大显存配额限制KV cache上限为容器预留更多余量服务重启后首次请求延迟特别高模型冷加载权重没有预热PID调度不连续观察启动日志时间线检查第一次请求耗时增加模型预热步骤使用常驻推理进程把探活请求做成预热数据5.3 几个实际的排查过程和心得举一个最近的真实案例。某个团队上报线上P99延迟突刺每隔几分钟就出现一次。我让他们同时采集GPU利用率和容器CPU使用率发现一个规律每次延迟突刺前容器CPU使用率都会先冲高同时GPU利用率掉到接近0。顺着这个线索查下去发现罪魁祸首是tokenizer和图像前处理逻辑跑在CPU上随着请求并发上升CPU先顶不住了请求全在CPU侧排队GPU只能干等。这种问题如果不是把两边指标放在一起看很容易被误判成“GPU资源不够”然后盲目扩容白花钱还解决不了问题。排查GPU调度问题一定不能只看GPU侧的指标CPU、内存、网络、磁盘IO都要纳入联合分析。另一个常见问题是MPS的配置方式。MPS需要先在宿主机上启动MPS控制守护进程然后在容器里设置环境变量才能生效。很多人只设置了容器的环境变量宿主机上的MPS服务压根没起导致配置了算力上限但完全不生效。MPS的管控平面和数据平面是分开的启动顺序不对结果就完全不对。还有一个小技巧GPU容器里的时间同步和日志时区要提前处理。推理服务的监控很多时候要靠日志时间线来对比分析如果容器时区是UTC宿主机是北京时间两个时间线错开8小时排查问题时会有很多多余的脑力消耗。建议容器统一用UTC日志统一打UTC时间面板里再做时区转换避免混乱。6. GPU虚拟化方案选型与工具链对比6.1 开源方案和商业方案的取舍聊到这块是因为很多团队在搭建推理容器GPU调度体系时都会面临一个选择题完全用开源自建还是采购商业GPU虚拟化方案。开源方案的核心是NVIDIA官方那一套device plugin Container Toolkit 共享模式 MPS。优点是没有额外成本、社区活跃、资料多缺点是官方对共享和算力切分的支持不算完善MPS本身有一些限制比如不同进程的CUDA context共享同一块显存空间容错性一般遇到复杂调度需求时得自己写扩展。对大多数中小团队来说这套已经够用了。商业方案比如各类GPU虚拟化和池化产品在调度灵活性、显存超卖、细粒度QoS保障上做得多比如支持显存超卖、按时间片切分算力、故障迁移等功能。优点是省心缺点是贵而且引入了额外的调度层定位问题时要多绕一道弯。如果业务规模没到几千张卡的体量我不建议一上来就上重度商业方案。先用好开源方案把监控和资源配额规则梳理清楚往往能解决八成的GPU浪费问题。6.2 工具链清单与部署要点结合我实际用过的组合整理一份适合中小团队的GPU容器工具链参考环节推荐工具备注驱动管理NVIDIA官方驱动生产环境不要用操作系统仓库自带的驱动版本太旧容易和新版CUDA不兼容容器GPU支持NVIDIA Container Toolkit统一所有节点的版本不要各装各的资源上报NVIDIA device plugin关注release notes升级前先在测试节点验证共享调度device plugin shared模式 / 自研调度器扩展简单场景用shared就够监控采集DCGM Exporter PrometheusDCGM的指标比nvidia-smi更丰富能采到功耗、温度、PCIe吞吐可视化Grafana把业务指标和GPU指标放同一面板日志采集Loki或ELK容器日志统一时间戳部署时的几个要点一是所有GPU节点尽量保持相同的驱动和Container Toolkit版本否则不同节点上的容器行为不一致排查问题时要多做好几倍的工作。二是DCGM Exporter要尽早部署不要等出了问题再补它采集的很多指标比如Xid错误、ECC错误是nvidia-smi给不了的。三是GPU节点要做CPU和内存的预留不要让所有的CPU核都被业务容器占满否则GPU驱动层的管理进程可能因为CPU饥饿而工作异常。7. 推理GPU调度的发展方向聊完实战再说说我对未来一段时间GPU调度方向的一些观察和判断。一个明显的趋势是从静态调度走向动态调度。现在的调度器大多基于“请求时声明资源、分配后固定不变”的逻辑但推理服务的负载在一天之内波动非常大白天高峰、夜里低谷的时候静态配额的资源浪费很明显。业界已经在尝试基于历史负载预测做副本数的弹性伸缩甚至做GPU资源的热迁移。对普通团队来说短期内可以先从“按时间段调整副本数”这个半自动方式做起把主流程跑稳再去追求全自动的动态调度。另一个趋势是GPU资源抽象层的标准化。Kubernetes社区在推进设备资源的标准模型AMD、Intel、NVIDIA都在往这个方向靠。未来调度GPU可能会像调度CPU和内存一样自然不需要写那么多多样化的定制插件。但短期内各家GPU的驱动和资源模型差异还是很大异构GPU混布时的调度依然是难点。还有一个我比较关注的方向是训练和推理的混部调度。大模型训练的波峰和推理服务的波峰在时间轴上不完全重叠理论上可以通过精细的调度把两者混布在同一批GPU上提升整体资源利用率。但这个话题涉及的因素太多算力隔离、显存隔离、优先级抢占、故障隔离都要处理得非常好否则混部带来的复杂度会超过它省下的成本。8. 写在最后的实操建议个人实际做下来最想给同行们提的几个建议算是踩坑总结。第一不要再抱着“一个容器一张卡”的粗放思维了。推理服务的GPU资源粒度应该是“MB级显存 百分比级算力”哪怕第一版只做显存维度的切分也比整卡分配浪费要强得多。第二调度策略上线前一定要有压测数据支撑。不要拍脑袋决定“一张卡上放3个副本”还是“放5个副本”每个模型、每个推理引擎、每个请求模式的资源消耗曲线都不一样。至少做一次固定负载的压测记录GPU利用率、显存占用、延迟指标用数据来定配额和副本数。第三监控一定要先行。没有完整的指标采集体系后面做任何调度优化都是盲人摸象。最少要把GPU利用率、显存用量、功耗温度、请求延迟、并发数这五类指标配上不要等到线上出问题再补。第四也是我自己最深的体会GPU调度的本质是“资源管理”而资源管理没有银弹。所有方案都有它适用的场景和条件关键是对自己业务的特征有足够清晰的认知。是延迟敏感还是吞吐优先是稳定流量还是突发流量是单模型还是多模型混布这些问题想清楚了调度策略自然就清晰了。