- Published on
KServe·KFServing GPU 추론 429·OOM 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 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는 대개 아래 중 하나에서 발생합니다.
- queue-proxy(혹은 activator) 큐 포화
- Pod 당 허용 동시 요청을 초과
- 처리 시간이 길어 큐가 빨리 쌓임
- 오토스케일 지연
- scale-to-zero 후 첫 요청이 activator를 거치며 대기
- 새 Pod가 뜨기 전에 큐가 한계에 도달
- 업스트림(게이트웨이/인그레스) 레이트 리밋
- 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 describe로InferenceService상태와 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 memory면 GPU 메모리 모델(동시성/배치/입력) 문제
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가지를 고정 순서로 조정하는 것입니다.
- 동시성 제한: Pod 당 1로 시작
- 배치 제한: dynamic batching을 쓰면 최대 배치 상한을 둠
- 입력 상한: 토큰 길이, 이미지 해상도, 프레임 수에 상한을 둠
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이 같이 올 때의 전형적인 악순환
운영에서 가장 흔한 패턴은 이렇습니다.
- 트래픽 증가
- 동시성 증가 또는 큐잉 증가
- GPU OOM으로 Pod 크래시
- 레플리카 감소로 처리량 급감
- 큐 포화로 429 폭증
- 클라이언트 재시도로 더 악화
해결은 “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분 안에 결론 내는 순서
- 429의 발생 지점 확인:
queue-proxy로그인지, 인그레스/게이트웨이인지 - OOM 종류 분리:
OOMKilled(CPU) vsCUDA out of memory(GPU) - Pod 동시성 1로 고정 후 재현
- 입력 상한(토큰/해상도) 강제
- 배치 상한 설정(dynamic batching 사용 시)
- minScale로 콜드스타트 제거(필요 시)
- 재시도 정책 정리(클라이언트/게이트웨이)
- 스케일 아웃이 실제로 가능한지 확인(ImagePullBackOff, 노드 NotReady)
10) 마무리: “GPU 서빙은 동시성이 아니라 제어가 핵심”
KServe(KFServing)에서 429와 OOM은 각각 다른 층의 문제처럼 보이지만, 실제로는 동시성 제어 실패가 두 문제를 동시에 증폭시키는 경우가 많습니다.
- 429는 큐와 스케일 정책으로 제어하고
- OOM은 “요청당 GPU 메모리”를 입력/배치/동시성으로 제한하며
- 둘을 함께 안정화한 뒤에야 처리량 최적화(배치, 엔진 최적화, 양자화)를 진행하는 것이 가장 빠른 길입니다.
원하시면 사용 중인 스택(예: vLLM, Triton, TorchServe, TensorRT-LLM, Transformers), GPU 종류, 평균 입력 길이/해상도, p95 지연시간을 기준으로 target/minScale/배치 상한을 산정하는 튜닝 표까지 구체적으로 만들어 드릴 수 있습니다.