목차
1. LLM 파라미터는 왜 “숫자”일까?
2. 양자화(Quantization)
3. 양자화 유형
4. 코드로 보는 양자화
5. 배운 점/느낀 점
Day 14 - 양자화 (Quantization)
1. LLM 파라미터는 왜 “숫자”일까?
1) 핵심 요약
- LLM의 모든 파라미터(weight)는 실수(float) 값
- 보통:
- 학습: float32 / bfloat16
- 추론: float16 / int8 / int4
- 이유:
- 정밀도 vs 메모리/속도 트레이드오프
2) LLM 파라미터는 왜 실수(float)인가?
예: (Llama 3 70B → 파라미터 700억 개)
LLM 내부에는 여러 개의 파라미터(가중치)가 있다.
모든 파라미터는 실수로 저장된다:
예시:
W = 0.12837
b = -0.05392
...
✔ 신경망은 모든 계산이 “행렬 곱 + 비선형 변환”이기 때문
→ 행렬의 원소가 파라미터, 즉 실수값.
3) float32 vs float16 차이
[실수에는 “정밀도”가 필요하다]
컴퓨터는 실수를 저장하기 위해 IEEE 754 규격을 사용한다.
- float32 → 32비트
- float16 → 16비트
- bfloat16 → 16비트(표현 범위는 커지고 정밀도는 작음)
=> 비트 수가 많을수록 더 정밀하게 수를 표현할 수 있음.
IEEE754 규격?
: 부호(sign) + 지수(exponent) + 가수(fraction/mantissa) 조합으로 표현되는 이진수
Float32 = 32비트 구성
[ 1비트(sign) ][ 8비트(exponent) ][ 23비트(fraction) ]
Float16 = 16비트 구성
[ 1비트(sign) ][ 5비트(exponent) ][ 10비트(fraction) ]
예시: float32에서 표현 가능하지만 float16에서 손실되는 경우
실수 값:
0.123456789
float32 저장:
00111101111111101101111100101011
float16 저장(정밀 손실 발생):
0011110001111100
- float32는 7자리 정밀도 → 거의 정확 저장
- float16은 3자리 정밀도 → “0.1234” 정도로 반올림됨
→ LLM 추론에서 부동소수점 오차가 발생하는 이유
[메모리 요구량이 다름]
float32가 4 byte인 이유?
=> IEEE 754 단정도(single precision) 부동소수점 규격이 32비트로 딱 정해져 있기 때문.
| 구성 요소 | 비트 수 |
| 부호(sign) | 1 bit |
| 지수(exponent) | 8 bit |
| 가수(mantissa) | 23 bit |
합계 = 1 + 8 + 23 = 32 bit = 4 byte
예를 들어 70B 모델을 float32로 저장하면:
70,000,000,000 parameters × 4 bytes = 280GB
float16으로 바꾸면:
70,000,000,000 parameters × 2 bytes = 140GB
무려 140GB 절약.
그래서 훈련은 float32 or bfloat16(정밀 중요)
추론은 float16, float8 등(속도 중요)으로 compressed 하는 것
4) 실제론 어떤 정밀도가 쓰일까?
Training(훈련)
- float32 또는 bfloat16
- 혼합정밀도(Mixed precision):
- 연산은 float16으로 빠르게
- 중요한 weight 업데이트는 float32로 안정성 확보
Inference(추론)
- float16
- NF4, INT8, INT4 등 더 작은 정밀도로도 가능 (quantization)
[유형별 차이]
| 유형 | byte | 특징 |
| FP32 | 4 byte | 학습 시 안정적 |
| FP16 / BF16 | 2 byte | 학습·추론 모두 널리 사용 |
| INT8 | 1 byte | 추론용 |
| NF4 / INT4 | 0.5 byte | LLM 추론 최적화 |
| INT2/1 | 실험적 | 극한의 경량화 |
[FP8/INT8 차이]
| 항목 | FP8 | INT8 |
| 데이터 형태 | 실수(float) | 정수(integer) |
| 연산 방식 | 부동소수점 연산 | 정수 연산 |
| 표현력 | 훨씬 풍부 | 상대적으로 낮음 |
| 학습 | 가능 (H100 등에서 적극 지원) | 어려움 (분포 clipping 필요) |
| 목적 | 학습 & 추론 | 거의 추론용 |
| 정밀도 손실 | 적음 | 큼 |
=> FP8은 ‘float를 그대로 줄인 버전’이라 학습·추론 품질이 좋다.
=> INT8은 정수라 모델 품질 손실 가능성이 더 크다.
2. 양자화(Quantization)
1) 요약
- 양자화(Quantization)는 LLM 파라미터를 더 작은 비트 수(예: 16bit→8bit→4bit)로 줄여서
- 모델의 메모리 사용량을 대폭 줄이고, 추론 속도를 빠르게 만드는 기술
- 정밀도가 조금 줄어들지만, 추론(inference)에서는 대부분 큰 문제없이 훨씬 효율적으로 모델을 돌릴 수 있음
2) 직관적 비유 (by GPT)
LLM의 파라미터는 "초정밀 레시피" 같은 것
- float32 → 재료를 mg 단위까지 정밀하게 측정하는 셰프
- float16 → 0.1g 단위로 측정 (조금 덜 정밀하지만 문제 없음)
- int8 → 1g 단위
- int4 → 5g 단위 (대략적인 느낌)
훈련: 맛을 완벽히 조절해야 하므로 float32/float16
추론: 레시피는 이미 완성됐으므로 int8/int4가 충분
3) 양자화 사용 이유?
① 메모리 절감
=> 70B 모델도 int4면 35GB → 1장 GPU로 로딩 가능
(양자화 잘하면 큰 모델도 온디바이스로도 돌릴 수 있음)
② 추론 속도 증가
정수 연산은 실수 연산보다 훨씬 빠름
→ TPS 상승, 비용 절감
4) 양자화 작동방식
예를 들어 파라미터가 이렇게 있다고 하자:
W = [ 0.12, -0.45, 1.03, 0.22 ]
float32로 저장:
→ 값 그대로 32비트 실수 저장
→ 파라미터 하나당 4 bytes 사용
✔ Step 1: 범위를 찾는다
W의 최소/최대:
min = -0.45
max = 1.03
✔ Step 2: 더 작게 저장할 수 있는 구간으로 나눈다
예: **int8 양자화(8비트)**라면 표현 가능한 정수는:
-128 ~ +127 (256개 값)
✔ Step 3: 실수를 정수로 매핑한다
각 실수 w를 다음 식으로 정수 q에 매핑한다:
q = round( (w - min) / (max - min) * 255 )
예를 들어 0.12는 대략:
q ≈ 86
✔ Step 4: 저장할 때는 정수만 저장
이제 파라미터는 이렇게 저장됨:
[86, 0, 255, 103]
이게 바로 양자화된 weight.
✔ Step 5: 추론 시 역양자화(dequantize)
모델이 계산할 때 정수를 실수로 복원:
w_est = min + (q / 255) * (max - min)
5) 학습과 추론(infernece)에서 서로 다른 비트수 진행이 가능한 이유?
(어떻게 FP32 → INT8/INT4 추론이 가능한가?)
✔ 1단계. 학습(Training)은 Float 정밀도가 필요
학습에서는 아래를 반복한다:
Weight += learning_rate * gradient
여기서 gradient는 매우 작은 값(1e-7, 1e-9) 일 때도 많음
그래서 정밀한 실수(float32 or BF16)가 아니면 업데이트 자체가 망가진다.
그래서:
- GPT·Llama·Mistral 학습 → FP32/BF16
- LoRA 학습 → BF16
만약 INT8로 학습하면?
→ 소금 3g 단위로만 조절하는 셰프 = 학습 자체 불가능.
✔ 2단계. 학습이 끝나면 "최종 weight"는 고정된다
학습이 끝나면 더 이상 weight를 업데이트하지 않는다.
이제 하는 일은:
y = W @ x
행렬 곱 계산만 하면 됨.
업데이트가 없으므로 FP32 정밀도가 필요 없다.
✔ 3단계. 추론용 weight를 변환(양자화)한다
이제 float weight를 정수로 매핑한다:
예:
W_float = [0.12, -0.45, 1.03]
W_int8 = [ 86, -47, 255 ] # 정규화 후 8비트로 저장
scale, zero_point도 함께 저장
이건 모델 구조를 바꾸는 게 아니라
메모리에 저장되는 weight만 정수형으로 바꾼 것이다.
✔ 4단계. 추론 시 계산은 어떻게 될까?
방식 1: 역양자화(dequantize) 후 계산
정수 → float로 다시 복구하고
FP16으로 계산한다.
방식 2: 정수 연산만으로 계산 (faster)
예:
W_int8 @ x_int8 → (내부는 INT8 연산)
마지막 출력 단계에서만 float로 변환.
이 방식은 TensorRT-LLM, llama.cpp, Triton kernels, vLLM INT4,
모두 이걸 사용한다.
즉, 학습한 float weight는 그대로 두고,
추론할 때는 정수 버전을 로딩해서 연산하는 것.
| 단계 | 정밀도 | 이유 |
| Training | FP32/BF16 | Gradient 업데이트에 고정밀 필요 |
| Save model | FP32/BF16 | 원본 모델 저장 |
| Quantize | INT8/INT4 | 메모리·속도 최적화 |
| Inference | INT8/INT4 | weight 업데이트 없음 → 정수 계산 가능 |
3. 양자화 유형
1) PTQ (Post-Training Quantization)
핵심 요약
- 이미 학습된 모델을 추론(inference)용으로 나중에 양자화하는 방식.
- 훈련을 다시 할 필요 없음 → 가장 쉬움 & 비용 거의 없음.
특징
- 학습 끝난 모델을 INT8/INT4로 변환
- gradient 업데이트 없음
- 성능 약간 떨어질 수 있음 (특히 4-bit 이하일 때)
- LLM에서 제일 널리 쓰임
대표 기법
- GPTQ
- AWQ
- GGUF 양자화 (llama.cpp, MLC-LLM 등)
2) QAT (Quantization-Aware Training)
핵심 요약
- 학습 자체를 “양자화될 것을 알고” 훈련하는 방식.
- 훈련 중에 weight/activation을 INT8/INT4처럼 흉내 내며 배움 → 성능 손실 최소.
특징
- 가장 높은 정확도
- 하지만 훈련 비용이 매우 큼
- 대형 LLM에는 거의 사용되지 않음 (학습비용이 너무 커서)
언제 쓰나?
- MobileNet, Vision 모델 (엣지 AI 모델)
- 임베디드/칩 기반 모델
3) QLoRA (Quantized LoRA Fine-tuning)
핵심 요약
- 기존의 거대한 LLM은 4bit로 압축(int4)해서 로드
- 미세튜닝은 LoRA(=추가된 작은 랭크 행렬)만 BF16/FP16으로 학습
- 원본 weight는 절대 건드리지 않음
이 덕분에 70B 모델도 단일 GPU(48GB/80GB VRAM)에서 파인튜닝 가능.
왜 QLoRA가 중요한가?
int4(양자화된 base 모델) ↔ bf16(LoRA 어댑터)
[저장은 4bit, 계산은 FP16로 하는 이유?]
이유 1) GPU는 matmul을 FP16/BF16/FP32로 최적화
Tensor Core는 FP16 연산에 최적화되어 있다.
4bit 연산은 보통 지원되지만 속도/정확도 면에서 이점이 적다.
그래서 저장은 4bit, 계산은 FP16이 가장 효율적.
이유 2) 정확도 유지
4bit weight 자체로 곱셈하면 precision loss가 너무 크다.
→ FP16으로 복원하고 연산하면
→ 거의 full-precision 모델과 유사한 성능을 얻는다.
QLoRA가 성능을 유지하는 이유가 바로 이것.
이유 3) 메모리 절감
저장은 4bit로 해서 메모리 절감, 작은 GPU로도 큰 모델 이용 가능
GPU VRAM 용량이 모델 크기를 결정한다
70B float16 = 140GB → H100 여러 장 필요 (1장에 80GB)
70B int4 = 약 35GB → 한 장으로도 가능
4. 코드로 보는 양자화
1) 모델 4비트(NF4) 로딩 설정
# Quantization Config - this allows us to load the model into memory and use less memory
quant_config = BitsAndBytesConfig(
load_in_4bit=True, # 4비트 로드 (모델 가중치를 4비트 형태(int4-ish)로 메모리에 로드할지 여부)
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4"
)
① load_in_4bit=True
모델 가중치를 4비트 형태(int4-ish)로 메모리에 로드할지 여부.
- FP16: 2 byte
- FP32: 4 byte
- 4bit: 0.5 byte
즉 메모리 사용량이 FP16 대비 4배 줄어든다.
② bnb_4bit_use_double_quant=True
더블 양자화(Double Quantization) 사용 여부
쉽게 말해:
- 1차 양자화로 4bit로 줄이고
- 2차 양자화로 scale 값까지 압축해 메모리를 더 줄임
효과:
- 메모리를 조금 더 줄임 (5~10% 정도 추가 절감)
- 정확도가 크게 떨어지지 않음
이 옵션은 대부분 “켜는 게 기본값”으로 여겨짐.
왜 scale을 또 양자화해야 하나?
4bit 양자화는 이렇게 동작함:
실수 weight들 → (scale, zero_point, 4bit 값들)
즉, 가중치 W는 다음처럼 저장됨:
W ≈ scale × (q - zero_point)
여기서 중요한 점:
- 4bit q 값은 압축됨 (0~15니까 4bit)
- scale은 float16 / float32로 저장됨 → 여기가 아직 무거움
그러면 실제 메모리 구조는:
항목 크기
| q (4bit 값들) | 아주 작음 |
| scale | FP16/Fp32라 커서 메모리 차지 큼 |
| zero_point | 작음 |
문제: scale이 너무 커서 전체 최적화된 건 아님
그래서 나온 것이 Double Quantization
Double Quantization 아이디어
✔ 핵심 아이디어
“scale 값도 다시 압축하면 더 압축되잖아?”
기존:
weight block → scale(float16), q(4bit values)
더블 양자화:
weight block → scale_q(4bit or 8bit), scale_scale(float16)
즉,
- 원래 weight들을 4bit로 양자화하기 위해 쓰였던 scale 값들을 또 양자화
- scale 값도 “블록(block)” 단위로 묶어서 더 작은 수(예: 4bit~8bit)로 변환
- 그 scale 값들을 복원하기 위한 상위 scale 하나만 float 형태로 유지
그래서 “양자화된 scale"이 생기는 것.
scale을 위한 scale
그래서 이름이 Double Quantization.
③ bnb_4bit_compute_dtype=torch.bfloat16
연산은 bfloat16으로 수행하겠다는 뜻.
-> 왜 이렇게 하냐?
4bit로 로드된 가중치라도
추론 중 연산을 4bit로 하면 정확도가 너무 떨어짐.
그래서 연산은 bfloat16(FP16보다 안정적)으로 함:
- 저장(storage): 4bit
- 연산(compute): bfloat16
④ bnb_4bit_quant_type="nf4"
NF4(“Normal Float 4”) 양자화 방식 사용.
NF4가 왜 특별한가?
➡ Meta(LLaMA 개발 팀)가 연구한 4비트 중 가장 LLM에 적합한 양자화 방식
➡ 일반 int4보다 훨씬 정확도 손실이 적음
NF4는 데이터 분포를 normal distribution(정규분포)에 맞춰 매핑하기 때문에
LLM의 weight 통계적 특성과 잘 맞아서 높은 품질을 낸다
2) 토크나이징 및 텐서 형태로 GPU에 올리는 과정
# Tokenizer
tokenizer = AutoTokenizer.from_pretrained(LLAMA)
tokenizer.pad_token = tokenizer.eos_token
inputs = tokenizer.apply_chat_template(messages, return_tensors="pt").to("cuda")

① tokenizer = AutoTokenizer.from_pretrained(LLAMA)
- 역할: 사전에 학습된 LLAMA 모델에 맞는 토크나이저를 불러옵니다.
② tokenizer.pad_token = tokenizer.eos_token
- 역할: 패딩 토큰(pad_token)이 없는 모델(예: Llama 계열)을 위한 설정
- eos_token은 문장의 끝을 의미하는 End Of Sentence 토큰이에요.
- Llama는 원래 pad 토큰이 없어서, 패딩이 필요한 상황(배치 입력 등)에 eos_token을 대신 사용하게끔 설정한 것.
③ inputs = tokenizer.apply_chat_template(messages, return_tensors="pt").to("cuda")
- 역할: 대화형(chat) 모델용 입력 포맷을 만들어 PyTorch 텐서로 변환한 후 GPU로 이동
apply_chat_template(messages, ...)
- messages는 아래처럼 대화 기록이 들어있는 리스트
- messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "안녕!"}, ]
- 이 함수는 모델이 학습될 때 사용된 채팅 포맷(template)을 그대로 적용해서 텍스트를 하나의 prompt로 합칩니다.
<|system|> You are a helpful assistant. <|user|> 안녕! <|assistant|>
return_tensors="pt"
- 결과를 PyTorch 텐서(torch.Tensor)로 반환합니다.
- 모델 입력 시 model(**inputs) 형태로 바로 넣을 수 있습니다.
.to("cuda")
- 생성된 텐서를 GPU 메모리로 전송합니다.
- 즉, 이후 model.generate() 등을 GPU에서 바로 수행할 수 있게 합니다.
3) 모델 로딩 + 양자화 적용 + GPU 자동 배치 + 실제 메모리 사용량 출력을 한 번에 수행

① AutoModelForCausalLM.from_pretrained()
- Hugging Face 모델을 불러와서 언어모델(Causal LM) 형태로 로딩.
- 즉, 프롬프트 → 다음 토큰 생성 같은 “생성형 모델”로 동작하는 구조.
LLAMA
- 모델 이름 문자열 (예: "meta-llama/Llama-3.1-8B-Instruct").
- 이 문자열을 기반으로 Hugging Face에서 모델 weight, config 등을 다운로드함.
device_map="auto"
- 모델을 가능한 디바이스로 자동 배치하는 옵션.
- 실행 환경이 GPU라면 GPU/CPU 메모리를 보고 자동으로 분배함.
예)
- GPU 1개 → GPU에 모델 전체 배치
- GPU 메모리 부족 시 → 일부 레이어 CPU, 일부 레이어 GPU
- Colab T4 같은 메모리 작은 GPU에서 특히 유용
② quantization_config=quant_config
- 양자화 설정을 적용하면서 모델을 로드하겠다는 의미.
- 앞에서 만들었던:
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_quant_type="nf4"
)
이 설정 그대로 적용 → 모델을 4 bit 양자화 상태로 로드해서 메모리를 크게 절약.
③ model.get_memory_footprint()
- 모델이 현재 차지하고 있는 전체 메모리(bytes)를 반환함.
- 텐서 weight
- 양자화 테이블
- 옵저버
- 기타 케시 등 포함
/ 1e6
- 바이트를 메가바이트(MB) 단위로 변환.
=> 아마 8b 모델 크기만 고려 시 아래처럼 나와야 하지만, 일부는 cpu에 모델이 올라가 있어서 1gb 정도만 메모리 PRINT에 찍히는 것으로 보임.
✔ 기본 weight (4bit)
✔ double quant 적용 시
- scale도 압축되어, 약 10~20% 추가 절감 (≈ 4GB → 약 3.2 ~ 3.6GB)
예를 들어:
- GPU: 25%
- CPU: 75%
이면
5. 배운 점/느낀 점
1. 실제 LLM 파라미터와 정밀도를 더 뜯어보면서, 이래서 특정 모델이 gpu 몇 장 필요해라고 했을 때,
"정밀도에 따라, 훈련용이냐/추론용이냐에 따라서도 다르다고 말하는 이유"를 느낄 수 있었고
대략적인 모델별 gpu 소요량 계산이 어떻게 되는지를 알 수 있었다.
2. 양자화 쪽을 더 깊게 파보면서 말로 많이 들어왔던 양자화가 실제로 어떻게 동작하는지에 대한 원리와, 왜 학습과 추론(inferenece)에서 서로 다른 정밀도를 사용하는지, 또 추론과정에서도 모델 저장(로딩)과 계산 시에 정밀도를 다르게 가져가는 이유, 어떻게 그게 가능한지를 원리적으로 이해해 볼 수 있었다.
3. 많이 들어왔던 파인튜닝 기법 중 하나인 QLoRA는 어떻게 동작하는지 단계별로 나눠서 볼 수 있었다.
4. 또한 코드단에서 이러한 양자화가 어떻게 구현이 되어서 실제 GPU에 모델이 올라가고, GPU에 올라가는 메모리 양들을 줄여나가는 지를 볼 수 있던 게 좋았다.
(꾸준히 공부하고, 적을 테니 많은 관심 부탁드립니다.)
Profile: