본문 바로가기
삵
AI

NIM Operator로 LLM 추론 선언형으로 배포하기

oopennova·2026년 11월 6일·조회 0

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까지 자동으로 늘어나는 걸 확인할 수 있다.

자주 묻는 질문

NIM Operator를 쓰려면 cert-manager를 꼭 설치해야 하나?

바닐라 쿠버네티스에서는 필요 없다. v3.1.x 차트의 admissionController.enabled 기본값이 false라 검증 웹훅이 꺼져 있고, 웹훅 없이도 NIMCache와 NIMService는 정상 생성된다. cert-manager는 operator.admissionController.enabled=true로 웹훅을 직접 켤 때만 요구된다. OpenShift는 v3.0.0부터 웹훅이 기본으로 켜지므로 이 경우 cert-manager가 필요하다.

NIMCache가 Ready로 안 가고 InProgress나 Failed에서 멈춘다.

먼저 storageClass가 ReadWriteMany를 지원하는지 본다. EBS gp3 같은 RWO 스토리지를 지정하면 PVC가 바운드되지 않는다. Failed로 떨어지면 kubectl logs로 모델 풀러 잡 로그를 확인한다. 대개 NGC API 키 오류나 모델 접근 권한 부족, 또는 PVC 용량 부족이 원인이다.

HPA의 TARGETS가 <unknown>으로 나온다.

type: Object나 External 같은 커스텀 메트릭은 쿠버네티스 기본 metrics-server로 못 읽는다. gpu_cache_usage_perc 같은 값을 쓰려면 Prometheus Adapter처럼 custom metrics API를 제공하는 어댑터가 있어야 한다. 어댑터 없이 쓸 거면 CPU 사용률 같은 Resource 메트릭으로 HPA를 잡는다.

HPA TARGETS가 퍼센트가 아니라 500m처럼 나오는데 정상인가?

정상이다. target.type을 Value로 둔 Object 메트릭은 퍼센트가 아니라 원시 수량으로 표시된다. 목표값 0.5가 500m으로 환산된 것이다. 퍼센트 표기(40%/70%)는 Resource 메트릭에 averageUtilization을 지정했을 때 나온다.

NIMService가 NIMCache 없이도 뜨나?

뜬다. storage.nimCache를 지정하지 않으면 파드가 기동할 때 NIM 이미지가 NGC에서 가중치를 직접 받는다. 다만 파드마다 수십 GB를 받아 콜드 스타트가 느려지므로, 스케일아웃을 고려한다면 NIMCache로 PVC에 미리 받아 공유하는 쪽을 권한다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.