> EulerForge > 튜토리얼 > 23. Lab: MoE 풀 파이프라인 (SFT → DPO → RM → PPO)

23. Lab: MoE 풀 파이프라인 (SFT → DPO → RM → PPO)

난이도: 최고 | 예상 시간: RTX 5090 1장 기준 24-48시간 | 경로: SFT → DPO → RM → PPO (전체)

이 과제는 2025-2026 최신 LLM이 갖추는 핵심 능력을 단일 모델에 부여하는 풀 파이프라인 실전입니다. DeepSeek-V3/R1의 MoE + reasoning, GPT o-시리즈의 extended thinking, Claude의 structured tool use가 공통적으로 지향하는 모델 특성을 4단계 훈련으로 구현합니다.


1. 과제 목표

최종 모델 프로필: 한국어/영어 이중 언어 + MoE expert specialization + 구조적 추론 + 코드 생성

능력 목표 수준 관련 최신 모델
구조적 추론 (thinking) <think> 태그로 사고 과정 출력 GPT o1/o3, DeepSeek-R1
코드 생성 함수 구현 + 설명 Claude, GPT-4o
수학 풀이 단계별 수식 유도 DeepSeek-V3, Qwen-2.5-Math
한국어 대화 자연스러운 한국어 응답 기존 KoAlpaca 데이터
MoE specialization expert별 도메인 특화 DeepSeek-V3, Mixtral
선호 정렬 정확하고 안전한 응답 선호 RLHF 전체 경로 (DPO → RM → PPO)

2. 전체 파이프라인

Phase 1: 데이터 수집 + 변환 (1-2시간)
    │
Phase 2: SFT — 멀티도메인 기초 (6-8시간)
    │   ├── 한국어 대화 (KoAlpaca)
    │   ├── 수학 (GSM8K + MATH)
    │   ├── 코딩 (CodeAlpaca + CodeFeedback)
    │   └── Thinking 패턴 (규칙 변환 + LLM 생성)
    │
Phase 3: DPO — 멀티도메인 선호 정렬 (4-6시간)
    │   ├── 수학 정답/오답 선호
    │   ├── Thinking/Non-thinking 선호
    │   └── 한국어 응답 품질 선호
    │
Phase 4: RM — Reward Model 학습 (3-4시간)
    │
Phase 5: PPO — RLHF 정책 최적화 (4-6시간)
    │
Phase 6: Bench — 원본 vs 최종 모델 비교

3. 데이터 수집 가이드

공통 데이터 수집 가이드: 19_data_collection.md에서 모든 데이터셋(SFT/DPO, 한국어/영어, 수학/코딩)의 수집 스크립트와 크기 조절 방법을 참조하세요. 아래는 이 과제에 특화된 혼합 가이드입니다.

3.1 SFT 데이터 — 4개 도메인 혼합

도메인 데이터셋 규모 출처
한국어 대화 KoAlpaca 10K data/sft_10k_raw.jsonl (보유)
수학 GSM8K 8.5K HuggingFace gsm8k
수학 (고급) MATH 7.5K HuggingFace hendrycks/competition_math
코딩 CodeAlpaca 10K HuggingFace sahil2801/CodeAlpaca-20k
Thinking 패턴 위 데이터 변환 5-10K 규칙/LLM 생성

총 SFT 데이터: ~40K rows (혼합 후 셔플)

# ── 3.1.1 한국어: 이미 보유
cp data/sft_10k_raw.jsonl data/pipeline/ko_sft.jsonl

# ── 3.1.2 수학 (GSM8K)
python -c "
from datasets import load_dataset
import json

ds = load_dataset('gsm8k', 'main', split='train')
with open('data/pipeline/math_gsm8k_sft.jsonl', 'w') as f:
    for row in ds:
        prompt = f'Solve the following math problem step by step.\n\nProblem: {row[\"question\"]}'
        response = row['answer']
        f.write(json.dumps({'prompt': prompt, 'response': response}, ensure_ascii=False) + '\n')
print(f'GSM8K: {len(ds)} rows')
"

# ── 3.1.3 수학 고급 (MATH)
python -c "
from datasets import load_dataset
import json

ds = load_dataset('hendrycks/competition_math', split='train')
with open('data/pipeline/math_competition_sft.jsonl', 'w') as f:
    count = 0
    for row in ds:
        prompt = f'Solve this math problem. Show your work.\n\n{row[\"problem\"]}'
        response = row['solution']
        if prompt and response:
            f.write(json.dumps({'prompt': prompt, 'response': response}, ensure_ascii=False) + '\n')
            count += 1
print(f'MATH: {count} rows')
"

# ── 3.1.4 코딩
python -c "
from datasets import load_dataset
import json

ds = load_dataset('sahil2801/CodeAlpaca-20k', split='train')
with open('data/pipeline/code_sft.jsonl', 'w') as f:
    count = 0
    for row in ds:
        prompt = row.get('instruction', '')
        inp = row.get('input', '')
        if inp:
            prompt = f'{prompt}\n\nInput: {inp}'
        response = row.get('output', '')
        if prompt and response:
            f.write(json.dumps({'prompt': prompt, 'response': response}, ensure_ascii=False) + '\n')
            count += 1
            if count >= 10000:
                break
print(f'Code: {count} rows')
"

# ── 3.1.5 Thinking 패턴 변환
python -c "
import json

# GSM8K의 step-by-step을 <think> 형식으로
with open('data/pipeline/math_gsm8k_sft.jsonl') as f_in, \
     open('data/pipeline/thinking_sft.jsonl', 'w') as f_out:
    count = 0
    for line in f_in:
        row = json.loads(line)
        answer = row['response']
        parts = answer.rsplit('####', 1)
        if len(parts) == 2:
            reasoning = parts[0].strip()
            final = parts[1].strip()
            response = f'<think>\nLet me work through this step by step.\n{reasoning}\n</think>\nThe answer is {final}.'
        else:
            response = f'<think>\n{answer}\n</think>\n{answer}'
        f_out.write(json.dumps({
            'prompt': row['prompt'].replace('step by step', 'step by step. Show your reasoning in <think> tags'),
            'response': response,
        }, ensure_ascii=False) + '\n')
        count += 1
print(f'Thinking: {count} rows')
"

# ── 3.1.6 병합 + 셔플
cat data/pipeline/ko_sft.jsonl \
    data/pipeline/math_gsm8k_sft.jsonl \
    data/pipeline/math_competition_sft.jsonl \
    data/pipeline/code_sft.jsonl \
    data/pipeline/thinking_sft.jsonl \
    | shuf --random-source=<(yes 42) > data/pipeline/all_sft_raw.jsonl

wc -l data/pipeline/all_sft_raw.jsonl
# 예상: ~40K rows

3.2 DPO 데이터 — 3가지 선호 축

mkdir -p data/pipeline

# ── 수학 정답/오답 선호
python -c "
from datasets import load_dataset
import json, random
random.seed(42)

ds = load_dataset('gsm8k', 'main', split='train')
with open('data/pipeline/math_dpo.jsonl', 'w') as f:
    for row in ds:
        prompt = f'Solve: {row[\"question\"]}'
        chosen = row['answer']
        parts = chosen.rsplit('####', 1)
        if len(parts) == 2:
            wrong = random.randint(1, 9999)
            rejected = parts[0] + f'#### {wrong}'
        else:
            rejected = 'I am not sure about this problem.'
        f.write(json.dumps({
            'prompt': prompt, 'chosen': chosen, 'rejected': rejected
        }, ensure_ascii=False) + '\n')
print(f'Math DPO: {len(ds)} rows')
"

# ── 한국어 선호 (이미 보유)
cp data/dpo_10k_raw.jsonl data/pipeline/ko_dpo.jsonl

# ── Thinking 선호 (thinking 있음 > 없음)
python -c "
import json

with open('data/pipeline/thinking_sft.jsonl') as f_think, \
     open('data/pipeline/math_gsm8k_sft.jsonl') as f_plain, \
     open('data/pipeline/thinking_dpo.jsonl', 'w') as f_out:
    think_data = {json.loads(l)['prompt']: json.loads(l)['response'] for l in f_think}
    count = 0
    for line in f_plain:
        row = json.loads(line)
        # thinking 버전이 있는지 찾기
        for tp, tr in think_data.items():
            if row['prompt'][:50] in tp:
                f_out.write(json.dumps({
                    'prompt': tp,
                    'chosen': tr,
                    'rejected': row['response'],
                }, ensure_ascii=False) + '\n')
                count += 1
                break
        if count >= 5000:
            break
print(f'Thinking DPO: {count} rows')
"

# ── 병합
cat data/pipeline/math_dpo.jsonl \
    data/pipeline/ko_dpo.jsonl \
    data/pipeline/thinking_dpo.jsonl \
    | shuf --random-source=<(yes 42) > data/pipeline/all_dpo_raw.jsonl

wc -l data/pipeline/all_dpo_raw.jsonl

3.3 RM/PPO 데이터

RM은 DPO 데이터를 재사용하고, PPO는 프롬프트만 필요합니다.

# RM: DPO 데이터 그대로 사용
cp data/pipeline/all_dpo_raw.jsonl data/pipeline/rm_raw.jsonl

# PPO: 프롬프트만 추출
python -c "
import json
seen = set()
with open('data/pipeline/all_sft_raw.jsonl') as f_in, \
     open('data/pipeline/ppo_prompts_raw.jsonl', 'w') as f_out:
    count = 0
    for line in f_in:
        row = json.loads(line)
        p = row['prompt'][:200]
        if p not in seen:
            seen.add(p)
            f_out.write(json.dumps({'prompt': row['prompt']}, ensure_ascii=False) + '\n')
            count += 1
        if count >= 5000:
            break
print(f'PPO prompts: {count}')
"

4. Phase 2: MoE SFT — 멀티도메인 기초

전략: moe_expert_lora (4 experts) — expert별 도메인 specialization 기대

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/pipeline/all_sft_raw.jsonl \
    --set data.max_length=1024 \
    --set training.max_train_steps=15000 \
    --set training.batch_size=2 \
    --set training.grad_accum_steps=8 \
    --set training.log_steps=100 \
    --set training.save_steps=3000 \
    --output-dir outputs/pipeline/01_sft

예상 시간: ~6-8시간 (RTX 5090, ~40K data, 15K micro-steps)

3-Phase 스케줄: - Phase 0 (step 0-2000): router 워밍업 — expert routing 패턴 학습 - Phase 1 (step 2000-): LoRA 훈련 — 도메인별 expert specialization

확인 포인트: - metrics.jsonl에서 loss가 step 5000까지 가파르게 감소 - step 10000 이후 2.0 이하로 수렴 - MoE routing stats (advanced metrics)로 expert 활용 균형 확인


5. Phase 3: 선호 정렬 — DPO 또는 ORPO

옵션 A: DPO (reference 모델 사용)

eulerforge train \
    --preset configs/presets/qwen3.5_0.8b_moe_expert_lora_dpo.yml \
    --set model_name=outputs/pipeline/01_sft/final \
    --set data.format=raw \
    --set data.task=prompted_preference \
    --set data.path=data/pipeline/all_dpo_raw.jsonl \
    --set data.max_length=1024 \
    --set training.max_train_steps=6000 \
    --set training.batch_size=2 \
    --set training.grad_accum_steps=8 \
    --output-dir outputs/pipeline/02_dpo

옵션 B: ORPO (DPO 대체 — 단일 GPU 권장)

DPO 대비 장점: 메모리 50% 절약 (1× forward), Handoff 호환, SFT+선호 결합 DPO 대비 단점: reference 비교 없으므로 선호 경계가 약간 덜 명확할 수 있음

eulerforge train \
    --preset configs/presets/qwen3.5_0.8b_dense_lora_orpo.yml \
    --set model_name=outputs/pipeline/01_sft/final \
    --set injection.strategy=moe_expert_lora \
    --set injection.num_experts=4 --set injection.top_k=2 \
    --set data.format=raw \
    --set data.task=preference \
    --set data.path=data/pipeline/all_dpo_raw.jsonl \
    --set data.max_length=1024 \
    --set training.max_train_steps=6000 \
    --output-dir outputs/pipeline/02_orpo

예상 시간: ~4-6시간 (DPO), ~3-4시간 (ORPO)

DPO 확인 포인트: - Phase 0 (router): reward_margin=0, loss=0.6931 (정상 — LoRA freeze 시 policy=reference) - Phase 1 (LoRA): reward_margin 점차 양수, accuracy > 0.5

ORPO 확인 포인트: - Phase 0: sft_loss 감소 시작 (ORPO는 SFT loss도 포함하므로 router phase에서도 유효) - Phase 1: orpo_loss 감소 + log_odds_ratio 양수 증가


6. Phase 4: RM — Reward Model 학습

eulerforge train \
    --preset configs/presets/qwen3.5_0.8b_dense_lora_rm.yml \
    --set model_name=outputs/pipeline/01_sft/final \
    --set data.format=raw \
    --set data.task=preference \
    --set data.path=data/pipeline/rm_raw.jsonl \
    --set data.max_length=1024 \
    --set training.max_train_steps=6000 \
    --output-dir outputs/pipeline/03_rm

예상 시간: ~3-4시간

참고: RM은 dense_lora 전략 권장 — MoE의 routing 불안정이 reward 예측에 부정적 영향을 줄 수 있음.

확인 포인트: - RM accuracy > 0.65 (chosen이 rejected보다 높은 점수를 받는 비율) - reward_head.ptfinal/에 저장되는지


7. Phase 5: PPO — RLHF 정책 최적화

eulerforge train \
    --preset configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml \
    --set model_name=outputs/pipeline/02_dpo/final \
    --set training.reward_model.checkpoint_path=outputs/pipeline/03_rm/final \
    --set data.format=raw \
    --set data.task=sft \
    --set data.path=data/pipeline/ppo_prompts_raw.jsonl \
    --set data.max_length=512 \
    --set training.max_train_steps=3000 \
    --output-dir outputs/pipeline/04_ppo

예상 시간: ~4-6시간

확인 포인트: - reward_mean이 점차 증가 - KL divergence가 너무 크지 않은지 (과도한 reward hacking 방지)


8. Phase 6: 벤치마크 — 4단계 비교

8.1 수학 능력 비교

# configs/bench/pipeline_math.yml
bench:
  task: sft
  data_path: data/pipeline/math_gsm8k_sft.jsonl
  sample:
    k: 20
    seed: 42
  generation:
    max_new_tokens: 512
    temperature: 0.1
  models:
    target:
      model_dir: "outputs/pipeline/04_ppo/final"
      device: "cuda:0"
    baseline:
      enabled: true
      provider: ollama
      model: "qwen3.5:0.8b"
    judge:
      enabled: true
      provider: ollama
      model: "gpt-oss:20b"
      mode: pointwise
  output:
    out_dir: outputs/bench_pipeline_math
    save_jsonl: true
    print_examples: true
eulerforge bench --preset configs/bench/pipeline_math.yml

8.2 한국어 대화 비교

eulerforge bench --preset configs/bench/pipeline_math.yml \
    --set bench.data_path=data/sft_1k_bench_raw.jsonl \
    --set bench.generation.temperature=0.7 \
    --set bench.generation.max_new_tokens=400 \
    --set bench.output.out_dir=outputs/bench_pipeline_ko

8.3 단계별 비교 (SFT → DPO → PPO 진화 추적)

# SFT 모델
eulerforge bench --preset configs/bench/pipeline_math.yml \
    --set bench.models.target.model_dir=outputs/pipeline/01_sft/final \
    --set bench.output.out_dir=outputs/bench_pipeline_sft

# DPO 모델
eulerforge bench --preset configs/bench/pipeline_math.yml \
    --set bench.models.target.model_dir=outputs/pipeline/02_dpo/final \
    --set bench.output.out_dir=outputs/bench_pipeline_dpo

# PPO 모델 (최종)
eulerforge bench --preset configs/bench/pipeline_math.yml \
    --set bench.models.target.model_dir=outputs/pipeline/04_ppo/final \
    --set bench.output.out_dir=outputs/bench_pipeline_ppo

9. 기대 결과 분석

9.1 단계별 능력 진화

단계 수학 정확도 Thinking 비율 한국어 품질 선호 정렬
원본 (Qwen3.5-0.8B) 낮음 0% 기본 없음
SFT ~30% 좋음 없음
DPO 중-상 ~60% 좋음 초기
PPO ~70%+ 좋음 강화

9.2 MoE Expert Specialization 확인

--metrics-level advanced로 훈련하면 expert routing 통계를 확인할 수 있습니다:

[MoE Routing] Layer 5: expert_0=32%, expert_1=28%, expert_2=22%, expert_3=18%

이상적으로는: - Expert 0: 수학/추론 (숫자가 많은 입력에 높은 비율) - Expert 1: 코딩 (코드 토큰에 높은 비율) - Expert 2: 한국어 (한국어 토큰에 높은 비율) - Expert 3: 일반/영어

실제로 이 수준의 specialization이 자연스럽게 발생하는지는 데이터 혼합 비율과 훈련 기간에 따라 달라집니다. 이것을 관찰하고 분석하는 것 자체가 이 과제의 핵심 학습 포인트입니다.


10. 고급 변형 실험

10.1 LoRA Handoff

SFT 단계에서 LoRA Handoff를 적용하면, 후반부에 LoRA가 base FFN으로 지식 이전됩니다:

training:
  lora_handoff:
    expert_lora:
      start_step: 8000
      duration_steps: 5000
      end_scale: 0.0
      curve: cosine
      end_action: freeze
    base_ffn_ramp:
      start_step: 8000
      end_step: 13000
      start_multiplier: 1.0
      end_multiplier: 3.0

10.2 전략 비교: dense_lora vs moe_expert_lora

동일 데이터에서 전략만 바꿔 비교: - dense_lora: 단순 LoRA, 모든 파라미터가 하나의 전문가 - moe_expert_lora: 4 experts, 도메인별 specialization 가능

10.3 데이터 비율 실험

도메인 혼합 비율을 바꿔 최적 비율 탐색: - 수학 heavy: 수학 50% + 나머지 50% - 코딩 heavy: 코딩 50% + 나머지 50% - 균형: 각 25%


11. 체크포인트 체인 요약

data/pipeline/
├── all_sft_raw.jsonl           # ~40K (4도메인 혼합)
├── all_dpo_raw.jsonl           # ~23K (3가지 선호)
├── rm_raw.jsonl                # = all_dpo_raw.jsonl
└── ppo_prompts_raw.jsonl       # ~5K (프롬프트만)

outputs/pipeline/
├── 01_sft/final/               # MoE SFT 완료 모델
├── 02_dpo/final/               # MoE DPO 정렬 모델
├── 03_rm/final/                # Reward Model + reward_head.pt
│   └── reward_head.pt
├── 04_ppo/final/               # 최종 RLHF 모델 ★
└── bench_pipeline_*/           # 각 단계별 벤치마크 결과

12. 이 과제에서 배우는 것

학습 포인트 내용
데이터 엔지니어링 다중 도메인 데이터 수집/변환/혼합/셔플
훈련 순서의 의미 SFT → DPO → RM → PPO 각 단계가 모델에 미치는 변화
MoE 동작 이해 expert routing, specialization, load balance
Phase scheduling router warmup → LoRA → full unfreeze 순서의 이유
선호 정렬의 한계 DPO만으로 충분한지, PPO가 추가하는 가치
벤치마크 해석 pointwise 스코어의 의미와 한계
최신 트렌드 실습 Thinking 패턴, MoE specialization, progressive alignment

관련 문서