이 글은 경량화 시리즈의 네 번째(#4)입니다. 양자화 #2에서 “8비트는 공짜, 2비트는 붕괴"까지 봤다면, 이번엔 왜 무너지는지, 그리고 어떻게 되살리는지를 봅니다. 코드는 github.com/warpspaceinc/efficient-ml-practicelow-bit-quantization.ipynb에 있고, 아래 모든 plot과 표는 그 코드를 직접 돌려 측정한 우리 숫자입니다.

FP4라는 무리수

NVIDIA Blackwell의 스펙 표에는 눈을 의심하게 하는 줄이 하나 있습니다. FP4 tensor core가 FP8의 정확히 2배 처리량이라는 것입니다. 비트를 절반으로 줄일 때마다 처리량이 2배가 되는 구조이니 당연한 산수인데, 문제는 FP4(E2M1)라는 포맷 자체입니다. 부호 1비트 + 지수 2비트 + 가수 1비트. 표현 가능한 값이 딱 15개입니다.

$$\pm\{0.5,\ 1,\ 1.5,\ 2,\ 3,\ 4,\ 6\}\ \cup\ \{0\}$$

FP4 vs INT4 격자 — FP4는 0 근처가 촘촘하고 끝으로 갈수록 성기다

INT4의 균등 격자와 달리 FP4는 0 근처가 촘촘합니다(간격 0.5, 끝은 2.0). 가중치가 0 근처에 종 모양으로 몰린다는 걸 생각하면 합리적인 배치입니다. 하지만 15개는 15개입니다.

표현 범위가 [−6, 6]뿐이라는 점도 중요합니다. 실수 가중치를 이 좁은 범위에 밀어 넣으려면 scale factor $S$로 나눠서 줄여야 합니다. 그리고 이 scale을 어떻게 정하느냐가 저비트 양자화의 성패를 가릅니다. 이 글의 나머지는 전부 그 이야기입니다.

실험은 #2와 같은 MNIST MLP(784→256→128→10, FP32 정확도 97.4%)로 합니다.


1. 범인: 아웃라이어 하나가 층을 지운다

가장 단순한 scale 설계는 텐서 전체에 하나를 쓰는 per-tensor입니다. 텐서에서 절대값이 가장 큰 가중치가 FP4의 최대값 6에 오도록 정하면 됩니다: $S = |W|_{\max} / 6$. 어떤 값도 잘리지 않으니 안전해 보입니다.

그런데 이 식을 다시 보면 무서운 점이 하나 있습니다. 격자 전체의 간격을 최대값 하나가 정한다는 것입니다. 최대값이 크면 scale이 커지고, scale이 커지면 격자가 성겨져서 0 근처에 몰려 있는 대다수 가중치의 해상도가 나빠집니다. 만약 어떤 이유로든 비정상적으로 큰 값, 즉 아웃라이어(outlier)가 하나라도 있다면?

직접 확인해 봅시다. fc1 가중치(20만 개)의 복사본에서 원소 딱 하나를 최대값의 50배로 키운 뒤, FP4로 양자화했을 때 나머지 가중치들이 입는 피해를 쟀습니다.

outlier 1개 주입 시 granularity별 MSE (log scale)

scale 단위MSE (정상)MSE (outlier 주입)증가율
per-tensor5.4e-051.2e-0322배
per-channel1.7e-052.3e-051배
per-group(32)1.2e-051.2e-051배

per-tensor의 “22배"는 숫자보다 내용이 끔찍합니다. 따라가 보면 이렇습니다. outlier가 $|W|_{\max}$를 50배로 끌어올리면 scale도 50배가 됩니다. 그러면 0이 아닌 가장 작은 FP4 레벨($0.5 \times S$)이 커져서, 정상 가중치 전부($|w| \le 0.3$)보다 큰 값이 됩니다. 모든 정상 가중치 입장에서 가장 가까운 격자점은 0이 됩니다. 즉 20만 개 가중치가 거의 전부 0으로 스냅됩니다. outlier 하나 때문에 층이 통째로 지워진 것입니다.

이게 인위적인 시나리오만은 아닙니다. MIT 6.5940 강의1가 보여주는 실제 사례가 MobileNetV2인데, 첫 depthwise 층은 채널 간 가중치 범위가 100배 이상 차이 납니다. 저비트 양자화의 실패는 대부분 이 패턴입니다. outlier 하나가 scale을 오염시켜, 나머지 전체의 해상도를 죽입니다.

2. 처방 ①: scale을 잘게 쪼갠다, 그룹 양자화

위 표가 이미 해법도 보여주고 있습니다. 문제의 본질이 “outlier 하나가 텐서 전체의 scale을 오염시킨다"라면, scale이 담당하는 범위를 줄이면 됩니다. 피해의 폭발 반경을 줄이는 것입니다.

  • per-channel: 채널(행)마다 scale을 하나씩. outlier의 피해가 자기 행(fc1 기준 784개)에 갇힙니다.
  • per-group(32): 원소 32개마다 scale을 하나씩. 피해가 같은 그룹의 31개에만 갇힙니다. 표에서 outlier를 주입해도 MSE가 꿈쩍하지 않는 이유입니다.

이 per-group 방식이 바로 NVIDIA Blackwell이 하드웨어로 지원하는 micro-tensor scaling입니다. 그리고 업계 표준 포맷으로 정리된 것이 MXFP4입니다: FP4 원소 + 원소 32개당 공유 scale 하나. FP4라는 무리수가 실전에서 성립하는 이유가 여기 있습니다. 그룹 scale이 outlier의 폭발 반경을 원소 31개로 줄여놨기 때문입니다.

3. 늘어난 scale의 비용: 2^k scale과 effective bits

물론 공짜는 아닙니다. fc1 기준으로 scale이 1개(per-tensor)에서 6,272개(per-group)로 늘었습니다. 이 scale들도 저장하고 연산해야 하는 데이터입니다. MXFP4는 이 비용을 두 가지 아이디어로 누릅니다.

첫째, scale을 2의 거듭제곱으로 제한합니다. MXFP4의 그룹 scale은 FP32 실수가 아니라 8비트 지수(E8M0), 즉 $2^k$ 꼴만 허용됩니다. 이러면 scale 곱셈이 지수 덧셈으로 바뀌어 하드웨어가 극도로 싸집니다. 대신 scale이 2배 단위로만 움직이니 최적값에서 어긋날 수 있는데, 그 손해를 실측해 보면:

layergroup32 + FP scalegroup32 + 2^k scale손해
fc11.23e-051.58e-051.28배
fc22.66e-053.62e-051.36배
fc36.12e-056.30e-051.03배

MSE 기준 1.0~1.4배입니다. outlier가 만들던 22배와 비교하면 자릿수가 다른, 싼 보험료입니다.

둘째, 오버헤드를 effective bits로 관리합니다. 원소 1개당 실제 저장 비용은 이렇게 계산합니다:

$$\text{effective bits} = \text{원소 비트} + \frac{\text{scale 비트}}{\text{그룹 크기}}$$

MXFP4는 $4 + 8/32 = 4.25$비트입니다. 만약 scale을 FP16으로 뒀다면 4.5비트가 됐을 텐데, $2^k$ 트릭 덕에 8비트 지수로 충분해서 4.25로 끝납니다. NVIDIA의 VS-Quant2는 같은 아이디어를 계층으로 쌓습니다. 16개마다 싼 INT4 scale을 붙이고, 텐서당 1개뿐인 비싼 FP scale이 절대 크기를 보정하는 구조로, 역시 4.25비트입니다.

4. FP4 정확도로 종합

이제 방식별로 MLP의 모든 가중치를 4비트로 양자화하고 MNIST 정확도를 재봤습니다.

FP4 양자화 방식별 MNIST 정확도

방식정확도effective bits
FP32 baseline97.36%32
FP4 per-tensor97.20%4.0
FP4 per-group(32)97.37%4.5
MXFP4 (group32, 2^k)97.18%4.25

MXFP4가 baseline과 0.2%p 차이로 7.5배 압축을 해냅니다. 4비트 가중치가 “무리수"가 아니라 실용적인 선택이 되는 순간입니다.


남은 세 가지 질문

여기까지가 weight를 4비트에 담는 이야기였습니다. 그룹 scale(처방 ①)이 outlier의 폭발 반경을 좁혀 4비트 가중치를 실용으로 만들었죠. 그런데 아직 세 가지 질문이 남습니다.

  1. 지금까지는 전부 weight 얘기였다. 입력마다 값이 바뀌는 activation은 어떻게 하나?
  2. scale을 정한 다음, 각 값을 “가장 가까운 격자점으로 반올림"하는 건 정말 최선인가?
  3. outlier를 가두는 게 아니라, 아예 없앨 수는 없나?

각 질문에 실전에서 쓰이는 무기(처방) 하나씩으로 답합니다.

5. 처방 ②: activation은 잘라낸다, KL clipping

weight는 학습이 끝나면 고정되므로 min/max를 정확히 알 수 있습니다. 하지만 activation은 입력마다 범위가 바뀝니다. 그래서 배포 전에 대표 입력 몇 배치(calibration 데이터)를 흘려보내며 “이 층의 activation은 대략 어떤 범위구나"를 통계로 수집해 둬야 합니다.

문제는 그 통계에서 어떤 값을 기준으로 scale을 잡느냐입니다. 관측된 최대값? 그러면 weight 때와 똑같은 병이 도집니다. ReLU 출력의 분포는 꼬리가 아주 길기 때문입니다. 실측해 보면 fc1 activation의 최대값은 19.7인데, 값의 99.9%는 9.7 아래에 있습니다. 최대값으로 scale을 잡는다는 건 상위 0.1%를 위해 나머지 99.9%의 해상도를 바치는 셈입니다.

그렇다면 어딘가에서 잘라내는(clipping) 게 이득일 텐데, 어디서 잘라야 할까요? 너무 일찍 자르면 잘려나간 값들의 정보가 사라지고(saturation 손실), 너무 늦게 자르면 격자가 성겨집니다(rounding 손실). 양쪽 사이 어딘가에 최적점이 있습니다.

아래 애니메이션이 그 트레이드오프입니다. 종 모양 분포 위에서 4비트 격자(빨간 선 16개)를 최대값에서부터 안쪽으로 조여 보면, 처음에는 격자가 촘촘해지며 MSE가 가파르게 떨어지다가, 분포의 몸통을 침범하기 시작하면 잘림 손실이 커져 다시 올라갑니다.

4-bit 격자를 조여가며 본 clipping 트레이드오프 — MSE가 U자를 그리며 최적점에서 멈춘다

이 U자는 실제 대형 모델에서도 그대로 나타납니다. NVIDIA의 OCTAV 논문3이 ResNet-50의 weight·activation 층에서 clipping 지점을 바꿔가며 양자화 MSE를 실측한 결과가 아래 그림입니다. 여기서 MSE는 텐서 원소별로 잰 $\mathbb{E}[(Q(x)-x)^2]$, 즉 양자화 전후 값 차이의 제곱 평균입니다(모델 출력의 차이가 아닙니다). 모든 층·비트에서 U자와 최적점(동그라미)이 보이고, 비트가 낮을수록 최적 clip이 안쪽으로 들어옵니다. OCTAV는 이 최적점을 Newton-Raphson 반복으로 매 학습 스텝마다 찾아내는 방법입니다.

clipping scalar에 따른 양자화 MSE 실측 — ResNet-50의 weight/activation 층, 4/6/8-bit

그림 출처: Sakr et al., ICML 20223, Figure 1.

TensorRT의 해법4은 이걸 정보 손실 최소화 문제로 풉니다. 후보 지점 T마다 “T에서 자른 원본 분포 P"와 “그걸 n레벨로 양자화했다가 P와 같은 눈금으로 다시 펼친 분포 Q"를 만들고(해상도가 달라진 두 분포는 직접 비교할 수 없으므로), 두 분포의 차이인 KL divergence $D_{KL}(P\|Q)$를 계산합니다. 이 값이 최소가 되는 T가 정보를 가장 덜 잃는 자름점입니다. 두 손실이 반대 방향으로 움직이므로 KL 곡선은 U자가 됩니다.

fc1 activation 분포와 KL-optimal clip 지점, KL vs T 곡선

우리 MLP에서 KL이 고른 지점은 T=12.8입니다(최대값 19.7). 이걸로 activation을 양자화해서 최대값 방식과 비교하면:

bitsclip = maxclip = KL
497.19%97.24%
396.91%96.93%
291.21%96.15%

비트가 낮아질수록 격차가 벌어집니다. 2비트에서는 outlier 몇 개를 포기한 대가로 5%p를 회수합니다. 이 방법의 실전 강점은 분포의 모양에 대한 가정이 없다는 것입니다. 층마다 activation 분포가 제각각이어도(단조 감소든, 종 모양이든) 똑같이 동작합니다.

6. 처방 ③: 반올림을 학습한다, AdaRound

scale과 clip을 다 정해도 자유도가 하나 남아 있습니다. 각 값을 격자점으로 보내는 반올림입니다. “가장 가까운 격자점으로”(round-to-nearest, RTN)가 너무 당연해서 선택지라는 생각조차 안 드는데, Qualcomm의 AdaRound5는 이게 최적이 아님을 보였습니다.

이유는 이렇습니다. 같은 층의 가중치들은 같은 입력에 곱해져 하나의 출력으로 합산됩니다. 그러니 가중치 하나하나의 반올림 오차는 독립적이지 않고, 출력단에서 서로 상쇄되기도 증폭되기도 합니다. RTN은 각 가중치의 오차만 보고 결정하므로 이 상호작용을 통째로 무시합니다. 개별 최선의 합이 전체 최선이 아닌 겁니다.

AdaRound는 그래서 기준을 바꿉니다. 가중치가 아니라 층의 출력을 가장 잘 복원하는 올림/내림 조합을 찾습니다:

$$\arg\min_{\mathbf{V}} \|\mathbf{W}\mathbf{x} - \lfloor\lfloor\mathbf{W}\rfloor + h(\mathbf{V})\rceil\,\mathbf{x}\|_F^2 + \lambda f_{reg}(\mathbf{V})$$

수식이 복잡해 보이지만 구조는 단순합니다. 각 가중치를 내림($\lfloor w \rfloor$)할지 올림($\lfloor w \rfloor + 1$)할지의 선택을 $h(V) \in (0,1)$라는 연속값으로 완화해서 gradient로 학습하고, 정규화 항 $f_{reg}$가 학습이 끝날 무렵 이 값을 0 아니면 1로 밀어붙입니다. 라벨도, 전체 재학습도 필요 없습니다. calibration 입력 몇 배치와 층별 짧은 최적화면 됩니다. 노트북에서는 층당 800스텝, CPU로 수십 초였습니다.

RTN vs AdaRound — INT3/INT2 정확도

bitsRTNAdaRound반올림이 바뀐 비율
INT396.83%97.23%11.8%
INT253.37%96.56%12.0%

INT2가 극적입니다. 반올림 결정의 12%를 바꿨을 뿐인데 53%가 97%로 돌아옵니다. “가장 가까운 값"이라는 직관이 저비트에서 얼마나 비싼 직관이었는지 보여줍니다.

7. 처방 ④: 좌표계를 돌려 outlier를 없앤다, Hadamard 회전

지금까지의 모든 처방은 outlier와의 공존이었습니다. 가두거나(그룹 scale), 잘라내거나(clipping), 피해서 돌아갔습니다(AdaRound). 최신 LLM 양자화(QuaRot6, SpinQuant7)는 발상이 다릅니다. outlier가 존재하지 않는 좌표계로 회전해 버립니다.

이게 가능한 이유는 계산 불변성 덕분입니다. 직교 행렬 $R$($RR^\top = I$)을 하나 고르면,

$$\mathbf{y} = \mathbf{W}\mathbf{x} = (\mathbf{W}R^\top)(R\,\mathbf{x})$$

가중치를 $WR^\top$로, 입력을 $Rx$로 미리 바꿔치기해도 출력은 수학적으로 완전히 동일합니다. 모델이 계산하는 함수는 그대로인데, 양자화가 보는 텐서의 “모양"만 바뀌는 겁니다.

여기서 $R$을 Hadamard 행렬(성분이 전부 ±1인 직교 행렬을 $\sqrt{n}$으로 나눈 것)로 고르면 마법이 일어납니다. 회전 후의 각 성분은 원래 성분 전체의 ±평균이 됩니다. 즉 한 자리에 몰려 있던 outlier가 모든 차원에 $1/\sqrt{n}$ 크기로 얇게 발라집니다. 뾰족했던 분포가 종 모양으로 돌아오는 것입니다.

아래 위젯에서 직접 돌려보세요. [1, 0]처럼 한 축에 몰린 벡터를 슬라이더로 회전시키면, 45°에서 두 좌표가 [0.707, 0.707]로 같아집니다. 길이는 그대로인 채 outlier가 두 축에 고르게 나눠집니다.

앞의 outlier 시나리오(가중치 하나를 50배로)를 fc2에서 재현하고 회전해 봤습니다:

Hadamard 회전 전후의 가중치 분포 — outlier가 사라지고 종 모양으로

분포가 얼마나 “정상"인지 재는 지표인 첨도(kurtosis, 가우시안이면 3)로 보면 13,717에서 54로, 입력·출력 양쪽을 다 회전(QuaRot 방식)하면 3.4까지 떨어집니다. outlier의 흔적이 통계적으로 소멸한 겁니다. FP4로 양자화한 뒤의 출력 오차로 확인하면:

per-tensorper-group(32)
outlier 없음 (참조)0.0590.045
outlier + 회전 없음0.8590.043
outlier + Hadamard 입력쪽만0.1970.089
outlier + Hadamard 양쪽 (QuaRot)0.0930.067

읽는 법은 이렇습니다. outlier가 있으면 per-tensor는 전멸하는데(0.86), 양쪽 회전이 참조 수준 근처(0.09)까지 되살립니다. 반대로 per-group은 회전해도 이득이 없습니다. 그룹 scale이 이미 outlier를 가두고 있었기 때문입니다. 여기서 회전의 성격이 드러납니다. 회전의 이득은 scale이 성길수록 크다. 그래서 회전은 scale을 촘촘히 붙이기 어려운 곳, 대표적으로 W4A4의 activation에서 진가를 발휘합니다.

비용 면에서도 매력적입니다. 그룹 scale은 저장 오버헤드(+0.25비트)가 붙지만 회전은 추가 저장이 0입니다. $WR^\top$는 배포 전에 미리 계산해 두면 되고, 추론 중의 $Rx$는 Hadamard 변환의 구조 덕에 $O(n \log n)$으로 처리됩니다.

한 가지 유의점: 우리 실험은 256차원이라 outlier가 1/16로만 줄었습니다. 실제 LLM은 hidden 차원이 4096 이상이라 1/64 이하로 줄어듭니다. 차원이 클수록 회전은 더 완벽해집니다. QuaRot이 LLaMA-2 70B의 W4A4 추론을 성사시킨 핵심 트릭이 이것이고, SpinQuant는 회전 행렬 자체를 학습해서 한 발 더 나아갑니다.


직접 돌려보기

위 plot과 표는 아래 노트북을 직접 돌려 나온 것입니다. 런타임에서 모두 실행이면 끝납니다.

정리: 처방 4개를 한 표에

저비트 양자화는 “평균적인 값"이 아니라 “최악의 값(outlier)“과의 싸움입니다. scale은 항상 최대값에 묶이기 때문입니다. 그 싸움의 처방 네 개를 한 표에 모으면:

처방아이디어비용우리 실측
① 그룹 scale (MXFP4)피해를 그룹 31개에 가둔다+0.25 bitFP4에서 baseline −0.2%p
② KL clippingoutlier 몇 개를 포기하고 해상도를 지킨다calibration 탐색2-bit에서 +5%p
③ AdaRound반올림을 층 출력 기준으로 학습한다층별 짧은 최적화INT2에서 53→97%
④ Hadamard 회전outlier 없는 좌표계로 회전한다추가 저장 0per-tensor 오차 9배 감소
  • 네 처방은 경쟁이 아니라 직교하는 도구입니다. 실제 W4A4 파이프라인은 회전(outlier 제거) + 그룹 scale(잔여 분산 흡수) + clipping(activation)을 함께 씁니다.

References


  1. Song Han. MIT 6.5940 TinyML and Efficient Deep Learning Computing, Lecture 6: Quantization Part II. efficientml.ai ↩︎

  2. Dai et al. VS-Quant: Per-Vector Scaled Quantization for Accurate Low-Precision Neural Network Inference. MLSys 2021. ↩︎

  3. Sakr et al. Optimal Clipping and Magnitude-aware Differentiation for Improved Quantization-aware Training. ICML 2022. ↩︎ ↩︎

  4. Szymon Migacz. 8-bit Inference with TensorRT. GTC 2017. ↩︎

  5. Nagel et al. Up or Down? Adaptive Rounding for Post-Training Quantization. PMLR 2020. ↩︎

  6. Ashkboos et al. QuaRot: Outlier-Free 4-Bit Inference in Rotated LLMs. NeurIPS 2024. ↩︎

  7. Liu et al. SpinQuant: LLM Quantization with Learned Rotations. 2024. ↩︎