8. PPO (RLHF) 훈련
개요
PPO(Proximal Policy Optimization)는 강화학습 기반 파인튜닝(RLHF)입니다. 모델이 프롬프트에 대한 응답을 직접 생성하고, 보상 모델(RM)이 그 응답에 점수를 매기면, 높은 점수를 받는 응답을 생성하는 방향으로 학습합니다.
PPO의 핵심 구조 — 누가 무엇을 하는가
┌─────────────────────────────────────────────────────────┐
│ 1. 정책 모델 (Policy Model) — 우리가 훈련하는 모델 │
│ = SFT 완료 모델 + LoRA │
│ 역할: 프롬프트를 받아 응답을 생성 │
│ │
│ 2. 보상 모델 (Reward Model / RewardHead) │
│ = 별도 RM 훈련으로 만든 "채점관" │
│ 역할: 정책 모델이 생성한 응답에 점수(스칼라 보상) 부여 │
│ │
│ 3. 참조 모델 (Reference Model) │
│ = 정책 모델의 adapter를 비활성화한 버전 │
│ 역할: KL divergence 계산 (정책이 원본에서 너무 벗어나지 않도록) │
└─────────────────────────────────────────────────────────┘
무엇이 훈련되는가?
| 구성 요소 | 훈련 여부 | 설명 |
|---|---|---|
| 정책 모델 (LoRA) | O (훈련됨) | 높은 보상을 받는 응답을 생성하도록 학습 |
| 보상 모델 (RewardHead) | X (frozen) | 이미 학습 완료된 상태로 사용 |
| 참조 모델 | X (frozen) | adapter disable = SFT 상태의 base model |
한 스텝의 흐름
프롬프트 → [정책 모델] → 응답 생성
↓
[보상 모델] → 점수 (reward)
↓
[PPO 알고리즘] → policy LoRA 업데이트
│
└─ KL penalty: [정책] vs [참조] 차이가 너무 크지 않도록
SFT와 RM은 필수인가?
SFT: 필수
| SFT 없이 | SFT 후 |
|---|---|
| 정책 모델이 의미 없는 응답 생성 | 정책 모델이 구조화된 응답 생성 |
| 보상 모델이 의미 없는 응답에 점수 → 학습 무의미 | 보상 모델이 유의미한 응답 비교 가능 |
| KL divergence가 빠르게 발산 | 안정적 학습 |
RM: 사실상 필수 (random init은 무의미)
코드 상 checkpoint_path: ""이면 랜덤 초기화된 RewardHead를 사용합니다. 이것은:
- 무작위 점수를 보상으로 사용 → 의미 없는 방향으로 학습
- 연구/디버깅 목적으로만 존재
실전에서는 반드시 RM 훈련 후 그 체크포인트를 지정해야 합니다.
올바른 파이프라인
SFT (필수) → RM (사실상 필수) → PPO
RM 없이 PPO를 실행하면 코드는 동작하지만, 랜덤 보상으로 학습하므로 결과에 의미가 없습니다.
RM의 크기와 policy의 관계
동일 모델 RM (기본)
# PPO 프리셋
training:
reward_model:
model_name: Qwen/Qwen3.5-0.8B-Base # policy와 동일한 base
checkpoint_path: outputs/rm_run/final # RM 훈련 체크포인트
동작: policy 모델의 hidden state에 RewardHead를 붙여 보상 계산. 별도 모델 로드 불필요.
더 큰 모델 RM (고급 — hidden_size 불일치 주의)
원칙적으로 RM은 policy보다 큰 모델이 더 좋은 보상을 제공할 수 있습니다. 그러나 현재 EulerForge의 PPO 구현은 policy 모델의 hidden state를 사용하므로:
- RM의 base 모델 = policy의 base 모델이어야 함
- hidden_size가 다르면 RewardHead 로드 시 size mismatch (경고 후 random init으로 fallback)
현재 제약: RM과 policy는 동일 base 모델이어야 합니다.
전체 파이프라인 순서
Step 1: SFT (필수)
eulerforge train --preset qwen3.5_0.8b_dense_lora_sft.yml
→ outputs/sft_run/final
Step 2: RM (사실상 필수)
eulerforge train --preset qwen3.5_0.8b_dense_lora_rm.yml
--set model_name=outputs/sft_run/final
→ outputs/rm_run/final (reward_head.pt 포함)
Step 3: PPO
eulerforge train --preset qwen3.5_0.8b_dense_lora_ppo.yml
--set model_name=outputs/sft_run/final ← policy = SFT 모델
--set training.reward_model.checkpoint_path=outputs/rm_run/final ← RM 체크포인트
프리셋에서의 역할 지정
# configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml
# ── 정책 모델 (Policy) ──
# 이것이 훈련되는 모델. SFT 완료 체크포인트를 지정.
model_name: Qwen/Qwen3.5-0.8B-Base # 또는 --set model_name=outputs/sft_run/final
# ── 보상 모델 (Reward) ──
training:
reward_model:
model_name: Qwen/Qwen3.5-0.8B-Base # RM의 base 모델 (policy와 동일해야)
checkpoint_path: "" # ← RM 체크포인트 경로 (비우면 random init)
# 실전에서는 반드시 지정:
# checkpoint_path: outputs/rm_run/final
# ── 참조 모델 (Reference) ──
# 별도 지정 불필요. policy 모델의 adapter를 disable하여 자동 생성.
사전 요구 사항
- SFT 훈련 완료 — PPO의 policy 모델로 사용
- RM 훈련 완료 — PPO의 보상 함수로 사용 (
reward_head.pt포함) - EulerForge 설치 완료 (시작 가이드 참조)
RM 없이 PPO를 실행하면 random init RewardHead가 사용됩니다. 이 경우 무작위 보상으로 학습하므로 실전에서는 의미 없습니다. 연구/디버깅 목적으로만 사용하세요.
데이터 포맷
PPO 전용 데이터 (prompt_only)
PPO는 프롬프트만 필요합니다. 응답은 정책 모델이 직접 생성합니다.
{"prompt": "인공지능의 역사에 대해 설명해주세요."}
{"prompt": "파이썬에서 리스트 정렬 방법을 알려주세요."}
data:
format: raw
path: data/ppo_1k_raw.jsonl
task: prompt_only
max_length: 256
SFT 데이터 재사용
기존 SFT raw 데이터(data/sft_10k_raw.jsonl)의 prompt 컬럼도 사용 가능합니다:
# SFT 데이터에서 prompt만 추출
python -c "
import json
with open('data/sft_10k_raw.jsonl') as f, open('data/ppo_prompts.jsonl', 'w') as out:
for line in f:
row = json.loads(line)
out.write(json.dumps({'prompt': row['prompt']}) + '\n')
"
프리셋
training:
type: ppo
lr: 1.0e-6 # SFT/DPO보다 낮은 lr 권장
ppo:
clip_range: 0.2 # PPO 클리핑 ε
kl_coef: 0.1 # KL 페널티 계수 (너무 크면 학습 안 됨, 너무 작으면 발산)
epochs: 4 # 배치당 PPO 업데이트 에폭
max_gen_len: 64 # 최대 생성 토큰 수
temperature: 1.0 # 샘플링 온도
reward_model:
checkpoint_path: outputs/rm_run/final # RM 체크포인트 (필수 지정 권장)
실행
풀 파이프라인 (SFT → RM → PPO)
# Step 1: SFT
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_sft.yml \
--set data.format=raw --set data.task=sft \
--set data.path=data/sft_10k_raw.jsonl \
--output-dir outputs/ppo_pipeline/01_sft
# Step 2: RM (SFT 모델 기반)
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_rm.yml \
--set model_name=outputs/ppo_pipeline/01_sft/final \
--set data.format=raw --set data.task=preference \
--set data.path=data/dpo_10k_raw.jsonl \
--output-dir outputs/ppo_pipeline/02_rm
# Step 3: PPO (SFT policy + RM reward)
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml \
--set model_name=outputs/ppo_pipeline/01_sft/final \
--set training.reward_model.checkpoint_path=outputs/ppo_pipeline/02_rm/final \
--set data.path=data/ppo_1k_raw.jsonl \
--output-dir outputs/ppo_pipeline/03_ppo
SFT 데이터로 실행
eulerforge train --preset configs/presets/qwen3.5_0.8b_dense_lora_ppo.yml \
--set model_name=outputs/sft_run/final \
--set training.reward_model.checkpoint_path=outputs/rm_run/final \
--set data.path=data/ppo_prompts.jsonl
주요 메트릭
| 메트릭 | 의미 | 기대 |
|---|---|---|
reward_mean |
평균 보상 | 점진적 증가 |
kl_divergence |
policy-reference KL | 안정적 (폭발하면 kl_coef 증가) |
ppo_loss |
PPO surrogate loss | 감소 |
entropy |
생성 다양성 | 너무 낮아지면 과적합 |
디버깅 및 트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
reward_mean이 변하지 않음 |
RM random init (checkpoint 미지정) | reward_model.checkpoint_path 지정 |
reward_mean이 초기에 음수 |
RM 품질 낮음 또는 hidden_size 불일치 | RM 재훈련, base 모델 일치 확인 |
kl_divergence 폭발 |
lr 너무 높음 또는 kl_coef 너무 낮음 | lr 감소, kl_coef 증가 |
| 생성 품질 하락 | reward hacking (RM 취약점 악용) | RM 데이터 다양화, kl_coef 증가 |
| OOM | generate + 3× forward (policy, ref, new) | batch_size 감소, max_gen_len 축소 |
| SDPA tensor size mismatch | generate()에서 right-padding 또는 gradient checkpointing 충돌 | 자동 처리됨 (left-padding + gc 임시 비활성화) |
관련 문서
- 18_training_pipeline.md — 전체 파이프라인 순서
- 07_rm_training.md — RM 훈련 (PPO 전 필수)
- 05_dpo_training.md — DPO (PPO 대안)