GPU 노드를 운영하면 nvidia-smi는 매일 치게 된다. 그런데 그 뒤에 뭐가 있는지는 잘 안 들여다본다. 그러다 파드 안에서 Failed to initialize NVML 같은 에러를 처음 만나면 그제야 'NVML이 대체 뭐지' 하고 검색하게 된다. 예전에 쿠버네티스에서 GPU 한 장을 Time-Slicing과 MIG로 나눠 쓰는 글을 쓰면서도 nvidia-smi 출력을 계속 들여다봤다. 이번엔 그 명령 밑바닥에 있는 라이브러리 얘기를 해본다.
NVML(NVIDIA Management Library)은 NVIDIA GPU의 상태를 조회하고 일부 설정을 바꾸는 C 기반 API이고, nvidia-smi도 이 NVML 위에서 만들어진 도구다. 따로 설치하는 게 아니라 NVIDIA 드라이버에 포함돼 배포된다. Python에서는 NVIDIA 공식 바인딩인 nvidia-ml-py(import 이름 pynvml)로 호출한다. GPU 모니터링 익스포터나 쿠버네티스 GPU 관련 컴포넌트도 결국 이 라이브러리를 거쳐 GPU 정보를 얻는다.
1. 개요
NVML은 데이터센터 GPU의 상태를 모니터링하고 관리하기 위한 프로그래밍 인터페이스다. NVIDIA는 이것을 서드파티 애플리케이션을 만드는 플랫폼으로 내놨다. 온도, 사용률, 메모리, 전력, ECC 에러 같은 값을 프로그램에서 직접 읽을 수 있게 해주는 게 존재 이유다.
예를 들어 nvidia-smi를 한 번 치면 GPU 이름, 온도, 전력, 메모리 사용량, 실행 중인 프로세스가 화면에 뜬다. 이 값 하나하나가 NVML 함수 호출 결과다. 사람이 보라고 표로 찍어주는 게 nvidia-smi이고, 프로그램이 가져다 쓰라고 함수로 열어둔 게 NVML이다.
내 서버에 NVML이 있는지는 드라이버 라이브러리가 잡혀 있는지로 확인한다. 리눅스에선 공유 라이브러리 이름이 libnvidia-ml.so다.
NVML을 쓰는 방법은 크게 세 갈래다.
- nvidia-smi: NVML을 감싼 CLI. 사람이 보거나 셸 스크립트에서 쓴다.
- 언어 바인딩: C 헤더(
nvml.h)를 직접 쓰거나 Python의pynvml, Go의go-nvml같은 바인딩으로 호출한다. - NVML 위에 얹힌 도구: DCGM, dcgm-exporter처럼 이미 만들어진 수집기를 쓴다.
아래에서는 nvidia-smi와의 관계부터 조회와 제어가 가능한 항목, Python 호출 방법, 자주 보는 에러, 실무에서 NVML이 닿는 곳을 차례로 살펴본다.
2. nvidia-smi와 NVML의 관계
NVIDIA 공식 소개 페이지에 따르면 nvidia-smi는 NVML을 사용해서 만든 도구다. 그래서 nvidia-smi가 보여주는 값과 NVML로 직접 읽은 값은 같은 원천에서 나온다. nvidia-smi에서 보이는 값이면 NVML로도 읽을 수 있다.
일단 라이브러리 위치부터 확인해본다. Ubuntu 기준으로 이런 게 나온다.
$ ldconfig -p | grep nvidia-ml libnvidia-ml.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libnvidia-ml.so.1
배포판과 드라이버 설치 방식에 따라 경로는 달라질 수 있다. 여기서 아무것도 안 나오면 드라이버가 제대로 안 깔린 것이다. 이 상태에선 nvidia-smi도, NVML 기반 도구도 동작하지 않는다.
nvidia-smi를 스크립트에서 쓸 때는 기본 표 출력을 파싱하지 말고 --query-gpu로 필요한 필드만 CSV로 뽑는다. 표 모양은 드라이버 버전마다 바뀔 수 있다. 막상 awk로 잘라 쓰다 보면 드라이버 업그레이드 후에 스크립트가 조용히 깨지는 경우가 생긴다.
아래 출력의 숫자는 A10G 한 장에 작업이 돌고 있는 상황을 가정한 예시 값이다. 실제 수치는 GPU 모델과 부하, 드라이버에 따라 다르다.
$ nvidia-smi --query-gpu=index,name,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw --format=csv index, name, utilization.gpu [%], memory.used [MiB], memory.total [MiB], temperature.gpu, power.draw [W] 0, NVIDIA A10G, 67 %, 14872 MiB, 24564 MiB, 58, 118.42 W
사용 가능한 필드 이름 전체는 nvidia-smi --help-query-gpu로 확인할 수 있다. 이 필드들이 각각 NVML의 어떤 조회 함수에 대응하는지 알아두면 나중에 코드로 옮길 때 편하다.
3. NVML로 조회할 수 있는 것
공식 문서가 조회 가능한 상태로 꼽는 항목은 아래와 같다. 괄호 안은 NVML API 레퍼런스의 Device Queries 그룹에 있는 대표 함수 이름이다.
식별 정보
보드 시리얼 번호, PCI 디바이스 ID, VBIOS와 Inforom 버전, 제품명 같은 정보다. 여러 장이 꽂힌 서버에서 GPU를 고유하게 가리킬 때는 인덱스 대신 UUID(nvmlDeviceGetUUID)를 쓴다. 인덱스는 환경 변수나 컨테이너 설정에 따라 달라질 수 있지만 UUID는 바뀌지 않는다.
GPU 사용률
컴퓨트 자원과 메모리 인터페이스의 현재 사용률을 퍼센트로 준다(nvmlDeviceGetUtilizationRates). 여기서 메모리 사용률은 메모리 용량을 얼마나 채웠는지가 아니라 메모리 대역을 얼마나 바쁘게 쓰는지를 뜻한다.
용량은 별도로 nvmlDeviceGetMemoryInfo의 total, free, used로 본다. 처음엔 이 둘을 헷갈려서 대시보드에 엉뚱한 값을 걸어두는 경우가 자주 있다.
ECC 에러 카운트
ECC는 GPU 메모리의 비트 오류를 잡아내는 기능이다. NVML은 정정 가능한 단일 비트 에러와 감지만 되는 이중 비트 에러를 모두 보고한다. 현재 부팅 이후 기준과 GPU 수명 전체 기준을 둘 다 준다(nvmlDeviceGetTotalEccErrors). 이중 비트 에러가 올라가기 시작한 GPU는 하드웨어 점검 대상으로 본다.
온도와 팬 속도
GPU 코어 온도(nvmlDeviceGetTemperature, 섭씨)와 팬 속도(nvmlDeviceGetFanSpeed, 퍼센트)를 준다. 팬 속도는 팬이 달린 제품에서만 의미가 있다. 데이터센터용 패시브 쿨링 카드에서는 지원되지 않는다는 결과가 돌아올 수 있다.
전력
지원되는 제품에서 현재 보드 전력 사용량(nvmlDeviceGetPowerUsage)과 전력 한도를 준다. 전력 사용량 단위는 밀리와트다. 코드에서 1000으로 나누지 않고 그대로 찍으면 수만 와트짜리 GPU가 대시보드에 나타난다. 실제로 드라이버가 적용 중인 한도는 nvmlDeviceGetEnforcedPowerLimit으로 본다.
실행 중인 컴퓨트 프로세스
해당 GPU에서 돌고 있는 프로세스의 PID와 할당된 GPU 메모리를 준다(nvmlDeviceGetComputeRunningProcesses_v3). 컨테이너 환경에서는 PID 네임스페이스가 달라서 컨테이너 안에서 본 PID와 호스트 PID가 일치하지 않을 수 있다.
클럭과 PState
주요 클럭 도메인의 현재와 최대 클럭(nvmlDeviceGetClockInfo, MHz)을 준다. 현재 성능 상태인 PState(nvmlDeviceGetPerformanceState)도 읽을 수 있다. PState는 P0이 최고 성능이고 숫자가 커질수록 절전 쪽이다.
이 외에도 API 레퍼런스에는 Event Handling, Accounting Statistics, Field Value Queries, Multi Instance GPU Management(MIG), NvLink, vGPU, GPM 같은 기능 그룹이 있다. MIG 인스턴스를 만들고 조회하는 것도 NVML 기능 그룹 중 하나다. MIG 글에서 다룬 nvidia-smi mig 명령도 결국 이 API를 호출한다.
4. NVML로 바꿀 수 있는 것
조회만 되는 게 아니라 일부 설정은 바꿀 수도 있다. 변경 계열은 root 권한이 필요하고, 권한이 없으면 NVML_ERROR_NO_PERMISSION이 돌아온다. 아래는 nvidia-smi로 같은 동작을 하는 명령이다.
퍼시스턴스 모드
GPU를 쓰는 클라이언트가 하나도 없을 때도 드라이버를 로드된 상태로 유지할지 정한다. 꺼져 있으면 GPU를 처음 쓰는 순간마다 드라이버 초기화 비용이 붙는다. 그래서 nvidia-smi 한 번 치는 데도 몇 초씩 걸리는 증상이 생길 수 있다.
$ sudo nvidia-smi -pm 1 Enabled persistence mode for GPU 00000000:00:1E.0. All done.
이 설정은 재부팅하면 풀린다. 서버에선 드라이버와 함께 오는 nvidia-persistenced 데몬을 켜두는 쪽을 권한다.
$ sudo systemctl enable --now nvidia-persistenced $ systemctl is-active nvidia-persistenced active
컴퓨트 모드
컴퓨트 프로세스를 아예 막을지, 한 프로세스만 배타적으로 쓰게 할지, 여러 프로세스가 동시에 쓰게 할지 정한다.
$ sudo nvidia-smi -i 0 -c EXCLUSIVE_PROCESS Set compute mode to EXCLUSIVE_PROCESS for GPU 00000000:00:1E.0. All done.
쿠버네티스에서 Time-Slicing으로 GPU를 나눠 쓰려는 노드에 이 값이 EXCLUSIVE_PROCESS로 박혀 있으면 두 번째 파드부터 CUDA 초기화가 실패할 수 있다. 공유 설정이 안 먹는다 싶으면 nvidia-smi -q | grep -i "compute mode"로 먼저 확인한다.
ECC 모드와 ECC 카운트 리셋
ECC를 켜고 끄거나(nvidia-smi -e 1 / -e 0), 에러 카운트를 초기화(nvidia-smi -p 0는 volatile, -p 1은 aggregate)할 수 있다. ECC 모드 변경은 재부팅 후에 적용된다. 운영 GPU에서 ECC를 끌 이유는 거의 없으니 켜둔 채로 둔다.
5. Python에서 NVML 호출하기
스크립트나 간단한 에이전트를 만들 땐 nvidia-smi 출력을 subprocess로 긁지 말고 Python 바인딩으로 NVML을 직접 부른다. 프로세스를 매번 띄우지 않아도 되고, 값이 이미 숫자로 들어온다.
패키지 이름에서 한 번 헷갈린다. NVIDIA가 공식 배포하는 PyPI 패키지 이름은 nvidia-ml-py이고, 코드에서 import하는 모듈 이름은 pynvml이다. PyPI에는 pynvml이라는 이름의 별도 패키지도 있다. 그래서 검색 결과대로 pip install pynvml을 치면 공식 패키지가 아닌 쪽이 깔릴 수 있다. 새로 시작한다면 nvidia-ml-py를 설치한다.
$ python3 -m pip install nvidia-ml-py $ python3 -m pip show nvidia-ml-py | head -2 Name: nvidia-ml-py Version: 13.615.71
버전 번호는 설치 시점의 PyPI 상태에 따라 다를 수 있다. 바인딩은 드라이버에 들어 있는 libnvidia-ml.so를 불러다 쓰는 얇은 래퍼라서, 실제 기능은 설치된 드라이버 버전이 결정한다. 드라이버가 지원하지 않는 함수를 부르면 NVMLError_FunctionNotFound나 NVMLError_NotSupported가 난다.
호출은 nvmlInit()으로 초기화하고, 인덱스로 디바이스 핸들을 얻고, 핸들로 값을 조회한 뒤, 끝나면 nvmlShutdown()을 부르는 순서로 한다. init과 shutdown은 참조 카운트로 관리된다. init을 두 번 불렀으면 shutdown도 두 번 불러야 실제로 정리된다.
# gpu_status.py
import pynvml as nv
nv.nvmlInit()
try:
print('Driver:', nv.nvmlSystemGetDriverVersion())
for i in range(nv.nvmlDeviceGetCount()):
h = nv.nvmlDeviceGetHandleByIndex(i)
name = nv.nvmlDeviceGetName(h)
uuid = nv.nvmlDeviceGetUUID(h)
util = nv.nvmlDeviceGetUtilizationRates(h)
mem = nv.nvmlDeviceGetMemoryInfo(h)
temp = nv.nvmlDeviceGetTemperature(h, nv.NVML_TEMPERATURE_GPU)
power_w = nv.nvmlDeviceGetPowerUsage(h) / 1000 # mW -> W
print(f'[{i}] {name} {uuid}')
print(f' util gpu={util.gpu}% mem_bw={util.memory}%')
print(f' mem used={mem.used // 2**20}MiB / {mem.total // 2**20}MiB')
print(f' temp={temp}C power={power_w:.1f}W')
for p in nv.nvmlDeviceGetComputeRunningProcesses(h):
used = p.usedGpuMemory // 2**20 if p.usedGpuMemory else 0
print(f' pid={p.pid} gpu_mem={used}MiB')
finally:
nv.nvmlShutdown()
돌려보면 이런 모양이 나온다. 아래 드라이버 버전, UUID, PID, 수치는 A10G 한 장을 기준으로 채운 예시 값이고 이 환경에서 측정한 결과가 아니다. 실제 값은 GPU 모델과 부하, 설치된 드라이버에 따라 다르다.
$ python3 gpu_status.py
Driver: 535.183.01
[0] NVIDIA A10G GPU-3f2a9c1e-7b4d-5e8a-9c0f-1d2e3b4a5c6d
util gpu=67% mem_bw=41%
mem used=14872MiB / 24564MiB
temp=58C power=118.4W
pid=23817 gpu_mem=14610MiB
여기서 한 번 걸리는 부분이 usedGpuMemory다. 권한이나 환경에 따라 프로세스별 메모리를 못 읽으면 값이 None으로 들어올 수 있다. 위처럼 방어 코드를 넣어두지 않으면 TypeError로 스크립트가 죽는다.
nvmlDeviceGetName도 조심할 부분이 있다. 예전 바인딩 버전에서는 str이 아니라 bytes를 돌려줬기 때문에, 오래된 예제를 복사하면 .decode()가 붙어 있을 수 있다.
6. 다른 언어에서 쓰기
원본은 C API라서 C/C++에서는 nvml.h를 include하고 -lnvidia-ml로 링크한다. 헤더는 CUDA Toolkit에 들어 있다.
$ gcc gpu_status.c -o gpu_status -I/usr/local/cuda/include -lnvidia-ml
C에서는 모든 함수가 nvmlReturn_t를 돌려주고 실제 값은 포인터 인자로 받는다. 반환값이 NVML_SUCCESS인지 매번 확인해야 하고, nvmlErrorString()으로 사람이 읽을 메시지를 얻는다.
Go에서는 NVIDIA가 관리하는 github.com/NVIDIA/go-nvml 바인딩을 쓴다. 쿠버네티스 쪽 NVIDIA 컴포넌트들이 Go로 작성돼 있어서, 그쪽 코드를 읽다 보면 이 패키지를 자주 마주친다.
Java나 Node.js에는 NVIDIA 공식 바인딩이 없다. 이런 언어에서는 NVML을 직접 붙이지 말고 dcgm-exporter 같은 수집기의 메트릭을 가져다 쓰기를 권한다.
7. 자주 만나는 NVML 에러
Driver/library version mismatch
$ nvidia-smi Failed to initialize NVML: Driver/library version mismatch
패키지 업데이트로 사용자 공간의 libnvidia-ml.so는 새 버전이 됐는데, 커널에 올라간 드라이버 모듈은 아직 옛 버전일 때 난다. unattended-upgrades가 드라이버를 올려버린 서버에서 흔히 본다. 로드된 커널 모듈 버전은 이렇게 확인한다.
$ cat /proc/driver/nvidia/version
재부팅하는 게 가장 확실하다. 재부팅이 어렵다면 GPU를 쓰는 프로세스를 모두 내리고 nvidia 커널 모듈을 언로드했다가 다시 올리는 방법도 있다. 다만 쿠버네티스 노드라면 cordon과 drain 후 재부팅하는 쪽을 권한다. 이 에러가 반복된다면 드라이버 패키지를 자동 업데이트 대상에서 빼둔다.
컨테이너 안에서 NVML Shared Library Not Found
pynvml.NVMLError_LibraryNotFound: NVML Shared Library Not Found
컨테이너 이미지에는 드라이버가 들어 있지 않다. libnvidia-ml.so는 NVIDIA Container Toolkit이 컨테이너를 띄울 때 호스트에서 마운트해준다. 그래서 GPU 런타임 없이 띄운 컨테이너나 GPU 리소스를 요청하지 않은 파드에서는 pynvml이 라이브러리를 못 찾는다.
Docker라면 --gpus 옵션을 줬는지, 쿠버네티스라면 파드 spec에 nvidia.com/gpu 리소스를 요청했는지부터 본다.
권한 부족
조회는 일반 사용자로도 대부분 된다. 하지만 퍼시스턴스 모드나 컴퓨트 모드, 전력 한도 변경 같은 제어 계열은 root가 아니면 NVML_ERROR_NO_PERMISSION(Python에서는 NVMLError_NoPermission)이 난다. 모니터링 에이전트에는 조회 권한만 주고, 설정 변경은 프로비저닝 단계에서 root로 처리하도록 나누는 게 깔끔하다.
8. 실무에서 NVML이 닿는 곳
직접 NVML 코드를 짜지 않더라도 GPU 운영 도구 대부분이 이 라이브러리를 밟고 있다. 어디서 NVML을 쓰는지 알아두면 문제가 났을 때 어느 층을 봐야 하는지 좁히기 쉽다.
DCGM과 dcgm-exporter
DCGM(Data Center GPU Manager)은 NVIDIA가 만든 GPU 클러스터 관리 도구다. DCGM 공식 문서에 따르면 DCGM 라이브러리는 NVIDIA 드라이버, NVML, CUDA Toolkit 위에 올라가 있다. 헬스 체크, 진단, 정책 같은 기능을 NVML 위에 더 얹은 층이다.
dcgm-exporter는 DCGM에서 GPU 메트릭을 받아 Prometheus 텍스트 포맷으로 내보낸다. 단독으로 띄워보면 9400 포트에 메트릭이 뜬다. 아래 UUID와 사용률 값은 앞의 예시와 맞춘 예시 값이며, 실제로는 GPU와 부하에 따라 다르게 나온다.
$ docker run -d --gpus all --cap-add SYS_ADMIN --rm -p 9400:9400 nvcr.io/nvidia/k8s/dcgm-exporter:<tag>
$ curl -s localhost:9400/metrics | grep DCGM_FI_DEV_GPU_UTIL
# HELP DCGM_FI_DEV_GPU_UTIL GPU utilization (in %).
# TYPE DCGM_FI_DEV_GPU_UTIL gauge
DCGM_FI_DEV_GPU_UTIL{gpu="0",UUID="GPU-3f2a9c1e-7b4d-5e8a-9c0f-1d2e3b4a5c6d",...} 67
이미지 태그는 NGC 카탈로그에서 드라이버에 맞는 것을 고른다. 쿠버네티스에서는 NVIDIA GPU Operator가 dcgm-exporter를 DaemonSet으로 같이 깔아준다.
쿠버네티스 GPU 노드
NVIDIA device plugin은 노드에 GPU가 몇 장 있는지, MIG 인스턴스가 어떻게 나뉘어 있는지를 파악해 kubelet에 nvidia.com/gpu 같은 리소스로 등록한다. 이 GPU 정보를 얻는 부분이 NVML이다.
그래서 노드에서 nvidia-smi가 실패하는 상태라면 device plugin도 GPU를 못 본다. 이 경우 노드의 allocatable에서 GPU가 0으로 보이는 증상으로 나타날 수 있다.
$ kubectl describe node <gpu-node> | grep -A2 -i allocatable $ kubectl -n gpu-operator logs ds/nvidia-device-plugin-daemonset | grep -i nvml
네임스페이스와 DaemonSet 이름은 설치 방식에 따라 다르다. 파드가 Pending에 걸려 GPU를 못 받는다면 스케줄러보다 먼저 노드에서 nvidia-smi가 되는지를 본다. 현장에서 이렇게 한 층씩 내려가 보면 원인이 NVML 초기화 실패, 즉 드라이버 문제로 드러나는 경우가 적지 않다.
무엇을 쓸지
- 사람이 한 번 보는 용도:
nvidia-smi. 스크립트라면--query-gpu --format=csv로만 쓴다. - 짧은 에이전트, 배치 작업 안 자가 점검, 커스텀 로직:
nvidia-ml-py로 NVML을 직접 호출한다. - 운영 모니터링과 알람: 직접 짜지 말고 dcgm-exporter와 Prometheus, Grafana 조합을 쓴다. 메트릭 이름과 라벨이 정리돼 있고 MIG 단위 메트릭도 나온다.
9. 정리
NVML은 NVIDIA 드라이버에 들어 있는 GPU 관리용 C 라이브러리다. nvidia-smi, DCGM, dcgm-exporter, 쿠버네티스 device plugin이 모두 그 위에서 GPU 상태를 읽는다. 사용률, 메모리, 온도, 전력, ECC, 프로세스, 클럭을 조회할 수 있고 퍼시스턴스 모드, 컴퓨트 모드, ECC 설정을 바꿀 수 있다.
Python에서는 nvidia-ml-py를 설치해 pynvml로 import하고, init, 핸들 조회, shutdown 순서로 쓴다. Failed to initialize NVML 계열 에러가 보이면 애플리케이션보다 드라이버와 컨테이너 런타임 쪽을 먼저 의심한다.