15장. 모델 경량화: 양자화, 프루닝, 지식증류
모델을 줄이는 까닭
이삿짐 트럭은 크기가 정해져 있습니다. 짐이 트럭보다 크면 두 번 나르거나 더 큰 트럭을 빌려야 합니다. GPU 메모리도 정해져 있어서, 모델이 다 들어가지 않으면 일부를 CPU 메모리로 옮겨 돌리느라 훨씬 느려집니다(6장에서 3B 가 CPU 로 넘친 일).
1번에서 파라미터 수와 숫자 하나의 크기(바이트)를 곱해 가중치 메모리를 구합니다. 2번에서 글을 이어 쓸 때 쓰는 KV 캐시와 계산 중간값을 더합니다. 3번에서 그 합을 GPU 메모리(이 PC 는 4GB)와 견줍니다. 넘치면 경량화하거나 더 작은 모델을 고릅니다. 여기서 트럭이 GPU 메모리, 짐이 모델입니다.
가중치만의 메모리
| 모델 | fp32(4바이트) | fp16(2바이트) | int8(1바이트) | int4(0.5바이트) |
|---|---|---|---|---|
| 0.5B | 2.0GB | 1.0GB | 0.5GB | 0.25GB |
| 3B | 12GB | 6GB | 3GB | 1.5GB |
| 7B | 28GB | 14GB | 7GB | 3.5GB |
4GB GPU 에 3B 모델은 fp16 으로는 들어가지 않지만 4비트로는 들어갑니다. 지금까지 Ollama 로 쓴 qwen2.5:3b 가 4비트(Q4_K_M)라서 돌았던 것입니다.
메모리 어림하기
def weight_gb(params_billion, bits):
"""파라미터 수(십억 개)와 비트 수로 가중치 메모리(GB)를 어림한다"""
return params_billion * 1e9 * bits / 8 / 1e9
for name, b in [("Qwen2.5-0.5B", 0.494), ("Qwen2.5-3B", 3.09), ("7B 모델", 7.0)]:
print(name, {bits: round(weight_gb(b, bits), 2) for bits in (32, 16, 8, 4)})코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
def weight_gb(params_billion, bits): | 파라미터 수(십억 개 단위)와 숫자 하나의 비트 수를 받아 가중치 메모리를 GB(기가바이트)로 돌려준다 |
1e9 | 1 × 10 의 9제곱, 곧 10억이다. e 는 exponent(지수)다 |
* bits / 8 | 비트를 바이트로 바꾼다. 8비트가 1바이트다 |
/ 1e9 | 바이트를 GB 로 바꾼다. 여기서는 10억 바이트를 1GB 로 셌다 |
{bits: round(...) for bits in (32, 16, 8, 4)} | 네 가지 비트마다 메모리를 계산해 사전으로 담는 한 줄 꼴이다 |
round(값, 2) | 소수점 아래 두 자리로 반올림한다 |
이 코드가 뜻하는 것
이삿짐 트럭에 실을 짐 무게를 어림하는 일과 같습니다. 상자 개수에 상자 하나의 무게를 곱하면 됩니다. 여기서 상자 개수가 파라미터 수, 상자 하나의 무게가 숫자 하나의 바이트(비트 ÷ 8)입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
return params_billion * 1e9 * bits / 8 / 1e9 | 개수 × 바이트를 GB 로 바꾼다 | 상자 개수 × 상자 무게 |
for name, b in [...] | 세 모델을 하나씩 본다 | 이삿짐 세 집 |
print(name, {...}) | 모델마다 32, 16, 8, 4비트일 때의 GB 를 찍는다 | 상자를 얼마나 얇게 하느냐에 따른 짐 무게 |
결과에서 Qwen2.5-3B 는 16비트일 때 6.18GB 로 4GB GPU 에 들어가지 않고, 4비트일 때 1.54GB 로 들어갑니다. 비트를 반으로 줄이면 메모리도 정확히 반이 됩니다. 비유와 다른 점은, 상자는 얇게 해도 내용물이 그대로지만 숫자는 비트를 줄이면 조금씩 덜 정확해진다는 것입니다.
주의할 점
- 위의 표는 가중치만입니다. 실제로는 KV 캐시, 계산 중간값, 프로그램 자체의 몫이 더해집니다. 여유를 20~30% 둡니다.
- 줄이면 대개 품질도 조금 떨어집니다. 경량화는 "얼마나 줄이고 얼마나 잃는지"를 재며 고르는 일입니다.