LoRA vs QLoRA, 2026년 6월 기준 “내 GPU/데이터/품질 목표”에 맞춰 고르는 실전 파인튜닝 튜토리얼
들어가며
LLM을 내 도메인(사내 용어, 고객 질의 패턴, 스타일 가이드, 특정 포맷 출력)에 맞추려면 결국 fine-tuning이 가장 강력합니다. 문제는 비용(시간/VRAM)과 운영 복잡도죠. “그냥 full fine-tuning”은 품질은 좋을 수 있지만 GPU 메모리/학습 비용이 커서 팀 단위 운영에 잘 안 맞습니다.
그래서 2026년에도 현업에서 가장 많이 쓰는 조합이 PEFT(Parameter-Efficient Fine-Tuning), 그중에서도 LoRA / QLoRA입니다. 핵심은 “모델 전체를 바꾸지 말고, 일부(어댑터)만 학습해서 비용을 줄이자”는 전략입니다. Hugging Face 생태계 기준으로도 LoRA 설정(예: target_modules="all-linear")과 4-bit 로딩/양자화 옵션(NF4, double quantization)이 정리되어 있고, TRL의 SFTTrainer로 SFT 파이프라인을 빠르게 구성할 수 있습니다. (huggingface.co)
언제 쓰면 좋나
- 도메인 특화 응답 품질/형식이 중요하고, 프롬프트로만은 한계가 있을 때
- 내부 지식이 아니라도(=RAG로 대체 불가하더라도) 말투/구조화 출력/툴 호출 패턴을 안정화하고 싶을 때
- 단일 GPU(예: 12–24GB)에서 현실적으로 학습해야 할 때(특히 QLoRA) (unsloth.ai)
언제 쓰면 안 되나
- 최신 지식/사내 문서 반영이 목표라면: 먼저 RAG/검색 인덱싱이 우선(파인튜닝은 지식을 “고정”시키는 성격)
- 데이터가 너무 적거나(수백 샘플) 라벨 품질이 낮으면: 모델이 스타일만 과하게 따라가거나 환각이 늘 수 있음
- 배포에서 “어댑터+베이스” 조합을 관리하기 싫고, 단일 체크포인트만 원한다면: 병합(merge) 전략/정밀도 손실까지 고려해야 함
🔧 핵심 개념
1) LoRA: “가중치 업데이트를 저랭크로 근사”
LoRA는 원래의 weight (W)를 학습하지 않고, 업데이트 (\Delta W)를 저랭크 행렬 곱 (BA)로 표현합니다.
- 원래: (W \leftarrow W + \Delta W)
- LoRA: (\Delta W \approx B A) (rank = r, 보통 8~64)
즉 학습 파라미터 수가 크게 줄고(특히 attention/MLP의 linear projection에 적용), 속도/메모리/저장 공간이 절약됩니다. PEFT의 LoraConfig에서 r, lora_alpha, target_modules 등으로 이를 제어합니다. (huggingface.co)
중요 포인트(현업 기준): target_modules를 대충 고르면 성능이 흔들립니다. PEFT 쪽 문서에서는 target_modules="all-linear"가 대부분의 경우 좋은 기본값이 될 수 있다고 안내합니다. (github.com)
2) QLoRA: “베이스는 4-bit로 얼리고, LoRA는 고정밀로 학습”
QLoRA는 LoRA에 4-bit quantization을 결합해, 베이스 모델을 4-bit로 로딩(메모리 절감)하고, 학습은 LoRA 어댑터만 진행합니다.
QLoRA 논문/정리에서 반복해서 등장하는 3요소:
- NF4(4-bit NormalFloat): LLM weight 분포(대체로 정규분포 근처)에 맞춘 4-bit 데이터 타입 (arxiv.org)
- Double Quantization: 양자화에 필요한 “양자화 상수(스케일 등)” 자체도 다시 양자화해서 오버헤드를 줄임 (arxiv.org)
- Paged Optimizers: 학습 중 순간적인 메모리 스파이크를 완화하기 위한 기법(주로 메모리 안정성 측면) (arxiv.org)
Hugging Face Transformers에서도 bitsandbytes 기반 4-bit 설정을 BitsAndBytesConfig로 노출하며, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True 같은 옵션 조합이 사실상 표준이 됐습니다. (huggingface.co)
3) 다른 접근과의 차이점(의사결정 관점)
- Full fine-tuning: 최고 비용/최대 자유도. 데이터가 크고 예산이 있으면 강력하지만 운영 부담 큼.
- LoRA(16-bit 베이스): 베이스는 fp16/bf16로 로딩 → VRAM 여유가 있으면 더 단순.
- QLoRA(4-bit 베이스): 베이스를 4-bit로 로딩 → 단일 GPU 환경에서 “현실적으로” 8B~급을 만지게 해줌. 대신 양자화/커널/드라이버 조합 이슈가 늘 수 있음.
💻 실전 코드
아래 예제는 “사내 고객지원 티켓 → 표준 답변 생성” 같은 현실적인 SFT를 가정합니다.
- 데이터:
question,policy,answer컬럼(내부 정책과 답변이 같이 들어감) - 목적: 답변을 정해진 형식(JSON) 으로 안정적으로 출력
- 학습: TRL
SFTTrainer+ PEFT LoRA + (선택) bitsandbytes 4-bit(=QLoRA)
1) 초기 셋업 (의존성/환경)
1
2
3
4
5
# Python 3.11+ 권장 (CUDA/드라이버는 환경에 맞게)
pip install -U "transformers>=4.49" "trl>=0.21" "peft" "accelerate" "datasets" "bitsandbytes"
# (권장) 로그인/캐시 설정은 환경에 맞게
# huggingface-cli login
2) QLoRA로 SFT 실행 코드 (프로덕션에 가까운 뼈대)
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
import os
import torch
from datasets import load_dataset
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig,
)
from peft import LoraConfig
from trl import SFTTrainer, SFTConfig
MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen3-4B-Instruct") # 예시
DATA_PATH = os.getenv("DATA_PATH", "data/helpdesk_sft.jsonl")
# 1) 데이터 로딩: jsonl 각 라인에 question/policy/answer가 있다고 가정
ds = load_dataset("json", data_files=DATA_PATH, split="train")
def format_example(ex):
# “toy”가 아니라 실제 운영에서 쓰는: 정책+질문+출력스키마를 명시
return (
"### System\n"
"You are a customer support assistant. Follow the company policy strictly.\n"
"Return ONLY valid JSON.\n\n"
"### Policy\n"
f"{ex['policy']}\n\n"
"### User\n"
f"{ex['question']}\n\n"
"### Assistant\n"
"{\n"
' "final_answer": ' # 모델이 여기부터 JSON을 채우도록 유도
)
# 2) Tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, use_fast=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 3) QLoRA: 4-bit 베이스 로딩 설정 (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.float32,
)
# 4) 베이스 모델 로딩(quantized) + FlashAttention 등은 모델/환경별로 조정
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
torch_dtype=torch.bfloat16,
)
# 5) LoRA 설정: 일단 "all-linear"로 시작하는 전략이 요즘 가장 안전한 편
# (모델 아키텍처별로 q_proj/v_proj 등으로 좁히는 튜닝은 이후 단계)
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules="all-linear",
bias="none",
task_type="CAUSAL_LM",
)
# 6) 학습 설정
# packing=True: 여러 샘플을 한 시퀀스로 pack해서 padding 낭비를 줄임(데이터가 짧을수록 효과 큼)
args = SFTConfig(
output_dir="outputs/helpdesk-qlora",
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=2e-4,
num_train_epochs=1,
logging_steps=10,
save_steps=200,
save_total_limit=2,
max_seq_length=2048,
packing=True,
bf16=torch.cuda.is_available(),
gradient_checkpointing=True,
optim="paged_adamw_8bit", # QLoRA에서 흔히 쓰는 조합(메모리 안정/절감)
report_to=[],
)
trainer = SFTTrainer(
model=model,
args=args,
train_dataset=ds,
processing_class=tokenizer,
formatting_func=format_example,
peft_config=peft_config,
)
trainer.train()
trainer.save_model()
# 7) 예상 출력(로그): 대략 이런 식
# step 10 | loss=1.23 | lr=...
# step 20 | loss=1.10 | ...
# ...
print("Saved to:", args.output_dir)
3) 확장: “LoRA만”으로 바꾸는 스위치(여유 VRAM 있을 때)
QLoRA에서 문제가 생기거나(환경/커널/드라이버), 품질을 조금이라도 더 단순하게 가져가고 싶다면:
BitsAndBytesConfig제거torch_dtype=bfloat16으로 베이스를 그대로 로딩- 나머지 파이프라인은 동일(LoRA만 적용)
TRL SFTTrainer는 from_pretrained()의 키워드 인자를 그대로 지원하므로, 로딩/양자화 전략을 바꿔도 트레이너 구조는 유지됩니다. (huggingface.co)
⚡ 실전 팁 & 함정
Best Practice (바로 효과 나는 것)
1) target_modules는 “all-linear → 축소” 순서로
- 처음부터
q_proj,v_proj만 찍고 시작하면 모델마다 레이어/이름이 달라 삽질이 잦습니다. - PEFT 문서에서도
target_modules="all-linear"가 대체로 좋은 기본값일 수 있다고 명시합니다. (github.com) - 이후 비용/속도 최적화 단계에서만 범위를 줄이세요.
2) packing=True를 기본으로 두고, 데이터 길이 분포를 먼저 본다
- 고객지원/대화 데이터는 샘플 길이가 들쭉날쭉이라 padding 낭비가 큽니다.
- packing은 throughput을 크게 올리지만, “샘플 경계”가 의미 있는 태스크(예: 엄격한 turn 단위 학습)에서는 부작용이 있을 수 있어 eval로 확인이 필요합니다.
3) 학습 목표를 “지식 주입”이 아니라 “행동/형식/판단 규칙”으로 잡아라
- 예: JSON 스키마 준수, 거절 정책, 단계적 체크리스트 수행, 툴 호출 형식.
- 최근 QLoRA로 “툴 지식 내재화” 같은 방향의 연구도 나오는데, 이런 유형은 RAG보다 파인튜닝이 더 잘 먹히는 경우가 있습니다(입력 길이 절감 + 구조적 출력 개선). (arxiv.org)
흔한 함정/안티패턴
- “데이터가 적으니 learning rate를 올리자”: 오히려 말투/편향만 과적합하고, 일반 질의 대응력이 떨어지기 쉽습니다. 적을수록 정제와 eval이 우선.
- 정답에 없는 정보를 꾸며내는 데이터(환각 유도)가 섞임: 파인튜닝은 그걸 “정책”으로 학습합니다. 특히 고객지원은 법/환불/계약 이슈라 위험.
- 베이스 모델의 system prompt 규칙과 충돌: Instruct 모델은 원래의 정렬 규칙이 있어서, 여러분의 포맷/정책과 충돌하면 손실이 줄어도 출력이 안 맞습니다. format을 더 강하게(스키마, 금지사항, 예시) 넣고, eval을 케이스 기반으로 하세요.
비용/성능/안정성 트레이드오프(체감 큰 것만)
- QLoRA(4-bit): VRAM 이득이 커서 “돌아가게” 만들기 좋음. 대신 환경 의존성이 커지고, 커널/드라이버 조합에 따라 속도/안정성이 흔들릴 수 있습니다(특히 소비자 GPU).
- LoRA(16-bit 베이스): 단순하고 재현성이 좋은 편. VRAM 여유가 있으면 개발/운영 비용이 더 낮습니다.
- 병합(merge) 여부: 배포 단순화(단일 체크포인트) vs. 원본 베이스 재사용/여러 어댑터 운영(멀티테넌시). 팀 운영 모델에 따라 결정하세요.
🚀 마무리
정리하면, 2026년 6월에도 LoRA/QLoRA는 “LLM을 내 제품에 맞게 바꾸는” 가장 실용적인 방법입니다.
- 내 GPU가 빡빡하다(단일 12–24GB) → QLoRA부터 시작(NF4 + double quant 조합) (huggingface.co)
- 재현성과 단순함이 최우선 → LoRA(16-bit 베이스)로 먼저 안정화
- 데이터가 ‘지식’이면 → RAG 우선, fine-tuning은 행동/형식/정책 강화에 집중
다음 학습 추천(실무 루트): 1) TRL SFTTrainer 로그/평가(케이스 기반) 정교화: “loss 감소”가 아니라 “업무 성공률”로 보세요. (huggingface.co)
2) PEFT의 LoRA 고급 옵션(초기화/대상 모듈/DoRA 등) 검토: 기본이 돌아가고 나서 건드리는 게 맞습니다. (huggingface.co)
3) 성능 한계가 오면: 더 나은 커널/프레임워크(예: Unsloth 기반 파이프라인)까지 포함해 학습 스택을 재검토하세요. (developers.redhat.com)