이번에는 다른 PEFT 기법인 QLoRA를 공부해보겠다. 마찬가지 이 기술에 대한 자료는 모두 "QLORAEfficient Finetuning of Quantized LLMs" 논문의 자료를 참고 하였다.
LoRA가 메모리를 줄여주긴 했지만, 대규모 모델의 Fine-tuning은 여전히 어려운 상황이었습니다.
| 모델 | Full Fine-tuning 메모리 | 접근성 |
|---|---|---|
| LLaMA 7B | ~60GB | 전문 GPU 1개 |
| LLaMA 33B | ~200GB | 다중 GPU 서버 |
| LLaMA 65B | 780GB 이상 | 대규모 클러스터 필요 |
"양자화된 4-bit 모델을 Fine-tuning해도 16-bit Full Fine-tuning과 동일한 성능을 낼 수 있을까?"
신경망 가중치가 정규분포를 따른다는 사실을 활용:
가중치 분포: N(0, σ) → 정규화 → [-1, 1] 범위로 매핑
| 데이터 타입 | Pile Perplexity |
|---|---|
| Int4 | 34.34 |
| Float4 (E2M1) | 31.07 |
| Float4 (E3M0) | 29.48 |
| NF4 + DQ | 27.41 |
양자화 상수(quantization constant)도 메모리를 차지
양자화 상수를 한 번 더 양자화
1차 양자화: W (FP32 → NF4) with c₂ (FP32)
2차 양자화: c₂ (FP32 → FP8) with c₁ (FP32)
Gradient checkpointing 시 긴 시퀀스에서 메모리 스파이크 발생 → OOM
NVIDIA Unified Memory 활용
GPU (VRAM) ↔ CPU (RAM)
자동 페이징
# Forward pass
Y = X @ dequant(dequant(c₁, c₂), W_NF4) + X @ L₁ @ L₂
# ↑ 4-bit 베이스 모델 (frozen) ↑ LoRA adapter (trainable)
# Computation flow:
# 1. Storage: NF4 (4-bit)
# 2. Computation: BF16 (16-bit)
# 3. Gradient: LoRA 파라미터에만 계산
기존 LoRA는 Query, Value에만 적용했지만, QLoRA에서는 모든 linear layer에 적용해야 16-bit 성능 재현 가능
| 모델 | Full FT 메모리 | QLoRA 메모리 | 감소율 |
|---|---|---|---|
| LLaMA 7B | ~60GB | 5GB | 12x |
| LLaMA 33B | ~200GB | 21GB | 10x |
| LLaMA 65B | 780GB+ | 48GB | 16x |
핵심 성과:
| LLaMA | 7B | 13B | 33B | 65B | 평균 |
|---|---|---|---|---|---|
| BF16 | 38.4 | 47.2 | 57.7 | 61.8 | 53.0 |
| FP4 | 37.2 | 47.3 | 55.9 | 61.3 | 52.2 |
| NF4+DQ | 39.0 | 47.5 | 57.3 | 61.8 | 53.1 |
→ NF4+DQ가 BF16과 동등하거나 더 나은 성능
OASST1 데이터셋(9,209 샘플)으로 학습한 Guanaco 모델:
| 모델 | 파라미터 | 메모리 | ChatGPT 대비 성능 |
|---|---|---|---|
| GPT-4 | - | - | 114.5% |
| Guanaco 65B | 65B | 41GB | 99.3% |
| Guanaco 33B | 33B | 21GB | 97.8% |
| Vicuna 13B | 13B | 26GB | 94.9% |
| ChatGPT | - | - | 100% |
| Guanaco 7B | 7B | 5GB | 87.0% |
| Alpaca 13B | 13B | 10GB | 69.4% |
| 모델 | Elo | 메모리 |
|---|---|---|
| GPT-4 | 1176 | - |
| Guanaco 65B | 1023 | 41GB |
| Guanaco 33B | 1009 | 21GB |
| ChatGPT | 916 | - |
| 항목 | Guanaco 33B | Guanaco 65B |
|---|---|---|
| 학습 시간 | 12시간 미만 | 24시간 |
| GPU | 소비자 24GB | 전문가 48GB |
| 데이터셋 | 9,209 샘플 | 9,209 샘플 |
"데이터셋 품질이 크기보다 중요하다"
QLoRA는 4-bit NormalFloat 양자화, Double Quantization, Paged Optimizers를 결합하여 65B 모델을 단일 48GB GPU에서 학습하면서도 16-bit Full Fine-tuning과 동일한 성능을 달성하는 혁신적인 기법이다.
| 항목 | LoRA | QLoRA |
|---|---|---|
| 베이스 모델 | 16-bit (frozen) | 4-bit (frozen) |
| Adapter | 16-bit | 16-bit |
| 메모리 감소 | 3x | 16x |
| 65B 학습 | 다중 GPU 필요 | 단일 48GB GPU |
| 핵심 혁신 | Low-rank decomposition | NF4 + DQ + Paged Opt |
pip install torch transformers peft accelerate bitsandbytes datasets trl
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType,
)
from trl import SFTTrainer
from datasets import load_dataset
# 1. 4-bit 양자화 설정 (NF4 + Double Quantization)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 양자화 활성화
bnb_4bit_quant_type="nf4", # NormalFloat4 사용
bnb_4bit_use_double_quant=True, # Double Quantization 활성화
bnb_4bit_compute_dtype=torch.bfloat16 # 연산은 BF16으로
)
# 2. 모델 로드 (4-bit 양자화 적용)
model_name = "beomi/Llama-3-Open-Ko-8B" # 또는 원하는 모델
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 3. kbit 학습을 위한 모델 준비
model = prepare_model_for_kbit_training(model)
# 4. LoRA 설정 (모든 linear layer에 적용)
lora_config = LoraConfig(
r=16, # LoRA rank
lora_alpha=32, # 스케일링 팩터
lora_dropout=0.05, # 드롭아웃
bias="none",
task_type=TaskType.CAUSAL_LM,
target_modules=[ # 모든 linear layer에 적용 (QLoRA 권장)
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
]
)
# 5. LoRA 적용
model = get_peft_model(model, lora_config)
# 학습 가능한 파라미터 확인
model.print_trainable_parameters()
출력 예시:
trainable params: 41,943,040 || all params: 8,030,261,248 || trainable%: 0.5222
# 데이터셋 로드 (예: 한국어 instruction 데이터)
dataset = load_dataset("your_dataset_name", split="train")
# 또는 직접 데이터 생성
train_data = [
{"text": "<s>[INST] 서울의 인구는? [/INST] 서울의 인구는 약 950만 명입니다.</s>"},
{"text": "<s>[INST] 파이썬이란? [/INST] 파이썬은 간결하고 읽기 쉬운 프로그래밍 언어입니다.</s>"},
# ... 더 많은 데이터
]
from datasets import Dataset
dataset = Dataset.from_list(train_data)
# 학습 설정
training_args = TrainingArguments(
output_dir="./qlora_output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
weight_decay=0.001,
fp16=False,
bf16=True, # BF16 사용
logging_steps=10,
save_strategy="epoch",
optim="paged_adamw_32bit", # Paged Optimizer 사용
lr_scheduler_type="cosine",
warmup_ratio=0.03,
gradient_checkpointing=True, # 메모리 절약
max_grad_norm=0.3,
)
# SFTTrainer로 학습
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=512,
packing=False,
)
# 학습 시작
trainer.train()
# 모델 저장 (LoRA 가중치만)
trainer.model.save_pretrained("./qlora_adapter")
tokenizer.save_pretrained("./qlora_adapter")
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
import torch
# 1. 베이스 모델 로드 (4-bit)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
base_model = AutoModelForCausalLM.from_pretrained(
"beomi/Llama-3-Open-Ko-8B",
quantization_config=bnb_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("beomi/Llama-3-Open-Ko-8B")
# 2. LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./qlora_adapter")
# 3. 추론
def generate_response(prompt):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=256,
temperature=0.7,
top_p=0.9,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
return response
# 테스트
prompt = "[INST] 머신러닝과 딥러닝의 차이점을 설명해주세요. [/INST]"
response = generate_response(prompt)
print(response)
import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
def get_model_memory(model_name, load_in_4bit=False, load_in_8bit=False):
"""모델의 GPU 메모리 사용량 측정"""
torch.cuda.empty_cache()
if load_in_4bit:
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
elif load_in_8bit:
model = AutoModelForCausalLM.from_pretrained(
model_name,
load_in_8bit=True,
device_map="auto"
)
else:
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
memory_gb = torch.cuda.memory_allocated() / 1024**3
print(f"GPU Memory: {memory_gb:.2f} GB")
del model
torch.cuda.empty_cache()
return memory_gb
# 메모리 비교 (예: 7B 모델)
model_name = "beomi/Llama-3-Open-Ko-8B"
print("16-bit (FP16):")
mem_16bit = get_model_memory(model_name, load_in_4bit=False)
print("\n8-bit (Int8):")
mem_8bit = get_model_memory(model_name, load_in_8bit=True)
print("\n4-bit (NF4 + DQ):")
mem_4bit = get_model_memory(model_name, load_in_4bit=True)
print(f"\n메모리 절약: {mem_16bit/mem_4bit:.1f}x")
예상 출력:
16-bit (FP16):
GPU Memory: 14.5 GB
8-bit (Int8):
GPU Memory: 8.2 GB
4-bit (NF4 + DQ):
GPU Memory: 5.1 GB
메모리 절약: 2.8x
추론 속도 향상을 위해 LoRA 가중치를 베이스 모델에 병합:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
# 1. 16-bit로 베이스 모델 로드 (병합을 위해)
base_model = AutoModelForCausalLM.from_pretrained(
"beomi/Llama-3-Open-Ko-8B",
torch_dtype=torch.float16,
device_map="auto",
)
# 2. LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./qlora_adapter")
# 3. 가중치 병합
merged_model = model.merge_and_unload()
# 4. 병합된 모델 저장
merged_model.save_pretrained("./merged_model")
tokenizer = AutoTokenizer.from_pretrained("beomi/Llama-3-Open-Ko-8B")
tokenizer.save_pretrained("./merged_model")
print("모델 병합 완료!")
"""
QLoRA Fine-tuning 전체 파이프라인
"""
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset
# =====================
# 1. 설정
# =====================
MODEL_NAME = "beomi/Llama-3-Open-Ko-8B"
OUTPUT_DIR = "./qlora_output"
DATASET_NAME = "your_dataset" # 변경 필요
# =====================
# 2. 4-bit 양자화 설정
# =====================
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16
)
# =====================
# 3. 모델 & 토크나이저 로드
# =====================
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=bnb_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token
# =====================
# 4. QLoRA 설정
# =====================
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
]
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# =====================
# 5. 데이터셋 로드
# =====================
dataset = load_dataset(DATASET_NAME, split="train")
# =====================
# 6. 학습 설정
# =====================
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
bf16=True,
logging_steps=10,
save_strategy="epoch",
optim="paged_adamw_32bit",
gradient_checkpointing=True,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
)
# =====================
# 7. 학습
# =====================
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=512,
)
trainer.train()
# =====================
# 8. 저장
# =====================
trainer.model.save_pretrained(f"{OUTPUT_DIR}/final_adapter")
tokenizer.save_pretrained(f"{OUTPUT_DIR}/final_adapter")
print("QLoRA 학습 완료!")
| 설정 | 권장값 | 설명 |
|---|---|---|
bnb_4bit_quant_type | "nf4" | NormalFloat4 (최적 양자화) |
bnb_4bit_use_double_quant | True | Double Quantization |
bnb_4bit_compute_dtype | torch.bfloat16 | 연산 정밀도 |
lora_r | 8~64 | LoRA rank (성능에 큰 영향 없음) |
target_modules | 모든 linear | QLoRA는 모든 레이어에 적용 권장 |
optim | "paged_adamw_32bit" | Paged Optimizer |
gradient_checkpointing | True | 메모리 절약 |