포스트

2026년 2월 기준: LoRA/QLoRA로 LLM Fine-tuning을 “현실적으로” 끝내는 방법 (원리+실전)

2026년 2월 기준: LoRA/QLoRA로 LLM Fine-tuning을 “현실적으로” 끝내는 방법 (원리+실전)

들어가며

LLM fine-tuning은 “성능은 좋은데 비용이 너무 비싸다”가 늘 문제였습니다. Full fine-tuning은 GPU 메모리/시간/비용이 기하급수로 커지고, 실무에서는 데이터도 충분히 크지 않은 경우가 많습니다. 그래서 2026년 2월에도 여전히 표준 해법은 PEFT(Parameter-Efficient Fine-Tuning), 그중에서도 LoRA/QLoRA입니다.

  • LoRA: base model weight는 고정(freeze)하고, 일부 Linear layer에 저랭크(rank) adapter만 학습해 파라미터/메모리를 크게 줄입니다.
  • QLoRA: base model을 4-bit quantization으로 로드해 VRAM을 더 아끼고, 그 위에 LoRA adapter를 학습합니다. QLoRA는 NF4, double quantization, paged optimizer 등으로 메모리 효율을 극대화했습니다. (arxiv.org)

결론적으로, “내 GPU 한 장(심지어 8~16GB)으로도” 최신 오픈웨이트 LLM을 업무용으로 커스터마이징하는 가장 현실적인 루트가 LoRA/QLoRA입니다.


🔧 핵심 개념

1) LoRA의 핵심 아이디어: ΔW를 저랭크로 근사

Transformer의 Linear weight를 (W)라 하면, LoRA는 학습 시 (W)를 직접 업데이트하지 않고 다음처럼 업데이트를 “우회”합니다.

  • 원래: (W \leftarrow W + \Delta W)
  • LoRA: (\Delta W \approx \frac{\alpha}{r} AB) (A: in→r, B: r→out)
    즉, 큰 행렬 업데이트 대신 작은 두 행렬(A, B)만 학습합니다. 이때 rank r가 작을수록 학습 파라미터가 줄고, 표현력도 함께 줄어듭니다. 실무에서는 ( \alpha )와 ( r )의 비율(스케일링)이 중요하고, 경험적으로 lora_alpha = r 또는 2*r 같은 규칙이 널리 쓰입니다. (unsloth.ai)

2) QLoRA: 4-bit로 base model을 “고정한 채” 역전파는 adapter로

QLoRA는 base model을 4-bit로 quantize해 VRAM을 줄이되, 학습 불안정성을 피하기 위해 학습은 LoRA adapter에만 일어납니다. 이때 자주 쓰는 설정이:

  • bnb_4bit_quant_type="nf4": 정규분포 가정의 weight에 최적화된 4-bit 타입 (arxiv.org)
  • bnb_4bit_use_double_quant=True: “양자화 상수”도 다시 양자화해 메모리 절감 (arxiv.org)
  • prepare_model_for_kbit_training(model): k-bit 학습을 위한 전처리(예: layernorm 처리 등) (huggingface.co)

3) 어디에 LoRA를 꽂을까: target_modules

아키텍처마다 layer명이 달라 골치 아픈데, 2026년 실무 팁은 가능하면 “all-linear”입니다. PEFT는 target_modules="all-linear"로 Transformer 내부의 Linear/Conv1D에 광범위하게 적용하는 옵션을 제공합니다. (huggingface.co)

또 하나의 함정: chat template에 특수 토큰이 들어가면 embedding / lm_head 처리가 중요합니다. TRL의 SFTTrainer 문서에서도 <|im_start|>, <|eot_id|> 같은 special token이 있는 경우 modules_to_saveembed_tokens, lm_head를 포함하지 않으면 출력이 망가질 수 있다고 명시합니다. (huggingface.co)


💻 실전 코드

아래 코드는 “2026년 2월 기준 가장 흔한 스택”인 Transformers + BitsAndBytes(4-bit) + PEFT(LoRA) + TRL(SFTTrainer) 조합으로, 로컬 GPU 1장 기준 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
# 실행 전 설치 예시:
# pip install -U "transformers>=4.45" "accelerate>=0.34" "datasets>=2.20" "trl>=0.16" "peft>=0.12" bitsandbytes

import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    BitsAndBytesConfig,
)
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
from trl import SFTTrainer, SFTConfig

# 1) Base model 선택: 가능하면 Instruct 계열 추천(대화 템플릿/토크나이저 정합성이 좋음)
model_id = "Qwen/Qwen2.5-0.5B"  # 데모용(작아서 누구나 재현 쉬움)
# 실무에서는 7B~14B Instruct를 QLoRA로 많이 감

# 2) QLoRA용 4-bit quantization 설정 (NF4 + double quant)
#    - NF4/double quant는 QLoRA 논문/PEFT 가이드에서 핵심 옵션으로 언급됨
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,  # Ampere+에서 bf16 권장
)

tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
# 일부 모델은 pad_token 미정의. 학습/배치에서 필요할 수 있음.
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

# 3) 4-bit로 base model 로드
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    quantization_config=bnb_config,
)

# 4) k-bit 학습 전처리: quantized model 위에서 adapter 학습을 안정화
model = prepare_model_for_kbit_training(model)

# 5) LoRA 설정
#    - QLoRA 스타일로 최대한 넓게: target_modules="all-linear"
#    - chat template에 special token이 있는 모델이면 modules_to_save 고려(특히 embed/lm_head)
peft_config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules="all-linear",
    modules_to_save=["lm_head", "embed_tokens"],  # 필요 시(모델/템플릿에 따라 조정)
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, peft_config)

# 6) 데이터셋: 예시는 TRL에서 자주 쓰는 공개 instruct 데이터
#    - 실무에서는 "내 업무 포맷"으로 정제된 고품질 소량 데이터가 더 중요
dataset = load_dataset("trl-lib/Capybara", split="train")

# 7) SFTTrainer 설정
#    - max_length/truncation은 성능과 비용을 좌우
#    - 작은 GPU면 batch_size를 낮추고 gradient_accumulation으로 보정
sft_args = SFTConfig(
    output_dir="./qlora_sft_out",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,
    learning_rate=2e-4,
    max_steps=200,
    logging_steps=10,
    save_steps=100,
    bf16=True,  # 가능하면 bf16 권장
    max_length=2048,  # 문서에서도 truncation 기본 동작 주의 강조
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=sft_args,
)

trainer.train()

# 8) adapter 저장 (base model은 그대로, LoRA만 저장)
trainer.model.save_pretrained("./adapter_lora")

# (선택) 추론 시:
# base model + adapter를 로드해서 사용하거나, merge 후 단일 모델로 저장할 수 있음.

위 코드 흐름에서 “QLoRA의 본질”은 딱 두 줄입니다.

  • 4-bit로 로드: quantization_config=bnb_config (huggingface.co)
  • adapter만 학습: get_peft_model(...)로 LoRA를 얹고, base는 freeze

⚡ 실전 팁

1) modules_to_save는 생각보다 중요 ChatML/Llama 계열처럼 special token을 쓰는 템플릿에서, embedding/lm_head가 학습/저장 경로에서 빠지면 “말이 붕괴”하거나 무한 반복 같은 문제가 나기도 합니다. TRL 문서가 이를 명시적으로 경고합니다. (huggingface.co)

  • 해결: modules_to_save=["lm_head","embed_tokens"]를 우선 넣고, 모델별로 검증 후 최소화하세요.

2) QLoRA target_modules는 “넓게”가 기본값 PEFT 가이드는 QLoRA-style로 target_modules="all-linear"를 권장합니다(레이어 명이 다양한 모델에서 특히). (huggingface.co)
정밀 튜닝이 필요하면 q/k/v/o + MLP(gate/up/down)로 좁혀가며 비용-성능 트레이드오프를 잡습니다.

3) 컨텍스트 길이(max_length)는 곧 비용 SFTTrainer는 기본적으로 truncation을 수행하고, tokenizer 설정에 따라 예상보다 짧게 잘릴 수 있습니다. 학습 전에 반드시 max_length가 의도대로인지 확인하세요. (huggingface.co)
실무적으로는 “최대 길이”보다 “데이터의 길이 분포”를 먼저 보고, 패딩/잘림을 줄이도록 샘플을 재구성하는 편이 효과가 큽니다.

4) “훈련은 completions만”이 의외로 잘 먹힌다 지시문까지 모두 loss에 넣으면, 모델이 프롬프트를 “정답처럼” 외우는 방향으로 학습될 수 있습니다. completions-only(assistant 구간만 loss)로 가면 지시 따르기 품질이 좋아지는 경우가 많습니다(특히 multi-turn). 이 전략은 QLoRA 계열 레시피에서 자주 언급됩니다. (unsloth.ai)

5) 검증은 “정량+정성” 둘 다 QLoRA 논문도 벤치마크의 신뢰성 문제를 지적하고, 평가가 생각보다 어렵다는 점을 강조합니다. (arxiv.org)
실무에서는:

  • 고정된 50~200개 “업무 대표 질문 세트”를 만들고
  • 학습 전/후를 동일 프롬프트로 비교
  • 실패 케이스를 데이터에 되먹임(데이터 큐레이션)이 가장 빠른 개선 루프입니다.

🚀 마무리

LoRA는 “적은 파라미터로 원하는 성격만 덧입히는” 방법이고, QLoRA는 거기에 4-bit quantization을 더해 “내 GPU 한 장에서도” fine-tuning을 가능하게 만든 방식입니다. 2026년 2월 기준 실무 베이스라인은:

  • QLoRA: BitsAndBytesConfig(load_in_4bit=True, nf4, double_quant) + prepare_model_for_kbit_training (huggingface.co)
  • LoRA: target_modules="all-linear"로 시작, 필요하면 좁혀가기 (huggingface.co)
  • TRL SFTTrainer: truncation/max_length와 modules_to_save 함정을 먼저 잡기 (huggingface.co)

다음 학습 추천은 두 갈래입니다. 1) 데이터 레시피 고도화: completions-only, 멀티턴 구성, 실패 케이스 중심 증강
2) 후속 정렬(Alignment): SFT 이후 DPO/ORPO 같은 선호 최적화로 “말투/정책/안전”을 더 정교하게 만들기

원하면, (1) 특정 모델(Llama 계열/Qwen 계열/Gemma 계열) 중 어떤 걸 목표로 하는지, (2) GPU VRAM, (3) 데이터 포맷(예: ShareGPT/ChatML/자체 JSON)을 알려주면 위 코드를 그 환경에 맞춰 “바로 돌릴 수 있는 형태”로 더 좁혀서 구성해드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.