- Published on
LLM 파인튜닝 실전 — LoRA, QLoRA, PEFT로 나만의 모델 만들기
- Authors
- Name
- 들어가며
- Full Fine-tuning vs Parameter-Efficient Fine-tuning
- LoRA: Low-Rank Adaptation
- QLoRA: 4-bit 양자화 + LoRA
- 데이터 준비와 학습
- 하이퍼파라미터 튜닝 가이드
- 고급 기법
- 마무리
- 퀴즈

들어가며
7B, 13B, 70B 파라미터의 LLM을 처음부터 학습하는 것은 수십~수백 개의 GPU와 수백만 달러가 필요합니다. 하지만 파인튜닝을 활용하면 소비자급 GPU 1장으로도 나만의 특화 모델을 만들 수 있습니다.
이 글에서는 LoRA, QLoRA, PEFT 라이브러리를 활용한 실전 파인튜닝 방법을 다룹니다.
Full Fine-tuning vs Parameter-Efficient Fine-tuning
Full Fine-tuning의 문제
7B 모델을 Full Fine-tuning하려면:
- 모델 파라미터: 7B × 4 bytes (FP32) = 28GB
- Optimizer 상태: Adam은 파라미터의 2배 = 56GB
- Gradient: 파라미터와 동일 = 28GB
- 총 VRAM: 약 112GB 이상 필요
→ A100 80GB 1장으로도 부족합니다.
PEFT의 등장
Parameter-Efficient Fine-tuning(PEFT)는 전체 파라미터의 0.1~1%만 학습합니다:
| 방법 | 학습 파라미터 비율 | VRAM (7B 기준) |
|---|---|---|
| Full Fine-tuning | 100% | ~112GB |
| LoRA | ~0.1-1% | ~16GB |
| QLoRA | ~0.1-1% | ~6GB |
LoRA: Low-Rank Adaptation
수학적 원리
LoRA의 핵심 아이디어: 가중치 업데이트 행렬 ΔW는 저랭크(low-rank)다.
기존 선형 변환:
LoRA 적용:
여기서:
- : 원본 가중치 (freeze)
- : 저랭크 행렬 (학습)
- : 저랭크 행렬 (학습)
- : 랭크 (보통 4~64, 원본 차원 대비 매우 작음)
- : 스케일링 팩터
원본 W (4096 × 4096) = 16M 파라미터 [freeze]
LoRA:
A (r × 4096) + B (4096 × r) = r × 8192 파라미터
r=8일 경우: 65,536 파라미터 (0.4%)
코드로 구현하기
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer
# 1. 기본 모델 로드
model_name = "meta-llama/Llama-3.1-8B"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 2. LoRA 설정
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # 랭크
lora_alpha=32, # 스케일링 (보통 r의 2배)
lora_dropout=0.05, # 드롭아웃
target_modules=[ # LoRA를 적용할 모듈
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
],
bias="none"
)
# 3. PEFT 모델 생성
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 41,943,040 || all params: 8,072,204,288 || trainable%: 0.5194
target_modules 선택 가이드
# 모델의 모든 Linear 레이어 확인
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
print(name, module.in_features, module.out_features)
# 일반적인 선택:
# - Attention만: ["q_proj", "v_proj"] — 최소 VRAM
# - Attention 전체: ["q_proj", "k_proj", "v_proj", "o_proj"] — 권장
# - MLP 포함: 위 + ["gate_proj", "up_proj", "down_proj"] — 최대 성능
QLoRA: 4-bit 양자화 + LoRA
QLoRA가 특별한 이유
QLoRA는 3가지 혁신으로 소비자 GPU에서 대형 모델 파인튜닝을 가능하게 합니다:
- 4-bit NormalFloat(NF4): 정규분포 가중치에 최적화된 양자화
- Double Quantization: 양자화 상수 자체도 양자화하여 메모리 추가 절감
- Paged Optimizers: GPU 메모리 부족 시 CPU로 자동 페이징
구현
from transformers import BitsAndBytesConfig
import torch
# 4-bit 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat4
bnb_4bit_compute_dtype=torch.bfloat16, # 연산은 bf16으로
bnb_4bit_use_double_quant=True, # Double Quantization
)
# 양자화된 모델 로드
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
quantization_config=bnb_config,
device_map="auto"
)
# LoRA 적용 (QLoRA = 4bit 양자화 모델 + LoRA)
model = get_peft_model(model, lora_config)
VRAM 사용량 비교
| 모델 | Full FP16 | LoRA FP16 | QLoRA 4-bit |
|---|---|---|---|
| 7B | ~28GB | ~16GB | ~6GB |
| 13B | ~52GB | ~30GB | ~10GB |
| 70B | ~280GB | ~160GB | ~48GB |
QLoRA를 사용하면 RTX 3090/4090 (24GB) 하나로 13B 모델까지 파인튜닝할 수 있습니다.
데이터 준비와 학습
데이터셋 포맷
Instruction 파인튜닝을 위한 데이터 포맷:
from datasets import load_dataset
# Alpaca 스타일 데이터셋
dataset = load_dataset("json", data_files="train_data.json")
# 데이터 예시
# {
# "instruction": "다음 텍스트를 요약해주세요.",
# "input": "Kubernetes는 컨테이너화된 워크로드와 서비스를...",
# "output": "Kubernetes는 컨테이너 오케스트레이션 플랫폼입니다."
# }
# 프롬프트 템플릿 적용
def format_instruction(sample):
if sample["input"]:
text = f"""### Instruction:
{sample["instruction"]}
### Input:
{sample["input"]}
### Response:
{sample["output"]}"""
else:
text = f"""### Instruction:
{sample["instruction"]}
### Response:
{sample["output"]}"""
return {"text": text}
dataset = dataset.map(format_instruction)
SFTTrainer로 학습
from trl import SFTTrainer, SFTConfig
training_args = SFTConfig(
output_dir="./output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 효과적 배치 = 4 × 4 = 16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
max_seq_length=2048,
bf16=True,
logging_steps=10,
save_strategy="epoch",
optim="paged_adamw_8bit", # QLoRA용 페이징 옵티마이저
gradient_checkpointing=True, # VRAM 추가 절감
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
args=training_args,
)
trainer.train()
학습 후 모델 저장 및 병합
# LoRA 어댑터만 저장 (수십 MB)
model.save_pretrained("./lora-adapter")
# 나중에 어댑터 로드
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
model = PeftModel.from_pretrained(base_model, "./lora-adapter")
# 원본 모델과 어댑터 병합 (배포용)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged-model")
하이퍼파라미터 튜닝 가이드
LoRA 랭크(r) 선택
# 랭크별 특성
# r=4: 가장 적은 파라미터, 간단한 도메인 적응에 적합
# r=8: 일반적인 시작점
# r=16: 좋은 균형 (추천)
# r=32: 복잡한 태스크, 더 많은 VRAM
# r=64+: Full Fine-tuning에 가까운 성능, 그만큼 비효율
# 실험적으로, r=16 + alpha=32가 대부분의 경우에 잘 작동
학습률
# LoRA/QLoRA 학습률은 Full FT보다 높게 설정
# Full FT: 1e-5 ~ 5e-5
# LoRA: 1e-4 ~ 3e-4
# QLoRA: 2e-4 (일반적)
고급 기법
DoRA: Weight-Decomposed Low-Rank Adaptation
LoRA의 발전형으로, 가중치를 크기(magnitude)와 방향(direction)으로 분해합니다:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
use_dora=True, # DoRA 활성화
)
여러 LoRA 어댑터 조합
from peft import PeftModel
# 기본 모델에 여러 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./adapter-korean")
model.load_adapter("./adapter-code", adapter_name="code")
# 어댑터 전환
model.set_adapter("code")
# 또는 어댑터 가중치 조합
model.add_weighted_adapter(
adapters=["default", "code"],
weights=[0.7, 0.3],
adapter_name="merged"
)
마무리
LoRA와 QLoRA는 LLM 파인튜닝의 접근성을 혁명적으로 바꿨습니다. 소비자 GPU 1장으로 수십억 파라미터 모델을 커스터마이징할 수 있다는 것은 AI 민주화의 핵심입니다.
핵심 요약:
- LoRA: 저랭크 분해로 학습 파라미터를 0.1~1%로 줄임
- QLoRA: 4-bit 양자화로 VRAM을 추가로 ~4배 절감
- PEFT: Hugging Face 라이브러리로 몇 줄의 코드로 적용 가능
직접 데이터를 준비하고, 학습하고, 배포해보세요. 생각보다 훨씬 쉽습니다.
퀴즈
Q1: LoRA에서 랭크(r)가 의미하는 것은?
가중치 업데이트 행렬 ΔW를 분해할 때의 저차원 크기. r이 작을수록 학습 파라미터가 적고 VRAM 소비가 줄지만, 모델의 표현력도 제한됩니다.
Q2: 7B 모델을 Full Fine-tuning하려면 약 얼마의 VRAM이 필요한가?
약 112GB 이상. 모델 파라미터(28GB) + Optimizer 상태(56GB) + Gradient(28GB)이 필요합니다.
Q3: QLoRA의 3가지 핵심 혁신은?
- 4-bit NormalFloat(NF4) 양자화, 2) Double Quantization(양자화 상수의 양자화), 3) Paged Optimizers(GPU→CPU 자동 페이징)
Q4: LoRA의 lora_alpha 파라미터의 역할은?
LoRA 업데이트의 스케일링 팩터. 실제 스케일은 alpha/r로 계산되며, 보통 r의 2배로 설정합니다 (r=16이면 alpha=32).
Q5: QLoRA에서 사용하는 NF4 양자화가 일반 INT4보다 나은 이유는?
신경망 가중치가 대략 정규분포를 따르므로, 정규분포에 최적화된 NF4 양자화가 균일 분포를 가정하는 INT4보다 정보 손실이 적습니다.
Q6: LoRA 어댑터를 원본 모델과 병합(merge)하는 이유는?
추론 시 추가 연산 오버헤드를 없애기 위해. 병합하면 원본 모델과 동일한 구조가 되어 추론 속도가 어댑터 분리 상태보다 빠릅니다.
Q7: gradient_checkpointing=True의 효과는?
순전파의 중간 활성값을 메모리에 저장하지 않고, 역전파에서 재계산합니다. VRAM을 절약하지만 학습 시간은 약 20-30% 증가합니다.
Q8: LoRA의 target_modules 선택이 성능에 미치는 영향은?
더 많은 모듈에 LoRA를 적용할수록 성능은 향상되지만 VRAM과 학습 시간이 증가합니다. Attention의 q_proj, v_proj만 적용하는 것이 최소 설정이고, MLP까지 포함하면 최대 성능을 얻을 수 있습니다.