LabHub

블로그

LLM 양자화 기법 완벽 비교: GPTQ, AWQ, GGUF 실전 적용 가이드

한국어English日本語

LLM 양자화 기법 비교

들어가며

70B 파라미터 LLM을 FP16으로 로딩하면 약 140GB의 VRAM이 필요하다. A100 80GB 2장으로도 빠듯한 수준이다. 하지만 양자화(Quantization)를 적용하면 동일한 모델을 단일 GPU에서 운용할 수 있다. INT4 양자화를 적용하면 70B 모델이 약 35GB로 줄어 A100 한 장에 올릴 수 있게 된다.

2024~2026년 사이에 양자화 기법은 급격히 발전했다. GPTQ, AWQ, GGUF, BitsAndBytes 등 다양한 방법이 등장하며, 각각 정확도-속도-메모리의 트레이드오프가 다르다. 이 글에서는 각 기법의 원리를 깊이 있게 분석하고, 실전 코드와 벤치마크를 통해 최적의 선택지를 안내한다.

양자화 기본 원리

FP16에서 INT4까지의 여정

양자화란 높은 정밀도의 부동소수점(FP16/FP32) 가중치를 낮은 비트의 정수(INT8/INT4)로 변환하는 과정이다.

데이터 타입비트 수표현 범위70B 모델 메모리
FP3232bit약 ±3.4×10^38~280GB
FP1616bit약 ±6.5×10^4~140GB
INT88bit-128 ~ 127~70GB
INT44bit-8 ~ 7~35GB

Absmax 양자화

가장 단순한 양자화 방법으로, 텐서의 절대 최대값을 기준으로 스케일링한다.

import torch

def absmax_quantize(tensor: torch.Tensor, bits: int = 8) -> tuple:
    """Absmax 양자화: 절대 최대값 기준 스케일링"""
    qmax = 2 ** (bits - 1) - 1  # INT8이면 127
    scale = tensor.abs().max() / qmax
    quantized = torch.round(tensor / scale).clamp(-qmax, qmax).to(torch.int8)
    return quantized, scale

def absmax_dequantize(quantized: torch.Tensor, scale: float) -> torch.Tensor:
    """역양자화: 원래 스케일로 복원"""
    return quantized.float() * scale

# 예시: FP16 가중치 양자화
weight = torch.randn(4096, 4096, dtype=torch.float16)
q_weight, scale = absmax_quantize(weight.float())
restored = absmax_dequantize(q_weight, scale)
error = (weight.float() - restored).abs().mean()
print(f"평균 양자화 오차: {error:.6f}")

Zero-Point 양자화

Absmax는 분포가 대칭이 아닐 때 비효율적이다. Zero-Point 양자화는 비대칭 분포를 처리한다.

import torch

def zeropoint_quantize(tensor: torch.Tensor, bits: int = 8) -> tuple:
    """Zero-Point 양자화: 비대칭 분포 대응"""
    qmin = -(2 ** (bits - 1))
    qmax = 2 ** (bits - 1) - 1

    rmin, rmax = tensor.min(), tensor.max()
    scale = (rmax - rmin) / (qmax - qmin)
    zero_point = torch.round(qmin - rmin / scale).clamp(qmin, qmax)

    quantized = torch.round(tensor / scale + zero_point).clamp(qmin, qmax).to(torch.int8)
    return quantized, scale, zero_point

def zeropoint_dequantize(quantized: torch.Tensor, scale: float, zero_point: float) -> torch.Tensor:
    """Zero-Point 역양자화"""
    return (quantized.float() - zero_point) * scale

# 비대칭 분포 테스트 (ReLU 출력처럼 양수 편향)
weight = torch.randn(4096, 4096).abs()  # 양수만
q_weight, scale, zp = zeropoint_quantize(weight)
restored = zeropoint_dequantize(q_weight, scale, zp)
error = (weight - restored).abs().mean()
print(f"Zero-Point 양자화 평균 오차: {error:.6f}")

Group-wise 양자화

텐서 전체가 아니라 작은 그룹(보통 128개 원소) 단위로 양자화하면 정확도가 크게 향상된다. GPTQ, AWQ 모두 group_size=128을 기본 설정으로 사용한다.

GPTQ: Generative Pre-trained Transformer Quantization

알고리즘 상세

GPTQ는 2022년 Frantar 등이 제안한 Post-Training Quantization(PTQ) 방법으로, OBQ(Optimal Brain Quantization) 알고리즘을 기반으로 한다. 핵심 아이디어는 다음과 같다.

  1. Layer-wise 양자화: 모델 전체를 한꺼번에 양자화하지 않고, 레이어 단위로 순차 처리한다
  2. Hessian 기반 보정: 양자화로 인한 출력 오차를 Hessian 역행렬을 활용해 나머지 가중치로 보상한다
  3. Column 순서 최적화: 양자화 순서를 최적화하여 오차 전파를 최소화한다

수학적으로, 각 가중치 w_q를 양자화할 때 나머지 가중치를 아래와 같이 업데이트한다:

delta_w = -(w - quant(w)) / H_inv[q,q] * H_inv[:, q]

여기서 H_inv는 Hessian 역행렬이다. 이를 통해 양자화 오차가 다른 가중치로 재분배된다.

AutoGPTQ로 양자화 수행

from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
from transformers import AutoTokenizer
import torch

# 1. 양자화 설정
model_name = "meta-llama/Llama-3.1-8B-Instruct"
quantize_config = BaseQuantizeConfig(
    bits=4,                  # 4비트 양자화
    group_size=128,          # 128개 원소 단위 그룹
    desc_act=True,           # Activation order 사용
    damp_percent=0.01,       # Hessian damping 비율
    sym=True,                # 대칭 양자화
)

# 2. 모델 및 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoGPTQForCausalLM.from_pretrained(
    model_name,
    quantize_config=quantize_config,
    torch_dtype=torch.float16,
)

# 3. 캘리브레이션 데이터 준비
calibration_data = [
    tokenizer("The meaning of life is", return_tensors="pt"),
    tokenizer("Artificial intelligence has", return_tensors="pt"),
    tokenizer("In the context of machine learning", return_tensors="pt"),
    # 실제로는 128~256개의 다양한 샘플 사용 권장
]

# 4. 양자화 실행 (Llama-3.1-8B 기준 약 15~30분)
model.quantize(calibration_data)

# 5. 양자화된 모델 저장
output_dir = "./llama-3.1-8b-gptq-4bit"
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"양자화 완료. 저장 위치: {output_dir}")

GPTQ의 장단점

장점:

단점:

AWQ: Activation-aware Weight Quantization

원리와 차별점

AWQ는 2024년 MIT의 Lin 등이 제안한 방법으로, 핵심 관찰은 모든 가중치가 동일하게 중요하지 않다는 점이다.

  1. Salient Weight 식별: 활성화(Activation) 크기를 기준으로 중요한 가중치 채널(약 1%)을 식별한다
  2. 선택적 보호: 중요 가중치는 스케일링 팩터를 적용하여 양자화 오차를 줄인다
  3. 나머지 양자화: 나머지 99% 가중치는 일반 양자화를 적용한다

GPTQ와의 핵심 차이점은 Hessian 역행렬 계산이 불필요하다는 점이다. AWQ는 단순한 통계 기반 접근으로 더 빠르게 양자화하면서도 비슷하거나 더 나은 품질을 달성한다.

AutoAWQ로 양자화 수행

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

# 1. 모델 로드
model_path = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoAWQForCausalLM.from_pretrained(
    model_path,
    low_cpu_mem_usage=True,
    use_cache=False,
)
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

# 2. 양자화 설정
quant_config = {
    "zero_point": True,       # Zero-Point 양자화 사용
    "q_group_size": 128,      # 그룹 크기
    "w_bit": 4,               # 4비트 양자화
    "version": "GEMM",        # GEMM 커널 (범용) vs GEMV (배치=1 최적화)
}

# 3. 양자화 실행 (GPTQ보다 빠름, 약 10분)
model.quantize(tokenizer, quant_config=quant_config)

# 4. 저장
output_dir = "./llama-3.1-8b-awq-4bit"
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)
print(f"AWQ 양자화 완료: {output_dir}")

AWQ의 장단점

장점:

단점:

GGUF: GPU-poor를 위한 범용 포맷

GGUF 포맷과 llama.cpp 생태계

GGUF(GPT-Generated Unified Format)는 양자화 알고리즘이 아니라 파일 포맷이다. llama.cpp 프로젝트에서 만든 이 포맷은 CPU와 GPU 하이브리드 추론을 지원한다.

GGUF의 핵심 특징:

GGUF 양자화 타입별 비교

양자화 타입비트 수Llama-3.1-8B 크기Perplexity 증가권장 용도
Q2_K2.6bit~2.8GB+2.5~3.0극한 메모리 제한
Q3_K_M3.3bit~3.5GB+1.0~1.5메모리 제한 환경
Q4_K_M4.5bit~4.9GB+0.3~0.5범용 권장
Q5_K_M5.3bit~5.7GB+0.1~0.2품질 중시
Q6_K6.6bit~6.6GB+0.05거의 무손실
Q8_08.0bit~8.5GB~0무손실에 가까움

llama.cpp로 GGUF 양자화

# 1. llama.cpp 빌드
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp && mkdir build && cd build
cmake .. -DGGML_CUDA=ON  # GPU 가속 사용 시
cmake --build . --config Release

# 2. HF 모델을 GGUF FP16으로 변환
python convert_hf_to_gguf.py \
    /path/to/Llama-3.1-8B-Instruct \
    --outtype f16 \
    --outfile llama-3.1-8b-f16.gguf

# 3. Q4_K_M으로 양자화
./build/bin/llama-quantize \
    llama-3.1-8b-f16.gguf \
    llama-3.1-8b-q4_k_m.gguf \
    Q4_K_M

# 4. 추론 테스트
./build/bin/llama-cli \
    -m llama-3.1-8b-q4_k_m.gguf \
    -p "Explain quantum computing in simple terms:" \
    -n 256 \
    --n-gpu-layers 35  # GPU 오프로딩 레이어 수

llama-cpp-python으로 Python 연동

from llama_cpp import Llama

# GGUF 모델 로드 (GPU 레이어 오프로딩 포함)
llm = Llama(
    model_path="./llama-3.1-8b-q4_k_m.gguf",
    n_gpu_layers=35,    # GPU에 올릴 레이어 수 (-1이면 전체)
    n_ctx=4096,          # 컨텍스트 길이
    n_threads=8,         # CPU 스레드 수
    verbose=False,
)

# 텍스트 생성
output = llm(
    "Explain the difference between TCP and UDP:",
    max_tokens=512,
    temperature=0.7,
    top_p=0.9,
    stop=["\\n\\n"],
)
print(output["choices"][0]["text"])

# ChatCompletion API 스타일
response = llm.create_chat_completion(
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "What is quantization in ML?"},
    ],
    max_tokens=512,
    temperature=0.7,
)
print(response["choices"][0]["message"]["content"])

BitsAndBytes와 QLoRA 통합

4비트 추론과 파인튜닝

BitsAndBytes는 Hugging Face Transformers와 직접 통합되어 별도 양자화 과정 없이 로딩 시점에 양자화를 적용한다. QLoRA와 결합하면 양자화된 모델 위에 LoRA 어댑터를 학습할 수 있다.

import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)

# 4비트 양자화 설정
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NormalFloat4 (QLoRA 논문)
    bnb_4bit_compute_dtype=torch.bfloat16,  # 연산은 BF16으로
    bnb_4bit_use_double_quant=True,       # 이중 양자화 (추가 메모리 절약)
)

# 모델 로드 (로딩 시 자동 양자화)
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 메모리 사용량 확인
print(f"모델 메모리: {model.get_memory_footprint() / 1e9:.2f} GB")

# 추론
inputs = tokenizer("Explain gradient descent:", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

BitsAndBytes의 특징

장점:

단점:

종합 비교: GPTQ vs AWQ vs GGUF vs BitsAndBytes

핵심 특성 비교표

항목GPTQAWQGGUFBitsAndBytes
양자화 방식PTQ (Hessian 기반)PTQ (Activation-aware)PTQ (다양한 방식)동적 양자화
기본 비트4bit4bit2~8bit 선택4bit (NF4)
캘리브레이션필요 (128~256샘플)필요 (소량)불필요불필요
양자화 시간15~30분 (8B)~10분 (8B)수분로딩 시 즉시
GPU 추론매우 빠름가장 빠름빠름 (오프로딩 시)보통
CPU 추론미지원미지원지원미지원
vLLM 지원지원지원부분 지원지원
파인튜닝제한적제한적미지원QLoRA 최적
모델 공유HF Hub 업로드HF Hub 업로드단일 파일 배포어려움
Perplexity (4bit)기준 +0.3~0.5기준 +0.2~0.4Q4_K_M: +0.3~0.5기준 +0.2~0.4

추론 성능 벤치마크 (Llama-3.1-8B, A100 80GB)

방법처리량 (tok/s)VRAM 사용HumanEval Pass@1Latency (TTFT)
FP16 (기준)~35016GB53.2%45ms
GPTQ 4bit~5205.5GB50.6%32ms
GPTQ + Marlin~7125.5GB50.6%25ms
AWQ 4bit~5505.2GB51.8%30ms
AWQ + Marlin~7415.2GB51.8%23ms
GGUF Q4_K_M~2804.9GB51.8%55ms
BitsAndBytes NF4~3005.8GB51.8%50ms

위 벤치마크에서 주목할 점은 커널 구현이 알고리즘보다 중요하다는 것이다. Marlin 커널을 사용하면 GPTQ는 2.6배, AWQ는 10.9배 속도 향상을 얻는다.

vLLM에서 양자화 모델 서빙

from vllm import LLM, SamplingParams

# AWQ 모델 서빙 (Marlin 커널 자동 적용)
llm = LLM(
    model="casperhansen/llama-3.1-8b-instruct-awq",
    quantization="awq_marlin",      # Marlin 커널 명시
    max_model_len=8192,
    gpu_memory_utilization=0.85,
    dtype="half",
)

sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=1024,
)

prompts = [
    "Explain the CAP theorem in distributed systems.",
    "Write a Python function to implement binary search.",
    "What are the SOLID principles in software engineering?",
]

outputs = llm.generate(prompts, sampling_params)

for output in outputs:
    prompt = output.prompt
    generated = output.outputs[0].text
    print(f"Prompt: {prompt[:50]}...")
    print(f"Output: {generated[:200]}...")
    print("---")

모델별 양자화 품질 가이드

모델 크기별 권장 양자화

모델 크기소비자 GPU (24GB)서버 GPU (80GB)CPU 전용
7~8BGPTQ/AWQ 4bitFP16 권장GGUF Q4_K_M
13BGPTQ/AWQ 4bitFP16 또는 AWQ 4bitGGUF Q4_K_M
34BGGUF Q4_K_MAWQ 4bit + MarlinGGUF Q3_K_M
70BGGUF Q3_K_M (부분)AWQ 4bit + MarlinGGUF Q2_K

태스크별 권장 양자화 수준

운영 시 주의사항과 트러블슈팅

자주 발생하는 문제

1. GPTQ 양자화 중 OOM 발생

캘리브레이션 시 메모리 부족이 발생하면 desc_act=False로 변경하고, damp_percent를 높여보자.

2. AWQ 모델에서 이상한 출력

양자화 버전(GEMM vs GEMV)과 추론 프레임워크가 맞지 않을 수 있다. vLLM에서는 awq_marlin을 명시하는 것이 안전하다.

3. GGUF 모델이 느릴 때

n_gpu_layers 값을 높여 더 많은 레이어를 GPU로 오프로딩하자. 전체 레이어 수보다 높게 설정하면 전부 GPU로 올라간다.

4. BitsAndBytes에서 torch 버전 충돌

bitsandbytes>=0.43.0torch>=2.1.0 조합을 권장한다. CUDA 버전도 12.1 이상이 안정적이다.

양자화 품질 검증 체크리스트

  1. Perplexity 측정: 원본 대비 perplexity 증가가 0.5 이내인지 확인
  2. 태스크별 벤치마크: 실제 사용 태스크(코드 생성, 요약 등)에서 품질 확인
  3. Edge Case 테스트: 긴 입력, 다국어, 수학 문제 등 경계 사례 검증
  4. 레이턴시 프로파일링: TTFT(Time to First Token)와 TPS(Tokens per Second) 측정
  5. 메모리 모니터링: 실제 서빙 환경에서의 VRAM 사용량 추적

마치며

LLM 양자화는 단순히 모델 크기를 줄이는 것이 아니라, 배포 가능성을 결정하는 핵심 기술이다. 2026년 현재 권장 사항을 정리하면 다음과 같다.

양자화 기법은 계속 발전하고 있다. 2025년 이후 등장한 Marlin 커널처럼, 알고리즘 자체보다 커널 구현의 최적화가 실제 성능에 더 큰 영향을 미치는 추세다. 새로운 양자화 방법을 평가할 때는 이론적 장점뿐 아니라 실제 서빙 환경에서의 벤치마크를 반드시 확인하자.

참고자료

  1. Frantar et al., "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers" (2022)
  2. Lin et al., "AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration" (2024)
  3. Hugging Face Transformers Quantization Guide
  4. vLLM Quantization Documentation
  5. llama.cpp GitHub Repository - GGUF Format
  6. The Complete Guide to LLM Quantization with vLLM: Benchmarks
  7. AutoGPTQ GitHub Repository
  8. AutoAWQ - Applying AWQ Quantization
  9. BitsAndBytes Foundation GitHub
  10. Dettmers et al., "QLoRA: Efficient Finetuning of Quantized LLMs" (2023)

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다