内核刚往上走一格,就只有那台节点没了驱动
目标
以节点标签作为事实的来源,制作生成预编译驱动镜像标签的工具和找出不匹配节点的检查器,体验节点增加时首先需要什么,并经过 PodDisruptionBudget 的拦截,把从 drain 到 uncordon 的升级流程完整走一遍。
为什么重要
GPU 驱动不是应用程序,而是内核模块。即使 GPU Operator 把驱动以容器形式启动,这个容器做的事也是把模块加载到主机内核中,所以内核一变,驱动也必须针对该内核重新构建。预编译驱动镜像的标签是 <드라이버브랜치>-<커널판>-<OS태그>(占位符依次为驱动分支、内核版本与 OS 标签),就是把这个事实写进了名称里。由此引出两点。第一,增加一个节点或内核升一个版本,就是镜像工作——不了解就放过去的话,只有那个节点上的驱动 Pod 会因找不到镜像而失败。第二,要加载驱动,必须先撤下该节点的 GPU 工作负载,而这件事要经过 eviction API,所以 PodDisruptionBudget 会阻止升级。 “升级在一个节点上停住了”这类报告,相当一部分不是驱动问题而是预算问题,不了解原因的话,就会只读几个小时的驱动日志。
步骤
- 在
/root/gpudrv中工作(export KUBECONFIG=/root/.kube/config)。先创建命名空间gpu-drv(后面步骤的 Pod 会在这里启动)。给三个节点添加标签。lab-node-0:nvidia.com/gpu.present=true、nvidia.com/cuda.driver.major=550、nvidia.com/cuda.driver.minor=90、nvidia.com/cuda.driver.rev=07、feature.node.kubernetes.io/kernel-version.full=5.15.0-119-generic、feature.node.kubernetes.io/system-os_release.ID=ubuntu、feature.node.kubernetes.io/system-os_release.VERSION_ID=22.04。lab-node-1:用相同的键,驱动为535/183/06,内核为5.15.0-107-generic,ubuntu22.04。lab-node-2:驱动为550/90/07,内核为6.8.0-45-generic,ubuntu24.04。三个节点都带有nvidia.com/gpu.present=true。 - 创建
/root/gpudrv/drv-tag.sh <노드이름>(占位符为节点名称)。读取该节点的标签,把预编译驱动镜像的标签以一行输出。格式是官方文档中的<드라이버브랜치>-<커널판>-<OS태그>(占位符依次为驱动分支、内核版本与 OS 标签),分支取自nvidia.com/cuda.driver.major,内核版本取自feature.node.kubernetes.io/kernel-version.full,OS 标签是把system-os_release.ID和system-os_release.VERSION_ID连在一起写(例如ubuntu与22.04就是ubuntu22.04)。依次对三个节点运行,并在/root/gpudrv/out/tags.txt中按<노드> <태그>(占位符依次为节点与标签)的形式写三行。不要把节点名称或标签写在脚本里——评分器会针对每个节点亲自调用它。 - 创建
/root/gpudrv/support-matrix.csv。第一行是表头driver_branch,kernel,os_tag,之后写三行公司内部注册表中实际构建好的组合——550,5.15.0-119-generic,ubuntu22.04、535,5.15.0-107-generic,ubuntu22.04、550,6.8.0-45-generic,ubuntu24.04。然后创建/root/gpudrv/drv-audit.sh——遍历nvidia.com/gpu.present=true的所有节点,检查该节点的组合是否在这张表中,没有的话,每个输出一行<노드> MISSING <태그>(占位符依次为节点与标签),并以退出码 1 结束;一个都没有,就输出一行OK并以 0 结束。创建之后运行它,并把输出保存到/root/gpudrv/out/audit.txt(现在应该是OK)。 - 用
/root/gpudrv/k8s/node3.yaml把新节点lab-node-3加入集群。标签为nvidia.com/gpu.present=true,驱动为550/90/07,内核为6.8.0-52-generic,ubuntu24.04,并且是带有kwok.x-k8s.io/node: fake注解和 Ready 条件的假节点(格式请看示例)。应用之后,运行drv-audit.sh,把输出保存到/root/gpudrv/out/skew.txt——新节点应该被查出来才正常。接着假设已经把这个组合构建并推送到了注册表,在support-matrix.csv中再加一行,把重新运行的输出保存到/root/gpudrv/out/skew-fixed.txt(现在应该是OK)。 - 在第 1 步创建的命名空间
gpu-drv中启动两个 Pod。在/root/gpudrv/k8s/cuda12-job.yaml中写出 Podcuda12-job——容器名称为trainer,镜像为nvcr.io/nvidia/pytorch:24.07-py3,required nodeAffinity 的一个 term 中有两个条件:nvidia.com/cuda.driver.major为In550,并且feature.node.kubernetes.io/system-os_release.VERSION_ID为In22.04。在/root/gpudrv/k8s/cuda13-job.yaml中写出 Podcuda13-job——镜像为nvcr.io/nvidia/pytorch:25.03-py3,条件只有一个:nvidia.com/cuda.driver.major为Gt560。两个都应用之后,请确认cuda12-job在 lab-node-0 上启动,cuda13-job在等待,并把cuda13-job的PodScheduled条件消息保存到/root/gpudrv/out/cuda.txt。 - 在
/root/gpudrv/k8s/trainer.yaml中写出 Deploymenttrainer——命名空间为gpu-drv,replicas: 4,Pod 标签和选择器为app: trainer,容器为trainer,镜像为nvcr.io/nvidia/pytorch:24.07-py3,并用 required podAntiAffinity,针对topologyKey: kubernetes.io/hostname,让带有相同app: trainer的 Pod 不能有两个落在同一个节点上(会分散到四个节点上各一个)。在/root/gpudrv/k8s/pdb.yaml中写出 PodDisruptionBudgettrainer-pdb——minAvailable: 4,选择器为app: trainer。四个 Pod 全部变为 Running 之后,运行kubectl drain lab-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=20s,并把输出连同标准错误一起保存到/root/gpudrv/out/drain-blocked.txt。应当被拒绝才正常。 - 严格按升级顺序进行。(1) 把 lab-node-1 升级到驱动
550.90.07,所需的标签就是550-5.15.0-107-generic-ubuntu22.04——请先把这个组合加到support-matrix.csv中(相当于确保了镜像)。(2) 为预算留出余量——把trainer-pdb的minAvailable降到3。(3) 再次运行同样的 drain 命令,这次让它成功,并把输出保存到/root/gpudrv/out/drain-ok.txt。(4) 把 lab-node-1 的三个驱动标签改为550/90/07(相当于重新加载了驱动——在这个环境中并不会真的安装)。(5) 用kubectl uncordon lab-node-1恢复该节点。最后再次运行drv-audit.sh,确认出现OK。 - GPU Operator 的升级控制器通过节点标签
nvidia.com/gpu-driver-upgrade-state表示进度状态。请在/root/gpudrv/out/upgrade-states.txt中按文档中出现的顺序写下这些状态,共八行——upgrade-required、cordon-required、pod-deletion-required、drain-required、pod-restart-required、validation-required、uncordon-required、upgrade-done。然后给 lab-node-1 添加nvidia.com/gpu-driver-upgrade-state=upgrade-done标签。最后在/root/gpudrv/out/upgrade-report.txt中写五行——NODES=<gpu.present 가 true 인 노드 수>、SKEW=<drv-audit.sh 가 낸 MISSING 줄 수>、LAB_NODE_1_TAG=<drv-tag.sh 가 lab-node-1 에 대해 내는 태그>、MATRIX_ROWS=<support-matrix.csv 의 머리글을 뺀 줄 수>、UPGRADE_STATE=upgrade-done(占位符依次为 gpu.present 为 true 的节点数、drv-audit.sh 输出的 MISSING 行数、drv-tag.sh 针对 lab-node-1 输出的标签、support-matrix.csv 中除表头外的行数)。数字和标签请从当前状态用命令提取后填写。
参考
- 从
export KUBECONFIG=/root/.kube/config开始。节点一开始是 lab-node-0/1/2 三个,第 4 步再亲手增加一个。产出物放在/root/gpudrv中,对象放在命名空间gpu-drv中。 - 不会真正安装驱动。 本环境中既没有 GPU,也没有内核模块和 nvidia-smi。所以内核版本和驱动版本用节点标签来表示,升级则用修改这些标签来表示。在真实的集群中,调度器和 Operator 所看的,归根结底也是这些标签。
- 而另一方面,节点添加、nodeAffinity、podAntiAffinity、PodDisruptionBudget、eviction 被拒绝、drain 和 uncordon,都是真实控制平面做的事,所以会如实工作。本实验所判定的也是这一部分。
- 常见错误:不带
--timeout运行 drain。一旦被预算拦住,它会永远重试。 - 常见错误:drain 结束之后漏掉 uncordon。这个节点会悄无声息地闲置,也没有任何错误。
- 常见错误:把节点名称或标签写在检查器里。节点一增加,这个工具从当天起就开始说谎。
- GPU Driver Upgrades · Precompiled Driver Containers · Safely Drain a Node · Specifying a Disruption Budget
把内核版本和驱动版本确立为节点的事实
在 /root/gpudrv 中工作(export KUBECONFIG=/root/.kube/config)。先创建命名空间 gpu-drv(后面步骤的 Pod 会在这里启动)。给三个节点添加标签。lab-node-0:nvidia.com/gpu.present=true、nvidia.com/cuda.driver.major=550、nvidia.com/cuda.driver.minor=90、nvidia.com/cuda.driver.rev=07、feature.node.kubernetes.io/kernel-version.full=5.15.0-119-generic、feature.node.kubernetes.io/system-os_release.ID=ubuntu、feature.node.kubernetes.io/system-os_release.VERSION_ID=22.04。lab-node-1:用相同的键,驱动为 535/183/06,内核为 5.15.0-107-generic,ubuntu 22.04。lab-node-2:驱动为 550/90/07,内核为 6.8.0-45-generic,ubuntu 24.04。三个节点都带有 nvidia.com/gpu.present=true。
本环境中既没有 GPU 也没有驱动,所以标签就是事实的来源。在真实的集群中,内核和 OS 标签由 nfd-worker 添加,nvidia.com/cuda.driver.* 由 gpu-feature-discovery 添加。驱动版本的标签被拆成 major.minor.rev 三段——550.90.07 就分别是 550、90、07。用 kubectl label node <이름> <키>=<값> --overwrite(占位符依次为节点名称、键与值)可以一次添加多个。像 22.04 这样含有点的值,并不违反标签值规范。
计算一个节点所需的驱动镜像标签
创建 /root/gpudrv/drv-tag.sh <노드이름>(占位符为节点名称)。读取该节点的标签,把预编译驱动镜像的标签以一行输出。格式是官方文档中的 <드라이버브랜치>-<커널판>-<OS태그>(占位符依次为驱动分支、内核版本与 OS 标签),分支取自 nvidia.com/cuda.driver.major,内核版本取自 feature.node.kubernetes.io/kernel-version.full,OS 标签是把 system-os_release.ID 和 system-os_release.VERSION_ID 连在一起写(例如 ubuntu 与 22.04 就是 ubuntu22.04)。依次对三个节点运行,并在 /root/gpudrv/out/tags.txt 中按 <노드> <태그>(占位符依次为节点与标签)的形式写三行。不要把节点名称或标签写在脚本里——评分器会针对每个节点亲自调用它。
NVIDIA 文档中的示例标签是 525-5.15.0-69-generic-ubuntu22.04。分支之后是内核版本,再之后是 OS 标签。内核版本里也有连字符,所以如果想把标签倒过来拆分阅读,就会混乱——制作时,只要把各段分别从标签中取出,再接起来就行了。只要缺少一个标签,标签就会悄悄变得奇怪。遇到空值时,最好以错误结束。用 jq 取标签时如果使用 //,不存在的标签和空值就无法区分。
与注册表中的组合对照,找出不匹配的节点
创建 /root/gpudrv/support-matrix.csv。第一行是表头 driver_branch,kernel,os_tag,之后写三行公司内部注册表中实际构建好的组合——550,5.15.0-119-generic,ubuntu22.04、535,5.15.0-107-generic,ubuntu22.04、550,6.8.0-45-generic,ubuntu24.04。然后创建 /root/gpudrv/drv-audit.sh——遍历 nvidia.com/gpu.present=true 的所有节点,检查该节点的组合是否在这张表中,没有的话,每个输出一行 <노드> MISSING <태그>(占位符依次为节点与标签),并以退出码 1 结束;一个都没有,就输出一行 OK 并以 0 结束。创建之后运行它,并把输出保存到 /root/gpudrv/out/audit.txt(现在应该是 OK)。
这张表就是“我们已有的驱动镜像列表”。实际上它是 NGC 注册表的标签列表,或者是公司内部构建并推送的镜像列表,无论哪种,不存在的标签只会表现为 Pod 以 ImagePullBackOff 失败。 所以在升级内核之前,先做这个对照才是正确的顺序。节点列表请随时获取——评分器会把某个节点的内核标签暂时改掉再来调用。不需要重新生成标签,调用第 2 步创建的脚本即可。
节点一增加,首先缺的就是驱动镜像
用 /root/gpudrv/k8s/node3.yaml 把新节点 lab-node-3 加入集群。标签为 nvidia.com/gpu.present=true,驱动为 550/90/07,内核为 6.8.0-52-generic,ubuntu 24.04,并且是带有 kwok.x-k8s.io/node: fake 注解和 Ready 条件的假节点(格式请看示例)。应用之后,运行 drv-audit.sh,把输出保存到 /root/gpudrv/out/skew.txt——新节点应该被查出来才正常。接着假设已经把这个组合构建并推送到了注册表,在 support-matrix.csv 中再加一行,把重新运行的输出保存到 /root/gpudrv/out/skew-fixed.txt(现在应该是 OK)。
新节点通常是用最新镜像安装的,所以内核比已有节点更靠前。即使驱动分支相同,内核版本不同,也需要不同的镜像——这就是预编译驱动的标签中包含内核版本的原因。不了解增加节点就是构建镜像的工作这一事实,就会在“只有新节点上的驱动 Pod 因找不到镜像而失败”这个问题上打转。在这个环境中,节点也只是 API 对象,所以可以用 kubectl apply 创建。往 CSV 中添加行时,请遵守表头的顺序(分支、内核、OS 标签)。
CUDA 只向前兼容——把这个要求写成标签条件
在第 1 步创建的命名空间 gpu-drv 中启动两个 Pod。在 /root/gpudrv/k8s/cuda12-job.yaml 中写出 Pod cuda12-job——容器名称为 trainer,镜像为 nvcr.io/nvidia/pytorch:24.07-py3,required nodeAffinity 的一个 term 中有两个条件:nvidia.com/cuda.driver.major 为 In 550,并且 feature.node.kubernetes.io/system-os_release.VERSION_ID 为 In 22.04。在 /root/gpudrv/k8s/cuda13-job.yaml 中写出 Pod cuda13-job——镜像为 nvcr.io/nvidia/pytorch:25.03-py3,条件只有一个:nvidia.com/cuda.driver.major 为 Gt 560。两个都应用之后,请确认 cuda12-job 在 lab-node-0 上启动,cuda13-job 在等待,并把 cuda13-job 的 PodScheduled 条件消息保存到 /root/gpudrv/out/cuda.txt。
驱动不认识比自己更晚发布的 CUDA 运行时。 反过来,旧的 CUDA 容器在新驱动上可以正常运行——也就是说,兼容性只朝一个方向开放。所以如果把“这个容器需要驱动版本在某个版本以上”写成节点标签条件,就会在调度阶段被拦住,而不是去了不匹配的节点、在运行中失败。把两个条件放进一个 term,就必须同时满足两者。等待的原因在 .status.conditions 的 PodScheduled 消息中。Gt 会把值读成整数,所以只能用于像分支编号这样的整数标签。
阻止升级的不是驱动,而是预算
在 /root/gpudrv/k8s/trainer.yaml 中写出 Deployment trainer——命名空间为 gpu-drv,replicas: 4,Pod 标签和选择器为 app: trainer,容器为 trainer,镜像为 nvcr.io/nvidia/pytorch:24.07-py3,并用 required podAntiAffinity,针对 topologyKey: kubernetes.io/hostname,让带有相同 app: trainer 的 Pod 不能有两个落在同一个节点上(会分散到四个节点上各一个)。在 /root/gpudrv/k8s/pdb.yaml 中写出 PodDisruptionBudget trainer-pdb——minAvailable: 4,选择器为 app: trainer。四个 Pod 全部变为 Running 之后,运行 kubectl drain lab-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=20s,并把输出连同标准错误一起保存到 /root/gpudrv/out/drain-blocked.txt。应当被拒绝才正常。
要加载驱动,必须先撤下该节点的 GPU 工作负载,撤下要经过 eviction API。PodDisruptionBudget 就是拦住这个 API 的装置——当前存活的数量一旦降到预算以下,就会拒绝。所以“驱动升级在一个节点上停住了”的真正原因,常常不是驱动,而是预算。drain 会先 cordon 节点,再逐个 evict Pod。所以即使被拦住,节点也已经处于不可调度状态。如果不指定 --timeout,drain 会永远重试。
先确保镜像,再留出余量,把流程走到底
严格按升级顺序进行。(1) 把 lab-node-1 升级到驱动 550.90.07,所需的标签就是 550-5.15.0-107-generic-ubuntu22.04——请先把这个组合加到 support-matrix.csv 中(相当于确保了镜像)。(2) 为预算留出余量——把 trainer-pdb 的 minAvailable 降到 3。(3) 再次运行同样的 drain 命令,这次让它成功,并把输出保存到 /root/gpudrv/out/drain-ok.txt。(4) 把 lab-node-1 的三个驱动标签改为 550/90/07(相当于重新加载了驱动——在这个环境中并不会真的安装)。(5) 用 kubectl uncordon lab-node-1 恢复该节点。最后再次运行 drv-audit.sh,确认出现 OK。
顺序就是这一步的全部。如果在确保镜像之前就先 drain,就会把节点腾空后干等;如果在为预算留出余量之前就 drain,就会像前一步那样被拒绝。PDB 可以用 kubectl patch pdb <이름> --type=merge -p '{"spec":{"minAvailable":N}}'(占位符为名称)来修改。修改预算之后,控制器需要几秒钟来重新计算 status.disruptionsAllowed。drain 结束后,该节点会保持 cordon 状态——如果漏掉 uncordon,这个节点就会悄无声息地闲置。
升级状态机与收尾报告
GPU Operator 的升级控制器通过节点标签 nvidia.com/gpu-driver-upgrade-state 表示进度状态。请在 /root/gpudrv/out/upgrade-states.txt 中按文档中出现的顺序写下这些状态,共八行——upgrade-required、cordon-required、pod-deletion-required、drain-required、pod-restart-required、validation-required、uncordon-required、upgrade-done。然后给 lab-node-1 添加 nvidia.com/gpu-driver-upgrade-state=upgrade-done 标签。最后在 /root/gpudrv/out/upgrade-report.txt 中写五行——NODES=<gpu.present 가 true 인 노드 수>、SKEW=<drv-audit.sh 가 낸 MISSING 줄 수>、LAB_NODE_1_TAG=<drv-tag.sh 가 lab-node-1 에 대해 내는 태그>、MATRIX_ROWS=<support-matrix.csv 의 머리글을 뺀 줄 수>、UPGRADE_STATE=upgrade-done(占位符依次为 gpu.present 为 true 的节点数、drv-audit.sh 输出的 MISSING 行数、drv-tag.sh 针对 lab-node-1 输出的标签、support-matrix.csv 中除表头外的行数)。数字和标签请从当前状态用命令提取后填写。
并不是要背下状态机,重要的是,升级停住时,能用一行看出停在了哪里。 用 kubectl get node -l nvidia.com/gpu.present -o jsonpath 把每个节点的这个标签一次性提取出来看,就能立刻看出停在 cordon-required 的节点和停在 pod-deletion-required 的节点,原因各不相同。这一步的数字,不应来自前面步骤的记忆,而应来自当前的集群和当前的文件——第 7 步中表里添加了一行,驱动版本也变了,所以值与前面的步骤不同。