GPTQ
더 많은 작업
- GPTQ
- 2차 정보(헤세 행렬)를 이용해 양자화 오차를 남은 가중치에 보정해 가며 한 열씩 양자화하는, 대형 언어 모델용 학습 후 가중치 양자화 기법
GPTQ는 2022년 10월 오스트리아 과학기술원(IST Austria)과 ETH 취리히 연구진(Elias Frantar, Saleh Ashkboos, Torsten Hoefler, Dan Alistarh)이 발표한 학습 후 양자화(PTQ) 기법이다. 재학습 없이 소량의 보정 데이터만으로 GPT류 모델의 가중치를 3~4비트로 줄인다. 1,750억 매개변수 모델을 A100 GPU 한 장으로 약 4시간에 3~4비트로 양자화하면서 퍼플렉시티 손실을 작게 유지했고, 1,750억 매개변수 모델을 GPU 한 장에서 생성 추론까지 돌린 첫 사례가 되었다. ICLR 2023에 실렸다.[1]
활성값은 FP16 그대로 두고 가중치만 낮은 비트로 저장하는 가중치 전용(weight-only, W4A16 등) 방식이다. 같은 가중치 전용 계열인 AWQ와 함께 vLLM, 허깅 페이스 Transformers 등 주요 추론 도구가 GPTQ 체크포인트를 읽는다. 양자화 일반은 신경망 양자화, 활성값까지 8비트로 줄이는 방식은 SmoothQuant 문서 참고.
GPTQ는 층 하나씩 따로 푼다. 선형 층의 가중치를 (행 , 열 ), 보정 데이터 개를 통과시켜 얻은 그 층의 입력을 라 하면, 양자화 격자 위의 중 출력 오차를 최소로 하는 것을 찾는다.[1]
가장 단순한 반올림(RTN, round-to-nearest)은 가중치마다 가장 가까운 격자점으로 옮길 뿐 입력의 통계를 보지 않는다. GPTQ는 이 목적 함수가 이차식이라는 점을 이용해, 한 가중치를 반올림해서 생긴 오차를 아직 양자화하지 않은 다른 가중치를 조정해 상쇄한다.
GPTQ는 가지치기 기법 OBS(Optimal Brain Surgeon, 1993)를 양자화로 옮긴 OBQ(2022)를 대규모로 확장한 것이다. 목적 함수를 의 행별 오차 합으로 쓰면, 행 하나에서 아직 양자화하지 않은 가중치 집합 의 헤세 행렬은 다. 다음에 양자화할 가중치 와 나머지 가중치의 최적 보정량 는 다음과 같다.[1]
를 확정한 뒤에는 역행렬을 다시 구하지 않고 가우스 소거 한 번으로 번째 행·열을 지운다.
OBQ는 행마다 오차가 가장 작은 가중치부터 탐욕적으로 고르므로 계산량이 이다. ResNet-50(2,500만 매개변수)은 GPU 1장에서 약 1시간이면 되지만 수십억 매개변수 모델에는 쓸 수 없었다.
| 단계 | 내용 | 효과 |
|---|---|---|
| 1. 임의 순서 | 탐욕 순서 대신 모든 행을 같은 열 순서로 양자화한다. 큰 층에서는 순서를 바꿔도 결과 차이가 작았다. 헤세 행렬은 입력 에만 의존하므로 모든 행이 같은 를 공유한다 | 역행렬 갱신이 가중치마다가 아니라 열마다 한 번. 계산량이 로 줄어든다 |
| 2. 지연 일괄 갱신 | 열 의 반올림은 그 열에 대한 갱신에만 영향을 받는다. 그래서 개 열 블록 안에서만 갱신하고, 블록이 끝나면 나머지 열을 행렬 곱 한 번으로 갱신한다 | 메모리 대역폭 병목을 피해 큰 모델에서 한 자릿수 이상 빨라짐 |
| 3. 촐레스키 분해 | 반복 갱신으로 가 수치적으로 깨져 큰 모델의 일부 층이 망가졌다. 필요한 정보는 각 행의 대각 이후 원소뿐이고, 이는 의 촐레스키 분해와 같다. 미리 한 번 계산해 둔다. 대각에 평균 대각값의 1%를 더하는 감쇠(dampening)도 함께 쓴다 | 수천억 매개변수 모델에서도 안정 |
OBQ 대비 계산량이 1,000배 이상 줄었다고 논문은 밝힌다.[1]
논문의 알고리즘 1을 옮기면 다음과 같다. , 블록 크기 .
Q ← 0 (d_row × d_col), E ← 0 (d_row × B)
H⁻¹ ← Cholesky(H⁻¹)ᵀ # 역헤세 정보를 한 번에 계산
for i = 0, B, 2B, ...: # 열 블록 단위
for j = i, ..., i+B-1:
Q[:, j] ← quant(W[:, j]) # 열 하나 양자화
E[:, j-i] ← (W[:, j] - Q[:, j]) / H⁻¹[j, j]
W[:, j:i+B] ← W[:, j:i+B] - E[:, j-i] · H⁻¹[j, j:i+B] # 블록 안 보정
W[:, i+B:] ← W[:, i+B:] - E · H⁻¹[i:i+B, i+B:] # 블록 밖 일괄 보정
층 하나를 양자화하는 파이토치 예시다. 행마다 하나의 비대칭 4비트 격자를 쓰고 그룹화와 열 재정렬은 뺐다. 상관이 있는 무작위 입력(512차원, 2,048개)으로 돌려 보면 같은 격자의 RTN보다 출력 제곱 오차가 절반 이하로 줄어든다(파이토치 2.5.1에서 확인).
import torch
def quantize(w, scale, zero, maxq):
q = torch.clamp(torch.round(w / scale) + zero, 0, maxq)
return scale * (q - zero)
def gptq_layer(W, X, bits=4, blocksize=128, percdamp=0.01):
"""W: [d_out, d_in] 가중치, X: [n, d_in] 보정 입력"""
W = W.clone().float()
d_out, d_in = W.shape
maxq = 2 ** bits - 1
wmin = W.min(1, keepdim=True).values
wmax = W.max(1, keepdim=True).values
scale = (wmax - wmin).clamp(min=1e-8) / maxq
zero = torch.round(-wmin / scale)
H = 2 * X.T @ X # 층 단위 헤세 행렬
H += percdamp * torch.mean(torch.diag(H)) * torch.eye(d_in) # 감쇠
Hinv = torch.linalg.cholesky(
torch.cholesky_inverse(torch.linalg.cholesky(H)), upper=True)
Q = torch.zeros_like(W)
for i1 in range(0, d_in, blocksize):
i2 = min(i1 + blocksize, d_in)
W1, Hinv1 = W[:, i1:i2].clone(), Hinv[i1:i2, i1:i2]
Err1 = torch.zeros_like(W1)
for j in range(i2 - i1):
w, d = W1[:, j], Hinv1[j, j]
q = quantize(w.unsqueeze(1), scale, zero, maxq).flatten()
Q[:, i1 + j] = q
err = (w - q) / d
W1[:, j:] -= err.unsqueeze(1) @ Hinv1[j, j:].unsqueeze(0) # 블록 안 보정
Err1[:, j] = err
W[:, i2:] -= Err1 @ Hinv[i1:i2, i2:] # 블록 밖 일괄 보정
return Q
모델 전체는 트랜스포머 블록을 앞에서부터 하나씩 GPU에 올려, 보정 입력을 통과시켜 층마다 를 누적하고 양자화한 뒤, 양자화된 블록의 출력을 다음 블록의 입력으로 넘기는 식으로 처리한다. 원 논문의 보정 데이터는 C4 데이터셋에서 무작위로 뽑은 2,048토큰 구간 128개였다.[1]
- 그룹 양자화: 가중치를 연속된 개씩 묶어 묶음마다 배율과 영점을 둔다. 그룹 크기 128이면 가중치당 약 0.15비트가 추가되고 정확도가 확실히 좋아진다. GPTQ는 그룹 매개변수를 양자화 도중 갱신된 최신 가중치로 정할 수 있어 그룹화와 잘 맞는다.[1] 실무 체크포인트는 대개 4비트·그룹 128(W4A16-G128)이다.
- 활성값 순서(act-order): 원 논문은 임의 순서도 괜찮다고 했지만, 이후 구현은 헤세 대각 값이 큰(입력 활성값이 큰) 열부터 양자화하는 옵션(
desc_act, act-order)을 두었다. 오차가 큰 열을 먼저 처리해 보정할 여지를 남긴다. 그룹화와 함께 쓰면 추론 때 열 재배열 비용이 생길 수 있다. - 보정 데이터: 배포 환경과 비슷한 데이터를 쓰는 것이 좋다. llm-compressor 예제는 512개 샘플, 길이 2,048에서 시작하고 정확도가 떨어지면 늘리라고 권한다.[2]
| 모델 | FP16 | RTN 4비트 | GPTQ 4비트 | RTN 3비트 | GPTQ 3비트 |
|---|---|---|---|---|---|
| OPT-175B | 8.34 | 10.54 | 8.37 | 7,300 | 8.68 |
| BLOOM-176B | 8.11 | 8.37 | 8.21 | 571 | 8.64 |
| OPT-13B | 10.13 | 11.32 | 10.31 | 3,400 | 11.61 |
값은 WikiText-2 퍼플렉시티(낮을수록 좋음)이다.[1]
- 3비트에서 RTN은 무너지고 GPTQ는 버틴다. 그룹 128을 더한 3비트 GPTQ는 OPT-175B에서 8.45였다.
- 양자화 시간: OPT-175B, BLOOM-176B를 A100 80GB 1장에서 약 4시간.
- 추론: 3비트 OPT-175B는 임베딩·출력 층(FP16 유지)을 포함해 약 63GB라 A100 80GB 1장에 들어간다. FP16은 80GB GPU 5장, LLM.int8()은 3장이 필요했다. 가중치를 필요할 때 풀어 쓰는 전용 커널로 토큰당 지연이 A100에서 FP16(5장) 대비 3.25배, A6000에서 FP16(8장) 대비 4.5배 줄었다.[1]
- 그룹화를 쓰면 약 2.2비트(2비트, 그룹 128)까지 내려가도 퍼플렉시티 증가가 1.5 미만이었다.
| 항목 | GPTQ | AWQ |
|---|---|---|
| 핵심 아이디어 | 헤세 행렬로 오차를 남은 가중치에 보정 | 활성값이 큰 채널의 가중치를 배율로 키워 보호 |
| 계산 | 층마다 누적, 촐레스키, 열 단위 갱신 | 채널별 평균 활성값 크기, 배율 격자 탐색 |
| 보정 데이터 의존 | 상대적으로 크다. 보정·평가 분포가 다르면 손실이 커질 수 있다 | 작다. AWQ 논문은 GPTQ 대비 10분의 1 보정 데이터로 충분하다고 보고[3] |
| 정확도 | 3~4비트에서 RTN보다 크게 좋음 | AWQ 논문의 LLaMA·Llama-2 실험에서는 GPTQ보다 조금 나음 |
| 결합 | 서로 독립이라 AWQ로 배율을 맞춘 뒤 GPTQ를 적용할 수 있다. AWQ 논문은 2비트에서 두 방법을 합쳐 더 좋아졌다고 보고[3] |
고르는 기준: 둘 다 같은 W4A16 형식이라 추론 속도는 커널이 좌우한다. 보정 데이터가 배포 분포와 잘 맞으면 GPTQ, 여러 도메인·멀티모달·지시 튜닝 모델처럼 보정 데이터에 과적합할 위험이 크면 AWQ가 무난하다. 실제로는 대상 모델로 두 가지를 다 만들어 작업 벤치마크로 비교하는 경우가 많다.
- 원 구현: IST-DASLab/gptq(GitHub).
- AutoGPTQ: 허깅 페이스 연동으로 쓰였으나 지금은 유지보수가 끊겨 저장소가 보관 처리되었고(마지막 갱신 2025년 4월), 저장소는 후속 도구로 GPTQModel을 권한다.[4]
- llm-compressor: vLLM 프로젝트의 압축 도구. GPTQ, AWQ, FP8 등을 한 레시피 문법으로 적용하고 vLLM이 바로 읽는 형식으로 저장한다.[2]
- 추론: vLLM, SGLang, 허깅 페이스 Transformers, TGI 등이 GPTQ 체크포인트를 읽는다. CPU·로컬 실행용 GGUF 포맷은 llama.cpp 고유의 양자화 형식이라 별개다.
# llm-compressor 예제 기준(2026-10). 4비트 가중치, 그룹 128
from transformers import AutoModelForCausalLM, AutoTokenizer
from llmcompressor import oneshot
from llmcompressor.modifiers.gptq import GPTQModifier
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_id)
tokenizer = AutoTokenizer.from_pretrained(model_id)
recipe = GPTQModifier(targets="Linear", scheme="W4A16", ignore=["lm_head"])
oneshot(model=model, dataset=ds, recipe=recipe, # ds: 토큰화한 보정 데이터셋
max_seq_length=2048, num_calibration_samples=512)
model.save_pretrained("Meta-Llama-3-8B-Instruct-W4A16-G128", save_compressed=True)
- 가중치 전용이라 메모리와 대역폭은 줄지만 행렬 곱 자체는 FP16으로 한다. 배치가 커서 연산에 묶이는 상황에서는 속도 이득이 줄어든다.
- 보정 데이터가 바뀌면 결과도 바뀐다. 특정 언어나 도메인 데이터로 보정하면 다른 도메인 성능이 더 떨어질 수 있다.
- 2비트 이하로 가면 그룹 크기를 줄여도 손실이 크다. 이 영역은 벡터 양자화 계열 기법이나 양자화 인식 학습이 더 낫다.
- ↑ 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 Elias Frantar, Saleh Ashkboos, Torsten Hoefler, Dan Alistarh, 〈GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers〉, ICLR 2023. arXiv:2210.17323
- ↑ 2.0 2.1 Quantizing to int4 using GPTQ, llm-compressor 예제, GitHub.
- ↑ 3.0 3.1 Ji Lin 외, 〈AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration〉, MLSys 2024. arXiv:2306.00978
- ↑ AutoGPTQ/AutoGPTQ, GitHub.