走一遍 nvidia.com/gpu:一次真实的调度演示

2026-10-12,在 prod102(10.0.1.47)上提交了两个短任务。第一个占住唯一名额,第二个排队;删掉第一个之后,第二个才跑起来。两个任务都已删除。vLLM 的 /health 在演示前后都是 200。

GPU Operator 事先已经把这张 H20 登记成资源 nvidia.com/gpu,数量是 1。这次没有改 Operator,也没有改 vLLM。只提交了两个 Pod,用来看调度器怎么记账、设备插件怎么把卡交给容器、名额满了以后下一个任务停在哪里。

节点名额allocatable nvidia.com/gpu = 1,演示前后都是 1
第一个任务gpu-demo-hold,Running,看见 H20
第二个任务gpu-demo-queue,先 Pending,名额空出后 Succeeded

整条链路

  1. 1
    YAML 里写上 nvidia.com/gpu: "1"这是向调度器要一个名额。没有这行,调度器不参与分卡。
  2. 2
    调度器看节点还剩几个名额prod102 当时剩 1。第一个任务被写上节点名 prod102。
  3. 3
    kubelet 创建容器,设备插件填入可见的卡容器里出现 NVIDIA_VISIBLE_DEVICES=这张 H20 的 UUID。
  4. 4
    节点账本变成 1 / 1第二个同样申请 1 的 Pod 没有节点可去,停在 Pending。
  5. 5
    第一个 Pod 删除,名额回到节点调度器立刻把第二个任务分到 prod102,容器执行 nvidia-smi 后结束。

演示前

节点上报的可分配数量是 1。集群里没有别的 Pod 在申请这个资源。vLLM 健康检查是 200,演示过程中没有重启它。

allocatable=1
vLLM /health 200

名额从哪来:Node Feature Discovery 给节点打过 nvidia.com/gpu.present=true,nvidia-device-plugin 向 kubelet 注册了 1 张卡。这两个程序在演示开始前就已经是 Running,这次没有动它们。

第一个任务:占住名额

Pod 名叫 gpu-demo-hold,命名空间 gpu-operator。镜像用节点上已经有的 vLLM 镜像,只为了里面带 nvidia-smi。命令是列出显卡、打印环境变量,然后睡眠,让名额在观察期间一直被占着。容器一退出,名额就会还回去,所以要用睡眠把过程留住。

apiVersion: v1
kind: Pod
metadata:
  name: gpu-demo-hold
  namespace: gpu-operator
spec:
  restartPolicy: Never
  containers:
    - name: smi
      image: docker.m.daocloud.io/vllm/vllm-openai:latest
      imagePullPolicy: IfNotPresent
      command: ["bash","-lc","nvidia-smi -L; env | grep NVIDIA; sleep 180"]
      resources:
        limits:
          nvidia.com/gpu: "1"

提交后大约 3 秒进入 Running。事件按顺序是:

谁发生了什么
default-schedulerSuccessfully assigned gpu-operator/gpu-demo-hold to prod102
kubelet镜像本机已经有,容器已创建、已启动

容器里 nvidia-smi -L 的输出:

GPU 0: NVIDIA H20 (UUID: GPU-0a9cfb9a-ef39-5b22-54ec-22474c4cfd76)

同一份日志里,和这次分配直接相关的环境变量是:

NVIDIA_VISIBLE_DEVICES=GPU-0a9cfb9a-ef39-5b22-54ec-22474c4cfd76
NVIDIA_DRIVER_CAPABILITIES=compute,utility
NVIDIA_VISIBLE_DEVICES 不是写在 YAML 里的。设备插件在分配这张卡时写进容器。值就是上面 nvidia-smi 看到的那张 H20。容器运行时按这个变量把设备放进容器。

这时节点账本

kubectl describe node prod102 的已分配资源里,显卡这一行变成:

Resource           Requests    Limits
nvidia.com/gpu     1           1

CPU、内存那几行是节点上其他 Pod 的合计,和这次演示无关。显卡这一行的 1,就是 gpu-demo-hold 占着的那个名额。可分配总量仍然是 1,所以剩余是 0。

第二个任务:排队

第一个还在跑的时候,提交 gpu-demo-queue。它也申请 nvidia.com/gpu: "1",命令只有 nvidia-smi -L,跑完就结束。

并排看两个 Pod:

NAME             READY   STATUS    IP            NODE
gpu-demo-hold    1/1     Running   10.244.0.62   prod102
gpu-demo-queue   0/1     Pending   <none>        <none>

第二个没有 IP,也没有节点。调度器的原话:

FailedScheduling
0/1 nodes are available: 1 Insufficient nvidia.com/gpu.
preemption: 0/1 nodes are available: 1 No preemption victims found for incoming pod.

第一句是名额不够:唯一的节点上,nvidia.com/gpu 已经是 0 个剩余。第二句是抢占没有发生。调度器找了一圈,没有可以踢掉的、优先级更低的 Pod。vLLM 没有申请这个资源,所以它不在这本账上,也不会被这次排队踢掉。

删掉第一个,第二个放行

执行 kubectl -n gpu-operator delete pod gpu-demo-hold --wait=true。名额回到节点后,第二个任务的事件多了四行:

结果事件
先排队FailedScheduling,Insufficient nvidia.com/gpu
随后分到Successfully assigned gpu-operator/gpu-demo-queue to prod102
跑完镜像已在本机,容器创建并启动,阶段变成 Succeeded

它的输出和第一个任务看到的是同一张卡:

GPU 0: NVIDIA H20 (UUID: GPU-0a9cfb9a-ef39-5b22-54ec-22474c4cfd76)

从删除第一个 Pod 到第二个变成 Succeeded,中间只隔了一次约 3 秒的观察。排队结束得很快,因为镜像已经在本机,命令本身只是列出显卡。

收场

随后删除 gpu-demo-queue。现场回到演示前:

仍在申请 nvidia.com/gpu 的 Pod:无
allocatable=1
vLLM /health 200

gpu-operator 命名空间里剩下的,仍是 Operator 自己的程序:device plugin、feature discovery、validator,以及已经 Completed 的 cuda-validator。没有演示 Pod 留下。

脚本在 k8s-探针与密钥/scripts/09-模拟GPU调度.sh。它会自己删除这两个 Pod。再跑一次会再次占用大约一分钟的调度名额。这次记录已经够看流程,不必为了复习再跑。

这次演示没有把卡从现有程序手里拿走

演示期间 vLLM 的健康检查一直是 200。宿主机上的 Python 进程也没有被停掉。原因用这次的账本就能对上:

程序在不在这本名额里演示期间
gpu-demo-hold / gpu-demo-queue申请了 nvidia.com/gpu: 1同一时间只有一个能跑
vLLM没有申请/health 保持 200
Xinference、宿主机 Python没有申请不经过这本账,继续使用同一张 H20 的显存

所以这次看到的「独占」,范围是调度器的名额:写了 nvidia.com/gpu: 1 的 Pod,同时只能有一个。显存仍然是原本那些进程一起用。两个演示容器只调用了 nvidia-smi,没有加载模型。