LLM 엔지니어링15장 · 모델 경량화: 양자화, 프루닝, 지식증류4 / 22← 이전목차다음 →MetaContents

15장. 모델 경량화: 양자화, 프루닝, 지식증류

숫자의 정밀도

자로 길이를 잴 때 밀리미터 눈금 자와 센티미터 눈금 자가 있습니다. 센티미터 자는 덜 정확하지만 대개 충분합니다. 컴퓨터의 숫자도 32비트로 촘촘하게 나타낼 수도, 16비트나 8비트로 성기게 나타낼 수도 있습니다.

32비트 실수, 16비트 실수, 8비트 정수, 4비트 정수의 네 가지 정밀도를 바이트와 촘촘함으로 견준 카드

네 카드는 왼쪽부터 촘촘한 순서입니다. fp32 는 학습할 때의 기본이고, fp16·bf16 은 GPU 추론의 기본입니다. int8 은 256 개, int4 는 16 개의 값만 나타낼 수 있어 스케일을 함께 써서 원래 크기로 되돌립니다. 여기서 밀리미터 자가 fp32, 센티미터 자가 int8 입니다. 다만 자의 눈금과 달리 양자화의 눈금 간격은 가중치의 크기에 맞춰 정합니다(다음 절).

정밀도 견주기

꼴비트나타낼 수 있는 값쓰는 곳
fp3232아주 촘촘한 실수학습, CPU
fp1616실수. 범위가 좁다(약 ±65,504)GPU 추론, 5장의 LoRA
bf1616실수. fp32 와 범위는 같고 덜 촘촘최신 GPU 의 학습과 추론(GTX 1650 은 느리다)
int88정수 -128~1278비트 양자화
int44정수 -8~74비트 양자화

fp16 이 버리는 자리

Python
import numpy as np

x = np.array([0.1234567, 1000.123, 70000.0], dtype=np.float32)
print("fp32:", x)
with np.errstate(over="ignore"):              # 넘침 경고는 일부러 일으키는 것이라 끈다
    print("fp16:", x.astype(np.float16))      # 뒷자리가 깎이고, 너무 큰 수는 inf 가 된다

코드에 나오는 함수

코드쉬운 뜻
np.array([...], dtype=np.float32)숫자 셋을 32비트 실수(floating point 32)로 담은 배열을 만든다. dtype 은 data type(자료형)이다
x.astype(np.float16)as type(이 꼴로)이라는 뜻이다. 같은 숫자를 16비트 실수로 바꾼다
np.errstate(over="ignore")error state(오류 처리 상태)를 잠시 바꾼다. over 는 overflow(넘침)이고, 넘쳐도 경고를 내지 말라는 뜻이다
with ...:그 아래 줄에서만 이 설정을 쓰고, 끝나면 원래대로 돌린다
infinfinity(무한대)다. 나타낼 수 있는 범위를 넘은 수가 이렇게 바뀐다

이 코드가 뜻하는 것

밀리미터 자로 잰 길이를 센티미터 자로 다시 적는 것과 같습니다. 작은 자리는 깎이고, 자의 길이보다 긴 것은 "자를 넘음"이라고만 적힙니다. 여기서 밀리미터 자가 fp32, 센티미터 자가 fp16, "자를 넘음"이 inf 입니다.

줄하는 일쉽게 말하면
x = np.array(..., dtype=np.float32)작은 수, 보통 수, 큰 수를 fp32 로 담는다밀리미터 자로 잰다
print("fp32:", x)fp32 로 찍는다밀리미터 자의 눈금
with np.errstate(over="ignore"):넘침 경고를 끈다넘친다는 것을 알고 일부러 잰다
print("fp16:", x.astype(np.float16))fp16 으로 바꿔 찍는다센티미터 자로 다시 적는다

결과에서 0.1234567 은 0.1235 로, 1000.123 은 1000 으로 뒷자리가 깎였고, 70000 은 fp16 의 범위(약 65,504)를 넘어 inf 가 됐습니다. 모델의 가중치는 대개 1 보다 작아서 fp16 으로 바꿔도 거의 그대로 남습니다.

작게 줄여서 눈으로 보기

fp16 의 눈금이 얼마나 촘촘한지 직접 물어봅니다.

Python
import numpy as np

info = np.finfo(np.float16)
print("가장 큰 수:", info.max)
print("1 다음 눈금까지의 간격:", info.eps)
print("1 + 0.0004 →", np.float16(1 + 0.0004))
print("2048 + 1 →", np.float16(2048 + 1))
출력
가장 큰 수: 6.55e+04
1 다음 눈금까지의 간격: 0.000977
1 + 0.0004 → 1.0
2048 + 1 → 2.048e+03
결과읽는 법
가장 큰 수: 6.55e+046.55 × 10 의 4제곱, 곧 약 65,500 이다. 위 표의 ±65,504 를 줄여 찍은 것이다
1 다음 눈금까지의 간격: 0.0009771 근처에서 fp16 의 눈금은 약 0.001 간격이다. eps 는 epsilon(아주 작은 수)이다
1 + 0.0004 → 1.00.0004 는 눈금 간격보다 작아서 더해도 1 그대로다
2048 + 1 → 2.048e+032048 근처에서는 눈금 간격이 2 라서 2049 를 나타내지 못하고 2048 이 된다

np.finfo 는 floating point information(실수 꼴의 정보)을 알려 주는 함수입니다. fp16 은 숫자가 클수록 눈금 간격도 넓어집니다. 그래서 작은 수의 아주 작은 차이와 큰 수의 정확한 값을 함께 지키지 못합니다.

주의할 점

  • fp16 은 범위가 좁아 아주 큰 값이 나오면 inf 가 됩니다. 학습에서 손실이 nan 이 되는 원인이 되기도 합니다. 추론에서는 대개 문제가 없습니다.
  • 비트를 줄이는 것은 메모리뿐 아니라 속도에도 영향을 주지만, 늘 빨라지지는 않습니다. 정수를 다시 실수로 바꾸는 계산이 더 들 수 있습니다(실습 2).
← 이전 절15장 · 모델을 줄이는 까닭다음 절 →15장 · 양자화의 원리