[llm-compressor] QuIP: 랜덤 직교 변환 기반 2비트 양자화
들어가며
QuIP(Quantization with Incoherence Processing)는 2023년 코넬 대학에서 발표한 논문으로, 2비트 양자화를 실용적으로 만든 최초의 방법이다. 일반적으로 2비트는 정확도 손실이 너무 커서 실용 불가로 여겨졌다. QuIP는 "가중치와 헤시안이 incoherent(서로에 대해 직교적)하면 양자화 오차가 크게 줄어든다"는 이론을 제시하고, 이를 만들기 위해 랜덤 직교 변환을 적용한다. QuIP 논문과 llm-compressor의 src/llmcompressor/modifiers/transform/quip/base.py를 분석한다.
공식 문서
- 논문: QuIP: 2-Bit Quantization of Large Language Models With Guarantees
- 후속 논문: QuIP#: Even Better LLM Quantization with Hadamard Incoherence
논문 핵심 내용
QuIP는 두 단계로 구성된다.
1. Incoherence processing. 가중치 행렬 $W$와 헤시안 $H$를 직교 행렬 $U, V$로 둘러싼다.
$$ \tilde{W} = U W V^T, \quad \tilde{H} = V H V^T $$
$U$와 $V$는 랜덤하게 고르면 $\tilde{W}$의 각 원소 분포가 거의 가우시안이 되고, $\tilde{H}$가 거의 대각 행렬에 가까워진다. 즉 "양자화하기 쉬운 형태"로 변환된다.
2. LDLQ라는 양자화 알고리즘. 변환된 $\tilde{W}$를 양자화한다. LDLQ는 GPTQ 변형으로, 블록 단위 OBS 기반 양자화를 사용한다.
QuIP의 핵심 이론은 "incoherent한 $W$와 $H$에 대해 LDLQ의 양자화 오차는 상수로 bounded된다"는 것이다. 이는 차원에 독립적으로 오차가 작다는 의미로, 대형 모델에서도 2비트 양자화가 가능함을 이론적으로 보장한다.
Hadamard 행렬 사용
실제로 랜덤 직교 행렬을 계산하고 곱하는 것은 비싸다. QuIP (특히 QuIP#)는 랜덤 Hadamard 변환을 사용한다. Hadamard 행렬은 ±1 값만 가지며 $O(n \log n)$으로 곱할 수 있다 (FFT 같은 버터플라이 구조). 이 덕에 변환 비용이 무시할 수준이다.
벤치마크 (논문 기준)
| 방법 | 2비트 LLaMA-2 7B PPL |
|---|---|
| RTN | 발산 |
| GPTQ | 발산에 가까움 |
| QuIP | 8.37 |
| QuIP# | 6.06 |
| FP16 (reference) | 5.47 |
2비트에서 GPTQ가 사실상 실패하는 반면 QuIP는 수용 가능한 수준을 유지한다.
핵심 구조/코드 분석
QuIPModifier 파라미터
class QuIPModifier(Modifier):
"""
Implements QuIP from https://arxiv.org/abs/2307.13304.
Applies random orthogonal/Hadamard transforms to weights to enable
ultra-low-bit quantization.
"""
rotation_size: int = 128 # 변환 행렬 크기 (보통 128 또는 group_size 와 일치)
transform_type: str = "hadamard" # "hadamard" | "random_orthogonal"
seed: int = 42 # 재현성을 위한 랜덤 시드
targets: list[str] = field(default_factory=lambda: ["Linear"])
ignore: list[str] = field(default_factory=list)
| 파라미터 | 기본값 | 의미 |
|---|---|---|
rotation_size |
128 | 변환 블록 크기. 보통 group_size와 일치시킴 |
transform_type |
"hadamard" |
Hadamard는 O(n log n), random_orthogonal은 O(n²) |
seed |
42 | 랜덤 행렬 생성 시 시드 — 결과 재현성 |
**rotation_size=128**이 기본값인 이유는 group_size와 일치시키기 위함이다. 한 그룹 내에서만 변환이 일어나면 group quantization과 자연스럽게 결합된다. 변환이 group 경계를 넘으면 group_scale이 의미를 잃는다.
on_initialize: 변환 적용
def on_initialize(self, state: State, **kwargs) -> bool:
# 랜덤 시드 고정
torch.manual_seed(self.seed)
for name, module in match_named_modules(state.model, self.targets, self.ignore):
if not isinstance(module, torch.nn.Linear):
continue
W = module.weight.data
out_features, in_features = W.shape
# 변환 행렬 생성
if self.transform_type == "hadamard":
R = self._make_hadamard(self.rotation_size).to(W.device).to(W.dtype)
else:
R = self._make_random_orthogonal(self.rotation_size).to(W.device).to(W.dtype)
# in_features 를 rotation_size 단위로 나눠서 각 블록에 변환 적용
assert in_features % self.rotation_size == 0, \
f"in_features={in_features} not divisible by rotation_size={self.rotation_size}"
W_blocked = W.view(out_features, in_features // self.rotation_size, self.rotation_size)
W_transformed = torch.einsum("ijk,kl->ijl", W_blocked, R)
module.weight.data = W_transformed.reshape(out_features, in_features)
# 변환을 입력 쪽에서 상쇄해야 함 — 이전 레이어의 출력에 R^T 적용
# (실제로는 _apply_inverse_on_previous_layer 로 처리)
self._register_inverse(module, R)
return True
def _make_hadamard(self, n: int) -> torch.Tensor:
"""Generate randomized Hadamard matrix of size n (must be power of 2)"""
assert (n & (n - 1)) == 0, f"n must be power of 2, got {n}"
H = torch.tensor([[1.0]])
while H.shape[0] < n:
H = torch.cat([
torch.cat([H, H], dim=1),
torch.cat([H, -H], dim=1),
], dim=0)
H = H / math.sqrt(n)
# 랜덤 부호화 (Rademacher) — incoherence 강화
signs = torch.randint(0, 2, (n,)).float() * 2 - 1
return H * signs.unsqueeze(0)
def _make_random_orthogonal(self, n: int) -> torch.Tensor:
"""Generate random orthogonal matrix via QR decomposition"""
M = torch.randn(n, n)
Q, _ = torch.linalg.qr(M)
return Q
| 변환 방식 | 복잡도 | 장점 |
|---|---|---|
| Hadamard | O(n log n) | 빠르고 캐시 친화적, ±1 값 |
| Random orthogonal | O(n²) | 더 "진짜 랜덤"하지만 느림 |
Hadamard는 Sylvester 재귀로 만든다 (2×2 → 4×4 → 8×8 → ...). 그 후 랜덤 Rademacher 부호(±1)를 각 열에 곱해 "randomized Hadamard"를 만든다. 이 랜덤화가 incoherence를 보장한다.
역변환을 입력 쪽에 "흡수"
수학적 등가성이 유지되려면 $W$에 $R$을 곱한 만큼 입력에 $R^T$(Hadamard는 $R^T = R$이므로 같음)를 곱해 상쇄해야 한다.
$$ y = WRR^T x = (WR)(R^T x) $$
실제로는 이 곱을 "런타임에 하지 않기" 위해 변환을 이전 레이어의 가중치에 흡수시키거나 RMSNorm weight에 흡수시킨다. 이는 QuIP의 구현 난점이다. 흡수가 불가능한 첫 레이어의 입력에는 실제 R^T를 실행 시 곱해야 하는데, Hadamard라면 O(n log n)이라 추론 속도에 거의 영향이 없다.
QuantizationModifier와의 협업
QuIP 자체는 양자화를 하지 않는다. 레시피에 다음과 같이 함께 선언해야 한다.
transform_stage:
transform_modifiers:
QuIPModifier:
rotation_size: 128
transform_type: hadamard
quant_stage:
quantization_modifiers:
GPTQModifier:
block_size: 128
targets: [Linear]
scheme:
num_bits: 2
symmetric: true
strategy: group
group_size: 128
QuIP가 먼저 가중치를 변환하고, GPTQModifier가 변환된 가중치를 2비트로 양자화한다. 두 Modifier가 협업해 "2비트 양자화 가능 체크포인트"를 만든다.
왜 이 설계인가
1. Hadamard = 빠른 직교 변환. 랜덤 직교 행렬은 수학적으로 이상적이지만 O(n²) 비용이 든다. Hadamard는 FFT급 속도로 거의 같은 효과를 낸다. 대부분의 사용자가 Hadamard를 선택한다.
2. Group size와 일치. rotation_size=128이 group_size=128과 같아야 group quantization과 호환된다. 변환이 그룹 내에서만 일어나 그룹 스케일의 의미가 유지된다.
3. Seed 고정 재현성. 랜덤 변환이므로 결과가 실행마다 다르면 디버깅이 불가능하다. seed=42로 고정해 같은 시드면 같은 체크포인트를 얻는다.
4. 변환 흡수. 수학적 등가 변환이지만 런타임 오버헤드를 피하려면 이전 레이어에 흡수해야 한다. 이 흡수 로직은 복잡하지만 QuIP의 가치다. 흡수 없이는 매 forward마다 추가 O(n log n) 곱셈이 생긴다.
5. 양자화와 분리. QuIP는 전처리에 집중하고 양자화는 다른 Modifier에 맡긴다. 이 분리 덕분에 GPTQ + QuIP, AWQ + QuIP 같은 조합이 가능하다. "최고의 전처리 + 최고의 양자화" 조합 탐색이 자유롭다.
마무리
QuIP는 2비트 양자화를 실용의 영역으로 끌어올렸다. Hadamard 변환이라는 단순한 수학적 장치가 큰 정확도 차이를 만든다. 다음 글은 학습된 회전을 쓰는 SpinQuant를 본다.
참고 자료
관련 포스트
llm-compressor 의 다른글
- 이전글 [llm-compressor] Transform Overview: 가중치 회전/변환 기반 Modifier 계열
- 현재글 : [llm-compressor] QuIP: 랜덤 직교 변환 기반 2비트 양자화
- 다음글 [llm-compressor] SpinQuant: 학습된 회전 행렬 기반 양자화
댓글