GPU 메모리에 모델이 안 올라갈 때, 양자화로 돌파하기

GPU 메모리에 모델이

GPU 메모리에 모델이 안 올라갈 때, 양자화 말고 다른 도리가 없을까 고민하시는 분들이 많습니다. 결론부터 말씀드리면 오프로딩이나 배치 크기 조정으로 약간은 버틸 수 있지만, 7B급 이상 모델을 로컬 GPU 한 장에 올리려는 상황이라면 결국 양자화가 가장 확실한 해결책입니다. 문제는 양자화 방식이 하나가 아니라는 점인데, bitsandbytes·GPTQ·AWQ·GGUF 각각이 메모리를 줄이는 원리와 대가로 치르는 품질·속도가 다릅니다. 이 글에서는 방식별로 실제 줄어드는 용량을 계산해보고, 어떤 상황에서 어떤 조합을 고르면 되는지 정리해 드립니다.

GPU 메모리에 모델이 안 올라갈 때 벌어지는 일

파이토치가 “CUDA out of memory”를 뱉는 순간, 실제로는 세 가지 메모리가 동시에 GPU에 쌓이고 있습니다. 첫 번째는 가중치 자체이고, 두 번째는 순전파 중 생기는 활성화 값, 세 번째는 트랜스포머 모델 특유의 KV 캐시입니다. 흔히 “모델 용량 = 파라미터 수 × 바이트 수”로만 계산하는데, 이 계산은 가중치 몫만 알려줄 뿐입니다.

가중치만 놓고 보면 계산은 단순합니다. FP16(2바이트)로 저장된 7B 모델은 파라미터 수 70억 × 2바이트, 즉 약 14GB를 차지합니다. 여기서 자료형을 더 촘촘한 값으로 표현하는 부동소수점 대신 정수 몇 비트로 값을 찍어 누르는 것이 양자화의 핵심 아이디어입니다. INT8(1바이트)로 내리면 약 7GB, 4비트(0.5바이트)로 내리면 약 3.5~4GB까지 줄어듭니다.

파라미터 수 FP16(2바이트) INT8(1바이트) 4bit(0.5바이트)
7B 약 14GB 약 7GB 약 3.5~4GB
13B 약 26GB 약 13GB 약 6.5~7GB
70B 약 140GB 약 70GB 약 35~38GB

이 표는 가중치만 계산한 값이라 실제로는 여기에 KV 캐시와 활성화 메모리가 추가로 얹힙니다. 특히 컨텍스트 길이가 길어지면 KV 캐시가 통제 불가능하게 늘어나서, 가중치를 아무리 줄여도 GPU 메모리에 모델이 올라간 뒤 긴 대화 중간에 다시 OOM이 나는 경우가 생깁니다.

bitsandbytes, GPTQ, AWQ, GGUF — 양자화 방식이 이렇게 다릅니다

가장 큰 차이는 “언제 양자화를 하느냐”입니다. bitsandbytes는 모델을 불러오는 순간 즉석에서 4bit·8bit로 변환합니다. 별도 준비 작업이 필요 없어서 코드 몇 줄만 추가하면 되고, QLoRA 방식 파인튜닝과도 그대로 호환됩니다.

고용량 VRAM을 갖춘 NVIDIA GPU 그래픽 카드 근접 촬영

반면 GPTQ와 AWQ, GGUF(k-quant)는 사전 양자화 방식입니다. 원본 모델을 오프라인에서 미리 압축해 별도 파일로 저장해두고, 추론할 때는 이미 압축된 파일만 불러옵니다. GPTQ는 소량의 보정 데이터셋으로 레이어별 오차를 최소화하는 방향으로 가중치를 재배치하고, AWQ는 활성화 값의 분포를 관찰해 중요한 채널의 정밀도를 더 세심하게 보존합니다. GGUF는 llama.cpp 생태계의 자체 포맷으로, Q4_K_M·Q5_K_M·Q8_0처럼 비트 폭과 그룹 단위가 다른 여러 변종을 제공합니다.

방식 대표 라이브러리 양자화 시점 보정 작업 강점 약점
bitsandbytes(NF4/INT8) transformers 로딩 시 즉석 변환 불필요 적용이 가장 간단, 파인튜닝 호환 순수 추론 속도 이득은 크지 않음
GPTQ AutoGPTQ, ExLlamaV2 사전 양자화 필요 4bit에서도 손실 작고 GPU 추론 빠름 모델별 사전 변환 시간 소요
AWQ AutoAWQ 사전 양자화 필요(활성화 통계) GPTQ보다 낮은 손실, 전용 커널로 속도 우수 지원 아키텍처 범위가 상대적으로 좁음
GGUF(k-quant) llama.cpp 사전 양자화 선택적 CPU/GPU 혼합 실행, 비트 폭 선택지 다양 최신 아키텍처 지원이 늦게 붙는 경우 존재

각 방식의 원리는 GPTQ 논문과 AWQ 논문에 정리돼 있고, bitsandbytes의 NF4 방식은 QLoRA 논문에서 처음 제안됐습니다. 실제 적용 코드는 아래와 같습니다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch

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

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-7b-hf",
    quantization_config=bnb_config,
    device_map="auto",
)
print(round(model.get_memory_footprint() / 1024**3, 2), "GB")

이 코드는 허깅페이스 공식 문서에 나온 설정 그대로이고, 7B 모델 기준으로 실행하면 4GB대 초반의 값이 출력됩니다. double_quant까지 켜면 양자화 상수 자체도 한 번 더 압축돼 여기서 수백 MB가 더 줄어듭니다.

비트 수와 속도·품질의 관계

비트를 낮춘다고 속도가 항상 빨라지는 건 아닙니다. bitsandbytes의 NF4는 연산 자체는 여전히 bfloat16으로 복원해서 수행하기 때문에, 메모리는 줄어도 토큰당 지연 시간은 FP16 대비 별 차이가 없거나 오히려 느려지는 경우가 있습니다. dequant 과정 자체가 추가 연산이기 때문입니다.

반대로 GPTQ·AWQ는 ExLlamaV2나 Marlin 같은 전용 커널을 붙이면 메모리 대역폭 사용량이 줄어든 만큼 실제 생성 속도도 함께 올라갑니다. GGUF도 llama.cpp의 CUDA 백엔드에서 비슷하게 동작하는데, k-quant 계열(Q4_K_M, Q5_K_M)은 양자화 그룹을 잘게 나눠 오차를 분산시킨 버전이라 같은 4bit라도 예전 방식(Q4_0)보다 품질 손실이 적습니다.

llama.cpp 저장소의 공식 문서에서 확인할 수 있듯, 7B 모델 기준으로 F16 원본이 약 13GB라면 Q8_0은 7GB 안팎, Q5_K_M은 5GB 안팎, Q4_K_M은 4GB 안팎으로 줄어듭니다(모델과 버전에 따라 다소 편차가 있습니다). 실제 변환 명령은 이렇습니다.

./llama-quantize ./models/llama-2-7b/ggml-model-f16.gguf \
  ./models/llama-2-7b/ggml-model-q4_k_m.gguf Q4_K_M

인공지능 모델 데이터 압축을 표현한 추상적 신경망 시각화

체감상 Q5_K_M까지는 원본과 답변 품질 차이를 알아채기 어렵고, Q4_K_M부터 미세한 단어 선택 차이가 보이기 시작합니다. Q2_K·Q3_K처럼 극단적으로 낮추면 코드 생성이나 수치 추론 같은 과제에서 오답률이 눈에 띄게 올라갑니다.

4비트로 줄였는데 답변 품질이 떨어졌다면?

같은 4bit라도 체감 품질이 크게 갈리는 이유는 라운딩 방식 때문입니다. 가장 단순한 RTN(Round-To-Nearest) 방식은 값 하나하나를 가장 가까운 격자에 반올림할 뿐이라, 값의 분포가 넓은 레이어에서 오차가 누적됩니다. 반면 GPTQ는 한 값을 반올림한 뒤 생기는 오차를 나머지 가중치에 재분배하고, AWQ는 활성화 크기가 큰 채널(살리언트 채널)을 미리 식별해 그 부분만 정밀도를 덜 깎습니다.

실무에서 품질이 유독 나쁘게 나온다면 우선 group size 설정을 의심해보시는 게 좋습니다. group size 128이 흔히 쓰이는 기본값인데, 이보다 큰 256으로 잡으면 압축률은 좋아지지만 그룹 내 값 편차가 커져 오차도 늘어납니다. GPTQ의 act-order(desc_act) 옵션을 켜면 중요한 채널부터 순서대로 양자화해 저비트에서도 손실을 줄일 수 있는데, 대신 추론 커널 호환성이 떨어지는 조합이 있어 라이브러리 버전을 확인하고 켜야 합니다.

내 GPU에 맞는 양자화 방식, 이 기준으로 골라 보세요

가진 VRAM 용량을 nvidia-smi로 먼저 확인한 다음, 아래 기준으로 시작점을 잡으시면 됩니다.

  • 8GB (RTX 4060, 3060 Ti 등): 7B 모델을 GGUF Q4_K_M 또는 bitsandbytes NF4로 로드하는 조합이 안정적입니다.
  • 12GB (RTX 4070, 3060 12GB 등): 7B는 8bit 그대로, 13B는 4bit로 내려서 사용할 수 있습니다.
  • 24GB (RTX 4090, 3090 등): 13B 8bit나 33B급 4bit가 여유 있게 들어가고, 70B 4bit도 컨텍스트를 짧게 잡으면 시도해볼 만합니다.
  • 48GB 이상(멀티 GPU 포함): 70B 4bit를 컨텍스트 여유 있게 돌릴 수 있고, 13~30B 구간은 8bit로도 충분합니다.

단, 이 방법이 모든 경우에 통하는 건 아닙니다. 가중치를 아무리 압축해도 KV 캐시는 별개로 계속 쌓이기 때문에, GPU 메모리에 모델이 올라간 직후에는 멀쩡하다가 대화가 길어지면서 다시 OOM이 나는 경우가 흔합니다. 이럴 때는 llama.cpp의 --cache-type-k q8_0 같은 KV 캐시 양자화 옵션이나 vLLM의 페이지드 어텐션처럼, 가중치가 아니라 캐시 쪽을 별도로 압축하는 조치가 필요합니다. 또한 Turing 이전 세대처럼 INT4/INT8 전용 연산 유닛이 없는 구형 GPU에서는 양자화를 해도 메모리만 줄 뿐 속도 이득이 거의 없다는 점도 감안하셔야 합니다.

지금 바로 해볼 수 있는 다음 단계는 간단합니다. nvidia-smi로 여유 VRAM을 확인하고, 위 기준표에서 자신의 용량대에 해당하는 모델·비트 조합 중 GGUF Q4_K_M부터 먼저 받아 테스트해보시기 바랍니다. 답변 품질이 만족스럽지 않을 때만 Q5_K_M이나 AWQ 쪽으로 한 단계씩 올려보는 순서가 가장 시행착오가 적습니다.

추론 모델로 바꾸고 비용이 뛰었다면? 사고 토큰과 effort 조절부터 확인하세요

Leave a Comment