9장. 모델 평가와 활용, QLoRA
QLoRA 의 원리
고화질 사진을 휴대폰에 수천 장 넣으려면 용량을 줄여 저장합니다. 화질이 조금 떨어지지만 알아보는 데는 문제가 없습니다. 보정 작업을 할 때는 그 사진을 잠깐 풀어 쓰고, 보정 내용은 별도의 보정 레이어(필름)에 고화질로 기록합니다. QLoRA 가 이 방식입니다. 기반 모델은 4비트로 줄여 저장하고(양자화), 계산할 때 잠깐 BF16 으로 되돌리며(역양자화), LoRA 행렬은 처음부터 높은 정밀도로 학습합니다.
강사자료의 그림입니다. 위쪽 경로의 "4비트 기반 가중치 W"는 NF4 로 저장되어 업데이트하지 않습니다. 아래쪽 경로의 "LoRA 행렬 A, B"는 높은 정밀도로 계산하며 학습 대상입니다. 두 경로의 결과가 더해져 출력 y 가 되고, 빨간 점선처럼 손실의 그래디언트(고칠 방향)는 LoRA 쪽으로만 흘러갑니다. 8장의 LoRA 그림과 같은 모양이고, 위쪽 상자가 4비트라는 점만 다릅니다.
QLoRA 의 세 가지 핵심
| 핵심 | 하는 일 | 사진으로 치면 |
|---|---|---|
| NF4 양자화 | 가중치가 대체로 0 근처에 몰린 정규분포라는 점을 이용해, 16개의 눈금을 분포에 맞게 배치한다 | 자주 나오는 색에 더 촘촘한 색 단계를 주기 |
| Double Quantization | 양자화에 필요한 스케일 값들까지 한 번 더 양자화한다 | 압축 설정표까지 압축하기 |
| Paged Optimizer | GPU 메모리가 순간 부족하면 옵티마이저 상태를 CPU 메모리로 옮겨 오류를 막는다 | 휴대폰이 꽉 차면 잠깐 클라우드에 옮겨 두기 |
정밀도와 숫자 하나의 크기
| 정밀도 | 비트 | 숫자 하나의 바이트 | 표현할 수 있는 값의 개수 |
|---|---|---|---|
| FP32 | 32 | 4 | 약 43억 가지 |
| BF16 | 16 | 2 | 65,536 가지 |
| NF4 | 4 | 0.5 | 16 가지 |
4비트로는 2×2×2×2 = 16 가지 값만 쓸 수 있습니다. 그 16개의 눈금을 어디에 둘지가 품질을 가릅니다.
짧은 예시
import numpy as np
# 가중치처럼 0 근처에 몰린 값 64개(평균 0, 표준편차 0.02 인 정규분포, 씨앗 고정)
rng = np.random.default_rng(0)
w = rng.normal(0, 0.02, size=64)
# 스케일: 가장 큰 절댓값으로 나눠 -1~1 범위로 맞춘다(이 값도 저장해야 한다)
scale = np.abs(w).max()
# 눈금 16개 두 가지: 고르게 놓은 눈금과 NF4 눈금(bitsandbytes 의 값을 소수 둘째 자리로 줄임)
even = np.linspace(-1, 1, 16)
nf4 = np.array([-1.00, -0.70, -0.53, -0.39, -0.28, -0.18, -0.09, 0.00,
0.08, 0.16, 0.25, 0.34, 0.44, 0.56, 0.72, 1.00])
def quantize(levels):
# 값마다 가장 가까운 눈금 번호(0~15, 4비트로 저장할 수)를 고른다
idx = np.abs(w[:, None] / scale - levels[None, :]).argmin(axis=1)
# 역양자화: 눈금 값 × 스케일로 되돌린다
return levels[idx] * scale
for name, levels in [("고른 눈금", even), ("NF4 눈금", nf4)]:
error = np.abs(w - quantize(levels)).mean()
print(f"{name}: 평균 오차 {error:.5f}")
# 결과: 고른 눈금 0.00159 / NF4 눈금 0.00116 → NF4 가 오차가 약 27% 작다이 코드가 뜻하는 것
사진의 색 단계를 16개로 줄일 때, 자주 나오는 색 근처에 단계를 촘촘히 두면 원본과 더 비슷해집니다. NF4 눈금은 0 근처가 촘촘하고(-0.09, 0.00, 0.08) 양 끝이 성깁니다(0.72, 1.00). 가중치도 0 근처에 몰려 있으므로 NF4 가 더 잘 맞습니다. 여기서 원본 사진이 w, 색 단계표가 levels, 압축했다 푼 사진이 quantize(levels) 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
scale = np.abs(w).max() | 범위를 -1~1 로 맞출 스케일 | 밝기 기준 정하기 |
argmin(axis=1) | 가장 가까운 눈금 번호 고르기 | 가장 비슷한 색 단계 |
levels[idx] * scale | 번호를 다시 실수로 되돌린다 | 압축 풀기(역양자화) |
np.abs(w - ...).mean() | 원본과의 평균 차이 | 화질 손실 |
실제 bitsandbytes 는 가중치를 64개씩 묶어 묶음마다 스케일을 하나씩 둡니다. 스케일이 많으면 그것도 메모리를 차지하므로, Double Quantization 으로 스케일들까지 한 번 더 줄입니다.
주의할 점
- QLoRA 에서 학습되는 파라미터는 4비트가 아닙니다. 4비트로 저장되는 것은 동결된 기반 모델이고, LoRA 행렬 A, B 는 높은 정밀도로 학습합니다.
- 계산할 때마다 역양자화를 하므로, 메모리는 줄지만 학습 속도는 LoRA 보다 느려질 수 있습니다.
- 4비트 양자화에는
bitsandbytes와 NVIDIA GPU 가 필요합니다. GPU 가 없는 PC 에서는 실습 4, 5를 실행할 수 없습니다.