5. DPO 훈련
개요
DPO(Direct Preference Optimization)는 선호/비선호 응답 쌍을 사용하여 모델을 정렬(align)하는 훈련 방식입니다. EulerForge에서 DPO는 인젝션 전략과 독립적으로 동작하며, 모든 전략(dense_lora, mixture_lora, moe_expert_lora, native_moe_expert_lora)과 결합할 수 있습니다.
- 적합한 용도: RLHF 대안, 모델 정렬, 선호 기반 파인튜닝
- SFT와의 핵심 차이: 단일 응답 학습 vs 선호/비선호 쌍 비교 학습
- 참조 프리셋:
configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml - 전제 조건: 반드시 SFT 훈련을 먼저 완료한 모델에 적용하세요.
SFT를 먼저 해야 하는 이유
DPO/ORPO 등 선호 학습은 이미 instruction-following 능력이 있는 모델에 적용해야 효과적입니다. Base 모델에 바로 DPO를 적용하면 "어떻게 대답해야 하는지"를 모르는 상태에서 "어떤 대답이 더 나은지"만 학습하게 되어 벤치 점수가 오히려 하락할 수 있습니다.
올바른 순서: SFT (instruction 학습) → DPO (선호 정렬) 잘못된 순서: Base Model → DPO (기초 능력 없이 선호만 학습)
SFT를 먼저 진행하고, 그 체크포인트(
final/)를 DPO의model_name으로 지정하세요.
SFT vs DPO 비교
| 항목 | SFT | DPO |
|---|---|---|
| 데이터 형식 | 단일 응답 (input_ids, labels) |
선호/비선호 쌍 (chosen_*, rejected_*) |
| 손실 함수 | Cross-entropy loss | DPO loss (로그 확률 비율) |
| 참조 모델 | 불필요 | 필요 (어댑터 비활성화로 대체) |
| 설정 키 | training.type: sft |
training.type: dpo, training.dpo_beta |
| 유효 배치 크기 | 배치 크기 그대로 | 배치 크기 × 2 (chosen + rejected) |
| 일반적 학습률 | 1.0e-5 |
5.0e-6 (더 작게) |
사전 요구 사항
1. DPO 데이터 형식
DPO 훈련에는 선호(chosen)/비선호(rejected) 응답 쌍이 필요합니다.
Raw 데이터 (권장)
data.format=raw를 사용하면 텍스트 JSONL을 훈련 시 자동 토큰화합니다:
{"prompt": "질문 내용", "chosen": "선호 응답", "rejected": "비선호 응답"}
data/dpo_10k_raw.jsonl: 표준 prompted_preference 형식 (데이터 전처리에서 변환)- 프롬프트 토큰은 자동으로
-100으로 마스킹됩니다.
Processed 데이터
사전 토큰화된 JSONL도 지원합니다:
| 필드 | 타입 | 설명 |
|---|---|---|
chosen_input_ids |
List[int] |
선호 응답의 토큰 ID |
chosen_labels |
List[int] |
선호 응답의 레이블 (-100으로 프롬프트 마스킹) |
rejected_input_ids |
List[int] |
비선호 응답의 토큰 ID |
rejected_labels |
List[int] |
비선호 응답의 레이블 (-100으로 프롬프트 마스킹) |
2. DPO 동작 원리
핵심 아이디어
DPO는 정책 모델(policy)과 참조 모델(reference)의 로그 확률 비율을 비교하여, 선호 응답의 확률을 높이고 비선호 응답의 확률을 낮춥니다.
EulerForge의 메모리 효율적 접근
일반적인 DPO는 정책 모델과 참조 모델 두 개를 메모리에 로드해야 합니다. EulerForge는 AdapterLayerMixin을 활용하여 단일 모델로 두 역할을 수행합니다.
[하나의 모델]
│
├── 정책 모드 (기본): base_layer + LoRA delta → 정책 로그 확률
│
└── 참조 모드 (어댑터 비활성화): base_layer만 → 참조 로그 확률
Forward 과정
[배치: chosen₁, rejected₁, chosen₂, rejected₂, ...]
│
├── 1) 정책 Forward (어댑터 활성화)
│ 모델(x) = base + LoRA/MoE delta
│ → policy_chosen_logps (짝수 인덱스)
│ → policy_rejected_logps (홀수 인덱스)
│
└── 2) 참조 Forward (no_grad)
[Pipeline DPO] → 초기 LoRA 상태(SFT)로 복원
[Fresh DPO] → 어댑터 비활성화 (base만)
→ ref_chosen_logps
→ ref_rejected_logps
DPO 손실 함수
π_logratios = policy_chosen_logps - policy_rejected_logps
ref_logratios = ref_chosen_logps - ref_rejected_logps
logits = π_logratios - ref_logratios
loss = -log(σ(β × logits))
β(dpo_beta): 선호 강도를 조절합니다. 클수록 선호/비선호 차이를 강하게 강제합니다.σ: 시그모이드 함수. 로그 확률 비율 차이를 0~1 범위로 정규화합니다.
3. AdapterLayerMixin 메커니즘
모든 어댑터 모듈(LoRALinear, MixtureLoRALinear)은 AdapterLayerMixin을 상속합니다.
동작 방식
class LoRALinear(nn.Module, AdapterLayerMixin):
def forward(self, x):
if self.is_adapter_disabled(): # 참조 모드
return self.base_layer(x) # base만 반환
base_out = self.base_layer(x)
return base_out + self._lora_forward(x) # 정책 모드
전략별 비활성화 동작
| 어댑터 모듈 | 비활성화 시 동작 |
|---|---|
LoRALinear |
base_layer(x) 반환 (LoRA delta 건너뜀) |
MixtureLoRALinear |
base_layer(x) 반환 (라우터 + 전문가 건너뜀) |
참조 Forward 코드
# Pipeline DPO (SFT→DPO): 초기 LoRA 상태(SFT)를 reference로 사용
ref_ctx = (_use_reference_lora(model, ref_lora_sd)
if ref_lora_sd is not None
else disable_adapter_layers(model))
with torch.no_grad():
with ref_ctx:
ref_outputs = model(input_ids=input_ids, attention_mask=attention_mask)
- Pipeline DPO:
_use_reference_lora(model, ref_sd)— SFT 가중치로 일시 복원 - Fresh DPO:
disable_adapter_layers(model)— base model을 reference로 사용 torch.no_grad(): 참조 모델은 그래디언트 불필요 (메모리 절약)- 컨텍스트 매니저 종료 시 자동으로 현재 LoRA 상태가 복원됩니다.
주의: PPO의 KL penalty 계산에서도 동일한 reference 메커니즘이 적용됩니다.
4. SFT에서 DPO로 전환하기
SFT 프리셋을 DPO로 변환할 때 변경해야 하는 부분은 최소한입니다. injection과 moe 섹션은 동일하게 유지합니다.
변경 사항 요약
training:
- type: sft
+ type: dpo
+ dpo_beta: 0.1
- lr: 1.0e-5
+ lr: 5.0e-6
- batch_size: 4
+ batch_size: 2
- grad_accum_steps: 4
+ grad_accum_steps: 8
- warmup_steps: 200
+ warmup_steps: 100
왜 이렇게 변경하는가?
| 변경 | 이유 |
|---|---|
type: dpo |
DPO 손실 함수와 참조 모델 로직 활성화 |
dpo_beta: 0.1 |
선호 강도 파라미터 (DPO 전용) |
lr 감소 |
DPO는 이미 학습된 모델을 미세 조정하므로 작은 학습률 필요 |
batch_size 감소 |
DPO는 배치당 2배 토큰 (chosen + rejected) 처리하므로 VRAM 절약 |
grad_accum_steps 증가 |
유효 배치 크기를 유지하기 위해 (2 × 8 = 16 ≈ 4 × 4) |
warmup_steps 감소 |
DPO는 이미 SFT된 모델에서 시작하므로 워밍업이 짧아도 됨 |
5. DPO 전용 설정
dpo_beta 파라미터
training:
type: dpo
dpo_beta: 0.1 # 범위: 0.05 ~ 0.5 (일반적으로 0.1)
| 값 | 효과 |
|---|---|
0.05 |
약한 선호 강제. 참조 모델에 가깝게 유지. 발산 위험 낮음. |
0.1 |
표준값. 대부분의 경우 적절한 균형. |
0.5 |
강한 선호 강제. 선호/비선호 차이를 크게 벌림. 과적합 위험. |
dpo_beta가 너무 작으면: 모델이 거의 변하지 않음 (참조 모델과 유사하게 유지)dpo_beta가 너무 크면: 선호 데이터에 과적합, 다양성 감소
6. 전체 설정 파일 해설
configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml 전문:
# ── 모델 정보 ──
device: cuda:0 # GPU 디바이스
backbone: qwen3 # 백본 어댑터: Qwen3Adapter
model_name: Qwen/Qwen3.5-0.8B-Base # HuggingFace 모델 ID
# ── 인젝션 설정 (SFT와 동일) ──
injection:
strategy: moe_expert_lora # 인젝션 전략 (SFT와 동일)
lora_r: 48 # LoRA 랭크
lora_alpha: 96 # 스케일링 팩터 (96/48 = 2.0)
lora_dropout: 0.05 # LoRA 드롭아웃
num_experts: 4 # MoE 전문가 수
top_k: 2 # 토큰당 활성 전문가 수
target_keywords: [gate_proj, up_proj, down_proj] # FFN 타겟
start_layer: 0 # 시작 레이어
num_layers: 0 # 0 = 전체
attn_lora: # 어텐션 LoRA
enabled: true
keywords: [q_proj, v_proj]
# ── MoE 안정성 설정 (SFT와 동일) ──
moe:
router_z_loss_coef: 0.001 # z-loss: 로짓 오버플로우 방지
load_balance:
type: aux_loss # 보조 손실 기반 부하 분산
aux_loss_coef: 0.01 # 보조 손실 가중치
router_dtype: float32 # 라우터 정밀도
# ── 훈련 설정 (DPO 전용 변경점 있음) ──
training:
type: dpo # [DPO] 훈련 타입
dpo_beta: 0.1 # [DPO] 선호 강도 파라미터
phases: # 3페이즈 (SFT와 동일 구조)
- step: 0 # 페이즈 0: 라우터 웜업
trainable: ["router"]
- step: 2000 # 페이즈 1: LoRA 훈련
trainable: ["lora", "attn_lora"]
- step: 8000 # 페이즈 2: 전체 해제
trainable: ["lora", "attn_lora", "router", "base_ffn"]
base_ffn_keywords: ["gate_proj", "up_proj", "down_proj"]
lr: 5.0e-6 # [DPO] SFT(1e-5)보다 낮은 학습률
weight_decay: 0.01 # 가중치 감쇠
warmup_steps: 100 # [DPO] SFT(200)보다 짧은 워밍업
max_train_steps: 15000 # 최대 훈련 스텝
batch_size: 2 # [DPO] SFT(4)보다 작은 배치 (chosen+rejected)
grad_accum_steps: 8 # [DPO] SFT(4)보다 큰 축적 (유효 배치 유지)
max_grad_norm: 1.0 # 그래디언트 클리핑
log_steps: 50 # 로그 출력 간격
save_steps: 1000 # 체크포인트 저장 간격
val_steps: 500 # 검증 간격
7. 실행하기
기본 실행
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml \
--set data.format=raw \
--set data.task=prompted_preference \
--set data.path=data/dpo_10k_raw.jsonl \
--set data.max_length=512
SFT 체크포인트에서 DPO 시작 (권장)
# 1단계: SFT 훈련
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_sft.yml \
--set data.format=raw \
--set data.task=sft \
--set data.path=data/sft_10k_raw.jsonl \
--set data.max_length=512
# 2단계: SFT 체크포인트로부터 DPO 훈련
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml \
--set data.format=raw \
--set data.task=prompted_preference \
--set data.path=data/dpo_10k_raw.jsonl \
--set data.max_length=512 \
--set model_name=/path/to/sft_checkpoint
Reference Model 자동 감지: SFT 체크포인트에서 DPO를 시작하면, 초기 LoRA 상태(SFT 모델)를 reference로 자동 사용합니다. 초기 loss가 ln(2) ≈ 0.693이면 정상입니다. 만약 0.693보다 크면 reference 설정에 문제가 있는 것입니다.
dpo_beta 조정
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml \
--set data.format=raw \
--set data.task=prompted_preference \
--set data.path=data/dpo_10k_raw.jsonl \
--set data.max_length=512 \
--set training.dpo_beta=0.05 # 보수적 정렬
프리플라이트 검사
eulerforge train --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml \
--preflight
8. DPO 메트릭 해석
DPO 훈련 중 다음 메트릭이 로그에 출력됩니다.
| 메트릭 | 의미 | 좋은 추세 |
|---|---|---|
dpo_loss |
DPO 손실값 | 감소 |
reward_chosen |
선호 응답의 보상 | 증가 |
reward_rejected |
비선호 응답의 보상 | 감소 또는 안정 |
reward_margin |
reward_chosen - reward_rejected |
양수로 증가 |
accuracy |
선호 응답 보상 > 비선호 응답 보상인 비율 | 증가 (0.7~0.8 도달 시 양호) |
메트릭 해석 가이드
✓ 양호한 훈련:
dpo_loss: 0.69 → 0.45 (감소)
reward_margin: 0.0 → 1.5 (양수로 증가)
accuracy: 0.5 → 0.75 (증가)
✗ 문제 징후:
accuracy > 0.95 → 과적합 가능성 (dpo_beta 감소 필요)
reward_margin < 0 → 모델이 비선호 응답을 선호 (데이터 또는 β 확인)
reward_margin > 5 → reference에서 과도하게 이탈 (과적합, lr/steps 줄이기)
dpo_loss 발산 → 학습률이 너무 높음
9. 다른 인젝션 전략과 결합하기
DPO는 모든 인젝션 전략과 결합 가능합니다. training 섹션만 변경하면 됩니다.
Plain LoRA + DPO
injection:
strategy: dense_lora # 인젝션 전략
# ... (Plain LoRA 설정)
training:
type: dpo # DPO로 변경
dpo_beta: 0.1
phases:
- step: 0
trainable: ["lora", "attn_lora"] # 단일 페이즈
lr: 5.0e-6
batch_size: 2
grad_accum_steps: 8
LoRA MoE + DPO
injection:
strategy: mixture_lora # 인젝션 전략
# ... (LoRA MoE 설정)
training:
type: dpo # DPO로 변경
dpo_beta: 0.1
phases:
- step: 0
trainable: ["router"] # 2페이즈
- step: 2000
trainable: ["lora", "attn_lora"]
lr: 5.0e-6
batch_size: 2
grad_accum_steps: 8
핵심: 페이즈 스케줄은 인젝션 전략에 의존합니다. DPO 여부와 관계없이 동일한 페이즈 구조를 사용합니다. 변경하는 것은
type,dpo_beta,lr,batch_size,grad_accum_steps등 훈련 파라미터뿐입니다.
10. Phase 0 Router-Only와 DPO 메트릭
MoE 전략에서 Phase 0이 ["router"]만 학습하는 경우, DPO 메트릭이 다음과 같이 나타납니다:
[Phase0] reward_chosen: 0.0000 | reward_rejected: 0.0000 | reward_margin: 0.0000 | accuracy: 0.0000 | dpo_loss: 0.6931
이것은 정상입니다. DPO는 policy_logprob - reference_logprob으로 reward를 계산하는데, Phase 0에서는 LoRA가 freeze 상태이므로 disable_adapter()와 enable_adapter()가 동일한 출력을 생성합니다. 따라서 reward = 0, accuracy = 0, loss = ln(2) ≈ 0.6931.
Phase 1에서 LoRA가 활성화되면 정상적인 DPO 학습이 시작됩니다.
11. max_train_steps와 grad_accum_steps
max_train_steps는 micro-step (forward/backward 횟수) 기준입니다. optimizer step (가중치 업데이트)은 max_train_steps / grad_accum_steps입니다.
training:
max_train_steps: 1500 # micro-steps = forward/backward 1500회
grad_accum_steps: 8 # 8번 축적 후 1번 업데이트
batch_size: 2 # 유효 배치 = 2 × 8 = 16
# → optimizer_steps = 1500 / 8 = 187
# → 총 학습 데이터 = 1500 × 2 = 3000 samples
로그에서 Step 6/187 (micro 50/1500):
- Step 6/187 = optimizer step 6 / 총 187
- micro 50/1500 = micro step 50 / 총 1500
12. 디버깅 및 트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
Phase 0에서 reward=0, loss=0.6931 |
LoRA freeze → policy=reference | 정상 — Phase 1에서 LoRA 활성화 후 해결 |
accuracy가 0.5에서 변하지 않음 |
dpo_beta가 너무 작음 또는 데이터 품질 문제 |
dpo_beta 증가 또는 데이터 확인 |
accuracy가 1.0에 빠르게 수렴 |
과적합 | dpo_beta 감소, 학습률 감소, 에포크 감소 |
reward_margin이 음수 |
데이터의 chosen/rejected가 뒤바뀌었거나 라벨 오류 | 데이터 확인, labels의 -100 마스킹 확인 |
| OOM (메모리 부족) | DPO는 2회 Forward (정책 + 참조) | batch_size 감소, model.load_precision.mode: int4 추가 |
dpo_loss가 NaN |
학습률이 너무 높거나 로그 확률 수치 불안정 | lr 감소, max_grad_norm: 1.0 확인 |
| 데이터 로딩 오류 | JSONL에 필수 필드 누락 | raw 사용 시: prompt, chosen, rejected 확인. processed 사용 시: chosen_input_ids, rejected_input_ids, chosen_labels, rejected_labels 확인 |
다음 단계
- 훈련 파이프라인 가이드: SFT → DPO → ORPO → RM → PPO 순서와 조합 전략은 18_training_pipeline.md 참조
- 각 인젝션 전략의 상세한 설명은 전략별 튜토리얼을 참조하세요:
- Plain LoRA 튜토리얼
- LoRA MoE 튜토리얼
- FFN MoE Expert LoRA 튜토리얼
- Native MoE Expert LoRA 튜토리얼