GPU服务器的采购成本通常是普通节点的十倍以上,但在没有做任何调度约束的Kubernetes集群里,一个不声明GPU资源的普通Pod完全可能被调度到GPU机器上,白白占掉宝贵的CPU和内存,甚至出现CPU密集型任务把GPU节点塞满的情况。反过来,深度学习训练任务如果没有正确声明显卡需求,也可能被调度到没有GPU的节点上直接启动失败。要解决这些问题,核心思路就是两件事:一是把GPU节点“保护”起来,只允许特定任务进入;二是让GPU任务能够精确声明并独占显卡资源。下面结合实际落地经验,逐一展开几种常用方案。

用标签与污点把GPU节点隔离出来
最基础也是最有效的手段,是给GPU节点打上专属标签,同时配合污点(taint)让默认调度器主动避开这些节点。标签解决的是“我想去哪里”的问题,污点解决的是“谁能进来”的问题,两者配合才能形成完整的闭环。只打标签不加污点,普通Pod依然可能被调度过来;只加污点不打标签,GPU任务也没法明确表达自己的落点偏好。
先给节点打标签和污点:
# 给所有GPU节点打上标签 kubectl label nodes gpu-node-01 hardware=gpu # 给节点添加污点,key为nvidia.com/gpu,效果为NoSchedule kubectl taint nodes gpu-node-01 nvidia.com/gpu=true:NoSchedule
污点有三种效果:NoSchedule 表示不容忍就绝不调度;PreferNoSchedule 表示尽量不调度,属于软性约束;NoExecute 则更严格,连已经在节点上运行的Pod如果不容忍该污点也会被驱逐。GPU节点一般建议使用NoSchedule,这样存量任务不会被误杀,但新任务会被严格拦截。
GPU任务这边需要在Pod spec中同时声明容忍度和节点选择器:
apiVersion: v1
kind: Pod
metadata:
name: train-job
spec:
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
hardware: gpu
containers:
- name: trainer
image: nvidia/cuda:12.4.0-base-ubuntu22.04
resources:
limits:
nvidia.com/gpu: 2
这里有个容易被忽略的细节:即使有节点标签,也建议显式声明nvidia.com/gpu的资源请求,否则kubelet不会为容器分配具体显卡,容器内也看不到GPU设备。标签只负责把Pod引到正确的机器,真正的显卡分配还是要靠extended resource的配额机制。
用亲和性实现更灵活的调度控制
nodeSelector的表达能力有限,只能做精确匹配。当集群里存在多种型号的GPU机器,比如A100训练集群、T4推理集群、消费级卡的实验集群时,就需要节点亲和性(nodeAffinity)来支持更复杂的匹配逻辑。它支持In、NotIn、Exists、Gt、Lt等多种操作符,还可以配合preferredDuringSchedulingIgnoredDuringExecution实现优先级式的软约束。
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu-model
operator: In
values: ["A100", "H100"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: zone
operator: In
values: ["zone-a"]
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
上面的配置表示任务必须在A100或H100节点上运行,同时尽量调度到zone-a机房。注意preferred是软约束,如果zone-a资源不足,调度器会退而求其次,不会让任务卡在Pending状态。这种硬软结合的写法在多机房GPU资源池里非常实用。
除了节点亲和性,还要考虑Pod级别的反亲和性。一个多副本的推理服务如果把所有副本都堆在同一台GPU机器上,一旦机器故障服务就全挂了。通过podAntiAffinity可以让副本尽量打散到不同节点:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: gpu-inference
topologyKey: kubernetes.io/hostname
topologyKey设置为hostname表示打散到不同节点,设置为topology.kubernetes.io/zone则表示打散到不同可用区,可以根据可用性要求灵活选择。
理解Device Plugin与显卡分配的底层机制
Kubernetes本身并不理解GPU,它把显卡当作一种extended resource来管理。真正干活的是NVIDIA提供的Device Plugin,它以DaemonSet形式运行在每个GPU节点上,负责三件事:向kubelet上报节点上的显卡数量、监控显卡健康状态、在Pod调度成功后把具体的设备路径挂载给容器。所以nvidia.com/gpu: 2这个声明能生效的前提,是Device Plugin已经在节点上注册了这个资源名。
可以用下面的命令确认节点上报的GPU数量:
kubectl describe node gpu-node-01 | grep -A 5 "Allocated resources"
默认情况下,GPU分配是整卡粒度的,一个容器声明了1张卡,这张卡在当前节点上就不能再分给其他容器,即使显存只用了一小部分。这种模式简单可靠,避免了显存争抢导致的OOM和性能抖动,但也带来资源利用率低的问题。对于推理这类小显存负载,可以考虑GPU共享方案。
目前主流的共享方案有两类:一是时间片共享,通过NVIDIA的MIG(Multi-Instance GPU)技术把A100、H100这类卡在硬件层面切分成多个隔离实例,每个实例有独立的显存和计算单元,隔离性最好,但只支持少数数据中心卡;二是显存虚拟化方案,例如HAMi或gpushare-scheduler-extender,它们通过改写调度器和挂载逻辑,允许多个Pod分时复用同一张卡,通过声明nvidia.com/gpumem这类自定义资源来申请具体显存量。选择哪种方案,要在隔离性和利用率之间做权衡。
常见踩坑点与落地建议
第一个坑是驱动与运行时版本不匹配。节点上需要提前安装NVIDIA驱动、nvidia-container-toolkit,并把容器运行时默认运行时配置为nvidia,否则Pod能启动但容器内执行nvidia-smi会报错或看不到设备。可以用nvidia-smi和kubectl logs结合排查。
第二个坑是kubelet的Device Plugin预留机制。如果Pod没有声明GPU资源,kubelet默认会对其隐藏所有显卡设备,这是好事;但有些任务需要通过IPC或者hostPath访问GPU,这类特殊需求要单独评估,不要为了绕过限制随意把宿主机设备路径挂进容器,容易造成资源泄漏。
第三个坑是抢占与优先级。GPU任务排队时间长是常态,建议配合PriorityClass给高优先级训练任务更高权重,并在集群层面通过ResourceQuota按团队划分GPU配额,避免某个业务把整个GPU池吃光。对于批处理任务,还可以引入Volcano或Kueue这类批调度器,获得Gang Scheduling能力,确保分布式训练的所有worker要么一起启动,要么都不启动,避免出现部分Pod占着GPU等待其他伙伴的死锁局面。
总结一下实践路径:标签加污点做节点隔离是地基,亲和性做精细化路由,Device Plugin机制保障整卡分配,共享方案提升利用率,配额与优先级保障公平性。按这个顺序逐步建设,就能得到一个既安全又高效的GPU资源池。
GPU调度Kubernetes节点亲和性修改时间:2026-09-07 02:10:36