AI云原生实战03-K8s跑AI模型GPU总不够用?从Pod到Node的GPU调度全攻略

发布时间:2026/7/20 11:09:36

AI云原生实战03-K8s跑AI模型GPU总不够用?从Pod到Node的GPU调度全攻略 1、AI程序员系列文章2、AI面试系列文章3、AI编程系列文章目录一、那个GPU它到底去哪了二、一张图看懂GPU调度全景三、第一步把Pod绑到GPU节点 — Node Selector Affinity3.1 最简单的方式Node Selector3.2 更智能的方式Node Affinity四、第二步保护你的GPU — Taints Tolerations4.1 给GPU节点打上专属污点4.2 让GPU Pod容忍污点4.3 进阶多级污点策略五、GPU资源管理nvidia.com/gpu 的前世今生5.1 资源声明5.2 GPU数量的坑六、NVIDIA Device PluginGPU翻译官6.1 安装Device Plugin最简方式6.2 Device Plugin配置详解七、GPU共享方案从一张卡一个人到全民共享7.1 方案一NVIDIA MIG多实例GPU—— 物理隔离7.2 方案二Time Slicing时间分片—— 最易上手7.3 方案三NVIDIA MPS多进程服务—— 推理场景首选八、多GPU拓扑分布不要让PCIe成为瓶颈8.1 GPU拓扑感知调度8.2 Volcano GPU调度进阶九、生产环境最佳实践一张清单打天下9.1 节点准备9.2 Pod配置模板开箱即用9.3 排障速查一、那个GPU它到底去哪了凌晨两点我盯着kubectl describe pod的输出看到了一个让我血压飙升的结果Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 4m28s default-scheduler 0/3 nodes are available: 3 Insufficient nvidia.com/gpu三台带GPU的节点一张卡都分不到。Pod就是这么硬生生Pending了4分钟。不是没卡是调度器不觉得他能用。这是一个典型的K8s GPU调度翻车现场。更惨的是隔壁老王直接裸机跑模型比我早两小时下班而我的Pod还在那儿挂着。如果你也正在经历——或者是预防性地想避免——Pod调度到GPU节点失败、GPU利用率低、多个人抢一张卡这些问题那这篇文章就是为你准备的。核心问题就一个K8s原生调度器对GPU的感知和AI工程师对GPU的需求中间隔着一道认知鸿沟。调度器只会数数几块卡但AI工作负载关心的是什么型号、什么拓扑、能不能共享、显存够不够。填平这个鸿沟就是本文要做的事。二、一张图看懂GPU调度全景先别急着写YAML我们得把整个调度链路搞清楚。下面这张图覆盖了从Pod创建到GPU分配的全流程graph TB subgraph 用户层 A[AI工程师br/提交Pod/Deployment] end subgraph K8s调度器 B[调度器接收Pod] C{节点过滤br/Filtering} D{节点打分br/Scoring} E[绑定Pod到Node] end subgraph 节点层面 F[Node Selectorbr/节点选择器] G[Node Affinitybr/节点亲和性] H[Taints Tolerationsbr/污点与容忍] end subgraph GPU资源层 I[NVIDIA Device Pluginbr/GPU发现与上报] J[nvidia.com/gpubr/资源计数] K[kubeletbr/设备分配] end subgraph GPU共享方案 L[NVIDIA MIGbr/物理分片] M[Time Slicingbr/时间分片] N[MPSbr/多进程服务] end A -- B B -- C C -- F C -- G C -- H C -- I C -- J C -- D D -- E I -- K K -- J L -- K M -- K N -- K style A fill:#ff6b6b,color:#fff style E fill:#51cf66,color:#fff style I fill:#339af0,color:#fff style J fill:#339af0,color:#fff style L fill:#fcc419,color:#333 style M fill:#fcc419,color:#333 style N fill:#fcc419,color:#333从这张图可以看出K8s的GPU调度是一个“漏斗”模型用户提交Pod声明需要多少GPU调度器挨个节点做过滤有没有足够GPU、有没有污点、亲和性匹不匹配过滤通过的节点再打分谁的资源更充裕、拓扑更优化最终把Pod绑定到分数最高的节点⚠️关键认知大部分GPU调度问题卡在第2步——过滤阶段。节点有卡但Pod就是调度不上去八成是你的Node Selector写错了或者Taint没配Toleration。三、第一步把Pod绑到GPU节点 — Node Selector Affinity有些人觉得我有GPU节点K8s不就该自动把GPU Pod调度过去吗错。K8s调度器不认识GPU Pod这个概念。除非你告诉它否则它可能把GPU Pod调度到只有CPU的节点上然后Pod自己发现没有CUDA设备原地失败。3.1 最简单的方式Node SelectorapiVersion: v1 kind: Pod metadata: name: gpu-training-pod spec: nodeSelector: accelerator: nvidia-tesla-v100 # 标签匹配 containers: - name: training-container image: nvidia/cuda:12.1.0-runtime-ubuntu22.04 resources: limits: nvidia.com/gpu: 1 command: [nvidia-smi]前提是你的GPU节点打上了对应标签kubectl label node gpu-node-01 acceleratornvidia-tesla-v100 kubectl label node gpu-node-02 acceleratornvidia-tesla-v100 kubectl label node gpu-node-03 acceleratornvidia-a100Node Selector 简单粗暴但有个致命缺陷只能做等于匹配。你说我要V100但三台V100节点都在跑任务只剩A100有闲卡——对不起调不上去。3.2 更智能的方式Node AffinityNode Affinity 支持更丰富的匹配规则apiVersion: apps/v1 kind: Deployment metadata: name: inference-service spec: replicas: 3 selector: matchLabels: app: inference template: metadata: labels: app: inference spec: affinity: nodeAffinity: # 硬性要求必须有GPU且型号至少是V100级别 requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: nvidia.com/gpu.present operator: In values: [true] - key: nvidia.com/gpu.product operator: In values: - NVIDIA-Tesla-V100 - NVIDIA-A100 - NVIDIA-A100-SXM4-40GB - NVIDIA-H100 # 软性偏好优先选A100/H100 preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: nvidia.com/gpu.product operator: In values: - NVIDIA-A100 - NVIDIA-H100 containers: - name: inference image: mymodel/inference:v2 resources: limits: nvidia.com/gpu: 1这里的两个关键字段字段行为类比requiredDuringSchedulingIgnoredDuringExecution硬约束不满足就Pending“必须有V100或A100别的我不要”preferredDuringSchedulingIgnoredDuringExecution软偏好尽量满足“有A100最好没有的话V100也行”实战建议GPU标签最好用NVIDIA Device Plugin自动打的标签如nvidia.com/gpu.product而非自己手动打。手动标签容易过期三周后你自己都不知道哪个节点是什么卡。四、第二步保护你的GPU — Taints TolerationsNode Selector 解决了GPU Pod去哪的问题但没解决另一个问题CPU Pod别上GPU节点试想你花几万块买的A100节点部署了一个Prometheus、一个Nginx、一个日志采集器。这些纯CPU Pod随手就调度上去了占端口、吃内存GPU Pod反而Pending。这就像买了辆法拉利天天开去买菜。4.1 给GPU节点打上专属污点# 给GPU节点打污点只有带对应toleration的Pod才能调度上来 kubectl taint nodes gpu-node-01 nvidia.com/gputrue:NoSchedule kubectl taint nodes gpu-node-02 nvidia.com/gputrue:NoSchedule kubectl taint nodes gpu-node-03 nvidia.com/gputrue:NoSchedule三种Taint效果Taint Effect含义使用场景NoSchedule不带容忍的Pod绝不调度上来GPU节点标配必须PreferNoSchedule尽量不调度但不绝对有普通卡混部的场景NoExecute不调度已经在跑的也驱逐暴力清理慎用4.2 让GPU Pod容忍污点apiVersion: v1 kind: Pod metadata: name: gpu-job spec: tolerations: - key: nvidia.com/gpu operator: Equal value: true effect: NoSchedule nodeSelector: accelerator: nvidia containers: - name: training image: pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime resources: limits: nvidia.com/gpu: 2⚠️一个常见翻车点Toleration 和 Node Selector都要配。Toleration 只是说我能上GPU节点不代表我只上GPU节点。不加Node Selector的话Pod还是可能被调度到CPU节点。两者是AND关系不是OR关系。4.3 进阶多级污点策略如果你的GPU节点还有等级之分可以用多级污点# 专属GPU池推理服务专用 kubectl taint nodes gpu-node-01 gpu-poolinference:NoSchedule # 通用GPU池训练/调试 kubectl taint nodes gpu-node-02 gpu-pooltraining:NoSchedule kubectl taint nodes gpu-node-03 gpu-pooltraining:NoSchedule对应的Pod配置# 推理服务 - 只上推理池 tolerations: - key: gpu-pool operator: Equal value: inference effect: NoSchedule nodeSelector: gpu-pool: inference # 训练任务 - 只上训练池 tolerations: - key: gpu-pool operator: Equal value: training effect: NoSchedule nodeSelector: gpu-pool: training这套组合拳下来你的GPU节点就刀枪不入了——只有你明确允许的Pod才能调度上去。五、GPU资源管理nvidia.com/gpu 的前世今生5.1 资源声明K8s中GPU资源的声明方式非常直观resources: limits: nvidia.com/gpu: 2 # 声明需要2张GPU但这里有个很多人忽略的细节GPU资源的 requests 和 limits 必须相等。你写resources: requests: nvidia.com/gpu: 1 # ❌ 这行是无效的 limits: nvidia.com/gpu: 2K8s处理GPU资源的方式和CPU/内存不同。对于扩展资源Extended Resourcesrequests会被K8s自动设置为等于limits即使你显式写了requests也没用GPU属于不可压缩资源不支持超卖每个Pod分配的GPU是独占的5.2 GPU数量的坑一张A100 80GB跑一个7B的模型只需要20GB显存。按说一张卡能跑4个实例——但在K8s默认配置下你只能跑一个。因为nvidia.com/gpu: 1的语义是一整张物理GPU卡不是1GB显存。这就是GPU利用率低的根源。pie title GPU真实利用率某AI团队实测数据 真正用于计算 : 35 显存占用但GPU空闲 : 25 完全空闲 : 4040%的时间GPU完全空闲25%的时间显存占着但GPU计算核心闲着。只有35%的时间真正在跑计算。这就是为什么GPU共享方案如此重要——放着一万块一张的卡闲着比程序出Bug还让人心疼。六、NVIDIA Device PluginGPU翻译官K8s本身不认识GPU。它只知道CPU、内存、磁盘这些基础资源。那GPU从哪来的答案是NVIDIA Device Plugin。它是Kubelet的一个插件专门负责发现扫描节点上有几张GPU、什么型号上报把GPU资源上报给K8s变成nvidia.com/gpu分配当Pod被调度到节点时把GPU设备挂载到容器里6.1 安装Device Plugin最简方式# 使用NVIDIA官方的Helm Chart推荐 helm repo add nvdp https://nvidia.github.io/k8s-device-plugin helm repo update helm install nvidia-device-plugin nvdp/nvidia-device-plugin \ --namespace nvidia-device-plugin \ --create-namespace \ --version 0.15.0 # 查看状态 kubectl get pods -n nvidia-device-plugin安装成功后检查节点资源kubectl describe node gpu-node-01 | grep nvidia输出示例nvidia.com/gpu: 4 nvidia.com/gpu.memory: 327680 nvidia.com/gpu.product: NVIDIA-A100-SXM4-40GB nvidia.com/gpu.replicas: 4 nvidia.com/gpu.sharing-strategy: none6.2 Device Plugin配置详解NVIDIA Device Plugin支持通过ConfigMap定制行为apiVersion: v1 kind: ConfigMap metadata: name: nvidia-device-plugin-config namespace: nvidia-device-plugin data: config.yaml: | version: v1 flags: migStrategy: none # MIG策略none / single / mixed failOnInitError: true # 初始化失败时是否让插件失败 nvidiaDriverRoot: / plugin: passDeviceSpecs: false deviceListStrategy: envvar deviceIDStrategy: uuid sharing: timeSlicing: resources: - name: nvidia.com/gpu replicas: 4 # 每张物理GPU虚拟成4份⚠️关于replicas参数它让一张物理GPU在K8s眼中变成N份。比如A100设replicas: 4K8s就认为这个节点有16个nvidia.com/gpu4张×416可以调度16个Pod上去。但这是虚假繁荣每个Pod分到的是时间片不是独立的物理GPU。所有Pod共享同一张卡的CUDA核心和显存。如果你4个Pod每个都要用80GB显存——会直接OOM。七、GPU共享方案从一张卡一个人到全民共享终于到了最核心的部分。上一节说到默认的GPU资源模型是1 Pod 1~N张物理卡这在推理场景下极其浪费。下面我们看看三种主流共享方案。graph LR subgraph 方案一: MIG A[物理GPU] -- A1[GPU实例1br/20GB] A -- A2[GPU实例2br/20GB] A -- A3[GPU实例3br/20GB] A -- A4[GPU实例4br/20GB] end subgraph 方案二: Time Slicing B[物理GPU] -- B1[时间片1] B -- B2[时间片2] B -- B3[时间片3] B -- B4[时间片4] end subgraph 方案三: MPS C[物理GPU] -- C1[进程1] C -- C2[进程2] C -- C3[进程3] C -- C4[进程4] end style A fill:#51cf66,color:#fff style B fill:#339af0,color:#fff style C fill:#fcc419,color:#3337.1 方案一NVIDIA MIG多实例GPU—— 物理隔离MIG是A100/A30/H100支持的硬件级分区技术把一张物理GPU切成多个独立的GPU实例每一份都有独立的显存、缓存和计算单元。优点✅ 硬件级隔离实例间互不影响✅ 显存和计算资源都是固定的不会互相抢占✅ 适合多租户场景推理服务A不会把推理服务B的显存撑爆缺点❌ 只有A100/A30/H100支持❌ 切分后单实例性能下降但隔离性换来的稳定更值钱❌ 配置后需要重启节点或重新配置GPUMIG配置示例A100 80GB切成 3g.20gb × 4# 在GPU节点上启用MIG模式 nvidia-smi -i 0 -mig 1 # 查看可用MIG配置 nvidia-smi mig -lgip # 创建MIG实例 nvidia-smi mig -cgi 19,19,19,19 -C # 查看MIG实例 nvidia-smi mig -lgi对应的K8s Device Plugin配置apiVersion: v1 kind: ConfigMap metadata: name: nvidia-device-plugin-config namespace: nvidia-device-plugin data: config.yaml: | version: v1 flags: migStrategy: mixed # 启用MIG模式 sharing: timeSlicing: resources: []启用MIG后K8s会看到类似nvidia.com/mig-3g.20gb的资源类型每个Pod可以请求一个MIG实例。7.2 方案二Time Slicing时间分片—— 最易上手Time Slicing是NVIDIA Device Plugin内置的能力把GPU的计算时间切片分配给不同Pod。优点✅ 配置简单改个ConfigMap即可✅ 支持所有GPU型号✅ 不需要修改应用代码缺点❌ 显存不隔离一个进程OOM全部受影响❌ 上下文切换有开销❌ 没有QoS保证完整配置示例apiVersion: v1 kind: ConfigMap metadata: name: nvidia-device-plugin-config namespace: nvidia-device-plugin data: config.yaml: | version: v1 sharing: timeSlicing: renameByDefault: false failRequestsGreaterThanOne: false resources: - name: nvidia.com/gpu replicas: 8 # 1张物理卡 8个虚拟GPU安装时指定ConfigMaphelm install nvidia-device-plugin nvdp/nvidia-device-plugin \ --namespace nvidia-device-plugin \ --create-namespace \ --set config.namenvidia-device-plugin-config \ --set config.defaultconfig配置后nvidia.com/gpu数量变成原来的8倍。每个Pod可以nvidia.com/gpu: 1但实际只分到1/8的时间片。⚠️Time Slicing的陷阱replicas: 8不意味着你能跑8个模型。假设一张A100 80GB你跑一个需要30GB显存的7B模型——最多只能跑2个60GB 80GB第3个Pod会因为显存不足OOM。Time Slicing只管时间不管显存。7.3 方案三NVIDIA MPS多进程服务—— 推理场景首选MPSMulti-Process Service是CUDA提供的一个服务允许多个CUDA进程共享GPU上下文减少上下文切换开销。优点✅ 真正的并行不是时间片轮转✅ 对推理场景提升显著吞吐量提升30-50%✅ 应用无感知缺点❌ 一个进程崩溃可能影响同一MPS组的所有进程❌ 需要额外配置❌ 错误隔离不如MIGMPS开启方式在Device Plugin配置中sharing: mps: resources: - name: nvidia.com/gpu replicas: 8方案选择速查表场景推荐方案原因多租户推理严格隔离MIG硬件级隔离显存互不影响同一团队的多个推理服务MPS性能最好团队内不需要严格隔离开发/测试环境Time Slicing配置最简单快速上手训练任务需要整卡不共享训练吃满计算共享意义不大混合场景MIG Time Slicing生产环境常用组合八、多GPU拓扑分布不要让PCIe成为瓶颈当你需要多张GPU时比如多卡训练GPU之间的通信带宽就成了关键。两张卡如果不在同一个PCIe Switch下你的训练速度可能直接腰斩。8.1 GPU拓扑感知调度K8s 1.27 引入了拓扑管理器Topology Manager配合Device Plugin实现NUMA感知的GPU分配。apiVersion: v1 kind: Pod metadata: name: distributed-training spec: containers: - name: training image: pytorch/pytorch:2.1.0-cuda12.1-cudnn8-devel resources: limits: nvidia.com/gpu: 4 env: - name: NCCL_DEBUG value: INFO - name: NCCL_SOCKET_IFNAME value: eth0但要真正做到拓扑感知调度需要GPU拓扑感知调度插件比如volcano的Gang Scheduling Topology Policy。8.2 Volcano GPU调度进阶Volcano是K8s的批调度引擎原生支持GPU拓扑感知apiVersion: batch.volcano.sh/v1alpha1 kind: Job metadata: name: multi-gpu-training spec: minAvailable: 4 schedulerName: volcano tasks: - replicas: 4 name: worker template: spec: containers: - name: training image: pytorch/pytorch:2.1.0-cuda12.1-cudnn8-devel resources: limits: nvidia.com/gpu: 2 command: - torchrun - --nproc_per_node2 - --nnodes4 - train.pyVolcano的优势Gang Scheduling要么所有Worker一次性调度成功要么都不调度避免部分节点卡着等Binpack尽量把Pod紧凑调度到同一节点减少跨节点通信拓扑感知配合nvidia-topology插件优先选择NVLink互连的GPU⚠️多GPU训练避坑指南检查GPU拓扑nvidia-smi topo -m看GPU间的连接方式NVLink PCIe Switch PCIe Host Bridge设置NCCL环境变量NCCL_P2P_LEVEL、NCCL_IB_DISABLE1没有RDMA时优先同节点部署跨节点的网络通信是百倍量级的延迟差距限制GPU可见性用NVIDIA_VISIBLE_DEVICES或CUDA_VISIBLE_DEVICES精确控制九、生产环境最佳实践一张清单打天下把前面所有知识点浓缩成一个实战Checklist9.1 节点准备# 1. 安装NVIDIA驱动 nvidia-container-toolkit # 2. 安装NVIDIA Device Plugin helm install nvidia-device-plugin nvdp/nvidia-device-plugin \ --namespace nvidia-device-plugin --create-namespace # 3. 打标签 kubectl label node gpu-node-0{1..4} node-role.kubernetes.io/gputrue kubectl label node gpu-node-01 nvidia.com/gpu.productNVIDIA-A100-SXM4-80GB # 4. 打污点 kubectl taint nodes gpu-node-0{1..4} gputrue:NoSchedule9.2 Pod配置模板开箱即用apiVersion: apps/v1 kind: Deployment metadata: name: production-inference namespace: ai-workloads spec: replicas: 2 selector: matchLabels: app: inference template: metadata: labels: app: inference spec: # 【1】污点容忍允许上GPU节点 tolerations: - key: gpu operator: Equal value: true effect: NoSchedule # 【2】节点亲和只选A100节点 affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: nvidia.com/gpu.product operator: In values: - NVIDIA-A100-SXM4-80GB - NVIDIA-H100-SXM # 【3】容器配置 containers: - name: inference-server image: myorg/llm-inference:v3.2 resources: limits: nvidia.com/gpu: 1 # 1张GPU memory: 64Gi # 显存主存需求 cpu: 8 requests: memory: 32Gi cpu: 4 # 【4】GPU环境变量 env: - name: CUDA_VISIBLE_DEVICES value: 0 - name: NVIDIA_DRIVER_CAPABILITIES value: compute,utility # 【5】健康检查模型加载慢给足时间 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 120 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 180 periodSeconds: 309.3 排障速查现象可能原因排查命令Pod一直Pending没有GPU节点 / 污点不匹配kubectl describe pod看Events调度成功但Pod CrashLoop容器内没有CUDA驱动kubectl logs看是否有CUDA错误GPU利用率低没有共享 / 显存用满但计算空闲kubectl exec -- nvidia-smi多卡训练慢GPU拓扑不对 / 跨节点通信nvidia-smi topo -mDevice Plugin不工作驱动版本不匹配kubectl logs -n nvidia-device-plugin十、写在最后K8s的GPU调度本质上是一套资源翻译策略路由系统。NVIDIA Device Plugin负责把物理GPU翻译成K8s能理解的资源而Node Selector、Affinity、Taints/Tolerations这些机制负责把资源路由到正确的Pod。记住三个原则隔离先行Taint Toleration 是所有配置的第一步让GPU节点只服务GPU负载表达精确Affinity 写清楚你需要的GPU型号别让调度器猜共享是刚需单一Pod独占一张GPU是巨大的浪费根据自己的场景选MIG/Time Slicing/MPS最后留一个思考题你现在用MIG把A100切成了4份每份20GB显存。来了一个Pod需要24GB显存跑模型——你怎么办提示答案是下篇文章要讲的内容——动态GPU资源调度。 下篇预告《GPU资源调度深度实战NVIDIA MIG Time Slicing HAMi》MIG到底该怎么切Time Slicing的坑有多少Hami阿里巴巴开源的GPU共享方案为什么能做得更好 下篇文章带你从配置到监控完整落地一套生产级GPU共享体系。 关注我不错过后续更新 如果这篇文章帮你少踩了几个坑不妨点赞— 让更多搞AI基建的同学看到 ⭐收藏— 下次配GPU调度回来翻比查文档快 评论— 把你的GPU调度血泪史说出来我们一起吐槽 关注— 下一期《GPU资源调度深度实战》已经在写了标签#Kubernetes #GPU调度 #AI工作负载 #NVIDIA #DevicePlugin #K8s #GPU共享原创文章转载请注明出处

相关新闻