요약

SUMMARY

한 물리 서버의 GPU를 컨소시엄사와 나눠 써야 했다. 그쪽이 사용할 GPU는 쿠버네티스가 아예 인식하지 못하게 감추고, 우리 몫만 스케줄링에 올리고 싶었다. gpu-operator가 그 노드에는 device-plugin을 배포하지 않도록 라벨로 막고, NVIDIA_VISIBLE_DEVICES로 노출할 GPU만 고정한 자체 device-plugin을 그 노드에만 따로 띄워서 해결했다. time-slicing으로 장치를 선택하는 직접적인 방법은 아직 device-plugin이 지원하지 않아 불가능했다.

1. 환경

  • Kubernetes 클러스터, containerd 런타임 (기본 런타임이 nvidia)
  • NVIDIA GPU Operator로 device-plugin·드라이버·container-toolkit 관리
  • GPU가 여러 장 달린 워커 노드 한 대(이하 gpu-node-01)

2. 왜 특정 GPU만 숨겼는가

컨소시엄사와 물리 서버 한 대의 GPU를 나눠 쓰기로 했다. 조건이 다소 까다로웠는데, 그쪽이 사용할 GPU는 우리 쿠버네티스가 개입하지도, 스케줄링에 올리지도 말아야 했다. 노드 자체는 우리 클러스터의 워커로 남아 있어야 하므로 노드를 통째로 빼는 것은 해답이 아니었다. 노드는 클러스터에 두되, 그 노드에 장착된 GPU 중 일부만 골라 k8s의 인식 대상에서 제거해야 했다.

요구는 이렇다.

  • gpu-node-01은 계속 클러스터 워커로 둔다.
  • 이 노드의 GPU 중 우리 몫(예: 0번)만 k8s가 스케줄 가능한 자원으로 본다.
  • 나머지 GPU는 k8s가 존재조차 모르게 한다. 컨소시엄사가 호스트에서 직접 쓴다.

3. GPU가 k8s에 노출되는 경로부터

숨기려면 어디서 노출되는지부터 알아야 했다. 쿠버네티스가 GPU를 자원으로 인식하는 통로는 NVIDIA device-plugin이다. 이 컴포넌트가 노드의 GPU를 조회하여 nvidia.com/gpu라는 확장 자원(Extended Resource)으로 kubelet에 광고한다. gpu-operator를 사용하면 device-plugin이 DaemonSet으로 모든 GPU 노드에 설치되고, 노드에 장착된 GPU 전부를 집계하여 등록한다. 여기서 “전부”가 문제였다. 컨소시엄사 몫까지 집계해버리기 때문이다.

그러므로 device-plugin이 집계하는 GPU 목록 자체를 좁히면 되겠다는 것이 방향이었다. device-plugin이 어떤 GPU를 볼지는 NVIDIA_VISIBLE_DEVICES 환경변수가 결정한다. 이 값에 GPU 인덱스나 UUID를 넣으면 그 GPU만 보이고, 넣지 않은 GPU는 컨테이너 입장에서 존재하지 않게 된다. gpu-operator가 기본으로 띄우는 device-plugin은 이 값이 all이라 전부 본다.

NOTE

NVIDIA_VISIBLE_DEVICES는 nvidia-container-toolkit(정확히는 컨테이너 런타임 훅)이 읽어서 컨테이너에 어떤 /dev/nvidia* 장치를 붙일지 정하는 값이다. all, none, 혹은 0, GPU-<uuid> 같은 목록을 받는다. device-plugin도 결국 이 규칙 위에서 동작하는 컨테이너이므로, 여기에 우리 몫만 기재하면 나머지는 device-plugin에게도 보이지 않는다.

4. 안 먹힌 길: time-slicing으로 장치 골라내기

처음에는 device-plugin의 time-slicing 설정으로 풀어보려 했다. time-slicing은 원래 GPU 한 장을 여러 개의 가상 슬롯으로 분할하여 여러 파드가 나눠 쓰게 하는 기능인데, 설정에 devices 필드가 있어서 “이 GPU들만 대상으로” 지정할 수 있는 것처럼 보였다. 그 필드에 우리 몫 GPU만 기재하면 나머지는 자연스럽게 제외되지 않을까 기대했다.

결론부터 말하면 안 됐다. 설정을 넣고 device-plugin 로그를 보니 이렇게 뱉고 있었다.

# 특정 device만 골라 time-slicing에 태우려고 devices 필드를 넣었더니,
# 지원하지 않는 필드라며 통째로 무시한다는 로그.
Customizing the 'devices' field in sharing.timeSlicing.resources is not yet supported in the config. Ignoring

devices 필드 커스터마이징을 아직 지원하지 않으니 무시한다는 뜻이다. GPT에게 물어봤을 때는 가능하다고 확신 있게 알려주었지만, 실제로는 할루시네이션이었던 셈이다. 나중에 찾아보니 device-plugin에 장치를 선택·제외하는 필터 기능을 추가하자는 PR(#1189)이 올라와 있었고, 딱 이 요구(“k8s 밖 용도로 일부 GPU를 예약하고 싶다”)를 겨냥한 것이었다. 그런데 병합되지 못하고 오래 방치되다 닫혀 있었다. 즉, 내가 원하던 직접적인 방법은 아직 기능 자체가 없는 상태였다.

5. 자체 device-plugin을 그 노드에만 따로 띄우기

기능이 없으면 우회한다. device-plugin이 집계하는 목록을 좁히는 핵심이 결국 NVIDIA_VISIBLE_DEVICES이므로, gpu-operator가 관리하는 전체 device-plugin은 그대로 두고 그 노드에만 내가 만든 device-plugin을 따로 배치하는 우회 방법을 택했다. 절차는 두 단계다.

첫째, gpu-operator가 이 노드에는 device-plugin을 설치하지 않도록 막는다. gpu-operator는 노드 라벨을 참조하여 어떤 컴포넌트를 배포할지 결정한다. nvidia.com/gpu.deploy.device-plugin=false 라벨을 설정하면 그 노드에서 operator의 device-plugin이 제외된다.

# gpu-node-01에서 operator가 관리하는 device-plugin을 내린다.
kubectl label node gpu-node-01 nvidia.com/gpu.deploy.device-plugin=false --overwrite
 
# 되돌릴 땐 라벨을 지운다(뒤의 '-'가 삭제).
kubectl label node gpu-node-01 nvidia.com/gpu.deploy.device-plugin-

둘째, 노출할 GPU만 고정한 device-plugin을 그 노드에 직접 띄운다. 이 파드에 NVIDIA_VISIBLE_DEVICES로 우리 몫 GPU만 지정한다. 그러면 이 device-plugin은 해당 GPU만 보고 그것만 nvidia.com/gpu로 광고한다. 나머지는 애초에 인식 범위에 들어오지 않으므로 k8s 자원 목록에서 사라진다. nodeSelectorgpu-node-01에만 스케줄링되게 하고, 권한은 최소로 유지했다.

# gpu-node-01 전용 device-plugin. NVIDIA_VISIBLE_DEVICES로
# k8s에 노출할 GPU만 못박고, 그 외 GPU는 광고하지 않는다.
spec:
  nodeSelector:
    kubernetes.io/hostname: gpu-node-01
  containers:
    - name: nvidia-device-plugin
      image: <nvidia device-plugin image>
      securityContext:
        privileged: false          # 전체 GPU를 긁어오는 특권 모드가 아니라
      env:
        - name: NVIDIA_VISIBLE_DEVICES
          value: "0"               # 우리 몫 GPU만. 나머지는 여기 없으니 안 보인다
        - name: NVIDIA_DRIVER_CAPABILITIES
          value: "all"

privileged: false가 핵심이다. 특권 모드로 띄우면 NVIDIA_VISIBLE_DEVICES와 무관하게 노드의 GPU를 전부 장악해버려서 감추는 의미가 없어진다. 권한을 낮춰야 NVIDIA_VISIBLE_DEVICES 목록이 실제로 경계 역할을 수행한다.

이렇게 구성하니 kubectl describe node gpu-node-01Allocatablenvidia.com/gpu가 우리 몫만큼만 표시되었다. 컨소시엄사 GPU는 k8s 어디에도 나타나지 않았고, 그쪽은 호스트에서 그대로 사용하면 되었다.

INFO

노출 GPU를 지정할 때 인덱스(0)보다 UUID(GPU-<uuid>)가 안전하다. 인덱스는 재부팅이나 드라이버 재로드 시점에 순서가 바뀔 수 있어서, “0번을 노출한다”라는 설정이 어느 순간 다른 물리 GPU를 가리킬 위험이 있다. 노출 대상이 뒤바뀌면 감추려던 GPU가 열리는 사고가 발생하므로, 고정하려는 GPU의 UUID로 명시해두는 편이 안전하다.

6. 남은 구멍: 파드가 직접 GPU를 훔쳐가는 문제

여기까지 구성하면 스케줄러 관점에서는 감춰진다. 그런데 확인 과정에서 찝찝한 점을 발견했다. 우리 클러스터는 containerd의 기본 런타임이 nvidia였다. 이 상태에서는 파드가 nvidia.com/gpu를 요청(request)하지 않아도, 파드 명세에 NVIDIA_VISIBLE_DEVICES 환경변수만 직접 넣으면 GPU를 그냥 가져다 쓸 수 있다. device-plugin을 거치지 않고 런타임 훅이 곧장 장치를 붙여주기 때문이다. 감춘 GPU라도 인덱스나 UUID만 알면 파드가 몰래 사용할 수 있다는 뜻이다.

이건 스케줄링을 아무리 막아도 안 닫히는 구멍이라, 막으려면 결이 다른 통제가 필요하다.

  • nvidia-container-toolkit 설정: /etc/nvidia-container-runtime/config.tomlaccept-nvidia-visible-devices-envvar-when-unprivileged를 비활성화하여, 비특권 컨테이너가 NVIDIA_VISIBLE_DEVICES 환경변수로 GPU를 확보하는 행위를 차단한다. (여기까지 확인만 했고 운영 반영 전 테스트는 더 필요하다.)
  • RuntimeClass 분리: 기본 런타임을 nvidia로 두지 말고, GPU가 필요한 파드만 명시적으로 nvidia RuntimeClass를 쓰게 한다.
  • Admission Controller / ResourceQuota: NVIDIA_VISIBLE_DEVICES 같은 위험한 환경변수를 직접 설정한 파드를 정책으로 걸러내거나, GPU 자원 총량을 네임스페이스 단위로 제한한다.

한 가지 더 시행착오를 겪었던 부분이 있다. 감춘 GPU를 누가 몰래 사용하는지 확인하려고 파드 안에서 nvidia-smi를 실행했는데 프로세스 목록이 비어 나왔다. hostPID: true를 설정해야 호스트의 GPU 프로세스가 제대로 보였다. 다만 이 옵션은 파드의 프로세스 격리를 약화시키고 보안 위험을 만드는 설정이므로, 잠깐 진단할 때만 켰다가 바로 껐다. 상시로 활성화해둘 옵션은 아니다.

참고