4비트 양자화와 LoRA를 결합한 QLoRA의 원리를 이해하고, 단일 소비자 GPU에서 대규모 모델을 파인튜닝하는 실전 방법을 다룹니다.
2023년 워싱턴 대학교 연구팀이 발표한 QLoRA(Quantized Low-Rank Adaptation)는 파인튜닝의 접근성을 혁신적으로 높였습니다. 70억 파라미터 모델의 파인튜닝에 필요한 GPU 메모리를 56GB에서 12GB 수준으로 줄여, 단일 소비자용 GPU에서도 대규모 모델의 파인튜닝을 가능하게 했습니다.
QLoRA의 핵심은 세 가지 기술의 결합에 있습니다. 4비트 NormalFloat(NF4) 양자화, 이중 양자화(Double Quantization), 그리고 페이지드 옵티마이저(Paged Optimizers)입니다. 이들을 LoRA와 결합하여 메모리 효율과 성능을 동시에 달성합니다.
메모리 사용량 비교 (7B 모델 기준):
방법 GPU 메모리 학습 가능
Full Fine-Tuning ~56 GB A100 80GB 필요
LoRA (FP16) ~18 GB A100 40GB / A6000
QLoRA (NF4) ~12 GB RTX 4070 Ti
QLoRA + GC ~8 GB RTX 4060 Ti 16GB
GC = Gradient Checkpointing양자화(Quantization)는 모델 가중치의 수치 정밀도를 낮추어 메모리 사용량을 줄이는 기법입니다. 원래 32비트(FP32)나 16비트(FP16/BF16) 부동소수점으로 저장되는 가중치를 8비트, 4비트, 심지어 2비트로 변환합니다.
데이터 타입별 메모리 사용량 (파라미터당):
FP32: 4 바이트 (32비트, 전체 정밀도)
FP16: 2 바이트 (16비트, 반정밀도)
BF16: 2 바이트 (16비트, Brain Float)
INT8: 1 바이트 (8비트 정수)
NF4: 0.5 바이트 (4비트, NormalFloat)
7B 모델의 가중치 크기:
FP32: 28 GB
FP16: 14 GB
INT8: 7 GB
NF4: 3.5 GBQLoRA에서 사용하는 NF4는 일반적인 4비트 정수 양자화와 다릅니다. 사전 학습된 모델의 가중치가 정규 분포(Normal Distribution)를 따른다는 관찰에 기반하여, 정규 분포에 최적화된 양자화 방식을 설계했습니다.
일반 INT4 양자화:
16개의 양자화 레벨을 균등하게 분포
[-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7]
문제: 대부분의 가중치는 0 근처에 집중되어 있으므로
극단값 영역의 레벨이 낭비됨
NF4 양자화:
16개의 양자화 레벨을 정규 분포에 맞게 불균등 분포
0 근처에 더 많은 레벨을 배치하여 정밀도 극대화
결과: 동일한 4비트로도 더 높은 정밀도 달성양자화에는 양자화 상수(Quantization Constants)가 필요합니다. 이 상수 자체도 메모리를 차지하는데, 이중 양자화는 이 상수를 다시 한번 양자화하여 메모리를 추가로 절약합니다.
단일 양자화:
가중치 블록 (64개) + 양자화 상수 (FP32, 4바이트)
상수 오버헤드: 4바이트 / 64 = 파라미터당 0.0625바이트
이중 양자화:
양자화 상수를 다시 8비트로 양자화
상수 오버헤드: 1바이트 / 64 + 4바이트 / (64*256)
= 파라미터당 약 0.016바이트
절약량: 7B 모델 기준 약 0.37 GB 추가 절약import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig
)
# 4비트 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4비트 양자화 활성화
bnb_4bit_quant_type="nf4", # NF4 양자화 사용
bnb_4bit_compute_dtype=torch.bfloat16, # 연산 시 BF16 사용
bnb_4bit_use_double_quant=True, # 이중 양자화 활성화
)
# 모델 로드 (4비트 양자화 적용)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb_config,
device_map="auto",
attn_implementation="flash_attention_2", # Flash Attention 사용
)
tokenizer = AutoTokenizer.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct"
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_tokenbnb_4bit_compute_dtype은 실제 연산에 사용되는 데이터 타입입니다. 가중치는 4비트로 저장되지만, 순전파와 역전파 시에는 이 타입으로 역양자화(Dequantize)되어 연산됩니다. BF16이 FP16보다 수치 안정성이 높아 권장됩니다.
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# 양자화된 모델에 대한 학습 준비
model = prepare_model_for_kbit_training(
model,
use_gradient_checkpointing=True # 메모리 절약
)
# LoRA 설정
lora_config = LoraConfig(
r=32,
lora_alpha=64,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
# LoRA 적용
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 167,772,160 || all params: 8,281,985,024
# || trainable%: 2.0258from transformers import TrainingArguments
from trl import SFTTrainer
from datasets import load_dataset
# 데이터셋 로드
dataset = load_dataset("json", data_files={
"train": "data/train.jsonl",
"validation": "data/val.jsonl"
})
def formatting_func(example):
"""대화 형식 데이터를 텍스트로 변환"""
return tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False
)
# 학습 설정
training_args = TrainingArguments(
output_dir="./qlora-output",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 실효 배치 크기 = 16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.1,
weight_decay=0.01,
logging_steps=10,
save_strategy="steps",
save_steps=200,
eval_strategy="steps",
eval_steps=200,
save_total_limit=3,
bf16=True, # BF16 혼합 정밀도
gradient_checkpointing=True, # 메모리 절약
gradient_checkpointing_kwargs={
"use_reentrant": False
},
optim="paged_adamw_8bit", # 페이지드 옵티마이저
max_grad_norm=0.3,
report_to="wandb",
run_name="qlora-llama3-8b",
)
# 트레이너 설정
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
eval_dataset=dataset["validation"],
tokenizer=tokenizer,
formatting_func=formatting_func,
max_seq_length=2048,
packing=True, # 시퀀스 패킹 활성화
)
# 학습 시작
trainer.train()
# 어댑터 저장
trainer.save_model("./qlora-adapter")QLoRA만으로도 메모리가 부족한 경우 추가 최적화 기법을 적용할 수 있습니다.
순전파 시 중간 활성화 값을 저장하지 않고, 역전파 시 필요할 때 재계산하는 방법입니다. 메모리를 절약하는 대신 약 30%의 학습 시간이 추가됩니다.
일반 학습:
순전파: 모든 레이어의 활성화 값 저장 (메모리 O(n))
역전파: 저장된 값 활용 (빠름)
Gradient Checkpointing:
순전파: 체크포인트 레이어만 저장 (메모리 O(sqrt(n)))
역전파: 필요한 활성화 값 재계산 (느림, 약 30% 추가)NVIDIA GPU의 통합 메모리(Unified Memory) 기능을 활용하여, GPU 메모리가 부족할 때 옵티마이저 상태를 CPU 메모리로 자동 이전합니다.
# 8비트 Adam + 페이지드 메모리
training_args = TrainingArguments(
optim="paged_adamw_8bit",
# 또는 "paged_adamw_32bit" (메모리는 더 쓰지만 안정적)
)어텐션 연산의 메모리 사용량을 O(n^2)에서 O(n)으로 줄이는 기법입니다. 특히 긴 시퀀스를 처리할 때 효과적입니다.
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
attn_implementation="flash_attention_2",
)Flash Attention 2는 Ampere 이상의 GPU(RTX 30xx, A100 등)에서만 지원됩니다. 구형 GPU에서는 sdpa(Scaled Dot-Product Attention)를 대안으로 사용할 수 있습니다.
사용 가능한 GPU에 따라 최적의 설정이 달라집니다. 주요 GPU별 권장 설정을 정리합니다.
GPU별 QLoRA 설정 가이드 (7B 모델 기준):
RTX 4060 Ti 16GB:
batch_size: 1
gradient_accumulation: 16
max_seq_length: 1024
gradient_checkpointing: True
packing: True
optim: paged_adamw_8bit
RTX 4070 Ti / 4080 (16GB):
batch_size: 2
gradient_accumulation: 8
max_seq_length: 2048
gradient_checkpointing: True
packing: True
optim: paged_adamw_8bit
RTX 4090 (24GB):
batch_size: 4
gradient_accumulation: 4
max_seq_length: 2048
gradient_checkpointing: 선택적
packing: True
optim: paged_adamw_8bit
A100 40GB:
batch_size: 8
gradient_accumulation: 2
max_seq_length: 4096
gradient_checkpointing: 불필요
packing: True
optim: adamw_torchGPU 메모리 부족(Out of Memory, OOM) 오류는 파인튜닝에서 가장 흔한 문제입니다. 단계별 대처 방법을 정리합니다.
OOM 발생 시 순차적 대처법:
1단계: batch_size를 1로 줄이고 gradient_accumulation을 늘림
2단계: gradient_checkpointing 활성화
3단계: max_seq_length 줄이기 (4096 -> 2048 -> 1024)
4단계: paged_adamw_8bit 옵티마이저 사용
5단계: LoRA rank 줄이기 (32 -> 16 -> 8)
6단계: target_modules 줄이기 (전체 -> Attention만)
7단계: 더 작은 베이스 모델 사용 고려# GPU 메모리 모니터링 함수
import torch
def print_gpu_memory():
"""현재 GPU 메모리 사용량 출력"""
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated() / 1024**3
reserved = torch.cuda.memory_reserved() / 1024**3
total = torch.cuda.get_device_properties(0).total_mem / 1024**3
print("GPU 메모리:")
print(" 할당됨: " + str(round(allocated, 2)) + " GB")
print(" 예약됨: " + str(round(reserved, 2)) + " GB")
print(" 전체: " + str(round(total, 2)) + " GB")
print(" 여유: " + str(round(total - reserved, 2)) + " GB")학습이 완료된 QLoRA 어댑터를 로드하여 추론하는 방법입니다.
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import PeftModel
import torch
# 베이스 모델을 4비트로 로드
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb_config,
device_map="auto",
)
# LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./qlora-adapter")
# 추론 모드로 전환
model.eval()프로덕션 환경에서는 양자화 어댑터를 FP16 모델로 병합하여 배포하는 것이 일반적입니다.
from transformers import AutoModelForCausalLM
from peft import PeftModel, AutoPeftModelForCausalLM
import torch
# FP16으로 베이스 모델 로드
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
torch_dtype=torch.float16,
device_map="auto",
)
# 어댑터 로드 및 병합
model = PeftModel.from_pretrained(base_model, "./qlora-adapter")
merged_model = model.merge_and_unload()
# FP16 모델로 저장
merged_model.save_pretrained("./merged-model-fp16")병합된 FP16 모델은 vLLM, TGI 등의 서빙 프레임워크에서 바로 사용할 수 있습니다. 서빙 시에는 서빙 프레임워크 자체의 양자화 기능(AWQ, GPTQ 등)을 적용하여 추론 성능을 최적화하는 것이 일반적입니다.
이번 장에서는 QLoRA를 사용한 메모리 효율적 파인튜닝을 실습했습니다.
다음 장에서는 학습 파이프라인의 전체적인 구축과 하이퍼파라미터 최적화 전략을 다룹니다. 학습률 스케줄링, 배치 크기 전략, 조기 종료 등 학습 효율을 극대화하는 기법을 체계적으로 안내합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
파인튜닝 학습 파이프라인의 전체 구조를 설계하고, 학습률, 배치 크기, 스케줄링 등 핵심 하이퍼파라미터를 최적화하는 전략을 다룹니다.
LoRA(Low-Rank Adaptation)의 수학적 원리를 이해하고, 타겟 레이어 선택부터 하이퍼파라미터 튜닝까지 실전 적용법을 다룹니다.
파인튜닝된 모델의 성능을 자동 메트릭, LLM 평가, 인간 평가를 통해 다각적으로 측정하고 벤치마킹하는 체계적인 방법을 다룹니다.