포스트

2026년 3월 기준 LoRA/QLoRA 파인튜닝 실전 튜토리얼: 4-bit NF4 + PEFT + TRL로 “효율”을 끝까지 뽑아내기

2026년 3월 기준 LoRA/QLoRA 파인튜닝 실전 튜토리얼: 4-bit NF4 + PEFT + TRL로 “효율”을 끝까지 뽑아내기

들어가며

LLM fine-tuning은 “내 도메인/내 톤/내 포맷”에 모델을 맞추는 가장 강력한 방법이지만, 비용이 항상 문제입니다. Full fine-tuning은 가중치·그라디언트·optimizer state가 함께 메모리를 잡아먹어(특히 AdamW의 FP32 상태가 큼) 현실적인 GPU 한 장에서 바로 터집니다. 그래서 2026년에도 여전히 현업의 표준은 PEFT(Parameter-Efficient Fine-Tuning) 계열, 그중에서도 LoRAQLoRA(4-bit quant + LoRA) 조합입니다.

최근 Hugging Face 생태계에서는 Transformers의 BitsAndBytesConfig(4-bit NF4, nested/double quant) + PEFT의 LoRA(예: target_modules=”all-linear”, rsLoRA, LoftQ 등 옵션) + TRL의 SFTTrainer로 “거의 공식 레시피”가 굳어졌습니다. 특히 NF4와 nested quant(= double quant)는 QLoRA의 핵심 구성으로 문서에 명확히 정리되어 있습니다. (huggingface.co)


🔧 핵심 개념

1) LoRA란?

LoRA는 큰 weight 업데이트(ΔW)를 직접 학습하지 않고, 저랭크 행렬 분해(BA) 형태로만 학습합니다. 즉,

  • 원래 가중치 W는 frozen
  • 학습 가능한 것은 작은 rank r의 어댑터(LoRA A,B)
  • 결과적으로 학습 파라미터 수와 optimizer state가 급감

PEFT에서는 LoraConfig + get_peft_model()로 주입하며, 초기화/대상 모듈/스케일링 전략(rsLoRA 등)까지 옵션화되어 있습니다. (huggingface.co)

2) QLoRA란? (LoRA + 4-bit quant)

QLoRA의 아이디어는 간단합니다:

  • base model weight를 4-bit(NF4)로 quantize해서 VRAM을 크게 절약하고,
  • quantized base weight는 그대로 두고(동결),
  • 그 위에 LoRA adapter만 학습합니다.

여기서 중요한 디테일이 3개입니다.

(a) NF4 (NormalFloat4)
LLM weight 분포가 대체로 “정규분포 주변”에 몰린 특성을 반영한 4-bit 포맷이라, 학습용 4-bit로 권장됩니다. Transformers 문서에서도 “훈련용 4-bit면 NF4를 쓰라”가 사실상 정답입니다. (huggingface.co)

(b) Nested quantization = bnb_4bit_use_double_quant=True
첫 양자화에서 생기는 “스케일 상수”까지 다시 한 번 양자화해 약 0.4 bits/param 정도를 추가로 절약합니다. 메모리가 빡빡할 때 체감이 크고, 공식 문서에서도 nested quant로 별도 항목을 둡니다. (huggingface.co)

(c) “QLoRA 스타일”의 target_modules 원 논문/레시피에서 흔히 강조되는 포인트는 “attention의 q,v만”이 아니라 가능하면 모든 linear에 LoRA를 붙이는 것입니다. PEFT에서는 이를 target_modules="all-linear"로 간단히 처리할 수 있게 가이드합니다. (huggingface.co)


💻 실전 코드

아래 코드는 Transformers + bitsandbytes(4-bit) + PEFT(LoRA) + TRL(SFTTrainer) 조합으로 “실행 가능한” QLoRA SFT 예시입니다.
(실무에선 dataset만 갈아끼우면 그대로 파이프라인이 됩니다.)

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
import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig

# 1) 모델/토크나이저 선택
model_id = "meta-llama/Llama-3.1-8B-Instruct"  # 예시. 접근 권한/라이선스에 맞게 변경
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)

# 일부 모델은 pad_token이 없을 수 있어 안전장치
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

# 2) 4-bit QLoRA용 quantization 설정 (NF4 + nested/double quant)
# - NF4: 학습용 4-bit 권장
# - bnb_4bit_use_double_quant: nested quant로 추가 메모리 절약
# - compute dtype: bf16 권장(가능한 GPU면). 속도/안정성 타협점
quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

# Transformers 문서의 4-bit 가이드가 이 형태를 권장 ([huggingface.co](https://huggingface.co/docs/transformers/v4.49.0/quantization/bitsandbytes?utm_source=openai))
base_model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=quant_config,
    device_map="auto",
)

# 3) LoRA 설정
# - QLoRA 스타일: 모든 linear 레이어에 적용(target_modules="all-linear")
# - r/alpha는 품질-자원 트레이드오프의 핵심 노브
lora_config = LoraConfig(
    r=32,
    lora_alpha=64,
    lora_dropout=0.05,
    target_modules="all-linear",
    bias="none",
    task_type="CAUSAL_LM",
    use_rslora=True,  # rsLoRA: 스케일링을 안정화하는 옵션(PEFT 가이드) ([huggingface.co](https://huggingface.co/docs/peft/v0.9.0/developer_guides/lora?utm_source=openai))
)

model = get_peft_model(base_model, lora_config)

# 4) 데이터셋 준비 (예시: instruction -> response 형태로 텍스트 합치기)
ds = load_dataset("tatsu-lab/alpaca", split="train[:2000]")

def format_example(ex):
    # 실무 팁: 학습 포맷을 “일관된 템플릿”으로 고정하면 수렴이 좋아짐
    return {
        "text": f"### Instruction:\n{ex['instruction']}\n\n### Response:\n{ex['output']}"
    }

ds = ds.map(format_example, remove_columns=ds.column_names)

# 5) TRL SFTTrainer 구성
# TRL은 PEFT 통합/예제를 공식 문서로 제공 ([huggingface.co](https://huggingface.co/docs/trl/peft_integration?utm_source=openai))
cfg = SFTConfig(
    output_dir="./qlora_adapter",
    max_seq_length=1024,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=8,
    learning_rate=2e-4,
    num_train_epochs=1,
    logging_steps=10,
    save_steps=200,
    bf16=True,  # GPU 지원 시
    optim="paged_adamw_32bit",  # QLoRA 레시피에서 메모리 스파이크 완화에 유용한 선택지로 알려짐 ([deepwiki.com](https://deepwiki.com/artidoro/qlora/3.1-memory-optimization-techniques?utm_source=openai))
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=ds,
    args=cfg,
    dataset_text_field="text",
)

trainer.train()

# 6) 어댑터만 저장(=배포/공유 비용 최소화)
trainer.model.save_pretrained("./qlora_adapter")
tokenizer.save_pretrained("./qlora_adapter")

⚡ 실전 팁

1) NF4 + bf16 compute dtype가 “기본값에 가까운 정답”
훈련용 4-bit면 NF4를 추천하고, compute dtype를 bf16로 두면 속도/안정성 밸런스가 좋습니다. (huggingface.co)

2) nested/double quant는 “메모리가 문제면 켜라”
bnb_4bit_use_double_quant=True는 추가 메모리 절약을 주고, 커뮤니티에서도 “메모리 부족이면 double quant, 정밀도는 NF4, 학습 속도는 16-bit dtype” 같은 룰오브썸이 공유됩니다. (discuss.huggingface.co)

3) target_modules="all-linear"로 QLoRA 스타일을 간단히 재현
모델 아키텍처마다 레이어 이름이 미묘하게 달라 “q_proj/v_proj만 찍기”는 깨지기 쉽습니다. PEFT가 all-linear 옵션을 제공하는 이유가 실전 호환성입니다. (huggingface.co)

4) bitsandbytes는 환경 제약이 있다 (특히 CUDA)
TPU 환경에서 bitsandbytes가 동작하지 않는다는 식의 제약이 반복적으로 언급됩니다. 즉, “QLoRA=bitsandbytes”로 가면 CUDA GPU 전제가 됩니다. (discuss.huggingface.co)

5) 어댑터 배포 전략을 분리하라 QLoRA의 진짜 운영 이점은 “base model은 그대로 + adapter만 교체”입니다. Hugging Face에 올라오는 많은 repo가 실제로 LoRA adapter만 공개/배포합니다. (huggingface.co)


🚀 마무리

LoRA는 “학습 파라미터를 줄여서” 파인튜닝을 현실화했고, QLoRA는 “base weight를 4-bit(NF4)로 눌러서” 단일 GPU에서도 꽤 큰 모델을 튜닝 가능하게 만들었습니다. 2026년 3월 기준 실전 레시피는 다음 조합이 가장 재현성이 높습니다.

  • Transformers BitsAndBytesConfig4-bit NF4 + nested/double quant
  • PEFT LoraConfig(target_modules="all-linear", rsLoRA 등)QLoRA 스타일 LoRA 주입
  • TRL SFTTrainerSFT 파이프라인 단순화

다음 단계로는 (1) completion-only 학습(불필요한 instruction loss 제거), (2) sequence packing으로 padding 낭비 줄이기, (3) LoftQ 같은 초기화/양자화 오차 대응, (4) 모델/프레임워크별 성능 최적화(예: Unsloth류, 커널 퓨전 기반 프레임워크 비교)까지 확장해보면 “효율적 학습”의 체감이 더 커집니다. (huggingface.co)

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