LabHub
学习 学习路径 课程

GPU Operator 与时间片

内核刚往上走一格,就只有那台节点没了驱动

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

노드 라벨을 사실의 자리로 삼아 미리 컴파일된 드라이버 이미지의 태그를 만드는 도구와 어긋난 노드를 찾아내는 점검기를 만들고, 노드가 늘었을 때 무엇이 먼저 필요한지 겪고, PodDisruptionBudget 이 막는 자리를 지나 드레인부터 uncordon 까지 업그레이드 절차를 끝까지 걸어 봅니다.

왜 중요한가

GPU 드라이버는 애플리케이션이 아니라 커널 모듈입니다. GPU Operator 가 드라이버를 컨테이너로 올리더라도 그 컨테이너가 하는 일은 호스트 커널에 모듈을 적재하는 것이고, 그래서 커널이 바뀌면 드라이버도 그 커널에 맞춰 다시 빌드돼야 합니다. 미리 컴파일된 드라이버 이미지의 태그가 <드라이버브랜치>-<커널판>-<OS태그> 인 것은 이 사실을 이름에 박아 둔 것입니다. 여기서 두 가지가 따라 나옵니다. 첫째, 노드가 한 대 늘거나 커널이 한 칸 올라가는 것은 곧 이미지 작업입니다 — 모르고 지나가면 그 노드에서만 드라이버 파드가 이미지 없음으로 죽습니다. 둘째, 드라이버를 올리려면 그 노드의 GPU 워크로드를 먼저 내려야 하는데, 그 일은 eviction API 를 거치므로 PodDisruptionBudget 이 업그레이드를 막습니다. 업그레이드가 한 노드에서 멈췄다는 신고의 상당수가 드라이버 문제가 아니라 예산 문제이고, 원인을 모르면 드라이버 로그만 몇 시간 읽게 됩니다.

단계

  1. /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 입니다.
  2. /root/gpudrv/drv-tag.sh <노드이름> 을 만드세요. 그 노드의 라벨을 읽어 미리 컴파일된 드라이버 이미지의 태그를 한 줄로 출력합니다. 형식은 공식 문서의 <드라이버브랜치>-<커널판>-<OS태그> 이고, 브랜치는 nvidia.com/cuda.driver.major, 커널판은 feature.node.kubernetes.io/kernel-version.full, OS 태그는 system-os_release.IDsystem-os_release.VERSION_ID붙여 쓴 것입니다 (예: ubuntu22.04 이면 ubuntu22.04). 세 노드에 차례로 돌려 /root/gpudrv/out/tags.txt<노드> <태그> 꼴로 세 줄을 적으세요. 노드 이름이나 태그를 스크립트 안에 적어 두지 마세요 — 채점기가 노드마다 직접 불러 봅니다.
  3. /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 여야 합니다).
  4. /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 여야 합니다).
  5. 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.majorIn 550, 그리고 feature.node.kubernetes.io/system-os_release.VERSION_IDIn 22.04. /root/gpudrv/k8s/cuda13-job.yaml 에 파드 cuda13-job 을 쓰세요 — 이미지는 nvcr.io/nvidia/pytorch:25.03-py3, 조건은 nvidia.com/cuda.driver.majorGt 560 하나입니다. 둘 다 적용한 뒤 cuda12-job 은 lab-node-0 에서 뜨고 cuda13-job 은 대기하는 것을 확인하고, cuda13-jobPodScheduled 조건 메시지를 /root/gpudrv/out/cuda.txt 에 저장하세요.
  6. /root/gpudrv/k8s/trainer.yaml 에 디플로이먼트 trainer 를 쓰세요 — 네임스페이스 gpu-drv, replicas: 4, 파드 라벨과 셀렉터는 app: trainer, 컨테이너 trainer, 이미지 nvcr.io/nvidia/pytorch:24.07-py3, 그리고 required podAntiAffinitytopologyKey: 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 에 저장하세요. 거절당해야 정상입니다.
  7. 업그레이드 순서를 그대로 밟습니다. (1) lab-node-1 을 드라이버 550.90.07 로 올리면 필요한 태그가 550-5.15.0-107-generic-ubuntu22.04 입니다 — 그 조합을 support-matrix.csv 에 먼저 더하세요(이미지를 확보한 셈입니다). (2) 예산에 여유를 만드세요 — trainer-pdbminAvailable3 으로 낮춥니다. (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 가 나오는지 확인하세요.
  8. 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. 숫자와 태그는 지금 상태에서 명령으로 뽑아 채우세요.

참고

커널 판과 드라이버 판을 노드의 사실로 세운다

/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.IDsystem-os_release.VERSION_ID붙여 쓴 것입니다 (예: ubuntu22.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.majorIn 550, 그리고 feature.node.kubernetes.io/system-os_release.VERSION_IDIn 22.04. /root/gpudrv/k8s/cuda13-job.yaml 에 파드 cuda13-job 을 쓰세요 — 이미지는 nvcr.io/nvidia/pytorch:25.03-py3, 조건은 nvidia.com/cuda.driver.majorGt 560 하나입니다. 둘 다 적용한 뒤 cuda12-job 은 lab-node-0 에서 뜨고 cuda13-job 은 대기하는 것을 확인하고, cuda13-jobPodScheduled 조건 메시지를 /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 podAntiAffinitytopologyKey: 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-pdbminAvailable3 으로 낮춥니다. (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단계에서 표에 줄을 더했고 드라이버 판도 바뀌었으니 값이 앞 단계와 다릅니다.