Published on

Transformers 로컬 LLM OOM? 7가지 즉시 해결

Authors

로컬 환경에서 Hugging Face Transformers로 LLM을 띄우다가 가장 자주 만나는 장애가 OOM(out of memory)입니다. 보통은 GPU VRAM 부족으로 CUDA out of memory가 터지지만, CPU RAM이 부족해 프로세스가 죽거나(리눅스라면 OOM Killer), 스왑을 타면서 속도가 급락하는 경우도 흔합니다.

이 글은 “지금 당장 모델을 돌려야 하는데 OOM이 난다”는 상황을 전제로, 효과가 큰 순서대로 7가지 해결책을 정리합니다. 각 항목은 단독으로도 효과가 있고, 여러 개를 조합하면 7B~13B급도 로컬에서 꽤 안정적으로 굴릴 수 있습니다.

문제 원인 분석이 필요하다면 리눅스 OOM Killer 로그를 먼저 확인하는 것도 좋습니다. 관련 내용은 리눅스 OOM Killer 로그로 메모리 누수 추적하기에서 더 깊게 다룹니다.

0. 먼저 확인: “어디” 메모리가 부족한가

OOM 메시지에 따라 처방이 달라집니다.

  • GPU VRAM OOM: 에러에 CUDA out of memory 또는 Tried to allocate 같은 문구가 보임
  • CPU RAM OOM: 프로세스가 강제 종료되거나, 커널 로그에 OOM Killer 기록이 남음
  • 단순 파편화: VRAM 총량은 남아 보이는데 큰 덩어리 할당이 실패

간단 체크 코드입니다.

import os
import torch

print("cuda available:", torch.cuda.is_available())
if torch.cuda.is_available():
    i = torch.cuda.current_device()
    print("gpu:", torch.cuda.get_device_name(i))
    print("allocated:", round(torch.cuda.memory_allocated(i)/1024**3, 2), "GB")
    print("reserved:", round(torch.cuda.memory_reserved(i)/1024**3, 2), "GB")

print("PYTORCH_CUDA_ALLOC_CONF:", os.environ.get("PYTORCH_CUDA_ALLOC_CONF"))

이제부터는 “GPU VRAM OOM”을 기준으로 설명하되, CPU RAM 이슈도 함께 커버합니다.

1) 1순위: 8bit 또는 4bit 양자화로 VRAM을 확 줄이기

가장 즉각적인 해결책입니다. bitsandbytes 기반 8bit 또는 4bit(NF4) 로딩을 쓰면, 가중치 메모리가 크게 줄어듭니다.

8bit/4bit 로딩 예시

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

model_id = "meta-llama/Llama-2-7b-hf"  # 예시

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

tok = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
)
  • load_in_4bit=True 가 가장 강력합니다.
  • compute_dtype는 GPU가 bf16을 잘 지원하면 torch.bfloat16, 아니면 torch.float16으로 바꾸세요.
  • 정확도 저하는 있을 수 있지만, “일단 돌아가게” 만드는 데는 최우선 카드입니다.

2) fp32를 버리고 fp16/bf16로: dtype만 바꿔도 절반 가까이 절약

양자화가 부담스럽거나, 품질 이슈로 fp16/bf16만 쓰고 싶다면 dtype를 내리세요. fp32는 로컬 추론에서 거의 불필요합니다.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "mistralai/Mistral-7B-Instruct-v0.2"  # 예시

tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.float16,  # 또는 torch.bfloat16
    device_map="auto",
)

추가 팁:

  • Ampere 이후 GPU는 bf16이 안정적인 경우가 많습니다.
  • 일부 GPU/드라이버 조합에서는 fp16이 더 빠르거나 호환성이 좋습니다.

3) device_map과 오프로딩(offload)으로 “모델을 쪼개서” 올리기

VRAM이 부족하면 GPU에 전부 못 올립니다. 이때는 acceleratedevice_map="auto"와 CPU/disk 오프로딩을 사용해 일부 레이어를 CPU로 보내거나, NVMe로 스필(spill)합니다.

from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "meta-llama/Llama-2-13b-hf"  # 예시

tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    offload_folder="./offload",  # 디스크 오프로딩 경로
    low_cpu_mem_usage=True,
)

주의할 점:

  • CPU 오프로딩은 VRAM을 줄여주지만 속도는 느려질 수 있습니다.
  • 디스크 오프로딩은 NVMe에서 그나마 쓸 만하며, SATA SSD나 HDD면 체감이 큽니다.

4) KV 캐시가 터진다: 컨텍스트 길이와 max_new_tokens를 줄이기

LLM 추론에서 “가중치”만큼이나 무서운 게 KV 캐시입니다. 특히 긴 프롬프트(context length) + 긴 생성(max_new_tokens) 조합은 KV 캐시가 눈덩이처럼 커져서 OOM을 유발합니다.

즉시 적용할 수 있는 안전한 조합은 다음입니다.

  • 프롬프트를 줄인다(불필요한 로그/문서 원문을 그대로 넣지 않기)
  • max_new_tokens를 줄인다
  • 배치 크기를 1로 유지한다
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

model_id = "mistralai/Mistral-7B-Instruct-v0.2"

tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.float16,
    device_map="auto",
)

prompt = "요약: 아래 텍스트를 5줄로 요약해줘..."  # 예시
inputs = tok(prompt, return_tensors="pt").to(model.device)

with torch.inference_mode():
    out = model.generate(
        **inputs,
        max_new_tokens=128,      # 길게 주면 KV 캐시가 커짐
        do_sample=False,
        use_cache=True,
    )

print(tok.decode(out[0], skip_special_tokens=True))

추가로, “대화 히스토리”를 무한히 누적하는 코드가 OOM의 단골 원인입니다. 히스토리는 요약하거나, 최근 N턴만 유지하는 식으로 제한하세요.

5) 배치/패딩 최적화: padding="longest"와 마이크로배치

추론에서도 여러 요청을 한 번에 처리하려고 배치를 키우면 OOM 확률이 급상승합니다. 특히 패딩이 많이 들어가면 “실제 토큰 수보다 더 큰 텐서”가 만들어져 메모리를 잡아먹습니다.

  • 가능하면 배치 크기 1부터 안정화
  • 배치가 필요하면 padding="longest"로 불필요한 패딩 최소화
  • 입력 길이 편차가 큰 요청은 배치를 나누기
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("gpt2")
texts = [
    "짧은 문장",
    "상대적으로 훨씬 더 긴 문장..." * 50,
]

batch = tok(
    texts,
    return_tensors="pt",
    padding="longest",   # "max_length"는 불필요한 패딩을 크게 만들 수 있음
    truncation=True,
    max_length=512,
)

6) PyTorch 메모리 파편화 완화: PYTORCH_CUDA_ALLOC_CONF 설정

VRAM 총량은 남아 있는데도 큰 텐서 할당이 실패하는 경우가 있습니다. 이때는 메모리 파편화(fragmentation) 가능성이 큽니다.

아래 설정은 “큰 블록을 잘게 쪼개 관리”하도록 유도해 OOM 빈도를 낮추는 데 도움이 됩니다.

export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128"

상황에 따라 64, 128, 256 등으로 조절합니다.

추가로, 반복 실행 루프에서 메모리가 누적되는 느낌이 들면 다음을 점검하세요.

  • torch.no_grad() 또는 torch.inference_mode()를 사용했는가
  • 텐서를 리스트에 계속 append하고 있지 않은가
  • 예측 결과를 GPU 텐서로 계속 들고 있지 않은가

필요 시 다음처럼 캐시를 비울 수는 있지만, 근본 해결은 “누적 참조 제거”입니다.

import torch

torch.cuda.empty_cache()

7) 로딩/추론 코드를 “추론 모드”로 고정하고 불필요한 그래프 생성 차단

의외로 많은 코드가 추론인데도 학습 그래프를 만들고 있습니다. 그 자체로 메모리 낭비이며, 누적되면 OOM으로 이어집니다.

  • model.eval() 호출
  • torch.inference_mode() 사용
  • 불필요한 output_hidden_states=True, output_attentions=True 끄기
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "gpt2"

tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id).eval()

inputs = tok("Hello", return_tensors="pt")

with torch.inference_mode():
    out = model.generate(
        **inputs,
        max_new_tokens=50,
        return_dict_in_generate=False,
        output_scores=False,
    )

특히 디버깅 목적으로 output_hidden_states=True를 켜두면 레이어별 텐서가 대량으로 생성되어 VRAM이 쉽게 터집니다.

자주 묻는 상황별 빠른 처방

7B도 OOM이면

  • 1번(4bit) + 4번(컨텍스트/생성 길이 축소) 조합부터 적용
  • device_map="auto"로 일부 CPU 오프로딩

13B 이상인데 12GB VRAM이면

  • 사실상 4bit + 오프로딩이 현실적인 선택
  • 속도보다 “성공”이 목표라면 디스크 오프로딩도 고려

CPU RAM이 먼저 터지면

  • low_cpu_mem_usage=True 적용
  • 오프로딩 폴더를 NVMe로 지정
  • 컨테이너/WSL이면 메모리 제한이 걸려 있지 않은지 확인

리눅스에서 프로세스가 갑자기 죽는다면 OOM Killer 로그로 원인을 확정하는 게 빠릅니다. 자세한 방법은 리눅스 OOM Killer 로그로 메모리 누수 추적하기 참고가 도움이 됩니다.

마무리 체크리스트

OOM을 “즉시” 잡는 우선순위는 보통 아래 순서가 가장 효율적입니다.

  1. 4bit 또는 8bit 양자화 적용
  2. torch_dtypefp16 또는 bf16
  3. device_map="auto" + 오프로딩으로 분산
  4. 컨텍스트 길이와 max_new_tokens 줄여 KV 캐시 폭발 방지
  5. 배치/패딩 최적화
  6. 파편화 완화용 PYTORCH_CUDA_ALLOC_CONF 설정
  7. 추론 모드 고정 및 불필요 출력 옵션 제거

여기까지 적용해도 불안정하다면, 결국은 “모델 크기 대비 하드웨어 스펙” 문제일 가능성이 큽니다. 그때는 더 작은 모델(예: 3B~7B)로 다운스케일하거나, vLLM 같은 서빙 엔진/스케줄러를 고려하는 것이 장기적으로 비용이 덜 듭니다.