Published on

KServe·KFServing GPU 추론 429·OOM 해결 가이드

Authors

서빙 중인 LLM/비전 모델이 갑자기 429 Too Many Requests를 뱉거나, 트래픽이 조금만 올라가도 GPU가 CUDA out of memory로 터지는 문제는 KServe(구 KFServing)에서 매우 흔합니다. 특히 다음 조합에서 자주 발생합니다.

  • Knative 기반 InferenceService에 트래픽이 몰림
  • 컨테이너 동시성(concurrency)과 배치(batch)가 모델 특성과 어긋남
  • GPU 메모리를 요청당 선형으로 더 쓰는 모델(긴 시퀀스, 큰 이미지, KV 캐시 등)
  • 오토스케일이 늦거나(콜드스타트), 스케일 아웃이 불가능한 제약(가용 GPU 부족)

이 글은 429는 “큐가 터졌다”, **OOM은 “요청 1개당 GPU 메모리 모델이 잘못됐다”**라는 관점으로 접근해, 재현부터 튜닝까지 한 번에 정리합니다.

1) KServe/KFServing에서 429가 나는 대표 경로

KServe는 내부적으로 Knative Serving(또는 RawDeployment 모드)을 사용합니다. 429는 대개 아래 중 하나에서 발생합니다.

  1. queue-proxy(혹은 activator) 큐 포화
    • Pod 당 허용 동시 요청을 초과
    • 처리 시간이 길어 큐가 빨리 쌓임
  2. 오토스케일 지연
    • scale-to-zero 후 첫 요청이 activator를 거치며 대기
    • 새 Pod가 뜨기 전에 큐가 한계에 도달
  3. 업스트림(게이트웨이/인그레스) 레이트 리밋
    • Istio, NGINX, API Gateway에서 429를 먼저 반환

핵심은 “동시성”과 “대기열”을 제어하는 설정이 어디에 걸려 있는지 분리해서 보는 것입니다.

2) OOM이 나는 대표 원인: GPU 메모리의 3층 구조

GPU OOM은 단순히 resources.limits.nvidia.com/gpu: 1을 줬다고 해결되지 않습니다. 보통 다음 3가지가 합쳐져 터집니다.

  • 모델 상주 메모리(고정 비용): 가중치, 텐서RT 엔진, 컴파일 캐시
  • 요청당 활성화 메모리(가변 비용): 입력 크기, 배치 크기, 시퀀스 길이
  • 동시성에 의한 곱셈 효과: 동시 요청 수 × 요청당 활성화 메모리

즉, Pod 동시성을 4로 올리면 처리량이 4배가 아니라 OOM 확률이 4배가 되는 모델이 많습니다(특히 KV 캐시가 큰 LLM). 로컬 LLM OOM을 다룬 글에서처럼 4bit, KV 캐시, 배치 튜닝은 서빙에서도 그대로 적용됩니다. 필요하면 Transformers 로컬 LLM OOM - 4bit+KV 캐시 튜닝도 함께 참고하세요.

3) 재현과 1차 진단: 로그·메트릭에서 무엇을 볼까

3.1 429 진단 체크

  • kubectl describeInferenceService 상태와 URL 확인
  • Revision(Knative) 이벤트에서 스케일/레디 실패 확인
  • Pod 내 queue-proxy 로그에서 429 발생 시점 확인
kubectl get inferenceservice -A
kubectl describe inferenceservice -n ml my-model

# Knative Revision/Pod 확인
kubectl get revision -n ml
kubectl get pod -n ml -l serving.kserve.io/inferenceservice=my-model -o wide

# queue-proxy 로그(429의 근원지인지 확인)
POD=$(kubectl get pod -n ml -l serving.kserve.io/inferenceservice=my-model -o jsonpath='{.items[0].metadata.name}')
kubectl logs -n ml "$POD" -c queue-proxy --tail=200

queue-proxy에서 429가 보이면, 대개 컨테이너 동시성/큐 설정/오토스케일 문제입니다.

3.2 OOM 진단 체크

  • 컨테이너 로그에 CUDA out of memory 또는 프레임워크별 OOM 메시지
  • Pod 이벤트에 OOMKilled(CPU 메모리)와 혼동하지 않기
  • GPU 메모리 사용량을 nvidia-smi로 확인
kubectl logs -n ml "$POD" -c kserve-container --tail=200
kubectl describe pod -n ml "$POD" | sed -n '1,200p'

# GPU 메모리 확인(디버그용)
kubectl exec -n ml -it "$POD" -c kserve-container -- nvidia-smi

여기서 중요한 분기:

  • OOMKilled가 뜨면 CPU 메모리 limit 문제
  • CUDA out of memoryGPU 메모리 모델(동시성/배치/입력) 문제

4) 429 해결: “동시성 제한 + 스케일 정책”을 먼저 잡는다

4.1 Pod 당 동시성(Concurrency)부터 보수적으로

Knative/KServe에서 Pod 당 동시성을 높이면 처리량이 오를 수 있지만, GPU 추론은 대부분 동시성에 취약합니다. 먼저 Pod 당 동시성을 1~2로 낮추고 안정화한 뒤, 배치나 다중 워커로 처리량을 올리는 편이 안전합니다.

아래 예시는 InferenceService에 Knative annotation을 넣어 컨테이너 동시성을 제한하는 패턴입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: my-model
  namespace: ml
spec:
  predictor:
    containers:
      - name: kserve-container
        image: myrepo/myimage:latest
    # Knative annotations는 보통 predictor 하위 템플릿에 들어갑니다.
    # 설치 버전에 따라 위치가 다를 수 있어, 적용 후 Revision에 반영됐는지 확인하세요.
  annotations:
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/metric: "concurrency"

운영 팁:

  • target을 1로 두면 “Pod 하나가 평균 동시 1을 넘기지 않도록” 스케일합니다.
  • 429가 줄어드는 대신 스케일 아웃이 늘 수 있으니, 클러스터 GPU 여유와 함께 봐야 합니다.

4.2 scale-to-zero를 끄거나 최소 레플리카를 둔다

콜드스타트가 길면 activator 대기열이 먼저 터져 429/타임아웃이 발생합니다. GPU 모델은 로딩이 길기 때문에, 트래픽이 상시 있으면 최소 레플리카를 두는 편이 낫습니다.

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

4.3 요청 타임아웃과 큐잉 정책을 현실적으로

  • 추론이 20~60초 걸릴 수 있는데 기본 타임아웃이 짧으면 상위 레이어에서 재시도 폭탄이 납니다.
  • 재시도가 동시성을 폭발시키며 429를 더 악화시킵니다.

Istio/게이트웨이/클라이언트 재시도 정책을 함께 점검하세요.

5) OOM 해결: “동시성 × 배치 × 입력”을 수식으로 다룬다

OOM을 막는 가장 실전적인 방법은 아래 3가지를 고정 순서로 조정하는 것입니다.

  1. 동시성 제한: Pod 당 1로 시작
  2. 배치 제한: dynamic batching을 쓰면 최대 배치 상한을 둠
  3. 입력 상한: 토큰 길이, 이미지 해상도, 프레임 수에 상한을 둠

5.1 동시성 1에서 안정화한 뒤 배치로 처리량 확보

예를 들어 Triton 기반 KServe라면 dynamic batching으로 처리량을 올리되, max_batch_size를 보수적으로 잡습니다.

# config.pbtxt 예시
name: "model"
platform: "pytorch_libtorch"
max_batch_size: 4

dynamic_batching {
  preferred_batch_size: [ 1, 2, 4 ]
  max_queue_delay_microseconds: 2000
}

여기서 포인트는:

  • 동시성(요청 병렬) 대신 배치(요청 결합) 로 GPU 효율을 올린다
  • 배치 상한을 두지 않으면 특정 순간에 배치가 커지며 OOM이 난다

5.2 입력 크기 가드레일: 토큰/해상도 상한은 필수

LLM은 max_new_tokens, max_input_tokens 상한을 API 레벨에서 강제해야 합니다. 이미지 모델은 width/height 상한과 리사이즈 정책이 필요합니다.

FastAPI 예시로 입력 상한을 강제하면, “비정상 요청 1개가 GPU를 터뜨리는” 사고를 크게 줄일 수 있습니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

MAX_INPUT_TOKENS = 4096
MAX_NEW_TOKENS = 1024

class Req(BaseModel):
    prompt: str
    max_new_tokens: int = 256

@app.post("/v1/generate")
def generate(req: Req):
    if req.max_new_tokens > MAX_NEW_TOKENS:
        raise HTTPException(status_code=400, detail="max_new_tokens too large")

    # 토큰 길이 계산은 사용하는 토크나이저로 측정
    # input_tokens = len(tokenizer.encode(req.prompt))
    input_tokens = len(req.prompt)  # 예시
    if input_tokens > MAX_INPUT_TOKENS:
        raise HTTPException(status_code=400, detail="prompt too long")

    return {"ok": True}

5.3 GPU 메모리 파편화와 워밍업

PyTorch 계열은 메모리 파편화로 인해 “남아 보이는데도 OOM”이 발생할 수 있습니다. 워밍업 요청을 넣고, 가능한 경우 allocator 설정을 조정합니다.

# 예: 파편화 완화(환경에 따라 효과 상이)
export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128,expandable_segments:True"

또한 모델 로딩 직후 첫 요청이 가장 무겁습니다(JIT, 커널 캐시). readiness probe 전에 워밍업을 수행하거나, 초기화 단계에서 더미 추론을 한 번 돌려 피크 메모리를 확인하세요.

6) 429와 OOM이 같이 올 때의 전형적인 악순환

운영에서 가장 흔한 패턴은 이렇습니다.

  1. 트래픽 증가
  2. 동시성 증가 또는 큐잉 증가
  3. GPU OOM으로 Pod 크래시
  4. 레플리카 감소로 처리량 급감
  5. 큐 포화로 429 폭증
  6. 클라이언트 재시도로 더 악화

해결은 “OOM을 먼저 끊고, 그 다음 429를 줄이는” 순서가 안전합니다.

  • OOM을 끊기: Pod 동시성 1, 배치 상한, 입력 상한
  • 429 줄이기: minScale, target 조정, 타임아웃/재시도 정책 정리

7) 배포/노드 이슈로 보이는 문제도 함께 점검

가끔 429/OOM처럼 보이지만 실제 원인은 이미지 풀 실패, 노드 불안정인 경우가 있습니다.

  • 새 레플리카가 떠야 하는데 ImagePullBackOff로 못 뜨면, 남은 Pod에 트래픽이 몰려 429/OOM이 연쇄로 납니다.
  • GPU 노드가 불안정하면 재스케줄링이 반복되어 콜드스타트가 늘고 429가 증가합니다.

아래 글을 함께 점검하면 “스케일 아웃이 안 되는” 원인을 빨리 찾을 수 있습니다.

8) 실전 권장 설정 프리셋(출발점)

아래는 “안전하게 시작해서 올리는” 쪽의 프리셋입니다.

  • Pod 동시성: 1
  • 오토스케일 metric: concurrency
  • target: 1
  • minScale: 상시 트래픽이면 1 이상
  • 입력 상한: 토큰/해상도/프레임 수 강제
  • 배치: dynamic batching 사용 시 max_batch_size 상한
  • 타임아웃: 모델 p95 지연시간 기반으로 설정(게이트웨이/클라이언트 포함)

InferenceService 예시(개념 템플릿):

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-infer
  namespace: ml
  annotations:
    autoscaling.knative.dev/metric: "concurrency"
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/minScale: "1"
    autoscaling.knative.dev/maxScale: "8"
spec:
  predictor:
    containers:
      - name: kserve-container
        image: myrepo/gpu-infer:1.0.0
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "16Gi"
          requests:
            cpu: "2"
            memory: "8Gi"
        env:
          - name: PYTORCH_CUDA_ALLOC_CONF
            value: "max_split_size_mb:128,expandable_segments:True"

주의: KServe/Knative 버전에 따라 annotation 위치와 지원 키가 달라질 수 있으니, 적용 후 Revision에 반영됐는지 반드시 확인하세요.

9) 체크리스트: 30분 안에 결론 내는 순서

  1. 429의 발생 지점 확인: queue-proxy 로그인지, 인그레스/게이트웨이인지
  2. OOM 종류 분리: OOMKilled(CPU) vs CUDA out of memory(GPU)
  3. Pod 동시성 1로 고정 후 재현
  4. 입력 상한(토큰/해상도) 강제
  5. 배치 상한 설정(dynamic batching 사용 시)
  6. minScale로 콜드스타트 제거(필요 시)
  7. 재시도 정책 정리(클라이언트/게이트웨이)
  8. 스케일 아웃이 실제로 가능한지 확인(ImagePullBackOff, 노드 NotReady)

10) 마무리: “GPU 서빙은 동시성이 아니라 제어가 핵심”

KServe(KFServing)에서 429와 OOM은 각각 다른 층의 문제처럼 보이지만, 실제로는 동시성 제어 실패가 두 문제를 동시에 증폭시키는 경우가 많습니다.

  • 429는 큐와 스케일 정책으로 제어하고
  • OOM은 “요청당 GPU 메모리”를 입력/배치/동시성으로 제한하며
  • 둘을 함께 안정화한 뒤에야 처리량 최적화(배치, 엔진 최적화, 양자화)를 진행하는 것이 가장 빠른 길입니다.

원하시면 사용 중인 스택(예: vLLM, Triton, TorchServe, TensorRT-LLM, Transformers), GPU 종류, 평균 입력 길이/해상도, p95 지연시간을 기준으로 target/minScale/배치 상한을 산정하는 튜닝 표까지 구체적으로 만들어 드릴 수 있습니다.