Published on

KServe GPU 모델 서빙 503·OOM 트러블슈팅

Authors

KServe로 GPU 모델을 올리면 초반엔 잘 되다가도 트래픽이 조금만 흔들리면 503 Service Unavailable 이 튀고, 어느 순간엔 Pod가 재시작되며 OOMKilled 또는 GPU CUDA out of memory 로 죽는 경우가 많습니다. 문제는 한 가지가 아니라 준비 상태 판정(프로브) + 오토스케일(KPA 또는 HPA) + GPU 메모리 특성(단편화, 워밍업, KV 캐시) + 서버 런타임 설정이 맞물려서 터진다는 점입니다.

이 글은 “503과 OOM을 한 번에 잡는” 식의 요행이 아니라, 증상을 분해해서 관측하고, 원인을 좁혀가며, KServe 설정으로 고정하는 실전 절차를 제공합니다.

참고로 L7 타임아웃이나 스트리밍 끊김까지 겹친다면 인그레스와 업스트림 튜닝이 먼저 필요할 수 있습니다. 해당 케이스는 아래 글이 큰 도움이 됩니다.

1) 503의 종류부터 분류하기

KServe에서 보이는 503은 크게 3가지로 갈립니다.

  1. 라우팅 계층 503: Ingress 또는 Knative queue-proxy가 “엔드포인트가 준비 안 됨”으로 판단
  2. 스케일링 503: scale-to-zero 이후 콜드스타트 동안 요청이 타임아웃
  3. 애플리케이션 503: 모델 서버 내부에서 과부하 또는 내부 예외로 503 반환

빠른 체크 명령

kubectl -n <ns> get inferenceservice
kubectl -n <ns> describe inferenceservice <name>
kubectl -n <ns> get pods -l serving.kserve.io/inferenceservice=<name>
kubectl -n <ns> logs deploy/<name>-predictor -c kserve-container --tail=200

위에서 다음을 우선 확인합니다.

  • Pod가 살아있는데도 503이면 라우팅 또는 readiness 문제일 가능성
  • Pod가 자주 재시작되면 OOM 또는 liveness 문제일 가능성
  • Revision 이 계속 새로 뜨면 이미지 풀, 모델 다운로드, 초기화 시간이 길거나 프로브가 빡빡한 상태

2) OOM도 2종류다: 컨테이너 OOMKilled vs CUDA OOM

OOM이라고 다 같은 OOM이 아닙니다.

  • 컨테이너 OOMKilled: cgroup 메모리 제한 초과. kubectl describe podOOMKilled 가 찍힘
  • CUDA out of memory: GPU VRAM 부족. 프로세스는 살아있을 수도, 죽을 수도 있음

확인 포인트

kubectl -n <ns> describe pod <pod>
# Events 섹션에서 OOMKilled 확인

kubectl -n <ns> exec -it <pod> -c kserve-container -- nvidia-smi
# GPU 메모리 사용량 확인

컨테이너 OOMKilled는 흔히 다음에서 터집니다.

  • 모델 파일 다운로드 및 압축 해제 시 RAM 사용 폭증
  • 토크나이저, 엔진 빌드, 그래프 컴파일 등 초기화 단계에서 피크
  • Python 프로세스가 워커 수만큼 모델을 중복 로딩

CUDA OOM은 보통 다음에서 터집니다.

  • 배치 크기 또는 동시성 증가로 KV 캐시가 급증
  • 프롬프트 길이 증가로 활성화 메모리 증가
  • 메모리 단편화로 “남아 보이는데 할당 실패”

LLM 계열의 VRAM OOM 패턴은 로컬 환경에서도 동일하게 나타납니다. 원리와 완화책은 아래 글의 접근을 그대로 가져올 수 있습니다.

3) KServe에서 503를 줄이는 핵심: 프로브와 초기화 시간 정렬

GPU 모델은 콜드스타트가 길고, “서버 프로세스는 떴지만 모델이 아직 로드 중”인 시간이 깁니다. 이때 readiness가 너무 일찍 Ready 가 되면 초기 요청이 실패하고, 반대로 너무 늦으면 오토스케일이 늦게 반응합니다.

권장 전략

  • 모델 로딩이 끝나기 전에는 readiness가 절대 통과하지 않게 만들기
  • liveness는 “프로세스가 살아있음” 정도로만 완만하게
  • 초기화가 길면 startupProbe 를 적극 사용

KServe의 predictor 컨테이너가 HTTP 서버를 띄우고 모델을 비동기로 로딩한다면, readiness 엔드포인트가 “모델 준비 완료”를 반영하는지부터 확인하세요.

예시: InferenceService에 프로브를 명시

아래 예시는 컨테이너 기반 predictor를 쓴다는 가정의 스니펫입니다. 클러스터 구성에 따라 필드가 다를 수 있으니, 실제 적용 전 kubectl explain 으로 스키마를 확인하세요.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-llm
spec:
  predictor:
    containers:
      - name: kserve-container
        image: <your-image>
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "24Gi"
          requests:
            nvidia.com/gpu: "1"
            cpu: "2"
            memory: "16Gi"
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 2
          failureThreshold: 24
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 6

포인트는 failureThresholdperiodSeconds 조합으로 **“모델 로딩에 필요한 최대 시간”**을 커버하는 것입니다. 예를 들어 위 readiness는 대략 5초 * 24 = 120초 동안 준비 실패를 허용합니다.

4) scale-to-zero가 503를 만든다: 최소 레플리카로 콜드스타트 제거

Knative 기반 KServe는 기본적으로 scale-to-zero가 켜져 있거나, 트래픽이 낮으면 0으로 내려갈 수 있습니다. GPU 모델은 콜드스타트가 길기 때문에 이 상태에서 첫 요청은 쉽게 타임아웃되고 503으로 보입니다.

대응

  • 최소 레플리카를 1로 고정
  • 또는 스케줄 기반 워밍업 잡을 두기

환경에 따라 annotation 또는 KServe 설정으로 minScale 을 줄 수 있습니다. 예시는 다음과 같습니다.

metadata:
  annotations:
    autoscaling.knative.dev/minScale: "1"
    autoscaling.knative.dev/maxScale: "3"

이 한 줄로 503이 “극적으로” 줄어드는 케이스가 많습니다. 비용은 늘지만, 운영 안정성 관점에서 GPU 서빙은 대개 minScale=1 이 기본값에 가깝습니다.

5) 동시성 제어가 곧 VRAM 제어다

LLM이나 diffusion류는 동시 요청이 늘면 VRAM이 선형 이상으로 증가합니다. 특히 KV 캐시가 있는 모델은 **동시성 N 이 곧 KV 캐시 N**이 됩니다.

따라서 OOM을 잡으려면 “배치 크기”만 보지 말고 아래를 함께 묶어야 합니다.

  • Pod 당 동시성 제한
  • 큐잉 정책(대기 허용 vs 즉시 실패)
  • 토큰 길이 제한(입력, 출력)
  • 서버 워커 수(프로세스 수)

실전 팁

  • Python 서버에서 workers=1 로 시작해 모델 중복 로딩을 피하기
  • 먼저 동시성을 낮춰 OOM을 없애고, 그 다음 TPS를 올리는 방향으로 튜닝하기

예를 들어 Gunicorn을 쓴다면 아래처럼 “GPU 모델은 프로세스 복제 비용이 크다”는 전제에서 접근합니다.

gunicorn app:app \
  -k uvicorn.workers.UvicornWorker \
  --workers 1 \
  --threads 4 \
  --timeout 0

워커를 늘려 처리량을 올리고 싶다면, 보통은 Pod 수를 늘리는 스케일 아웃이 더 예측 가능하고, VRAM OOM에도 강합니다.

6) 모델 다운로드와 압축 해제가 RAM OOM의 주범일 때

Hugging Face나 S3에서 모델을 받아서 컨테이너 로컬에 풀어 쓰는 구조라면, 초기화 시점에 다음이 동시에 일어납니다.

  • 큰 파일 다운로드 버퍼
  • 압축 해제 임시 공간
  • 로딩 중 메모리 매핑 또는 역직렬화

이때 컨테이너 메모리 limit이 빡빡하면 OOMKilled 로 죽고, 그 결과 readiness가 통과하지 못해 503이 연쇄 발생합니다.

대응 체크리스트

  • 모델을 이미지에 bake-in 해서 런타임 다운로드를 제거
  • 또는 PVC에 미리 받아두고 read-only로 마운트
  • 임시 디렉터리(TMPDIR)를 충분한 디스크로
  • 초기화 피크를 기준으로 메모리 limit 산정

PVC를 쓰는 단순 예시는 다음과 같습니다.

spec:
  predictor:
    volumes:
      - name: model-store
        persistentVolumeClaim:
          claimName: llm-model-pvc
    containers:
      - name: kserve-container
        volumeMounts:
          - name: model-store
            mountPath: /models
        env:
          - name: MODEL_PATH
            value: /models

이 구조는 재시작 시에도 모델이 남아 콜드스타트를 줄이고, 다운로드로 인한 RAM 스파이크도 완화합니다.

7) CUDA 메모리 단편화와 PyTorch allocator 튜닝

“VRAM이 남아 있는데도 CUDA OOM”이 뜨는 전형적인 원인은 단편화입니다. 특히 다양한 크기의 텐서를 자주 할당/해제하는 서빙 워크로드에서 잘 나타납니다.

PyTorch를 쓴다면 allocator 설정으로 완화할 수 있습니다.

env:
  - name: PYTORCH_CUDA_ALLOC_CONF
    value: "max_split_size_mb:128,garbage_collection_threshold:0.8"
  • max_split_size_mb 를 낮추면 큰 블록 분할을 줄여 단편화를 완화하는 경우가 있습니다.
  • 값은 정답이 없어서, 모델과 트래픽 패턴에 맞춰 실험이 필요합니다.

또한 아래도 같이 보세요.

  • torch.inference_mode() 적용으로 그래프/오토그라드 메모리 최소화
  • 요청 종료 시 캐시 해제 로직을 무분별하게 넣지 말기(오히려 단편화 악화 가능)

8) KServe 503과 OOM을 동시에 줄이는 운영 절차

한 번에 다 바꾸면 원인 추적이 불가능해집니다. 아래 순서로 “안정성 우선”으로 고정한 뒤 성능을 올리는 편이 빠릅니다.

1단계: 관측 고정

  • 503 발생 시점의 pod, revision, events 를 수집
  • nvidia-smi 를 주기적으로 스냅샷
  • 애플리케이션 로그에 “모델 로딩 완료” 타임스탬프 남기기

2단계: 503 제거

  • minScale=1 로 콜드스타트 제거
  • readiness를 “모델 준비 완료” 기준으로 엄격히
  • 인그레스 타임아웃은 모델 특성에 맞게 충분히(스트리밍이면 더 길게)

3단계: OOM 제거

  • 워커 수 1 로 시작
  • 동시성 제한을 낮추고 안정화
  • 토큰 길이 제한을 명시
  • 필요 시 4bit, KV 캐시 최적화, xFormers 등 적용

Stable Diffusion 계열처럼 VRAM 최적화 테크닉이 직접적인 경우도 많습니다.

4단계: 성능 올리기

  • Pod 수를 늘려 스케일 아웃
  • 배치 처리나 continuous batching 도입(가능한 런타임이면)
  • GPU 메모리 헤드룸을 남긴 상태에서만 동시성 상향

9) 자주 나오는 실패 시나리오와 처방전

시나리오 A: 배포 직후 2~3분간 503, 이후 정상

  • 원인: 콜드스타트 + readiness가 너무 빨리 통과 또는 라우팅이 먼저 열림
  • 처방: minScale=1, startupProbe 또는 readiness failureThreshold 상향, 모델 로딩 완료 후에만 ready

시나리오 B: 트래픽 스파이크 때만 503과 재시작이 같이 발생

  • 원인: 동시성 증가로 CUDA OOM 발생, 프로세스 크래시 또는 큐잉 불가로 503
  • 처방: 동시성 제한, 토큰 길이 제한, 워커 수 축소, 스케일 아웃

시나리오 C: 모델 다운로드가 있는 배포에서 OOMKilled 반복

  • 원인: 다운로드/압축 해제/로딩 피크가 메모리 limit 초과
  • 처방: PVC 프리웜, 이미지 bake-in, 메모리 limit 상향, initContainer로 사전 다운로드

10) 마무리: “503 먼저, OOM 다음”이 가장 빠르다

GPU 서빙에서 503과 OOM은 서로 영향을 주지만, 해결 우선순위는 보통 이렇습니다.

  1. 503을 먼저 줄여서 트래픽이 안정적으로 Pod에 도달하게 만든다
  2. 그 상태에서 동시성, 워커, 토큰 길이를 조절해 OOM을 없앤다
  3. 마지막으로 스케일 아웃과 런타임 최적화로 처리량을 올린다

운영에서 중요한 건 “최대 TPS”가 아니라 “장애 시에도 예측 가능한 동작”입니다. KServe는 구성 요소가 많아 보이지만, 결국은 프로브, 스케일, 동시성, 메모리 네 축만 제대로 잡으면 503과 OOM의 대부분은 재발하지 않게 만들 수 있습니다.