QLoRA
더 많은 작업
- Quantized Low-Rank Adaptation, QLoRA; QLoRA
- 사전 학습 모델을 4비트로 양자화해 고정하고, 그 위에 붙인 LoRA 어댑터만 16비트로 학습해 미세조정 메모리를 크게 줄이는 기법
QLoRA(Quantized Low-Rank Adaptation)는 2023년 5월 워싱턴 대학교의 팀 데트머스(Tim Dettmers) 등이 발표한 파라미터 효율적 미세조정 기법이다.[1] 기반 모델 가중치를 4비트 NormalFloat(NF4)로 저장해 얼려 두고, 기울기는 이 양자화된 가중치를 거쳐 LoRA 어댑터로만 역전파한다. 논문은 16비트 전체 미세조정에 GPU 메모리 780GB 넘게 필요한 650억 매개변수 모델을 48GB GPU 한 장에서 미세조정하면서도 16비트 미세조정 성능을 유지했다고 보고했다.
QLoRA는 세 가지 장치로 메모리를 줄인다. 정규 분포를 따르는 가중치에 맞춘 4비트 자료형 NF4, 양자화 상수까지 다시 양자화하는 이중 양자화(double quantization), 메모리가 순간적으로 넘칠 때 옵티마이저 상태를 CPU 메모리로 내보내는 페이지드 옵티마이저(paged optimizer)다. 구현은 bitsandbytes 라이브러리에 들어가 허깅 페이스 Transformers와 PEFT에서 설정 몇 줄로 쓸 수 있다.
NF4는 분위수 양자화(quantile quantization)에 바탕을 둔 자료형이다. 분위수 양자화는 양자화 구간마다 같은 수의 값이 들어가게 구간을 나누므로 정보 이론적으로 최적이지만, 텐서마다 분위수를 추정하는 비용이 크다. 그런데 사전 학습된 가중치는 대체로 평균 0의 정규 분포를 따르므로, 표준 정규 분포 의 분위수로 고정된 코드표를 한 번 만들어 두고 가중치를 그 범위로 정규화하면 된다.[1]
- 의 분위수 함수 로 개의 값을 정한다.
- 값을 로 정규화한다.
- 가중치 블록을 절대값 최대(absmax)로 나눠 에 맞춘 뒤 가장 가까운 코드로 바꾼다.
대칭으로 만들면 0을 정확히 표현할 수 없으므로, 음수 쪽 개와 양수 쪽 개 분위수를 따로 구해 합치고 겹치는 0 하나를 빼서, 0을 포함한 비대칭 16개 값을 만든다.[1] 실제 NF4 값은 다음과 같다.
-1.0000 -0.6962 -0.5251 -0.3949 -0.2844 -0.1848 -0.0911 0.0000 0.0796 0.1609 0.2461 0.3379 0.4407 0.5626 0.7230 1.0000
bitsandbytes는 이 표를 create_normal_map(offset=0.9677083)으로 만든다.[2] 같은 계산과 블록 양자화를 넘파이로 재현하면 다음과 같다(SciPy 1.15에서 실행해 논문 부록의 값과 일치함을 확인).
import numpy as np
from scipy.stats import norm
def nf4_levels(offset=0.9677083):
pos = norm.ppf(np.linspace(offset, 0.5, 9)[:-1]) # 양수 쪽 8개
neg = -norm.ppf(np.linspace(offset, 0.5, 8)[:-1]) # 음수 쪽 7개
v = np.sort(np.concatenate([pos, [0.0], neg])) # 0을 정확히 포함한 16개
return v / np.abs(v).max() # [-1, 1]로 정규화
def quantize_blockwise(w, levels, block=64):
w = w.reshape(-1, block)
absmax = np.abs(w).max(axis=1, keepdims=True) # 블록마다 양자화 상수 1개
idx = np.abs((w / absmax)[..., None] - levels).argmin(-1) # 가장 가까운 4비트 코드
return idx.astype(np.uint8), absmax
def dequantize_blockwise(idx, absmax, levels):
return levels[idx] * absmax
levels = nf4_levels()
w = np.random.default_rng(0).normal(0, 0.02, size=4096 * 64)
idx, c = quantize_blockwise(w, levels)
err = np.abs(dequantize_blockwise(idx, c, levels).ravel() - w).mean()
# 이 예에서 평균 절대 오차는 약 1.46e-3. 같은 방식의 균등 간격 4비트는 약 1.71e-3
논문은 같은 조건에서 NF4가 4비트 정수(Int4)와 4비트 부동소수점(FP4)보다 낫고, FP4로 QLoRA를 하면 16비트 LoRA보다 MMLU가 약 1%p 낮았지만 NF4는 16비트 성능을 재현했다고 보고했다.[1]
블록 양자화는 블록마다 양자화 상수(absmax)를 하나씩 저장해야 한다. 블록 크기 64에 32비트 상수를 쓰면 매개변수당 평균 비트가 추가된다. 이중 양자화는 이 상수들을 다시 8비트 부동소수점, 블록 크기 256으로 양자화한다.[1]
매개변수당 약 0.373비트, 650억 모델 기준 약 3GB를 아낀다. 상수는 모두 양수이므로 평균을 빼서 0 중심으로 만든 뒤 대칭 양자화한다.
긴 시퀀스가 든 미니배치를 처리할 때 그래디언트 체크포인팅 등으로 메모리 사용량이 순간적으로 치솟아 메모리 부족 오류가 날 수 있다. 페이지드 옵티마이저는 NVIDIA 통합 메모리(unified memory)로 옵티마이저 상태를 할당해, GPU 메모리가 모자라면 자동으로 CPU 메모리로 내보냈다가 옵티마이저 갱신 때 다시 불러온다. 논문은 배치 크기 16에서 650억 모델을 48GB GPU로 학습할 때 일반 옵티마이저와 속도가 같았다고 보고했다.[1] 8비트 옵티마이저 상태 자체는 같은 저자의 블록 단위 양자화 연구에서 나왔다.[3]
| 항목 | 내용[1] |
|---|---|
| 메모리 | 650억 모델 미세조정 메모리를 780GB 초과에서 48GB 미만으로 |
| 성능 | GLUE, Super-NaturalInstructions, MMLU에서 16비트 전체 미세조정, 16비트 LoRA와 같은 수준 |
| 과나코(Guanaco) | OASST1 데이터 9,209개로 학습한 모델군. 650억 모델은 GPU 한 장으로 24시간 학습해 Vicuna 벤치마크에서 ChatGPT 대비 99.3%(GPT-4 판정). 4비트 모델 메모리 41GB |
| 소형 모델 | 70억 과나코는 메모리 5GB로, 26GB인 알파카 모델보다 Vicuna 벤치마크에서 20%p 넘게 높음 |
| 학습 규모 | 8개 지시 데이터셋, 여러 모델 계열과 크기로 1,000개 넘는 모델을 학습 |
논문은 몇 가지 실무 교훈도 남겼다. 기존 관행처럼 쿼리와 값 투영에만 LoRA를 붙이면 16비트 전체 미세조정 성능을 재현하지 못했고, 모든 선형층에 붙이는 것이 결정적이었다. 모든 층에 붙이면 랭크 는 성능과 거의 무관했다. 데이터는 양보다 질과 과제 적합성이 중요해, 챗봇 성능에서는 9천 개 남짓한 OASST1이 45만 개를 뽑아 쓴 FLAN v2보다 나았다.[1]
2026년 10월 기준 Transformers 5.20, PEFT 0.21.2, bitsandbytes 0.50.2, TRL 1.16.0으로 확인한 설정이다. BitsAndBytesConfig의 기본값은 FP4, 이중 양자화 끔, 계산 자료형 FP32이므로 QLoRA 설정은 직접 켜야 한다.[4][5]
# pip install "transformers==5.20.0" "peft==0.21.2" "bitsandbytes==0.50.2" "trl==1.16.0"
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, prepare_model_for_kbit_training
from trl import SFTConfig, SFTTrainer
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # 기본값은 "fp4"
bnb_4bit_use_double_quant=True, # 이중 양자화
bnb_4bit_compute_dtype=torch.bfloat16, # 계산 자료형
)
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", quantization_config=bnb)
model = prepare_model_for_kbit_training(model) # 층 정규화를 FP32로, 기반 가중치 고정, 체크포인팅 켜기
lora = LoraConfig(
r=16, lora_alpha=32, lora_dropout=0.05,
target_modules="all-linear", # 모든 선형층에 어댑터(QLoRA 논문의 교훈)
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir="qwen3-8b-qlora",
optim="paged_adamw_8bit", # 페이지드 8비트 AdamW
gradient_checkpointing=True,
learning_rate=2e-4,
bf16=True,
)
trainer = SFTTrainer(
model=model, args=args, peft_config=lora,
train_dataset=load_dataset("trl-lib/Capybara", split="train"),
)
trainer.train()
Transformers의 optim 값에는 paged_adamw_32bit, paged_adamw_8bit, paged_lion_8bit 등이 있다.[5] 학습률 는 QLoRA 논문이 70억·130억 모델에 쓴 값이다.[1]
- 속도: 매 순전파·역전파마다 역양자화가 들어가므로 16비트 LoRA보다 계산이 늘어난다. 메모리를 아끼는 대신 시간을 쓰는 거래다.
- 어댑터 병합: 학습한 어댑터는 16비트이고 기반 모델은 4비트라 그대로 합칠 수 없다. 배포용 단일 모델이 필요하면 16비트 기반 모델을 불러와 어댑터를 병합하고, 필요하면 그 결과를 다시 양자화하는 방법이 있다. 이때 어댑터는 양자화된 가중치 기준으로 학습되었다는 점을 감안해야 한다.
- 양자화 오차와 초기화: 어댑터는 양자화된 가중치를 기준으로 학습되므로 양자화 오차를 안고 시작한다. LoftQ는 양자화 오차를 최소화하도록 LoRA 가중치를 초기화하며, PEFT는 이를 쓸 때 NF4와
target_modules="all-linear"를 권한다.[4] - 추론용 양자화와의 관계: QLoRA는 학습 중 메모리를 줄이는 기법이다. 학습이 끝난 모델을 배포용으로 압축하는 사후 양자화는 GPTQ, AWQ 문서를 참고한다.
- ↑ 1.00 1.01 1.02 1.03 1.04 1.05 1.06 1.07 1.08 1.09 1.10 Tim Dettmers, Artidoro Pagnoni, Ari Holtzman, Luke Zettlemoyer, 〈QLoRA: Efficient Finetuning of Quantized LLMs〉, NeurIPS 2023. arXiv:2305.14314
- ↑ bitsandbytes/functional.py, bitsandbytes 0.50.2.
- ↑ Tim Dettmers 외, 〈8-bit Optimizers via Block-wise Quantization〉, ICLR 2022. arXiv:2110.02861
- ↑ 4.0 4.1 Quantization, 허깅 페이스 PEFT 문서(v0.21.2).
- ↑ 5.0 5.1 quantization_config.py와 training_args.py, 허깅 페이스 Transformers v5.20.0.