2026년 4월 기준 LoRA/QLoRA 파인튜닝: “VRAM 한계”를 설계로 이기는 실전 튜토리얼
들어가며
LLM fine-tuning을 실제 프로젝트에 넣으려 하면, 대부분 “성능”보다 먼저 “학습 인프라/비용”에서 막힙니다. 특히 7B~14B급을 도메인 데이터로 SFT(Supervised Fine-Tuning) 하려는 순간 GPU VRAM, batch size, sequence length, optimizer state가 한꺼번에 터지죠. LoRA/QLoRA는 이 병목을 학습 가능한 파라미터를 극단적으로 줄이고(LoRA), 베이스 모델 가중치 메모리를 4-bit로 낮춰(QLoRA) “한 장의 GPU로도 가능한” 수준까지 끌어내리는 전형적인 해법입니다. (arxiv.org)
- 언제 쓰면 좋은가
- 도메인/사내 문서/고객 응대 톤 같은 특화가 필요하지만, full fine-tuning 인프라(멀티 GPU, ZeRO-3, 대규모 체크포인트 관리)를 깔기 어렵다.
- “베이스 모델은 유지 + adapter만 교체”하는 형태로 배포/롤백을 단순화하고 싶다.
- 같은 베이스 모델에 대해 여러 업무별 adapter를 운영(멀티 테넌시)하려 한다.
- 언제 쓰면 안 되는가
- 모델의 “지식 자체”를 바꾸는 수준(대규모 사전학습에 가까운 변화)이 필요하거나, 대규모 데이터로 전면 재학습이 필요하다(LoRA로는 한계가 빨리 온다).
- 극도로 낮은 latency/단일 바이너리 배포가 중요하고, adapter 합성/관리 오버헤드가 싫다(물론 merge로 완화 가능하지만 운영 복잡도는 남습니다).
- 규제/감사 관점에서 “학습 파이프라인”을 엄격히 고정해야 하는데, bitsandbytes/드라이버/CUDA 조합 변수가 부담이다.
🔧 핵심 개념
1) LoRA: “가중치 업데이트는 low-rank로 충분하다”
Transformer의 Linear 레이어 가중치 (W)를 전부 업데이트하지 않고, 업데이트 (\Delta W)를 저랭크 행렬곱으로 근사합니다.
- 원래: (W’ = W + \Delta W)
- LoRA: (\Delta W \approx B A) ( (A \in \mathbb{R}^{r \times d}), (B \in \mathbb{R}^{d \times r}), (r \ll d) )
즉 학습 파라미터는 (W) 전체가 아니라 A,B만입니다. 결과적으로:
- optimizer state / gradient 메모리 급감
- 여러 작업에 대해 adapter만 바꿔 끼우는 운영 가능
- target_modules(어느 레이어에 LoRA를 박을지) 선택이 성능/비용을 좌우 (huggingface.co)
2) QLoRA: “베이스는 4-bit로 고정, 학습은 LoRA로”
QLoRA는 LoRA 위에 “Quantization”을 얹습니다. 핵심은:
- 베이스 모델 가중치 (W) 는 4-bit(NF4)로 저장해 VRAM을 크게 절약
- forward 시에는 내부적으로 필요한 순간 dequantize하여 연산하고, 학습 가능한 것은 LoRA adapter 쪽 (arxiv.org)
QLoRA가 자주 언급되는 3요소: 1) NF4(4-bit NormalFloat): LLM weight 분포(대체로 정상분포에 가까움)에 맞춘 4-bit 포맷 (huggingface.co)
2) Double Quantization: 4-bit로 만들 때 필요한 스케일/상수까지 다시 quantize 해서 오버헤드 감소 (arxiv.org)
3) Paged Optimizers: 학습 중 순간적으로 튀는 메모리 피크를 unified memory로 완화 (arxiv.org)
요약하면 “학습 파라미터는 작게(LoRA), 베이스 메모리는 더 작게(4-bit)”가 QLoRA의 설계입니다.
3) LoRA vs QLoRA vs (대안들)
- LoRA(FP16/BF16 베이스): 베이스를 그대로 올리니 VRAM 요구량이 큼. 대신 단순하고 디버깅이 수월.
- QLoRA(4-bit 베이스): VRAM 효율 최강. 대신 bitsandbytes 커널/드라이버 조합, throughput 저하(특히 dequant overhead) 등 운영 변수가 늘어남. 최근엔 NF4 dequantization 자체를 빠르게 만드는 연구도 계속 나옵니다(“왜 느릴 수 있는지”를 이해하는 게 중요). (arxiv.org)
- DoRA 등 변형: LoRA류의 변형/개선들이 PEFT 생태계에 들어오고 있어, “내 태스크에서 LoRA가 부족한가?”를 판단하는 다음 옵션이 됩니다. (huggingface.co)
💻 실전 코드
아래는 현실적인 사내 시나리오(고객지원 티켓 → 표준 답변 스타일로 SFT) 기준입니다.
- 데이터:
jsonl(티켓/맥락/정답 템플릿) - 목표: Instruct 모델을 “우리 회사 톤 + 정책 준수”로 학습
- 학습: TRL
SFTTrainer+ PEFT LoRA + bitsandbytes 4-bit(QLoRA) - 전제: CUDA 머신(예: RTX 4090/5090, A100 등). CPU/MPS는 QLoRA 효율이 크게 떨어집니다.
1) 환경 셋업
1
2
3
4
5
6
7
8
# Python 3.10+ 권장 (프로젝트 고정)
python -m venv .venv
source .venv/bin/activate
pip install -U "torch" "transformers" "datasets" "accelerate" "peft" "trl" "bitsandbytes"
# (권장) 실험 로깅
pip install -U wandb
2) 데이터 포맷 예시 (support_tickets.jsonl)
운영에서 흔한 형태로: “맥락(context) + 정책(policy) + 답변(answer)”를 하나의 prompt로 합칩니다.
1
{"ticket_id":"INC-10293","context":"사용자가 환불을 요청했으나 사용 기간이 45일 경과.","policy":"환불은 결제 후 30일 이내만 가능. 예외: 장애로 인한 미사용이 증명되면 승인.","answer":"안녕하세요. 요청 주신 환불은 결제 후 30일 기준으로 처리되며, 현재는 45일이 경과하여 일반 환불은 어렵습니다. 다만 장애로 인한 미사용 증빙이 가능하시면 검토 후 예외 처리할 수 있어요. 장애 발생 시점/스크린샷/로그가 있다면 회신 부탁드립니다."}
3) 학습 코드 (train_qlora_sft.py)
- 포인트:
BitsAndBytesConfig로 4-bit NF4 + double quant 세팅 (huggingface.co)LoraConfig에서 target_modules를 모델 아키텍처에 맞게 선정(예: llama 계열은 q_proj/v_proj/k_proj/o_proj, MLP up/down/gate 등)- TRL
SFTTrainer로 SFT 루프 단순화 (huggingface.co)
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
MODEL_ID = os.environ.get("MODEL_ID", "meta-llama/Meta-Llama-3-8B-Instruct")
DATA_PATH = os.environ.get("DATA_PATH", "support_tickets.jsonl")
OUT_DIR = os.environ.get("OUT_DIR", "./ckpt-support-qlora")
def format_example(ex):
# 운영 프롬프트: "정책 준수 + 톤"을 강제
prompt = (
"### Role\n"
"You are a customer support agent. Follow company policy strictly.\n\n"
"### Policy\n"
f"{ex['policy']}\n\n"
"### Ticket Context\n"
f"{ex['context']}\n\n"
"### Task\n"
"Write a helpful, concise answer in Korean. If policy blocks the request, explain and provide next steps.\n\n"
"### Answer\n"
f"{ex['answer']}"
)
return {"text": prompt}
def main():
# 4-bit QLoRA 설정 (NF4 + double quant 권장)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float16,
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
)
model.config.use_cache = False # training 시 권장(gradient checkpointing과 충돌 방지)
ds = load_dataset("json", data_files=DATA_PATH, split="train")
ds = ds.map(format_example, remove_columns=ds.column_names)
# LoRA 설정: target_modules는 모델에 맞게 조정
lora = 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", "up_proj", "down_proj", "gate_proj"],
)
cfg = SFTConfig(
output_dir=OUT_DIR,
max_seq_length=2048,
packing=True, # 여러 샘플을 한 시퀀스로 packing(throughput 개선)
per_device_train_batch_size=1,
gradient_accumulation_steps=16,
learning_rate=2e-4,
num_train_epochs=2,
logging_steps=10,
save_steps=200,
bf16=True,
gradient_checkpointing=True,
optim="paged_adamw_8bit", # QLoRA에서 흔히 쓰는 paged optimizer 계열 ([arxiv.org](https://arxiv.org/abs/2305.14314?utm_source=openai))
warmup_ratio=0.03,
lr_scheduler_type="cosine",
report_to="none",
)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=ds,
peft_config=lora,
args=cfg,
dataset_text_field="text",
)
trainer.train()
trainer.save_model(OUT_DIR)
# 예상 산출물:
# OUT_DIR/
# adapter_model.safetensors (또는 유사 파일)
# adapter_config.json
# tokenizer files (옵션)
print(f"Saved QLoRA adapter to: {OUT_DIR}")
if __name__ == "__main__":
main()
예상 출력(요약)
- 10 step마다 loss 로그
ckpt-support-qlora/에 adapter 저장 (베이스 모델 전체가 아니라 LoRA adapter 중심)
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) “어디에 LoRA를 꽂을지”를 실험 단위로 관리
- 성능이 안 나오면 무조건 r을 올리기 전에
target_modules부터 점검하세요. Attention의q_proj/v_proj만으로도 충분한 태스크가 있고, 도메인 문체/규칙 준수는 MLP 모듈까지 포함해야 안정적으로 잡히는 경우가 많습니다. - 프로젝트 관점에선 “adapter 실험 매트릭스”를 만들어 (모듈 범위 / r / seq_len / 데이터 품질) 우선순위를 두는 게 비용을 줄입니다.
2) 데이터는 “정답”보다 “정책/제약”을 명시적으로 프롬프트에 넣기
- SFT는 결국 next-token prediction이라, 제약이 prompt에 안정적으로 박혀야 “거부/대안 제시” 같은 패턴이 재현됩니다.
- 특히 CS/헬프데스크는 “불가 안내 + 다음 스텝”이 중요해서, 답변 템플릿을 일정하게 만드는 게 실제 지표(재문의율/처리시간)에 직결됩니다.
3) 평가는 학습 loss가 아니라 “운영 케이스 리그레션”으로
- LoRA/QLoRA는 빠르게 과적합할 수 있습니다(작은 데이터 + 큰 모델). 운영 티켓을 유형별(환불/계정/장애/약관)로 나눠 고정된 evaluation set을 만들고, “정책 위반률/환각률” 같은 실패 지표를 먼저 보세요.
흔한 함정/안티패턴
- (함정) 4-bit 로딩만 하고 QLoRA라고 생각하기: 핵심은 “베이스는 quantized + 학습은 adapter” 조합입니다. 베이스를 4-bit로 올렸는데도 target_modules를 과도하게 늘리거나 seq_len을 무리하게 키우면 결국 OOM 납니다.
- (안티패턴) LR을 너무 낮게: full fine-tuning 감각으로
1e-5같은 LR을 쓰면 LoRA는 학습이 거의 안 됩니다. LoRA는 상대적으로 큰 LR(예: 1e-4~2e-4)에서 잘 움직이는 경우가 많습니다(단, 데이터가 작으면 epoch/early stopping이 더 중요). - (함정) “훈련은 되는데 추론이 느림/불안정”: NF4는 메모리를 줄이지만 dequantize 비용이 있고, 커널/드라이버 조합에 따라 체감이 달라질 수 있습니다. 최근에도 NF4 dequantization을 가속하는 연구가 나오는 배경이 이 포인트입니다. (arxiv.org)
비용/성능/안정성 트레이드오프
- 최저 비용/최저 VRAM: QLoRA(4-bit NF4 + double quant) + 작은 r + 짧은 seq_len
- 성능 우선: LoRA(FP16/BF16 베이스) 또는 QLoRA에서 r/모듈 범위를 늘리되, 데이터 품질/평가 체계를 먼저 강화
- 운영 안정성 우선: 베이스를 고정하고 adapter만 배포(버전 관리 단순). 다만 “어떤 adapter가 어떤 정책/데이터로 학습됐는지” 메타데이터를 남겨야 사고가 줄어듭니다.
🚀 마무리
LoRA는 “학습 파라미터를 줄여서” fine-tuning을 실무 가능하게 만들고, QLoRA는 거기에 “베이스 모델 메모리까지 줄여서” 단일 GPU/제한된 예산에서도 7B~8B급 특화를 현실화합니다. 핵심은 기술 자체보다 내 제약(예산/VRAM/배포 방식/평가 체계)에 맞춰 설계를 선택하는 겁니다. (arxiv.org)
도입 판단 기준(현실 체크리스트):
- “베이스 모델을 바꾸지 않고” 도메인 톤/정책/형식을 맞추는 게 목표인가? → LoRA/QLoRA 적합
- 1장 GPU에서 학습해야 하는가, seq_len이 길어야 하는가? → QLoRA 우선 검토
- 운영에서 adapter 버전이 늘어날 때 추적/검증 프로세스가 있는가? → 없으면 먼저 MLOps 쪽부터 정리
다음 학습 추천:
- Hugging Face PEFT LoRA 가이드에서 target_modules/고급 옵션(DoRA, quantization 연계)을 훑고, (huggingface.co)
- Transformers bitsandbytes quantization 문서에서 NF4/double quant/compute dtype를 정확히 고정해 재현성을 올리며, (huggingface.co)
- TRL SFTTrainer 문서 기준으로 packing, max_seq_length, 데이터 필드 설계를 표준화하세요. (huggingface.co)
원하시면, (1) 사용하려는 베이스 모델(예: Llama/Qwen/Gemma), (2) GPU/VRAM, (3) 데이터 크기/평균 길이, (4) 목표 지표(정확도 vs 정책위반률 vs 톤 일관성)를 알려주시면 위 코드를 당신 환경에 맞는 “권장 config(r/seq_len/모듈/optimizer)”로 구체적으로 튜닝해 드리겠습니다.