初始化项目,由ModelHub XC社区提供模型

Model: pathcosmos/frankenstallm
Source: Original Platform
This commit is contained in:
ModelHub XC
2026-07-14 04:21:16 +08:00
commit d4abdb70fa
516 changed files with 5584477 additions and 0 deletions

View File

@@ -0,0 +1,506 @@
# 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).*