- Published on
KServe로 LLM 추론 오토스케일링 실패 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 트래픽이 늘었는데도 파드가 안 늘거나, 늘어도 너무 늦어서 타임아웃이 나는 상황은 KServe 기반 LLM 추론에서 흔합니다. 특히 LLM은 cold start 비용(모델 로딩, GPU 메모리 할당, 토크나이저 초기화)이 크고, 요청당 지연이 길며, 동시성 제어가 까다로워서 오토스케일 정책이 조금만 어긋나도 바로 장애로 이어집니다.
이 글은 KServe + (Knative Serving 또는 RawDeployment) + GPU 노드 조합에서 오토스케일링이 실패하는 대표 원인을 증상별로 분해하고, 바로 적용 가능한 설정과 운영 체크리스트를 제공합니다.
1) 먼저 구조부터: KServe의 스케일링 경로 이해
KServe 배포 방식에 따라 스케일러가 달라집니다.
predictor가serverless모드(대개 Knative Serving 사용)인 경우- Knative Pod Autoscaler, 즉
KPA가 기본적으로 스케일을 담당합니다. - 메트릭은 보통
concurrency또는RPS기반이며, Activator와 Queue Proxy가 개입합니다.
- Knative Pod Autoscaler, 즉
RawDeployment모드인 경우- 일반 Kubernetes
HPA를 사용합니다. - 메트릭은
CPU/Memory또는 Custom Metrics(예: Prometheus Adapter)로 구성합니다.
- 일반 Kubernetes
오토스케일이 안 되는 대부분의 케이스는 “나는 HPA를 보고 있는데 실제로는 KPA가 스케일을 쥐고 있음” 또는 그 반대처럼, 관측/제어 대상이 엇갈릴 때 발생합니다.
배포 모드 확인 명령
kubectl get inferenceservice -A
kubectl describe inferenceservice -n ${NS} ${NAME}
# Knative Service가 생성되는지 확인
kubectl get ksvc -n ${NS}
# RawDeployment면 Deployment/Service 중심으로 생성
kubectl get deploy,svc -n ${NS} -l serving.kserve.io/inferenceservice=${NAME}
2) 증상 A: 트래픽이 와도 0에서 1로 안 올라간다
원인 1: scale-to-zero 이후 Activator 경로에서 병목 또는 타임아웃
Knative 기반이면 scale-to-zero 상태에서 첫 요청은 Activator를 거쳐 파드를 깨웁니다. LLM은 기동 시간이 길어 첫 요청이 타임아웃으로 끊기고, 결과적으로 “스케일이 안 된다”처럼 보일 수 있습니다.
해결책
- LLM 서빙은 가능하면
minScale을 1 이상으로 두고, 완전한 0 스케일을 피합니다. - 첫 요청 타임아웃과 프로브를 LLM 기동 시간에 맞게 늘립니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm
namespace: llm
spec:
predictor:
minReplicas: 1
maxReplicas: 10
# KServe 버전과 runtime에 따라 annotations 위치가 다를 수 있음
# Knative annotations를 template에 넣는 방식이 일반적
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "10"
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/target: "2"
target은 파드당 목표 동시성입니다. LLM은 GPU 메모리와 KV cache 때문에 동시성이 높을수록 급격히 느려질 수 있으니, 실측으로 안전한 값을 잡는 게 중요합니다.
원인 2: readinessProbe가 계속 실패해서 트래픽이 파드로 안 붙음
파드는 늘었는데도 Ready가 안 되면 스케일이 “멈춘 것”처럼 보입니다. LLM 서버는 모델 로딩 중 포트를 열지 않거나, /health가 준비되기 전까지 503을 내는 경우가 많습니다.
해결책
startupProbe를 적극적으로 사용하고, readiness는 “모델 로딩 완료”를 기준으로 통과시키세요.- 모델 로딩 시간을 고려해
failureThreshold와periodSeconds를 조정합니다.
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 120
periodSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
3) 증상 B: 파드는 늘어나는데 QPS가 늘지 않고 지연만 폭증한다
원인 1: 오토스케일 메트릭이 LLM 특성과 안 맞음
Knative concurrency는 “요청이 큐에 대기하는 수”와 “진행 중 요청 수”를 기반으로 동작합니다. LLM은 요청이 길고 스트리밍이 많아, 동시성만으로는 GPU 포화나 토큰 생성 속도 저하를 제대로 반영하지 못합니다.
해결책 1: 동시성 목표를 낮추고, maxScale을 명확히 제한
- 파드당 동시성을 낮추면 지연은 줄고 스케일아웃이 빨라집니다.
- 대신 GPU 비용이 늘 수 있으니
maxScale로 상한을 둡니다.
annotations:
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/target: "1"
autoscaling.knative.dev/maxScale: "8"
해결책 2: RawDeployment로 전환 후 Custom Metrics 기반 HPA
LLM은 GPU util, tokens per second, queue length 같은 지표가 더 직접적입니다. Knative 경로가 복잡하거나, 스트리밍 때문에 동시성 메트릭이 왜곡되면 RawDeployment + Prometheus Adapter 조합이 더 단순하고 예측 가능합니다.
아래는 Prometheus 지표 llm_inflight_requests를 기준으로 HPA를 거는 예시입니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: llm-hpa
namespace: llm
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: llm-predictor
minReplicas: 1
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: llm_inflight_requests
target:
type: AverageValue
averageValue: "2"
핵심은 “CPU가 아니라 실제 병목을 설명하는 메트릭”으로 스케일링해야 한다는 점입니다.
4) 증상 C: 스케일아웃은 되는데 너무 늦다
원인 1: GPU 노드 스케일링과 파드 스케일링이 분리되어 있음
파드는 늘었지만 스케줄링이 안 되고 Pending에 걸리는 경우가 많습니다. 이는 대개 GPU 노드가 부족하거나, Cluster Autoscaler가 GPU 노드를 못 늘리는 상황입니다.
체크 포인트
kubectl describe pod에서Insufficient nvidia.com/gpu같은 이벤트가 있는지 확인- 노드 그룹이 GPU 인스턴스를 스케일할 수 있는지 확인
taint와toleration,nodeSelector가 맞는지 확인
kubectl get pod -n llm
kubectl describe pod -n llm ${POD}
kubectl get events -n llm --sort-by=.lastTimestamp | tail -n 50
원인 2: 이미지 풀, 모델 다운로드, 웜업이 느림
LLM은 컨테이너 이미지가 크고, 시작 시 모델을 원격 스토리지에서 받으면 수 분이 걸릴 수 있습니다.
해결책
- 이미지: 레이어 최적화, 노드 이미지 캐시 활용
- 모델: 노드 로컬 캐시, PV 캐시, 또는 initContainer로 사전 다운로드
- 웜업: 기동 후 더미 요청으로 CUDA 커널 컴파일, KV cache 경로 준비
예시로 initContainer에서 모델을 PVC에 미리 내려받는 패턴입니다.
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: llm-model-pvc
initContainers:
- name: warm-model
image: alpine:3.19
command: ["sh", "-c"]
args:
- |
apk add --no-cache curl;
test -f /models/model.bin || curl -L -o /models/model.bin "$MODEL_URL";
volumeMounts:
- name: model-cache
mountPath: /models
containers:
- name: predictor
volumeMounts:
- name: model-cache
mountPath: /models
스토리지가 S3 계열이라면 권한 문제로 initContainer가 조용히 실패하거나 재시도하면서 기동이 늦어질 수 있습니다. EKS라면 IRSA 설정도 함께 점검하세요.
5) 증상 D: 스케일링이 오락가락한다(플랩핑)
원인 1: 안정화 윈도우와 스케일 다운 정책 부재
LLM은 트래픽이 스파이크 형태로 들어오는 경우가 많고, 스케일다운이 너무 빠르면 다시 스케일업을 반복하며 지연이 흔들립니다.
해결책
HPA를 쓴다면 behavior로 안정화 윈도우를 설정합니다.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 4
periodSeconds: 60
Knative KPA라면 autoscaling.knative.dev/window 같은 튜닝 포인트가 있으며, 설치 버전에 따라 ConfigMap 기반 조정이 필요할 수 있습니다.
원인 2: 큐잉과 제한이 맞물려 메트릭이 왜곡됨
LLM 서버 내부에 max concurrency, batching, queue가 있고, 외부(Queue Proxy, Ingress)에도 큐잉이 있으면 “어디서 대기하는지”에 따라 메트릭이 달라집니다.
권장 접근은 다음 순서입니다.
- 서버 내부의 동시성 제한을 먼저 확정
- 그 한도에 맞춰 KPA의
target을 맞춤 - 인그레스 타임아웃과 재시도를 LLM 지연에 맞춤
6) 실전 디버깅: 어디서 막히는지 15분 안에 찾기
1단계: 스케일러가 누구인지 확정
# Knative면 KService/Revision이 핵심
kubectl get ksvc,rev -n llm
# RawDeployment면 HPA/Deployment가 핵심
kubectl get hpa -n llm
kubectl get deploy -n llm
2단계: 파드가 늘지 않는지, 늘었는데 Ready가 아닌지 분리
kubectl get pod -n llm -w
kubectl describe pod -n llm ${POD}
Pending이면 노드 리소스, GPU, 스케줄링, 노드 오토스케일Running인데NotReady면 프로브, 모델 로딩, 권한, 네트워크
3단계: 이벤트와 로그로 “첫 실패 지점” 찾기
kubectl get events -n llm --sort-by=.lastTimestamp | tail -n 80
kubectl logs -n llm ${POD} -c predictor --tail=200
모델 다운로드 실패, 권한 거부, DNS 문제는 오토스케일보다 먼저 해결돼야 합니다. AKS에서 이미지 풀 실패가 스케일을 가로막는 경우도 흔합니다.
7) LLM 오토스케일링을 “성공”으로 정의하는 기준
오토스케일이 동작했다는 것은 단순히 레플리카 수가 증가했다가 아닙니다. LLM에서는 아래 3가지를 함께 만족해야 운영적으로 의미가 있습니다.
SLO내에서 지연이 유지된다(특히P95,P99)- 스파이크에서 타임아웃이 폭증하지 않는다
- 스케일다운 후에도 재스파이크에서 회복이 빠르다
이를 위해 최소한 다음 지표를 대시보드로 고정하세요.
- 요청 지연:
P50/P95/P99 - 큐 길이 또는 inflight
- 파드 Ready까지 걸린 시간
- GPU 사용률과 메모리 사용률
- 스케줄링 대기 시간(
Pendingduration)
8) 추천 설정 프리셋: 실패를 줄이는 기본값
Knative 기반(KPA) 프리셋
minScale1target concurrency1 또는 2부터 시작- scale-to-zero는 트래픽이 매우 드물고 첫 요청 지연을 허용할 때만
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "10"
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/target: "1"
RawDeployment(HPA) 프리셋
- CPU 기반은 보조로만 사용
- 가능하면
inflight,queue,GPU util같은 커스텀 메트릭으로 전환 behavior로 scaleDown 안정화
9) 마무리: 오토스케일 실패는 대개 “스케일러 문제가 아니다”
KServe에서 LLM 오토스케일이 실패하는 케이스를 보면, 실제로는 다음 중 하나인 경우가 많습니다.
- 파드는 늘었지만
Ready가 안 됨(프로브, 모델 로딩, 권한) - 파드는 늘고 싶어도 GPU 노드가 없어
Pending - 메트릭이 LLM 병목을 반영하지 못함(동시성, 스트리밍, 내부 큐)
- scale-to-zero의 첫 요청 경로가 LLM 기동 시간과 충돌
위의 체크리스트대로 “스케일러 확인, Ready 여부 분리, Pending 원인 확인, 메트릭 적합성 검증” 순으로 보면 대부분 재현 가능하게 해결됩니다.
추가로, LLM이 RAG 파이프라인과 엮여 있다면 벡터 DB 인덱싱 지연이 추론 지연처럼 보일 수 있습니다. 추론 오토스케일을 손보기 전에 업스트림 지연도 함께 확인하세요.