- Published on
Transformers 로컬 LLM OOM? 4bit로 VRAM 절약
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
로컬 GPU에서 transformers로 LLM을 올리려다 CUDA out of memory(OOM)를 한 번이라도 겪었다면, 문제는 대개 “모델 파라미터를 어떤 정밀도로, 어디에, 어떻게 올렸는가”로 귀결됩니다. 특히 7B13B급 모델을 기본 설정(12GB인 환경에서 로딩 단계에서 바로 터지거나, 첫 float16 또는 bfloat16)으로 로드하면, VRAM이 8GBgenerate에서 KV 캐시가 붙으면서 뒤늦게 터집니다.
이 글에서는 bitsandbytes 4bit 양자화(NF4)를 사용해 로컬 LLM OOM을 실전적으로 줄이는 방법을 정리합니다. 단순히 “4bit로 로드하세요”가 아니라, 왜 OOM이 나는지, 어떤 옵션이 메모리를 좌우하는지, 자주 하는 실수(특히 device_map/KV 캐시/max_new_tokens)까지 함께 다룹니다.
또한 양자화는 성능과 정확도 트레이드오프가 있으므로, 품질이 떨어졌을 때 점검할 포인트도 마지막에 체크리스트로 제공합니다. (int8 양자화에서의 정확도 이슈는 아래 글도 함께 참고하면 좋습니다: PyTorch int8 양자화 후 정확도 급락 7가지 해결)
로컬 LLM에서 OOM이 나는 3가지 지점
1) 모델 가중치 로딩 자체가 VRAM을 초과
가장 흔합니다. 예를 들어 7B 모델을 fp16으로 올리면 가중치만 대략 14GB 수준이 필요할 수 있습니다(대략 파라미터 수 * 2바이트). 여기에 옵티마이저는 없더라도, 런타임 버퍼와 CUDA 컨텍스트 등이 추가로 먹습니다.
2) 첫 생성 시 KV 캐시가 급증
generate는 토큰을 생성할 때마다 어텐션의 KV 캐시를 쌓습니다. max_new_tokens를 크게 주거나, 컨텍스트 길이(max_length 또는 입력 토큰)가 길면 KV 캐시가 모델 크기와 무관하게 크게 늘어 OOM을 유발합니다.
3) 장치 배치 실수로 중복 상주
device_map을 잘못 쓰면 GPU와 CPU에 중복으로 올라가거나, 의도치 않게 전체를 GPU에 올리려고 시도합니다. 또한 torch_dtype를 안 맞추면 내부 캐스팅으로 임시 텐서가 생겨 피크 메모리가 튀는 경우도 있습니다.
bitsandbytes 4bit가 OOM에 강한 이유
bitsandbytes의 4bit 양자화는 가중치를 4bit로 저장하고, 연산 시 필요한 범위에서 복원/스케일링해 계산합니다. 특히 NF4는 LLM 가중치 분포에 맞춘 4bit 포맷으로, 단순 4bit보다 품질 손실을 줄이면서 메모리를 크게 절약합니다.
핵심 효과는 다음과 같습니다.
- 가중치 메모리 대폭 감소:
fp16대비 이론상 4분의 1 수준 - 로딩 단계에서 OOM 확률 감소
- 8GB~12GB VRAM에서도 7B급 모델을 현실적으로 운용 가능
다만 4bit는 “모든 문제가 해결”은 아닙니다. KV 캐시, 긴 컨텍스트, 배치, 샘플링 파라미터는 여전히 OOM을 만들 수 있습니다.
준비물: 패키지 설치 및 버전 주의
아래 조합이 가장 무난합니다.
transformersacceleratebitsandbytestorch(CUDA 빌드)
pip install -U transformers accelerate bitsandbytes
환경에 따라 bitsandbytes가 CUDA를 제대로 못 잡으면 CPU 폴백이 발생하거나 로딩이 느려질 수 있습니다. 이 경우에는 nvidia-smi로 CUDA 드라이버와 GPU 상태를 먼저 확인하고, torch.cuda.is_available()가 True인지부터 체크하세요.
기본 해법: 4bit(NF4)로 모델 로드하기
가장 많이 쓰는 패턴은 BitsAndBytesConfig를 사용해 4bit 로딩을 활성화하는 것입니다.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
model_id = "meta-llama/Llama-2-7b-chat-hf" # 예시
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.float16,
)
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
prompt = "Explain OOM and how 4bit quantization helps."
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.inference_mode():
out = model.generate(
**inputs,
max_new_tokens=128,
do_sample=True,
temperature=0.7,
top_p=0.9,
)
print(tokenizer.decode(out[0], skip_special_tokens=True))
옵션 해설(메모리 관점)
load_in_4bit=True: 가중치 4bit 로딩의 핵심 스위치bnb_4bit_quant_type="nf4": 품질과 안정성 측면에서 가장 많이 권장되는 타입bnb_4bit_use_double_quant=True: 2단계 양자화로 메모리를 더 줄이거나 품질을 개선하는 경우가 많음bnb_4bit_compute_dtype=torch.float16: 연산 dtype. 보통float16또는bfloat16선택device_map="auto":accelerate가 GPU/CPU 오프로딩을 자동으로 나눠줌
device_map="auto"는 편하지만, “GPU에 다 못 올리면 CPU로 일부를 넘긴다”는 뜻이므로 속도는 느려질 수 있습니다. 대신 OOM 회피에는 매우 유효합니다.
그래도 OOM이 난다면: 원인별 처방전
1) max_new_tokens와 입력 길이를 먼저 줄이기(KV 캐시)
4bit로 가중치를 줄여도, 생성 길이가 길면 KV 캐시가 VRAM을 다시 잡아먹습니다. 특히 “긴 문서 요약”이나 “RAG 컨텍스트를 길게 넣는” 경우가 여기에 해당합니다.
out = model.generate(
**inputs,
max_new_tokens=64, # 먼저 줄여서 테스트
do_sample=False,
)
RAG를 붙여 컨텍스트가 길어지는 워크로드라면, 검색 단계에서 컨텍스트를 줄이거나 rerank로 압축하는 전략이 효과적입니다. 관련 튜닝은 RAG 품질 급락? 하이브리드 검색+Rerank 튜닝도 함께 참고하세요.
2) device_map을 명시하고 CPU 오프로딩을 적극 활용
auto가 애매하게 잡을 때는 명시가 더 안정적입니다. 또한 CPU RAM이 충분하다면 일부 레이어를 CPU로 보내 OOM을 피할 수 있습니다.
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map={"": 0}, # 전체를 GPU 0에 올리려는 시도
)
위처럼 강제하면 다시 OOM이 날 수 있습니다. 반대로 “가능한 만큼만 GPU”로 두고 나머지는 CPU로 넘기려면 device_map="auto"가 일반적으로 낫습니다. 중요한 건 “내 VRAM에서 감당 가능한 범위를 넘기지 않게” 만드는 것입니다.
3) PyTorch 메모리 파편화 완화
OOM 로그에 “reserved memory is much larger than allocated memory” 같은 문구가 보이면 파편화 가능성이 있습니다. 이때는 실행 전 환경변수로 allocator 동작을 조정해 피크를 낮추기도 합니다.
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,max_split_size_mb:128
또한 반복 실행 중이라면, 중간에 큰 텐서를 만들고 지우는 패턴이 파편화를 악화시킬 수 있습니다. 실험 코드를 단순화하거나 프로세스를 재시작하는 것이 의외로 빠른 해결책입니다.
4) torch_dtype를 함부로 섞지 않기
4bit 로딩에서 compute_dtype는 중요하지만, 별도로 torch_dtype=torch.float16 등을 섞어 넣다가 내부 캐스팅이 늘어나면 피크 메모리가 튈 수 있습니다. 기본적으로는 quantization_config에 dtype를 위임하고, 추가 dtype 지정은 최소화하는 편이 안전합니다.
5) 배치 처리, 동시 요청을 줄이기
로컬 서버로 올려두고 동시에 여러 요청을 받으면 KV 캐시가 요청 수만큼 늘 수 있습니다. 간단히는 동시성을 제한하세요.
- 한 번에 한 요청만 처리
- 큐잉 적용
max_new_tokens상한 설정
운영 환경에서 메모리 급증이 반복된다면 “서비스 재시작 루프”로 이어질 수 있으니, 장애 진단 관점에서는 systemd 서비스 재시작 루프 10분 진단 가이드처럼 프로세스/로그/재시작 정책까지 함께 점검하는 것이 좋습니다.
실제로 얼마나 줄어드나: 대략적인 감
정확한 수치는 모델 아키텍처, 컨텍스트 길이, 커널 구현에 따라 달라지지만, 체감 포인트는 이렇습니다.
fp16로딩 시 VRAM이 터지던 7B 모델이, 4bit에서는 “로딩은 되는데 생성 길이에 따라 터질 수 있음” 상태로 바뀌는 경우가 많습니다.- 즉, 4bit는 가중치 상주 비용을 줄여 초기 OOM을 막아주고, 이후에는 KV 캐시와 컨텍스트 관리가 승부처가 됩니다.
4bit 사용 시 품질/속도 트레이드오프 체크리스트
4bit는 만능이 아니므로, 적용 후 아래를 확인하세요.
- 답변 품질 저하: 특정 도메인에서 환각이 늘거나 문장 안정성이 떨어질 수 있음
- 속도 저하: CPU 오프로딩이 섞이면 토큰 생성 속도가 크게 떨어질 수 있음
- 긴 컨텍스트에서 불안정: 컨텍스트가 길수록 KV 캐시 비용이 커져 OOM 재발 가능
- 샘플링 파라미터 영향:
temperature를 올리면 품질이 흔들리는 것이 더 눈에 띌 수 있음
품질이 크게 흔들리면 다음 순서로 조정해보세요.
bnb_4bit_quant_type를nf4로 유지했는지 확인bnb_4bit_compute_dtype를float16또는 GPU가 지원하면bfloat16로 테스트- 가능한 경우 “더 작은 모델” 또는 “더 짧은 컨텍스트”로 문제를 단순화
- 특정 태스크는 8bit 또는
fp16이 더 나을 수 있으니 비교 실험
결론: 4bit는 시작점, KV 캐시가 다음 병목
로컬에서 transformers로 LLM을 돌릴 때 OOM을 가장 빠르게 줄이는 방법은 bitsandbytes 4bit(NF4) 양자화입니다. 다만 4bit로 “로딩 OOM”을 넘겼다면, 다음 병목은 거의 항상 KV 캐시와 컨텍스트 길이입니다.
정리하면 운영 관점의 우선순위는 다음과 같습니다.
- 4bit(NF4)로 가중치 메모리부터 줄이기
max_new_tokens와 입력 토큰 길이로 KV 캐시를 통제하기device_map과 오프로딩으로 VRAM 한계를 넘지 않게 배치하기- 파편화/동시성/재시작 루프까지 포함해 시스템적으로 안정화하기
이 4단계를 잡으면, “로컬 LLM은 무조건 비싼 GPU가 필요하다”는 인식에서 벗어나, 제한된 VRAM에서도 꽤 실용적인 개발 환경을 만들 수 있습니다.