- Published on
KServe GPU 추론 503, readiness·오토스케일 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
KServe로 GPU 추론 서비스를 운영하다 보면, 모델은 잘 떠 있는 것 같은데 간헐적으로 503 Service Unavailable 이 튀는 순간이 있습니다. 특히 트래픽이 들쭉날쭉하거나, 스케일 0 에서 다시 올라오는 구간, 혹은 새 리비전으로 롤아웃되는 구간에서 빈도가 높습니다.
이 글은 “왜 503이 나는가”를 KServe·Knative의 요청 라우팅과 readiness, 오토스케일 관점에서 분해한 뒤, 운영에서 바로 적용 가능한 튜닝 포인트를 정리합니다. (KServe 버전/런타임마다 세부는 다르지만, 원리와 접근은 대부분 동일합니다.)
관련해서 무중단 롤아웃 관점은 아래 글도 함께 보면 좋습니다.
503이 발생하는 대표 시나리오
KServe의 InferenceService 는 내부적으로 Knative Serving 위에서 동작하고, 요청은 대략 다음 경로를 탑니다.
- 클라이언트 요청
- Knative Activator / Queue-Proxy
- 사용자 컨테이너(모델 서버)
이때 503 은 “라우팅할 준비가 된 엔드포인트가 없다” 혹은 “업스트림이 준비되지 않았다”는 의미로 나타나는 경우가 많습니다. GPU 추론에서 흔한 시나리오는 다음과 같습니다.
1) 스케일 0 에서 콜드 스타트
GPU 파드는 뜨는 데 시간이 오래 걸립니다.
- 이미지 pull
- 모델 파일 다운로드(또는 PVC 마운트)
- CUDA 컨텍스트 초기화
- TensorRT/torch compile, 커널 캐시 생성
- 첫 inference 워밍업
Knative는 “리비전이 준비됨(Ready)”으로 판단되기 전까지 트래픽을 보내지 않으려 하지만, 설정/프로브가 부정확하면 준비가 덜 됐는데도 라우팅되거나, 반대로 준비가 됐는데도 Ready가 늦게 찍혀 Activator가 503을 뱉는 상황이 생깁니다.
2) readiness 프로브가 실제 준비 상태를 반영하지 못함
가장 흔한 케이스는 “HTTP 서버는 떠 있지만 GPU가 아직 준비 안 됨” 입니다.
/health는200을 주지만- 실제
/v1/models/...:predict는 첫 요청에서 CUDA init 때문에 수십 초 걸리거나 timeout - 또는 OOM으로 죽고 재기동을 반복
이 경우 readiness는 통과하지만 실서비스는 실패하면서, 클라이언트 입장에서는 5xx가 섞여 보입니다.
3) 오토스케일 정책과 동시성 설정 불일치
Knative는 기본적으로 동시성/지연/메트릭 기반으로 파드를 늘립니다. GPU 모델은 보통 “한 파드가 처리 가능한 동시성”이 낮은데, 기본값을 그대로 쓰면 다음이 발생합니다.
- 한 파드에 요청이 몰려 큐가 길어짐
- 요청이 timeout
- 스케일아웃이 늦게 따라옴
이때 외부에서는 503 또는 504 로 관측되기도 합니다(환경에 따라 다름).
4) 롤아웃 중 트래픽 스플릿과 준비성 타이밍
새 리비전으로 트래픽을 넘기는 과정에서, 새 리비전이 “진짜로” 준비되기 전에 일부 트래픽이 흘러가면 503이 섞입니다. 특히 GPU 워밍업이 긴 모델에서 자주 보입니다.
1단계: 어디서 503이 나는지 먼저 분리하기
503이 “애플리케이션이 반환한 503”인지, “Knative 레이어가 반환한 503”인지부터 구분해야 합니다.
관측 포인트
- Knative Revision 상태:
kubectl get revision -n ... - InferenceService 상태:
kubectl get inferenceservice -n ... -o yaml - Queue-proxy/Activator 로그:
kubectl logs - Istio/Ingress가 있다면 Envoy access log
아래처럼 리비전이 Ready=False 인데 요청이 들어오면 Activator가 503을 낼 가능성이 큽니다.
kubectl get revision -n ml
kubectl describe revision -n ml <revision-name>
또한 KServe는 predictor 와 transformer 를 함께 쓸 수 있는데, 체인 중 어디가 준비 안 됐는지도 분리해야 합니다.
kubectl get pods -n ml -l serving.kserve.io/inferenceservice=<svc>
kubectl logs -n ml <pod> -c kserve-container
kubectl logs -n ml <pod> -c queue-proxy
2단계: readiness를 “GPU 추론 준비” 기준으로 재정의
핵심은 “HTTP 서버가 떴다”가 아니라 “첫 inference가 SLA 내로 성공한다”를 readiness에 반영하는 것입니다.
(권장) 별도 readiness 엔드포인트에서 워밍업 완료 여부 확인
모델 서버에 다음과 같은 상태를 둡니다.
- 시작 시
warmingUp=true - GPU 초기화 + 모델 로드 + 더미 inference 1회 성공 후
warmingUp=false /readyz는warmingUp=false일 때만200반환
예시(파이썬 FastAPI 개념 코드):
from fastapi import FastAPI, Response
app = FastAPI()
state = {"ready": False}
@app.on_event("startup")
def startup():
# 1) 모델 로드
# 2) CUDA 컨텍스트 초기화
# 3) 더미 입력으로 1회 추론
state["ready"] = True
@app.get("/readyz")
def readyz():
if state["ready"]:
return {"ready": True}
return Response(content="not ready", status_code=503)
이렇게 하면 readiness가 “실제 추론 가능”을 의미하게 됩니다.
KServe/Knative에서 프로브가 어떻게 주입되는지 이해
KServe는 predictor 컨테이너에 대해 readiness/liveness를 자동으로 구성하기도 하고, 런타임에 따라 기본 포트/경로가 다릅니다. 따라서 다음을 확인해야 합니다.
- readinessProbe가 어떤 path/port를 보고 있는지
initialDelaySeconds와failureThreshold가 GPU 워밍업 시간을 커버하는지
kubectl get pod -n ml <pod> -o yaml | sed -n '1,220p'
프로브가 너무 공격적이면, 워밍업 중에 계속 재시작되어 “영원히 Ready가 안 되는” 루프에 빠집니다.
최소한의 튜닝 가이드
- 워밍업이 60초 걸리면 readiness
initialDelaySeconds를 60초 이상으로 - 또는
periodSeconds * failureThreshold로 허용 시간을 확보 - 첫 추론이 무거운 모델은 readiness timeout도 고려(쿠버네티스는 probe timeout이
timeoutSeconds로 제한됨)
3단계: Knative 오토스케일을 GPU 워크로드에 맞추기
GPU 추론은 CPU 웹서비스와 다르게 “한 파드가 처리할 수 있는 동시 요청 수”가 작고, 메모리/VRAM 특성상 급격한 스파이크에 취약합니다.
containerConcurrency 를 명시적으로 설정
기본값을 믿기보다, “GPU 1장당 안전한 동시성”을 실측해 고정하는 편이 안정적입니다.
- 예: LLM 계열은
1또는2 - 가벼운 CV 모델은
4~16도 가능
KServe InferenceService 에서는 보통 Knative 어노테이션/스펙을 통해 지정합니다(환경에 따라 필드가 다를 수 있어, 아래는 대표 예시입니다).
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model
namespace: ml
annotations:
autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
autoscaling.knative.dev/metric: concurrency
autoscaling.knative.dev/target: "1"
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "5"
spec:
predictor:
containerConcurrency: 1
containers:
- name: kserve-container
image: myrepo/gpu-model:1.0.0
resources:
limits:
nvidia.com/gpu: "1"
포인트는 두 가지입니다.
containerConcurrency와 오토스케일 타겟(target)을 같은 세계관으로 맞추기- GPU는 스케일아웃 비용이 크므로
minScale을1이상으로 두는 것을 진지하게 고려하기
스케일 0 을 유지해야 한다면: “콜드 스타트 허용량”을 설계
비용 때문에 minScale=0 을 고집해야 한다면, 503을 완전히 없애기 어렵고 “지연 증가”를 받아들이는 설계가 필요합니다.
실전 팁:
- 프론트/게이트웨이에서 콜드 스타트 구간에 대한 재시도 정책(지수 백오프)을 둠
- 첫 요청은
warmup트래픽으로 분리(사전 호출) scaleDownDelay류 설정으로 너무 빨리0으로 내려가지 않게 함
Knative의 스케일 다운 지연은 설정/버전에 따라 키가 달라질 수 있으니, 현재 클러스터의 Knative 설정(ConfigMap)에서 유효 키를 확인하세요.
kubectl get configmap config-autoscaler -n knative-serving -o yaml
GPU 파드는 “빠르게 늘릴 수 없다”는 전제를 둔다
GPU 노드가 부족하면 스케일아웃 이벤트가 떠도 파드가 Pending에 머뭅니다. 이때도 외부에서는 503으로 보일 수 있습니다.
- 노드 오토스케일러가 있더라도 GPU 노드 증설은 수 분 단위
- 이미지 pull과 모델 로드까지 고려하면 더 길어짐
따라서 다음을 함께 점검합니다.
kubectl get events -n ml --sort-by=.lastTimestamp
kubectl describe pod -n ml <pod>
Insufficient nvidia.com/gpu 가 보이면 readiness가 아니라 “수용량(capacity)” 문제입니다.
4단계: 요청 타임아웃과 큐잉 레이어를 조정
503이 readiness에서만 발생하는 게 아니라, 큐잉 레이어의 타임아웃 때문에 발생할 수도 있습니다.
- Activator/queue-proxy가 업스트림 응답을 기다리다 타임아웃
- Ingress가 idle timeout에 걸림
이때는 다음을 같이 봐야 합니다.
- 클라이언트 타임아웃
- Ingress/Envoy 타임아웃
- Knative 요청 타임아웃(리비전 단위)
리비전 타임아웃은 보통 어노테이션으로 설정합니다.
metadata:
annotations:
serving.knative.dev/timeoutSeconds: "300"
주의할 점:
- 타임아웃을 무작정 늘리면 “문제 은폐”가 될 수 있음
- 대신 동시성 제한 + 워밍업 + minScale로 “대기열이 길어지지 않게” 만드는 게 우선
5단계: GPU 워밍업을 배포 파이프라인에 포함
readiness를 잘 잡아도, 첫 추론이 너무 느리면 사용자 경험은 여전히 나쁩니다. 그래서 “배포 직후 워밍업 트래픽”을 넣는 패턴이 유효합니다.
예: 배포 후 Job 으로 더미 요청을 1회 보내 캐시를 만든 뒤, 실제 트래픽을 열기.
apiVersion: batch/v1
kind: Job
metadata:
name: gpu-model-warmup
namespace: ml
spec:
template:
spec:
restartPolicy: Never
containers:
- name: warmup
image: curlimages/curl:8.6.0
command:
- sh
- -c
- |
set -e
URL="http://gpu-model.ml.example.com/v1/models/model:predict"
curl -sS -m 120 -X POST "$URL" \
-H 'Content-Type: application/json' \
-d '{"instances":[[0,0,0]]}'
클러스터 내부 DNS로 호출할지, 외부 게이트웨이로 호출할지는 네트워크 구성에 따라 선택합니다.
6단계: 실전 체크리스트(503 줄이는 순서)
운영에서 효과가 큰 순서로 정리하면 다음과 같습니다.
queue-proxy로그와 리비전 상태로 503의 발생 레이어를 확정- readiness를
/readyz같은 “실제 추론 준비” 기준으로 변경 - readiness 프로브의
initialDelaySeconds/failureThreshold를 워밍업 시간에 맞춤 containerConcurrency를 실측 기반으로 낮추고, 오토스케일 타겟을 정합성 있게 설정- 비용이 허용되면
minScale=1로 콜드 스타트 제거 - GPU 노드 수용량과 Pending 이벤트 확인(스케일이 안 되는 구조적 문제 제거)
- 필요한 범위에서만 timeout을 조정하고, 워밍업 Job으로 첫 요청 지연을 줄임
부록: 문제 재현과 검증을 위한 간단한 부하 테스트
503이 “스케일 이벤트 구간”에서만 나는지 확인하려면, 트래픽을 계단식으로 올렸다 내리는 테스트가 유용합니다.
# 예: 10초마다 0->1->5->10 rps로 변경(환경에 맞게 수정)
for rps in 0 1 5 10 5 1 0; do
echo "RPS=$rps"
if [ "$rps" -eq 0 ]; then
sleep 10
continue
fi
hey -z 10s -q "$rps" -c "$rps" \
-m POST \
-H 'Content-Type: application/json' \
-d '{"instances":[[0,0,0]]}' \
http://gpu-model.ml.example.com/v1/models/model:predict
done
이때 다음을 함께 수집하면 원인 규명이 빨라집니다.
kubectl get pods -w로 스케일 변화kubectl logs -c queue-proxy에서 503/timeout 패턴- GPU 사용률(
nvidia-smi)과 VRAM 변동
마무리
KServe GPU 추론의 503은 “모델이 죽었다”기보다, readiness가 실제 준비 상태를 반영하지 못하거나, Knative 오토스케일이 GPU 워크로드 특성과 어긋나면서 생기는 경우가 많습니다.
readiness를 “첫 추론 성공” 기준으로 재정의하고, 동시성과 스케일 정책을 보수적으로 잡으며, 필요하면 minScale 로 콜드 스타트를 제거하면 503은 체감상 크게 줄어듭니다. 롤아웃 중 503까지 함께 잡고 싶다면 위 내부 링크의 무중단 롤아웃 전략도 같이 적용하는 것을 권합니다.