Files
frankenstallm/reports/2026-03-08_ORPO_TRAINING_JOURNEY.md
ModelHub XC d4abdb70fa 初始化项目,由ModelHub XC社区提供模型
Model: pathcosmos/frankenstallm
Source: Original Platform
2026-07-14 04:21:16 +08:00

507 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FRANKENSTALLM 3B — Phase 3 ORPO 학습 여정
**작성일**: 2026-03-08 (최종 업데이트: 2026-03-09)
**작성자**: Claude Opus 4.6
---
## 1. ORPO 선택 배경
### 1.1 SFT의 한계
Phase 2 SFT v2 학습 완료 후 6차원 평가를 수행했다. 결과는 다음과 같다.
| 차원 | 지표 | 결과 | 목표 | 판정 |
|------|------|------|------|------|
| 1. 지식 보존 | PPL forgetting | 0.9% | <5% | **PASS** |
| 2. 생성 품질 | Greedy 반복률 | 72.97% | <5% | **FAIL** |
| 3. 종료 능력 | EOS 종료율 | 0% | >90% | **FAIL** |
| 4. 한국어 이해 | KoBEST | 43.26% | >55% | **FAIL** |
| 5. 형식 준수 | 포맷 정확도 | 95%+ | >90% | **PASS** |
| 6. 안전성 | 유해 출력률 | <1% | <5% | **PASS** |
**핵심 문제 분석:**
- **반복 생성 (72.97%)**: 모델이 동일한 토큰 시퀀스를 끊임없이 반복한다. SFT는 "좋은 응답" 학습하기 때문에, "나쁜 응답(반복)" 억제하는 음의 신호(negative signal) 전혀 없다.
- **EOS 미종료 (0%)**: 반복과 밀접하게 연결된 문제. 모델이 반복 루프에 빠지면 EOS 토큰을 생성할 기회를 잃는다.
- **KoBEST 저조 (43.26%)**: 한국어 이해력 부족. 다만 문제는 preference optimization보다 데이터 품질과 양에 의존한다.
3개 항목 PASS(지식 보존, 형식, 안전성) SFT 기반이 건전하다는 의미이며, 특히 **PPL forgetting 0.9%** 추가 학습의 기반이 안정적임을 확인해준다.
### 1.2 왜 ORPO인가? (DPO vs ORPO 비교)
SFT만으로는 "좋은 응답 vs 나쁜 응답" 구분하는 preference signal을 없다. Preference optimization 기법이 필요하다.
| 기준 | DPO | ORPO |
|------|-----|------|
| Reference model | 필요 (메모리 2배) | **불필요** |
| 구현 복잡도 | 보통 | **낮음** |
| 메모리 효율 | 낮음 | **높음** |
| 학습 안정성 | 검증 많음 | 비교적 새로움 |
| 반복 억제 | 효과적 | 효과적 |
**ORPO 선택 근거:**
1. **메모리 절약**: Reference model이 불필요하여 3B 모델 기준 6GB VRAM 절약. 8-GPU DDP에서는 ~48GB 절약.
2. **구현 간결성**: TRL의 `ORPOTrainer` DPO 대비 설정이 단순하다.
3. **SFT 기반 건전성**: PPL forgetting 0.9% ORPO 추가 학습이 기존 지식을 크게 훼손하지 않을 것임을 시사한다.
**위험 요소:**
- Preference 데이터 명확한 반복 차이가 있는 (chosen이 정상, rejected가 반복) 전체의 **3.3% 불과**하다. ORPO가 적은 비율의 신호만으로 반복을 억제할 있는지가 핵심 불확실성이다.
**Plan B**: ORPO가 실패할 경우 DPO(`loss_type='sigmoid'`, reference model 사용) 전환한다.
### 1.3 Preference 데이터 현황
- **원본**: 683,181 preference pairs (기존 문서의 795K는 집계 오류)
- **NaN 방지 필터 **: ~630,000 pairs
- prompt > `max_length - 16` → 제거
- response > `max_length` → 제거
- **Eval split**: 5% (seed=42) → ~31,000 eval pairs
- **Effective batch size**: 4 (per-device) x 8 GPU x 4 (gradient accumulation) = **128**
---
## 2. 6-Config HP Sweep 설계
### 2.1 탐색 축 설계
3개의 독립적인 하이퍼파라미터 축을 선정했다.
**축 1: Beta (반복 억제 강도)**
Beta는 ORPO의 odds ratio loss 가중치를 조절한다. 값이 클수록 chosen/rejected 간 차이를 더 적극적으로 학습한다.
- 낮은 beta (0.15): 보수적. SFT 품질을 유지하면서 약하게 preference 학습.
- 중간 beta (0.25): 표준적인 출발점. 논문 기본값 부근.
- 높은 beta (0.35): 공격적. 반복 억제를 강하게 밀어붙이지만 과적합 위험.
**축 2: Learning Rate (수렴 속도)**
- 낮은 LR (5e-6): 안정적이지만 느린 수렴. 200 steps 내 효과 미미할 수 있음.
- 중간 LR (8e-6): 표준적. 200 steps에서 의미 있는 변화 기대.
- 높은 LR (1.2e-5): 빠른 수렴. 200 steps 짧은 sweep에서 효과를 빨리 확인 가능하지만 발산 위험.
**축 3: Max Length (VRAM vs 커버리지)**
- 1536 tokens: 대부분의 prompt+response를 커버. VRAM 더 사용.
- 1024 tokens: VRAM 절약. 긴 응답은 잘리지만 대다수는 1024 이내.
### 2.2 왜 6개인가?
3축의 full factorial 조합은 3 x 3 x 2 = **18개** (또는 각 3개씩이면 27개)로, 200 steps sweep이라 해도 비현실적이다.
**중심축 고정 방식**을 채택했다:
- 중심축: `beta=0.25`, `lr=8e-6`, `max_length=1536`
- 각 축을 한 번씩 양방향으로 변형하되, 나머지 축은 중심값 고정
- 이 방식으로 **6개 config**만으로 3개 축의 영향을 독립적으로 측정 가능
이는 실험 설계의 "one-factor-at-a-time" (OFAT) 방식에 해당한다.
### 2.3 각 Config의 목적
| Run | Name | Beta | LR | Max Length | 설계 의도 |
|-----|------|------|----|-----------|-----------|
| 1 | `baseline_b015_lr8e6` | 0.15 | 8e-6 | 1536 | 약한 beta 베이스라인. SFT 품질 유지 우선 |
| 2 | `baseline_b025_lr8e6` | 0.25 | 8e-6 | 1536 | **중심축**. 모든 비교의 기준점 |
| 3 | `strong_b035_lr8e6` | 0.35 | 8e-6 | 1536 | 강한 beta. 적극적 반복 억제, 과적합 감시 |
| 4 | `fast_b025_lr12e6` | 0.25 | 1.2e-5 | 1536 | 높은 LR. 200 steps에서 빠른 수렴 확인 |
| 5 | `conserv_b025_lr5e6` | 0.25 | 5e-6 | 1536 | 보수적 LR. 안정성 우선, 느린 변화 |
| 6 | `short_b025_lr8e6` | 0.25 | 8e-6 | 1024 | 짧은 max_length. VRAM 절약 효과 측정 |
---
## 3. 시도 이력 — 5번의 실패, 1번의 성공
총 6번의 시도 끝에 학습이 정상 실행되었다. 각 실패에서 얻은 교훈은 대규모 분산 학습의 실전 지식이다.
### 3.1 시도 1: NCCL Timeout
- **시각**: ~03:00
- **증상**: `torch.distributed.DistBackendError: wait timeout after 1800000ms`
- **원인**: Rank 0이 649K 샘플을 토크나이징하는 데 `num_proc=8`로 약 30분 소요. 그 동안 다른 7개 rank가 NCCL communicator setup에서 대기하다 기본 timeout(1800초 = 30분)을 초과했다.
- **수정**:
- `ddp_timeout=7200` (30분 → 2시간으로 확대)
- `dataset_num_proc=64` (8 → 64 프로세스로 토크나이징 병렬화)
- **교훈**: 대규모 데이터 + DDP 환경에서는 데이터 전처리 시간이 NCCL timeout에 직접 영향을 준다. 데이터가 클수록 timeout을 넉넉하게 설정해야 한다.
### 3.2 시도 2: Config 충돌
- **시각**: 03:28
- **증상**: `ValueError: save_steps(9999) is not a round multiple of eval_steps(100)`
- **원인**: `load_best_model_at_end=True`일 때 HuggingFace Trainer는 `save_steps``eval_steps`의 정수배여야 한다는 validation을 수행한다. 최선 모델을 eval 시점에 저장해야 하므로 두 주기가 동기화되어야 하기 때문이다.
- **수정**: `--no_load_best --save_steps 200`
- **교훈**: TRL/HuggingFace Trainer의 config validation은 예상보다 엄격하다. 특히 `load_best_model_at_end`와 관련된 설정 간 정합성을 사전에 확인해야 한다.
### 3.3 시도 3: 포트 충돌 + QKV 변환 버그
- **시각**: 03:45
**증상 (2가지 동시 발생):**
1. **Run 1**: `port 29510 EADDRINUSE` — 이전 실행의 좀비 프로세스가 소켓 점유
2. **Run 2~6**: `q_proj/k_proj/v_proj MISSING` — QKV weight가 랜덤 초기화됨. GPU 0만 100% utilization, 나머지 7개 GPU는 0%.
**원인 (포트)**: 이전 실행이 비정상 종료되면서 `torchrun` 프로세스가 좀비로 남아 29510 포트를 점유.
**원인 (QKV)**: `convert_to_hf.py``remap_weights()` 함수가 TransformerEngine의 fused QKV 프로젝션을 처리하지 못했다.
TransformerEngine은 Q, K, V를 하나의 가중치로 합친다:
- `attn.qkv_proj.weight` shape: `[5120, 3072]`
- Q: 3072 (24 heads x 128 dim), K: 1024 (8 kv_heads x 128 dim), V: 1024 (8 kv_heads x 128 dim)
- 합계: 3072 + 1024 + 1024 = 5120
HuggingFace 형식은 `q_proj`, `k_proj`, `v_proj`를 분리하여 저장한다.
**수정**: QKV split 로직을 `convert_to_hf.py`에 추가:
```python
q_dim = num_heads * head_dim # 24 * 128 = 3072
k_dim = num_kv_heads * head_dim # 8 * 128 = 1024
v_dim = num_kv_heads * head_dim # 8 * 128 = 1024
qkv = state_dict[fused_key]
state_dict[f"{prefix}.q_proj.weight"] = qkv[:q_dim]
state_dict[f"{prefix}.k_proj.weight"] = qkv[q_dim:q_dim + k_dim]
state_dict[f"{prefix}.v_proj.weight"] = qkv[q_dim + k_dim:]
del state_dict[fused_key]
```
체크포인트 키 수: 171 → 255 (fused → split 변환으로 키 증가).
- **교훈**: TransformerEngine FP8 → HuggingFace 변환 시 fused projection(QKV, gate-up 등) 처리가 필수이다. 변환 후 반드시 키 매핑을 검증해야 한다.
### 3.4 시도 4: TRL NaN 버그
이 시도는 시도 3 이전에 발생한 것으로, TRL 라이브러리 내부의 심각한 버그를 발견하고 패치한 과정이다.
**증상**: 8-GPU DDP + 풀 데이터(683K) 학습 시 step 10~20에서 `loss=0`, `grad_norm=NaN` 발생. 단일 GPU나 소규모 데이터에서는 재현되지 않았다.
**근본 원인**: TRL의 `tokenize_row` 함수에서 chosen과 rejected response를 동일한 길이 기준으로 잘랐다.
```python
# TRL 원본 코드 (버그)
longer_response_length = max(chosen_len, rejected_len)
# 이후 양쪽 모두 longer_response_length 기준으로 truncation
```
문제 시나리오: `longer_response_length > max_length`이면, shorter response가 음수 인덱스로 잘려서 **0 tokens**가 된다 (Python의 `list[:-음수]` = `[]`).
**NaN 전파 체인:**
```
0 response tokens
→ labels 전부 -100 (학습 대상 없음)
→ get_batch_logps = 0.0 (log probability 합이 0)
→ log1mexp(0.0) = log(1 - exp(0)) = log(1 - 1) = log(0) = -inf
→ log_odds = -inf - (-inf) = NaN
→ gradient NaN → 전체 weight 오염
```
**3중 패치** (TRL 라이브러리 파일 직접 수정):
**Patch 1: `get_batch_logps` — division by zero 방지**
```python
# 파일: trl/trainer/utils.py
if average_log_prob:
denom = loss_mask.sum(-1).clamp(min=1)
return (per_token_logps * loss_mask).sum(-1) / denom
```
**Patch 2: `odds_ratio_loss` — logps clamp**
```python
# 파일: trl/trainer/orpo_trainer.py (또는 experimental)
policy_chosen_logps = policy_chosen_logps.float().clamp(max=-1e-4)
policy_rejected_logps = policy_rejected_logps.float().clamp(max=-1e-4)
```
logps가 0에 가까우면 `log1mexp`에서 `-inf`가 발생하므로, 최대값을 `-1e-4`로 제한한다.
**Patch 3: `tokenize_row` — 독립 truncation (근본 원인 수정)**
```python
# 파일: trl/trainer/orpo_trainer.py (또는 experimental)
for answer_tokens in [chosen_tokens, rejected_tokens]:
prompt_len = len(answer_tokens["prompt_input_ids"])
response_len = len(answer_tokens["input_ids"])
if prompt_len + response_len > self.max_length:
max_response = max(self.max_length - prompt_len, 1)
for k in ["input_ids", "attention_mask"]:
answer_tokens[k] = answer_tokens[k][:max_response]
```
각 response를 독립적으로 truncation하여, 하나가 길다고 해서 다른 하나가 0으로 잘리는 상황을 방지한다.
추가로 `train/orpo.py`에 데이터 필터를 추가했다:
- prompt > `max_length - 16` → 제거 (response 공간이 16 tokens 미만이면 무의미)
- response > `max_length` → 제거
683,181 → ~630,000 pairs로 축소.
- **교훈**: 분산 학습에서만 재현되는 NaN 버그는 데이터 분포의 꼬리(극단적으로 긴 prompt + 짧은 response 조합)에서 발생한다. 단일 GPU 테스트로는 발견하기 어렵다. TRL 같은 성숙한 라이브러리도 edge case에서 수치적 불안정성을 가질 수 있다.
### 3.5 시도 5: TRL Tokenizer 호환 문제
- **증상**: 한국어 tokenizer의 merge operations으로 인해 prompt token 길이 불일치 발생 → `zip(strict=True)` ValueError
- **수정**: TRL 소스 파일 8건에 패치 적용 (`.029bak` 백업 생성):
- `zip(..., strict=True)``zip(...)` (strict 제거)
- `build_tokenized_answer`의 length mismatch → graceful fallback
- **교훈**: 영어 중심으로 테스트된 TRL이 한국어 tokenizer(BPE merge 특성이 다름)와 호환성 문제를 가질 수 있다.
### 3.6 시도 6: 성공!
- **시각**: 04:20
**사전 정리 작업:**
```bash
# 좀비 프로세스 정리
pkill -9 -f torchrun
pkill -9 -f orpo
# 포트 해제 확인
ss -tlnp | grep 29510
# sweep 디렉터리 초기화
rm -rf checkpoints/orpo_sweep/*
```
**실행**: 6-config sweep이 순차적으로 정상 진행 시작.
---
## 4. 스윕 결과
### 4.1 결과 테이블
각 config는 200 steps만 실행하여 경향을 파악하는 "탐색" 수준의 sweep이다.
| Run | Name | Beta | LR | Train Loss | Eval Loss | Margin | Time(s) | Status |
|-----|------|------|----|-----------|-----------|--------|---------|--------|
| 1 | `baseline_b015_lr8e6` | 0.15 | 8e-6 | 1.811 | 1.827 | 0.004 | 2,344 | 완료 |
| 2 | `baseline_b025_lr8e6` | 0.25 | 8e-6 | 1.890 | 1.906 | 0.009 | 2,360 | 완료 |
| 3 | `strong_b035_lr8e6` | 0.35 | 8e-6 | 2.055 | 1.985 | 0.007 | 2,390 | 완료 |
| 4 | `fast_b025_lr12e6` | 0.25 | 1.2e-5 | 1.917 | 1.862 | 0.009 | 2,416 | 완료 |
| 5 | `conserv_b025_lr5e6` | 0.25 | 5e-6 | - | - | - | - | 진행중 |
| 6 | `short_b025_lr8e6` | 0.25 | 8e-6 | - | - | - | - | 대기중 |
### 4.2 분석
**Beta 축 분석 (Run 1 vs 2 vs 3, LR=8e-6 고정):**
- Beta가 높아질수록 train loss가 일관되게 증가한다: 1.811 → 1.890 → 2.055
- 이는 예상된 결과다. Beta가 높으면 odds ratio loss의 가중치가 커지므로 전체 loss가 상승한다.
- **Eval loss도 상승하지만 기울기가 완만하다**: 1.827 → 1.906 → 1.985. Run 3(beta=0.35)에서 train-eval gap이 가장 작아(0.070) 과적합 징후는 없다.
- Margin: Run 1(0.004)이 가장 낮고, Run 2(0.009)와 Run 3(0.007)은 비슷하다. Beta를 높인다고 반드시 margin이 비례 증가하지는 않는다.
**LR 축 분석 (Run 2 vs 4 vs 5, Beta=0.25 고정):**
- Run 4(`lr=1.2e-5`): **eval_loss 1.862로 최저값**. 높은 LR이 200 steps라는 짧은 구간에서 더 빠르게 유용한 방향으로 수렴했음을 시사.
- Run 2(`lr=8e-6`): eval_loss 1.906. 기준선.
- Run 5(`lr=5e-6`): 아직 진행 중. 낮은 LR이 200 steps에서 충분한 변화를 보일지 관건.
**잠정 결론**: 200-step sweep 기준으로 `lr=1.2e-5, beta=0.25` 조합(Run 4)이 가장 유망하다. 다만 full training에서는 높은 LR이 후반부 불안정성을 가져올 수 있으므로, cosine scheduler와 함께 warmup ratio를 조정해야 한다.
---
## 5. 기술 세부사항
### 5.1 TRL 0.29.0 API
TRL 0.29.0에서 ORPO 관련 클래스의 위치가 변경되었다.
```python
# 기존 (0.28 이하)
from trl import ORPOConfig, ORPOTrainer # 제거됨
# 0.29.0
from trl.experimental.orpo import ORPOConfig, ORPOTrainer
```
또한 `max_prompt_length` 파라미터가 제거되었다. Prompt 길이 제한이 필요하면 데이터 전처리 단계에서 직접 필터링해야 한다.
### 5.2 DDP 최적화
**Rank별 데이터 처리 분리:**
```python
if local_rank == 0:
# Rank 0만 num_proc=64로 토크나이징
dataset = dataset.map(tokenize_fn, num_proc=64)
# 결과가 캐시되므로 다른 rank는 캐시 히트
else:
# Rank 1~7은 num_proc=1로 호출 (캐시에서 로드)
dataset = dataset.map(tokenize_fn, num_proc=1)
```
이 방식으로 72코어를 rank 0에 집중 투입하여 토크나이징 시간을 단축하고, 나머지 rank는 캐시를 재사용한다.
**DDP 설정 주의사항:**
- `ddp_find_unused_parameters=False`: TransformerEngine과 함께 사용 시 필수. `True`로 설정하면 TE의 FP8 버퍼에서 오류 발생.
- `static_graph=True` **사용 금지**: TE와 호환되지 않는다.
### 5.3 하드웨어 설정
```bash
# NCCL 설정
NCCL_IB_DISABLE=1 # InfiniBand 비활성화 (단일 노드)
NCCL_BUFFSIZE=134217728 # 128MB 버퍼
NCCL_P2P_LEVEL=NVL # NVLink P2P 통신
# CPU 스레드
OMP_NUM_THREADS=9 # 72 cores / 8 GPUs
MKL_NUM_THREADS=9
# 학습 설정
dataset_num_proc=64
bf16=true # B200 BF16 네이티브
dataloader_pin_memory=true
ddp_timeout=7200 # NCCL timeout 2시간
# 모델 설정
flash_attention_2 # FlashAttention-2 활성화
gradient_checkpointing=true # VRAM 절약
```
### 5.4 안전장치
**SIGHUP 3중 방어:**
터미널 세션 종료 시 학습이 중단되는 것을 방지하기 위한 3중 보호 체계:
1. **nohup + setsid**: 프로세스를 세션 리더에서 분리
2. **Python signal handler**: SIGHUP 수신 시 무시하고 학습 계속
3. **Emergency checkpoint**: 비정상 종료 감지 시 현재 상태를 즉시 저장
**텔레그램 알림**: 각 run 완료/실패 시 자동 알림 전송 (Python `urllib`, curl 차단 환경 대응).
**Early stopping**: eval_loss 기반. patience 설정으로 과적합 시 자동 중단.
---
## 6. 스윕 최종 결과 + Best Config 선정
### 6.1 전체 스윕 완료 결과
6개 config 모두 200 steps 완료.
| Run | Name | Beta | LR | MaxLen | Train Loss | Eval Loss | Margin | Time(s) |
|-----|------|------|----|--------|-----------|-----------|--------|---------|
| 1 | `baseline_b015_lr8e6` | 0.15 | 8e-6 | 1536 | 1.811 | 1.827 | 0.004 | 2,344 |
| 2 | `baseline_b025_lr8e6` | 0.25 | 8e-6 | 1536 | 1.890 | 1.906 | 0.009 | 2,360 |
| 3 | `strong_b035_lr8e6` | 0.35 | 8e-6 | 1536 | 2.055 | 1.985 | 0.007 | 2,390 |
| 4 | `fast_b025_lr12e6` | 0.25 | 1.2e-5 | 1536 | 1.917 | 1.862 | 0.009 | 2,416 |
| 5 | `conserv_b025_lr5e6` | 0.25 | 5e-6 | 1536 | 1.833 | 1.910 | 0.004 | 2,350 |
| 6 | `short_b025_lr8e6` | 0.25 | 8e-6 | 1024 | 1.664 | 1.695 | 0.007 | 1,840 |
### 6.2 Best Config 선정: Run 4 (lr=1.2e-5, beta=0.25)
**선정 근거:**
1. **Eval loss 최저 (1.862)**: maxlen=1536 그룹 내 eval loss 기준 1위.
2. **높은 margin (0.009)**: chosen/rejected 구분 능력이 강함. Run 2와 동률.
3. **빠른 수렴**: 200 steps 만에 다른 config 대비 가장 큰 개선폭을 보여, 긴 학습에서도 유리할 것으로 판단.
**참고**: Run 6(short_1024)의 eval_loss가 1.695로 절대값은 가장 낮지만, 이는 max_length=1024로 짧은 시퀀스를 다루기 때문이며 1536 시퀀스와 직접 비교할 수 없다.
### 6.3 Throughput 벤치마크
본 학습에 앞서 4가지 batch/grad_accum 조합의 throughput을 벤치마크하여 최적 설정을 결정했다.
| Config | batch_size | grad_accum | max_length | eff_batch | Throughput (samples/s) |
|--------|-----------|-----------|-----------|----------|----------------------|
| **1** | **4** | **4** | **1536** | **128** | **80.63** |
| 2 | 2 | 8 | 1536 | 128 | 73.14 |
| 3 | 8 | 2 | 1536 | 128 | OOM |
| 4 | 4 | 4 | 1024 | 128 | 91.25 |
**Config 1 (bs=4, accum=4, maxlen=1536)** 선정. 동일 effective batch size에서 ~10% 높은 throughput.
CPU 스레드도 NUMA-aware로 최적화: `OMP_NUM_THREADS=9, MKL_NUM_THREADS=9` (72코어 ÷ 8 GPU = 9코어/GPU).
---
## 7. Full Training 시작 (2026-03-09)
### 7.1 학습 설정
| 파라미터 | 값 | 비고 |
|---------|-----|------|
| Beta | 0.25 | Sweep Run 4에서 선정 |
| Learning rate | 1.2e-5 | Sweep eval_loss 최저 |
| Batch size (per-device) | 4 | Throughput 벤치마크 최적 |
| Gradient accumulation | 4 | |
| Effective batch | 128 | 4 × 4 × 8 GPU |
| Max length | 1536 | |
| Epochs | 2 | |
| Warmup ratio | 0.05 | |
| Weight decay | 0.01 | |
| Eval steps | 500 | |
| Early stopping patience | 3 | |
| GPU VRAM 사용 | ~52GB / 183GB (28%) | |
| 예상 총 steps | 9,840 | |
| 예상 학습 시간 | ~4.8시간 | ~1.75 s/step |
### 7.2 SIGHUP 3중 방어 실행
```bash
nohup setsid bash scripts/launch_3b_orpo.sh \
> checkpoints/korean_3b_orpo_v1/train.log 2>&1 &
```
1. **nohup + setsid**: 프로세스를 세션에서 완전 분리
2. **Python SIGHUP handler**: orpo.py 내 signal.signal(SIGHUP, handler) — 무시 처리
3. **Emergency checkpoint**: 비정상 종료 감지 시 즉시 체크포인트 저장
### 7.3 학습 지표 추이
| 지표 | step ~250 | step ~1,160 | 변화 |
|------|-----------|-------------|------|
| **loss** | 1.952 | **1.709** | -0.243 |
| **nll_loss** | 1.757 | **1.593** | -0.164 |
| **rewards/margins** | 0.002 | **0.330** | +0.328 |
| **rewards/accuracies** | 0.473 | **0.719** | +0.246 |
| **log_odds_chosen** | -0.021 | **1.468** | +1.489 |
```
step 1163/9840 (12%), epoch 0.24, 경과 40분, 남은 ~4.4시간
속도: ~1.82 s/step
GPU VRAM: ~52GB/183GB, utilization 91~98%
```
**관찰**: rewards/accuracies가 0.47(랜덤 수준) → 0.72로 빠르게 상승. 모델이 chosen/rejected를 구분하는 능력이 강화되고 있다. margins도 0.002 → 0.330으로 급상승하여 ORPO가 preference signal을 효과적으로 학습 중임을 시사한다.
---
## 8. 다음 단계 + 교훈 요약
### 8.1 다음 단계
1. **Full training 완료 대기**: 9,840 steps, 예상 ~4.8시간
2. **6차원 재평가**: 특히 반복률(72.97% → 목표 <5%) EOS 종료율 개선 확인
3. **GGUF 변환 + Ollama 배포**: 평가 통과 Phase 4 진행
4. **Plan B 준비**: ORPO 효과 미미 DPO 전환
### 6.2 교훈 요약
| # | 교훈 | 카테고리 |
|---|------|---------|
| 1 | 대규모 데이터 + DDP에서 토크나이징 시간을 NCCL timeout에 반영해야 한다 | 분산 학습 |
| 2 | TRL/HF Trainer의 config validation은 예상보다 엄격하다. 특히 `load_best_model_at_end` 관련 | 프레임워크 |
| 3 | TransformerEngine FP8 HF 변환 fused projection 처리 필수 | 모델 변환 |
| 4 | TRL의 `tokenize_row` 극단적 길이 조합에서 NaN을 생성한다. 분산 학습에서만 재현 | 버그 |
| 5 | NaN은 0 response -100 labels logps=0 log(0)=-inf NaN 체인으로 전파된다 | 수치 안정성 |
| 6 | 영어 중심 TRL이 한국어 tokenizer BPE와 호환성 문제를 가진다 | 다국어 |
| 7 | 좀비 프로세스의 포트 점유를 사전에 확인해야 한다 | 인프라 |
| 8 | 단일 GPU 테스트로는 분산 환경의 edge case를 발견하기 어렵다 | 테스트 |
| 9 | HP sweep은 full factorial 대신 중심축 고정 OFAT로 효율적 탐색 가능 | 실험 설계 |
| 10 | SFT의 "좋은 응답만 학습" 한계는 preference optimization으로만 해결 가능 | 학습 전략 |
---
*이 보고서는 FRANKENSTALLM 3B Phase 3 ORPO 학습의 전 과정을 기록한다. 5번의 실패에서 얻은 교훈은 대규모 언어 모델 학습의 실전 지식으로, 향후 유사 프로젝트의 참고 자료가 될 것이다. 현재 본 학습이 진행 중이다 (2026-03-09).*