GPU 노드에 LLM 추론을 올릴 때 Deployment와 Service, PVC, ConfigMap, HPA를 손으로 엮다 보면 모델 하나 올리는 데 매니페스트가 대여섯 개로 불어난다. 모델 가중치를 어디에 캐싱할지, 헬스 프로브 경로를 뭘로 잡을지 매번 똑같은 고민을 반복하게 된다. NVIDIA가 내놓은 NIM Operator는 이걸 CRD 두 개로 줄여 준다.
정리하자면, NIM Operator를 설치하면 NIMCache와 NIMService라는 커스텀 리소스가 생긴다. NIMCache로 모델 아티팩트를 PVC에 미리 받아 두고, NIMService로 그 캐시를 참조해 추론 서버를 올리면 오토스케일과 헬스 프로브는 오퍼레이터가 알아서 붙여 준다. cert-manager는 오퍼레이터 구동에 꼭 필요한 게 아니라, 검증 웹훅(admission webhook)을 켤 때만 요구된다.
1. 선언형으로 모델을 올린다는 것
NIM은 NVIDIA Inference Microservice의 약자로, 모델 가중치와 추론 엔진(TensorRT-LLM 또는 vLLM), OpenAI 호환 API 서버를 한 컨테이너 이미지에 담아 배포하는 패키징 방식이다. 이미지를 받아서 docker run만 하면 /v1/chat/completions가 뜨는 구조다.
NIM Operator는 그 NIM을 쿠버네티스에서 선언형으로 다루게 해 주는 오퍼레이터다. 직접 Deployment를 쓰는 대신 NIMService 리소스 하나를 kubectl apply 하면, 오퍼레이터 컨트롤러가 그에 맞는 Deployment, Service, 그리고 필요하면 HPA까지 만들어 조정한다.
핵심은 모델 캐싱을 NIMCache로 떼어 낸 부분이다. NIM 이미지는 가중치를 런타임에 NGC에서 내려받는데, 파드가 뜰 때마다 수십 GB를 받으면 스케일아웃이 느려진다. NIMCache는 모델 아티팩트를 ReadWriteMany PVC에 한 번만 받아 두고, 여러 NIMService 파드가 그 PVC를 공유해 콜드 스타트를 줄이는 역할을 한다. 두 리소스 모두 API 그룹은 apps.nvidia.com/v1alpha1이다.
2. 오퍼레이터 설치와 cert-manager의 자리
Helm으로 설치한다. v3.1.x 기준 명령은 다음과 같다. 버전 번호는 저장소 상태에 따라 다를 수 있다.
$ helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update
$ kubectl create namespace nim-operator
$ helm upgrade --install nim-operator nvidia/k8s-nim-operator \
-n nim-operator --version=3.1.2
$ kubectl get pods -n nim-operator
NAME READY STATUS RESTARTS AGE
nim-operator-k8s-nim-operator-7c9d4b6f9c-x2k7m 2/2 Running 0 40s
여기서 cert-manager를 먼저 깔아야 하는지 헷갈리기 쉽다. 결론부터 말하자면 바닐라 쿠버네티스에서는 필요 없다. NIM Operator의 검증 웹훅은 OpenShift에서만 v3.0.0부터 기본으로 켜지고, 그 밖의 플랫폼에서는 꺼져 있다. v3.1.x 차트의 admissionController.enabled 기본값이 false라서 그렇다.
웹훅을 끈 상태로도 NIMCache와 NIMService는 정상적으로 생성되고 조정된다. cert-manager가 등장하는 건 웹훅을 직접 켤 때뿐이다. 매니페스트 유효성 검증을 어드미션 단계에서 받고 싶다면 cert-manager를 먼저 설치하고 다음처럼 켠다.
$ helm upgrade --install nim-operator nvidia/k8s-nim-operator \
-n nim-operator --version=3.1.2 \
--set operator.admissionController.enabled=true
cert-manager 없이 이 옵션만 켜면 웹훅 서버 TLS 인증서가 발급되지 않아 리소스 생성이 거부된다. 반대로 말하면, OpenShift가 아닌데 cert-manager를 깔 생각이 없다면 이 옵션은 그냥 두면 된다.
모델을 받으려면 NGC 인증 시크릿 두 개가 작업 네임스페이스에 있어야 한다. 하나는 nvcr.io에서 이미지를 당기는 도커 레지스트리 시크릿, 다른 하나는 가중치를 받을 때 쓰는 API 키 시크릿이다.
$ kubectl create namespace nim-service
$ kubectl create secret docker-registry ngc-secret \
--docker-server=nvcr.io \
--docker-username='$oauthtoken' \
--docker-password=$NGC_API_KEY \
-n nim-service
$ kubectl create secret generic ngc-api-secret \
--from-literal=NGC_API_KEY=$NGC_API_KEY \
-n nim-service
3. NIMCache로 모델을 먼저 받아 둔다
Llama 3.1 8B Instruct를 TensorRT-LLM 엔진으로 받는 NIMCache 예시다. modelPuller는 가중치를 내려받는 전용 이미지이고, storage.pvc.create: true로 두면 오퍼레이터가 PVC를 직접 만든다.
apiVersion: apps.nvidia.com/v1alpha1
kind: NIMCache
metadata:
name: meta-llama3-8b-instruct
namespace: nim-service
spec:
source:
ngc:
modelPuller: nvcr.io/nim/meta/llama-3.1-8b-instruct:1.3.3
pullSecret: ngc-secret
authSecret: ngc-api-secret
model:
engine: tensorrt_llm
tensorParallelism: "1"
storage:
pvc:
create: true
storageClass: nfs-client
size: "50Gi"
volumeAccessMode: ReadWriteMany
resources: {}
volumeAccessMode를 ReadWriteMany로 둔 이유는 여러 추론 파드가 같은 PVC를 동시에 마운트하기 때문이다. 그래서 storageClass는 RWX를 지원하는 프로비저너여야 한다. EBS gp3 같은 RWO 전용 스토리지를 지정하면 PVC가 바운드되지 않고 NIMCache가 멈춘다. EKS면 EFS CSI, 온프렘이면 NFS 계열을 쓴다.
적용한 뒤 상태를 본다. 상태는 Pending에서 Started, InProgress를 거쳐 Ready로 간다. 50GB를 받는 동안 꽤 걸리므로 Ready가 뜰 때까지 기다린다.
$ kubectl get nimcache -n nim-service NAME STATUS PVC AGE meta-llama3-8b-instruct InProgress 2m # 몇 분 뒤 $ kubectl get nimcache -n nim-service NAME STATUS PVC AGE meta-llama3-8b-instruct Ready meta-llama3-8b-instruct-pvc 12m
여기서 한 번 막히기 쉬운 지점이 인증이다. InProgress에서 Failed로 떨어지면 먼저 모델 풀러 잡의 로그를 본다. kubectl logs job/<...> -n nim-service로 보면 대개 NGC API 키가 틀렸거나 해당 모델 접근 권한이 없다는 메시지가 찍힌다.
4. NIMService로 서빙과 오토스케일 올리기
캐시가 Ready면 NIMService를 올린다. storage.nimCache.name으로 앞서 만든 NIMCache를 참조하면, 파드가 그 PVC를 마운트해 가중치를 다시 받지 않는다.
apiVersion: apps.nvidia.com/v1alpha1
kind: NIMService
metadata:
name: meta-llama3-8b-instruct
namespace: nim-service
spec:
image:
repository: nvcr.io/nim/meta/llama-3.1-8b-instruct
tag: "1.3.3"
pullPolicy: IfNotPresent
pullSecrets:
- ngc-secret
authSecret: ngc-api-secret
storage:
nimCache:
name: meta-llama3-8b-instruct
profile: ''
replicas: 1
resources:
limits:
nvidia.com/gpu: 1
expose:
service:
type: ClusterIP
port: 8000
scale:
enabled: true
hpa:
minReplicas: 1
maxReplicas: 3
metrics:
- type: Object
object:
metric:
name: gpu_cache_usage_perc
describedObject:
apiVersion: v1
kind: Service
name: meta-llama3-8b-instruct
target:
type: Value
value: "0.5"
헬스 프로브는 따로 적지 않았는데, 오퍼레이터가 기본값으로 세 개를 붙인다. 리브니스와 레디니스는 각각 /v1/health/live와 /v1/health/ready를 15초 지연 뒤 HTTP GET으로 찌르고, 스타트업 프로브는 /v1/health/ready를 30초 지연에 실패 임계 120회로 잡아 모델 로딩이 20분 가까이 걸려도 버티게 한다. 큰 모델이 뜨는 동안 레디니스가 먼저 실패해 파드가 재시작되는 사고를 이 스타트업 프로브가 막아 준다.
적용하고 상태를 본다.
$ kubectl apply -n nim-service -f nimservice.yaml $ kubectl get nimservice -n nim-service NAME STATUS AGE meta-llama3-8b-instruct Ready 8m $ kubectl get deploy,svc -n nim-service NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/meta-llama3-8b-instruct 1/1 1 1 8m NAME TYPE CLUSTER-IP PORT(S) AGE service/meta-llama3-8b-instruct ClusterIP 10.100.42.17 8000/TCP 8m
HPA도 같이 만들어진다. 다만 위 매니페스트처럼 type: Object에 target.type: Value로 잡은 커스텀 메트릭은 쿠버네티스 기본 메트릭 서버로는 못 읽는다. gpu_cache_usage_perc 같은 값을 HPA가 쓰려면 Prometheus Adapter처럼 custom metrics API를 제공하는 어댑터가 클러스터에 있어야 한다.
$ kubectl get hpa -n nim-service NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE meta-llama3-8b-instruct Deployment/meta-llama3-8b-instruct <unknown>/500m 1 3 1 8m
어댑터가 없으면 TARGETS가 <unknown>/500m으로 남고 스케일이 동작하지 않는다. Value 타입 Object 메트릭이라 퍼센트가 아니라 500m처럼 원시 수량으로 표시되는 점도 같이 봐 둔다. 목표값 0.5가 500m으로 환산된 것이다. 어댑터를 붙여 메트릭이 들어오기 시작하면 앞자리가 300m/500m처럼 실제 값으로 바뀐다.
어댑터를 당장 붙이기 어렵다면 GPU 전용 커스텀 메트릭 대신 CPU 사용률 같은 Resource 메트릭으로 HPA를 잡는 방법도 있다. Resource 메트릭은 metrics-server만 있으면 되고, 이 경우 TARGETS가 40%/70% 같은 퍼센트로 표시된다. LLM 추론 부하를 CPU로 재는 게 정확하진 않지만, 메트릭 파이프라인을 갖추기 전 임시로는 쓸 만하다.
마지막으로 추론이 실제로 뜨는지 확인한다. 서비스로 포트포워딩해서 OpenAI 호환 엔드포인트를 때려 본다.
$ kubectl port-forward -n nim-service svc/meta-llama3-8b-instruct 8000:8000 &
$ curl -s http://localhost:8000/v1/models | head
{"object":"list","data":[{"id":"meta/llama-3.1-8b-instruct","object":"model", ...
여기까지 오면 모델 캐싱과 서빙, 헬스 프로브는 선언형으로 끝난 셈이다. 다음으로 손댈 곳은 HPA가 실제로 스케일하도록 만드는 메트릭 파이프라인이다. GPU KV 캐시 사용률이나 큐 대기 요청 수를 Prometheus로 긁어 custom metrics API로 노출하는 작업을 이어서 해 두면, maxReplicas까지 자동으로 늘어나는 걸 확인할 수 있다.