初始化项目,由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,589 @@
# 🚀 3B 한국어 LLM 마스터 플랜
**작성일**: 2026-02-27 04:27 KST
**프로젝트**: `/PROJECT/0325120031_A/ghong/taketimes/llm-bang/`
**결정**: 1B → 3B 전환 (1B 구조적 한계 확인)
**총 예상 기간**: ~35시간
---
## 0. 현황 요약
### 1B에서 확인된 것
| 항목 | 결과 |
|------|------|
| 반복률 (raw, 올바른 포맷) | 30.7% |
| 반복률 (rep_penalty=1.1) | 18.0% |
| val_loss | 2.2062 |
| 자연 종료율 | 60% |
| 짧은 QA 품질 | ✅ 양호 (수도, 김치 등) |
| 복잡한 질문 품질 | ❌ 반복 퇴화 심각 |
### 3B 전환 근거
1. **반복률 18%는 1B 구조적 한계** — d_model=2048, 24 layers로는 긴 시퀀스에서 hidden state 붕괴 불가피
2. **Scaling law 예측**: 3B는 loss ~7% 감소 → 반복률 5~8% 예상
3. **ORPO 없이도 목표 달성 가능**: 3B SFT만으로 <10%, +ORPO로 <3%
4. **총 소요시간 ORPO 실패→3B (39h) vs 3B 직행 (30h)** 직행이 빠름
---
## 1. 3B 모델 아키텍처
| 파라미터 | 1B (현재) | 3B (목표) | 변화 |
|---------|----------|----------|------|
| d_model | 2048 | **3072** | 1.5× |
| n_layers | 24 | **32** | 1.33× |
| n_heads | 16 | **24** | 1.5× |
| n_kv_heads (GQA) | 4 | **8** | 2× (GQA 3:1) |
| d_ffn (SwiGLU) | 5472 | **8192** | 1.5× |
| vocab_size | 64000 | **64000** | 동일 |
| max_seq_len | 4096 | **4096** | 동일 |
| rope_theta | 500000 | **500000** | 동일 |
| **총 파라미터** | **1.19B** | **~3.42B** | 2.9× |
### 파라미터 수 상세
```
Embedding: 64000 × 3072 = 196.6M
Attention/layer: Q(3072×3072) + K(3072×1024) + V(3072×1024) + O(3072×3072) = 25.1M
FFN/layer: SwiGLU gate(3072×8192) + up(3072×8192) + down(8192×3072) = 75.5M
Layer total: 25.1 + 75.5 = 100.6M × 32 layers = 3,219M
LM Head: tied with embedding
총계: 196.6M + 3,219M ≈ 3.42B
```
### GPU 메모리 예상 (8× B200 183GB)
```
모델 (FP8): 3.42 GB
Optimizer (FP32): 27.4 GB (DDP 분산 → ~3.4 GB/GPU)
Gradients (BF16): 6.84 GB (분산 → ~0.86 GB/GPU)
Activations (bs=4): ~15-25 GB (gradient checkpointing)
Per GPU 합계: ~28 GB → B200의 15% → 매우 여유
```
### Config 파일
```yaml
# configs/korean_3b_fp8.yaml
model:
vocab_size: 64000
d_model: 3072
n_layers: 32
n_heads: 24
n_kv_heads: 8
d_ffn: 8192
max_seq_len: 4096
rope_theta: 500000.0
dropout: 0.0
bias: false
use_flash_attn: true
use_fp8: true
train:
max_steps: 34000 # 8.91B × 4 epoch
batch_size: 4
grad_accum_steps: 8 # eff_batch: 4 × 8 × 8GPU × 4096 ≈ 1M tok/step
lr: 1.5e-4
min_lr: 1.5e-5
weight_decay: 0.1
warmup_steps: 2000
max_grad_norm: 1.0
log_interval: 10
save_interval: 500
eval_interval: 200
fp8_format: "MXFP8"
tokenizer:
vocab_size: 64000
type: sentencepiece_unigram
```
---
## 2. 데이터 파이프라인
### 즉시 사용 가능
| 소스 | 크기 | 토큰 | 상태 |
|------|------|---------|------|
| korean_c4_train.bin | 15.1 GB | 7.56B | 토큰화 완료 |
| korean_namuwiki_train.bin | 2.2 GB | 1.08B | 토큰화 완료 |
| korean_wiki_train.bin | 0.5 GB | 0.26B | 토큰화 완료 |
| **합계 (korean_train.bin)** | **17.8 GB** | **8.91B** | **즉시 시작 가능** |
### 추가 준비 필요 (병렬 토큰화)
| 소스 | 크기 | 추정 토큰 | 작업 | 예상 소요 |
|------|------|----------|------|----------|
| culturax_ko | 60 GB | ~30-40B | parquet토큰화 | 4-6h |
| hplt_ko | 23 GB | ~12-15B | 토큰화 | 2-3h |
| cc100_ko | 14 GB | ~8-10B | xz해제+토큰화 | 2h |
| oscar_ko | 9.2 GB | ~5-6B | 토큰화 | 1-2h |
| korean_textbooks | 6.4 GB | ~3-4B | 토큰화 | 1h |
| **합계** | **~123 GB** | **~70-80B** | | **8-12h (병렬)** |
### Chinchilla 분석
```
3.42B × 20 = 68.4B tokens (최적)
즉시 사용 가능: 8.91B × 4 epoch = 35.6B (최적의 52%)
extra 포함 시: ~80-90B → 충분 (131%)
```
### 데이터 타임라인
| 시점 | 행동 |
|------|------|
| **지금** | korean_train.bin 8.91B로 사전학습 시작 (4 epoch) |
| **병렬** | korean_extra 토큰화 + MinHash 중복제거 + PPL 필터 진행 |
| **Phase 2** | 전체 60-80B 토큰으로 extended pretrain (선택) |
### SFT 데이터
| 항목 | |
|------|-----|
| 현재 클린 데이터 | ~120-135K 샘플 (필터링 ) |
| Val split | 10% (~12-13K) |
| 3B에 충분? | (7B Alpaca도 52K로 학습) |
### 추가 고품질 SFT 소스 (선택)
- `hPark/orca-ko` (~200K)
- `maywell/synatra-orca` (~300K)
- `HAERAE-HUB/qarv-instruct-100k` (100K)
- 필터링 200-300K 사용 가능
---
## 3. 사전학습 계획
### 하이퍼파라미터
| 파라미터 | | 근거 |
|---------|-----|------|
| LR | 1.5e-4 | 3B 표준 (1B의 3e-4 대비 보수적) |
| Min LR | 1.5e-5 | LR의 10% |
| Warmup | 2,000 steps | ~6% |
| Weight Decay | 0.1 | Pretrain 표준 |
| Batch Size | 4/GPU × 8GPU × 8 grad_accum = 256 | eff ~1M tok/step |
| Max Steps | 34,000 | 8.91B × 4 epoch |
| Precision | MXFP8 | B200 최적화 |
| Grad Clip | 1.0 | 표준 |
### 예상 소요 시간
```
1B 실측: ~75,700 tok/s (단일 B200)
3B 예상: 파라미터 3× → throughput ~40-50% 감소
BUT batch 최적화 + FP8 → 보정
보수적 추정:
8.91B × 4 epoch = 35.6B tokens
처리량: ~400K tok/s (8× B200, FP8, 최적 배치)
소요: 35.6B / 400K = 89,000초 ≈ 24.7시간
낙관적 추정:
처리량: ~600K tok/s → 16.5시간
채택 추정: ~26시간
```
### 모니터링
```bash
# 실시간 로그
tail -f checkpoints/korean_3b_fp8/train.log
# TensorBoard
tensorboard --logdir checkpoints/korean_3b_fp8/tensorboard --port 6007
# GPU 상태
watch -n 10 nvidia-smi
```
**핵심 관찰 수치**:
| 수치 | 정상 범위 | 경고 | 즉시 중단 |
|------|----------|------|----------|
| Train Loss | 시작 ~10, 수렴 ~3-4 | 정체 5000+ steps | 발산 (상승) |
| GNorm | 0.5-2.0 | >5.0 | >50 |
| PPL | 하강 추세 | 정체 | 상승 |
| GPU Util | >90% | <70% | <50% (병목) |
| tok/s | >300K | <200K | <100K |
### 체크포인트 전략
| Step | 행동 |
|------|------|
| 500 | Sanity check loss 하강 ? OOM 없나? |
| 5,000 | 1 epoch 완료 PPL 측정, 한국어 텍스트 perplexity <20? |
| 10,000 | 중간점 loss 추세 확인, 과적합 징후? |
| 17,000 | 2 epoch PPL < 15? |
| 25,000 | 3 epoch PPL < 12? |
| 34,000 | 최종 PPL < 10 목표 |
**디스크**: 체크포인트 1개 ~27GB (모델 7GB + optimizer 20GB) × save_interval=500 68개 = ~1.8TB
**save_interval=2000으로 변경 권장** 17개 = ~460GB
---
## 4. SFT 계획
### 1B 교훈 전부 적용
| 교훈 | 1B에서 발견 | 3B에 적용 |
|------|------------|-----------|
| Dynamic padding 필수 | 4096 고정으로 90% 낭비 | sft_dataset.py 수정 완료, 그대로 사용 |
| EOS 보존 | 트렁케이션 EOS 손실 | `response_ids[-1] = eos_id` 강제 |
| Val split 필수 | 과적합 모니터링 불가했음 | 10% split |
| 3-4 epoch | 2 epoch은 underfitting | max_steps 계산 |
| OpenOrca 과대표집 방지 | 5× 가중치로 과적합 | 2.0× 이하 |
| 데이터 품질 필터 | `</s>` 리터럴, Q/A 마커 오염 | 필터 스크립트 완성 |
| 올바른 포맷 통일 | 학습/추론 포맷 불일치 | `<\|user\|>/<\|assistant\|>` 일관 |
| Early stopping | val_loss 상승해도 학습 계속됨 | patience=5 구현 |
| NEFTune alpha | 10.0은 과도 | 5.0으로 조정 |
### 하이퍼파라미터
| 파라미터 | | 근거 |
|---------|-----|------|
| LR | 2e-5 | SFT 표준 (Alpaca, Vicuna 동일) |
| Warmup | 300 steps | ~3% |
| Max Steps | 10,000 | ~3-4 epoch (데이터 크기 따라 조정) |
| Batch Size | 4/GPU × 2 grad_accum × 8GPU = 64 | SFT 표준 |
| Weight Decay | 0.01 | SFT 표준 (pretrain 0.1보다 낮게) |
| NEFTune | alpha=5.0 | 과적합 방지 |
| Eval Interval | 500 steps | |
| Early Stopping | patience=5 | 2,500 steps 무개선 중단 |
| Dropout | 0.05 | 과적합 방지 (1B에서 0.0이었음) |
### 실행 명령어
```bash
RUN_NAME=korean_3b_sft \
BASE_CHECKPOINT=checkpoints/korean_3b_fp8/checkpoint-BEST \
SFT_DATA=data/sft_v2/train.jsonl \
VAL_DATA=data/sft_v2/val.jsonl \
MAX_STEPS=10000 \
LR=2.0e-5 \
WARMUP_STEPS=300 \
bash scripts/launch_sft.sh
```
**예상 시간**: ~2시간 (3B는 1B 대비 ~2.5× 느림)
### 성공 기준
| 지표 | 목표 | 실패 기준 |
|------|------|----------|
| Train Loss | < 1.85 | > 2.00 |
| Val Loss | Train의 1.1배 이내 | 1.2배 초과 |
| 반복률 (raw) | < 10% | > 15% |
| 반복률 (rep_penalty=1.1) | < 3% | > 8% |
| EOS 종료율 | > 80% | < 60% |
---
## 5. ORPO 계획
### 타이밍: SFT 완료 후, 반복률 >5%일 때만
### 데이터
| 소스 | 샘플 | 유형 |
|------|---------|------|
| maywell/ko_Ultrafeedback_binarized | ~60K | 일반 도메인 preference |
| kuotient/orca-math-korean-dpo-pairs | 수천 | 수학 도메인 |
| **자체 생성** (3B SFT 모델로) | ~2-5K | 반복 타겟 preference |
**자체 생성 방법**:
1. 3B SFT 모델로 1000 프롬프트 × 4 temperature 생성
2. 반복 출력 rejected, 깨끗한 출력 chosen
3. 3B에서는 반복률 낮으므로 **1B보다 훨씬 편향 적음**
### 하이퍼파라미터
```python
ORPOConfig(
learning_rate=5e-7, # 매우 낮은 LR (정렬 학습)
num_train_epochs=1, # 1 epoch 충분
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
beta=0.1, # ORPO coefficient
)
```
### 예상 시간: 1-2시간
### 목표: 반복률 <3% (raw), <1% (rep_penalty=1.1)
---
## 6. 평가 계획
### 벤치마크
| 벤치마크 | 도구 | 1B 예상 | 3B 목표 |
|---------|------|---------|---------|
| ko_ifeval | lm-eval-harness | 15-25% | **35-45%** |
| ko_winogrande | lm-eval-harness | 53-58% | **60-68%** |
| KoBEST (5 tasks avg) | lm-eval-harness | 55-60% | **65-75%** |
| 반복률 (raw) | test_generation_params.py | 18% | **<8%** |
| 반복률 (+rep_penalty) | test_generation_params.py | ~5-8% | **<3%** |
### 실행 명령어
```bash
# ko_ifeval
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_3b_sft/checkpoint-BEST,dtype=bfloat16 \
--tasks ko_ifeval --device cuda:0
# KoBEST
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_3b_sft/checkpoint-BEST,dtype=bfloat16 \
--tasks kobest_boolq,kobest_copa,kobest_wic,kobest_hellaswag,kobest_sentineg \
--device cuda:0
```
### 판단 기준
```
[3B SFT 평가 결과]
┌─────────────────────┼─────────────────────┐
│ │ │
✅ PASS ⚠️ PARTIAL ❌ FAIL
반복률<5% 반복률 5-10% 반복률>10%
ko_ifeval>35% ko_ifeval 25-35% ko_ifeval<25%
│ │ │
▼ ▼ ▼
배포 준비 ORPO 적용 원인 분석
(Phase 배포) (Phase 5) (데이터/아키텍처 점검)
```
---
## 7. 전체 타임라인
### 한눈에 보기
```
Day 0 (지금, 04:30)
├── [04:30] Config 작성 + sanity check 30분
├── [05:00] 🔥 사전학습 시작 ← 오늘 밤 시작
├── [05:00] (병렬) korean_extra 토큰화 시작 8-12h
Day 1 (내일)
├── [~07:00] 사전학습 진행 중... (~26시간)
├── [중간] 체크포인트 PPL 확인
Day 1.5
├── [~07:00+26h = Day 2 07:00] 사전학습 완료
├── [07:00] SFT 시작 2시간
├── [09:00] SFT 완료 → 평가
├── [09:30] 반복률 <5%? → 배포
├── [09:30] 반복률 5-10%? → ORPO 1-2시간
├── [11:30] ORPO 완료 → 최종 평가
Day 2
├── 벤치마크 풀 스위트 2시간
├── HuggingFace 업로드 1시간
├── vLLM 서빙 테스트 1시간
└── 🎉 배포 완료
```
### 표 형식
| 단계 | 시작 | 소요 | 완료 | 의존성 |
|------|------|------|------|--------|
| **0. Config + Sanity** | Day 0 04:30 | 30분 | 05:00 | 없음 |
| **1. 사전학습** | Day 0 05:00 | **26시간** | Day 1 ~07:00 | Config |
| **(병렬) Extra 토큰화** | Day 0 05:00 | 8-12시간 | Day 0 ~17:00 | 없음 |
| **2. SFT** | Day 1 07:00 | **2시간** | Day 1 09:00 | 사전학습 완료 |
| **3. 1차 평가** | Day 1 09:00 | 30분 | Day 1 09:30 | SFT 완료 |
| **4. ORPO (조건부)** | Day 1 09:30 | 1-2시간 | Day 1 11:30 | 반복률 >5% |
| **5. 풀 벤치마크** | Day 1 11:30 | 2시간 | Day 1 13:30 | |
| **6. 배포** | Day 1 13:30 | 2시간 | Day 1 15:30 | 벤치마크 통과 |
---
## 8. 의사결정 트리
### Phase 1: 사전학습 중 (Step 5000, 10000, ...)
```
Loss 하강 중?
├── YES → 계속
└── NO (정체 3000+ steps)
├── 데이터 품질 문제? → PPL 필터 강화 + 재시작
├── LR 문제? → LR 반감 후 resume
└── 모델 아키텍처? → d_model/n_layers 조정 (최후 수단)
PPL (한국어 텍스트)?
├── < 15 at 2 epoch → 정상
├── 15-20 at 2 epoch → 주의 (데이터 부족?)
└── > 20 at 2 epoch → 문제 (데이터 품질 or 하이퍼파라미터)
OOM?
├── batch_size 4→2, grad_accum 8→16
└── gradient checkpointing 확인
```
### Phase 2: SFT 후
```
반복률 (raw)?
├── < 5% → ✅ 배포 가능! (ORPO 건너뜀)
├── 5-10% → ⚠️ ORPO 적용
├── 10-15% → 🟠 SFT 하이퍼파라미터 조정 후 재시도
└── > 15% → ❌ 사전학습 품질 문제 → Phase 1 재점검
ko_ifeval?
├── > 35% → ✅ 목표 달성
├── 25-35% → 🟡 데이터 augmentation 고려
└── < 25% → 🔴 3B에서도 이러면 데이터 문제 심각
```
### Phase 3: ORPO 후
```
반복률?
├── < 3% → ✅ 완료
├── 3-5% → 🟡 서빙 시 rep_penalty=1.05로 보완
└── > 5% → 🔴 preference 데이터 재검토
```
---
## 9. 예외 대응
| 시나리오 | 확률 | 대응 |
|---------|------|------|
| **OOM** | 5% | batch_size 4→2, grad_accum 2× |
| **Loss 발산** | 5% | LR 반감, grad_clip 0.5로 강화 |
| **GPU Hang / NCCL** | 10% | `pkill torchrun` → latest checkpoint에서 resume |
| **디스크 부족** | 3% | save_interval 2000→5000, 오래된 ckpt 삭제 |
| **사전학습 후 PPL >20** | 10% | 데이터 추가 (korean_extra) + extended training |
| **SFT 후 반복률 >15%** | 10% | 데이터 필터 재강화 + LR/epoch 조정 |
| **ORPO 후 품질 퇴행** | 15% | ORPO LR 낮추기 (5e-7 → 1e-7), beta 조정 |
| **FP8 수치 불안정** | 5% | BF16으로 폴백 (시간 1.5× 증가) |
### NCCL/GPU 복구 스크립트
```bash
# 프로세스 정리
pkill -f torchrun && sleep 5
# 최신 체크포인트 찾기
LATEST=$(ls -d checkpoints/korean_3b_fp8/checkpoint-[0-9]* 2>/dev/null \
| sort -t- -k2 -n | tail -1)
# 재시작
bash scripts/run_pretrain.sh --config configs/korean_3b_fp8.yaml --resume "${LATEST}"
```
---
## 10. 1B에서 배운 교훈 체크리스트
### 학습 전 필수 확인
- [ ] **Dynamic padding 작동 확인**: `SFTDataset.__getitem__`이 가변 길이 텐서 반환, `dynamic_collate_fn` 배치별 패딩
- [ ] **EOS 보존 확인**: `grep -n "eos_token_id" data/sft_dataset.py` — 트렁케이션 시 강제 부착
- [ ] **Val split 존재**: `wc -l data/sft_v2/val.jsonl` → 10K+ 확인
- [ ] **데이터 오염 제거**: `</s>` 리터럴, Q/A 마커, 자체 반복 패턴 필터 적용됨
- [ ] **OpenOrca 가중치 ≤ 2.0**: prepare_sft_data.py에서 확인
- [ ] **프롬프트 포맷 통일**: 학습 = 추론 = `<|user|>/<|assistant|>`
- [ ] **Labels shift 정상**: trainer.py에서 `logits[t]``targets[t]` 직접 비교, labels에서 shift 처리
### 학습 중 필수 모니터링
- [ ] **Val loss 추적**: 매 eval_interval마다 기록, 3연속 상승 시 주의
- [ ] **Early stopping 활성화**: patience=5
- [ ] **Loss 0 감지**: 3 step 연속 loss < 0.01 labels 버그 즉시 확인
- [ ] **Grad norm**: > 10이면 경고, > 50이면 중단
### 학습 후 필수 확인
- [ ] **올바른 포맷으로 생성 테스트**: `<|user|>\n{질문}\n<|assistant|>\n`
- [ ] **rep_penalty 없이 반복률 측정**: 목표 <10%
- [ ] **rep_penalty=1.1로 반복률**: 목표 <3%
- [ ] **벤치마크 실행**: ko_ifeval, KoBEST
### 절대 반복하지 말 것
- 학습/추론 포맷 불일치 상태로 평가하지
- Val split 없이 학습하지
- 특정 소스 5× 이상 업샘플링하지
- 2 epoch 미만으로 학습하지
- Dynamic padding 미작동 상태로 학습하지
- 반복률 측정 없이 "loss 낮으니 OK" 판단하지
---
## 🔥 오늘 밤 지금 당장 시작할 첫 번째 명령어
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
# 1. 3B config 작성
cat > configs/korean_3b_fp8.yaml << 'YAML'
model:
vocab_size: 64000
d_model: 3072
n_layers: 32
n_heads: 24
n_kv_heads: 8
d_ffn: 8192
max_seq_len: 4096
rope_theta: 500000.0
dropout: 0.0
bias: false
use_flash_attn: true
use_fp8: true
train:
max_steps: 34000
batch_size: 4
grad_accum_steps: 8
lr: 1.5e-4
min_lr: 1.5e-5
weight_decay: 0.1
warmup_steps: 2000
max_grad_norm: 1.0
log_interval: 10
save_interval: 2000
eval_interval: 500
fp8_format: "MXFP8"
YAML
# 2. 사전학습 시작!
bash scripts/run_pretrain.sh --config configs/korean_3b_fp8.yaml
```
---
## ⚡ 가장 중요한 판단 포인트 3개
### 1⃣ 사전학습 Step 5,000 (1 epoch 완료) — "기초 체력 확인"
- **PPL < 20?** 정상, 계속
- **PPL > 20?** → 데이터 품질 or 하이퍼파라미터 문제. 즉시 진단
### 2⃣ SFT 후 반복률 측정 — "3B의 진짜 실력"
- **<5%?** 🎉 ORPO 없이 바로 배포. 대성공
- **5-10%?** ORPO 1라운드로 해결 가능
- **>10%?** → 사전학습 품질 재검토 필요 (이 확률은 낮음)
### 3⃣ ko_ifeval 점수 — "실사용 가능 수준?"
- **>35%?** → 3B 한국어 모델로서 경쟁력 있음. 배포
- **25-35%?** → 추가 SFT 데이터로 개선 여지 있음
- **<25%?** 사전학습 데이터가 부족했을 가능성 extended pretrain 고려
---
*"1B에서 배웠고, 3B에서 증명한다."*
*이 문서는 각 Phase 완료 시 실측 결과로 업데이트할 것.*

View File

@@ -0,0 +1,162 @@
# 3B 사전학습 데이터 파이프라인
**작성일:** 2026-02-27
**프로젝트:** `/PROJECT/0325120031_A/ghong/taketimes/llm-bang/`
---
## 1. 현재 데이터 현황
### 토큰화 완료 (즉시 사용 가능)
| 파일 | 크기 | 토큰 수 |
|------|------|---------|
| korean_c4_train.bin | 15 GB | **7.56B** |
| korean_namuwiki_train.bin | 2.1 GB | **1.08B** |
| korean_wiki_train.bin | 500 MB | **262M** |
| korean_train.bin (위 3개 병합) | 17 GB | **9.0B** |
| train.bin (영어 기타) | 1.2 GB | **606M** |
**즉시 사용 가능: ~9.6B 토큰**
### 미토큰화 원본 (korean_extra/)
| 소스 | 디스크 크기 | 형식 | 예상 토큰 수 | 비고 |
|------|------------|------|-------------|------|
| CulturaX (ko) | 60 GB (32 parquet) | parquet, `text` 컬럼 | **~11.6B** | 고품질 웹 + mC4 중복제거 |
| cc100 (ko) | 14 GB xz → 54 GB text | xz 압축 텍스트 | **~13.5B** | 줄 단위 텍스트 |
| HPLT (en-ko) | 23 GB (193 parquet) | 병렬 코퍼스, `tgt_doc.sentences` | **~3.7B** | 한국어 측만 추출 필요 |
| OSCAR (ko) | 9.2 GB (27 parquet) | 중첩 구조, `text[].text` | **~2.0B** | 웹 텍스트 |
| korean_webtext | 4.2 GB (18 parquet) | parquet | **~1.5B** | 한국어 웹 |
| korean_textbooks | 6.4 GB | MMLU 스타일 parquet | **~0.5B** | 교과서/시험 (구조적) |
| finepdfs_edu_ko | 2.9 GB (parquet) | parquet | **~0.8B** | 교육 PDF |
| namuwiki_extracted | 2.2 GB | 텍스트 | **~0.6B** | 이미 namuwiki_train.bin과 중복 가능 |
| kovast | 449 MB | - | **~0.1B** | 소량 |
| korean_safe_conv | 51 MB | jsonl | **~15M** | 대화 데이터 |
| evol_instruct_ko | 144 MB | - | **~40M** | SFT용, pretrain 부적합 |
**미토큰화 총 예상: ~34B 토큰**
**전체 합계: ~43B 토큰 (중복 제거 전)**
---
## 2. 3B 모델 학습 목표
Chinchilla 스케일링 법칙에 따른 최적:
- **최소:** 3B × 20 = **60B 토큰**
- **최적:** 3B × 50 = **150B 토큰**
- **현실적 목표:** 가용 데이터 ~35B 토큰 (중복 제거 후) → **2~3 epoch 반복으로 60-100B 토큰**
### 데이터 믹싱 전략
| 카테고리 | 소스 | 비율 | 이유 |
|----------|------|------|------|
| 고품질 웹 | CulturaX | 35% | 이미 중복 제거된 고품질 |
| 대규모 웹 | cc100 + mC4(기존) | 35% | 양적 확보 |
| 백과사전 | 위키 + 나무위키 | 10% | 사실 지식 |
| 보조 웹 | OSCAR + korean_webtext + HPLT | 15% | 다양성 |
| 전문 도메인 | textbooks + finepdfs | 5% | 교육 품질 |
---
## 3. 데이터 품질 필터링 계획
### Phase 1: 기본 필터 (빠름, 1-2시간)
- **언어 필터:** `langdetect`로 한국어 비율 < 50% 문서 제거
- HPLT: 병렬 코퍼스라 한국어 추출만 하면
- cc100: 이미 한국어지만 혼입 확인
- **길이 필터:** 50자 미만 문서 제거
- **중복 제거:** 같은 5회 이상 반복하는 문서 제거
### Phase 2: MinHash 중복 제거 (4-8시간)
```
도구: datasketch (pip install datasketch)
방법: 5-gram MinHash, 128 permutations
임계값: Jaccard > 0.8 → 중복
예상 제거율: 15-25% (특히 cc100 + CulturaX 간)
```
- CulturaX는 이미 내부 중복 제거됨 다른 소스와의 교차 중복만 체크
- 72코어 병렬 처리로 ~4시간 예상
### Phase 3: Perplexity 필터 (선택, 12-24시간)
- 현재 1B 모델로 문서 perplexity 계산
- 하위 5% (너무 쉬움 = 템플릿/반복) + 상위 5% (노이즈) 제거
- **권장: 3B 학습 2차 학습 적용** (시간 절약)
---
## 4. 토큰화 파이프라인
### 우선순위 및 예상 시간 (72코어 기준)
| 순위 | 소스 | 예상 시간 | 이유 |
|------|------|----------|------|
| 1 | CulturaX | 3-4시간 | 60GB parquet, 가장 크고 고품질 |
| 2 | cc100 | 2-3시간 | xz 해제 30분 + 토큰화 2시간 |
| 3 | OSCAR | 1시간 | 9.2GB, 구조 파싱 필요 |
| 4 | korean_webtext | 30분 | 4.2GB |
| 5 | HPLT (Korean) | 1-2시간 | 한국어 추출 + 토큰화 |
| 6 | textbooks + finepdfs | 30분 | 소량 |
**총 예상: 8-12시간 (병렬 처리 시)**
---
## 5. 타임라인
### 최소 시작 방안 (즉시, 0시간)
- 기존 `korean_train.bin` (9B 토큰)으로 3B 학습 시작 가능
- 1 epoch에 부족하지만 학습 코드 검증 + 초기 학습에는 충분
### Phase A: 빠른 확장 (8-12시간)
1. CulturaX 토큰화 (3-4시간)
2. cc100 해제 + 토큰화 (2-3시간, CulturaX와 병렬 가능)
3. 기타 소스 토큰화 (2시간)
4. 병합 ~35B 토큰
### Phase B: 품질 필터링 (추가 4-8시간)
1. 기본 필터 (1-2시간)
2. MinHash 중복 제거 (4-8시간)
3. 최종 병합 ~28-30B 토큰 (깨끗한 데이터)
### Phase C: 학습
- 30B 토큰 × 2 epoch = 60B 토큰 (Chinchilla 최소)
- 또는 30B × 3 epoch = 90B 토큰 (안전)
---
## 6. 파일 구조 (최종)
```
data/
├── korean_train.bin # 기존 9B (c4+wiki+namuwiki)
├── korean_val.bin
├── culturax_train.bin # ~11.6B
├── culturax_val.bin
├── cc100_train.bin # ~13.5B
├── cc100_val.bin
├── oscar_train.bin # ~2B
├── oscar_val.bin
├── webtext_train.bin # ~1.5B
├── webtext_val.bin
├── hplt_ko_train.bin # ~3.7B
├── hplt_ko_val.bin
├── extra_train.bin # textbooks + finepdfs + kovast
├── extra_val.bin
├── merged_3b_train.bin # 전체 병합 (~35B)
└── merged_3b_val.bin
```
---
## 7. 요약
| 항목 | |
|------|-----|
| 즉시 사용 가능 토큰 | **9.6B** |
| 추가 토큰화 | **~35B** (중복 제거 ) |
| 중복 제거 예상 | **~28-30B** |
| 3B Chinchilla 최소 | 60B (2 epoch) |
| 데이터 준비 최소 시간 | **0시간** (기존 데이터 사용) |
| 전체 파이프라인 완료 | **12-20시간** |
| 학습 시작 추천 | 기존 9B로 즉시 시작 + CulturaX 병렬 준비 |

View File

@@ -0,0 +1,223 @@
# 3B Korean LLM 학습 파이프라인 — 전체 계획
**작성일:** 2026-02-27
**서버:** 8× B200 192GB, NVSwitch, CUDA 13.1, PyTorch 2.10, TransformerEngine FP8
**프로젝트:** `/PROJECT/0325120031_A/ghong/taketimes/llm-bang/`
---
## 1. 모델 아키텍처 (3B)
| 항목 | 1B (현재) | 3B (신규) | 근거 |
|------|-----------|-----------|------|
| d_model | 2048 | 3072 | LLaMA-3 3B 참고 |
| n_layers | 24 | 28 | |
| n_heads | 16 | 24 | |
| n_kv_heads | 4 (GQA 4:1) | 8 (GQA 3:1) | 더 큰 모델에서 KV 좀 더 여유 |
| d_ffn | 5472 | 8192 | ~2.67× d_model, 128배수 (FP8) |
| max_seq_len | 4096 | 4096 | 동일 |
| vocab_size | 64000 | 64000 | 동일 토크나이저 |
| 총 파라미터 | ~1.0B | ~3.0B | |
## 2. 사전학습 하이퍼파라미터
| 항목 | 1B 값 | 3B 값 | 근거 |
|------|-------|-------|------|
| **Learning Rate** | 2e-4 | **1.5e-4** | μP scaling ~1/√(3), LLaMA-3 3B 참고 |
| **LR Schedule** | cosine decay | cosine decay | 동일 |
| **Warmup Steps** | 2000 | 2000 | 57k의 3.5% (적절) |
| **Weight Decay** | 0.1 | 0.1 | 표준 |
| **Gradient Clip** | 1.0 | 1.0 | 표준 |
| **Batch Size (local)** | 8 | 8 | per GPU |
| **Grad Accum** | 4 | 4 | |
| **Eff Batch** | 1M tok/step | 1M tok/step | 8×8×4×4096 |
| **Max Steps** | 34,000 | **57,000** (60B tok) | Chinchilla: 3B → 60B min |
| **총 토큰** | 35.6B | **60B** (최소) / 95k steps=100B (권장) | |
| **Save Interval** | 500 | **2000** | 27GB/체크포인트 → 덜 자주 |
| **Eval Interval** | 200 | **500** | |
| **FP8** | MXFP8 | MXFP8 | B200 네이티브 |
## 3. 자원 예측
### 체크포인트
- model.pt: 3B × 1B(FP8) ≈ **3GB**
- optimizer.pt: 3B × 8B(FP32 states) ≈ **24GB**
- 체크포인트당 **~27GB**
- 2000 step 간격 → 최대 ~28개 = **756GB**
- /PROJECT 여유 19TB → **충분**
### VRAM
- 모델: ~6GB (FP8)
- Optimizer states: ~24GB
- Activations: ~40-60GB (batch 8, seq 4096)
- 총: ~80-90GB/GPU → B200 192GB로 **충분** (47% 사용)
### 학습 시간 예상
- 1B: 34k steps → ~12h (관찰값 기반)
- 3B: step당 ~3배 → step ~1.05s 예상
- 57k steps × 1.05s ≈ **~17h** (60B tokens)
- 95k steps × 1.05s ≈ **~28h** (100B tokens)
- 안전 마진 포함: **24~36h**
## 4. NCCL 최적화 (B200 NVSwitch)
```bash
export NCCL_IB_DISABLE=1 # 단일 노드, IB 불필요
export NCCL_ALGO=Ring,Tree # 3B gradient 크기에 두 알고리즘 병행
export NCCL_PROTO=Simple # NVLink bulk transfer 최적
export NCCL_MIN_NCHANNELS=16
export NCCL_MAX_NCHANNELS=16
export NCCL_BUFFSIZE=134217728 # 128MB (1B의 64MB에서 증가)
export NCCL_P2P_LEVEL=NVL # NVLink 직접 P2P
```
## 5. SFT 계획 (사전학습 완료 후)
### 데이터
- 1B SFT 데이터 (161k 샘플) — 기존 검증 완료
- 추가 고품질 데이터 고려:
- Ko-Alpaca 확장
- ShareGPT-ko 추가
- 목표: **200k+ 샘플**
### 하이퍼파라미터
| 항목 | 1B SFT | 3B SFT | 근거 |
|------|--------|--------|------|
| LR | 2e-5 | **1e-5** | 더 큰 모델 → 더 낮은 LR |
| Batch (local) | 4 | 4 | |
| Grad Accum | 2 | 2 | |
| Eff Batch | 64 | 64 | |
| Max Steps | 9000 | **12000** | 3B는 수렴에 좀 더 필요 |
| Warmup | 300 | 500 | |
| Max Seq Len | 4096 | 4096 | |
### 1B SFT 교훈 반영
- ✅ Labels shift 버그 — sft_dataset.py 이미 수정됨
- ✅ 프로세스 중복 방지 — launch 스크립트에 pgrep 체크 추가
- ✅ Loss 0 감지 — 모니터링에 포함
### 예상 SFT 시간
- 12000 steps × ~0.8s/step ≈ **~3h**
## 6. ORPO 계획 (SFT 완료 후)
### 데이터
- **`maywell/ko_Ultrafeedback_binarized`** — Korean preference 데이터
- 다운로드:
```python
from datasets import load_dataset
ds = load_dataset("maywell/ko_Ultrafeedback_binarized")
```
### 설정
| 항목 | 값 | 근거 |
|------|-----|------|
| LR | 5e-6 | ORPO 표준, SFT보다 낮게 |
| β (ORPO lambda) | 0.1 | 논문 기본값 |
| Batch (local) | 2 | chosen+rejected 쌍 → 메모리 2배 |
| Grad Accum | 4 | eff_batch = 64 |
| Max Steps | ~3000-5000 | 데이터 크기에 따라 |
| Max Seq Len | 4096 | |
### 예상 ORPO 시간
- 5000 steps × ~1.5s/step (쌍 비교) ≈ **~2h**
## 7. 전체 타임라인
```
Phase 1: 사전학습 (60B tokens)
├─ 준비: configs, scripts 확인 ~30분
├─ 학습: 57k steps ~24-36h
└─ 평가: eval suite ~1h
Phase 2: SFT
├─ 데이터 준비: 161k+ 검증 ~30분
├─ 학습: 12k steps ~3h
└─ 평가 ~30분
Phase 3: ORPO
├─ 데이터 다운로드+처리 ~30분
├─ 학습: 3-5k steps ~2h
└─ 최종 평가 ~1h
총 예상: 약 3-4일 (사전학습 포함)
사전학습만: 24-36h
SFT+ORPO: ~6h
```
## 8. 예외 대응 플레이북 (3B 특화)
### 8-1. 서버 재시작이 필요한 경우
1. `Ctrl+C`로 graceful stop (SIGINT → 현재 step 완료 후 체크포인트 저장)
2. 안 되면 `kill -15 <PID>` → 10초 대기 → `kill -9`
3. 재시작 후: `bash scripts/launch_3b_pretrain.sh` (자동 resume 감지)
### 8-2. 체크포인트 손상
```bash
# 최근 체크포인트 무결성 확인
python -c "
import torch
ckpt = torch.load('checkpoints/korean_3b_fp8_run1/checkpoint-XXXXX/model.pt', weights_only=True)
print(f'Keys: {len(ckpt)}')
print(f'Total params: {sum(v.numel() for v in ckpt.values()):,}')
"
# 손상 시 → 이전 체크포인트로 resume
bash scripts/launch_3b_pretrain.sh --resume checkpoints/korean_3b_fp8_run1/checkpoint-YYYYY
```
### 8-3. NCCL Hang 감지
- `monitor_3b.sh` 로그 10분 멈춤 → CRITICAL 알림
- `--auto-restart` 옵션으로 자동 kill + 재시작 가이드
- 수동: `kill -9 $(pgrep -f pretrain.py)` → 재실행
### 8-4. 디스크 공간 부족
- `monitor_3b.sh --auto-cleanup`: MAX_CHECKPOINTS(15) 초과 시 오래된 것 자동 삭제
- 수동 정리:
```bash
ls -d checkpoints/korean_3b_fp8_run1/checkpoint-* | sort -V | head -10 | xargs rm -rf
```
### 8-5. Loss 발산 (NaN / spike)
1. 즉시 중단
2. 최근 정상 체크포인트에서 resume
3. LR 50% 감소하여 재시작: `--lr 7.5e-5`
4. 반복 시 warmup 늘리기: `--warmup_steps 4000`
## 9. 모니터링 스크립트
### 실시간 감시
```bash
bash scripts/monitor_3b.sh # 기본 (60초 간격)
bash scripts/monitor_3b.sh --check-once # 1회
bash scripts/monitor_3b.sh --auto-cleanup # 자동 체크포인트 정리
bash scripts/monitor_3b.sh --auto-restart # NCCL hang 시 자동 kill
```
### TensorBoard
```bash
tensorboard --logdir checkpoints/korean_3b_fp8_run1/tensorboard --port 6007
```
## 10. 실행 커맨드 요약
```bash
# Step 1: 사전학습 시작
bash scripts/launch_3b_pretrain.sh
# Step 1b: 모니터링 (별도 터미널)
bash scripts/monitor_3b.sh --auto-cleanup
# Step 2: 사전학습 완료 후 SFT (launch_sft.sh 3B 버전 필요)
BASE_CHECKPOINT=checkpoints/korean_3b_fp8_run1/checkpoint-XXXXX \
RUN_NAME=korean_3b_sft \
bash scripts/launch_sft.sh --lr 1e-5 --max_steps 12000 --warmup_steps 500
# Step 3: ORPO (별도 스크립트 필요 — train/orpo.py 작성 필요)
# TBD after SFT
```
---
**상태:** ✅ 파이프라인 설계 완료, 스크립트 작성 완료
**다음 액션:** `bash scripts/launch_3b_pretrain.sh` 실행

View File

@@ -0,0 +1,601 @@
# 🗺️ MASTER PLAN: 한국어 LLM 1B 재학습 → 3B → 배포
**작성일**: 2026-02-27
**프로젝트**: `/PROJECT/0325120031_A/ghong/taketimes/llm-bang/`
**결정**: Restart (base checkpoint에서 클린 재학습)
**총 예상 기간**: ~35시간 (1B: 3시간 → 3B pretrain: 26시간 → 3B SFT+평가: 6시간)
---
## 📊 전체 타임라인 한눈에 보기
```
Phase 0 ██░░░░░░░░░░░░░░░░░░░░░░ 30분 데이터/코드 준비
Phase 1 ████░░░░░░░░░░░░░░░░░░░░ 40분 1B SFT 재학습
Phase 2 ██████░░░░░░░░░░░░░░░░░░ 2시간 1B 평가
────── 여기서 판단 ──────
Phase 3A ████████░░░░░░░░░░░░░░░░ 3-5시간 (조건부) 1B 추가 개선
Phase 3B ████████████████████████ 26시간 3B 사전학습
Phase 4 ████░░░░░░░░░░░░░░░░░░░░ 2시간 3B SFT
Phase 5 ██████░░░░░░░░░░░░░░░░░░ 4시간 평가 & 배포
```
---
## Phase 0: 재학습 직전 준비 (오늘, ~30분)
### 체크리스트
#### ☐ 0-1. 데이터 재생성 (~20분)
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
# prepare_sft_data.py 재실행 (강화 필터 + 수정된 가중치)
python data/prepare_sft_data.py \
--output_dir data/sft_v2/ \
--val_split 0.1
```
**확인 사항**:
- 필터링 후 **120K-135K 샘플** 남아야 함 (기존 159K에서 저품질 제거)
- `</s>` 리터럴 113건, Q/A 마커 ~550건, 자체반복 57건 제거 확인
- OpenOrca 가중치: 5.0 → 2.0으로 감소 확인
- Val split: ~12-13K 샘플 (10%)
- 짧은 output (<80자) 제거 확인
```bash
# 결과 확인
wc -l data/sft_v2/train.jsonl data/sft_v2/val.jsonl
# 예상: train ~108K-120K, val ~12K-13K
```
**완료 기준**: train 100K+ 샘플, val 10K+ 샘플. 제거된 샘플 spot check 실제 저품질.
#### ☐ 0-2. sft_dataset.py 수정 확인 (~5분)
이미 수정된 항목 확인:
| 수정 사항 | 파일 | 확인 |
|-----------|------|------|
| Dynamic padding 실제 작동 | `data/sft_dataset.py` `__getitem__` | 패딩 없이 실제 길이 텐서 반환 |
| EOS 보존 | `data/sft_dataset.py` L130-134 | `response_ids[:allowed-1] + [eos_id]` |
| Collate fn | `data/sft_dataset.py` `dynamic_collate_fn` | 배치별 가변 패딩 |
```bash
# 핵심 코드 확인
grep -n "allowed_response" data/sft_dataset.py
grep -n "eos_token_id" data/sft_dataset.py
grep -n "torch.full" data/sft_dataset.py # 4096 고정 패딩 없어야 함
```
#### ☐ 0-3. launch_sft.sh 수정 (~5분)
```bash
# 변경할 값들:
# RUN_NAME=korean_1b_sft_v2
# SFT_DATA=data/sft_v2/train.jsonl
# VAL_DATA=data/sft_v2/val.jsonl
# MAX_STEPS=10000 (3-4 epoch, 기존 5000에서 증가)
# WARMUP_STEPS=300 (3%)
cp scripts/launch_sft.sh scripts/launch_sft_v2.sh
# 편집 후 diff 확인
```
#### ☐ 0-4. Sanity Check (~5분)
```bash
# 100 steps만 빠르게 돌려서 파이프라인 정상 확인
bash scripts/launch_sft_v2.sh --max_steps 100
# 확인:
# - Loss가 2.0-2.5 범위에서 시작하는가? ✅
# - 배치 내 시퀀스 길이가 가변적인가? (로그에서 확인) ✅
# - Val loss가 출력되는가? ✅
# - OOM 없는가? ✅
```
**완료 기준**: 100 steps 에러 없이 완료, loss 합리적 범위, val loss 출력 확인.
---
## Phase 1: 1B SFT 재학습 (오늘, ~40분)
### 실행 명령어
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
RUN_NAME=korean_1b_sft_v2 \
BASE_CHECKPOINT=checkpoints/korean_1b_fp8_run1/checkpoint-0034000 \
SFT_DATA=data/sft_v2/train.jsonl \
VAL_DATA=data/sft_v2/val.jsonl \
MAX_STEPS=10000 \
WARMUP_STEPS=300 \
LR=2.0e-5 \
bash scripts/launch_sft.sh
```
### 모니터링
**실시간 로그**:
```bash
tail -f checkpoints/korean_1b_sft_v2/train.log
```
**TensorBoard**:
```bash
tensorboard --logdir checkpoints/korean_1b_sft_v2/tensorboard --port 6007
```
**핵심 수치**:
| 수치 | 정상 범위 | 경고 | 즉시 중단 |
|------|----------|------|----------|
| Train Loss | 시작 2.0-2.5, 최종 <1.90 | >2.5 at step 500+ | >3.0 (발산) |
| Val Loss | Train의 1.0-1.1배 | Train의 1.2배 | Train 대비 계속 상승 (과적합) |
| GNorm | 0.8-1.5 | >2.0 | >5.0 (gradient 폭발) |
| 학습 속도 | 기존 대비 2x+ (dynamic padding 효과) | 기존과 비슷 | 기존보다 느림 |
**체크포인트 관찰**:
- Step 500: 파이프라인 안정성 확인
- Step 2500: 중간 지점, loss 추세 확인
- Step 5000: 기존 학습과 비교 (loss < 1.97이어야 )
- Step 7500: 수렴 여부 확인
- Step 10000: 최종
### 성공 기준
| 지표 | 목표 | 실패 기준 |
|------|------|----------|
| Final Train Loss | < 1.90 | > 2.00 |
| Final Val Loss | < 2.00 | Train 대비 1.2배 초과 |
| Val Loss 추세 | 하강 or 안정 | 3연속 상승 (과적합) |
| 학습 시간 | ~40-60분 | >2시간 (dynamic padding 미작동) |
### 실패 시 대응
| 상황 | 원인 추정 | 대응 |
|------|----------|------|
| Loss 발산 (>3.0) | LR 과다 or 데이터 버그 | LR=1e-5로 재시도 |
| OOM | 배치 크기 과다 | BATCH_SIZE=2로 감소 |
| Loss 정체 (step 2000+ 변화 없음) | LR 부족 or 데이터 문제 | 데이터 점검, LR=3e-5 시도 |
| Val Loss 발산 (과적합) | Epoch 과다 | Early stop at best val checkpoint |
| 학습 속도 기존과 같음 | Dynamic padding 미작동 | sft_dataset.py 재점검 |
---
## Phase 2: 1B SFT 평가 (~2시간)
### 평가 순서
#### 2-1. 반복률 측정 (30분)
```bash
# 올바른 포맷(<|user|>/<|assistant|>)으로 생성 테스트
python eval/test_generation_params.py \
--checkpoint checkpoints/korean_1b_sft_v2/checkpoint-0010000
# 다양한 rep_penalty 테스트
# rep_penalty=1.0 (없음): 목표 <10%
# rep_penalty=1.1: 목표 <3%
# rep_penalty=1.2: 목표 <1%
```
#### 2-2. 생성 품질 주관 평가 (30분)
```bash
python eval/generate.py \
--checkpoint checkpoints/korean_1b_sft_v2/checkpoint-0010000 \
--prompts_file eval/test_prompts.txt \
--temperature 0.8 --top_p 0.9
```
**체크**: 한국어 자연스러움, instruction following, EOS 정상 종료
#### 2-3. 공식 벤치마크 (1시간)
```bash
# ko_ifeval
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_1b_sft_v2/checkpoint-0010000,dtype=bfloat16 \
--tasks ko_ifeval \
--device cuda:0 \
--output_path eval/results/sft_v2_ko_ifeval.json
# ko_winogrande (선택)
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_1b_sft_v2/checkpoint-0010000,dtype=bfloat16 \
--tasks ko_winogrande \
--device cuda:0 \
--output_path eval/results/sft_v2_ko_winogrande.json
```
### 판단 기준 & 분기
```
[Phase 2 평가 결과]
┌─────────────────────┼─────────────────────┐
│ │ │
✅ PASS ⚠️ PARTIAL ❌ FAIL
반복률<5% 반복률 5-15% 반복률>15%
ko_ifeval>25% ko_ifeval 15-25% ko_ifeval<15%
│ │ │
▼ ▼ ▼
Phase 3B Phase 3A 원인 분석
(3B 전환) (추가 개선) (데이터/코드 재검토)
```
**상세 기준**:
| 지표 | ✅ Pass | ⚠️ 추가 조정 | ❌ 재학습 |
|------|---------|-------------|----------|
| 반복률 (rep_penalty 없이) | <10% | 10-20% | >20% |
| 반복률 (rep_penalty=1.1) | <5% | 5-15% | >15% |
| ko_ifeval | >25% | 15-25% | <15% |
| EOS 정상 종료율 | >85% | 60-85% | <60% |
---
## Phase 3A: 1B 추가 개선 (조건부, ~3-5시간)
> **Phase 2 결과가 ⚠️ PARTIAL일 때만 진입**
### 옵션 A: ORPO 학습 (~3시간)
#### Preference Data 준비 (1시간)
```bash
# 한국어 preference 데이터 다운로드
python -c "
from datasets import load_dataset
# 옵션 1: ko_Ultrafeedback (60K, 일반 도메인)
ds = load_dataset('maywell/ko_Ultrafeedback')
# 옵션 2: 자체 생성 (현재 모델로 rejected 생성)
"
```
**자체 생성 방법**:
1. 현재 SFT 모델로 동일 프롬프트에 여러 생성
2. 반복/저품질 출력 rejected
3. 깨끗한 데이터의 정답 chosen
4. ~10K-20K 생성
#### ORPO 학습 (1.5시간)
```python
from trl import ORPOConfig, ORPOTrainer
config = ORPOConfig(
learning_rate=5e-7,
num_train_epochs=1,
per_device_train_batch_size=4,
gradient_accumulation_steps=2,
beta=0.1, # ORPO coefficient
)
trainer = ORPOTrainer(model, config, train_dataset=preference_data)
trainer.train()
```
#### 평가 (30분)
- 반복률 재측정: 목표 <5% (rep_penalty=1.1)
- ko_ifeval 재측정: 목표 >20%
### 옵션 B: 추가 SFT (데이터 보강, ~5시간)
#### 추가 데이터 수집 (2시간)
```python
from datasets import load_dataset
# 고품질 한국어 데이터 추가
datasets = {
"hPark/orca-ko": 200_000, # 고품질 합성
"nayohan/llama3-instruct-ko-dataset": 58_000, # Llama3 한국어
"FreedomIntelligence/evol-instruct-korean": 70_000, # GPT-4 생성
}
# 기존 120K + 추가 ~300K → 필터 후 ~350K
```
#### 재학습 (2시간)
```bash
# 증가된 데이터로 재학습
RUN_NAME=korean_1b_sft_v3 \
SFT_DATA=data/sft_v3/train.jsonl \
MAX_STEPS=15000 \
bash scripts/launch_sft.sh
```
### Phase 3A 성공 기준
| 지표 | 목표 |
|------|------|
| 반복률 (rep_penalty=1.1) | <5% |
| ko_ifeval | >20% |
**실패 시**: 1B 한계 인정, Phase 3B (3B 전환)로 바로 이동.
---
## Phase 3B: 3B 사전학습 (Phase 2 통과 후, ~26시간)
### 3B 모델 아키텍처
| 파라미터 | 1B (현재) | 3B (목표) | 비고 |
|---------|----------|----------|------|
| d_model | 2048 | 2560 | ~1.25x |
| n_layers | 24 | 32 | ~1.33x |
| n_heads | 16 | 32 | 2x |
| n_kv_heads (GQA) | 4 | 8 | 2x |
| d_ffn | 5472 | 6912 | ~1.26x |
| vocab_size | 64000 | 64000 | 동일 |
| max_seq_len | 4096 | 4096 | 동일 |
| **총 파라미터** | **1.19B** | **~3.0B** | ~2.5x |
### 설정 파일 작성
```bash
# configs/korean_3b_fp8.yaml 작성
cat > configs/korean_3b_fp8.yaml << 'EOF'
model:
d_model: 2560
n_layers: 32
n_heads: 32
n_kv_heads: 8
d_ffn: 6912
vocab_size: 64000
max_seq_len: 4096
rope_theta: 500000
training:
lr: 3.0e-4
min_lr: 3.0e-5
warmup_steps: 2000
max_steps: 100000
batch_size: 4
grad_accum: 4
weight_decay: 0.1
use_fp8: true
data:
sources:
- cc100_ko
- culturax_ko
- existing_pretrain
EOF
```
### 사전학습 데이터
| 소스 | 토큰 수 | 상태 |
|------|---------|------|
| CulturaX ko | 24.8B | ✅ 보유 |
| cc100 ko (재수집) | ~65-100B | ⚠️ 재수집 필요 (노이즈 필터링) |
| 기존 pretrain 데이터 | ~8.9B | ✅ 보유 |
| 추가 수집 (나무위키, 뉴스 등) | ~20-50B | 선택적 |
| **합계** | **~120-180B** | Chinchilla 60B 최소 충족 |
**데이터 준비 명령어**:
```bash
# cc100 재수집 + 품질 필터링
python scripts/download_cc100_ko.py --quality_filter --dedup
# MinHash dedup + perplexity filter
python scripts/quality_filter.py --input data/pretrain/ --max_ppl 1000
```
### 학습 실행
```bash
# 3B pretrain 시작 (8× B200, ~26시간)
bash scripts/run_pretrain.sh --config configs/korean_3b_fp8.yaml
# 예상 처리 속도: ~1.6M tok/s (8× B200)
# 150B tokens / 1.6M tok/s ≈ 26시간
```
### 모니터링
```bash
# 로그 확인
tail -f checkpoints/korean_3b_fp8/train.log
# 중간 체크포인트에서 base 품질 확인 (step 10000마다)
python eval/perplexity.py --checkpoint checkpoints/korean_3b_fp8/checkpoint-0010000
```
**성공 기준**: PPL < 10 (한국어 텍스트), loss 지속 하강
---
## Phase 4: 3B SFT (~2시간)
### 1B에서 배운 교훈 전부 적용
| 교훈 | 적용 |
|------|------|
| Dynamic padding 작동 확인 | sft_dataset.py 수정 완료, 그대로 사용 |
| EOS 보존 | 동일 코드 |
| Val split 필수 | 10% split |
| 3-4 epoch | MAX_STEPS 계산하여 설정 |
| OpenOrca 과다 가중치 방지 | 2.0x 이하 |
| 데이터 품질 필터링 | Phase 0에서 생성한 클린 데이터 사용 |
| 올바른 프롬프트 포맷 | `<\|user\|>/<\|assistant\|>` |
### 실행
```bash
RUN_NAME=korean_3b_sft \
BASE_CHECKPOINT=checkpoints/korean_3b_fp8/checkpoint-BEST \
SFT_DATA=data/sft_v2/train.jsonl \
VAL_DATA=data/sft_v2/val.jsonl \
MAX_STEPS=10000 \
LR=2.0e-5 \
WARMUP_STEPS=300 \
bash scripts/launch_sft.sh
```
**예상 시간**: ~2시간 (3B는 1B 대비 ~2.5x 느림)
### 성공 기준
| 지표 | 목표 |
|------|------|
| Train Loss | < 1.85 |
| Val Loss | Train의 1.1배 이내 |
| 반복률 (rep_penalty 없이) | < 10% |
| 반복률 (rep_penalty=1.1) | < 3% |
---
## Phase 5: 평가 및 배포 (~4시간)
### 5-1. 전체 벤치마크 (~2시간)
```bash
# ko_ifeval
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_3b_sft/checkpoint-BEST,dtype=bfloat16 \
--tasks ko_ifeval --device cuda:0
# ko_winogrande
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_3b_sft/checkpoint-BEST,dtype=bfloat16 \
--tasks ko_winogrande --device cuda:0
# KoBEST (선택)
lm_eval --model hf \
--model_args pretrained=checkpoints/korean_3b_sft/checkpoint-BEST,dtype=bfloat16 \
--tasks kobest_boolq,kobest_copa,kobest_wic,kobest_hellaswag,kobest_sentineg \
--device cuda:0
```
**3B 목표 수치**:
| 벤치마크 | 1B 예상 | 3B 목표 |
|---------|---------|---------|
| ko_ifeval | 20-30% | **35-45%** |
| ko_winogrande | 53-58% | **60-68%** |
| KoBEST (avg) | 55-60% | **65-75%** |
| 반복률 | <5% | **<3%** |
### 5-2. HuggingFace Hub 업로드 (~1시간)
```bash
# HF 포맷 변환
python scripts/convert_to_hf.py \
--checkpoint checkpoints/korean_3b_sft/checkpoint-BEST \
--output_dir hf_models/korean-3b-instruct
# Model card 작성
cat > hf_models/korean-3b-instruct/README.md << 'EOF'
---
language: ko
license: apache-2.0
tags:
- korean
- llm
- instruction-tuning
---
# Korean 3B Instruct
...벤치마크 결과, 사용법 등...
EOF
# 업로드
huggingface-cli upload ghong/korean-3b-instruct hf_models/korean-3b-instruct
```
### 5-3. vLLM 서빙 설정 (~1시간)
```bash
# vLLM 서버 시작
python -m vllm.entrypoints.openai.api_server \
--model hf_models/korean-3b-instruct \
--dtype bfloat16 \
--tensor-parallel-size 1 \
--max-model-len 4096 \
--port 8000
# 테스트
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "korean-3b-instruct",
"messages": [{"role": "user", "content": "한국의 수도는?"}],
"temperature": 0.7
}'
```
**FP8 서빙 (B200 최적)**:
```bash
python -m vllm.entrypoints.openai.api_server \
--model hf_models/korean-3b-instruct \
--quantization fp8 \
--tensor-parallel-size 1 \
--max-model-len 4096
```
**GGUF 변환 (Ollama/로컬 배포)**:
```bash
bash scripts/convert_to_gguf.sh checkpoints/korean_3b_sft/checkpoint-BEST
# Ollama Modelfile 작성 후
ollama create korean-3b -f Modelfile
```
---
## 📋 Phase별 요약 테이블
| Phase | 소요 시간 | 필요한 | 성공 기준 | 실패 |
|-------|----------|----------|----------|---------|
| **0: 준비** | 30분 | prepare_sft_data.py, sft_dataset.py 수정 | 클린 데이터 120K+, sanity 100steps 통과 | 코드 디버그 |
| **1: 1B SFT** | 40분 | 8×B200, 클린 데이터, 수정된 코드 | Loss<1.90, ValLoss 안정 | LR 조정 or 데이터 재점검 |
| **2: 1B 평가** | 2시간 | lm-eval-harness, 평가 스크립트 | 반복률<5%, ko_ifeval>25% | Phase 3A |
| **3A: 추가개선** | 3-5시간 | Preference 데이터, ORPO/추가 SFT | 반복률<5% 달성 | 1B 한계 인정3B |
| **3B: 3B PT** | 26시간 | 150B+ 토큰, configs/korean_3b_fp8.yaml | PPL<10, loss 하강 | 데이터 추가 or 아키텍처 조정 |
| **4: 3B SFT** | 2시간 | Phase 0의 클린 데이터 재사용 | Loss<1.85, 반복률<3% | LR/epoch 조정 |
| **5: 배포** | 4시간 | HF 계정, vLLM | ko_ifeval>35%, 서빙 정상 | 모델 개선 후 재배포 |
---
## 🔥 오늘 당장 시작할 첫 번째 명령어
```bash
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
python data/prepare_sft_data.py --output_dir data/sft_v2/ --val_split 0.1
```
이 명령어 하나로 Phase 0의 가장 중요한 작업(클린 데이터 생성)이 시작된다.
---
## ⚡ 가장 중요한 판단 포인트
### 1차 판단: Phase 1 완료 후 (Step 10000)
- **Val Loss가 Train Loss의 1.2배 이상?** → 과적합. Best checkpoint 사용.
- **Train Loss > 2.0?** → 무언가 잘못됨. 코드/데이터 재점검.
### 2차 판단: Phase 2 평가 후 (가장 중요!)
- **반복률 <5% AND ko_ifeval >25%?** → ✅ 3B 전환 (Phase 3B)
- **반복률 5-15%?** → ⚠️ ORPO 시도 (Phase 3A)
- **반복률 >15%?** → ❌ 원인 분석. 데이터/코드 재검토.
### 3차 판단: Phase 3B 중간 (3B pretrain step 50000)
- **Loss 하강 멈춤?** → 데이터 품질 문제. 필터링 강화.
- **PPL > 15?** → 데이터 부족. 추가 수집 필요.
---
## 🛡️ 리스크 매트릭스
| 리스크 | 확률 | 영향 | 예방/대응 |
|--------|------|------|----------|
| Dynamic padding 여전히 미작동 | 10% | 높음 (속도 3-8x 낭비) | Sanity check에서 배치 길이 확인 |
| 데이터 필터링 과다 (100K 미만) | 15% | 중간 | 필터 기준 완화 (80자→50자) |
| 1B 재학습 후에도 반복 >15% | 15% | 중간 | ORPO or 3B 전환 |
| 3B pretrain 중 OOM | 10% | 높음 | batch_size 줄이기, gradient checkpointing |
| cc100 재수집 시간 초과 | 20% | 낮음 | CulturaX만으로 시작 (24.8B) |
| 디스크 공간 부족 | 5% | 높음 | 현재 19TB 가용, 충분 |
---
*"40분 아끼려고 기술 부채를 안고 가지 마라. 3시간 투자해서 깨끗한 기반을 만들어라."*
*이 문서는 각 Phase 완료 시 결과로 업데이트할 것.*

View File

@@ -0,0 +1,447 @@
# SFT 학습 예외 상황 플레이북
**프로젝트:** Korean 1B SFT 재학습
**서버:** 8× B200 183GB, Driver 580.95.05, CUDA 13.1, PyTorch 2.10
**작성일:** 2026-02-26
**설정:** bs=4 × 8GPU × grad_accum=2 = effective batch 64, max_steps=10000, lr=2e-5, FP8
---
## 시나리오 1: Loss가 0으로 떨어지는 경우
### 감지 기준
- **즉각 경고:** loss < 0.01이 3 step 연속 발생
- **주의:** loss < 0.1이 10 step 이상 지속
- **정상 범위:** 1B SFT에서 수렴 loss 1.5~2.0. 0에 가까우면 100% 비정상
### 즉각 대응
1. 학습 즉시 중단 (Ctrl+C 또는 `kill -SIGINT <PID>`)
2. 가장 최근 정상 체크포인트 확인:
```bash
ls -lt checkpoints/korean_1b_sft/checkpoint-* | head -5
```
### 원인별 진단 및 대응
#### 1-A. Labels Shift 버그 재발
**확인 방법:**
```python
# 데이터에서 샘플 하나 로드 후 labels 검증
from data.sft_dataset import SFTDataset
from tokenizers import Tokenizer
tok = Tokenizer.from_file("tokenizer/korean_sp/tokenizer.json")
ds = SFTDataset("data/sft/train.jsonl", tok, max_seq_len=4096)
ids, labels = ds[0]
# labels에서 -1이 아닌 부분이 input_ids의 다음 토큰과 일치하는지 확인
mask = labels != -1
print(f"유효 labels 수: {mask.sum()}")
print(f"첫 유효 label 위치: {mask.nonzero()[0].item() if mask.any() else 'NONE'}")
# labels[i]는 input_ids[i+1]과 같아야 함 (autoregressive)
# 만약 labels == input_ids 이면 shift 안 됨 → 버그
```
**수정:** `sft_dataset.py`에서 `labels = input_ids[1:]`, `input_ids = input_ids[:-1]` shift 확인
#### 1-B. 데이터 오염
**확인 방법:**
```python
# 랜덤 배치에서 실제 학습 토큰 검사
for batch in train_loader:
ids, labels, mask = batch
valid = (labels != -1)
print(f"유효 토큰 비율: {valid.float().mean():.4f}")
# 유효 토큰이 0이면 모든 labels가 -1 → loss=0
if valid.sum() == 0:
print("🔴 모든 labels가 ignore_index! 데이터 문제")
break
```
**대응:** 데이터 재생성, `prepare_sft_data.py` 재실행
#### 1-C. Learning Rate 문제
**확인:** loss가 갑자기 0이면 lr 문제보다는 labels 버그일 가능성이 훨씬 높음. 그래도 확인:
```bash
grep "lr " checkpoints/korean_1b_sft/train.log | tail -20
# lr이 비정상적으로 높으면 (>1e-3) 수정
```
---
## 시나리오 2: Loss Spike (급등)
### 감지 기준
- **Spike 정의:** 이전 log_interval 평균 대비 **3배 이상** 급등
- **예:** 평균 loss 1.9에서 갑자기 5.7 이상
- **GNorm 기준:** grad_norm > 10.0이면 주의, > 50.0이면 심각
### 원인별 대응
| 원인 | 진단 | 대응 |
|------|------|------|
| Bad batch (이상 데이터) | 해당 step의 배치 내용 확인 | 1~2회 spike 후 자연 복구되면 무시 |
| LR 문제 | warmup 직후 spike → lr 너무 높음 | lr을 1e-5로 낮추고 재시작 |
| GNorm 폭발 | gnorm > 50 | max_grad_norm을 0.5로 강화 |
| FP8 수치 불안정 | FP8 관련 warning 확인 | `--use_fp8` 제거하고 BF16으로 전환 |
### 대응 절차
1. **1회 spike:** 무시 (단발성 bad batch). 다음 log에서 복구 확인
2. **연속 3회 spike:** 학습 중단
3. **복구 방법:**
```bash
# 마지막 정상 체크포인트에서 재시작, lr 낮추기
bash scripts/launch_sft.sh --resume checkpoints/korean_1b_sft/checkpoint-XXXX --lr 1e-5
```
### 현재 코드의 보호 장치
- ✅ `max_grad_norm=1.0` (gradient clipping 활성화)
- ✅ Non-finite loss 감지 → RuntimeError 발생 (trainer.py `_step()`)
- ❌ Loss spike 자동 감지/skip은 미구현 → `monitor_training.sh`로 보완
---
## 시나리오 3: 과적합 (val_loss > train_loss 지속)
### 감지 기준
- **주의:** val_loss - train_loss > 0.15 (상대갭 8% 이상)
- **심각:** val_loss가 3회 연속 eval에서 상승 (train_loss는 하강 중)
- **eval_interval:** 현재 250 steps → 매 250 step마다 val_loss 기록됨
### 현재 코드 상태
- ✅ `val_loader` 지원 (sft.py에서 `--val_data` 인자 있음)
- ✅ `_run_validation()` 구현됨 (trainer.py)
- ✅ Best checkpoint 자동 저장 (`val_loss < self._best_val_loss`)
- ❌ **Early stopping 미구현** — val_loss가 올라도 max_steps까지 학습 계속
### 대응
#### 즉시 가능한 조치
1. **수동 early stop:** 모니터링 스크립트가 경고 → 수동 중단
2. **Best checkpoint 사용:** `checkpoint-best` 디렉토리에 자동 저장됨
```bash
ls checkpoints/korean_1b_sft/checkpoint-best/
```
#### 과적합 해소 방법 (재학습 시)
| 방법 | 설정 변경 |
|------|-----------|
| LR 낮추기 | `--lr 1e-5` |
| Weight decay 높이기 | `--weight_decay 0.05` |
| 데이터 augmentation | NEFTune 이미 활성화 (noise_alpha=10.0) ✅ |
| Steps 줄이기 | `--max_steps 7000` (과적합 시작 전 step에서 멈춤) |
| Dropout | 모델 구조 수정 필요 (현재 코드에서 쉽지 않음) |
#### Early Stopping 추가 방법 (trainer.py 수정)
```python
# trainer.py의 train() 메서드에서 validation 후:
if val_loss > self._best_val_loss:
self._patience_counter += 1
if self._patience_counter >= 5: # 5회 연속 개선 없으면 중단
self._log("Early stopping triggered")
break
else:
self._patience_counter = 0
self._best_val_loss = val_loss
```
---
## 시나리오 4: OOM (Out of Memory)
### 현재 메모리 추정
| 항목 | 추정 |
|------|------|
| 모델 파라미터 (1.19B, BF16) | ~2.4 GB |
| 옵티마이저 상태 (AdamW, fp32) | ~9.5 GB |
| Gradient (BF16) | ~2.4 GB |
| Activation (bs=4, seq=4096, gradient checkpointing ON) | ~8-15 GB |
| **Peak 총합 (per GPU)** | **~25-35 GB** |
| **B200 여유** | **183 - 35 = ~148 GB 여유** |
→ 1B 모델에서 OOM 가능성 **극히 낮음**
### 만약 발생한다면
1. **증상:** `torch.cuda.OutOfMemoryError` → trainer.py에서 이미 catch하여 상세 메시지 출력
2. **즉시 대응:**
```bash
# batch_size 줄이기 (4→2), grad_accum 늘리기 (2→4) → effective batch 동일
bash scripts/launch_sft.sh --batch_size 2 --grad_accum 4 --resume <last_ckpt>
```
3. **Gradient checkpointing:**
- ✅ **이미 활성화됨** (sft.py에서 `model.gradient_checkpointing_enable()`)
4. **추가 조치:**
```bash
# 메모리 fragmentation 방지
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
```
### 메모리 모니터링
```bash
watch -n 5 nvidia-smi # 실시간 확인
# 또는 monitor_training.sh 사용 (아래 참조)
```
---
## 시나리오 5: GPU Hang / NCCL 통신 장애
### 감지 방법
- **증상:** 학습 로그가 멈춤 (새 step이 N분 이상 안 나옴)
- **NCCL timeout:** 기본 30분 후 에러 발생
- `nvidia-smi`에서 특정 GPU utilization 0%
### 진단
```bash
# 1. GPU 상태 확인
nvidia-smi
# 2. NCCL 디버그 활성화하여 재시작
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=ALL
# 3. 프로세스 상태 확인
ps aux | grep torchrun
```
### 복구 방법
```bash
# 1. 기존 프로세스 정리
pkill -f torchrun
sleep 5
# 2. 가장 최근 체크포인트 자동 감지
LATEST_CKPT=$(ls -d checkpoints/korean_1b_sft/checkpoint-* 2>/dev/null \
| grep -v best | sort -t- -k2 -n | tail -1)
echo "Latest checkpoint: ${LATEST_CKPT}"
# 3. 재시작
bash scripts/launch_sft.sh --resume "${LATEST_CKPT}"
```
### 최근 체크포인트 자동 감지 스크립트
```bash
#!/bin/bash
# find_latest_checkpoint.sh
CKPT_DIR="${1:-checkpoints/korean_1b_sft}"
LATEST=$(ls -d "${CKPT_DIR}"/checkpoint-[0-9]* 2>/dev/null \
| sort -t- -k2 -n | tail -1)
if [[ -z "$LATEST" ]]; then
echo "No checkpoint found in ${CKPT_DIR}" >&2
exit 1
fi
echo "$LATEST"
```
### 예방
- `save_interval=500` (현재 설정) → 최대 500 step 손실
- NCCL timeout 조정: `export NCCL_TIMEOUT=1800` (30분 → 필요 시 줄이기)
---
## 시나리오 6: 학습 완료 후 반복률 >15%
### 판단 기준
| 반복률 | 판단 | 대응 |
|--------|------|------|
| <5% (rep_penalty 없이) | ✅ 성공 | 배포 가능 |
| 5-10% | 🟡 OK | rep_penalty=1.1로 배포 |
| 10-20% | 🟠 경계 | 아래 파라미터 조정 시도 |
| >20% | 🔴 실패 | 재학습 필요 |
### 파라미터 조정으로 해결 시도 (재학습 없이)
```python
# 추론 시 적용
generate_kwargs = {
"repetition_penalty": 1.1, # 1.05~1.2 범위 탐색
"no_repeat_ngram_size": 3, # 3-gram 반복 차단
"temperature": 0.7, # 약간 낮추면 반복 감소
"top_p": 0.9,
}
```
### 재학습이 필요한 경우
- rep_penalty=1.2 + no_repeat_3gram에서도 >10%
- 원인 분석:
1. **데이터 내 반복 패턴:** `data_quality_audit.py`로 재확인
2. **Epoch 과다:** 5+ epoch은 반복 패턴 암기 유발 → 3-4 epoch이 적정
3. **EOS 학습 부족:** truncation 시 EOS 손실 여부 확인
### 고급 대응 (추가 학습 방법)
| 방법 | 설명 | 소요 |
|------|------|------|
| ORPO | Preference optimization, 반복 패턴 직접 penalize | +3-6시간 |
| DPO | Chosen(비반복) vs Rejected(반복) 쌍 필요 | +4-8시간 |
| rep_penalty fine-tuning | 추론 시 penalty 결과를 reward로 RL | 복잡 |
---
## 시나리오 7: ko_ifeval 기대치 미달 (<15%)
### 원인 분석 방법
#### Step 1: 모델 출력 직접 확인
```bash
# ko_ifeval 실패 샘플 분석
python -c "
# lm_eval 결과에서 실패 케이스 추출
# 지시문 이해 부족 vs 포맷 오류 vs 한국어 능력 부족 구분
"
```
#### Step 2: 카테고리별 분석
| 실패 유형 | 의미 | 대응 |
|-----------|------|------|
| 지시 무시 (wrong format) | instruction following 약함 | SFT 데이터에 format-constrained 샘플 추가 |
| 한국어 이해 실패 | 한국어 능력 부족 | 한국어 비율 높이기 (현재 ~70%) |
| 추론 오류 | 1B 모델 한계 | 모델 크기 한계 → 3B 전환 |
#### Step 3: 모델 한계 vs 데이터 문제 구분
```
1B 모델 ko_ifeval 현실적 범위: 15-30%
- <15%: 데이터/학습 문제 가능성 높음
- 15-25%: 정상 범위, 데이터로 개선 여지 있음
- 25-30%: 1B 한계에 근접, 3B 전환 필요
- >30%: 1B에서 달성하기 어려움
```
### 데이터 추가 수집 방향
1. **Korean instruction-following 데이터:** KoAlpaca, KULLM 등에서 format-constrained 샘플
2. **Multi-turn 한국어 대화:** 지시 따르기 능력 강화
3. **ko_ifeval과 유사한 포맷 데이터:** "~형식으로 답하시오" 유형
---
## 시나리오 8: 디스크 공간 부족
### 현재 상태
```
/PROJECT: 3.5TB 총, 1.4TB 사용, 2.2TB 가용 (39% 사용)
```
### 체크포인트 크기 추정
| 항목 | 크기 |
|------|------|
| model.pt (1.19B BF16) | ~2.4 GB |
| optimizer.pt (AdamW states) | ~9.5 GB |
| scheduler + meta | ~1 MB |
| **체크포인트 1개** | **~12 GB** |
| 10,000 steps / 500 save = 20개 | **~240 GB** |
| + best checkpoint | +12 GB |
| + tensorboard logs | ~100 MB |
| **총 예상** | **~252 GB** |
→ 2.2TB 가용 대비 충분하지만, 여러 실험 시 누적 주의
### 체크포인트 관리 전략
#### 저장 주기 최적화
- **현재:** 500 step마다 (추천 유지)
- 디스크 부족 시: 1000 step으로 변경 → 120 GB로 절반 감소
- `train_config.save_interval = 1000`
#### 오래된 체크포인트 자동 삭제
```bash
#!/bin/bash
# cleanup_checkpoints.sh — 최신 N개만 유지, best는 항상 보존
CKPT_DIR="${1:-checkpoints/korean_1b_sft}"
KEEP="${2:-5}" # 최신 5개 유지
CKPTS=$(ls -d "${CKPT_DIR}"/checkpoint-[0-9]* 2>/dev/null | sort -t- -k2 -n)
TOTAL=$(echo "$CKPTS" | wc -l)
DELETE=$((TOTAL - KEEP))
if [[ $DELETE -gt 0 ]]; then
echo "$CKPTS" | head -n "$DELETE" | while read ckpt; do
echo "Removing: $ckpt"
rm -rf "$ckpt"
done
echo "Kept latest $KEEP checkpoints + checkpoint-best"
else
echo "Only $TOTAL checkpoints, nothing to delete (keep=$KEEP)"
fi
```
### 디스크 모니터링
```bash
# 학습 중 주기적 확인
df -h /PROJECT | awk 'NR==2 {if ($5+0 > 80) print "🔴 DISK >80%: "$5}'
```
---
## 학습 재시작 가이드
### 현재 코드의 Resume 지원
✅ **완전 지원됨:**
- `sft.py`에 `--resume` 인자 있음
- `load_checkpoint()`으로 model, optimizer, scheduler 상태 모두 복원
- `start_step` 반환 → 이어서 학습
### 재시작 명령어
```bash
# 방법 1: 최신 체크포인트에서 자동 재시작
LATEST=$(ls -d checkpoints/korean_1b_sft/checkpoint-[0-9]* 2>/dev/null \
| sort -t- -k2 -n | tail -1)
bash scripts/launch_sft.sh --resume "${LATEST}"
# 방법 2: 특정 체크포인트 지정
bash scripts/launch_sft.sh --resume checkpoints/korean_1b_sft/checkpoint-0003000
# 방법 3: LR 변경하며 재시작 (과적합/spike 대응)
bash scripts/launch_sft.sh --resume "${LATEST}" --lr 1e-5
```
### 주의사항
- **cosine schedule:** resume 시 scheduler가 중간 step에서 복원됨 → LR이 올바른 위치에서 재개
- **max_steps 변경 시:** 원래 5000 step 기준 schedule인데 10000으로 변경하면 LR curve가 달라짐 → 처음부터 재학습 권장
- **DDP seed:** resume 시 동일 seed 사용해야 데이터 순서 재현 (현재 코드에서 자동 처리)
---
## 모니터링 자동화
별도 스크립트: `scripts/monitor_training.sh` 참조
### 감시 항목 요약
| 항목 | 임계값 | 의미 |
|------|--------|------|
| loss = 0.0000 (3 step 연속) | 🔴 Critical | Labels 버그 |
| loss spike (3× 평균) | 🟠 Warning | Bad batch / LR |
| gnorm > 10.0 | 🟠 Warning | 불안정 |
| gnorm > 50.0 | 🔴 Critical | 발산 직전 |
| GPU util < 50% | 🟡 Info | 병목 (data loading?) |
| 로그 5분 이상 멈춤 | 🔴 Critical | Hang / NCCL 장애 |
| 디스크 사용 > 80% | 🟠 Warning | 체크포인트 정리 필요 |
---
## 위험도 순위 (높음 → 낮음)
| 순위 | 시나리오 | 위험도 | 예방 |
|------|----------|--------|------|
| 1 | **Loss → 0 (Labels 버그)** | 🔴🔴🔴 | 학습 전 labels shift 검증 스크립트 실행 |
| 2 | **GPU Hang (NCCL)** | 🔴🔴 | save_interval=500, NCCL 환경변수 설정 |
| 3 | **과적합** | 🔴 | val_data 필수, 모니터링 |
| 4 | **반복률 >15%** | 🟠🟠 | 깨끗한 데이터, 적정 epoch |
| 5 | **Loss Spike** | 🟠 | grad_clip=1.0, 이미 설정됨 |
| 6 | **ko_ifeval 미달** | 🟠 | 1B 한계 인지, 데이터 다양성 |
| 7 | **디스크 부족** | 🟡 | 2.2TB 여유, 자동 정리 |
| 8 | **OOM** | 🟢 | 183GB에 1B 모델, 거의 불가능 |
---
## 학습 전 체크리스트
```
□ 데이터 필터링 완료 (data_quality_audit.py)
□ Val split 생성 (90/10)
□ Labels shift 검증 (위 코드 스니펫 실행)
□ sft_dataset.py 수정 확인 (dynamic padding, EOS 보존)
□ launch_sft.sh 설정 확인 (max_steps, val_data, lr)
□ 디스크 공간 확인 (df -h /PROJECT)
□ GPU 상태 확인 (nvidia-smi)
□ monitor_training.sh 백그라운드 실행
□ tensorboard 실행: tensorboard --logdir checkpoints/korean_1b_sft/tensorboard
```

View File

@@ -0,0 +1,533 @@
# SFT 개선 방안 심층 조사
> 프로젝트: 1B Korean LLM SFT (188k 샘플, 8×B200, FP8)
> 현재 구현: NEFTune, dynamic padding, gradient checkpointing, cosine LR, BF16+FP8
> 작성일: 2026-02-26
---
## 1. Curriculum Learning (교육과정 학습)
### 개념
쉬운 샘플에서 어려운 샘플 순서로 학습하여 수렴 속도와 최종 성능 향상.
### 구현 방법
**방법 A: Perplexity 기반 정렬 (권장)**
```python
# scripts/compute_difficulty.py
import torch, json
from pathlib import Path
from tokenizers import Tokenizer
from model import LLM
def compute_sample_perplexity(model, tokenizer, data_path, output_path, device="cuda:0"):
"""현재 pretrain 모델로 각 샘플의 output perplexity 계산"""
model.eval()
results = []
with open(data_path) as f:
samples = [json.loads(line) for line in f]
with torch.no_grad():
for i, sample in enumerate(samples):
# conversation에서 assistant turn만 추출
messages = sample["messages"]
# 전체 시퀀스 토크나이즈
full_text = tokenizer.encode(
"".join(m["content"] for m in messages)
)
input_ids = torch.tensor([full_text.ids[:4096]], device=device)
logits = model(input_ids)
# response 토큰에 대한 CE loss = perplexity proxy
shift_logits = logits[:, :-1, :]
shift_labels = input_ids[:, 1:]
loss = torch.nn.functional.cross_entropy(
shift_logits.reshape(-1, shift_logits.size(-1)),
shift_labels.reshape(-1),
reduction='mean'
)
ppl = loss.exp().item()
results.append({"idx": i, "ppl": ppl})
if i % 1000 == 0:
print(f" {i}/{len(samples)} done")
# ppl 오름차순 정렬 = 쉬운 것부터
results.sort(key=lambda x: x["ppl"])
with open(output_path, "w") as f:
json.dump(results, f)
return results
```
**방법 B: 길이 기반 (가장 간단)**
- 짧은 응답 → 긴 응답 순서로 정렬
- SFTDataset에서 `__getitem__` 시 정렬된 인덱스 사용
**방법 C: IFD Score**
- Cherry LLM 논문 (2024): `IFD = PPL(output|instruction) / PPL(output)`
- 높은 IFD = instruction이 output 생성을 잘 유도하지 못함 = 어려움
### 실제 효과
- **Curriculum Learning for LLMs (Xu et al., 2024)**: SFT에서 MT-Bench +0.3~0.5점
- **효과 제한적 의견**: Bengio et al.의 원 연구 이후 SFT에서의 효과는 mixed results
- **예상**: ko_ifeval +1~2%, 반복률 변화 미미
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | ko_ifeval +1~2% |
| 구현 복잡도 | 2/5 |
| 소요 시간 | PPL 계산 2~3시간 + 코드 수정 2시간 |
| 적용 가능 | ✅ DataLoader sampler 수정으로 가능 |
---
## 2. "Less is More" 전략 (LIMA, AlpaGasus)
### 핵심 논문
- **LIMA (Zhou et al., 2023)**: 1000개 고품질 > 52k 저품질. 65B 모델에서 검증.
- **AlpaGasus (Chen et al., 2023)**: GPT-4로 품질 점수 → 9k에서 3k 선별 → Alpaca 대비 우수
- **DEITA (Liu et al., 2024)**: complexity + quality + diversity 3축 필터링
### 품질 점수 계산 방법 (외부 API 없이)
```python
# scripts/quality_filter.py
import json, math, torch
import numpy as np
from collections import Counter
def compute_quality_scores(data_path, model, tokenizer, device="cuda:0"):
"""다차원 품질 점수 계산"""
with open(data_path) as f:
samples = [json.loads(line) for line in f]
scored = []
model.eval()
for i, sample in enumerate(samples):
msgs = sample["messages"]
# 1) 길이 점수: 너무 짧거나 너무 긴 건 감점
response = "".join(m["content"] for m in msgs if m["role"] == "assistant")
resp_len = len(response)
len_score = min(resp_len / 500, 1.0) * (1.0 if resp_len < 3000 else 3000 / resp_len)
# 2) 반복 감점: n-gram 반복률
tokens = list(response)
if len(tokens) > 10:
trigrams = [tuple(tokens[j:j+3]) for j in range(len(tokens)-2)]
unique_ratio = len(set(trigrams)) / len(trigrams)
else:
unique_ratio = 1.0
rep_score = unique_ratio
# 3) Perplexity 점수 (중간이 좋음 - 너무 낮으면 trivial, 너무 높으면 noise)
# 사전 계산된 ppl 사용
# 4) Instruction 복잡도: instruction 길이
instruction = "".join(m["content"] for m in msgs if m["role"] == "user")
inst_complexity = min(len(instruction) / 200, 1.0)
# 종합 점수
quality = 0.3 * len_score + 0.3 * rep_score + 0.2 * inst_complexity + 0.2
scored.append({"idx": i, "quality": quality, "sample": sample})
return scored
def select_top_k(scored, k):
"""상위 k개 선별"""
scored.sort(key=lambda x: x["quality"], reverse=True)
return scored[:k]
```
### 권장 샘플 수
- 188k 전체 → **50k~80k** 권장 (상위 30~40%)
- 1B 모델 규모에서는 LIMA처럼 극단적 축소보다 moderate 필터링이 적합
- 근거: AlpaGasus는 ~30% 선별에서 최적, 1B는 65B보다 데이터 의존도 높음
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 반복률 -30~50%, ko_ifeval +3~5% |
| 구현 복잡도 | 2/5 |
| 소요 시간 | 품질 계산 3~4시간, 필터링 코드 1시간 |
| 적용 가능 | ✅ 데이터 전처리 단계 |
---
## 3. Packing (Sequence Packing)
### 개념
짧은 시퀀스들을 하나의 max_seq_len에 패킹하여 padding 낭비 제거.
### 현 프로젝트 상황
- 이미 `dynamic_collate_fn`으로 batch-level dynamic padding 구현됨
- Packing은 그 이상: **여러 샘플을 하나의 시퀀스로 concatenate**
### 주의사항: Cross-contamination
- 패킹된 서로 다른 샘플 간 attention이 흐르면 안 됨
- **해결**: Flash Attention v2의 `cu_seqlens` 파라미터 (varlen attention)
- 또는 block diagonal attention mask
### 구현 방법
```python
# data/packed_sft_dataset.py
class PackedSFTDataset:
"""여러 SFT 샘플을 하나의 시퀀스로 패킹"""
def __init__(self, samples, tokenizer, max_seq_len=4096):
self.packed = []
self.cu_seqlens = [] # Flash Attention varlen용
buffer_ids = []
buffer_labels = []
seq_lens = []
for sample in samples:
ids, labels = self._tokenize(sample, tokenizer)
if len(buffer_ids) + len(ids) > max_seq_len:
# 현재 버퍼 저장
if buffer_ids:
self._save_buffer(buffer_ids, buffer_labels, seq_lens, max_seq_len)
buffer_ids = ids
buffer_labels = labels
seq_lens = [len(ids)]
else:
buffer_ids.extend(ids)
buffer_labels.extend(labels)
seq_lens.append(len(ids))
if buffer_ids:
self._save_buffer(buffer_ids, buffer_labels, seq_lens, max_seq_len)
def _save_buffer(self, ids, labels, seq_lens, max_seq_len):
# Pad to max_seq_len
pad_len = max_seq_len - len(ids)
ids = ids + [0] * pad_len
labels = labels + [-1] * pad_len
# cu_seqlens for varlen flash attention
cu = [0]
for l in seq_lens:
cu.append(cu[-1] + l)
self.packed.append({
"input_ids": torch.tensor(ids),
"labels": torch.tensor(labels),
"cu_seqlens": torch.tensor(cu, dtype=torch.int32),
})
```
### 속도 개선 예상
- 현재 평균 시퀀스 길이가 max_seq_len(4096)보다 훨씬 짧다면 **1.5~3× 속도 향상**
- SFT 데이터 특성상 평균 ~1000 토큰이면 ~3× 효율 향상 예상
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 학습 속도 1.5~3×, 성능 변화 없거나 미미 |
| 구현 복잡도 | 3/5 (Flash Attention varlen 연동 필요) |
| 소요 시간 | 1~2일 |
| 적용 가능 | ⚠️ 모델의 attention 구현이 cu_seqlens 지원해야 함 |
---
## 4. Multi-task SFT (도메인별 Loss Weighting)
### 개념
데이터 소스별로 도메인을 분류하고, 도메인별 loss weight를 다르게 적용.
### 현재 데이터 소스 분석 (추정)
- `korean_safe_conv/raw/` 하위: hatespeech, square, evol, yitingxie, gamseong, koalpaca, conversation
- 카테고리: 안전성, QA, 창작, 일반 대화
### 구현 방법
```python
# 도메인 태그를 JSONL에 추가
# {"messages": [...], "domain": "qa"}
# {"messages": [...], "domain": "creative"}
# trainer에서 도메인별 loss weight
DOMAIN_WEIGHTS = {
"qa": 1.0,
"creative": 0.8,
"safety": 1.2,
"code": 1.0,
"math": 1.0,
"conversation": 0.6, # 일반 대화는 낮게
}
```
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 특정 벤치마크 +1~3% |
| 구현 복잡도 | 3/5 |
| 소요 시간 | 도메인 분류 0.5일 + 구현 0.5일 |
| 적용 가능 | ✅ loss 계산 시 weight 곱셈 |
---
## 5. Token-level Loss Weighting / Focal Loss
### 개념
모든 response 토큰에 동일 weight 대신, 모델이 예측하기 어려운 토큰에 더 높은 weight.
### Focal Loss 구현
```python
# train/focal_loss.py
import torch
import torch.nn.functional as F
def focal_cross_entropy(logits, targets, gamma=2.0, ignore_index=-1):
"""
Focal loss: down-weight easy tokens, up-weight hard tokens.
Lin et al., "Focal Loss for Dense Object Detection", ICCV 2017.
"""
# Standard CE
ce_loss = F.cross_entropy(
logits.reshape(-1, logits.size(-1)),
targets.reshape(-1),
ignore_index=ignore_index,
reduction='none'
)
# p_t = probability of correct class
log_pt = -ce_loss
pt = torch.exp(log_pt)
# Focal weight: (1 - p_t)^gamma
focal_weight = (1 - pt) ** gamma
loss = focal_weight * ce_loss
# Mask ignored tokens
mask = (targets.reshape(-1) != ignore_index)
loss = loss[mask].mean()
return loss
```
### 적용: trainer.py 수정
```python
# trainer.py의 _compute_loss에서
# 기존: F.cross_entropy(logits, targets, ignore_index=-1)
# 변경: focal_cross_entropy(logits, targets, gamma=2.0, ignore_index=-1)
```
### 실제 효과
- SFT에서 focal loss 적용 논문은 제한적
- **SelectIT (Liu et al., 2024)**: token-level selection으로 IFEval +2~4%
- gamma=2.0이 일반적이나 SFT에서는 gamma=1.0~1.5 권장 (너무 강하면 불안정)
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | ko_ifeval +1~3%, 반복률 영향 미미 |
| 구현 복잡도 | 1/5 |
| 소요 시간 | 2시간 |
| 적용 가능 | ✅ loss 함수만 교체 |
---
## 6. Data Augmentation for Korean
### 방법 A: Self-Paraphrase
```bash
# 현재 모델(또는 더 큰 모델)로 response 재생성
# instruction은 유지, output만 다양화
# → 동일 instruction에 대한 N개 다른 응답 확보
```
### 방법 B: Back-translation
```python
# 영어 고품질 데이터 (Alpaca, Dolly, OpenAssistant) → 한국어 번역
# 서버에서 실행 가능한 방법:
# 1) NLLB-200 3.3B (Meta): 오프라인 번역, 1GPU로 실행 가능
# 2) 한국어 특화 모델로 직접 번역 생성
# pip install transformers
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
model_name = "facebook/nllb-200-3.3B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to("cuda:7") # 여유 GPU 1개 사용
def translate_en_to_ko(text):
inputs = tokenizer(text, return_tensors="pt", max_length=1024, truncation=True).to("cuda:7")
outputs = model.generate(**inputs, forced_bos_token_id=tokenizer.lang_code_to_id["kor_Hang"], max_new_tokens=1024)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
```
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 다양성 증가, ko_ifeval +2~4% (좋은 소스 데이터 시) |
| 구현 복잡도 | 3/5 |
| 소요 시간 | 번역 파이프라인 1일 + 번역 실행 1~2일 |
| 적용 가능 | ✅ 별도 GPU에서 병렬 실행 가능 |
---
## 7. 학습 안정성 개선
### FP8 학습 주의사항
현재 설정: MXFP8 + BF16 기반. 주요 주의점:
1. **Loss spike 방지**
- `max_grad_norm: 1.0` 이미 적용됨 ✅
- LR 2e-5는 보수적 ✅
- 추가: gradient norm 모니터링 + 자동 LR 감소
```python
# 연속 3번 grad_norm > threshold면 lr 반감
if grad_norm > 5.0:
spike_count += 1
if spike_count >= 3:
for pg in optimizer.param_groups:
pg['lr'] *= 0.5
```
2. **Weight Decay**
- 현재 0.01 (적절)
- SFT에서는 0.01~0.05 범위가 표준
3. **Dropout**
- 현재 `dropout: 0.0` — SFT에서는 **0.05~0.1 추가 권장**
- 과적합 방지에 직접적 효과
```yaml
dropout: 0.05 # configs/korean_1b_sft.yaml
```
4. **FP8 amax 설정**
- `fp8_amax_history_len: 16` + `fp8_amax_compute_algo: "max"` — 적절
- MXFP8은 DelayedScaling보다 안정적
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 안정성 ↑, 과적합 -10~20%, 반복률 -5~10% |
| 구현 복잡도 | 1/5 |
| 소요 시간 | 1시간 |
| 적용 가능 | ✅ config 변경만으로 가능 |
---
## 8. 평가 기반 데이터 선택 (Self-Play → ORPO)
### 파이프라인
```
1. 현재 SFT 모델로 각 instruction에 대해 N=4개 응답 생성
2. 자동 평가 (반복률, 길이, 일관성)로 best/worst 선정
3. (chosen, rejected) 페어 구성
4. ORPO/DPO 학습 (이미 train/orpo.py 존재!)
```
### 구체적 단계
```python
# scripts/generate_self_play.py
def generate_candidates(model, tokenizer, instructions, n=4, temp=0.8):
pairs = []
for inst in instructions:
responses = []
for _ in range(n):
out = model.generate(inst, temperature=temp, max_new_tokens=1024)
score = auto_evaluate(out) # 반복률, 길이, coherence
responses.append((out, score))
responses.sort(key=lambda x: x[1], reverse=True)
pairs.append({
"instruction": inst,
"chosen": responses[0][0],
"rejected": responses[-1][0],
})
return pairs
```
### 평가
| 항목 | 값 |
|------|-----|
| 예상 효과 | 반복률 -40~60%, ko_ifeval +3~5% |
| 구현 복잡도 | 4/5 |
| 소요 시간 | 생성 1~2일 + ORPO 학습 0.5일 |
| 적용 가능 | ✅ orpo.py 이미 존재 |
---
## 종합 비교표
| 기법 | 예상 효과 (반복률) | 예상 효과 (ko_ifeval) | 구현 복잡도 | 소요 시간 | 우선순위 |
|------|-------------------|---------------------|------------|----------|---------|
| 1. Curriculum Learning | - | +1~2% | 2/5 | 5시간 | 중기 |
| 2. Less is More | **-30~50%** | **+3~5%** | 2/5 | 5시간 | **즉시** |
| 3. Packing | (속도만) | (변화없음) | 3/5 | 1~2일 | 중기 |
| 4. Multi-task Weighting | - | +1~3% | 3/5 | 1일 | 중기 |
| 5. Focal Loss | - | +1~3% | **1/5** | **2시간** | **즉시** |
| 6. Data Augmentation | - | +2~4% | 3/5 | 2~3일 | 중기 |
| 7. 학습 안정성 (dropout) | **-5~10%** | - | **1/5** | **1시간** | **즉시** |
| 8. Self-Play → ORPO | **-40~60%** | **+3~5%** | 4/5 | 2~3일 | 중기 |
---
## 🚀 즉시 적용 Top 3
### 1위: "Less is More" 데이터 필터링
- **근거**: LIMA, AlpaGasus 논문에서 일관되게 입증. 188k → 50~80k 필터링으로 저품질/반복적 샘플 제거
- **예상 효과**: 반복률 -30~50%, ko_ifeval +3~5%
- **소요**: 5시간 (PPL 계산 + 필터링 스크립트)
- **리스크**: 낮음 (최악의 경우 전체 데이터로 롤백)
### 2위: Focal Loss 적용
- **근거**: 어려운 토큰에 집중 → instruction following 능력 향상. 구현 극히 간단
- **예상 효과**: ko_ifeval +1~3%
- **소요**: 2시간 (loss 함수 1개 추가)
- **리스크**: 매우 낮음 (gamma 값만 조정하면 됨)
### 3위: Dropout 추가 (0.05)
- **근거**: 현재 dropout=0.0으로 과적합 위험. SFT에서 light dropout은 표준
- **예상 효과**: 과적합 감소, 반복률 -5~10%
- **소요**: config 한 줄 변경
- **리스크**: 없음
---
## 📅 중기 적용 Top 3
### 1위: Self-Play → ORPO (SFT 이후)
- **근거**: SFT 완료 후 선호 학습은 반복률 감소에 가장 효과적. orpo.py 이미 구현됨
- **예상 효과**: 반복률 -40~60%, ko_ifeval +3~5%
- **소요**: 2~3일 (생성 + 학습)
### 2위: Sequence Packing
- **근거**: 학습 속도 1.5~3× 향상. 향후 반복 실험에 필수적
- **예상 효과**: 학습 시간 대폭 단축
- **소요**: 1~2일
### 3위: Curriculum Learning + Data Augmentation (Back-translation)
- **근거**: 데이터 다양성과 학습 효율 동시 개선
- **예상 효과**: ko_ifeval +2~4%
- **소요**: 3~4일
---
## 권장 실행 순서
```
Phase 1 (즉시, SFT 전):
1. dropout: 0.05 설정
2. 데이터 품질 필터링 (188k → 60~80k)
3. Focal loss 적용 (gamma=1.5)
→ SFT 실행
Phase 2 (SFT 후):
4. Self-Play 데이터 생성
5. ORPO 학습
Phase 3 (다음 라운드):
6. Packing 구현 (반복 실험 가속)
7. Back-translation으로 데이터 확장
8. Curriculum learning 실험
```

View File

@@ -0,0 +1,258 @@
# SFT 파이프라인 완전 재설계 보고서
> 작성일: 2026-02-26
> 목표: 반복률 <5%, ko_ifeval 20-30%
> 모델: 1B 한국어 LLM, 188K 학습 샘플
---
## 1. 현재 코드 검증 결과
### ✅ 올바르게 수정된 것들
| 항목 | 상태 | 근거 |
|------|------|------|
| Dynamic padding | ✅ 정상 | `SFTDataset.__getitem__`이 raw-length 텐서 반환, `dynamic_collate_fn`이 batch-local max + 64-align 패딩 |
| EOS 강제 부착 | ✅ 정상 | truncation 시 `response_ids[-1] = self.eos_token_id` 강제 |
| Enhanced quality filter | ✅ 적용됨 | `_enhanced_quality_filter``quality_filter()` 안에서 호출됨 (EOS 리터럴, Q/A 오염, 50자 미만) |
| Val data 연결 | ✅ 정상 | `launch_sft.sh``VAL_DATA="data/sft/val.jsonl"`, `--val_data` 전달됨 |
| Val loss 기록 | ✅ 구현됨 | `trainer.py``_run_validation()` + `eval_interval=250` step마다 실행 |
| Best checkpoint | ✅ 구현됨 | `val_loss < self._best_val_loss` 시 suffix="best" 저장 |
| NEFTune | ✅ 적용됨 | `noise_alpha=10.0`으로 embedding hook 등록 |
| Gradient checkpointing | ✅ 적용됨 | `model.gradient_checkpointing_enable()` 호출 |
### ⚠️ 남은 문제 (수정 필요)
#### 🔴 P0: Labels off-by-one 버그
```python
# sft_dataset.py line ~168
resp_label_start = max(0, resp_start - 1) # ← 이게 문제
resp_label_end = resp_label_start + len(response_ids)
labels[resp_label_start:resp_label_end] = torch.tensor(response_ids, dtype=torch.long)
```
**문제**: `resp_start - 1`은 프롬프트의 마지막 토큰 위치에 response_ids[0]을 넣음.
이는 next-token prediction에서 "프롬프트 마지막 토큰이 들어오면 response 첫 토큰을 예측하라"는 의미로 **의도적 설계**일 수 있음.
**그러나**: 이 경우 labels의 길이가 `len(response_ids)`인데 `resp_label_start`부터 시작하므로 response의 마지막 토큰(EOS)에 대한 label이 한 칸 앞으로 밀려, **마지막 response 토큰의 label이 누락될 수 있음**.
실제로:
- `resp_start = len(prompt_ids)` (예: 50)
- `resp_label_start = 49`
- `resp_label_end = 49 + len(response_ids)`
- `labels[49:49+R] = response_ids[0:R]`
이렇게 하면 position 49의 label = response_ids[0], position 50의 label = response_ids[1], ... 이건 autoregressive LM 관점에서:
- position 49 입력 = prompt 마지막 토큰 → 예측해야 할 것 = response 첫 토큰 ✅
- position (49+R-1) 입력 = response[R-2] → 예측해야 할 것 = response[R-1] (=EOS) ✅
- position (49+R) 입력 = response[R-1] (=EOS) → label = -1 (무시) ✅
**결론: 이 로직은 실제로 올바름.** next-token prediction의 teacher-forcing에서 shift가 모델 내부가 아닌 labels에서 이루어지는 패턴. 단, **모델의 forward가 logits[t]로 position t+1을 예측하는지 확인 필요**. 대부분 decoder LM은 `loss = CE(logits[:, :-1], labels[:, 1:])` 형태를 씀.
**확인 사항**: `trainer.py``_compute_loss`를 보면:
```python
nn.functional.cross_entropy(logits.view(B*T, V), targets.view(B*T), ignore_index=-1)
```
여기서 **shift가 없음**. 즉 `logits[t]``targets[t]`를 직접 비교함.
일반적으로 decoder LM의 logits[t]는 position t의 hidden state로 t+1번째 토큰을 예측. 따라서:
- `logits[t]` = position t 다음에 올 토큰 예측
- `targets[t]`가 t+1 토큰이어야 함
**현재 코드의 labels 배치:**
- `labels[resp_start - 1] = response_ids[0]` → logits[resp_start-1]로 response[0] 예측 ✅ (prompt 마지막 토큰 → 첫 응답)
- `labels[resp_start - 1 + k] = response_ids[k]` → logits[prompt_last + k]로 response[k] 예측
이것이 맞으려면 `input_ids[resp_start - 1 + k]`가 response_ids[k]의 **이전** 토큰이어야 함.
- k=0: `input_ids[resp_start - 1]` = prompt 마지막 토큰, label = response[0] ✅
- k=1: `input_ids[resp_start]` = response[0], label = response[1] ✅
- k=R-1: `input_ids[resp_start + R - 2]` = response[R-2], label = response[R-1] = EOS ✅
**결론: off-by-one이 아님. 이 코드는 shift를 labels 측에서 수행하는 정상적인 패턴.**
#### 🟡 P1: Early stopping 미구현
현재 best checkpoint은 저장하지만 **학습을 중단하는 로직은 없음**. val_loss가 N번 연속 개선 안 되면 중단하는 patience 로직 추가 권장.
#### 🟡 P2: `_quality_filter`의 한국어 비율 30%는 너무 느슨
output의 한국어 문자 비율 30%는 영어/코드가 대부분인 샘플도 통과시킴. 한국어 SFT 목적에는 **50% 이상** 권장.
#### 🟢 P3: weight_decay=0.01이 launch_sft.sh에 명시 안 됨
`sft.py`의 default=0.01을 그대로 사용 중. 명시적으로 적는 게 좋음.
---
## 2. 데이터 파이프라인 설계
### 2.1 포맷 확인
현재 `sft_dataset.py`의 chat template:
```
<|user|>
{instruction}
<|assistant|>
{output}</s>
```
**→ 목표 포맷과 일치함.** 추가 수정 불필요.
### 2.2 샘플 품질 기준 최종안
| 기준 | 현재값 | 권장값 | 근거 |
|------|--------|--------|------|
| 최소 output 길이 | 50자 | **100자** | 50자는 1-2문장 수준으로 너무 짧음. IF-eval 같은 벤치마크는 구조화된 답변 필요 |
| 최대 output 길이 | 4000자 | **3000자** | 극단적으로 긴 응답은 반복 퇴화 리스크 ↑ |
| 한국어 비율 | 30% | **50%** | 한국어 LLM 목적에 30%는 너무 느슨 |
| 3-gram 반복 unique ratio | <0.5 제거 | **<0.6 제거** | 엄격한 반복 필터링으로 반복률 <5% 달성 |
| 최소 instruction 길이 | 10자 | **15자** | 극히 짧은 지시문은 모호한 학습 신호 |
### 2.3 데이터 믹싱 전략
현재 가중치 & 권장 조정:
| 소스 | 현재 가중치 | 권장 가중치 | 비고 |
|------|------------|------------|------|
| KOR-OpenOrca-Platypus-v3 | 2.0 | **1.5** | 여전히 과대표집 가능. 다양성 확보 위해 축소 |
| kullm-v2 | 1.0 | **1.0** | 유지 |
| ko-alpaca-12k | 2.0 | **1.5** | 소규모 고품질이지만 2배는 과도. 1.5배 적정 |
| korean_safe_conversation | 1.5 | **1.5** | 안전 정렬 중요, 유지 |
| evol-instruct-korean | 1.5 | **2.0** | 복잡한 추론/코드 능력은 IF-eval에 직결. 상향 |
| kovast | 0.8 | **0.5** | 멀티턴 턴만 추출하므로 품질 불안정, 축소 |
**도메인 균형 목표 (대략적):**
- 일반 지식/QA: ~35%
- 안전/정렬: ~15%
- 추론/코드/수학: ~25% (IF-eval 핵심)
- 대화/창작: ~15%
- 기타: ~10%
---
## 3. 하이퍼파라미터 최종 확정
### 3.1 설정 배경
- 모델: 1B params
- 데이터: ~188K samples
- GPU: 8× B200 (FP8)
- Effective batch: 4 × 2 (grad_accum) × 8 (GPU) = **64 sequences/step**
- Steps per epoch: 188K / 64 **2,940 steps**
- 5000 steps **1.7 epochs**
### 3.2 최종 권장값
| 파라미터 | 현재값 | **최종 권장값** | 학술 근거 |
|----------|--------|----------------|-----------|
| **learning_rate** | 2e-5 | **2e-5** | Alpaca, Vicuna, LIMA 1-7B SFT 표준. 1e-5는 너무 보수적 (underfitting), 5e-6는 1B에 비현실적으로 낮음 |
| **epochs** | ~1.7 (5000 steps) | **3 epochs (~8,820 steps)** | 188K는 중규모. 3 epoch이면 충분한 학습 + overfitting 방지. Stanford Alpaca도 3 epoch 사용 |
| **max_steps** | 5000 | **9,000** | 3 epoch + 약간 여유 |
| **warmup_steps** | 150 | **300** | 9000 steps의 ~3%. Llama-2-chat 논문 기준 2-5% warmup 권장 |
| **weight_decay** | 0.01 | **0.01** | SFT 표준. pretrain(0.1)보다 낮아야 . 적절 |
| **max_seq_len** | 4096 | **4096** | dynamic padding이 있으므로 4096 유지해도 짧은 샘플에서 낭비 없음. 응답 학습 가능 |
| **max_grad_norm** | 1.0 | **1.0** | 표준값. 변경 불필요 |
| **batch_size (per GPU)** | 4 | **4** | B200 메모리 충분. 유지 |
| **grad_accum** | 2 | **2** | eff_batch=64 적절 |
| **NEFTune alpha** | 10.0 | **5.0** | 논문 default=5. 10은 aggressive. 반복률 개선에 도움되지만 과도하면 품질 하락 |
| **eval_interval** | 250 | **500** | 9000 steps에서 250은 너무 빈번. 500이면 ~18회 평가 |
| **save_interval** | 500 | **1000** | 디스크 절약 |
### 3.3 Launch script 수정사항
```bash
MAX_STEPS=9000
WARMUP_STEPS=300
# NEFTune alpha는 sft.py 코드에서 10→5로 변경 필요
```
---
## 4. 학습 모니터링 강화
### 4.1 현재 구현 상태
| 기능 | 상태 | 비고 |
|------|------|------|
| Val loss 주기적 기록 | eval_interval=250마다 | TensorBoard + stdout |
| Best checkpoint 저장 | val_loss 기준 | suffix="best" |
| TensorBoard 로깅 | loss, lr, grad_norm, tok/s | |
| Early stopping | 미구현 | **추가 필요** |
| 반복률 추적 | 미구현 | 학습 실시간 추적은 비현실적 |
### 4.2 Early Stopping 구현 방안
`trainer.py` 추가할 로직:
```python
# __init__에 추가
self._patience = 5 # eval_interval * patience = 5 * 500 = 2500 steps 동안 개선 없으면 중단
self._no_improve_count = 0
# _run_validation 후 (train 메서드 내)
if val_loss < self._best_val_loss:
self._best_val_loss = val_loss
self._no_improve_count = 0
else:
self._no_improve_count += 1
if self._no_improve_count >= self._patience:
self._log(f"Early stopping at step {step+1} (no improvement for {self._patience} evals)")
break # training loop 탈출
```
### 4.3 반복률 추적
학습 실시간 반복률 측정은 generation이 필요해 비용이 . 대안:
- **eval_interval마다 5개 샘플 생성** 3-gram 반복 비율 계산 TensorBoard 기록
- 이는 선택사항이며, 최종 평가 체크포인트별로 측정하는 것이 현실적
---
## 5. 재학습 전 체크리스트
### 데이터
- [ ] `data/sft/train.jsonl` 존재 확인 (188,234 lines)
- [ ] `data/sft/val.jsonl` 존재 확인 (9,907 lines)
- [ ] 데이터 품질 필터 재적용 여부 결정 (한국어 비율 50%, min_output 100자로 강화 재실행 필요)
- [ ] 재실행 시: `python data/prepare_sft_data.py --min_output_len 100`
### 코드 수정
- [ ] `prepare_sft_data.py`: 한국어 비율 0.3 **0.5** 변경
- [ ] `prepare_sft_data.py`: `_quality_filter` max output 4000 **3000** 변경
- [ ] `prepare_sft_data.py`: DATASET_WEIGHTS 조정 (OpenOrca 2.01.5, evol-instruct 1.52.0, kovast 0.80.5)
- [ ] `sft.py`: NEFTune alpha 10.0 **5.0** 변경
- [ ] `trainer.py`: Early stopping patience=5 로직 추가
- [ ] `launch_sft.sh`: MAX_STEPS=9000, WARMUP_STEPS=300 변경
### 인프라
- [ ] GPU 상태 확인: `nvidia-smi` (8× B200 가용)
- [ ] 디스크 공간: 체크포인트당 ~4GB × ~9개 = ~36GB 필요
- [ ] Base checkpoint 존재: `checkpoints/korean_1b_fp8_run1/checkpoint-0034000`
- [ ] Tokenizer 존재 확인
### 학습 실행
- [ ] TensorBoard 확인: `tensorboard --logdir checkpoints/korean_1b_sft/tensorboard`
- [ ] 처음 100 step에서 loss 급감 확인 (정상이면 2-3 1.5 이하)
- [ ] 500 step에서 val_loss 확인
- [ ] 반복 생성 테스트: 1000 step 체크포인트에서 간단한 프롬프트로 생성 테스트
---
## 6. 요약
### 현재 코드 상태: **양호 (minor 수정 6건)**
주요 버그는 이미 수정됨. labels off-by-one으로 보였던 것은 실제로는 정상적인 teacher-forcing shift 패턴. 추가 수정은 품질 향상을 위한 파라미터 튜닝 수준.
### 핵심 변경사항 우선순위
1. **MAX_STEPS 9000, WARMUP 300** (3 epoch 학습)
2. **데이터 품질 필터 강화** (한국어 50%, min output 100자) `prepare_sft_data.py` 재실행
3. **Early stopping 구현**
4. **NEFTune alpha 5.0으로 하향**
5. **데이터 가중치 미세 조정**
### 예상 학습 시간
- 9000 steps × 8 GPU B200 FP8 **2-4시간** (seq_len 분포에 따라)