직접 해 보기: 양자화 오차와 KV 캐시 메모리 계산하기
넘파이로 INT8 · INT4 양자화 출력 오차를 재고 가중치 · 학습 · KV 캐시 메모리 계산기를 만드는 실습
먼저 알아야 할 것
다루는 내용
가짜 가중치 행렬을 INT8 · INT4로 양자화해 층 출력 오차를 재고, 이상치 몇 개가 전체 스케일을 망치며 64개 묶음 스케일이 이를 회복하는 것을 확인합니다. 모델 크기 · 비트 · 문맥 길이로 가중치 · 학습 상태 · KV 캐시 메모리를 계산하고, 디코드 지연 하한과 추측 디코딩 기대 토큰 수를 표로 뽑습니다. 코드는 실제로 실행해 출력을 실었습니다.
해설
직접 해 보기: 양자화 오차와 KV 캐시 메모리 계산하기
앞 수업에서 손으로 했던 계산을 파이썬 하나로 반복합니다. ① 가중치 행렬을 INT8 · INT4로 양자화해 출력이 얼마나 틀어지는지 재고, 이상치와 묶음(그룹) 스케일의 효과를 확인합니다. ② 모델 크기 · 비트 수 · 문맥 길이를 넣으면 가중치 · 학습 상태 · KV 캐시 메모리 를 계산하는 작은 계산기를 만듭니다. ③ 디코드 지연의 이론 하한과 추측 디코딩의 기대 토큰 수를 표로 뽑습니다. 넘파이만 있으면 노트북에서 1초 안에 끝납니다.
| 항목 | 내용 |
|---|---|
| 준비물 | 파이썬 3 · 넘파이(pip install numpy). GPU 필요 없음 |
| 트랙 · 수준 | 스케일링·효율 · 중급 · 실습 |
| 걸리는 시간 | 30~40분(코드 읽기 · 실행 · 바꿔 보기) |
| 실행 확인 | 2026-10-22, 파이썬 3.14 · 넘파이 2.5.1(Windows)에서 아래 코드를 그대로 실행해 아래 출력을 얻었습니다 |
들어가기 전에
- 수치 형식 · 혼합 정밀도 · 양자화 — absmax 양자화 식, 이상치 문제, 묶음 스케일.
- 추론 최적화: KV 캐시 · MQA/GQA · 배칭 · 추측 디코딩 — KV 캐시 공식과 추측 디코딩 기대 토큰 수.
- 스케일링 법칙과 연산 예산 — 파라미터당 16바이트 학습 상태.
한눈에 보기
- 양자화 오차를 '가중치 오차'가 아니라 출력 오차 로 재는 방법을 익힙니다.
- 이상치 몇 개가 행렬 전체 스케일을 망치고, 묶음 스케일이 이를 살린다는 것을 숫자로 봅니다.
- 메모리 계산기를 손에 쥐고, 내 GPU에 어떤 모델이 들어가는지 스스로 판단할 수 있게 됩니다.
1. 실험 설계
무엇을 재나. 가중치 를 양자화한 로 바꿨을 때, 같은 입력 에 대한 출력의 상대 오차를 잽니다.
| 기호 | 뜻 |
|---|---|
| 원래 가중치(256×256, 평균 0 · 표준편차 0.02 정규분포로 만든 가짜 가중치) | |
| 양자화했다가 되돌린 가중치 | |
| 입력 8개(256차원, 표준 정규분포) | |
| 행렬 원소 제곱합의 제곱근(프로베니우스 노름) |
왜 출력 오차인가. 실제로 중요한 것은 가중치가 조금 틀린 것 자체가 아니라, 그 때문에 층의 출력이 얼마나 바뀌는가 입니다. GPTQ 같은 방법도 층 출력 오차를 줄이는 것을 목표로 삼습니다.
비교 조건 4가지 × 2종 가중치.
- INT8 / INT4
- 스케일을 행렬 전체에 1개 / 64개씩 묶음마다 1개
- 보통 가중치 / 값 1.0짜리 이상치 4개를 심은 가중치(보통 값의 약 50배)
2. 코드 전체
아래를 quant_kv_lab.py 로 저장하고 python quant_kv_lab.py 로 실행합니다. Windows 터미널에서 한글이 깨지거나 인코딩 오류가 나면 python -X utf8 quant_kv_lab.py 로 실행하세요.
import numpy as np
# ---------- 1부: 양자화 오차 실험 ----------
def quantize_absmax(w, bits, group=None):
"""대칭(absmax) 양자화 후 바로 되돌린 값을 돌려준다. group=None이면 행렬 전체에 스케일 1개."""
qmax = 2 ** (bits - 1) - 1 # 8비트 127, 4비트 7
flat = w.reshape(-1) if group is None else w.reshape(-1, group)
amax = np.abs(flat).max(axis=-1, keepdims=True)
scale = qmax / np.maximum(amax, 1e-12)
q = np.clip(np.round(flat * scale), -qmax, qmax)
return (q / scale).reshape(w.shape)
def rel_err(a, b):
return np.linalg.norm(a - b) / np.linalg.norm(a)
rng = np.random.default_rng(0)
W = rng.normal(0, 0.02, size=(256, 256)) # 보통 가중치
X = rng.normal(0, 1.0, size=(8, 256)) # 입력 8개
W_out = W.copy()
W_out[rng.integers(0, 256, 4), rng.integers(0, 256, 4)] = 1.0 # 이상치 4개 심기
print("== 1부: 양자화 오차 (출력 상대 오차 = ||XW - XWq|| / ||XW||) ==")
for name, M in [("보통 가중치", W), ("이상치 4개", W_out)]:
ref = X @ M
for bits in (8, 4):
for group in (None, 64):
Mq = quantize_absmax(M, bits, group)
tag = "전체 1개 스케일" if group is None else f"{group}개 묶음 스케일"
print(f"{name:7s} | INT{bits} | {tag:12s} | 출력 오차 {rel_err(ref, X @ Mq):.4f}")
# ---------- 2부: 메모리 계산기 ----------
GB = 1e9
def weight_mem(n_params, bits): return n_params * bits / 8 / GB
def train_state_mem(n_params, bytes_per=16): return n_params * bytes_per / GB
def kv_cache_mem(n_layer, n_kv, d_head, seq, batch, bytes_per=2):
return 2 * n_layer * n_kv * d_head * seq * batch * bytes_per / GB
print("\n== 2부: 메모리 계산기 ==")
for n in (7e9, 13e9, 70e9):
print(f"{n/1e9:>4.0f}B | FP16 {weight_mem(n,16):6.1f} GB | INT8 {weight_mem(n,8):6.1f} GB"
f" | INT4 {weight_mem(n,4):6.1f} GB | 학습 상태(16B/파라미터) {train_state_mem(n):7.1f} GB")
# LLaMA 65B 구성(층 80, 헤드 64, 헤드 차원 128)으로 KV 캐시
for n_kv, label in ((64, "MHA"), (8, "GQA 8그룹(가정)"), (1, "MQA")):
print(f"KV 캐시 {label:14s} | 길이 2048 · 배치 1 : {kv_cache_mem(80, n_kv, 128, 2048, 1):6.2f} GB"
f" | 배치 32 : {kv_cache_mem(80, n_kv, 128, 2048, 32):7.2f} GB")
# ---------- 3부: 디코드 지연 하한 · 추측 디코딩 ----------
print("\n== 3부: 디코드 지연 하한과 추측 디코딩 ==")
bw = 2.039e12 # A100 80GB SXM 메모리 대역폭(바이트/초)
for n, bits in ((7e9, 16), (7e9, 4), (70e9, 16)):
print(f"{n/1e9:.0f}B · {bits}비트 가중치 1회 읽기 = 토큰당 최소 {n*bits/8/bw*1e3:6.2f} ms")
for a in (0.6, 0.8, 0.9):
row = " ".join(f"γ={g}:{(1-a**(g+1))/(1-a):.2f}" for g in (2, 4, 8))
print(f"수락률 α={a} → 기대 토큰 수 {row}")코드 읽기 요점.
qmax = 2**(bits-1) - 1: 부호 있는 정수에서 대칭으로 쓰는 최댓값(8비트 127, 4비트 7).reshape(-1, group): 가중치를 64개씩 한 줄로 묶어, 줄마다 최댓값 → 스케일을 따로 구합니다. 이것이 '묶음(그룹) 양자화'입니다.np.clip: 반올림 결과가 범위를 넘지 않게 자릅니다.- 2부 함수 세 개는 각각 197 · 196 · 199 수업의 공식을 그대로 옮긴 것입니다.
3. 실행 결과 (실제 출력)
== 1부: 양자화 오차 (출력 상대 오차 = ||XW - XWq|| / ||XW||) ==
보통 가중치 | INT8 | 전체 1개 스케일 | 출력 오차 0.0107
보통 가중치 | INT8 | 64개 묶음 스케일 | 출력 오차 0.0059
보통 가중치 | INT4 | 전체 1개 스케일 | 출력 오차 0.1940
보통 가중치 | INT4 | 64개 묶음 스케일 | 출력 오차 0.1022
이상치 4개 | INT8 | 전체 1개 스케일 | 출력 오차 0.1015
이상치 4개 | INT8 | 64개 묶음 스케일 | 출력 오차 0.0089
이상치 4개 | INT4 | 전체 1개 스케일 | 출력 오차 0.9162
이상치 4개 | INT4 | 64개 묶음 스케일 | 출력 오차 0.1091
== 2부: 메모리 계산기 ==
7B | FP16 14.0 GB | INT8 7.0 GB | INT4 3.5 GB | 학습 상태(16B/파라미터) 112.0 GB
13B | FP16 26.0 GB | INT8 13.0 GB | INT4 6.5 GB | 학습 상태(16B/파라미터) 208.0 GB
70B | FP16 140.0 GB | INT8 70.0 GB | INT4 35.0 GB | 학습 상태(16B/파라미터) 1120.0 GB
KV 캐시 MHA | 길이 2048 · 배치 1 : 5.37 GB | 배치 32 : 171.80 GB
KV 캐시 GQA 8그룹(가정) | 길이 2048 · 배치 1 : 0.67 GB | 배치 32 : 21.47 GB
KV 캐시 MQA | 길이 2048 · 배치 1 : 0.08 GB | 배치 32 : 2.68 GB
== 3부: 디코드 지연 하한과 추측 디코딩 ==
7B · 16비트 가중치 1회 읽기 = 토큰당 최소 6.87 ms
7B · 4비트 가중치 1회 읽기 = 토큰당 최소 1.72 ms
70B · 16비트 가중치 1회 읽기 = 토큰당 최소 68.66 ms
수락률 α=0.6 → 기대 토큰 수 γ=2:1.96 γ=4:2.31 γ=8:2.47
수락률 α=0.8 → 기대 토큰 수 γ=2:2.44 γ=4:3.36 γ=8:4.33
수락률 α=0.9 → 기대 토큰 수 γ=2:2.71 γ=4:4.10 γ=8:6.13난수 씨앗을 default_rng(0) 으로 고정했으므로 같은 넘파이 버전이면 같은 숫자가 나옵니다. 버전이 다르면 소수점 끝자리가 조금 다를 수 있습니다.
4. 결과 읽기
① 비트 수. 보통 가중치에서 INT8 출력 오차는 약 1%, INT4는 약 19%(전체 스케일)입니다. 눈금이 255칸에서 15칸으로 줄면 오차가 크게 늘어납니다. 그래서 INT4는 거의 항상 묶음 스케일이나 GPTQ · AWQ 같은 보정 기법과 함께 씁니다.
② 묶음 스케일. 같은 INT4라도 64개 묶음마다 스케일을 두면 오차가 0.194 → 0.102로 거의 절반이 됩니다. 묶음마다 자기 최댓값에 맞춰 눈금을 촘촘하게 쓰기 때문입니다.
③ 이상치. 이상치 4개(전체 65,536개 중 0.006%)만으로 INT8 전체 스케일 오차가 0.011 → 0.10으로 약 10배, INT4는 0.92로 사실상 쓸 수 없는 수준이 됩니다. 묶음 스케일로 바꾸면 이상치가 든 몇 개 묶음만 피해를 보아 각각 0.009, 0.109로 돌아옵니다. LLM.int8() 이 이상치 차원을 따로 빼서 16비트로 계산한 이유가 바로 이것입니다.
④ 메모리. 7B 모델은 INT4로 3.5GB라 노트북 GPU에도 들어가지만, 학습 상태는 112GB라 혼자서는 학습할 수 없습니다. 미세조정에 QLoRA 같은 방법이 필요한 이유입니다.
⑤ KV 캐시. 65B 구성으로 동시 요청 32개 × 2,048토큰이면 MHA 기준 약 172GB로 가중치보다 큽니다. GQA 8그룹(해설용 가정)이면 약 21GB로 8분의 1입니다.
⑥ 지연과 추측 디코딩. 70B FP16은 대역폭만으로도 토큰당 최소 약 69ms(초당 약 15토큰)입니다. 추측 디코딩은 수락률이 높고(α=0.9) 초안 길이가 길면(γ=8) 큰 모델 한 번에 평균 6개 넘게 만들 수 있지만, 수락률이 0.6이면 γ를 8로 늘려도 2.5개에 그칩니다. 초안 모델이 목표 모델과 얼마나 닮았느냐가 핵심입니다.
flowchart LR
A[가짜 가중치 W · 입력 X 만들기] --> B[이상치 심은 W_out]
A --> C[absmax 양자화<br/>INT8 · INT4 × 전체 · 64묶음]
B --> C
C --> D[출력 오차 비교]
E[모델 크기 · 비트 · 길이 입력] --> F[메모리 계산기<br/>가중치 · 학습 상태 · KV 캐시]
F --> G[내 GPU에 들어가나 판단]
H[대역폭 · 수락률] --> I[지연 하한 · 기대 토큰 수]5. 바꿔 보기 과제
- 묶음 크기를 32, 128, 256으로 바꿔 INT4 출력 오차가 어떻게 변하는지 표로 만들어 보세요.
- 힌트 · 예상: 묶음이 작을수록 오차는 줄지만 스케일 저장분이 늘어납니다. FP16 스케일이면 64개 묶음은 원소당 비트가 더 듭니다.
quantize_absmax를 비대칭(영점) 양자화로 바꿔, ReLU를 지난 것처럼 음수가 없는 가중치(np.abs(W))에서 absmax와 비교해 보세요.- 힌트: 스케일 , 영점 .
- 여러분이 쓰는 모델의 설정 파일(config.json)에서 층 수 · KV 헤드 수 · 헤드 차원을 찾아
kv_cache_mem에 넣고, 원하는 문맥 길이에서 GPU 하나에 동시 요청을 몇 개 받을 수 있는지 계산해 보세요.- 힌트: (GPU 메모리 − 가중치 메모리 − 여유분) ÷ 요청 하나의 KV 캐시.
bw를 여러분 GPU의 공식 메모리 대역폭으로 바꿔 토큰당 지연 하한을 구하고, 실제 측정 속도와 비교해 보세요.
연습 문제
- INT4 묶음 64개, 스케일을 FP16으로 저장하면 원소당 실제 비트 수는?
- 풀이: 비트입니다.
- 13B 모델을 INT4(위 방식, 4.25비트)로 저장하면?
- 풀이: GB입니다.
- 출력 결과에서 이상치가 있을 때 INT8 '전체 스케일'과 '64개 묶음'의 오차 비는?
- 풀이: 배입니다.
스스로 점검
- 왜 이 실습은 가중치 오차가 아니라 출력 오차를 쟀나요?
- 답: 모델 품질에 영향을 주는 것은 층 출력의 변화이고, 같은 가중치 오차라도 입력과 곱해진 뒤의 영향은 다를 수 있기 때문입니다.
- 묶음 스케일이 이상치에 강한 이유는?
- 답: 이상치가 속한 묶음의 스케일만 커지고, 나머지 묶음은 자기 값 범위에 맞는 촘촘한 눈금을 유지하기 때문입니다.
- 디코드 지연 하한 계산에서 무엇을 무시했나요?
- 답: KV 캐시 읽기, 실제 커널 효율, 통신 등입니다. 그래서 실제 지연은 이 값보다 깁니다.
용어 정리
| 용어 | 쉬운 뜻 |
|---|---|
| 출력 상대 오차 | 양자화 전후 층 출력 차이를 원래 출력 크기로 나눈 값 |
| 묶음(그룹) 양자화 | 가중치를 일정 개수씩 묶어 묶음마다 스케일을 두는 양자화 |
| 이상치 | 다른 값보다 훨씬 큰 소수의 값 |
| 프로베니우스 노름 | 행렬 모든 원소 제곱합의 제곱근 |
| 난수 씨앗(seed) | 같은 '무작위' 숫자를 다시 만들기 위해 고정하는 값 |
| 메모리 대역폭 | 메모리에서 초당 읽을 수 있는 바이트 수 |
다음에 볼 것
- 모델 압축과 희소성: 지식 증류 · 가지치기 · 전문가 혼합 — 비트 수 말고 모델 자체를 줄이거나 일부만 쓰는 방법
- 중간 점검: 스케일링 · 효율 — 이 트랙 전체 복습 문제
라이선스
본문과 코드는 KAITABASE가 이 수업을 위해 직접 작성했습니다. 코드는 자유롭게 고쳐 써도 됩니다. A100 대역폭 값은 NVIDIA 제품 페이지, LLaMA 65B 구성은 LLaMA 논문에서 가져왔고, GQA 8그룹은 해설용 가정입니다. 가중치 · 입력은 무작위로 만든 가짜 값이라 실제 LLM의 양자화 오차 수치와는 다릅니다.
출처
- NVIDIA A100 제품 페이지(메모리 대역폭): https://www.nvidia.com/en-us/data-center/a100/
- Touvron 외, LLaMA (2023): https://arxiv.org/abs/2302.13971
- Dettmers 외, LLM.int8() (2022): https://arxiv.org/abs/2208.07339
- Leviathan 외, Fast Inference from Transformers via Speculative Decoding (2022): https://arxiv.org/abs/2211.17192
- Rajbhandari 외, ZeRO (2019): https://arxiv.org/abs/1910.02054
AI 원리부터 이해하기 · 스케일링·효율 모듈
스케일링·효율 모듈 학습
이 글은 스케일링·효율 모듈의 학습 자료입니다. 전체 19개 자료 중 14번째입니다.
출처·원문
원문 보기요약만