- Authors
- Name
- 들어가며
- 양자화 기본 원리
- GPTQ: Generative Pre-trained Transformer Quantization
- AWQ: Activation-aware Weight Quantization
- GGUF: GPU-poor를 위한 범용 포맷
- BitsAndBytes와 QLoRA 통합
- 종합 비교: GPTQ vs AWQ vs GGUF vs BitsAndBytes
- 모델별 양자화 품질 가이드
- 운영 시 주의사항과 트러블슈팅
- 마치며
- 참고자료

들어가며
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 모델 메모리 |
|---|---|---|---|
| FP32 | 32bit | 약 ±3.4×10^38 | ~280GB |
| FP16 | 16bit | 약 ±6.5×10^4 | ~140GB |
| INT8 | 8bit | -128 ~ 127 | ~70GB |
| INT4 | 4bit | -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) 알고리즘을 기반으로 한다. 핵심 아이디어는 다음과 같다.
- Layer-wise 양자화: 모델 전체를 한꺼번에 양자화하지 않고, 레이어 단위로 순차 처리한다
- Hessian 기반 보정: 양자화로 인한 출력 오차를 Hessian 역행렬을 활용해 나머지 가중치로 보상한다
- 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의 장단점
장점:
- 4비트까지 양자화해도 원본 대비 높은 정확도 유지
- Marlin 커널 사용 시 추론 속도 대폭 향상 (2.6배 속도 증가)
- Hugging Face Transformers 네이티브 지원
단점:
- 캘리브레이션 데이터 필요 (보통 128~256 샘플)
- 양자화 시간이 상대적으로 긺 (8B 모델 기준 15~30분)
- GPU 전용 (CPU 추론 미지원)
AWQ: Activation-aware Weight Quantization
원리와 차별점
AWQ는 2024년 MIT의 Lin 등이 제안한 방법으로, 핵심 관찰은 모든 가중치가 동일하게 중요하지 않다는 점이다.
- Salient Weight 식별: 활성화(Activation) 크기를 기준으로 중요한 가중치 채널(약 1%)을 식별한다
- 선택적 보호: 중요 가중치는 스케일링 팩터를 적용하여 양자화 오차를 줄인다
- 나머지 양자화: 나머지 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의 장단점
장점:
- GPTQ보다 빠른 양자화 속도
- Activation-aware 접근으로 높은 품질 유지 (HumanEval Pass@1: 51.8%)
- Marlin-AWQ 커널 적용 시 741 tok/s의 높은 처리량
- vLLM 네이티브 지원
단점:
- GPU 전용 (CPU 추론 미지원)
- GGUF 대비 에코시스템이 작음
- 일부 모델 아키텍처에서 호환성 문제
GGUF: GPU-poor를 위한 범용 포맷
GGUF 포맷과 llama.cpp 생태계
GGUF(GPT-Generated Unified Format)는 양자화 알고리즘이 아니라 파일 포맷이다. llama.cpp 프로젝트에서 만든 이 포맷은 CPU와 GPU 하이브리드 추론을 지원한다.
GGUF의 핵심 특징:
- 단일 파일: 모델 가중치, 토크나이저, 메타데이터가 하나의 파일에 포함
- CPU 추론 지원: GPU 없이도 추론 가능
- 하이브리드 오프로딩: 일부 레이어는 GPU, 나머지는 CPU로 처리
- 다양한 양자화 수준: Q2_K부터 Q8_0까지 세밀한 품질-크기 조절
GGUF 양자화 타입별 비교
| 양자화 타입 | 비트 수 | Llama-3.1-8B 크기 | Perplexity 증가 | 권장 용도 |
|---|---|---|---|---|
| Q2_K | 2.6bit | ~2.8GB | +2.5~3.0 | 극한 메모리 제한 |
| Q3_K_M | 3.3bit | ~3.5GB | +1.0~1.5 | 메모리 제한 환경 |
| Q4_K_M | 4.5bit | ~4.9GB | +0.3~0.5 | 범용 권장 |
| Q5_K_M | 5.3bit | ~5.7GB | +0.1~0.2 | 품질 중시 |
| Q6_K | 6.6bit | ~6.6GB | +0.05 | 거의 무손실 |
| Q8_0 | 8.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의 특징
장점:
- 사전 양자화 과정 불필요 (로딩 시 즉시 적용)
- QLoRA 파인튜닝과 자연스러운 통합
- Hugging Face 에코시스템 완벽 지원
- NF4 양자화로 높은 품질 유지
단점:
- 추론 속도가 GPTQ/AWQ 대비 느림 (커스텀 커널 부재)
- 양자화된 모델을 파일로 저장/공유하기 어려움
- CUDA 전용 (AMD/CPU 미지원, 다만 2025년부터 ROCm 부분 지원)
종합 비교: GPTQ vs AWQ vs GGUF vs BitsAndBytes
핵심 특성 비교표
| 항목 | GPTQ | AWQ | GGUF | BitsAndBytes |
|---|---|---|---|---|
| 양자화 방식 | PTQ (Hessian 기반) | PTQ (Activation-aware) | PTQ (다양한 방식) | 동적 양자화 |
| 기본 비트 | 4bit | 4bit | 2~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.4 | Q4_K_M: +0.3~0.5 | 기준 +0.2~0.4 |
추론 성능 벤치마크 (Llama-3.1-8B, A100 80GB)
| 방법 | 처리량 (tok/s) | VRAM 사용 | HumanEval Pass@1 | Latency (TTFT) |
|---|---|---|---|---|
| FP16 (기준) | ~350 | 16GB | 53.2% | 45ms |
| GPTQ 4bit | ~520 | 5.5GB | 50.6% | 32ms |
| GPTQ + Marlin | ~712 | 5.5GB | 50.6% | 25ms |
| AWQ 4bit | ~550 | 5.2GB | 51.8% | 30ms |
| AWQ + Marlin | ~741 | 5.2GB | 51.8% | 23ms |
| GGUF Q4_K_M | ~280 | 4.9GB | 51.8% | 55ms |
| BitsAndBytes NF4 | ~300 | 5.8GB | 51.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~8B | GPTQ/AWQ 4bit | FP16 권장 | GGUF Q4_K_M |
| 13B | GPTQ/AWQ 4bit | FP16 또는 AWQ 4bit | GGUF Q4_K_M |
| 34B | GGUF Q4_K_M | AWQ 4bit + Marlin | GGUF Q3_K_M |
| 70B | GGUF Q3_K_M (부분) | AWQ 4bit + Marlin | GGUF Q2_K |
태스크별 권장 양자화 수준
- 코드 생성: AWQ 4bit 권장 (HumanEval 기준 가장 높은 정확도)
- 장문 생성/요약: Q5_K_M 이상 권장 (낮은 양자화에서 반복 증가)
- 분류/NER: INT4도 충분 (정확도 영향 미미)
- 수학적 추론: Q6_K 이상 또는 AWQ 4bit 권장 (수치 민감)
운영 시 주의사항과 트러블슈팅
자주 발생하는 문제
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.0과 torch>=2.1.0 조합을 권장한다. CUDA 버전도 12.1 이상이 안정적이다.
양자화 품질 검증 체크리스트
- Perplexity 측정: 원본 대비 perplexity 증가가 0.5 이내인지 확인
- 태스크별 벤치마크: 실제 사용 태스크(코드 생성, 요약 등)에서 품질 확인
- Edge Case 테스트: 긴 입력, 다국어, 수학 문제 등 경계 사례 검증
- 레이턴시 프로파일링: TTFT(Time to First Token)와 TPS(Tokens per Second) 측정
- 메모리 모니터링: 실제 서빙 환경에서의 VRAM 사용량 추적
마치며
LLM 양자화는 단순히 모델 크기를 줄이는 것이 아니라, 배포 가능성을 결정하는 핵심 기술이다. 2026년 현재 권장 사항을 정리하면 다음과 같다.
- 프로덕션 GPU 서빙: AWQ + Marlin 커널 (최고 처리량 + 높은 품질)
- 개발/실험: BitsAndBytes NF4 (양자화 과정 불필요, QLoRA 통합)
- 엣지/CPU 배포: GGUF Q4_K_M (범용성, 단일 파일 배포)
- 레거시 호환: GPTQ (가장 넓은 에코시스템 지원)
양자화 기법은 계속 발전하고 있다. 2025년 이후 등장한 Marlin 커널처럼, 알고리즘 자체보다 커널 구현의 최적화가 실제 성능에 더 큰 영향을 미치는 추세다. 새로운 양자화 방법을 평가할 때는 이론적 장점뿐 아니라 실제 서빙 환경에서의 벤치마크를 반드시 확인하자.
참고자료
- Frantar et al., "GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers" (2022)
- Lin et al., "AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration" (2024)
- Hugging Face Transformers Quantization Guide
- vLLM Quantization Documentation
- llama.cpp GitHub Repository - GGUF Format
- The Complete Guide to LLM Quantization with vLLM: Benchmarks
- AutoGPTQ GitHub Repository
- AutoAWQ - Applying AWQ Quantization
- BitsAndBytes Foundation GitHub
- Dettmers et al., "QLoRA: Efficient Finetuning of Quantized LLMs" (2023)