Chapter 09

양자화

Llama 3 70B를 BF16으로 저장하면 140 GB다. H100 한 장(80 GB)에 들어가지 않고, 들어간다 해도 토큰 하나를 만들 때마다 그 140 GB를 메모리에서 통째로 읽어야 한다. 숫자 하나에 16비트를 쓰던 것을 8비트, 4비트로 줄이면 모델이 작아지고 디코딩이 빨라진다. 문제는 "얼마나 망가지는가"다. 이 장에서는 비트 하나하나를 직접 뒤집어 부동소수점의 구조를 보고, 실수를 정수 격자에 맞추는 스케일과 영점, 스케일을 얼마나 잘게 나눌지(입도), 이상치가 왜 골칫거리인지, 그리고 GPTQ·SmoothQuant 같은 방법이 오차를 어떻게 줄이는지를 실제 계산으로 따라간다.

왜 양자화인가: 토큰당 읽는 바이트

신경망의 파라미터는 결국 숫자 배열이다. 학습은 보통 FP32 마스터 가중치와 BF16 연산으로 하고, 공개되는 체크포인트는 BF16(파라미터당 2바이트)인 경우가 많다. 그러면 모델 크기는 단순한 곱셈이다. 70B(700억) 파라미터 × 2바이트 = 140 GB, 8B 모델(Llama 3 8B, 약 8.03B) × 2바이트 ≈ 16 GB다.

크기가 문제인 이유는 두 가지다. 첫째는 용량이다. 140 GB는 80 GB짜리 H100 한 장에 들어가지 않는다. 둘째, 더 중요한 이유는 속도다. 8장에서 본 것처럼 자기회귀 디코딩은 토큰을 하나씩 만든다. 배치가 1이면 토큰 하나를 만들 때 각 가중치는 곱셈·덧셈 한 번(2 FLOP)에만 쓰이고 버려진다. 즉 바이트당 연산이 고작 1 FLOP 정도다. H100은 초당 약 989조 번(BF16 dense) 계산할 수 있지만 메모리에서는 초당 약 3.35 TB밖에 읽지 못한다. 계산기는 대부분의 시간을 가중치가 도착하기를 기다리며 논다. 이런 상태를 메모리 대역폭 바운드(memory-bandwidth bound)라고 한다(11장의 루프라인에서 자세히 다룬다).

$$t_{\text{token}} \;\gtrsim\; \frac{\text{토큰당 읽는 바이트}}{\text{메모리 대역폭}} \;\approx\; \frac{N_{\text{param}} \times (b/8)}{BW}$$
여기서 \(b\)는 가중치 하나당 비트 수, \(BW\)는 GPU 메모리 대역폭이다. 배치 1, KV 캐시 읽기는 무시한 상한이다.

70B·BF16을 H100 대역폭으로 읽으면 140 GB ÷ 3.35 TB/s ≈ 42 ms, 즉 이론 상한이 초당 약 24 토큰이다. 같은 모델을 4비트로 줄이면 읽을 바이트가 4분의 1이 되어 상한이 약 4배 오른다. 양자화는 계산을 빠르게 하는 기술이기 이전에 읽어야 할 바이트를 줄이는 기술이다.

HBM (80 GB) 층 1 가중치 층 2 가중치 ⋮ 층 L KV 캐시 토큰마다 전부 읽음 ≈3.35 TB/s 좁은 길 연산기 ≈989 TFLOPS 대부분 대기 H100 SXM, 배치 1 디코딩 70B 모델, 토큰당 읽기 시간 (상한) BF16 140 GB · 42 ms · ~24 tok/s FP8 70 GB · 21 ms · ~48 INT4 36 GB · 11 ms · ~93 3비트 27 GB · 8 ms · ~124 막대 길이 = 읽을 바이트 = 토큰당 최소 시간
그림 9-1. 배치 1 디코딩에서는 토큰을 하나 만들 때마다 모든 층의 가중치를 HBM에서 읽어 와야 한다. 연산기는 충분히 빠르므로 토큰당 시간은 '읽을 바이트 ÷ 대역폭'으로 정해진다. INT4는 그룹 스케일 오버헤드를 포함한 약 4.125비트로 계산했다.
정밀도비트/파라미터Llama 3 8BLlama 3 70B쓰임
FP323232.1 GB282 GB학습 마스터 가중치, 옵티마이저 상태
BF16 / FP161616.1 GB141 GB학습 연산, 표준 배포 체크포인트
FP8 / INT88 (+ε)8.0 GB71 GBH100급 서빙(FP8), W8A8
INT4 (g128)≈4.1254.1 GB36 GBGPTQ·AWQ, 소비자 GPU·로컬 추론
3비트급≈3.1~3.5≈3.3 GB≈29 GBGGUF Q3_K 등, 품질 손실 눈에 띔
2비트급≈2.1~2.6≈2.4 GB≈21 GB연구·극한 압축, 큰 품질 손실

표의 값은 가중치만 센 것이다(70.6B 파라미터 기준). 실제 서빙에서는 여기에 KV 캐시와 활성값 작업 공간이 더해진다(11장). 그리고 임베딩·출력층·정규화 파라미터는 보통 더 높은 정밀도로 남겨 두므로, 실제 파일 크기는 표보다 조금 크다.

무엇을 양자화하는가

양자화 대상은 셋이다. 가중치(W)는 한 번 변환해 두면 끝이라 가장 쉽고 이득이 가장 크다(가중치 전용 양자화, 예: W4A16). 활성값(A)까지 낮추면 행렬곱 자체를 INT8/FP8 텐서 코어로 돌려 계산량이 큰 프리필이 빨라진다(W8A8). KV 캐시를 낮추면 긴 컨텍스트와 큰 배치에서 메모리와 대역폭을 아낀다. 표기 "W4A16"은 가중치 4비트, 활성값 16비트라는 뜻이다.

부동소수점의 구조: 범위와 정밀도

컴퓨터가 실수를 저장하는 표준 방법은 부동소수점(floating point)이다. 과학적 표기법 \(6.02 \times 10^{23}\)을 이진수로 옮긴 것이다. 비트열을 세 부분으로 나눈다.

$$x = (-1)^{S} \times 2^{\,E - \text{bias}} \times \left(1 + \frac{M}{2^{m}}\right), \qquad \text{bias} = 2^{e-1} - 1$$
\(S\): 부호 1비트. \(E\): \(e\)비트 지수 필드(부호 없는 정수). \(M\): \(m\)비트 가수 필드. 맨 앞의 1은 저장하지 않는 '숨은 비트'다. \(E = 0\)이면 숨은 비트가 0이 되는 비정규수(subnormal) \(x = (-1)^S \times 2^{1-\text{bias}} \times M/2^m\)로, 0 근처를 메운다.

지수 비트 수 \(e\)는 범위(얼마나 크고 작은 수를 담을 수 있나)를, 가수 비트 수 \(m\)은 정밀도(한 옥타브 \([2^k, 2^{k+1})\) 안에 값이 몇 개 있나)를 정한다. 한 옥타브 안에는 항상 \(2^m\)개의 값이 균일 간격으로 있고, 옥타브가 하나 올라갈 때마다 간격이 두 배가 된다. 그래서 부동소수점의 상대 오차는 크기와 무관하게 대략 \(2^{-m-1}\)로 일정하다. 이것이 정수와의 결정적 차이다.

FP32bias 127 지수 8가수 23 · 최대 ≈3.4×10³⁸ · 유효 ≈7.2자리 FP16bias 15 지수 5가수 10 최대 65504 · 유효 ≈3.3자리 BF16bias 127 지수 8가수 7 FP32와 같은 범위 · 유효 ≈2.4자리 FP8 E4M3bias 7 43 최대 448 · Inf 없음 · 추론·순전파용 FP8 E5M2bias 15 52 최대 57344 · IEEE식 Inf/NaN · 기울기용 FP4 E2M1bias 1 값 ±{0, 0.5, 1, 1.5, 2, 3, 4, 6} · 블록 스케일과 함께 사용 S
그림 9-2. 주요 형식의 비트 배치(한 칸 = 1비트). 분홍 = 부호, 보라 = 지수, 주황 = 가수. BF16은 FP32의 상위 16비트를 그대로 잘라 낸 것이어서 범위는 FP32와 같고 정밀도만 낮다. FP16은 반대로 정밀도는 높지만 범위가 좁아(최대 65504) 학습 중 오버플로를 막으려면 손실 스케일링이 필요했다.

딥러닝이 BF16을 선호하는 이유가 여기 있다. 학습 중의 기울기와 활성값은 \(10^{-6}\)부터 \(10^{4}\)까지 넓게 퍼져 있어 범위가 정밀도보다 중요하다. 신경망은 개별 값의 작은 잡음에 강하지만, 오버플로로 생긴 Inf 하나는 학습 전체를 망친다. FP8은 한 걸음 더 나가서 용도별로 두 형식을 쓴다. 순전파의 가중치·활성값에는 정밀도가 조금 나은 E4M3를, 범위가 넓어야 하는 역전파의 기울기에는 E5M2를 쓴다. OCP(Open Compute Project) FP8 규격의 E4M3는 Inf를 없애고 그 비트 패턴을 일반 값으로 돌려써서 최대값을 240이 아닌 448까지 늘렸다(NaN은 S.1111.111 하나만 남긴다).

SIMULATOR

비트 편집기: 부동소수점 해부

—
형식
숫자 입력 → 가장 가까운 표현값
빠른 값
아래 숫자선 범위
디코딩 값—
종류—
이 위치의 간격 (ULP)—
입력 대비 오차—
비트 칸을 누르면 0/1이 바뀌고 값이 바로 다시 디코딩된다. 위 그래프는 형식별로 표현 가능한 양수 범위(log₂ 축, 옅은 부분 = 비정규수)와 현재 값의 위치, 아래 숫자선은 현재 형식의 표현 가능한 값을 모두 찍은 것이다(눌러서 끌면 그 위치의 가장 가까운 값으로 이동). 해볼 것: ① BF16과 FP16에서 70000을 변환해 보자. FP16은 Inf가 되고 BF16은 70144가 된다(범위 vs 정밀도). ② 0.1을 FP32·BF16·E4M3로 바꿔 상대 오차가 가수 비트에 따라 어떻게 커지는지 보자. ③ E4M3에서 지수를 1111, 가수를 111로 만들면 NaN, 가수 110이면 448이다. E5M2에서 지수 11111은 Inf/NaN이다. ④ 숫자선을 0~8로 두고 FP4와 E4M3를 비교하면 1을 넘을 때마다 간격이 두 배가 되는 것이 보인다.
형식지수/가수최대값최소 정규최소 비정규1 근처 간격
FP328 / 233.40×10³⁸1.18×10⁻³⁸1.4×10⁻⁴⁵2⁻²³ ≈ 1.2×10⁻⁷
FP165 / 10655046.1×10⁻⁵6.0×10⁻⁸2⁻¹⁰ ≈ 9.8×10⁻⁴
BF168 / 73.39×10³⁸1.18×10⁻³⁸9.2×10⁻⁴¹2⁻⁷ ≈ 7.8×10⁻³
FP8 E4M3 (OCP)4 / 34482⁻⁶ ≈ 0.01562⁻⁹ ≈ 0.001950.125
FP8 E5M25 / 2573442⁻¹⁴ ≈ 6.1×10⁻⁵2⁻¹⁶ ≈ 1.5×10⁻⁵0.25
FP4 E2M12 / 1610.50.5

FP8의 최대값 448은 작아 보이지만, 실제로는 텐서마다(또는 블록마다) 스케일을 곱해 값을 이 범위 안으로 옮긴 뒤 저장한다. 즉 FP8 양자화도 다음 절의 정수 양자화와 똑같이 "스케일 + 격자"라는 구조를 갖는다. 다른 점은 격자 간격이 균일하지 않고 0 근처가 촘촘하다는 것뿐이다.

정수 양자화: 스케일과 영점

가장 널리 쓰이는 양자화는 실수 \(x\)를 \(b\)비트 정수 \(q\)로 바꾸는 균일 양자화(uniform / affine quantization)다. 실수 축 위에 간격 \(s\)짜리 격자를 깔고, 각 값을 가장 가까운 격자점으로 보낸다.

$$q = \operatorname{clamp}\!\left(\operatorname{round}\!\left(\frac{x}{s}\right) + z,\; q_{\min},\; q_{\max}\right), \qquad \hat{x} = s\,(q - z)$$
\(s\): 스케일(격자 간격, 실수). \(z\): 영점(zero point, 정수) — 실수 0이 대응하는 정수. \(\hat x\): 역양자화(dequantize)한 근사값. 저장하는 것은 \(q\)(b비트)와 그룹마다 하나씩인 \(s, z\)다.

두 가지 흔한 방식이 있다.

대칭 (z = 0) −α0+α −7−5−3−101357 xq 비대칭 (z ≠ 0) β = −10α = 6 0z≈2⋯15 격자 낭비 없음
그림 9-3. 4비트 예. 위: 대칭 양자화는 \([-\alpha, \alpha]\)를 \(-7 \ldots 7\)에 대응시킨다(간략히 하려고 홀수 정수만 눈금을 그렸다). 분포가 한쪽으로 치우쳐 있으면 반대쪽 격자가 놀게 된다. 아래: 비대칭은 실제 범위 \([\beta, \alpha]\)에 16개 격자를 모두 깔고, 실수 0은 정수 영점 \(z\)로 간다.

반올림 오차와 클리핑 오차

범위 \([\beta,\alpha]\)를 어떻게 잡을지가 핵심 설계 문제다. 가장 단순한 선택은 데이터의 최소·최대값(absmax)을 쓰는 것이다. 그런데 분포에 이상치가 하나라도 있으면 범위가 넓어지고, 격자 간격 \(s\)가 커져 대부분의 '평범한' 값들이 거친 격자에 놓인다. 반대로 범위를 좁히면(클리핑) 격자가 촘촘해지지만 범위를 벗어난 값은 경계로 잘려 큰 오차를 낸다. 격자 안쪽 값의 반올림 오차는 균일 분포라 가정하면 평균 제곱이 \(s^2/12\)다.

$$\text{MSE}(\alpha) \;\approx\; \underbrace{\frac{s(\alpha)^2}{12}\,P(|x|\le\alpha)}_{\text{반올림 }(\alpha\uparrow\ \Rightarrow\ \uparrow)} \;+\; \underbrace{\mathbb{E}\big[(|x|-\alpha)^2;\,|x|>\alpha\big]}_{\text{클리핑 }(\alpha\uparrow\ \Rightarrow\ \downarrow)}, \qquad \text{SQNR} = 10\log_{10}\frac{\mathbb{E}[x^2]}{\text{MSE}}\ \text{dB}$$
대칭 양자화에서 \(s(\alpha) = \alpha / (2^{b-1}-1)\). 두 항이 반대로 움직이므로 MSE를 최소로 하는 최적 \(\alpha^*\)가 존재한다. 비트가 1개 늘 때마다 \(s\)가 절반이 되어 반올림 SQNR이 약 6.02 dB씩 오른다.
SIMULATOR

양자화 숫자선: 클리핑 범위를 끌어 보자

방식
분포
클리핑 범위 [β, α]—
스케일 s—
MSE (반올림 + 클리핑)—
SQNR—
잘린 값 비율—
위: 4000개 샘플(시드 고정)의 히스토그램과 양자화 격자(세로선), 분홍 막대는 양자화 후 각 격자점에 모인 샘플 수다. 양 끝의 둥근 핸들을 끌어 클리핑 범위를 바꾼다(대칭 모드에서는 두 핸들이 함께 움직인다). 아래: 클리핑 배율(α / max|x|)에 따른 MSE를 반올림·클리핑 성분으로 나눠 그린 곡선 — 눌러서 그 배율로 이동한다. 해볼 것: ① 4비트·이상치 켜기에서 absmax와 최적 클리핑의 SQNR을 비교하자. 이상치 몇 개 때문에 수 dB 이상을 잃는다. ② 비트를 8로 올리면 최적점이 오른쪽(덜 자르기)으로 이동한다 — 격자가 촘촘하면 클리핑이 상대적으로 비싸다. ③ 라플라스 분포는 꼬리가 두꺼워 같은 비트에서 SQNR이 낮다. ④ 'GELU 뒤' 분포에서 대칭 ↔ 비대칭을 바꾸면 비대칭이 약 6 dB(1비트만큼) 이득이다.

실무에서 쓰는 범위 결정법(보정(calibration))은 absmax, 백분위수(예: 99.99%), MSE 최소화, KL 발산 최소화(TensorRT의 엔트로피 보정) 등이 있다. 가중치는 값을 모두 알고 있으니 MSE 최적 클리핑을 직접 찾을 수 있지만, 활성값은 입력마다 달라지므로 보정 데이터 수백 개를 흘려 통계를 모으거나(정적 양자화), 실행 중에 토큰마다 범위를 계산한다(동적 양자화).

입도: 스케일을 얼마나 잘게 나눌까

스케일 \(s\)를 텐서 전체에 하나만 둘 수도, 더 잘게 나눠 여러 개 둘 수도 있다. 이 단위를 입도(granularity)라고 한다. 선형층 \(y = Wx\)의 가중치 \(W \in \mathbb{R}^{d_{out}\times d_{in}}\)을 예로 들자.

per-tensor 스케일 1개 per-channel (행마다) 스케일 d_out개 per-group (g = 4) 스케일 d_out × d_in / g개 가운데 그룹 = 이상치 입력 채널 ← 입력 차원 d_in (행 = 출력 채널) 정확도 ↑ · 스케일 저장·커널 복잡도 ↑ →
그림 9-4. 같은 6×12 가중치 행렬을 세 가지 입도로 양자화할 때 스케일을 공유하는 영역(같은 색 = 같은 스케일). 그룹 양자화는 큰 값을 가진 입력 채널(가운데 열 묶음)의 영향을 그 그룹 안에 가둔다.

그룹을 잘게 나눌수록 정확해지지만 공짜는 아니다. 그룹마다 FP16 스케일(16비트)을 저장하면 가중치 하나당 \(16/g\)비트가 추가된다. 비대칭이면 영점도 저장해야 한다.

$$b_{\text{eff}} = b + \frac{b_s + b_z}{g} \qquad \text{예: INT4, } g = 128, \text{ FP16 스케일만} \;\Rightarrow\; 4 + \tfrac{16}{128} = 4.125 \text{ 비트/가중치}$$
\(b_s\): 스케일 비트(FP16이면 16), \(b_z\): 영점 비트(대칭이면 0, 비대칭이면 보통 4~16). \(g = 32\)이면 4.5비트, \(g = 64\)이면 4.25비트가 된다.
SIMULATOR

가중치 행렬 양자화: 입도와 그룹 크기

입도
그룹 크기 g
비트 수
가중치 상대 오차 ‖W−Ŵ‖/‖W‖—
출력 상대 오차 ‖Wx−Ŵx‖/‖Wx‖—
SQNR—
유효 비트/가중치—
70B 모델로 환산—
시드 고정 난수로 만든 64×256 가중치(출력 64 × 입력 256)다. 실제 LLM처럼 몇몇 입력 열(채널)과 출력 행이 유난히 크고, 드문드문 큰 값이 박혀 있다. 위부터 원본 W, 양자화 후 Ŵ(같은 색 척도), 오차 W−Ŵ(오차 최대값 기준으로 확대). 원본 맵을 누르거나 문지르면 그 자리에 큰 이상치를 심는다. 출력 오차는 무작위 입력 벡터 16개에 대한 평균이다. 해볼 것: ① 4비트에서 텐서 → 채널 → 그룹(128 → 32)으로 바꾸며 출력 오차가 몇 배 줄어드는지 보자. ② 텐서 입도에서 원본 맵 아무 곳에나 이상치 하나를 심으면 전체 오차 맵이 밝아진다. 그룹 입도에서는 그 그룹만 밝아진다. ③ 3비트·g32 비대칭과 4비트·g128 대칭의 유효 비트와 오차를 비교하자 — 비트를 그룹 스케일에 쓰는 것이 이득일 때가 있다.

실제 4비트 LLM 양자화(GPTQ, AWQ, bitsandbytes NF4, GGUF)는 거의 모두 그룹 크기 32~128을 쓴다. Llama 3 70B의 FFN 행렬은 \(28672 \times 8192\)이므로 g = 128이면 행마다 64개, 행렬 하나에 약 180만 개의 스케일이 생긴다. 많아 보여도 가중치(약 2.35억 개)의 0.8% 수준이다.

활성값 이상치와 SmoothQuant

가중치는 비교적 얌전한 분포를 갖는다. 문제는 활성값이다. 2022년 LLM.int8() 논문(Dettmers 외)은 모델이 약 6.7B 파라미터를 넘으면 은닉 상태의 몇 개 특정 채널에서 다른 채널보다 수십~100배 큰 값이 거의 모든 토큰에 걸쳐 나타난다고 보고했다. 차원은 수천 개인데 이상치 채널은 손에 꼽을 정도로 적지만, 이 채널을 0으로 만들면 모델 성능이 무너질 만큼 중요하다.

활성값 \(X \in \mathbb{R}^{T\times C}\)(토큰 × 채널)를 INT8로 양자화하는 효율적인 방법은 per-tensor 또는 per-token(행마다) 스케일이다. 행렬곱 \(XW\)에서 스케일을 밖으로 빼려면 \(X\)는 행(토큰) 방향, \(W\)는 열(출력) 방향으로만 스케일을 나눌 수 있다. 그런데 이상치는 채널(열) 방향으로 몰려 있다. 결국 이상치 채널이 스케일을 결정해 버리고, 나머지 채널은 몇 개의 격자점만 쓰게 된다. LLM.int8()은 이상치 채널을 따로 떼어 FP16으로 계산하는 혼합 정밀도 분해로 이 문제를 풀었지만, 커널이 복잡하고 느렸다.

SmoothQuant(Xiao 외, 2022)는 더 간단한 수학적 트릭을 쓴다. 행렬곱은 채널별 스케일을 한쪽에서 나누고 다른 쪽에서 곱해도 결과가 변하지 않는다.

$$Y = XW = \big(X\,\operatorname{diag}(\mathbf{s})^{-1}\big)\big(\operatorname{diag}(\mathbf{s})\,W\big) = \hat X \hat W, \qquad s_j = \frac{\max_t |X_{tj}|^{\alpha}}{\max_k |W_{jk}|^{1-\alpha}}$$
\(j\): 입력 채널. \(\alpha \in [0,1]\): 양자화 난이도를 활성값에서 가중치로 얼마나 옮길지 정하는 '이동 강도'. \(\alpha = 0.5\)이면 \(\max|\hat X_j| = \max|\hat W_j| = \sqrt{\max|X_j|\max|W_j|}\)로 두 쪽의 채널 최대값이 같아진다. \(\operatorname{diag}(\mathbf s)^{-1}\)은 앞 LayerNorm의 가중치에 미리 흡수할 수 있어 실행 비용이 0이다.
활성값 X: 채널별 max|·| 이상치 채널이 스케일을 독점 가중치 W: 입력 채널별 max|·| 가중치는 고르다 (양자화 쉬움) 스무딩 후 (α = 0.5) X/s (보라) · s·W (주황) 높이가 같아짐 ÷ s_j ↔ × s_j 곱 XW는 그대로 · 양자화 난이도만 두 쪽에 나눠 가짐
그림 9-5. SmoothQuant의 난이도 이동. 활성값의 이상치 채널을 채널별 스케일 \(s_j\)로 나누고, 같은 \(s_j\)를 가중치의 해당 입력 채널(행)에 곱한다. 수학적으로 등가인 변환이지만, 두 행렬이 모두 '양자화하기 쉬운' 모양이 된다.
SIMULATOR

SmoothQuant: 이동 강도 α

양자화 비트 (W·X 모두)
X 양자화 오차—
W 양자화 오차—
출력 오차 ‖Y−Ŷ‖/‖Y‖—
스무딩 없을 때—
최적 α—
교육용으로 만든 작은 예시: 토큰 64개 × 채널 32개 활성값 X(채널 5·19·27이 이상치), 32×32 가중치 W. X와 W를 각각 per-tensor 대칭 양자화한 뒤 곱해 원래 Y = XW와 비교한다. 위: 채널별 max|X̂j|(보라)와 max|Ŵj|(주황)를 각 텐서의 최대값으로 정규화해 그린 막대, 옅은 막대는 스무딩 전. 아래: α에 따른 세 오차(로그 축). 아래 그래프를 누르거나 끌어 α를 바꿀 수 있다. 해볼 것: ① α = 0(활성값이 난이도를 모두 떠안음)에서 1(가중치가 모두 떠안음)로 끌며 X 오차는 줄고 W 오차는 느는 것을 보자. 출력 오차는 그 사이 어딘가에서 최소다. ② 이상치 크기를 1로 줄이면 스무딩 이득이 거의 사라진다. ③ INT4로 바꾸면 최적 α에서도 오차가 커서, W4A4가 왜 훨씬 어려운지 보인다.

SmoothQuant 논문은 대부분의 모델에서 \(\alpha \approx 0.5\)가, 이상치가 심한 일부 모델(GLM-130B 등)에서는 0.75 정도가 좋다고 보고했다. 이 방법으로 OPT-175B 같은 대형 모델도 W8A8로 정확도를 거의 잃지 않고 돌릴 수 있게 됐다. 이후 연구는 채널별 스케일 대신 직교 회전 행렬 \(R\)(\(XW = (XR)(R^\top W)\))을 곱해 이상치를 모든 채널에 골고루 퍼뜨리는 방향(QuaRot, SpinQuant 등, 2024)으로 발전했고, 이로써 W4A4에 가까운 설정도 시도되고 있다.

PTQ와 QAT, 그리고 GPTQ

양자화를 언제 하느냐에 따라 두 갈래가 있다.

가장 단순한 PTQ는 각 가중치를 가장 가까운 격자점으로 보내는 RTN(round-to-nearest)이다. RTN은 가중치 하나하나의 오차 \(|w - \hat w|\)를 최소화하지만, 우리가 진짜 신경 쓰는 것은 층의 출력 \(WX\)다. 여러 가중치의 오차가 서로 상쇄되도록 고르면 개별 오차가 조금 커지더라도 출력 오차는 작아질 수 있다.

GPTQ: 한 열씩 양자화하고, 오차를 남은 열에 떠넘긴다

GPTQ(Frantar 외, 2022)는 층별 재구성 문제를 푼다. 보정 입력 \(X \in \mathbb{R}^{d_{in}\times N}\)에 대해 각 행 \(\mathbf w\)(출력 채널 하나)의 출력 오차는 이차식이다.

$$\min_{\hat{\mathbf w}}\ \|\mathbf w X - \hat{\mathbf w} X\|_2^2 = (\mathbf w - \hat{\mathbf w})\, H\, (\mathbf w - \hat{\mathbf w})^\top, \qquad H = 2XX^\top$$
\(H\)는 이 이차 손실의 헤시안(\(d_{in}\times d_{in}\))이다. 입력 채널끼리 상관이 있으면 \(H\)가 대각이 아니고, 한 채널의 오차를 다른 채널 가중치를 조정해 보상할 수 있다.

알고리즘은 OBS(Optimal Brain Surgeon)에서 왔다. 입력 채널(열) \(q\)를 하나 골라 양자화하면 생긴 오차 \(\delta_q = w_q - \hat w_q\)를, 아직 양자화하지 않은 나머지 가중치를 다음처럼 움직여 최적으로 보상한다.

$$\mathbf w_{F} \leftarrow \mathbf w_{F} - \frac{\delta_q}{[H_F^{-1}]_{qq}}\,[H_F^{-1}]_{q,F}, \qquad H_{F\setminus q}^{-1} = H_F^{-1} - \frac{[H_F^{-1}]_{:,q}[H_F^{-1}]_{q,:}}{[H_F^{-1}]_{qq}}$$
\(F\): 아직 양자화하지 않은 열의 집합. GPTQ의 핵심 관찰은 (1) 모든 행이 같은 열 순서를 쓰면 \(H^{-1}\) 갱신을 행끼리 공유할 수 있고, (2) 이 갱신이 촐레스키 분해 한 번으로 미리 계산된다는 것이다. 덕분에 175B 모델도 GPU 한 장으로 몇 시간 안에 양자화한다.
가중치 W (행 = 출력 채널, 열 = 입력 채널) 양자화 완료 (고정) 열 q 아직 실수 — 보정으로 계속 갱신 ① 열 q를 반올림 ŵ_q = Q(w_q) ② 오차 δ를 분배 w_F −= δ·H⁻¹_qF / H⁻¹_qq ③ H⁻¹에서 q 제거 다음 열로 →
그림 9-6. GPTQ는 왼쪽 열부터 차례로 양자화한다. 열 \(q\)를 반올림해 생긴 오차를, 입력 상관(헤시안 역행렬)에 비례해 오른쪽의 아직 양자화되지 않은 열에 미리 반영해 둔다. 마지막 열에 가까워질수록 보정할 여지가 줄어든다.
SIMULATOR

RTN vs GPTQ: 오차 보정의 효과

보정 입력 X
GPTQ 진행
RTN 출력 MSE—
GPTQ 출력 MSE—
개선 (RTN / GPTQ)—
진행—
교육용으로 만든 작은 층: W는 16×32(출력 16, 입력 32), 보정 입력 X는 32차원 × 256개 샘플이다. '상관 강함'은 X를 8차원 잠재 변수에서 만들어 채널끼리 강하게 얽혀 있고, '독립'은 채널마다 따로 뽑는다. 두 방법 모두 행마다 같은 대칭 격자(행 absmax 스케일)를 쓰고, GPTQ는 위 식을 그대로(헤시안 역행렬 갱신, 1% 감쇠) 계산한다. 위: 원본 W / 진행 중인 W(왼쪽부터 양자화된 열, 오른쪽은 보정으로 바뀐 실수 열) / 원본과의 차이. 아래: 비트별 출력 MSE(로그 축), 막대를 누르면 그 비트로 바뀐다. 해볼 것: ① '한 열 →'을 여러 번 눌러 오른쪽 열들이 미리 조금씩 바뀌는 것을 보자. ② 3비트에서 RTN 대비 GPTQ의 MSE가 몇 배 줄어드는지 확인하자. ③ '채널 독립'으로 바꾸면 H가 거의 대각이 되어 보정할 상대가 없고, GPTQ ≈ RTN이 된다.
AWQ: 중요한 채널을 스케일로 보호한다

AWQ(Activation-aware Weight Quantization, Lin 외, 2023)는 "가중치의 중요도는 그 가중치에 곱해지는 활성값의 크기가 정한다"는 관찰에서 출발한다. 활성값이 큰 입력 채널의 가중치 오차는 출력에 크게 증폭되므로, 그런 약 1%의 채널만 보호해도 품질이 크게 좋아진다. 그렇다고 그 채널만 FP16으로 두면 커널이 복잡해지므로, SmoothQuant와 같은 등가 변환을 쓴다. 중요 채널의 가중치에 \(s_j > 1\)을 곱해 키우고(상대 반올림 오차 감소) 활성값을 \(s_j\)로 나눈다. \(s_j = (\overline{|x_j|})^{\alpha}\) 꼴로 두고 보정 데이터에서 출력 오차가 최소가 되는 \(\alpha\)를 격자 탐색한다. 역전파도 헤시안도 필요 없어 빠르고, 보정 데이터에 덜 과적합한다는 장점이 있다. GPTQ와 함께 4비트 가중치 양자화의 양대 표준이다.

그 밖의 형식과 기법

KV 캐시 양자화

8장의 KV 캐시는 컨텍스트 길이와 배치에 비례해 자란다. Llama 3 70B(80층, KV 헤드 8개, 헤드 차원 128)는 BF16에서 토큰당 \(2 \times 80 \times 8 \times 128 \times 2\) 바이트 ≈ 320 KB, 128K 토큰이면 요청 하나에 약 43 GB다. 긴 컨텍스트에서는 가중치보다 KV 캐시가 더 커진다. KV를 FP8이나 INT8(토큰별·헤드별 스케일)로 저장하면 절반이 되고, 서빙 엔진(vLLM, TensorRT-LLM 등)은 FP8 KV 캐시를 옵션으로 지원한다. 키 벡터에는 특정 채널의 이상치가, 값 벡터에는 토큰별 변동이 커서, 키는 채널별·값은 토큰별 스케일을 쓰는 방식(KIVI 등)도 제안됐다.

NF4와 QLoRA

신경망 가중치는 대략 0을 중심으로 한 정규분포다. 균일 격자는 값이 드문 꼬리에도 격자점을 똑같이 배분하므로 낭비가 있다. NF4(NormalFloat4, QLoRA 논문, Dettmers 외 2023)는 표준정규분포의 분위수(quantile)에 격자점 16개를 놓아 각 격자 구간에 같은 확률 질량이 들어가게 한다. 블록(64개)마다 absmax로 정규화한 뒤 이 표에서 가장 가까운 값을 고른다. QLoRA는 기반 모델을 NF4로 얼려 두고(스케일까지 다시 8비트로 양자화하는 이중 양자화 포함), 그 위에 작은 LoRA 어댑터만 BF16으로 학습한다. 덕분에 65B 모델 미세조정이 48 GB GPU 한 장으로 가능해졌다.

가중치 분포 (블록 absmax로 정규화) NF4 INT4 −10+1
그림 9-7. NF4(분홍)는 정규분포의 분위수에 격자점을 둬서 값이 많은 0 근처가 촘촘하고 꼬리가 성기다. 대칭 INT4(주황, −7…7)는 균일 간격이다. NF4는 0을 정확히 포함하도록 양수 쪽 8개·음수 쪽 7개로 비대칭하게 설계됐다.

GGUF와 k-quant

llama.cpp의 파일 형식 GGUF는 CPU·Apple 실리콘·소비자 GPU에서 LLM을 돌리는 데 널리 쓰인다. 그 안의 k-quant(Q2_K ~ Q6_K)는 2단계 블록 구조를 쓴다. 대략 말하면 256개 가중치로 된 '슈퍼블록'을 16~32개짜리 하위 블록으로 나누고, 하위 블록의 스케일(과 최소값)을 다시 6비트 정도로 양자화한 뒤 슈퍼블록마다 FP16 스케일 하나만 둔다. 스케일 비용을 줄이면서 작은 블록의 이점을 얻는 방법이다. 이름의 숫자는 대략적 비트 수이고, 실제 유효 비트는 Q4_K_M ≈ 4.8, Q5_K_M ≈ 5.7, Q3_K_M ≈ 3.9비트 정도다(_S/_M/_L은 층마다 정밀도를 다르게 섞는 정도). 정확한 블록 구성은 버전에 따라 바뀌므로 여기서는 개념만 기억하자.

FP8 학습과 MX 블록 포맷

H100(Hopper)부터 텐서 코어가 FP8을 지원해 BF16 대비 약 2배 연산 처리량을 낸다. DeepSeek-V3(2024년 12월 보고)는 대부분의 행렬곱을 FP8로 학습했는데, 텐서 하나에 스케일 하나가 아니라 활성값은 1×128 타일, 가중치는 128×128 블록마다 스케일을 둬서 이상치 문제를 피했다. 이 '작은 블록마다 스케일'이라는 아이디어를 하드웨어 표준으로 만든 것이 OCP MX(Microscaling) 포맷이다. 32개 원소가 8비트 지수 스케일(E8M0, 2의 거듭제곱) 하나를 공유하고, 원소는 FP8·FP6·FP4(MXFP8/6/4) 또는 INT8로 저장한다. MXFP4는 원소 4비트 + 8/32 = 4.25비트다. NVIDIA Blackwell(B200, 2024~25)은 MXFP4와 자체 NVFP4(16개마다 FP8 스케일)를 텐서 코어에서 직접 지원한다. 블록 스케일이 하드웨어 안으로 들어오면서 '양자화된 형식으로 그대로 계산하기'가 표준이 되어 가고 있다.

방법대상핵심 아이디어대표 설정
RTNW가장 가까운 격자점W8, W4 g128
LLM.int8()W·A이상치 채널만 FP16으로 분리W8A8 + FP16 혼합
SmoothQuantW·A채널 스케일로 난이도 이동W8A8
GPTQW헤시안 기반 순차 오차 보정W4/W3 g128
AWQW활성값 기준 중요 채널 스케일 보호W4 g128
NF4 / QLoRAW정규분포 분위수 격자 + LoRA 학습W4 블록 64
GGUF k-quantW2단계 블록 스케일Q4_K_M ≈ 4.8비트
FP8 (E4M3)W·A·KV텐서/블록 스케일 + FP8 텐서 코어W8A8 FP8
MXFP4 / NVFP4W·A하드웨어 블록 스케일(32/16개)W4A4 (Blackwell)
양자화 말고도: 프루닝과 증류

프루닝(pruning)은 가중치의 일부를 0으로 만들어 없앤다. 아무 위치나 0으로 만드는 비정형 희소성은 하드웨어 이득을 얻기 어렵고, NVIDIA 텐서 코어는 4개 중 2개가 0인 2:4 희소성을 지원해 이론상 2배 처리량을 낸다(SparseGPT, Wanda 등). 지식 증류(distillation)는 큰 '교사' 모델의 출력 분포를 작은 '학생' 모델이 따라 배우게 한다. Llama 3.2 1B/3B처럼 큰 모델의 로짓을 이용해 작은 모델을 만드는 것이 대표적이다. 세 기법은 서로 배타적이지 않아서, 증류한 작은 모델을 다시 4비트로 양자화해 휴대폰에서 돌리는 식으로 함께 쓴다.

크기와 속도 계산기

지금까지의 내용을 하나의 계산으로 묶어 보자. 배치 1 디코딩의 토큰당 시간 하한은 "가중치 바이트 ÷ 대역폭"이다. 그룹 양자화의 스케일 오버헤드까지 넣으면 다음과 같다.

$$M_W = N \cdot \frac{b + 16/g}{8}\ \text{바이트}, \qquad \text{tok/s}_{\max} \approx \frac{BW}{M_W}$$
\(N\): 파라미터 수, \(b\): 가중치 비트, \(g\): 그룹 크기(16비트 스케일 가정, 16비트 원본에는 오버헤드 없음), \(BW\): 메모리 대역폭. KV 캐시 읽기, 커널 효율(실제로는 대역폭의 약 60~85%)은 무시한 이론 상한이다. MoE 모델은 토큰마다 일부 전문가만 읽으므로 \(M_W\) 대신 활성 파라미터 바이트를 쓴다.
SIMULATOR

모델 크기 · 디코딩 속도 계산기

프리셋
가중치 비트
그룹 크기 (스케일 FP16)
유효 비트/가중치—
가중치 메모리—
GPU 한 장에—
디코딩 상한 (배치 1)—
필요한 GPU 수 (최소)—
막대는 비트 수별 가중치 메모리(GB, 10진), 점선은 GPU 메모리에서 여유분을 뺀 '가중치에 쓸 수 있는 용량', 막대 위 숫자는 tok/s 상한이다. 막대를 누르면 그 비트를 선택한다. GPU 사양은 공개 자료 기준 근사값이며 B200은 2024~25년 발표치다. M2 Ultra는 통합 메모리라 GPU가 쓸 수 있는 양이 전체보다 적다(여기서는 75%로 가정). 해볼 것: ① 70B를 H100 한 장에 올리려면 몇 비트가 필요한가? ② 같은 70B 4비트를 RTX 4090과 M2 Ultra에서 비교하자 — 4090은 용량이 모자라고, M2 Ultra는 들어가지만 대역폭이 낮다. ③ 405B는 4비트(≈209 GB)로도 B200 한 장(~180 GB)을 넘는다. 3비트까지 내려야 겨우 들어간다. ④ 8B·4비트를 H100에서 돌리면 상한이 수백 tok/s지만, 실제로는 커널 오버헤드 때문에 이 상한에 한참 못 미친다.

이 계산은 서빙의 출발점일 뿐이다. 동시에 여러 사용자를 처리하면(배치 > 1) 같은 가중치 읽기를 여러 토큰이 나눠 쓰므로 총 처리량이 크게 오르고, 대신 KV 캐시가 메모리를 차지한다. 이 트레이드오프는 11장의 서빙 플레이그라운드에서 이어서 다룬다.

품질에 미치는 영향

양자화가 모델을 얼마나 망가뜨리는지는 모델 크기·구조·방법·평가 과제에 따라 크게 달라서 하나의 숫자로 말하기 어렵다. 다만 지난 몇 년의 보고들에서 반복되는 대략적인 경향은 있다.

품질 손실 (개념적) 2346816 가중치 비트 수 (로그에 가까운 눈금) 급격한 저하 소폭 손실 거의 무손실 ― 단순 RTN - - GPTQ·AWQ 등 보정
그림 9-8. 가중치 비트 수와 품질 손실의 대략적 관계(개념도, 실측값 아님). 8비트는 거의 손실이 없고, 4비트는 좋은 방법을 쓰면 소폭이며, 3비트 이하에서는 급격히 나빠진다. 보정 기법은 곡선을 왼쪽으로 민다.
숫자를 읽을 때 주의

퍼플렉시티 비교는 같은 평가 데이터·같은 컨텍스트 길이·같은 토크나이저에서만 의미가 있다. 퍼플렉시티 차이가 작아도 특정 과제(긴 추론, 도구 호출 형식 지키기)에서는 차이가 날 수 있으므로, 실제 배포 전에는 그 용도의 평가 세트로 직접 확인해야 한다. 이 장의 수치들은 모두 "대략, 모델·방법에 따라 다름"으로 읽자.

핵심 정리

  1. 배치 1 디코딩은 메모리 대역폭 바운드다. 토큰당 시간 ≈ 가중치 바이트 ÷ 대역폭이므로 비트를 절반으로 줄이면 속도 상한이 두 배가 된다. 70B·BF16 = 140 GB, H100에서 이론 상한 ~24 tok/s.
  2. 부동소수점은 지수 비트로 범위를, 가수 비트로 정밀도를 산다. BF16은 FP32의 범위에 7비트 가수, FP8 E4M3는 최대 448(OCP, Inf 없음), E5M2는 범위가 넓어 기울기용이다.
  3. 정수 양자화 \(q = \operatorname{round}(x/s) + z\). 범위를 넓게 잡으면 반올림 오차가, 좁게 잡으면 클리핑 오차가 커지므로 MSE 최적 클리핑이 존재한다. 비트 1개 ≈ 6 dB SQNR.
  4. 입도를 텐서 → 채널 → 그룹으로 잘게 나눌수록 이상치가 국소화되어 정확해지고, 스케일 비용이 \(16/g\)비트씩 든다. INT4 g128 ≈ 4.125비트.
  5. LLM 활성값에는 소수 채널의 큰 이상치가 있다. SmoothQuant는 \(XW = (X\,\mathrm{diag}(s)^{-1})(\mathrm{diag}(s)W)\)로 난이도를 가중치로 옮겨 W8A8을 가능하게 했다.
  6. GPTQ는 헤시안 \(H = 2XX^\top\)을 써서 한 열의 반올림 오차를 남은 열로 보정하고, AWQ는 활성값이 큰 중요 채널을 스케일로 보호한다. 둘 다 재학습 없는 PTQ다.
  7. NF4(정규 분위수 격자), GGUF k-quant(2단계 블록 스케일), MX/NVFP4(하드웨어 블록 스케일), FP8 KV 캐시 등 '작은 블록마다 스케일'이 현재 저비트 형식의 공통 원리다.

확인 퀴즈

1. 배치 1로 디코딩할 때 70B 모델을 BF16에서 INT4(g128)로 바꾸면 토큰 생성 속도의 이론 상한은 대략 어떻게 되는가?

배치 1 디코딩은 가중치를 읽는 시간이 지배한다. 16 / 4.125 ≈ 3.9배 적은 바이트를 읽으므로 상한도 그만큼 오른다. 역양자화 계산은 남는 연산 능력으로 감출 수 있어서(가중치 전용 양자화 커널) 대개 병목이 아니다.

2. BF16이 FP16보다 학습에 선호되는 주된 이유는?

BF16은 FP32의 상위 16비트(부호 1 + 지수 8 + 가수 7)다. 정밀도는 FP16(가수 10)보다 낮지만 범위가 FP32와 같아 손실 스케일링 없이도 학습이 안정적이다. 신경망은 개별 값의 작은 잡음보다 범위 초과에 훨씬 취약하다.

3. 어떤 가중치 행의 절대값 최대가 2.54다. absmax 대칭 INT8 양자화(q ∈ [−127, 127])에서 스케일 s와 값 1.0의 양자화 정수 q는?

s = 2.54 / 127 = 0.02. q = round(1.0 / 0.02) = 50. 역양자화하면 50 × 0.02 = 1.0으로 정확하다. 비대칭이 아니므로 영점은 0이다.

4. INT4 가중치를 그룹 크기 64, 그룹마다 FP16 스케일과 FP16 영점을 저장하면 가중치 하나당 유효 비트는?

\(b_{\text{eff}} = 4 + (16 + 16)/64 = 4.5\)비트. 대칭(스케일만)이면 4.25비트, g128 대칭이면 4.125비트다.

5. SmoothQuant의 변환 \(XW = (X\,\mathrm{diag}(s)^{-1})(\mathrm{diag}(s)\,W)\)에 대한 설명으로 옳은 것은?

대각 스케일을 한쪽에서 나누고 다른 쪽에서 곱하므로 양자화 전에는 수학적으로 완전히 같다. 이상치는 채널(열) 방향에 몰려 있으므로 채널별 \(s_j\)로 나눈다. α = 1이면 \(s_j = \max|X_j|\)로 활성값을 완전히 평탄하게 만들고 그 대신 가중치가 그 크기를 모두 떠안는다.

6. 보정 입력 X의 채널들이 서로 완전히 무상관이어서 \(H = 2XX^\top\)가 대각 행렬이라면 GPTQ의 결과는?

\(H^{-1}\)도 대각이므로 \([H^{-1}]_{q,F} = 0\)이고, 한 열의 오차를 다른 열로 넘길 통로가 없다. 각 가중치는 독립적으로 반올림되어 RTN과 같다. GPTQ의 이득은 입력 채널 사이의 상관에서 나온다(위 시뮬레이터의 '채널 독립').