포스트

2026년 7월, Kubernetes에서 LLM 서빙을 “GPU 오토스케일”로 굴리는 현실적인 방법: KEDA + (Queue Depth / KV Cache) + DCGM/NVML

2026년 7월, Kubernetes에서 LLM 서빙을 “GPU 오토스케일”로 굴리는 현실적인 방법: KEDA + (Queue Depth / KV Cache) + DCGM/NVML

들어가며

LLM inference를 Kubernetes 위에 올리면 금방 마주치는 문제가 있습니다. HPA는 CPU/Memory 위주로 설계되어 있어서, 실제 병목인 GPU pressure(Compute, VRAM, KV cache, queueing)를 제대로 반영하지 못합니다. 결과는 뻔합니다:

  • 트래픽이 늘었는데도 replica가 늦게 늘어 P95 latency/SLO가 터지거나
  • 반대로 GPU utilization 같은 “그럴듯한” 지표에 매달리다가 불필요하게 과스케일해서 비용이 폭증합니다(특히 vLLM 계열). (kubenatives.com)

언제 쓰면 좋은가

  • vLLM/Triton 같은 GPU inference를 운영하며, 요청량 변동이 크고(주간/야간, 배치/실시간 혼재) scale-to-zero 또는 빠른 scale-out이 필요할 때
  • “GPU 사용률”이 아니라 queue depth / in-flight requests / KV cache처럼 서빙 병목을 직접 나타내는 지표로 스케일하고 싶을 때 (kserve.github.io)

언제 쓰면 안 되는가

  • 모델 로딩이 5~10분 이상이고, 워밍업/캐시가 핵심이라서 스케일 이벤트 자체가 장애가 되는 워크로드(이 경우는 “오토스케일”보다 “warm pool + 예약 용량”이 먼저입니다)
  • 트래픽이 일정하고(24/7 steady) GPU가 항상 포화라서 스케일링보다 배치/배포 전략(모델 샤딩, 텐서 병렬, MIG, 라우팅)이 본질인 경우

🔧 핵심 개념

1) 왜 “GPU utilization 기반 오토스케일”이 자주 망하나

2026년에도 현업에서 반복되는 교훈은 비슷합니다: vLLM 같은 LLM 서빙에서는 GPU utilization이 ‘수요’를 잘 표현하지 못할 때가 많다는 점입니다. 예를 들어,

  • 단일 긴 generation이 GPU를 오래 붙잡아 utilization이 높아져도, 실제로는 “대기열”이 없을 수 있고
  • 커널/배치 설정 문제로 utilization이 높게 보이지만 throughput이 안 나오는 “나쁜 포화”가 생기면, utilization 스케일은 문제 복제(Replica만 증가)를 합니다. (reddit.com)

그래서 최근 실무 가이드는 queue depth / waiting requests / KV cache 사용률처럼 “서빙 병목”과 더 직결된 지표를 추천합니다. (zartis.com)

2) 2026년 주류 아키텍처: KEDA가 HPA를 ‘조종’한다

KEDA는 “별도 오토스케일러”가 아니라, 구조적으로는 외부/커스텀 metric을 HPA로 연결해주는 컨트롤 플레인에 가깝습니다.

  • (A) LLM 서버(vLLM/KServe 등)가 Prometheus metric을 노출 (예: vllm:num_requests_waiting, vllm:gpu_cache_usage_perc) (kserve.github.io)
  • (B) Prometheus가 scrape
  • (C) KEDA ScaledObject가 Prometheus query(또는 외부 scaler)를 통해 값을 읽음 (keda.sh)
  • (D) KEDA가 HPA를 생성/갱신하고, HPA가 replica 수를 조절

여기서 중요한 차이점:

  • “표준 HPA + custom metrics adapter(prometheus-adapter)”도 가능하지만, KEDA는 scale-to-zero, 다양한 scaler, 운영 단순화 측면에서 우세합니다. (docs.vllm.ai)

3) GPU metric 경로: DCGM(표준) vs NVML direct(지연/복잡도 축소)

GPU 지표를 얻는 전통적인 경로는 다음입니다.

  • NVIDIA GPU Operator → dcgm-exporter → Prometheus → (KEDA Prometheus scaler) (docs.nvidia.com)
    • 장점: 표준적, 대시보드/관측 생태계 풍부
    • 단점: 구성요소가 많아지고, metric lag(스크랩/쿼리 주기)이 생깁니다

2026년 5월 CNCF 블로그에서는 “Prometheus 체인 없이” NVML을 DaemonSet에서 직접 읽고 KEDA에 gRPC external scaler로 제공하는 접근을 소개합니다. 즉, 구성요소/지연을 줄이는 방향입니다. (cncf.io)

실무적으로는:

  • 정교한 관측/리포팅이 이미 Prometheus 중심이면 DCGM 체인이 무난
  • 스케일 신호의 지연을 줄이고 운영 컴포넌트를 최소화하고 싶으면 NVML direct external scaler가 매력적입니다 (cncf.io)

💻 실전 코드

아래 예시는 “toy”가 아니라, 실제 운영에서 자주 쓰는 vLLM 기반 LLM 서빙 + KEDA 오토스케일(Queue Depth 우선) + GPU 메모리 가드레일 패턴입니다.

시나리오

  • vLLM이 노출하는 metric(vllm:num_requests_waiting)으로 scale-out
  • 동시에 DCGM metric으로 VRAM 압박을 감지해 “너무 빡빡한 상태에서 무작정 scale”하지 않도록(=실패 확률 증가 구간 회피) 운영자가 알람/대시보드 기준을 잡는다
    • 참고: DCGM Exporter의 대표 지표는 DCGM_FI_DEV_GPU_UTIL, DCGM_FI_DEV_FB_USED 등입니다. (docs.nvidia.com)

0) 전제(의존성)

  • Kubernetes에 NVIDIA GPU Operator 설치(=드라이버/디바이스 플러그인/DCGM Exporter 포함 가능) (docs.nvidia.com)
  • Prometheus(또는 kube-prometheus-stack) 설치
  • KEDA 설치
  • vLLM(또는 KServe+vLLM)에서 metrics endpoint 활성화 (kserve.github.io)

1) vLLM Deployment (현실적인 포인트 포함)

  • 모델 다운로드/로딩이 느리면 scale-out이 늦습니다. PVC에 모델을 캐시하거나 이미지에 bake-in하는 전략이 필요합니다(여기서는 PVC 가정). (kubenatives.com)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# vllm-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm
  namespace: llm
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vllm
  template:
    metadata:
      labels:
        app: vllm
    spec:
      terminationGracePeriodSeconds: 120
      containers:
        - name: vllm
          image: vllm/vllm-openai:latest
          args:
            - "--model=/models/llama"
            - "--host=0.0.0.0"
            - "--port=8000"
            - "--metrics-port=8001"
            # 운영에서는 아래 같은 옵션(배치/컨텍스트/메모리)을 반드시 모델/트래픽에 맞춰 튜닝
          ports:
            - name: http
              containerPort: 8000
            - name: metrics
              containerPort: 8001
          resources:
            limits:
              nvidia.com/gpu: "1"
            requests:
              nvidia.com/gpu: "1"
          volumeMounts:
            - name: model-cache
              mountPath: /models
      volumes:
        - name: model-cache
          persistentVolumeClaim:
            claimName: llm-model-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: vllm
  namespace: llm
spec:
  selector:
    app: vllm
  ports:
    - name: http
      port: 8000
      targetPort: 8000
    - name: metrics
      port: 8001
      targetPort: 8001

적용:

1
2
3
kubectl create ns llm
kubectl apply -f vllm-deploy.yaml
kubectl -n llm get pods -w

예상 확인:

1
2
3
# metrics가 노출되는지 확인(포트포워드)
kubectl -n llm port-forward svc/vllm 8001:8001
curl -s localhost:8001/metrics | head

2) Prometheus가 vLLM metrics를 scrape하도록 ServiceMonitor

(kube-prometheus-stack 기준)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# vllm-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: vllm
  namespace: llm
spec:
  selector:
    matchLabels:
      app: vllm
  namespaceSelector:
    matchNames: ["llm"]
  endpoints:
    - port: metrics
      interval: 15s
      path: /metrics
1
kubectl apply -f vllm-servicemonitor.yaml

3) KEDA ScaledObject: “대기열(Queue Depth)”로 스케일

KServe 문서/가이드에서도 vLLM의 waiting/running metric이나 KV cache 기반 스케일을 예시로 듭니다. (kserve.github.io)
KEDA의 Prometheus scaler는 Prometheus HTTP API를 query합니다. (keda.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: vllm-queue-autoscale
  namespace: llm
spec:
  scaleTargetRef:
    name: vllm
  minReplicaCount: 0
  maxReplicaCount: 10
  cooldownPeriod: 300   # GPU pod는 스타트업이 느리니 scale-down을 공격적으로 하면 비용보다 장애가 커짐
  pollingInterval: 15
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://kube-prometheus-stack-prometheus.monitoring:9090
        metricName: vllm_requests_waiting
        # 핵심: "레플리카당" 대기열을 기준으로 목표를 잡아야 함
        # 예: replica당 waiting이 평균 2를 넘으면 확장
        query: |
          sum(vllm_num_requests_waiting{namespace="llm",service="vllm"}) 
          /
          max(count(kube_pod_info{namespace="llm",pod=~"vllm-.*"}), 1)
        threshold: "2"
1
2
3
kubectl apply -f keda-scaledobject.yaml
kubectl -n llm get hpa
kubectl -n llm describe scaledobject vllm-queue-autoscale

스케일 동작 확인(부하를 준다는 가정):

  • 동시 요청이 늘어 vllm_num_requests_waiting이 올라가면, KEDA가 HPA를 통해 replica를 증가시킵니다. (KServe 문서도 “요청 수/타겟당 pod 수” 예시로 같은 원리를 설명합니다.) (kserve.github.io)

⚡ 실전 팁 & 함정

Best Practice (바로 적용 가능한 것만)

1) 스케일 지표는 “GPU utilization”이 아니라 “서빙 병목 지표”를 우선

  • vLLM: num_requests_waiting, num_requests_running, 또는 gpu_cache_usage_perc 계열이 운영 판단에 더 직접적입니다. (kserve.github.io)
  • GPU utilization/DCGM은 “참고 지표” 또는 알람/디버깅에 두고, 오토스케일 트리거는 대기열 기반이 보통 더 안전합니다. (kubenatives.com)

2) cooldown/scale-down을 보수적으로

  • LLM pod는 (모델 로드 + CUDA init + 캐시 워밍) 때문에 scale-up 이득이 늦게 오고, scale-down은 캐시를 날려서 다음 요청에 페널티를 줍니다.
  • 따라서 cooldownPeriod를 길게(예: 300s 이상) 두는 패턴이 자주 권장됩니다. (stackpulsar.com)

3) 관측 스택(DCGM Exporter)은 “오토스케일”뿐 아니라 “원인 분석”을 위해 필수

  • DCGM Exporter는 Kubernetes에서 GPU metric을 Prometheus로 내보내는 표준 축이고, GPU Operator와 함께 쓰는 구성이 일반적입니다. (docs.nvidia.com)
  • 대표 지표: DCGM_FI_DEV_GPU_UTIL, DCGM_FI_DEV_FB_USED 등 (docs.nvidia.com)

흔한 함정/안티패턴

  • 함정 1: GPU utilization 70% 타겟 같은 단일 룰로 끝내기
    vLLM에서는 KV cache/메모리 모델 때문에 “util 높음 = 더 많은 replica 필요”가 아닐 수 있습니다. 잘못하면 비용만 증가합니다. (kubenatives.com)

  • 함정 2: scale-to-zero를 켰는데 첫 요청 latency를 감당 못함
    비즈니스가 “첫 토큰 시간(TTFT)”에 민감하면, scale-to-zero는 사용자 경험을 망칩니다. 이 경우는 minReplicaCount=1 + 야간 스케줄 scale-down 같은 절충이 더 실용적입니다(일부 실무 글에서도 warm node/스케줄 패턴을 언급). (buildmvpfast.com)

  • 함정 3: 메트릭 지연(스크랩 주기+쿼리 주기)으로 ‘늦은 스케일’
    DCGM→Prometheus→KEDA 체인이 길면 지연이 생길 수 있어, 2026년에는 NVML direct external scaler처럼 경량화 접근도 제시됩니다. (cncf.io)

비용/성능/안정성 트레이드오프 한 줄 요약

  • Queue-based autoscaling: 안정적(SLO 친화) / 비용 효율 좋음 / 대신 “요청 비용 편차(긴 generation)”가 크면 튜닝 필요 (reddit.com)
  • GPU util-based autoscaling: 구현은 쉬움 / 하지만 LLM에서는 오판이 잦아 비용·장애 모두 위험 (kubenatives.com)
  • NVML direct scaler: 지연·구성요소↓ / 대신 운영 표준(관측/감사) 측면에서 팀 합의 필요 (cncf.io)

🚀 마무리

핵심은 “Kubernetes에서 LLM 서빙을 오토스케일한다”가 아니라, 어떤 신호가 ‘서빙 병목’을 대표하는가입니다. 2026년 7월 기준 실무 흐름은:

  • 오토스케일 엔진은 KEDA가 사실상 표준 레일이고 (kserve.github.io)
  • 스케일 신호는 GPU utilization보다 queue depth / waiting requests / KV cache가 더 낫다는 경험칙이 강해지고 (zartis.com)
  • GPU telemetry는 DCGM Exporter(표준) 또는 NVML direct external scaler(지연/복잡도 축소) 두 갈래로 발전 중입니다. (docs.nvidia.com)

도입 판단 기준(실무 체크리스트)

  • 우리 서비스의 SLO는 “처리량”인가 “P95 latency/TTFT”인가?
  • 모델/서버의 병목은 Compute인가 KV cache/VRAM인가? (vLLM이면 KV cache 쪽 가능성이 큼)
  • scale-to-zero의 cold start를 제품이 허용하는가?
  • Prometheus 기반 관측 스택을 이미 운영 중인가, 아니면 스케일 신호만 빠르게 뽑고 싶은가?

다음 학습 추천

  • vLLM Production Stack의 KEDA 연동 문서(Helm values로 통합되는 지점 확인) (docs.vllm.ai)
  • KServe의 KEDA autoscaling 가이드(Serving 레이어에서 metric을 어떻게 잡는지) (kserve.github.io)
  • DCGM Exporter 최신 문서에서 metric 라벨/구성 옵션(특히 pod/namespace 라벨링) 정리 (docs.nvidia.com)

원하시면, (1) 사용 중인 서빙 스택(vLLM 단독 / KServe / Triton), (2) 클라우드(GKE/EKS/AKS/온프렘), (3) 모델 크기와 목표 SLO를 기준으로 ScaledObject 쿼리/threshold/cooldown을 실제 값으로 튜닝하는 버전으로 더 구체화해 드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.