티스토리 뷰

 

지난 글에서는 학습한 모델을 GGUF로 바꿔 Ollama에 올렸다. 그때 "GGUF Q8_0으로 변환했다"고 한 줄 적고 넘어갔는데, 사실 그 한 줄에는 숨은 결정이 하나 있었다. 어느 정밀도로 양자화할 것인가. 이번 글은 RAG 구축 고려사항 중 ⑦모델 양자화에 해당하는, 그 결정에 대한 이야기다.

1. 양자화는 결국 "비트를 줄이는 일"이다

모델은 수십억 개의 가중치, 그러니까 숫자 덩어리다. 이 숫자를 원본은 보통 16-bit 부동소수점(fp16)으로 저장한다. 양자화는 이 숫자를 더 적은 비트로 근사해서 저장하는 일이다. 8-bit, 4-bit로 누르는 식이다.

 

왜 이게 크기에 직접 영향을 주냐면, 모델 용량은 거의 "가중치 개수 × 가중치 하나당 바이트"로 정해지기 때문이다. 정밀도를 낮추면 가중치 하나가 차지하는 바이트가 그대로 줄어든다.

정밀도        가중치 1개   1.7B 모델 크기(대략)
16-bit (fp16)   2 byte       ~3.4 GB
8-bit  (Q8)     1 byte       ~1.7 GB
4-bit  (Q4)     0.5 byte     ~0.9 GB

대신 공짜는 아니다. 비트 수가 줄면 표현할 수 있는 값의 해상도가 떨어진다. 원래 16비트로 촘촘히 표현하던 숫자를 8비트, 4비트 격자에 욱여넣으니 미세한 오차가 생긴다.

 

양자화는 이 "크기 ↓ vs 정밀도 ↓"의 맞교환이다.

2. 원본 그대로는 너무 무거웠다

클라우드에서 개발할 때는 모델 크기를 크게 신경 쓰지 않았다. GPU 메모리가 넉넉했으니까. 그런데 온디바이스로 넘어오니 위 표의 숫자가 곧바로 벽이 됐다.

 

1.7B를 원본 정밀도로 올리면 그것만 3GB가 넘는다. 여기에 추론 런타임이 올라가고, 사용자가 실제로 쓰는 다른 프로그램도 같이 돌아가야 한다. 일반 PC에 이대로 얹기는 부담스럽다. 학습 때도 마찬가지였다. 무료 Colab T4의 14GB 안에 1.7B를 통째로 올리는 것 자체가 빠듯했다. 그래서 양자화는 "하면 좋은 것"이 아니라 "안 하면 안 올라가는 것"이었다.

3. 학습할 때 — 4-bit로 올리고 어댑터만 (QLoRA)

재미있는 건, 이 실험에서 양자화를 두 단계에서 다른 목적으로 썼다는 점이다.

 

먼저 학습 단계. T4에 1.7B를 올리려고 베이스 모델을 4-bit로 눌러 동결한 뒤, 그 위에 작은 LoRA 어댑터만 학습했다. 이게 QLoRA다. 핵심은 무거운 베이스는 4-bit로 메모리만 차지하게 두고, 실제로 학습되는 건 고정밀도의 작은 어댑터(전체의 1% 남짓)뿐이라는 것. 덕분에 원본을 통째로 올리면 안 들어갈 모델도 무료 GPU 한 장에서 파인튜닝할 수 있었다.

 

방법은 의외로 간단하다. 모델을 불러올 때 4-bit 로드 한 줄이면 된다.

# 학습: 베이스를 4-bit로 올리고 LoRA만 학습 (QLoRA)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen3-1.7B",
    load_in_4bit=True,        # ← 여기서 4-bit 양자화로 로드
)

즉 여기서 양자화의 목적은 "품질"이 아니라 "빠듯한 GPU에 학습을 욱여넣기"였다. 학습이 끝나면 4-bit 베이스는 버리고, 남기는 건 어댑터다.

4. 서빙할 때 — GGUF로 배포 (Q8_0, Q4_K_M)

학습이 끝난 모델은 배포·추론용으로 GGUF 포맷으로 바꿨다. GGUF는 llama.cpp·Ollama 계열에서 쓰는 포맷인데, 내보낼 때 원하는 정밀도를 지정하면 그 정밀도로 양자화해서 저장해준다.

# 서빙: GGUF로 내보내며 원하는 정밀도로 양자화
model.save_pretrained_gguf(
    "qwen3_filter_gguf", tokenizer,
    quantization_method="q8_0",   # 또는 "q4_k_m"
)

파일 이름에는 그 정밀도가 그대로 박힌다.

Q8_0     8-bit, 단순 양자화. 품질 손실이 거의 없음
Q4_K_M   4-bit K-quant. Q=비트수, K=블록별 스케일로 오차를 줄인 방식, M=중간 크기 변형

여기서 K-quant가 핵심이다. 그냥 4-bit로 일괄해서 누르면 손실이 크지만, 가중치를 블록으로 나눠 블록마다 스케일을 따로 두면(K-quant) 같은 4-bit라도 오차를 꽤 줄일 수 있다.

 

그래서 4-bit를 쓸 때도 보통 Q4_K_M 같은 K-quant 변형을 쓴다.

 

학습용 4-bit(QLoRA)와 서빙용 GGUF는 도구도 목적도 다르다는 걸 기억해두면 헷갈리지 않는다. 전자는 학습을 GPU에 올리려는 것이고, 후자는 추론을 가볍게 배포하려는 것이다.

5. 그래서 Q4냐 Q8이냐

서빙에서 실제로 고민한 건 정밀도였다. 더 낮게 누를수록 작아지지만 그만큼 정보가 깎인다.

 

 

Q8은 원본 대비 절반 크기에 품질은 거의 그대로다. Q4는 더 작아지지만 손실 위험이 따라온다. 특히 작은 모델일수록 Q4가 더 아프다. 파라미터가 적어 애초에 여유가 없는데 정밀도까지 깎으면, 같은 4-bit라도 큰 모델보다 출력이 더 쉽게 흔들린다. 큰 모델은 어느 정도 깎여도 버틸 여력이 있지만 작은 모델은 그렇지 못하다.

 

그래서 서빙 기본값은 Q8로 잡았다. 크기를 절반으로 줄이면서 정확도는 지키는, 가장 안전한 선이었다. Q4는 0.6B 같은 초소형 모델에서만, 크기를 최대한 줄여야 할 때 조심스럽게 썼다.

6. 실제로 고른 것

서빙용으로 변환해 둔 모델들의 크기는 이랬다.

모델     정밀도    크기       메모
0.6B     Q4_K_M    ~380 MB    초소형, 크기 최소화
0.8B     Q8_0      ~775 MB    주력 (정확도·크기 균형)
1.7B     Q8_0      ~1.75 GB   품질은 좋지만 무거움

결국 주력은 0.8B Q8이었다. 표준 평가셋 정확도가 가장 좋으면서 크기는 1GB 아래라, 온디바이스에 올리기에 균형이 가장 잘 맞았다. 1.7B는 품질이 비슷해도 두 배 이상 무거웠고, 0.6B Q4는 크기는 가장 작지만 품질이 떨어졌다.

7. 작다고 빠른 건 아니었다

여기서 한 가지 함정이 있었다. 가장 작게 누른 0.6B Q4가, 셋 중 가장 느렸다.

 

양자화는 모델을 작게 만들지만, 추론 속도는 크기만으로 정해지지 않는다. 실제 응답 시간은 "토큰을 몇 개나 생성하고 언제 멈추는가"에 더 크게 좌우됐다. 학습이 부족한 0.6B는 JSON을 다 뱉고도 멈추지 못하고 토큰을 계속 만들어내서, 크기가 가장 작은데도 응답이 제일 늦었다. (속도·정확도 수치 비교는 파인튜닝 결과 글실습 글에 있다.)

 

그러니 "양자화로 크기를 줄였다"가 곧 "빨라졌다"는 아니었다. 크기를 줄이는 것과, 원하는 형식으로 짧고 안정적으로 끝내는 것은 별개의 문제였다.

마치며

온디바이스에서 양자화는 사실상 필수였지만, 정리하고 보니 기준은 단순했다. 정밀도는 Q8을 기본 안전선으로 두고, Q4는 손실을 감수할 수 있는 초소형 모델에서만 쓴다.

 

학습에서는 4-bit로 GPU에 꾸겨넣고(QLoRA), 서빙에서는 GGUF로 가볍게 배포하되 정밀도는 품질이 버티는 선까지만 내린다.

 

크기를 줄였다고 끝이 아니라, 추론이 짧고 안정적으로 끝나는지까지 봐야 비로소 "온디바이스에 올릴 만하다"고 말할 수 있었다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함