LoRA·QLoRA·DoRA 완전 정복 — 무엇을, 언제, 왜 쓰는가
대형 모델을 적은 자원으로 파인튜닝하는 PEFT 3대 기법 LoRA·QLoRA·DoRA를 원리·용도·차이·코드까지 그림과 함께 정리합니다.
수십억 개 파라미터를 가진 LLM을 우리 도메인에 맞게 길들이고 싶을 때, 모델 전체를 다시 학습하는 것은 대부분의 팀에게 비현실적입니다. LoRA·QLoRA·DoRA는 "모델의 극히 일부만 학습해서 전체를 새로 학습한 것과 비슷한 효과를 내는" 기법들입니다. 이 글은 세 기법이 각각 무엇을 위해, 언제 쓰는지를 원리·차이·코드와 함께 그림으로 풀어 설명합니다.
용어가 낯설다면 AI 용어집의 "효율적 파인튜닝(PEFT)" 항목을 먼저 훑어보면 좋습니다.
파인튜닝은 왜 비싼가
풀 파인튜닝(full fine-tuning)은 모델의 모든 가중치를 갱신합니다. 문제는 GPU 메모리입니다. 학습 한 번에 다음 네 가지가 동시에 메모리에 올라갑니다.
- 모델 가중치 (예: 7B 모델 = 약 14GB, FP16 기준)
- 그래디언트 (가중치와 같은 크기)
- 옵티마이저 상태 (Adam은 가중치의 약 2배)
- 활성값(activation)
대략 가중치의 4~6배 메모리가 필요합니다. 7B 모델 풀 파인튜닝에 80GB GPU가 빠듯하고, 70B는 여러 장이 필요합니다. 게다가 작업마다 모델 전체를 복제해 저장해야 하니 운영 부담도 큽니다.
여기서 출발하는 발상이 PEFT(Parameter-Efficient Fine-Tuning) 입니다. 거대한 베이스 모델은 얼리고(freeze), 아주 작은 추가 파라미터만 학습합니다.
LoRA — 가중치 변화를 저랭크로 근사하기
핵심 아이디어
파인튜닝이란 결국 원래 가중치 W에 변화량 ΔW를 더하는 일입니다 (W' = W + ΔW). LoRA(Low-Rank Adaptation)의 통찰은 이 ΔW가 사실 저랭크(low-rank) 구조라는 것입니다. 즉 큰 행렬 ΔW를 작은 행렬 두 개의 곱 B·A로 근사할 수 있습니다.
W가 d×d 크기라면, A는 r×d, B는 d×r 크기이고 여기서 랭크 r은 보통 4~64로 아주 작습니다. 학습하는 파라미터 수가 극적으로 줄어듭니다.
베이스 가중치 W는 그대로 두고, 옆에 붙인 저랭크 경로 B·A만 학습합니다. 학습이 끝나면 B·A를 W에 합쳐(merge) 하나의 가중치로 만들 수 있어 추론 시 추가 지연이 없습니다.
주요 하이퍼파라미터
| 파라미터 | 역할 | 실무 감각 |
|---|---|---|
r (rank) | 저랭크 경로의 크기 | 작을수록 가볍고, 클수록 표현력↑ (보통 8~32) |
lora_alpha | B·A에 곱하는 스케일(alpha/r) | 보통 r의 2배에서 시작 |
target_modules | LoRA를 붙일 층 | 어텐션의 q_proj·v_proj가 기본, 전체에 붙이면 성능↑ 비용↑ |
lora_dropout | 과적합 방지 | 0.05 안팎 |
코드로 보기
허깅페이스 peft 라이브러리로 몇 줄이면 됩니다.
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")
config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
model = get_peft_model(base, config)
model.print_trainable_parameters()
# 예: trainable params: 6.8M || all params: 8.0B || trainable%: 0.085학습 대상이 전체의 0.1% 미만으로 줄어드는 것을 볼 수 있습니다.
LoRA는 언제 쓰나
- 하나의 베이스, 여러 작업: 작업마다 수 MB짜리 어댑터만 따로 저장하면 됩니다. 베이스 모델 하나에 어댑터를 갈아 끼우는 멀티테넌트 서빙에 이상적입니다.
- 빠르고 저렴한 반복 실험: 학습이 빠르고 메모리가 적게 들어 실험 사이클이 짧습니다.
- 풀 파인튜닝이 부담스러운 대부분의 도메인 적응: 말투·포맷·도메인 지식 주입 등.
QLoRA — 4비트로 양자화해서 더 크게
핵심 아이디어
LoRA로 학습 대상은 줄였지만, 동결된 베이스 모델 자체는 여전히 메모리에 통째로 올라갑니다(7B면 14GB). QLoRA(Quantized LoRA)는 이 베이스 모델을 4비트로 양자화해서 올립니다. 14GB가 약 4~5GB로 줄어, 단일 소비자용 GPU에서도 대형 모델을 파인튜닝할 수 있게 됩니다.
포인트는 베이스는 4비트로 저장하되, 실제 계산 순간에만 16비트로 잠깐 되돌려(dequantize) 쓴다는 것입니다. 그래디언트는 오직 16비트 LoRA 어댑터로만 흐릅니다. 베이스는 끝까지 얼어 있습니다.
QLoRA를 가능케 한 세 가지
- NF4 (4-bit NormalFloat): 신경망 가중치의 분포(정규분포에 가까움)에 최적화된 4비트 데이터 타입. 일반 INT4보다 정확도 손실이 적습니다.
- 이중 양자화 (Double Quantization): 양자화에 쓰이는 상수(quantization constants)까지 한 번 더 양자화해 메모리를 추가로 아낍니다.
- 페이지드 옵티마이저 (Paged Optimizers): 메모리 급증 순간 옵티마이저 상태를 CPU로 잠시 내보내 OOM(메모리 부족)을 방지합니다.
코드로 보기
bitsandbytes로 4비트 로딩 후 LoRA를 얹습니다.
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
import torch
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4 데이터 타입
bnb_4bit_use_double_quant=True, # 이중 양자화
bnb_4bit_compute_dtype=torch.bfloat16,
)
base = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-70B", quantization_config=bnb, device_map="auto",
)
base = prepare_model_for_kbit_training(base)
model = get_peft_model(base, LoraConfig(
r=16, lora_alpha=32, target_modules="all-linear",
lora_dropout=0.05, task_type="CAUSAL_LM",
))QLoRA는 언제 쓰나
- GPU 자원이 빠듯한데 큰 모델을 파인튜닝해야 할 때 (예: 단일 24GB/48GB GPU에서 13B~70B).
- 비용 최소화가 최우선일 때. 약간의 양자화 손실을 감수하고 자원을 크게 아낍니다.
- 다만 추론 품질이 극도로 중요하고 GPU가 충분하다면, 양자화 없는 LoRA나 풀 파인튜닝이 더 안전할 수 있습니다.
DoRA — 크기와 방향을 분리해서
핵심 아이디어
LoRA는 가볍지만, 학습 동역학이 풀 파인튜닝과 미묘하게 달라 성능 격차가 날 때가 있습니다. DoRA(Weight-Decomposed Low-Rank Adaptation)는 가중치를 크기(magnitude)와 방향(direction) 으로 분해합니다. 그리고 방향 성분에만 LoRA를 적용하고, 크기는 별도의 작은 학습 가능한 벡터로 따로 조정합니다.
이렇게 크기와 방향을 따로 학습하면 풀 파인튜닝에 더 가까운 학습 패턴을 보여, 같은 파라미터 예산에서 LoRA보다 정확도가 높게 나오는 경향이 있습니다. 추론 시에는 LoRA처럼 다시 합쳐 추가 지연이 없습니다.
코드로 보기
peft에서는 LoRA 설정에 플래그 하나만 켜면 됩니다.
from peft import LoraConfig
config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
use_dora=True, # DoRA 활성화
task_type="CAUSAL_LM",
)DoRA는 언제 쓰나
- LoRA를 써봤는데 성능이 살짝 아쉬울 때, 파라미터를 크게 늘리지 않고 품질을 끌어올리고 싶을 때.
- 다만 분해·계산이 추가되어 학습이 LoRA보다 다소 느립니다. "LoRA의 가벼움"과 "풀 파인튜닝의 품질" 사이 절충점입니다.
한눈 비교
| 항목 | LoRA | QLoRA | DoRA |
|---|---|---|---|
| 한 줄 정의 | ΔW를 저랭크 B·A로 근사 | 베이스를 4비트로 양자화 + LoRA | 가중치를 크기·방향으로 분해 후 방향에 LoRA |
| 주 목적 | 학습 파라미터 절감 | GPU 메모리 절감(더 큰 모델) | LoRA 대비 품질 향상 |
| 베이스 정밀도 | 16비트 (동결) | 4비트 (동결) | 16비트 (동결) |
| 메모리 | 낮음 | 가장 낮음 | 낮음 (LoRA와 비슷) |
| 학습 속도 | 빠름 | 빠름(양자화 오버헤드 약간) | LoRA보다 다소 느림 |
| 추론 지연 | 합치면 없음 | 합치면 없음 | 합치면 없음 |
| 대표 용도 | 멀티 작업·빠른 실험 | 자원 제약하 대형 모델 | 품질이 아쉬운 LoRA의 업그레이드 |
셋은 경쟁이 아니라 조합입니다. 실제로 "QLoRA + DoRA"처럼 4비트 베이스 위에 DoRA를 얹어, 메모리도 아끼고 품질도 챙기는 구성을 많이 씁니다.
무엇을 언제 쓸까
정리하면 의사결정은 두 축입니다. 자원(메모리)이 부족하면 양자화(QLoRA), 품질이 부족하면 분해(DoRA), 둘 다면 조합. 별다른 제약이 없는 출발점은 언제나 평범한 LoRA입니다.
마무리
세 기법은 모두 "베이스는 얼리고, 작은 부분만 학습한다"는 PEFT의 한 줄 원칙을 공유합니다. 거기서 LoRA는 학습량을, QLoRA는 메모리를, DoRA는 품질을 각각 더 밀어붙인 변형입니다. 대부분의 프로젝트는 LoRA로 시작해, 자원이 모자라면 QLoRA로, 품질이 모자라면 DoRA로 옮겨가는 흐름이면 충분합니다.
더 많은 관련 용어(PEFT, 양자화, 어댑터, 랭크 등)는 AI 용어집에서 한 번에 확인할 수 있습니다.