Skip to content
Published on

LLM 양자화(Quantization) 실전 가이드: GPTQ·AWQ·GGUF 포맷 비교와 정밀도-성능 트레이드오프

Authors
  • Name
    Twitter

LLM 양자화 실전 가이드

1. 서론: 왜 양자화인가

LLM을 프로덕션에 배포할 때 가장 큰 병목은 GPU 메모리와 추론 비용이다. Llama 3 70B 모델은 FP16 기준 약 140GB의 GPU 메모리를 필요로 하며, 이는 A100 80GB GPU 2장이 최소 요구 사양이다. 양자화(Quantization)는 모델 가중치의 정밀도를 줄여 메모리 사용량과 연산량을 대폭 절감하는 기술이다.

2025-2026년 현재, 양자화 기술은 단순한 비용 절감 수단을 넘어 프로덕션 배포의 필수 요소로 자리잡았다. AWQ가 GPTQ 대비 정확도와 속도 모두에서 우위를 입증했고, llama.cpp의 GGUF 포맷이 엣지 디바이스 배포의 사실상 표준이 되었다.

이 글에서는 양자화의 기초 이론부터 GPTQ, AWQ, GGUF, bitsandbytes NF4까지 주요 기법을 비교 분석하고, 실전 코드와 벤치마크를 통해 최적의 양자화 전략을 제시한다.

2. 양자화 기초 이론

2.1 수치 표현과 정밀도

딥러닝 모델에서 사용하는 수치 표현의 종류와 특성을 이해하는 것이 양자화의 출발점이다.

데이터 타입비트 수범위용도
FP3232약 1.18e-38 ~ 3.4e+38학습 기본값
FP1616약 5.96e-8 ~ 65504혼합 정밀도 학습
BF1616FP32과 동일 범위, 낮은 정밀도대규모 모델 학습
INT88-128 ~ 127양자화 추론
INT44-8 ~ 7공격적 양자화
NF44정규분포 최적화QLoRA/bitsandbytes

2.2 양자화의 수학적 원리

양자화는 연속적인 부동소수점 값을 이산적인 정수 값으로 매핑하는 과정이다.

import numpy as np

def symmetric_quantize(weights, n_bits=8):
    """대칭 양자화: zero point가 0"""
    qmax = 2**(n_bits - 1) - 1
    qmin = -2**(n_bits - 1)

    # 스케일 계산
    abs_max = np.max(np.abs(weights))
    scale = abs_max / qmax

    # 양자화
    quantized = np.clip(np.round(weights / scale), qmin, qmax).astype(np.int8)

    return quantized, scale

def asymmetric_quantize(weights, n_bits=8):
    """비대칭 양자화: zero point 사용"""
    qmax = 2**n_bits - 1
    qmin = 0

    w_min = np.min(weights)
    w_max = np.max(weights)

    scale = (w_max - w_min) / (qmax - qmin)
    zero_point = int(np.round(-w_min / scale))

    quantized = np.clip(
        np.round(weights / scale) + zero_point, qmin, qmax
    ).astype(np.uint8)

    return quantized, scale, zero_point

def dequantize(quantized, scale, zero_point=0):
    """역양자화: 정수를 다시 부동소수점으로"""
    return scale * (quantized.astype(np.float32) - zero_point)

# 예시
weights = np.random.randn(1000).astype(np.float32)
q_sym, scale_sym = symmetric_quantize(weights, n_bits=4)
q_asym, scale_asym, zp = asymmetric_quantize(weights, n_bits=4)

print(f"원본 메모리: {weights.nbytes} bytes")
print(f"INT4 메모리: {q_sym.nbytes // 2} bytes (이론값)")
print(f"압축률: {weights.nbytes / (q_sym.nbytes // 2):.1f}x")

2.3 PTQ vs QAT

양자화 기법은 크게 두 가지로 나뉜다.

Post-Training Quantization (PTQ)

  • 이미 학습된 모델에 양자화를 적용
  • 추가 학습이 필요 없어 빠르고 간편
  • GPTQ, AWQ, GGUF가 이 방식
  • 캘리브레이션 데이터만 필요 (보통 128~512 샘플)

Quantization-Aware Training (QAT)

  • 학습 과정에서 양자화 효과를 시뮬레이션
  • 모델이 양자화 노이즈에 적응하여 더 높은 정확도
  • 전체 학습 비용 발생
  • 소규모 모델에서 주로 사용
# PTQ vs QAT 비교 파이프라인
class QuantizationPipeline:
    """양자화 파이프라인 비교"""

    @staticmethod
    def ptq_pipeline(model, calibration_data, method="awq"):
        """PTQ 파이프라인"""
        # 1. 캘리브레이션 데이터로 통계 수집
        # 2. 양자화 파라미터 결정 (scale, zero_point)
        # 3. 가중치 양자화 수행
        # 4. 양자화된 모델 저장
        steps = [
            "Load pretrained model",
            "Run calibration (128 samples)",
            "Compute quantization parameters",
            "Quantize weights",
            "Save quantized model",
        ]
        return steps

    @staticmethod
    def qat_pipeline(model, training_data):
        """QAT 파이프라인"""
        # 1. Fake quantization 노드 삽입
        # 2. 전체 또는 부분 재학습
        # 3. 실제 양자화 적용
        # 4. 양자화된 모델 저장
        steps = [
            "Insert fake quantization nodes",
            "Fine-tune with quantization simulation",
            "Convert to actual quantized model",
            "Evaluate and save",
        ]
        return steps

2.4 그룹 양자화 (Group Quantization)

현대 양자화 기법의 핵심은 그룹 양자화이다. 전체 텐서에 하나의 scale/zero_point를 사용하는 대신, 일정 크기의 그룹 단위로 별도의 양자화 파라미터를 사용한다.

def group_quantize(weights, n_bits=4, group_size=128):
    """그룹 단위 양자화"""
    qmax = 2**(n_bits - 1) - 1
    qmin = -2**(n_bits - 1)

    # 그룹 단위로 분할
    num_groups = weights.shape[-1] // group_size
    w_grouped = weights.reshape(-1, num_groups, group_size)

    scales = []
    zeros = []
    quantized_groups = []

    for i in range(num_groups):
        group = w_grouped[:, i, :]
        abs_max = np.max(np.abs(group), axis=-1, keepdims=True)
        scale = abs_max / qmax

        q = np.clip(np.round(group / (scale + 1e-10)), qmin, qmax)
        quantized_groups.append(q)
        scales.append(scale)

    return quantized_groups, scales

# group_size가 작을수록 정밀도 높지만 오버헤드 증가
# 일반적으로 group_size=128이 최적 균형점

3. GPTQ: 초기 혁신

3.1 GPTQ 알고리즘 원리

GPTQ(Generative Pre-trained Transformer Quantization)는 2022년 Frantar et al.이 제안한 PTQ 알고리즘으로, Optimal Brain Quantization(OBQ)을 개선한 것이다. 핵심 아이디어는 양자화 오류를 최소화하기 위해 헤시안(Hessian) 정보를 활용하는 것이다.

GPTQ의 주요 특징은 다음과 같다.

  • 열(column) 단위로 순차적 양자화 수행
  • 한 열의 양자화 오류를 이후 열의 가중치 업데이트로 보상
  • Cholesky 분해를 활용한 효율적 헤시안 역행렬 계산
  • 캘리브레이션 데이터 128개로 충분

3.2 AutoGPTQ를 이용한 양자화

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

# 1. 모델과 토크나이저 로드
model_name = "meta-llama/Llama-3.1-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 2. 양자화 설정
quantize_config = BaseQuantizeConfig(
    bits=4,                  # 4비트 양자화
    group_size=128,          # 그룹 크기
    desc_act=True,           # Activation order (정확도 향상)
    damp_percent=0.01,       # 댐핑 비율
    sym=True,                # 대칭 양자화
    true_sequential=True,    # 순차적 양자화 (정확도 향상)
    model_seqlen=4096,       # 시퀀스 길이
)

# 3. 모델 로드
model = AutoGPTQForCausalLM.from_pretrained(
    model_name,
    quantize_config=quantize_config,
    torch_dtype=torch.float16,
)

# 4. 캘리브레이션 데이터 준비
from datasets import load_dataset

dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
calibration_texts = [text for text in dataset["text"] if len(text) > 100][:128]

calibration_data = []
for text in calibration_texts:
    tokens = tokenizer(
        text,
        return_tensors="pt",
        max_length=2048,
        truncation=True,
        padding=False,
    )
    calibration_data.append(tokens.input_ids)

# 5. 양자화 실행 (7B 모델 기준 약 15-30분)
model.quantize(calibration_data)

# 6. 양자화된 모델 저장
output_dir = "./llama3-8b-gptq-4bit"
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)

print(f"양자화 완료! 저장 위치: {output_dir}")

3.3 GPTQ 모델 추론

from auto_gptq import AutoGPTQForCausalLM
from transformers import AutoTokenizer

# 양자화된 모델 로드
model = AutoGPTQForCausalLM.from_quantized(
    "./llama3-8b-gptq-4bit",
    device_map="auto",
    use_safetensors=True,
    inject_fused_attention=True,   # Fused attention 사용
    inject_fused_mlp=True,         # Fused MLP 사용
    use_triton=False,              # CUDA 커널 사용
    disable_exllama=False,         # ExLlama v2 커널 활성화
    exllama_config={"version": 2}, # ExLlama v2 사용
)

tokenizer = AutoTokenizer.from_pretrained("./llama3-8b-gptq-4bit")

# 추론
prompt = "Explain quantum computing in simple terms:"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=256,
        temperature=0.7,
        do_sample=True,
    )

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

4. AWQ: 활성화 인지 양자화

4.1 AWQ 알고리즘 원리

AWQ(Activation-aware Weight Quantization)는 MIT Han Lab에서 2023년에 제안한 양자화 기법으로, GPTQ의 한계를 극복한다. 핵심 통찰은 "모든 가중치가 동등하게 중요하지 않다"는 것이다.

AWQ의 핵심 원리는 다음과 같다.

  1. 활성화 인지: 입력 활성화의 크기가 큰 채널의 가중치가 더 중요
  2. 채널별 스케일링: 중요한 채널의 가중치를 스케일 업하여 양자화 오류 감소
  3. 등가 변환: 스케일링 팩터를 다음 레이어에 흡수하여 추가 연산 비용 없음
  4. 가중치만 양자화: 활성화는 FP16 유지

4.2 AWQ vs GPTQ 핵심 차이

특성GPTQAWQ
양자화 방식헤시안 기반 오류 보상활성화 인지 스케일링
캘리브레이션 속도느림 (15-30분)빠름 (5-10분)
4bit 정확도좋음매우 좋음
추론 속도빠름더 빠름
메모리 효율좋음좋음
커널 최적화ExLlama v2AWQ GEMM 커널
vLLM 지원지원우선 지원
대형 모델 (70B+)정확도 저하 가능안정적

4.3 AutoAWQ를 이용한 양자화

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

# 1. 모델 로드
model_name = "meta-llama/Llama-3.1-8B"
model = AutoAWQForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 2. 양자화 설정
quant_config = {
    "zero_point": True,        # 비대칭 양자화 (정확도 향상)
    "q_group_size": 128,       # 그룹 크기
    "w_bit": 4,                # 4비트 양자화
    "version": "GEMM",        # GEMM 커널 사용 (배치 추론 최적)
}

# 3. 양자화 실행 (7B 모델 기준 약 5-10분)
model.quantize(tokenizer, quant_config=quant_config)

# 4. 저장
output_dir = "./llama3-8b-awq-4bit"
model.save_quantized(output_dir)
tokenizer.save_pretrained(output_dir)

print(f"AWQ 양자화 완료! 저장 위치: {output_dir}")

4.4 AWQ GEMM vs GEMV 커널

AWQ는 두 가지 커널 구현을 제공한다.

# GEMM 커널: 배치 추론에 최적화
quant_config_gemm = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM",   # 배치 크기 > 1일 때 최적
}

# GEMV 커널: 단일 요청 추론에 최적화
quant_config_gemv = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMV",   # 배치 크기 = 1일 때 최적
}

# 선택 가이드:
# - 서빙 (여러 사용자 동시 처리) -> GEMM
# - 로컬 추론 (단일 사용자) -> GEMV
# - vLLM 통합 -> GEMM (필수)

4.5 AWQ 고급 설정

from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer

model_name = "meta-llama/Llama-3.1-70B"

# 대형 모델 양자화 설정
model = AutoAWQForCausalLM.from_pretrained(
    model_name,
    safetensors=True,
    device_map="auto",     # 여러 GPU에 자동 분배
)

tokenizer = AutoTokenizer.from_pretrained(model_name)

# 70B 모델 최적 설정
quant_config = {
    "zero_point": True,
    "q_group_size": 128,
    "w_bit": 4,
    "version": "GEMM",
}

# 커스텀 캘리브레이션 데이터 사용
from datasets import load_dataset

# 도메인 특화 캘리브레이션 데이터
calib_data = load_dataset(
    "HuggingFaceH4/ultrachat_200k",
    split="train_sft",
)

# 양자화 실행 (70B 모델 기준 약 1-2시간, A100 80GB x2)
model.quantize(
    tokenizer,
    quant_config=quant_config,
    calib_data=calib_data,
    n_samples=128,
    seqlen=2048,
)

model.save_quantized("./llama3-70b-awq-4bit")
tokenizer.save_pretrained("./llama3-70b-awq-4bit")

5. GGUF: 엣지 디바이스의 표준

5.1 GGUF 포맷 개요

GGUF(GPT-Generated Unified Format)는 llama.cpp 프로젝트에서 개발한 모델 포맷으로, CPU 및 엣지 디바이스에서의 추론에 최적화되어 있다. 이전 GGML 포맷을 대체하며, 다음과 같은 특징을 가진다.

  • 단일 파일에 모델 가중치와 메타데이터 포함
  • 다양한 양자화 레벨 지원 (Q2_K ~ Q8_0)
  • CPU, Metal (Apple), CUDA, Vulkan 등 다양한 백엔드 지원
  • 메모리 맵(mmap) 기반 빠른 로딩
  • llama.cpp, ollama, LM Studio 등에서 사용

5.2 GGUF 양자화 타입 비교

양자화 타입비트방식품질속도용도
Q2_K2.6K-quant mixed낮음매우 빠름극한 압축
Q3_K_S3.4K-quant small보통 이하빠름메모리 제한
Q3_K_M3.9K-quant medium보통빠름균형
Q4_04.5Legacy보통빠름레거시
Q4_K_S4.6K-quant small좋음빠름추천
Q4_K_M4.8K-quant medium좋음보통추천 (기본)
Q5_K_S5.5K-quant small매우 좋음보통품질 중시
Q5_K_M5.7K-quant medium매우 좋음보통품질 중시
Q6_K6.6K-quant우수느림거의 원본 수준
Q8_08.5Round-to-nearest최상느림최고 품질
F1616무양자화원본가장 느림기준값

5.3 llama.cpp를 이용한 GGUF 변환

# 1. llama.cpp 빌드
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make -j$(nproc) LLAMA_CUDA=1  # CUDA 지원 빌드

# 2. HuggingFace 모델을 GGUF로 변환
python convert_hf_to_gguf.py \
    ../meta-llama/Llama-3.1-8B \
    --outtype f16 \
    --outfile llama3-8b-f16.gguf

# 3. 양자화 수행
./llama-quantize \
    llama3-8b-f16.gguf \
    llama3-8b-Q4_K_M.gguf \
    Q4_K_M

# 4. 다양한 양자화 레벨로 변환
for quant in Q3_K_M Q4_K_S Q4_K_M Q5_K_M Q6_K Q8_0; do
    ./llama-quantize \
        llama3-8b-f16.gguf \
        "llama3-8b-${quant}.gguf" \
        "$quant"
    echo "Completed: $quant"
done

# 5. Importance Matrix를 활용한 고품질 양자화
./llama-imatrix \
    -m llama3-8b-f16.gguf \
    -f calibration_data.txt \
    --output-frequency 10 \
    -o imatrix.dat

./llama-quantize \
    --imatrix imatrix.dat \
    llama3-8b-f16.gguf \
    llama3-8b-IQ4_XS.gguf \
    IQ4_XS

5.4 GGUF 모델 추론

# llama.cpp CLI로 추론
./llama-cli \
    -m llama3-8b-Q4_K_M.gguf \
    -p "Explain the theory of relativity:" \
    -n 256 \
    --temp 0.7 \
    --top-p 0.9 \
    -ngl 35    # GPU에 오프로드할 레이어 수

# 서버 모드로 실행
./llama-server \
    -m llama3-8b-Q4_K_M.gguf \
    --host 0.0.0.0 \
    --port 8080 \
    -ngl 35 \
    --ctx-size 4096 \
    --parallel 4   # 동시 요청 수
# Python에서 llama-cpp-python으로 사용
from llama_cpp import Llama

# 모델 로드
llm = Llama(
    model_path="./llama3-8b-Q4_K_M.gguf",
    n_gpu_layers=35,     # GPU 오프로드
    n_ctx=4096,          # 컨텍스트 크기
    n_batch=512,         # 배치 크기
    verbose=False,
)

# 추론
output = llm(
    "Explain quantum computing in simple terms:",
    max_tokens=256,
    temperature=0.7,
    top_p=0.9,
    stop=["###", "\n\n\n"],
)

print(output["choices"][0]["text"])

# OpenAI 호환 API로 사용
output = llm.create_chat_completion(
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "What is quantum computing?"},
    ],
    max_tokens=256,
    temperature=0.7,
)

print(output["choices"][0]["message"]["content"])

6. bitsandbytes NF4: QLoRA의 핵심

6.1 NormalFloat4 (NF4) 이론

bitsandbytes의 NF4(NormalFloat4)는 QLoRA 논문에서 소개된 데이터 타입으로, 정규분포를 따르는 가중치에 최적화된 4비트 양자화이다.

핵심 특징은 다음과 같다.

  • 가중치 분포가 정규분포를 따른다는 가정 하에 최적의 양자화 레벨을 결정
  • 일반 INT4 대비 정보 이론적으로 최적
  • 이중 양자화(Double Quantization)로 양자화 상수의 메모리도 절약
  • 추론보다는 파인튜닝(QLoRA)에 주로 사용

6.2 bitsandbytes 4비트 로딩

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

# NF4 양자화 설정
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                    # 4비트 로딩
    bnb_4bit_quant_type="nf4",            # NF4 타입
    bnb_4bit_compute_dtype=torch.bfloat16, # 연산은 BF16
    bnb_4bit_use_double_quant=True,        # 이중 양자화
    bnb_4bit_quant_storage=torch.uint8,    # 저장 타입
)

# 모델 로드
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.bfloat16,
)

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

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

# INT8 양자화도 가능
bnb_config_8bit = BitsAndBytesConfig(
    load_in_8bit=True,
    llm_int8_threshold=6.0,     # 아웃라이어 임계값
    llm_int8_has_fp16_weight=False,
)

6.3 bitsandbytes + QLoRA 파인튜닝

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import TrainingArguments, Trainer

# 4비트 모델을 파인튜닝 가능하도록 준비
model = prepare_model_for_kbit_training(model)

# LoRA 설정
lora_config = LoraConfig(
    r=16,                          # LoRA 랭크
    lora_alpha=32,                 # 스케일링 팩터
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)

# 학습 가능한 파라미터 확인
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"학습 가능: {trainable_params:,} / 전체: {total_params:,}")
print(f"비율: {100 * trainable_params / total_params:.2f}%")

7. 종합 비교 및 벤치마크

7.1 메모리 사용량 비교

모델 크기FP16GPTQ 4bitAWQ 4bitGGUF Q4_K_MNF4
7B14 GB4.2 GB4.1 GB4.4 GB4.5 GB
13B26 GB7.8 GB7.6 GB8.2 GB8.4 GB
34B68 GB20 GB19.5 GB21 GB21.5 GB
70B140 GB40 GB39 GB42 GB43 GB

7.2 Perplexity 비교 (WikiText-2)

모델FP16GPTQ 4bitAWQ 4bitGGUF Q4_K_MGGUF Q5_K_M
Llama 3 8B6.146.486.326.416.22
Llama 3 70B3.323.553.423.513.38
Mistral 7B5.255.585.415.495.32
Qwen2.5 72B3.183.413.293.373.24

7.3 추론 속도 비교 (tokens/sec, A100 80GB)

모델FP16GPTQ 4bitAWQ 4bitGGUF Q4_K_M (GPU)
Llama 3 8B (bs=1)45829578
Llama 3 8B (bs=16)58011501320N/A
Llama 3 70B (bs=1)12283224
Llama 3 70B (bs=16)142320365N/A

7.4 메모리 절감 계산

def calculate_memory_savings(
    model_params_billions: float,
    original_bits: int = 16,
    target_bits: int = 4,
    group_size: int = 128,
):
    """양자화 메모리 절감 계산기"""
    # 원본 메모리
    original_bytes = model_params_billions * 1e9 * (original_bits / 8)
    original_gb = original_bytes / (1024**3)

    # 양자화 후 가중치 메모리
    quantized_bytes = model_params_billions * 1e9 * (target_bits / 8)

    # 양자화 메타데이터 (scale + zero_point per group)
    num_groups = model_params_billions * 1e9 / group_size
    # 각 그룹당 scale(FP16=2bytes) + zero_point(FP16=2bytes)
    metadata_bytes = num_groups * 4

    total_quantized_bytes = quantized_bytes + metadata_bytes
    quantized_gb = total_quantized_bytes / (1024**3)

    # KV 캐시 메모리 (FP16, 별도)
    # 대략적 계산: 2 * num_layers * 2 * hidden_dim * seq_len * 2bytes
    # 이 부분은 양자화와 무관

    savings_pct = (1 - quantized_gb / original_gb) * 100

    return {
        "original_gb": round(original_gb, 2),
        "quantized_gb": round(quantized_gb, 2),
        "savings_gb": round(original_gb - quantized_gb, 2),
        "savings_pct": round(savings_pct, 1),
        "compression_ratio": round(original_gb / quantized_gb, 2),
    }

# 각 모델 크기별 절감 효과
for params in [7, 13, 34, 70]:
    result = calculate_memory_savings(params)
    print(f"\n{params}B 모델:")
    print(f"  FP16: {result['original_gb']} GB")
    print(f"  INT4: {result['quantized_gb']} GB")
    print(f"  절감: {result['savings_gb']} GB ({result['savings_pct']}%)")
    print(f"  압축률: {result['compression_ratio']}x")

8. vLLM에서의 양자화 모델 서빙

8.1 AWQ 모델 서빙

from vllm import LLM, SamplingParams

# AWQ 모델 로드 (자동 감지)
llm = LLM(
    model="./llama3-8b-awq-4bit",
    quantization="awq",
    dtype="half",
    gpu_memory_utilization=0.90,
    max_model_len=8192,
    tensor_parallel_size=1,
)

# 서빙
sampling_params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=256,
)

prompts = [
    "What is machine learning?",
    "Explain neural networks:",
    "How does gradient descent work?",
]

outputs = llm.generate(prompts, sampling_params)

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

8.2 GPTQ 모델 서빙

from vllm import LLM, SamplingParams

# GPTQ 모델 서빙
llm = LLM(
    model="./llama3-8b-gptq-4bit",
    quantization="gptq",
    dtype="half",
    gpu_memory_utilization=0.90,
    max_model_len=8192,
)

# vLLM OpenAI 호환 서버로 실행
# python -m vllm.entrypoints.openai.api_server \
#     --model ./llama3-8b-gptq-4bit \
#     --quantization gptq \
#     --dtype half \
#     --port 8000 \
#     --gpu-memory-utilization 0.9

8.3 vLLM 양자화 모델 성능 최적화

# vLLM 서버 실행 (프로덕션 설정)
python -m vllm.entrypoints.openai.api_server \
    --model ./llama3-70b-awq-4bit \
    --quantization awq \
    --dtype half \
    --tensor-parallel-size 2 \
    --gpu-memory-utilization 0.92 \
    --max-model-len 8192 \
    --max-num-batched-tokens 32768 \
    --max-num-seqs 256 \
    --enable-chunked-prefill \
    --port 8000

# 벤치마크 실행
python -m vllm.entrypoints.openai.api_server \
    --model ./llama3-8b-awq-4bit \
    --quantization awq &

# 부하 테스트
python benchmarks/benchmark_serving.py \
    --model ./llama3-8b-awq-4bit \
    --num-prompts 1000 \
    --request-rate 10 \
    --endpoint /v1/completions

9. 양자화 포맷 선택 가이드

9.1 의사결정 흐름

양자화 포맷 선택 흐름:

1. 목적이 무엇인가?
   - 파인튜닝 -> bitsandbytes NF4 + QLoRA
   - 프로덕션 서빙 -> 2번으로
   - 로컬/엣지 배포 -> GGUF

2. 서빙 프레임워크?
   - vLLM -> AWQ (최적) 또는 GPTQ
   - TensorRT-LLM -> AWQ (권장)
   - 직접 구현 -> AWQ

3. 하드웨어?
   - NVIDIA GPU -> AWQ/GPTQ
   - Apple Silicon -> GGUF (Metal)
   - CPU 전용 -> GGUF
   - AMD GPU -> GGUF (ROCm/Vulkan)

4. 품질 요구 수준?
   - 최대 품질 -> Q5_K_M 또는 Q6_K (GGUF)
   - 균형 -> Q4_K_M (GGUF) 또는 AWQ 4bit
   - 최소 메모리 -> Q3_K_M 또는 Q2_K (GGUF)

9.2 시나리오별 추천

시나리오추천 포맷이유
프로덕션 API 서빙AWQ 4bit + vLLM최고의 throughput
로컬 개발/테스트GGUF Q4_K_M범용 호환성
MacBook 로컬 추론GGUF Q4_K_M + MetalApple Silicon 최적화
파인튜닝NF4 + QLoRA메모리 효율적 학습
엣지 디바이스GGUF Q3_K_M최소 메모리
고품질 서빙AWQ 4bit낮은 perplexity 열화
멀티 GPU 서빙AWQ + TP스케일아웃 지원
배치 처리GPTQ + ExLlama v2안정적 배치 성능

10. 트러블슈팅

10.1 일반적인 문제와 해결책

CUDA OOM (Out of Memory)

# 문제: 양자화 중 GPU 메모리 부족
# 해결 1: max_memory 설정
model = AutoAWQForCausalLM.from_pretrained(
    model_name,
    max_memory={0: "20GiB", 1: "20GiB", "cpu": "30GiB"},
)

# 해결 2: CPU 오프로드
model = AutoAWQForCausalLM.from_pretrained(
    model_name,
    device_map="auto",
    offload_folder="./offload",
)

양자화 후 품질 저하

# 문제: 양자화 후 출력 품질 급격히 저하
# 해결 1: 그룹 크기 줄이기
quant_config = {"q_group_size": 64, "w_bit": 4}  # 128 -> 64

# 해결 2: 5비트 사용
quant_config = {"q_group_size": 128, "w_bit": 5}  # 4bit -> 5bit (GPTQ only)

# 해결 3: 도메인 특화 캘리브레이션 데이터 사용
# 의료/법률 등 특수 도메인은 해당 도메인 텍스트로 캘리브레이션

vLLM에서 양자화 모델 로드 실패

# 문제: vLLM이 양자화 모델을 인식하지 못함
# 해결: config.json에 quantization_config 확인
cat model_dir/config.json | python -m json.tool | grep -A 10 quant

# GPTQ config 예시 (config.json에 포함되어야 함):
# "quantization_config": {
#     "bits": 4,
#     "group_size": 128,
#     "quant_method": "gptq"
# }

# AWQ config 예시:
# "quantization_config": {
#     "bits": 4,
#     "group_size": 128,
#     "quant_method": "awq",
#     "version": "gemm",
#     "zero_point": true
# }

10.2 성능 최적화 체크리스트

  • AWQ GEMM 커널 사용 (배치 서빙 시)
  • ExLlama v2 커널 활성화 (GPTQ 사용 시)
  • GPU 레이어 오프로드 수 최적화 (GGUF)
  • 캘리브레이션 데이터 도메인 일치 확인
  • group_size 128 사용 (기본 권장)
  • vLLM gpu_memory_utilization 0.9 이상 설정
  • Tensor Parallelism으로 대형 모델 분산
  • KV 캐시 크기 확인 및 조정
  • 배치 크기에 따른 커널 선택 (GEMM vs GEMV)
  • 양자화 후 perplexity 측정으로 품질 검증

11. 실전 벤치마크 스크립트

import time
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

def benchmark_model(model, tokenizer, prompts, max_tokens=128):
    """모델 추론 벤치마크"""
    results = []

    for prompt in prompts:
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

        # Warmup
        with torch.no_grad():
            model.generate(**inputs, max_new_tokens=1)

        # 측정
        torch.cuda.synchronize()
        start = time.perf_counter()

        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=max_tokens,
                do_sample=False,
            )

        torch.cuda.synchronize()
        elapsed = time.perf_counter() - start

        num_tokens = outputs.shape[1] - inputs.input_ids.shape[1]
        tokens_per_sec = num_tokens / elapsed

        results.append({
            "prompt_len": inputs.input_ids.shape[1],
            "output_len": num_tokens,
            "time_sec": round(elapsed, 3),
            "tokens_per_sec": round(tokens_per_sec, 1),
        })

    # GPU 메모리 사용량
    memory_gb = torch.cuda.max_memory_allocated() / (1024**3)

    avg_tps = sum(r["tokens_per_sec"] for r in results) / len(results)

    return {
        "avg_tokens_per_sec": round(avg_tps, 1),
        "gpu_memory_gb": round(memory_gb, 2),
        "results": results,
    }

# 벤치마크 프롬프트
test_prompts = [
    "Explain the concept of quantum entanglement in detail:",
    "Write a Python function to implement binary search:",
    "Describe the architecture of a modern web application:",
    "What are the key differences between SQL and NoSQL databases?",
    "Explain how neural networks learn from data:",
]

# FP16 벤치마크
print("=== FP16 Benchmark ===")
model_fp16 = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    torch_dtype=torch.float16,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")
result_fp16 = benchmark_model(model_fp16, tokenizer, test_prompts)
print(f"FP16 - Speed: {result_fp16['avg_tokens_per_sec']} tok/s, "
      f"Memory: {result_fp16['gpu_memory_gb']} GB")

del model_fp16
torch.cuda.empty_cache()

# AWQ 4bit 벤치마크
print("\n=== AWQ 4bit Benchmark ===")
from awq import AutoAWQForCausalLM
model_awq = AutoAWQForCausalLM.from_quantized(
    "./llama3-8b-awq-4bit",
    fuse_layers=True,
)
result_awq = benchmark_model(model_awq.model, tokenizer, test_prompts)
print(f"AWQ  - Speed: {result_awq['avg_tokens_per_sec']} tok/s, "
      f"Memory: {result_awq['gpu_memory_gb']} GB")

12. 최신 동향 및 전망

12.1 2026년 양자화 기술 트렌드

  1. AWQ의 표준화: vLLM, TensorRT-LLM 등 주요 서빙 프레임워크에서 AWQ를 기본 양자화 포맷으로 채택
  2. FP8 양자화: H100/H200 GPU의 FP8 네이티브 지원으로 8비트 양자화가 성능 손실 없이 가능
  3. GGUF 생태계 확장: llama.cpp, ollama, LM Studio 등 로컬 추론 도구의 급성장
  4. 혼합 정밀도 양자화: 레이어별로 다른 비트 수를 적용하여 정확도-효율 최적화
  5. 1-bit LLM (BitNet): Microsoft의 BitNet b1.58 연구로 극단적 양자화 가능성 입증

12.2 FP8 양자화 (차세대 표준)

# vLLM에서 FP8 양자화 (H100/H200 전용)
from vllm import LLM

llm = LLM(
    model="meta-llama/Llama-3.1-70B",
    quantization="fp8",
    dtype="auto",
    tensor_parallel_size=2,
    gpu_memory_utilization=0.92,
)

# FP8은 INT4 대비:
# - 거의 무손실 (perplexity 차이 0.1% 미만)
# - 메모리 50% 절감 (FP16 대비)
# - H100 FP8 텐서코어 활용으로 높은 throughput
# - 캘리브레이션 불필요 (동적 양자화)

13. 참고 자료

  1. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers - Frantar et al., 2022
  2. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration - Lin et al., 2023
  3. QLoRA: Efficient Finetuning of Quantized LLMs - Dettmers et al., 2023
  4. llama.cpp GitHub Repository - GGUF 포맷 및 양자화 구현
  5. AutoGPTQ GitHub Repository - GPTQ 자동 양자화 도구
  6. AutoAWQ GitHub Repository - AWQ 자동 양자화 도구
  7. vLLM Quantization Documentation - vLLM 양자화 가이드
  8. bitsandbytes GitHub Repository - NF4 및 INT8 양자화
  9. The Era of 1-bit LLMs (BitNet) - Ma et al., 2024
  10. SmoothQuant: Accurate and Efficient Post-Training Quantization - Xiao et al., 2022