Kubernetes에서 LLM 서빙을 “GPU 기준”으로 오토스케일링하기: KServe(vLLM) + KEDA + DCGM, 그리고 노드까지 따라오는 설계(2026년 4월)
들어가며
LLM inference는 CPU 웹서비스처럼 “QPS만 보고” HPA로 늘리면 자주 망합니다. 이유는 간단합니다. 스파이크가 오면 GPU가 포화되기 전에 이미 큐(대기 요청)가 쌓이고, 새 Pod는 뜨자마자 바로 처리 못 하는 경우(모델 로딩, KV cache warm-up)가 많기 때문입니다. Red Hat/IBM 사례에서도 “새 Pod가 모델을 GPU 메모리에 올릴 때까지는 트래픽을 못 받는다”는 점을 반복해서 강조합니다. (developers.redhat.com)
그래서 2026년 4월 기준 “실전”에서 가장 많이 채택되는 패턴은:
- (Pod 오토스케일) KServe + KEDA로 request/queue 기반 스케일링 (vLLM의
num_requests_waiting같은 지표) (kserve.github.io) - (GPU 지표 수집) NVIDIA DCGM Exporter를 DaemonSet으로 깔고 Prometheus가 스크랩 (docs.nvidia.com)
- (노드 오토스케일) Cluster Autoscaler 또는 Karpenter 계열로 GPU 노드를 따라 늘림(단, 노드 프로비저닝은 분 단위라 “스파이크 흡수”는 Pod 레벨에서 먼저 해야 함) (sedai.io)
언제 쓰면 좋나:
- 트래픽 변동이 큰 LLM API, multi-tenant inference, “낮엔 바쁘고 밤엔 한가한” 서비스
- SLO(예: TTFT/latency) 관리가 필요하고, 비용을 위해 scale-to-zero/최소 GPU를 고민할 때
언제 쓰면 안 되나(혹은 신중히):
- 초저지연(수 초 이내 노드 증설이 불가능한 수준의) 스파이크를 “오토스케일만으로” 해결하려는 경우
- GPU time-slicing을 켜고 “컨테이너 단위 GPU 메트릭”으로 정교하게 스케일링하려는 경우(DCGM Exporter 제한이 문서에 명시됨) (docs.nvidia.com)
🔧 핵심 개념
1) “GPU utilization 기반 스케일링”이 LLM에 자주 늦는 이유
DCGM Exporter → Prometheus scrape → KEDA polling → HPA 반영까지 측정/전파 지연이 생깁니다. 실제로 커뮤니티에서도 “스크랩/반응 지연 때문에 스파이크를 놓친다”는 피드백이 나옵니다. (reddit.com)
게다가 LLM은 input 길이/출력 토큰/동시성에 따라 처리량이 요동치므로, “GPU util 80% 넘으면 scale” 같은 정책이 SLO 관점에서 둔합니다. (developers.redhat.com)
2) LLM 서빙에서 더 나은 신호: “큐/대기 요청” (work-conserving 지표)
vLLM은 서빙 내부 큐/동시 처리 상황을 지표로 내보내고, KServe는 이를 Prometheus 기반 KEDA autoscaling으로 연결하는 가이드를 제공합니다. 예시로 vllm:num_requests_running, vllm:num_requests_waiting 같은 메트릭을 직접 스케일 트리거로 사용합니다. (kserve.github.io)
이 접근의 장점:
- GPU가 아직 70%라도 대기 요청이 늘면 미리 스케일 아웃
- 반대로 GPU util이 순간 90%여도 대기 큐가 0이면 무작정 증설하지 않도록 설계 가능
3) KServe + KEDA의 구조(흐름)
흐름을 “제어면/데이터면”으로 쪼개면 이해가 쉽습니다.
- 데이터면: 사용자 요청 → KServe ingress/gateway → predictor(vLLM) Pod
- 관측: vLLM / DCGM Exporter가
/metrics로 Prometheus 형식 노출 (docs.nvidia.com) - 제어면(스케일): KEDA가 Prometheus query를 주기적으로 평가 → External Metrics로 HPA에 전달 → Deployment/InferenceService replica 변경 (keda.sh)
여기서 중요한 차이점:
- “KServe autoscaling”은 결국 내부적으로 KEDA/HPA를 활용하는 형태가 많고, generative workload에 맞춘 request-based 접근을 강조합니다. (kserve.github.io)
💻 실전 코드
아래 예제는 “현실적인” 상황(모델이 크고, cold start가 비싸며, 트래픽이 출렁임)을 가정합니다.
- 목표:
1) KServe로 vLLM 기반 LLM endpoint 배포
2)vllm:num_requests_waiting기반으로 KEDA가 Pod 스케일
3) (선택) GPU util 기반 스케일도 보조로 걸 수 있게 토대 마련(DCGM)
단계 0) 전제(의존성)
- Kubernetes 클러스터에 GPU 노드 존재
- NVIDIA GPU Operator 또는 동등한 드라이버/디바이스플러그인 설치(DCGM Exporter를 같이 깔면 더 편함) (docs.nvidia.com)
- Prometheus가 있고, KEDA가 설치되어 있어야 함(KEDA Prometheus scaler 사용) (keda.sh)
단계 1) KServe InferenceService (vLLM)
KServe는 vLLM 등 “optimized backend”와 generative inference autoscaling을 문서로 안내합니다. (kserve.github.io)
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
# llm-isvc.yaml
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llama3-8b
namespace: llm
annotations:
serving.kserve.io/autoscalerClass: "keda"
spec:
predictor:
# 운영에서는 대개 RawDeployment가 디버깅/튜닝이 쉬움(멀티GPU/멀티노드도 RawDeployment에 제약이 있음)
# multi-node가 필요하면 RawDeployment만 지원된다는 문서도 참고. ([kserve.github.io](https://kserve.github.io/archive/0.15/modelserving/v1beta1/llm/huggingface/multi-node?utm_source=openai))
containers:
- name: vllm
image: vllm/vllm-openai:latest
args:
- "--model"
- "meta-llama/Meta-Llama-3-8B-Instruct"
- "--served-model-name"
- "llama3-8b"
- "--port"
- "8000"
# 트래픽 스파이크 완충을 위해 동시성/배치 관련 옵션은 환경에 맞게 튜닝
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
requests:
nvidia.com/gpu: "1"
env:
# Hugging Face 토큰 등은 Secret로 관리 권장
- name: HF_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: token
예상 동작:
kubectl -n llm get pods에서 predictor Pod가 뜨고- OpenAI-compatible endpoint로 호출 가능(KServe가 이 프로토콜을 강조) (kserve.github.io)
단계 2) KEDA ScaledObject: “대기 요청 수” 기반 스케일
KServe/Red Hat 문서에서 핵심으로 미는 방식이 vLLM queue depth(num_requests_waiting) 입니다. (zartis.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
# keda-vllm-queue-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llama3-8b-queue
namespace: llm
spec:
scaleTargetRef:
kind: Deployment
name: llama3-8b-predictor # 실제 생성되는 이름은 환경/모드에 따라 다를 수 있어 확인 필요
minReplicaCount: 1
maxReplicaCount: 6
pollingInterval: 5 # 너무 짧게 하면 Prometheus/adapter 비용↑, 너무 길면 반응성↓
cooldownPeriod: 120 # 모델 로딩 비용이 큰 LLM은 과도한 scale-in을 막는 게 보통 유리
triggers:
- type: prometheus
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring:9090
metricName: vllm_requests_waiting
# 핵심: (전체 대기 요청 합) / (현재 replica 수) 가 임계치 넘으면 scale-out
# 라벨은 실제 vLLM metrics 라벨(pod, namespace 등)에 맞게 조정
query: |
sum(vllm:num_requests_waiting{namespace="llm", service="llama3-8b"})
/
max(count(up{namespace="llm", job=~".*llama3-8b.*"}), 1)
threshold: "2"
예상 출력(관찰 포인트):
kubectl -n llm describe scaledobject llama3-8b-queue에서 KEDA가 metric을 찾고 HPA가 붙습니다(유사 로그/상태는 AKS 가이드에도 나옵니다). (learn.microsoft.com)- 부하를 주면 replica가 1→N으로 늘고, 부하가 빠지면 cooldown 이후 줄어듭니다.
단계 3) (선택) DCGM 기반 “GPU util”은 보조 신호로만
DCGM Exporter는 GPU 텔레메트리를 /metrics로 노출하고, Kubernetes에서 DaemonSet으로 배포하는 전형적인 구성이 문서화되어 있습니다. (docs.nvidia.com)
다만 LLM은 queue 신호가 더 직접적이므로, GPU util은 “과열 방지/안전장치” 정도로 섞는 것을 권합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# keda-gpu-util-scaledobject.yaml (보조 스케일: GPU util이 계속 높을 때만 완만히 스케일)
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llama3-8b-gpu-util
namespace: llm
spec:
scaleTargetRef:
kind: Deployment
name: llama3-8b-predictor
minReplicaCount: 1
maxReplicaCount: 6
pollingInterval: 15
cooldownPeriod: 180
triggers:
- type: prometheus
metadata:
serverAddress: http://kube-prometheus-stack-prometheus.monitoring:9090
metricName: dcgm_gpu_util
# DCGM 메트릭 이름/라벨은 배포 설정에 따라 다를 수 있음(대시보드/문서로 확인)
# 예: DCGM_FI_DEV_GPU_UTIL
query: |
avg(DCGM_FI_DEV_GPU_UTIL{namespace="gpu-operator"}) / 100
threshold: "0.85"
⚡ 실전 팁 & 함정
Best Practice 1) “노드 스케일”은 별개의 문제로 분리하라
Pod 스케일은 초~수십 초, GPU 노드 프로비저닝은 보통 분 단위입니다. EKS에서 Karpenter를 쓸 때도 “미리 ASG를 맞춰두는 Cluster Autoscaler 방식의 한계(특정 GPU 타입 없으면 Pending)”가 자주 언급됩니다. (sedai.io)
권장:
- (Pod) 큐 기반 KEDA로 먼저 흡수
- (Node) Karpenter/CA로 천천히 따라오게 설계
- 최악의 스파이크를 위해 “baseline GPU(최소 노드/최소 replica)”는 남겨두기
Best Practice 2) cooldown/scale-down을 공격적으로 하지 마라(LLM은 cold start가 비싸다)
vLLM Pod는 뜬 뒤 모델 웨이트 로딩이 끝나야 트래픽을 처리합니다. 즉, 너무 빨리 줄이면 다음 스파이크 때 TTFT/SLO가 박살납니다. (developers.redhat.com)
실무적으로는:
- scale-out은 빠르게, scale-in은 느리게(긴 cooldown, 안정화 윈도우)
- 모델 파일을 로컬 SSD 캐시/공유 스토리지 전략으로 최적화(단, 공유 스토리지 병목 주의)
함정 1) GPU time-slicing 켠 뒤 “컨테이너별 GPU 메트릭”을 기대
NVIDIA GPU Operator 문서에 time-slicing 시 DCGM Exporter가 컨테이너에 메트릭을 연결하지 못한다는 제한이 명시되어 있습니다. (docs.nvidia.com)
즉, “Pod별 GPU util 기반 오토스케일”을 정밀하게 하고 싶다면:
- MIG(하드 파티셔닝) 쪽으로 가거나
- 아예 queue 기반으로 스케일하고 GPU util은 노드/풀 수준으로만 보거나
- (실험적) NVML 직접 수집처럼 파이프라인을 줄이는 접근을 검토(커뮤니티/문서화된 시도 존재) (researchgate.net)
함정 2) “GPU util = 성능”으로 착각
LLM은 토큰 생성 특성상 GPU util이 높아도 SLO가 이미 깨졌을 수 있고(큐가 쌓였을 수 있음), 반대로 util이 낮아도 latency가 나쁠 수 있습니다(메모리/IO/컨텍스트 스위칭/배치 정책). 그래서 2026년 연구/실무 흐름이 “lagging indicator(util) 대신 request/토큰 처리율 같은 신호”를 강조하는 쪽으로 가는 중입니다. (arxiv.org)
🚀 마무리
정리하면, 2026년 4월 기준 Kubernetes에서 LLM 서빙 GPU 오토스케일을 “프로덕션답게” 하려면:
- 1순위 스케일 신호: GPU util보다 queue/request 기반(vLLM metrics) (kserve.github.io)
- 구성의 표준 조합: KServe(서빙/컨트롤 플레인) + KEDA(Prometheus scaler) + DCGM Exporter(GPU 관측) (kserve.github.io)
- 도입 판단 기준:
- “스파이크가 잦고 비용이 민감”하면 강력 추천
- “항상 일정 트래픽 + 고정 GPU”면 굳이 복잡도를 올릴 이유가 적음
- time-slicing/MIG 같은 GPU 공유 전략이 들어가면 메트릭/격리/과금(내부 쇼백)까지 같이 설계해야 함 (docs.nvidia.com)
다음 학습 추천(바로 실무에 도움 되는 순서): 1) KServe generative autoscaling 문서(어떤 vLLM 메트릭으로 스케일할지) (kserve.github.io)
2) KEDA Prometheus scaler 인증/TriggerAuthentication 패턴 (keda.sh)
3) DCGM Exporter 메트릭 카탈로그/대시보드로 “내 클러스터에서 어떤 라벨로 보이는지” 확정 (docs.nvidia.com)
원하면, (1) EKS+Karpenter 또는 (2) GKE DCGM managed Prometheus 또는 (3) AKS KEDA 가이드 중 하나로 클라우드별로 YAML/라벨/권한(Workload Identity)까지 맞춘 “실제 적용 버전”으로 변환해 드릴까요? (cloud.google.com)