Published on

TorchServe GPU OOM 해결 - 배치·워커·A10 최적화

Authors

서빙 중 CUDA out of memory 는 단순히 배치가 커서가 아니라, TorchServe의 워커 모델과 PyTorch CUDA 메모리 캐시, 동시성, 입력 길이 분포, 그리고 A10의 메모리 용량·대역폭 특성이 맞물려 발생하는 경우가 많습니다. 이 글에서는 TorchServe에서 OOM을 재현 가능하게 만들고, 원인을 좁히고, 배치·워커·A10 최적화로 안정적인 설정을 찾는 흐름으로 정리합니다.

OOM을 먼저 분류하자: “진짜 부족” vs “파편화/피크”

TorchServe OOM은 크게 두 부류로 나뉩니다.

  1. 진짜로 VRAM이 부족한 경우
  • 모델 파라미터가 크거나, 입력이 길거나, 배치가 커서 항상 같은 지점에서 터집니다.
  • 재시도해도 거의 동일한 시점에 실패합니다.
  1. 메모리 파편화·피크·동시성으로 간헐적으로 터지는 경우
  • 같은 트래픽에서도 어떤 순간에만 OOM이 납니다.
  • 워커 수를 늘렸을 때만 발생하거나, 특정 입력 길이(긴 문장, 큰 이미지)에서만 발생합니다.

이 분류가 중요한 이유는 해결책이 다르기 때문입니다. 1)은 배치·정밀도·모델 최적화로 “절대량”을 줄여야 하고, 2)는 워커/동시성/할당 패턴을 바꿔 “피크와 파편화”를 줄여야 합니다.

TorchServe에서 OOM이 나는 대표 패턴 5가지

1) 워커 수만큼 모델이 GPU에 중복 로드

TorchServe는 기본적으로 워커 프로세스마다 모델을 로드합니다. GPU 모델 서빙에서 minWorkers=2 로만 올려도, 모델 가중치와 각종 버퍼가 2배로 늘어납니다.

  • 작은 모델은 괜찮지만, A10 24GB에서도 중형 이상 모델은 워커 2~4개에서 바로 OOM이 납니다.

2) 배치 크기와 입력 길이 분포가 곱으로 폭발

NLP나 멀티모달에서 메모리는 보통

  • 배치 크기
  • 시퀀스 길이(토큰 수)
  • 히든 사이즈 에 비례해 증가합니다.

즉 “평균 입력” 기준으로는 안전해도, 긴 입력이 섞인 배치에서 순간 피크가 크게 튀며 OOM이 납니다.

3) 동시 요청이 워커 큐에 쌓이며 “숨은 마이크로배치” 발생

TorchServe는 요청을 워커로 라우팅하고, 워커 내부에서 전처리·후처리·추론이 겹칩니다. 외부에서 동시에 요청이 들어오면, 체감상 배치가 커진 것과 유사한 피크가 생깁니다.

4) PyTorch CUDA 캐시와 파편화

PyTorch는 성능을 위해 GPU 메모리를 캐싱합니다. 이때 다양한 크기의 텐서 할당/해제가 반복되면 파편화로 인해

  • reserved 는 큰데
  • allocated 는 상대적으로 작고
  • “연속된 큰 블록”을 못 구해 OOM 이 나는 전형적인 케이스가 나옵니다.

5) 핸들러에서 텐서 참조가 남아 메모리 누수처럼 보이는 경우

TorchServe 커스텀 핸들러에서

  • GPU 텐서를 리스트에 쌓아두거나
  • torch.no_grad() / torch.inference_mode() 미적용
  • 반환 직전까지 큰 텐서를 잡고 있는 구조 라면 트래픽이 증가할수록 점진적으로 OOM이 납니다.

A10에서 특히 신경 쓸 포인트

A10은 보통 24GB VRAM 환경이 많아 “넉넉해 보이지만” 다음 특성 때문에 OOM이 더 빨리 체감될 수 있습니다.

  • 워커 중복 로드가 치명적: 24GB는 워커 2개로도 금방 차는 구간이 존재합니다.
  • BF16 지원은 강력한 카드: FP16만 고집하기보다 BF16이 가능한 모델이면 BF16이 안정적인 경우가 많습니다.
  • MIG가 아닌 환경이 일반적: 한 프로세스가 메모리를 넓게 잡아먹기 쉬워, 파편화/피크 관리가 중요합니다.

정밀도 최적화는 장기적으로는 TensorRT가 가장 강력하지만, 운영 중 빠르게 안정화하려면 TorchServe 설정과 PyTorch 런타임 튜닝이 먼저입니다. 더 공격적으로 최적화할 계획이라면 FP8/엔진화 이슈는 별도 트러블슈팅이 필요합니다. 관련해서는 PyTorch→ONNX→TensorRT FP8 양자화 트러블슈팅 도 함께 참고해 두면 좋습니다.

1단계: “현재” 메모리 상태를 수치로 잡기

nvidia-smi 로 프로세스별 VRAM 확인

가장 먼저 워커 수만큼 프로세스가 뜨고, 그만큼 VRAM이 늘어나는지 확인합니다.

watch -n 1 nvidia-smi

핸들러에 메모리 로그 추가

OOM을 재현할 때는 요청 단위로 메모리 스냅샷을 남기는 게 빠릅니다.

import torch

def log_cuda_mem(tag: str):
    if not torch.cuda.is_available():
        return
    alloc = torch.cuda.memory_allocated() / 1024**2
    reserved = torch.cuda.memory_reserved() / 1024**2
    max_alloc = torch.cuda.max_memory_allocated() / 1024**2
    print(f"[{tag}] alloc={alloc:.1f}MiB reserved={reserved:.1f}MiB max_alloc={max_alloc:.1f}MiB")

핸들러의 preprocess / inference / postprocess 구간에 태그를 박아두면, 어디서 피크가 터지는지 바로 드러납니다.

파편화 의심 시 memory_summary

OOM 직전이나 직후에 아래를 찍으면 “reserved는 큰데 할당이 안 되는” 패턴을 확인할 수 있습니다.

print(torch.cuda.memory_summary())

2단계: TorchServe 설정으로 워커·배치를 먼저 안정화

TorchServe 튜닝은 대개 아래 우선순위가 안전합니다.

  1. 워커 수를 줄여 “중복 로드”를 끊는다
  2. 배치를 줄여 “피크”를 줄인다
  3. 동시성을 제한해 “숨은 배치”를 줄인다
  4. 그 다음 정밀도/엔진화로 처리량을 올린다

config.properties 예시

아래는 “OOM 방지 우선”의 보수적인 시작점입니다.

inference_address=http://0.0.0.0:8080
management_address=http://0.0.0.0:8081
metrics_address=http://0.0.0.0:8082

# 요청이 과도하게 쌓이지 않게 제한
job_queue_size=100

# 워커당 동시성 과도 증가 방지(환경에 따라 조정)
number_of_netty_threads=8

# 응답 지연이 길어져도 워커가 무한 대기하지 않게
default_response_timeout=120

모델별 워커/배치는 model-config.yaml 또는 등록 시 파라미터로 관리하는 편이 재현성이 좋습니다.

모델 등록 시 워커/배치 파라미터

TorchServe는 모델 등록 시 minWorkers, maxWorkers, batchSize, maxBatchDelay 를 줄 수 있습니다. (환경/버전에 따라 전달 방식이 다를 수 있으니 사용 중인 배포 방식에 맞춰 적용하세요.)

권장 시작점:

  • GPU 1장당 워커 1개부터 시작
  • 배치가 필요하면 batchSize 를 작은 값으로 시작
  • maxBatchDelay 는 지연 허용 범위 내에서만

개념적으로는 다음과 같이 생각하면 됩니다.

  • 워커를 늘리면: 지연은 줄 수 있지만 VRAM이 급증
  • 배치를 늘리면: 처리량은 늘 수 있지만 피크 VRAM이 급증

OOM이 나는 상황에서는 “워커 증가로 처리량 확보” 전략이 가장 위험합니다.

3단계: 핸들러 코드에서 OOM을 유발하는 습관 제거

torch.inference_mode() 는 거의 필수

추론인데도 그래프가 일부 남거나 오토그래드 관련 오버헤드가 생기면 메모리 피크가 커집니다.

with torch.inference_mode():
    outputs = model(**inputs)

입력 텐서를 즉시 GPU로 올리지 말고, 필요한 것만 올리기

전처리에서 모든 중간 결과를 GPU로 올리면 피크가 커집니다. 특히 이미지 전처리나 토크나이징 결과를 무심코 GPU로 넘기면 손해가 큽니다.

  • CPU 전처리
  • 필요한 텐서만 .to("cuda")
  • 불필요한 큰 텐서는 지역 변수 스코프를 짧게

후처리에서 GPU 텐서를 CPU로 옮기고 참조 끊기

pred = outputs.logits
pred_cpu = pred.detach().float().cpu()
# pred, outputs 등 큰 텐서 참조를 길게 유지하지 않기

긴 입력을 자르는 “가드레일” 추가

OOM의 80%는 “긴 꼬리 입력”에서 발생합니다. 운영에서는 품질보다 안정성이 우선인 구간이 존재하므로, 최대 길이를 제한하거나 긴 입력은 별도 큐로 보내는 정책이 필요합니다.

MAX_TOKENS = 2048

if input_ids.shape[-1] > MAX_TOKENS:
    input_ids = input_ids[:, :MAX_TOKENS]

4단계: PyTorch CUDA allocator 튜닝으로 파편화 완화

파편화가 의심되면 PYTORCH_CUDA_ALLOC_CONF 를 먼저 시도해볼 가치가 큽니다. 특히 다양한 크기의 입력이 섞이는 온라인 서빙에서 효과가 나는 경우가 많습니다.

export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128,garbage_collection_threshold:0.8"
  • max_split_size_mb 를 낮추면 큰 블록 분할을 줄여 파편화를 완화하는 방향으로 작동할 수 있습니다.
  • 다만 워크로드에 따라 성능/메모리 트레이드오프가 있으니, 반드시 부하 테스트로 확인해야 합니다.

또한 “OOM 직전 메모리 정리”를 위해 무조건 torch.cuda.empty_cache() 를 남발하는 것은 권장하지 않습니다. 캐시를 비우면 오히려 할당이 잦아지고 지연이 튈 수 있습니다. 대신 할당 패턴을 단순화(배치 고정, 입력 길이 상한, 워커 1개)하는 게 더 근본적입니다.

5단계: A10에서 현실적인 목표 설정과 튜닝 순서

A10 단일 GPU에서의 실전 튜닝 순서는 다음이 안전합니다.

1) 워커 1개 고정으로 안정화

  • minWorkers=1, maxWorkers=1
  • OOM이 사라지는지 확인

여기서도 OOM이면 “진짜 부족”일 확률이 높고, 아래로 넘어가야 합니다.

2) 정밀도 낮추기: FP16 또는 BF16

모델이 지원하면 BF16이 수치 안정성 측면에서 운영 친화적인 경우가 많습니다.

model = model.to("cuda")
model.eval()

use_bf16 = True
amp_dtype = torch.bfloat16 if use_bf16 else torch.float16

with torch.inference_mode(), torch.autocast(device_type="cuda", dtype=amp_dtype):
    outputs = model(**inputs)

3) 배치는 “작게 시작해서” 점진적으로

  • batchSize=1 로 안정화
  • 입력 길이 상한을 둔 뒤 batchSize=2, 4 순으로 증가
  • OOM이 아니라 지연과 처리량 곡선을 보고 결정

4) 동시성은 워커가 아니라 “외부에서” 제어

워커를 늘리면 모델이 중복 로드되어 VRAM이 급증하므로, 동시성은 다음으로 제어하는 편이 안전합니다.

  • 인그레스/게이트웨이에서 동시 요청 제한
  • 큐 기반 비동기 처리
  • K8s HPA로 파드 수를 늘리고 파드당 워커 1개 유지

K8s에서 파드를 늘리는 전략을 쓰면 스케줄링 이슈가 같이 따라오므로, GPU 파드가 Pending 에 걸릴 때의 체크리스트도 알아두면 좋습니다. 관련 글: K8s Pod가 Pending? 스케줄링 실패 12가지

운영에서 자주 쓰는 “안전한” 레시피

레시피 A: OOM 즉시 멈추게 하지 말고 우회(폴백) 설계

GPU OOM은 종종 특정 입력에서만 발생합니다. 이때 전체 장애로 확산되지 않게

  • 긴 입력은 거절하거나 축약
  • CPU 경량 모델로 폴백
  • 재시도는 제한적으로 같은 회로 차단기 패턴이 유효합니다.

서빙 실패를 재시도/폴백으로 다루는 관점은 GPU 서빙에도 그대로 적용됩니다. 장애 전파를 막는 실전 패턴은 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커 의 설계 아이디어를 참고할 수 있습니다.

레시피 B: 입력 길이·해상도 상한 + 워커 1 + 소배치

  • 입력 상한으로 피크를 제거
  • 워커 1로 중복 로드 제거
  • 배치를 소폭만 올려 처리량 확보

이 조합이 A10에서 가장 “예측 가능한” 운영 경험을 줍니다.

체크리스트: OOM이 계속 날 때 빠른 진단 순서

  1. nvidia-smi 에서 TorchServe 워커 프로세스 수와 VRAM 사용량이 워커 수만큼 증가하는가
  2. 워커를 1로 줄이면 OOM이 사라지는가
  3. OOM이 특정 입력(긴 텍스트, 큰 이미지)에서만 나는가
  4. 핸들러에 torch.inference_mode()autocast 가 적용되어 있는가
  5. allocated 대비 reserved 가 과도하게 큰가(파편화 의심)
  6. 배치가 실제로는 더 크게 형성되는 구간(동시 요청, 큐 적체)이 있는가

마무리: “처리량”보다 먼저 “예측 가능성”을 만든다

TorchServe GPU OOM을 잡는 가장 빠른 길은, 복잡한 최적화보다 먼저

  • 워커 1개로 중복 로드를 제거하고
  • 입력 분포의 꼬리를 자르고
  • 배치와 동시성을 보수적으로 시작한 뒤
  • 정밀도와 엔진화를 단계적으로 적용 하는 것입니다.

특히 A10 환경에서는 워커를 늘려 처리량을 얻는 전략이 OOM을 부르는 경우가 많습니다. 먼저 안정적인 단일 워커 구성에서 메모리 피크를 통제하고, 그 다음에 파드를 수평 확장하는 쪽이 운영 난이도 대비 효과가 좋습니다.