内核刚往上走一格,就只有那台节点没了驱动
한국어 원문으로 표시합니다.
목표
노드 라벨을 사실의 자리로 삼아 미리 컴파일된 드라이버 이미지의 태그를 만드는 도구와 어긋난 노드를 찾아내는 점검기를 만들고, 노드가 늘었을 때 무엇이 먼저 필요한지 겪고, PodDisruptionBudget 이 막는 자리를 지나 드레인부터 uncordon 까지 업그레이드 절차를 끝까지 걸어 봅니다.
왜 중요한가
GPU 드라이버는 애플리케이션이 아니라 커널 모듈입니다. GPU Operator 가 드라이버를 컨테이너로 올리더라도 그 컨테이너가 하는 일은 호스트 커널에 모듈을 적재하는 것이고, 그래서 커널이 바뀌면 드라이버도 그 커널에 맞춰 다시 빌드돼야 합니다. 미리 컴파일된 드라이버 이미지의 태그가 <드라이버브랜치>-<커널판>-<OS태그> 인 것은 이 사실을 이름에 박아 둔 것입니다. 여기서 두 가지가 따라 나옵니다. 첫째, 노드가 한 대 늘거나 커널이 한 칸 올라가는 것은 곧 이미지 작업입니다 — 모르고 지나가면 그 노드에서만 드라이버 파드가 이미지 없음으로 죽습니다. 둘째, 드라이버를 올리려면 그 노드의 GPU 워크로드를 먼저 내려야 하는데, 그 일은 eviction API 를 거치므로 PodDisruptionBudget 이 업그레이드를 막습니다. 업그레이드가 한 노드에서 멈췄다는 신고의 상당수가 드라이버 문제가 아니라 예산 문제이고, 원인을 모르면 드라이버 로그만 몇 시간 읽게 됩니다.
단계
/root/gpudrv에서 작업합니다(export KUBECONFIG=/root/.kube/config). 먼저 네임스페이스gpu-drv를 만드세요(뒤 단계의 파드가 여기에 뜹니다). 세 노드에 라벨을 붙이세요. 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태그>이고, 브랜치는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에 파드 둘을 올립니다./root/gpudrv/k8s/cuda12-job.yaml에 파드cuda12-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에 파드cuda13-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에 디플로이먼트trainer를 쓰세요 — 네임스페이스gpu-drv,replicas: 4, 파드 라벨과 셀렉터는app: trainer, 컨테이너trainer, 이미지nvcr.io/nvidia/pytorch:24.07-py3, 그리고 required podAntiAffinity 로topologyKey: kubernetes.io/hostname에 대해 같은app: trainer끼리 한 노드에 둘이 못 오게 하세요(노드 네 대에 하나씩 퍼집니다)./root/gpudrv/k8s/pdb.yaml에 PodDisruptionBudgettrainer-pdb를 쓰세요 —minAvailable: 4, 셀렉터는app: trainer. 네 파드가 모두 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. 숫자와 태그는 지금 상태에서 명령으로 뽑아 채우세요.
참고
export KUBECONFIG=/root/.kube/config로 시작합니다. 노드는 lab-node-0/1/2 셋으로 시작하고 4단계에서 한 대를 직접 늘립니다. 산출물은/root/gpudrv, 오브젝트는 네임스페이스gpu-drv에 둡니다.- 드라이버를 실제로 설치하지 않습니다. 이 환경에는 GPU 도 커널 모듈도 nvidia-smi 도 없습니다. 그래서 커널 판과 드라이버 판은 노드 라벨로 표현하고, 업그레이드는 그 라벨을 바꾸는 것으로 나타냅니다. 실제 클러스터에서도 스케줄러와 오퍼레이터가 보는 것은 결국 그 라벨입니다.
- 반면 노드 추가·nodeAffinity·podAntiAffinity·PodDisruptionBudget·eviction 거부·drain·uncordon 은 진짜 컨트롤 플레인이 하는 일이라 그대로 동작합니다. 이 실습이 판정하는 것도 그쪽입니다.
- 흔한 실수:
--timeout없이 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 를 만드세요(뒤 단계의 파드가 여기에 뜹니다). 세 노드에 라벨을 붙이세요. 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태그> 이고, 브랜치는 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 레지스트리의 태그 목록이거나 사내에서 빌드해 올린 이미지 목록이고, 어느 쪽이든 없는 태그는 파드가 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 여야 합니다).
새 노드는 대개 최신 이미지로 깔려 있어서 커널이 기존 노드보다 앞섭니다. 드라이버 브랜치가 같아도 커널판이 다르면 다른 이미지가 필요합니다 — 미리 컴파일된 드라이버의 태그에 커널판이 들어 있는 이유입니다. 노드 추가가 곧 이미지 빌드 작업이라는 사실을 모르면, 새 노드에서만 드라이버 파드가 이미지 없음으로 죽는 자리에서 헤맵니다. 이 환경에서는 노드도 그냥 API 오브젝트라 kubectl apply 로 만들 수 있습니다. CSV 에 줄을 더할 때는 머리글 순서(브랜치, 커널, OS 태그)를 지키세요.
CUDA 는 앞으로만 호환된다 — 그 요구를 라벨 조건으로 적는다
1단계에서 만든 네임스페이스 gpu-drv 에 파드 둘을 올립니다. /root/gpudrv/k8s/cuda12-job.yaml 에 파드 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 에 파드 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 에 디플로이먼트 trainer 를 쓰세요 — 네임스페이스 gpu-drv, replicas: 4, 파드 라벨과 셀렉터는 app: trainer, 컨테이너 trainer, 이미지 nvcr.io/nvidia/pytorch:24.07-py3, 그리고 required podAntiAffinity 로 topologyKey: kubernetes.io/hostname 에 대해 같은 app: trainer 끼리 한 노드에 둘이 못 오게 하세요(노드 네 대에 하나씩 퍼집니다). /root/gpudrv/k8s/pdb.yaml 에 PodDisruptionBudget trainer-pdb 를 쓰세요 — minAvailable: 4, 셀렉터는 app: trainer. 네 파드가 모두 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 합니다. 그래서 막혀도 노드는 이미 스케줄 불가 상태입니다. --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 가 나오는지 확인하세요.
순서가 이 단계의 전부입니다. 이미지를 확보하기 전에 드레인부터 하면 노드를 비워 놓고 기다리게 되고, 예산에 여유를 만들기 전에 드레인하면 앞 단계처럼 거절당합니다. PDB 는 kubectl patch pdb <이름> --type=merge -p '{"spec":{"minAvailable":N}}' 로 고칠 수 있습니다. 예산을 고친 직후에는 컨트롤러가 status.disruptionsAllowed 를 다시 계산할 시간이 몇 초 필요합니다. 드레인이 끝나면 그 노드는 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. 숫자와 태그는 지금 상태에서 명령으로 뽑아 채우세요.
상태 기계를 외우라는 것이 아니라, 업그레이드가 멈췄을 때 어디서 멈췄는지 한 줄로 알 수 있다는 사실이 중요합니다. kubectl get node -l nvidia.com/gpu.present -o jsonpath 로 노드마다 이 라벨을 한 번에 뽑아 보면, cordon-required 에서 멈춘 노드와 pod-deletion-required 에서 멈춘 노드의 원인이 서로 다르다는 것이 바로 보입니다. 이 단계의 숫자들은 앞 단계의 기억이 아니라 지금 클러스터와 지금 파일에서 나와야 합니다 — 7단계에서 표에 줄을 더했고 드라이버 판도 바뀌었으니 값이 앞 단계와 다릅니다.