LabHub
배우기 러닝패스 코스

GPU Operator 와 타임슬라이싱 · 드라이버는 애플리케이션이 아니라 커널 모듈이다 · 이론

드라이버 판 맞추기 — 태그에 커널판이 들어 있는 이유

LabHub 에서 이어서 보기

한 줄 요약

컨테이너로 배포되는 GPU 드라이버가 실제로 하는 일은 호스트 커널에 모듈을 올리는 것이라, 커널이 바뀌면 드라이버도 다시 빌드돼야 하고(그래서 미리 컴파일된 이미지의 태그에 커널판이 들어 있다), 드라이버를 올리려면 그 노드의 GPU 워크로드를 먼저 비워야 한다(그래서 PodDisruptionBudget 이 업그레이드를 막는다).

왜 이게 문제가 되나

보통의 컨테이너는 격리된 사용자 공간에서 돈다. 호스트에 무엇이 깔려 있든 이미지 안의 라이브러리를 쓰고, 그래서 "컨테이너로 만들면 호스트와 무관해진다" 는 직관이 성립한다.

드라이버는 그렇지 않다. NVIDIA 문서는 드라이버 업그레이드에 특별한 고려가 필요한 이유를 이렇게 적는다. 드라이버 커널 모듈은 드라이버 컨테이너가 다시 시작될 때마다 내려졌다가 다시 올라가야 한다. 그리고 그 절차는 다섯 걸음이다 — 드라이버를 쓰는 모든 클라이언트를 멈추고, 현재 커널 모듈을 내리고, 새 드라이버 파드를 띄우고, 새 모듈을 올리고, 클라이언트를 다시 켠다.

여기서 두 가지 성질이 나온다. 첫째, 드라이버는 호스트 커널에 종속된다. 커널 모듈은 커널 판에 맞춰 컴파일되어야 하므로, 커널이 한 칸 올라가면 그 커널용 드라이버가 따로 필요하다. 둘째, 드라이버 교체는 그 노드의 GPU 워크로드를 반드시 건드린다. 모듈을 내리려면 그것을 쓰는 프로세스가 없어야 하기 때문이다.

어떻게 동작하나 — 이미지 태그가 계약이다

기본 방식의 드라이버 컨테이너는 노드에서 커널 헤더와 컴파일러를 받아 부팅할 때 모듈을 빌드한다. 인터넷이 필요하고 시간이 걸리고 CPU 를 먹는다. 그래서 NVIDIA 는 미리 컴파일된 드라이버 컨테이너를 따로 낸다. 문서는 그 이점을 인터넷 접근이 제한된 곳과 자원이 빠듯한 곳에 특히 값어치가 있다고 적는다.

미리 컴파일된 이미지의 태그는 이렇게 생겼다.

<driver-branch>-<linux-kernel-version>-<os-tag>예: 525-5.15.0-69-generic-ubuntu22.04

세 조각이 태그 하나에 박혀 있다는 것이 이 방식의 계약이다. 드라이버 브랜치가 같아도 커널판이 다르면 다른 이미지이고, 커널판이 같아도 OS 가 다르면 또 다른 이미지다. 그래서 운영에서 다음 일들이 전부 "이미지 작업" 이 된다.

없으면 어떻게 드러날까. 그 노드에서만 드라이버 파드가 이미지를 받지 못해 죽는다. 다른 노드는 멀쩡하니 클러스터 차원의 경보는 울리지 않고, 그 노드의 GPU 만 조용히 빠진다. 문서는 지원되는 조합이 표로 정해져 있다는 것과, 목록에 없는 커널 변종을 쓰면 직접 이미지를 빌드해 자기 레지스트리에 올려야 한다는 것을 분명히 적어 두었다.

그래서 성숙한 팀은 "우리가 가진 드라이버 이미지 목록" 을 대장으로 들고 노드의 현재 조합과 주기적으로 대조한다. 커널 업그레이드 계획이 잡히면 그 대조를 먼저 돌려 빠진 조합을 미리 빌드한다. 이 순서를 지키지 않으면 노드를 비워 놓고 이미지를 기다리게 된다.

CUDA 는 앞으로만 호환된다

드라이버와 CUDA 런타임 사이에도 판의 문제가 있고, 방향이 한쪽으로만 열려 있다. 오래된 CUDA 컨테이너는 새 드라이버 위에서 잘 돈다. 반대는 안 된다. 드라이버는 자기보다 나중에 나온 CUDA 런타임을 알지 못하기 때문이다.

실무에서 이것이 뜻하는 바는 단순하다. 새 프레임워크 이미지를 쓰기 시작할 때는 클러스터에서 가장 낮은 드라이버 판이 그 이미지를 감당하는지부터 본다. 그리고 그 요구를 사람의 기억이 아니라 파드 스펙에 적어 둔다 — gpu-feature-discovery 가 붙여 주는 nvidia.com/cuda.driver.major 라벨에 nodeAffinity 조건을 걸면, 맞지 않는 노드로 가서 실행 중에 실패하는 대신 스케줄 단계에서 막힌다. 실패를 앞으로 당기는 것이 이 조건의 값어치다.

업그레이드가 멈추는 자리

드라이버를 올리려면 그 노드의 GPU 워크로드를 내려야 하고, 내리는 일은 eviction API 를 거친다. 그리고 eviction API 앞에는 PodDisruptionBudget 이 있다. 지금 살아 있는 수가 예산 아래로 내려가면 API 는 요청을 거절한다.

error when evicting pods/"trainer-..." (will retry after 5s):Cannot evict pod as it would violate the pod's disruption budget.

kubectl drain 은 노드를 먼저 cordon 한 다음 파드를 하나씩 evict 하므로, 막혀도 노드는 이미 스케줄 불가 상태다. 여기서 알아채지 못하면 그 노드는 새 파드를 안 받으면서 드라이버도 못 올리는 어정쩡한 상태로 남는다. --timeout 없이 돌린 drain 은 영원히 다시 시도하므로 화면만 보면 "느리다" 로 보인다.

GPU Operator 의 업그레이드 컨트롤러는 이 절차를 자동화하면서 진행 상태를 노드 라벨 nvidia.com/gpu-driver-upgrade-state 에 적는다. 문서가 정의한 상태는 이렇다.

| 상태 | 뜻 |
| --- | --- |
| upgrade-required | 드라이버 파드가 최신이 아니다 |
| cordon-required | 노드를 스케줄 불가로 표시할 차례다 |
| wait-for-jobs-required | 지정한 잡이 끝나기를 기다린다 |
| pod-deletion-required | GPU 를 할당받은 파드를 지운다 |
| drain-required | 파드 삭제로 부족해 노드를 드레인한다 |
| pod-restart-required | 드라이버 파드를 재시작해 새 판을 올린다 |
| validation-required | 새 드라이버를 검증한다 |
| uncordon-required | 노드를 다시 스케줄 가능으로 돌린다 |
| upgrade-done | 끝났다 |

이 라벨의 값어치는 상태 이름 자체가 아니라 어디서 멈췄는지 한 줄로 알 수 있다는 데 있다. cordon-required 에서 멈춘 노드와 pod-deletion-required 에서 멈춘 노드는 원인이 다르다. 노드 전체를 한 번에 뽑아 보면 어느 단계에서 얼마나 쌓였는지가 바로 보인다.

문서가 특히 강하게 경고하는 것이 drain.enable 이다. 기본값이 꺼짐인 이유가 있다 — 드레인은 GPU 와 무관한 워크로드까지 그 노드에서 전부 내쫓는다. 먼저 GPU 파드 삭제 설정을 조정하고, 그것으로 모자랄 때만 드레인을 켜되 podSelector 로 범위를 좁히라고 적혀 있다.

현장에서 만나는 모습

첫째, "한 노드만 GPU 가 안 보인다". 커널이 그 노드만 올라갔거나, 그 노드만 나중에 들어와 이미지가 다르거나. 확인은 그 노드의 커널 라벨과 드라이버 파드의 이미지 태그를 나란히 놓는 것으로 끝난다.

둘째, "업그레이드가 세 시간째 한 노드에서 멈춰 있다". 드라이버 로그를 아무리 읽어도 안 나오는 이유는 원인이 드라이버가 아니기 때문이다. PDB 가 eviction 을 거절하고 있거나, 컨트롤러가 기다리라고 지정한 잡이 끝나지 않고 있거나. 업그레이드 상태 라벨을 먼저 보면 3초에 끝난다.

셋째, 미리 설치된 드라이버. 이미지에 드라이버를 구워 두는 팀에서는 오퍼레이터가 드라이버를 관리하지 않는다. 문서도 호스트에 미리 설치된 드라이버의 수명 주기는 오퍼레이터가 관리하지 않는다고 적고 있다. 편해 보이지만 대신 판 관리가 통째로 사람의 몫이 되므로, 어느 쪽이 나은지는 팀의 이미지 파이프라인 성숙도에 달려 있다.

넷째, 이 환경의 정직한 한계. 실습에서는 드라이버를 실제로 설치하지 않는다. GPU 도 커널 모듈도 nvidia-smi 도 없다. 그래서 커널 판과 드라이버 판은 노드 라벨로 표현하고, 업그레이드는 그 라벨을 바꾸는 것으로 나타낸다 — 실제 클러스터에서도 스케줄러와 오퍼레이터가 보는 것은 결국 그 라벨이다. 반면 노드 추가, PodDisruptionBudget 의 판정, eviction 거부, drain 과 uncordon 은 진짜 컨트롤 플레인이 그대로 수행한다.

참고 문서

다음 실습에서 할 것

노드 라벨을 사실의 자리로 삼아, 노드가 필요로 하는 드라이버 이미지 태그를 계산하는 도구와 그것을 사내 이미지 목록과 대조하는 점검기를 만든다. 노드를 한 대 늘려 그 노드만 조합이 없다는 것을 점검기가 먼저 알려 주는 것을 확인하고, CUDA 요구를 nodeAffinity 로 적어 맞지 않는 요구가 스케줄 단계에서 막히는 것을 본다. 그리고 PodDisruptionBudget 이 드레인을 실제로 거절하는 자리를 지난 뒤, 이미지 확보부터 uncordon 까지 업그레이드 절차를 순서대로 끝까지 걸어 본다.