LabHub
배우기 러닝패스 코스

GPU Operator and Time-Slicing

The kernel moved up one notch, and only that node lost its driver

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단계에서 표에 줄을 더했고 드라이버 판도 바뀌었으니 값이 앞 단계와 다릅니다.