LLM 25일 코스 - Day 23: 양자화(Quantization) 가이드

Day 23: 양자화(Quantization) 가이드

양자화는 모델의 가중치 정밀도를 낮춰서 모델 크기와 메모리 사용량을 줄이는 기법입니다. 70B 모델을 소비자 GPU에서 실행할 수 있게 해주는 핵심 기술입니다.

정밀도별 비교

숫자 하나를 표현하는 데 사용하는 비트 수에 따라 모델의 크기와 성능이 달라집니다. FP32(32비트)가 가장 정확하지만 가장 크고, INT4(4비트)가 가장 작지만 약간의 품질 손실이 있습니다.

# 정밀도별 모델 크기와 성능 비교
precision_table = {
    "정밀도":   ["FP32",  "FP16",  "INT8",  "INT4"],
    "비트/값":  ["32bit", "16bit", "8bit",  "4bit"],
    "7B 크기":  ["28GB",  "14GB",  "7GB",   "3.5GB"],
    "13B 크기": ["52GB",  "26GB",  "13GB",  "6.5GB"],
    "70B 크기": ["280GB", "140GB", "70GB",  "35GB"],
    "품질 손실": ["없음",  "거의 없음", "미미함", "약간 있음"],
    "추론 속도": ["느림",  "빠름",  "매우 빠름", "가장 빠름"],
}

for key, values in precision_table.items():
    print(f"{key:10} | {'  |  '.join(values)}")

bitsandbytes로 4bit/8bit 로드

가장 간단한 양자화 방법은 bitsandbytes를 통해 모델을 로드할 때 바로 양자화하는 것입니다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch

# 8bit 양자화 (INT8)
model_8bit = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    load_in_8bit=True,
    device_map="auto",
)
print(f"8bit 메모리: {model_8bit.get_memory_footprint() / 1e9:.1f} GB")

# 4bit 양자화 (NF4 - QLoRA에서 사용하는 방식)
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_4bit = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    quantization_config=bnb_config,
    device_map="auto",
)
print(f"4bit 메모리: {model_4bit.get_memory_footprint() / 1e9:.1f} GB")

GGUF 포맷과 llama.cpp

GGUF(GPT-Generated Unified Format)는 llama.cpp에서 사용하는 포맷으로, CPU에서도 LLM을 실행할 수 있게 해줍니다. 다양한 양자화 수준(Q2_K ~ Q8_0)을 제공합니다.

# llama.cpp를 활용한 GGUF 양자화
# 1단계: llama.cpp 설치
# git clone https://github.com/ggerganov/llama.cpp
# cd llama.cpp && make

# 2단계: HF 모델을 GGUF로 변환
# python convert_hf_to_gguf.py ../model_path --outtype f16 --outfile model.gguf

# 3단계: 양자화 실행
# ./llama-quantize model.gguf model-q4_k_m.gguf Q4_K_M

# GGUF 양자화 수준별 비교
gguf_levels = {
    "양자화":  ["Q2_K", "Q3_K_M", "Q4_K_M", "Q5_K_M", "Q6_K", "Q8_0"],
    "비트":   ["2bit", "3bit",   "4bit",   "5bit",   "6bit", "8bit"],
    "7B 크기": ["2.7GB","3.3GB",  "4.1GB",  "4.8GB",  "5.5GB","7.2GB"],
    "품질":   ["낮음", "보통",    "좋음",    "매우좋음", "우수", "거의무손실"],
    "추천":   ["비추", "제한적",  "범용추천", "고품질",  "고품질","메모리여유시"],
}

for key, vals in gguf_levels.items():
    print(f"{key:8} | {'  |  '.join(vals)}")

AWQ와 GPTQ 양자화

AWQ(Activation-aware Weight Quantization)와 GPTQ는 학습 데이터 기반으로 더 정밀하게 양자화하여 품질 손실을 최소화합니다.

# AWQ 양자화 모델 사용 (미리 양자화된 모델 로드)
from transformers import AutoModelForCausalLM, AutoTokenizer

# AWQ 모델 로드 (Hub에서 이미 양자화된 모델)
model_awq = AutoModelForCausalLM.from_pretrained(
    "TheBloke/Llama-2-7B-Chat-AWQ",
    device_map="auto",
)

# GPTQ 모델 로드
# pip install auto-gptq
model_gptq = AutoModelForCausalLM.from_pretrained(
    "TheBloke/Llama-2-7B-Chat-GPTQ",
    device_map="auto",
)

# 양자화 방식 비교 요약
methods = {
    "방식":        ["bitsandbytes", "GGUF",      "AWQ",       "GPTQ"],
    "속도":        ["로드 시 변환",  "사전 양자화", "사전 양자화", "사전 양자화"],
    "CPU 지원":    ["아니오",       "예",         "아니오",     "아니오"],
    "추론 속도":   ["보통",         "빠름(CPU)",  "빠름(GPU)",  "빠름(GPU)"],
    "파인튜닝 지원": ["예(QLoRA)", "아니오",     "제한적",     "제한적"],
}
for key, vals in methods.items():
    print(f"{key:12} | {'  |  '.join(vals)}")

오늘의 연습문제

  1. 같은 모델을 FP16, INT8, INT4로 로드하여 get_memory_footprint()로 실제 메모리 사용량을 비교하고, 동일 프롬프트에 대한 출력 품질을 정성적으로 평가해보세요.
  2. Hugging Face Hub에서 GGUF 포맷 모델을 다운로드하고, llama.cpp 또는 Ollama로 실행하여 응답 속도를 측정해보세요.
  3. Q4_K_M과 Q8_0 양자화 모델의 출력을 10개 프롬프트에 대해 비교하고, 품질 차이가 유의미한 경우가 있는지 분석해보세요.

이 글이 도움이 되었나요?