Skip to content
Published on

Unsloth로 LLM 파인튜닝 완전 가이드 2025: QLoRA, 4bit 양자화, 2배 빠른 학습

Authors

서론: 왜 Unsloth인가?

LLM 파인튜닝의 가장 큰 진입 장벽은 **GPU 메모리(VRAM)**입니다. Llama 3.1 8B를 Full Fine-tuning하려면 약 60GB VRAM이 필요하고, 이는 A100 80GB 하나로도 빠듯합니다. QLoRA가 이 문제를 해결했지만, 학습 속도는 여전히 느렸습니다.

Unsloth는 이 두 가지 문제를 동시에 해결합니다:

비교 항목HuggingFace PEFTAxolotlUnsloth
학습 속도1x (기준)1.1x2x
메모리 사용100%95%40%
설정 난이도중간높음낮음
지원 모델전체전체주요 모델
Flash Attention별도 설치내장내장
커스텀 커널없음없음Triton 커널

Unsloth의 핵심 비밀은 커스텀 Triton 커널입니다. Attention, MLP, Cross-Entropy Loss 등의 핵심 연산을 GPU에 최적화된 커스텀 커널로 대체하여 2배 빠른 학습과 60% 메모리 절약을 달성합니다.

지원 모델 (2025년 기준):

  • Llama 3 / 3.1 / 3.2 (8B, 70B)
  • Mistral / Mixtral
  • Phi-3 / Phi-3.5
  • Qwen 2 / 2.5
  • Gemma 2
  • Yi
  • DeepSeek V2

1. LoRA/QLoRA 이론

1.1 Full Fine-tuning vs LoRA vs QLoRA

Full Fine-tuning (모든 파라미터 업데이트)
┌──────────────────────┐
W (d x d)<- 전체 가중치 업데이트
│   예: 4096 x 4096= 16M 파라미터
= 64MB (FP16)└──────────────────────┘

LoRA (Low-Rank Adaptation)
┌──────────────────────┐
W0 (고정) + B * AW0: 4096 x 4096<- 고정 (업데이트 안 함)
B: 4096 x 16<- 학습 (65K 파라미터)
A: 16 x 4096<- 학습 (65K 파라미터)
= 0.25MB (FP16)    │     총 130K 파라미터
└──────────────────────┘

QLoRA (Quantized LoRA)
┌──────────────────────┐
W0 (4bit) + B * AW0: 4096 x 4096<- 4bit 양자화 (8MB)
B: 4096 x 16<- FP16 학습
A: 16 x 4096<- FP16 학습
= 8.25MB total     │
└──────────────────────┘

1.2 Low-Rank Decomposition 원리

LoRA의 핵심 아이디어는 **가중치 업데이트 행렬이 실제로 저차원(low-rank)**이라는 관찰에 기반합니다.

원래의 가중치 업데이트:

W_new = W_old + delta_W

LoRA는 delta_W를 두 개의 작은 행렬의 곱으로 분해합니다:

delta_W = B * A
여기서:
  B는 d x r 행렬 (d=모델 차원, r=LoRA 랭크)
  A는 r x d 행렬
  r << d (: r=16, d=4096)

파라미터 절약 효과:

# Full Fine-tuning 파라미터 수
d = 4096
full_params = d * d  # = 16,777,216 (16.7M)

# LoRA 파라미터 수
r = 16
lora_params = d * r + r * d  # = 131,072 (131K)

# 절약률
savings = 1 - (lora_params / full_params)
print(f"파라미터 절약: {savings:.2%}")  # 99.22%

1.3 4-bit NormalFloat 양자화 (NF4)

QLoRA에서 사용하는 NF4 양자화는 일반 4-bit와 다릅니다:

일반 4-bit INT 양자화:

  • 균일하게 16개 구간으로 나눔
  • 값 분포를 고려하지 않음

NF4 (NormalFloat4):

  • 가중치가 정규분포를 따른다는 사실을 활용
  • 정규분포의 분위수(quantile)에 맞춰 16개 값 설정
  • 정보 이론적으로 최적에 가까운 양자화
# NF4 양자화 값 예시 (정규분포 분위수 기반)
nf4_values = [
    -1.0, -0.6962, -0.5251, -0.3949,
    -0.2844, -0.1848, -0.0911, 0.0,
    0.0796, 0.1609, 0.2461, 0.3379,
    0.4407, 0.5626, 0.7230, 1.0,
]

1.4 Double Quantization

QLoRA의 또 다른 혁신은 **이중 양자화(Double Quantization)**입니다:

  1. 가중치를 4-bit로 양자화 (NF4)
  2. 양자화 상수(scaling factor)를 다시 8-bit로 양자화
  3. 추가 메모리 절약: 블록당 32bit에서 8bit로

1.5 메모리 비교표

모델Full FT (FP16)LoRA (FP16)QLoRA (4bit)
Llama 3 8B~60GB~18GB~6GB
Llama 3 70B~500GB~160GB~40GB
Mistral 7B~52GB~16GB~5GB
Phi-3 3.8B~28GB~9GB~3GB
Qwen 2 7B~52GB~16GB~5GB

2. 환경 설정

2.1 GPU 요구사항

GPUVRAM학습 가능 모델 (QLoRA)
T4 (Colab Free)16GB7B~8B (seq_len 1024)
A10G24GB7B~13B
RTX 409024GB7B~13B
A100 40GB40GB7B~70B
A100 80GB80GB70B+
Apple M2 Ultra192GBCPU 학습 (느림)

2.2 Google Colab 설정

# Colab에서 Unsloth 설치 (T4 GPU 기준)
# 런타임 -> 런타임 유형 변경 -> T4 GPU 선택

# 1. Unsloth 설치
!pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
!pip install --no-deps "xformers<0.0.27" "trl<0.9.0" peft accelerate bitsandbytes

# 2. GPU 확인
import torch
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1024**3:.1f} GB")

2.3 로컬 환경 설정

# Conda 환경 생성
conda create -n unsloth python=3.11
conda activate unsloth

# PyTorch 설치 (CUDA 12.1)
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

# Unsloth 설치
pip install "unsloth[cu121] @ git+https://github.com/unslothai/unsloth.git"
pip install --no-deps trl peft accelerate bitsandbytes

# 설치 확인
python -c "from unsloth import FastLanguageModel; print('Unsloth OK')"

2.4 Docker 환경

FROM nvidia/cuda:12.1.0-devel-ubuntu22.04

RUN apt-get update && apt-get install -y python3.11 python3-pip git

RUN pip install torch --index-url https://download.pytorch.org/whl/cu121
RUN pip install "unsloth[cu121] @ git+https://github.com/unslothai/unsloth.git"
RUN pip install --no-deps trl peft accelerate bitsandbytes

WORKDIR /workspace
CMD ["python3"]

3. Unsloth 파인튜닝 단계별 가이드

3.1 모델 로딩

from unsloth import FastLanguageModel
import torch

# 모델과 토크나이저 로딩
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Meta-Llama-3.1-8B-bnb-4bit",  # 4bit 사전 양자화 모델
    max_seq_length=2048,    # 최대 시퀀스 길이
    dtype=None,             # 자동 감지 (A100: bfloat16, 기타: float16)
    load_in_4bit=True,      # 4bit 양자화 로드
)

# GPU 메모리 확인
gpu_stats = torch.cuda.get_device_properties(0)
start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
max_memory = round(gpu_stats.total_mem / 1024 / 1024 / 1024, 3)
print(f"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.")
print(f"{start_gpu_memory} GB of memory reserved.")

사전 양자화 모델 추천:

용도모델크기
일반 한국어unsloth/Meta-Llama-3.1-8B-bnb-4bit~5GB
한국어 특화beomi/Llama-3-Open-Ko-8B-bnb-4bit~5GB
코딩unsloth/Mistral-7B-v0.3-bnb-4bit~4.5GB
경량unsloth/Phi-3.5-mini-instruct-bnb-4bit~2.5GB
다국어unsloth/Qwen2.5-7B-bnb-4bit~4.5GB

3.2 LoRA 어댑터 설정

# LoRA 어댑터 추가
model = FastLanguageModel.get_peft_model(
    model,
    r=16,                          # LoRA 랭크 (8, 16, 32, 64)
    target_modules=[               # LoRA를 적용할 모듈
        "q_proj", "k_proj", "v_proj", "o_proj",  # Attention
        "gate_proj", "up_proj", "down_proj",       # MLP
    ],
    lora_alpha=16,                 # LoRA alpha (보통 r과 같게)
    lora_dropout=0,                # Unsloth에서는 0이 최적
    bias="none",                   # bias 학습 안 함
    use_gradient_checkpointing="unsloth",  # Unsloth 최적화 체크포인팅
    random_state=3407,
    use_rslora=False,              # Rank-Stabilized LoRA (실험적)
    loftq_config=None,             # LoftQ 설정
)

# 학습 가능한 파라미터 확인
def print_trainable_parameters(model):
    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    total = sum(p.numel() for p in model.parameters())
    print(f"학습 가능: {trainable:,} / 전체: {total:,} = {trainable/total:.2%}")

print_trainable_parameters(model)
# 학습 가능: 41,943,040 / 전체: 8,030,261,248 = 0.52%

LoRA 랭크 선택 가이드:

LoRA r파라미터 수VRAM 추가권장 사용
8~21M~80MB간단한 태스크, VRAM 제한
16~42M~160MB일반적 권장값
32~84M~320MB복잡한 태스크
64~168M~640MB대규모 데이터, 높은 표현력 필요
128~336M~1.3GB실험적, Full FT에 근접

4. 데이터 준비

4.1 Chat Template 포매팅

# Alpaca 프롬프트 템플릿
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

# 데이터셋 포매팅 함수
EOS_TOKEN = tokenizer.eos_token

def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs = examples["input"]
    outputs = examples["output"]
    texts = []
    for instruction, input_text, output in zip(instructions, inputs, outputs):
        text = alpaca_prompt.format(instruction, input_text, output) + EOS_TOKEN
        texts.append(text)
    return {"text": texts}

4.2 데이터셋 로딩 및 변환

from datasets import load_dataset

# KoAlpaca 데이터셋 로딩
dataset = load_dataset("beomi/KoAlpaca-v1.1a", split="train")

# 포맷 변환
def format_koalpaca(examples):
    texts = []
    for instruction, output in zip(examples["instruction"], examples["output"]):
        text = alpaca_prompt.format(instruction, "", output) + EOS_TOKEN
        texts.append(text)
    return {"text": texts}

dataset = dataset.map(format_koalpaca, batched=True)

# ShareGPT 형식 데이터 로딩 (다중 턴)
sharegpt_dataset = load_dataset("philschmid/sharegpt-raw", split="train")

def format_sharegpt(examples):
    texts = []
    for conversations in examples["conversations"]:
        text = ""
        for turn in conversations:
            if turn["from"] == "human":
                text += f"### Human:\n{turn['value']}\n\n"
            elif turn["from"] == "gpt":
                text += f"### Assistant:\n{turn['value']}\n\n"
        text += EOS_TOKEN
        texts.append(text)
    return {"text": texts}

# OpenAI Messages 형식 (Llama 3 chat template 사용)
def format_openai_messages(examples):
    texts = []
    for messages in examples["messages"]:
        text = tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=False,
        )
        texts.append(text)
    return {"text": texts}

4.3 Max Sequence Length 고려사항

# 시퀀스 길이 분포 분석
def analyze_sequence_lengths(dataset, tokenizer):
    lengths = []
    for item in dataset:
        tokens = tokenizer.encode(item["text"])
        lengths.append(len(tokens))

    import numpy as np
    print(f"평균 길이: {np.mean(lengths):.0f}")
    print(f"중앙값: {np.median(lengths):.0f}")
    print(f"95 퍼센타일: {np.percentile(lengths, 95):.0f}")
    print(f"99 퍼센타일: {np.percentile(lengths, 99):.0f}")
    print(f"최대 길이: {max(lengths)}")

    # 권장 max_seq_length = 95 퍼센타일
    recommended = int(np.percentile(lengths, 95))
    print(f"\n권장 max_seq_length: {recommended}")
    return lengths

analyze_sequence_lengths(dataset, tokenizer)

5. 학습 설정

5.1 SFTTrainer 설정

from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    dataset_num_proc=2,          # 데이터 전처리 병렬 수
    packing=False,               # 짧은 시퀀스 패킹 (True: 메모리 효율)
    args=TrainingArguments(
        # === 기본 설정 ===
        output_dir="./outputs",
        num_train_epochs=3,

        # === 배치 & 메모리 ===
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,  # 유효 배치 = 2 * 4 = 8

        # === 학습률 ===
        learning_rate=2e-4,             # QLoRA 권장 학습률
        lr_scheduler_type="cosine",     # 코사인 스케줄러
        warmup_steps=5,                 # 워밍업 스텝

        # === 정밀도 ===
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),

        # === 로깅 ===
        logging_steps=1,
        logging_dir="./logs",
        report_to="wandb",             # Weights & Biases 연동

        # === 저장 ===
        save_strategy="steps",
        save_steps=100,
        save_total_limit=3,

        # === 최적화 ===
        optim="adamw_8bit",            # 8bit AdamW (메모리 절약)
        weight_decay=0.01,
        max_grad_norm=0.3,
        seed=3407,
    ),
)

5.2 학습 파라미터 상세 설명

학습률 (Learning Rate) 가이드:

시나리오권장 학습률이유
QLoRA 기본2e-4QLoRA 논문 권장값
큰 데이터셋 (100K+)1e-4과적합 방지
작은 데이터셋 (1K 이하)5e-5 ~ 1e-4세밀한 학습
도메인 적응2e-5 ~ 5e-5기존 지식 보존
Continued Pre-training1e-5 ~ 5e-5안정적 학습

배치 크기 vs Gradient Accumulation:

# 동일한 유효 배치 크기 8 달성하는 두 가지 방법

# 방법 1: 큰 배치 (VRAM 많이 필요)
per_device_train_batch_size = 8
gradient_accumulation_steps = 1
# 유효 배치 = 8 * 1 = 8, VRAM: ~12GB

# 방법 2: 작은 배치 + Gradient Accumulation (VRAM 적게 필요)
per_device_train_batch_size = 2
gradient_accumulation_steps = 4
# 유효 배치 = 2 * 4 = 8, VRAM: ~6GB
# 주의: 학습 속도는 조금 느려짐

5.3 Wandb 연동

import wandb

# Wandb 로그인 및 프로젝트 설정
wandb.login(key="your-wandb-api-key")
wandb.init(
    project="korean-llm-finetuning",
    name="llama3-8b-koalpaca-qlora",
    config={
        "model": "Meta-Llama-3.1-8B",
        "dataset": "KoAlpaca-v1.1a",
        "lora_r": 16,
        "learning_rate": 2e-4,
        "epochs": 3,
    },
)

5.4 학습 실행

# 학습 시작
trainer_stats = trainer.train()

# 학습 결과 출력
print(f"학습 시간: {trainer_stats.metrics['train_runtime']:.2f}초")
print(f"최종 Loss: {trainer_stats.metrics['train_loss']:.4f}")

# GPU 메모리 사용량 확인
used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
print(f"최대 VRAM 사용: {used_memory} GB")

6. VRAM 최적화 기법

6.1 Gradient Checkpointing

# Unsloth 최적화 Gradient Checkpointing
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                     "gate_proj", "up_proj", "down_proj"],
    use_gradient_checkpointing="unsloth",  # 핵심! 30% VRAM 절약
)

# 일반 gradient checkpointing vs Unsloth
# "unsloth": Unsloth 최적화 버전 (더 빠르고 메모리 효율적)
# True: 표준 PyTorch gradient checkpointing
# False: 사용 안 함 (가장 빠르지만 메모리 많이 사용)

6.2 Flash Attention 2

# Unsloth는 Flash Attention 2를 자동으로 사용
# 별도 설정 불필요!

# 수동으로 확인하려면:
print(f"Flash Attention 사용: {hasattr(model.config, '_attn_implementation')}")

6.3 시퀀스 패킹

# 짧은 시퀀스를 하나로 묶어 GPU 활용률 향상
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    packing=True,           # 시퀀스 패킹 활성화
    max_seq_length=2048,    # 패킹된 전체 길이
)

# 패킹 효과:
# 패킹 OFF: [토큰토큰PAD PAD PAD PAD] [토큰PAD PAD PAD PAD PAD]
# 패킹 ON:  [토큰토큰토큰SEP토큰토큰토큰] -> GPU 활용률 증가

6.4 VRAM 사용량 표 (Unsloth QLoRA 기준)

모델Batch=1Batch=2Batch=4Batch=8
Llama 3 8B4.2GB5.8GB8.5GB14.2GB
Mistral 7B3.8GB5.2GB7.8GB13.0GB
Phi-3 3.8B2.4GB3.2GB4.8GB7.6GB
Qwen 2 7B3.8GB5.2GB7.8GB13.0GB
Llama 3 70B36GB42GB56GBOOM

* max_seq_length=2048, gradient_checkpointing="unsloth" 기준


7. 모델 내보내기 및 변환

7.1 LoRA 어댑터 저장

# LoRA 어댑터만 저장 (작은 크기)
model.save_pretrained("lora_adapter")
tokenizer.save_pretrained("lora_adapter")

# 저장된 파일 확인
import os
for f in os.listdir("lora_adapter"):
    size = os.path.getsize(f"lora_adapter/{f}") / 1024 / 1024
    print(f"  {f}: {size:.1f} MB")

# adapter_config.json: 0.0 MB
# adapter_model.safetensors: 160.0 MB  <- LoRA 가중치
# tokenizer.json: 17.1 MB

7.2 어댑터 병합 (Merge)

# LoRA 어댑터를 베이스 모델과 병합
merged_model = model.merge_and_unload()

# 병합된 모델 저장
merged_model.save_pretrained("merged_model")
tokenizer.save_pretrained("merged_model")

7.3 GGUF 변환 (llama.cpp용)

# Unsloth의 내장 GGUF 변환 기능
# 다양한 양자화 레벨 지원

# Q4_K_M: 가장 일반적 (품질/크기 균형)
model.save_pretrained_gguf(
    "model_gguf",
    tokenizer,
    quantization_method="q4_k_m",
)

# Q5_K_M: 더 높은 품질
model.save_pretrained_gguf(
    "model_q5",
    tokenizer,
    quantization_method="q5_k_m",
)

# Q8_0: 최고 품질 (크기 큼)
model.save_pretrained_gguf(
    "model_q8",
    tokenizer,
    quantization_method="q8_0",
)

# F16: 양자화 없음 (가장 큼)
model.save_pretrained_gguf(
    "model_f16",
    tokenizer,
    quantization_method="f16",
)

GGUF 양자화 비교:

양자화파일 크기 (8B)품질추론 속도권장
Q4_K_M~4.5GB좋음빠름일반 사용
Q5_K_M~5.5GB매우 좋음보통품질 중시
Q8_0~8.0GB우수느림최고 품질
F16~16GB원본가장 느림참고용

7.4 GPTQ 변환

# GPTQ 양자화 (GPU 추론용)
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig

quantize_config = BaseQuantizeConfig(
    bits=4,
    group_size=128,
    desc_act=False,
)

# 캘리브레이션 데이터 준비
calibration_data = [
    tokenizer(text, return_tensors="pt")
    for text in calibration_texts[:128]
]

# GPTQ 양자화 실행
gptq_model = AutoGPTQForCausalLM.from_pretrained(
    "merged_model",
    quantize_config=quantize_config,
)
gptq_model.quantize(calibration_data)
gptq_model.save_quantized("model_gptq")

7.5 Hugging Face Hub 업로드

# 모델을 Hugging Face Hub에 업로드

# LoRA 어댑터만 업로드
model.push_to_hub(
    "my-org/llama3-8b-korean-lora",
    token="hf_xxxxx",
    private=True,
)
tokenizer.push_to_hub(
    "my-org/llama3-8b-korean-lora",
    token="hf_xxxxx",
    private=True,
)

# GGUF 파일 업로드
model.push_to_hub_gguf(
    "my-org/llama3-8b-korean-gguf",
    tokenizer,
    quantization_method="q4_k_m",
    token="hf_xxxxx",
)

8. 평가 및 테스트

8.1 파인튜닝된 모델로 추론

# 추론 모드로 전환
FastLanguageModel.for_inference(model)

# 단일 프롬프트 추론
def generate_response(instruction, input_text=""):
    prompt = alpaca_prompt.format(instruction, input_text, "")
    inputs = tokenizer([prompt], return_tensors="pt").to("cuda")

    outputs = model.generate(
        **inputs,
        max_new_tokens=512,
        temperature=0.7,
        top_p=0.9,
        repetition_penalty=1.15,
        do_sample=True,
    )

    response = tokenizer.batch_decode(outputs)[0]
    # Response 부분만 추출
    response = response.split("### Response:\n")[-1]
    response = response.replace(tokenizer.eos_token, "").strip()
    return response

# 테스트
test_questions = [
    "한국의 전통 명절에 대해 설명해주세요.",
    "파이썬에서 데코레이터의 동작 원리를 설명해주세요.",
    "건강한 식습관을 위한 팁을 알려주세요.",
]

for q in test_questions:
    print(f"Q: {q}")
    print(f"A: {generate_response(q)}")
    print("-" * 80)

8.2 생성 파라미터 튜닝

# 생성 파라미터별 효과
generation_configs = {
    "정확한 답변 (factual)": {
        "temperature": 0.1,
        "top_p": 0.9,
        "repetition_penalty": 1.0,
    },
    "창의적 답변 (creative)": {
        "temperature": 0.8,
        "top_p": 0.95,
        "repetition_penalty": 1.15,
    },
    "균형잡힌 답변 (balanced)": {
        "temperature": 0.5,
        "top_p": 0.9,
        "repetition_penalty": 1.1,
    },
}

8.3 lm-eval-harness 벤치마크

# lm-eval-harness로 벤치마크 평가
pip install lm-eval

# 한국어 벤치마크 평가
lm_eval --model hf \
    --model_args pretrained=./merged_model \
    --tasks kobest_boolq,kobest_copa,kobest_hellaswag,kobest_sentineg,kobest_wic \
    --batch_size 4 \
    --output_path ./eval_results
# Python에서 실행
from lm_eval import evaluator

results = evaluator.simple_evaluate(
    model="hf",
    model_args="pretrained=./merged_model",
    tasks=["kobest_boolq", "kobest_copa", "kobest_hellaswag"],
    batch_size=4,
)

for task, metrics in results["results"].items():
    print(f"{task}: acc={metrics.get('acc', 'N/A')}")

9. 고급 기법

9.1 Multi-GPU 학습 (DeepSpeed ZeRO)

# deepspeed_config.json
"""
{
    "zero_optimization": {
        "stage": 2,
        "offload_optimizer": {
            "device": "cpu",
            "pin_memory": true
        },
        "allgather_partitions": true,
        "reduce_scatter": true
    },
    "bf16": {
        "enabled": true
    },
    "train_batch_size": "auto",
    "train_micro_batch_size_per_gpu": "auto"
}
"""

# 실행
# deepspeed --num_gpus 4 train.py --deepspeed deepspeed_config.json

9.2 DPO 학습

from trl import DPOTrainer, DPOConfig
from unsloth import FastLanguageModel, PatchDPOTrainer

# DPO 패치 적용
PatchDPOTrainer()

# DPO 데이터셋 준비
dpo_dataset = load_dataset("argilla/ultrafeedback-binarized-preferences", split="train")

# DPO Trainer 설정
dpo_trainer = DPOTrainer(
    model=model,
    ref_model=None,           # Unsloth에서는 None (자동 처리)
    args=DPOConfig(
        output_dir="./dpo_output",
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        learning_rate=5e-7,       # DPO는 낮은 학습률
        num_train_epochs=1,
        beta=0.1,                 # DPO beta (KL divergence 가중치)
        fp16=not is_bfloat16_supported(),
        bf16=is_bfloat16_supported(),
        logging_steps=1,
    ),
    train_dataset=dpo_dataset,
    tokenizer=tokenizer,
)

dpo_trainer.train()

9.3 Continued Pre-training (도메인 적응)

# 도메인 특화 텍스트로 Continued Pre-training
from trl import SFTTrainer

# 도메인 텍스트 데이터 (의료, 법률, 금융 등)
domain_dataset = load_dataset("my-org/medical-korean-corpus", split="train")

# Continued Pre-training은 낮은 학습률 사용
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=domain_dataset,
    dataset_text_field="text",
    max_seq_length=4096,     # 긴 문서
    packing=True,            # 효율성 위해 패킹 사용
    args=TrainingArguments(
        output_dir="./cpt_output",
        learning_rate=2e-5,  # 매우 낮은 학습률
        num_train_epochs=1,  # 1 epoch면 충분
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        optim="adamw_8bit",
        warmup_ratio=0.1,
    ),
)

trainer.train()

10. 일반적인 문제와 해결법

10.1 OOM (Out of Memory) 오류

# 증상: CUDA out of memory
# RuntimeError: CUDA out of memory.
# Tried to allocate 256.00 MiB

# 해결법 순서:
# 1. batch_size 줄이기
per_device_train_batch_size = 1  # 최소값

# 2. gradient_accumulation_steps 늘리기
gradient_accumulation_steps = 8

# 3. max_seq_length 줄이기
max_seq_length = 1024  # 2048 -> 1024

# 4. LoRA rank 줄이기
r = 8  # 16 -> 8

# 5. gradient checkpointing 확인
use_gradient_checkpointing = "unsloth"

# 6. 캐시 비우기
torch.cuda.empty_cache()
import gc
gc.collect()

10.2 NaN Loss

# 증상: loss가 NaN으로 발산
# 원인: 학습률이 너무 높거나 데이터 문제

# 해결법:
# 1. 학습률 낮추기
learning_rate = 1e-5  # 2e-4 -> 1e-5

# 2. max_grad_norm 설정
max_grad_norm = 0.3  # gradient clipping

# 3. 데이터 검증
def check_data_issues(dataset, tokenizer):
    """데이터 문제 검사"""
    issues = []
    for i, item in enumerate(dataset):
        text = item["text"]
        # 빈 텍스트 확인
        if not text.strip():
            issues.append(f"[{i}] 빈 텍스트")
        # 너무 긴 텍스트
        tokens = tokenizer.encode(text)
        if len(tokens) > 4096:
            issues.append(f"[{i}] 너무 긴 텍스트: {len(tokens)} tokens")
        # 특수 문자만 있는 경우
        if not any(c.isalnum() for c in text):
            issues.append(f"[{i}] 유효한 텍스트 없음")
    return issues

10.3 Catastrophic Forgetting (파국적 망각)

# 증상: 파인튜닝 후 기존 지식이 사라짐
# 해결법:

# 1. 낮은 학습률 사용
learning_rate = 5e-5

# 2. 적은 epoch (1-3)
num_train_epochs = 1

# 3. 데이터에 일반 지식 혼합
# 원래 데이터 80% + 일반 지식 데이터 20%

# 4. LoRA rank 낮추기 (변경 폭 제한)
r = 8

# 5. 정규화 강화
weight_decay = 0.1

10.4 과적합 탐지

# 과적합 지표
# 1. Train Loss는 줄어드는데 Eval Loss가 증가
# 2. 학습 데이터를 거의 외우는 수준의 출력
# 3. 새로운 프롬프트에 대한 성능 저하

# 해결법:
# 1. 데이터 양 늘리기
# 2. 정규화 (dropout, weight_decay)
# 3. Early stopping
from transformers import EarlyStoppingCallback

trainer = SFTTrainer(
    callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],
    args=TrainingArguments(
        evaluation_strategy="steps",
        eval_steps=50,
        load_best_model_at_end=True,
    ),
)

11. 퀴즈

Q1. LoRA에서 r=16이면 원본 가중치 대비 몇 %의 파라미터만 학습하나요?

정답: 약 0.5% (99.5% 절약)

d=4096인 경우:

  • Full: 4096 x 4096 = 16,777,216
  • LoRA r=16: (4096 x 16) + (16 x 4096) = 131,072
  • 비율: 131,072 / 16,777,216 = 0.78%

실제로 여러 모듈(q, k, v, o, gate, up, down)에 적용하므로 총 파라미터 대비 약 0.5% 수준입니다.

Q2. QLoRA의 NF4 양자화가 일반 INT4보다 나은 이유는?

정답: 가중치의 정규분포 특성을 활용한 최적 양자화

NF4는 신경망 가중치가 대체로 정규분포를 따른다는 점을 이용합니다. 정규분포의 분위수에 맞춰 16개 양자화 값을 배치하므로, 균일 분할인 INT4보다 정보 손실이 적습니다. 이론적으로 정규분포 데이터에 대해 최적에 가까운 양자화를 달성합니다.

Q3. Unsloth가 기존 HuggingFace PEFT보다 2배 빠른 핵심 이유는?

정답: 커스텀 Triton 커널

Unsloth는 Attention, MLP, Cross-Entropy Loss 등의 핵심 연산을 Triton으로 작성한 커스텀 GPU 커널로 대체합니다. 이 커널들은 메모리 접근 패턴을 최적화하고, 불필요한 중간 텐서 생성을 줄여 2배 빠른 학습과 60% 메모리 절약을 달성합니다.

Q4. Gradient Checkpointing의 원리와 트레이드오프는?

정답:

원리: Forward pass에서 중간 활성화값(activation)을 메모리에 저장하지 않고, Backward pass에서 필요할 때 다시 계산합니다.

트레이드오프:

  • 장점: VRAM 사용량 약 30~50% 감소
  • 단점: 재계산으로 인해 학습 시간 약 20~30% 증가

Unsloth의 커스텀 gradient checkpointing은 일반 PyTorch 구현보다 더 효율적이어서 시간 증가가 적습니다.

Q5. GGUF Q4_K_M과 Q8_0의 차이와 각각의 권장 사용 시나리오는?

정답:

Q4_K_M (4-bit Mixed):

  • 파일 크기: 원본의 약 28% (8B 모델 기준 약 4.5GB)
  • 품질: 원본 대비 약간의 성능 저하
  • 속도: 빠름
  • 권장: 일상 사용, 모바일/엣지 배포, VRAM/RAM 제한 환경

Q8_0 (8-bit):

  • 파일 크기: 원본의 약 50% (8B 모델 기준 약 8GB)
  • 품질: 원본에 매우 가까움
  • 속도: Q4 대비 느림
  • 권장: 품질 최우선, 충분한 메모리가 있는 환경, 정확한 추론이 필요한 서비스

12. 참고 자료

  1. LoRA: Low-Rank Adaptation of Large Language Models - Hu et al., 2021
  2. QLoRA: Efficient Finetuning of Quantized LLMs - Dettmers et al., 2023
  3. Unsloth Documentation - github.com/unslothai/unsloth
  4. PEFT: Parameter-Efficient Fine-Tuning - HuggingFace
  5. TRL: Transformer Reinforcement Learning - HuggingFace
  6. Flash Attention 2 - Dao et al., 2023
  7. LLM.int8(): 8-bit Matrix Multiplication - Dettmers et al., 2022
  8. llama.cpp - github.com/ggerganov/llama.cpp
  9. GPTQ: Accurate Post-Training Quantization - Frantar et al., 2022
  10. DeepSpeed ZeRO - Rajbhandari et al., 2020
  11. Direct Preference Optimization - Rafailov et al., 2023
  12. Scaling Data-Constrained Language Models - Muennighoff et al., 2023
  13. Training Compute-Optimal Large Language Models (Chinchilla) - Hoffmann et al., 2022
  14. The Llama 3 Herd of Models - Meta AI, 2024