- Published on
Transformers 로컬 LLM OOM, 4bit+CPU 오프로딩
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
로컬에서 LLM을 transformers로 띄우려다 가장 먼저 부딪히는 벽이 CUDA OOM(Out Of Memory)입니다. 특히 7B~13B급 모델을 기본 설정(대개 FP16)으로 올리면, 추론만 해도 VRAM이 빠듯하거나 아예 로딩 단계에서 바로 터집니다.
이 글에서는 4bit 양자화(bitsandbytes) + CPU 오프로딩(accelerate의 device map) 조합으로, “일단 로딩부터 성공”시키고 실사용 가능한 속도/안정성까지 끌어올리는 방법을 정리합니다. 마지막에는 OOM을 재발시키는 흔한 실수(캐시/컨텍스트 길이/배치/옵션 충돌)도 체크리스트로 제공합니다.
관련해서 운영 중 장애를 추적하는 방식은 LLM 서버에도 그대로 적용됩니다. 예를 들어 서비스가 계속 죽고 재시작한다면 원인 추적은 이 글의 방식과 유사한 흐름으로 진행합니다: systemd 서비스가 계속 재시작될 때 원인 추적법
왜 OOM이 나는가: “가중치만”이 아니다
OOM은 단순히 모델 가중치(weight) 크기 때문만이 아닙니다. 로컬 추론에서 메모리를 잡아먹는 항목은 크게 4가지입니다.
- 가중치(Weights): FP16이면 파라미터당 2바이트. 7B 모델은 대략 14GB 근처(오버헤드 제외)로 생각하면 됩니다.
- KV 캐시(KV Cache): 시퀀스 길이(
max_new_tokens, 프롬프트 길이,max_position_embeddings)와 레이어/헤드 수에 비례해서 늘어납니다. 긴 문맥을 쓰면 추론 중 VRAM이 계속 증가합니다. - 활성화/버퍼(Activations/Buffers): 배치 크기, attention 구현, 일부 커널/옵션에 따라 달라집니다.
- 프레임워크 오버헤드:
torchallocator,transformers내부 버퍼,acceleratedevice 이동 등.
즉 “7B니까 8GB VRAM에도 되겠지?” 같은 계산은 자주 틀립니다. 그래서 실전에서는 가중치를 줄이는 4bit 양자화와 나머지를 CPU로 넘기는 오프로딩을 함께 씁니다.
준비물: 패키지 버전과 설치
아래 조합이 가장 무난합니다.
transformersacceleratebitsandbytestorch(CUDA 빌드)
pip install -U transformers accelerate bitsandbytes
# torch는 환경에 맞게 설치 (CUDA 버전 주의)
bitsandbytes는 환경에 따라 설치/동작 이슈가 있을 수 있습니다. 특히 Windows나 특정 CUDA 조합에서 문제가 나면, 우선은 WSL2 또는 Linux 환경에서 검증하는 편이 빠릅니다.
1) 4bit 양자화로 “가중치 메모리”부터 줄이기
핵심은 BitsAndBytesConfig로 4bit 로딩을 켜는 것입니다. 일반적으로 다음 옵션이 밸런스가 좋습니다.
load_in_4bit=Truebnb_4bit_quant_type="nf4"(정확도/안정성 측면에서 많이 사용)bnb_4bit_compute_dtype=torch.bfloat16(가능하면 BF16 권장)bnb_4bit_use_double_quant=True(추가 압축)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "meta-llama/Llama-2-7b-chat-hf" # 예시
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="cuda", # 우선 GPU에 최대한 올려보기
)
여기까지로도 많은 경우 OOM이 해결됩니다. 하지만 GPU VRAM이 6GB~8GB 급이거나, 모델이 13B 이상이거나, 컨텍스트를 길게 쓰면 여전히 터질 수 있습니다. 그때 필요한 게 CPU 오프로딩입니다.
2) CPU 오프로딩: GPU에 못 올리는 레이어를 CPU로 보내기
device_map="auto"를 쓰면 accelerate가 레이어를 GPU/CPU로 자동 배치합니다. 중요한 건 CPU에 내려간 가중치를 담아둘 메모리(RAM) 와, 일부 설정에서 필요한 오프로딩 폴더입니다.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "meta-llama/Llama-2-13b-chat-hf" # 예시
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto", # 핵심: 자동 분산
torch_dtype=torch.bfloat16,
low_cpu_mem_usage=True,
)
max_memory로 상한을 걸어 OOM을 예방
자동 배치는 편하지만, 가끔 “GPU에 너무 욕심내다가” 로딩 중 OOM이 나기도 합니다. 그럴 때는 max_memory로 장치별 상한을 명시합니다.
주의: 아래 예시의 문자열에 들어가는 단위는 "GiB" 같은 형태를 씁니다.
max_memory = {
0: "6GiB", # GPU 0에 6GiB까지만 사용
"cpu": "48GiB" # CPU RAM 상한
}
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
max_memory=max_memory,
low_cpu_mem_usage=True,
)
이렇게 하면 로딩 단계에서 VRAM이 넘치지 않도록 방지할 수 있습니다.
오프로딩이 느린데 괜찮나?
CPU 오프로딩은 확실히 느려집니다. 하지만 “아예 못 띄우는 상태”를 “일단 서비스 가능”으로 바꾸는 데 의미가 큽니다.
- 응답 속도가 중요하면: 가능한 한 많은 레이어를 GPU에 남기고(
max_memory를 보수적으로 조정), 컨텍스트 길이를 줄이는 쪽이 체감이 큽니다. - 동시 요청이 많으면: 오프로딩은 병목이 되기 쉬우니, 큐/백오프 전략도 함께 고려해야 합니다. API 호출 폭주를 다루는 방식은 로컬 LLM 서버에도 그대로 적용됩니다: OpenAI 429/RateLimitError 재시도·백오프·큐 설계
3) OOM의 주범: KV 캐시와 컨텍스트 길이
모델을 4bit로 줄여도, 긴 프롬프트 + 긴 생성은 KV 캐시를 크게 만들어 OOM을 유발합니다. 특히 채팅형으로 대화 로그를 계속 누적하면 갑자기 터지는 패턴이 흔합니다.
실전 팁
max_new_tokens를 무작정 크게 두지 말기- 프롬프트를 요약/압축하거나, 시스템 메시지/대화 히스토리를 관리하기
- 가능하면
torch.no_grad()또는inference_mode()사용
import torch
prompt = "한국어로 5줄 요약을 해줘: ..."
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.inference_mode():
out = model.generate(
**inputs,
max_new_tokens=256,
do_sample=True,
temperature=0.7,
top_p=0.9,
eos_token_id=tokenizer.eos_token_id,
)
print(tokenizer.decode(out[0], skip_special_tokens=True))
inputs.to(model.device)가 항상 안전한 건 아닙니다. device_map="auto"로 레이어가 CPU/GPU에 섞여 있으면 입력 텐서는 보통 GPU에 두는 게 맞지만, 환경에 따라 경고/에러가 날 수 있습니다. 문제가 생기면 inputs = tokenizer(..., return_tensors="pt")만 하고, generate가 내부적으로 처리하게 두는 방식도 시도해볼 만합니다.
4) 자주 터지는 케이스별 처방전
케이스 A: 로딩 중 바로 OOM
device_map="auto"로 변경max_memory로 GPU 상한 설정- 이미 다른 프로세스가 VRAM을 잡고 있는지 확인
nvidia-smi
추가로, 파이썬 프로세스가 죽었다 살아나는 형태면 서비스 매니저 로그로 원인을 명확히 보는 게 빠릅니다. 운영 환경에서의 재시작 루프 추적은 다음 글의 접근이 도움이 됩니다: systemd 서비스가 계속 재시작될 때 원인 추적법
케이스 B: 몇 번 생성하다가 갑자기 OOM
- 대화 히스토리 누적을 의심 (프롬프트 길이 증가)
max_new_tokens/컨텍스트 길이 제한- 동시 요청 시 배치가 커지는지 확인
케이스 C: 4bit인데도 속도가 너무 느림
- CPU 오프로딩 비중이 너무 큼 (GPU 상한을 조금 올리거나 더 큰 GPU 필요)
temperature/top_p자체는 속도에 큰 영향이 없고, 토큰 수가 속도를 좌우- 가능하면 Flash Attention 계열을 쓰고 싶겠지만, 모델/환경 호환성 이슈가 많아 우선은 안정적인 설정부터 고정하는 편이 좋습니다.
5) 최소 구성 “OOM 안 나게” 템플릿
아래는 로컬에서 가장 흔한 목표인 “8GB 전후 VRAM에서 7B~13B를 어떻게든 띄우기”에 초점을 둔 템플릿입니다.
import os
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
os.environ["TOKENIZERS_PARALLELISM"] = "false"
model_id = "mistralai/Mistral-7B-Instruct-v0.2" # 예시
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
max_memory = {
0: "7GiB",
"cpu": "48GiB",
}
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
max_memory=max_memory,
low_cpu_mem_usage=True,
)
prompt = "다음 문장을 개발자 블로그 톤으로 바꿔줘: ..."
inputs = tokenizer(prompt, return_tensors="pt")
with torch.inference_mode():
output = model.generate(
input_ids=inputs["input_ids"].to(model.device),
attention_mask=inputs.get("attention_mask", None).to(model.device) if "attention_mask" in inputs else None,
max_new_tokens=200,
do_sample=True,
temperature=0.7,
top_p=0.9,
pad_token_id=tokenizer.eos_token_id,
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
환경에 따라 attention_mask가 None일 수 있으니 방어적으로 처리했습니다. 또한 pad_token_id를 지정해 경고를 줄였습니다.
6) 체크리스트: OOM/불안정 재발 방지
- VRAM 상주 프로세스 확인:
nvidia-smi로 다른 점유 제거 - 컨텍스트 관리: 대화 히스토리 무제한 누적 금지, 요약/슬라이딩 윈도우 적용
- 생성 길이 제한:
max_new_tokens상한 설정 - 동시성 제한: 로컬 LLM은 생각보다 쉽게 병목이 생김. 큐/백오프 필요
- 장치 배치 고정:
device_map="auto"+max_memory로 재현성 확보 - 옵션 충돌 주의: 4bit 양자화 상태에서
torch_dtype를 무리하게 바꾸거나, 다른 최적화 옵션을 섞다 보면 오히려 로딩 실패가 날 수 있음
마무리
로컬 LLM OOM은 “모델이 커서”라기보다, 가중치 + KV 캐시 + 오버헤드가 합쳐져 발생하는 경우가 대부분입니다. 해결은 단계적으로 접근하는 게 가장 안전합니다.
- 4bit 양자화로 가중치 메모리부터 줄이고
device_map="auto"와max_memory로 CPU 오프로딩을 걸어 로딩을 성공시키고- 컨텍스트/생성 길이/동시성 정책으로 KV 캐시 폭증을 막는다
이 3단계만 지켜도 “로딩도 못 하던 환경”에서 “현실적으로 운영 가능한 로컬 추론”까지 도달하는 경우가 많습니다.