15 KiB
15 KiB
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% 비정상
즉각 대응
- 학습 즉시 중단 (Ctrl+C 또는
kill -SIGINT <PID>) - 가장 최근 정상 체크포인트 확인:
ls -lt checkpoints/korean_1b_sft/checkpoint-* | head -5
원인별 진단 및 대응
1-A. Labels Shift 버그 재발
확인 방법:
# 데이터에서 샘플 하나 로드 후 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. 데이터 오염
확인 방법:
# 랜덤 배치에서 실제 학습 토큰 검사
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 버그일 가능성이 훨씬 높음. 그래도 확인:
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회 spike: 무시 (단발성 bad batch). 다음 log에서 복구 확인
- 연속 3회 spike: 학습 중단
- 복구 방법:
# 마지막 정상 체크포인트에서 재시작, 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까지 학습 계속
대응
즉시 가능한 조치
- 수동 early stop: 모니터링 스크립트가 경고 → 수동 중단
- Best checkpoint 사용:
checkpoint-best디렉토리에 자동 저장됨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 수정)
# 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 가능성 극히 낮음
만약 발생한다면
- 증상:
torch.cuda.OutOfMemoryError→ trainer.py에서 이미 catch하여 상세 메시지 출력 - 즉시 대응:
# batch_size 줄이기 (4→2), grad_accum 늘리기 (2→4) → effective batch 동일 bash scripts/launch_sft.sh --batch_size 2 --grad_accum 4 --resume <last_ckpt> - Gradient checkpointing:
- ✅ 이미 활성화됨 (sft.py에서
model.gradient_checkpointing_enable())
- ✅ 이미 활성화됨 (sft.py에서
- 추가 조치:
# 메모리 fragmentation 방지 export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
메모리 모니터링
watch -n 5 nvidia-smi # 실시간 확인
# 또는 monitor_training.sh 사용 (아래 참조)
시나리오 5: GPU Hang / NCCL 통신 장애
감지 방법
- 증상: 학습 로그가 멈춤 (새 step이 N분 이상 안 나옴)
- NCCL timeout: 기본 30분 후 에러 발생
nvidia-smi에서 특정 GPU utilization 0%
진단
# 1. GPU 상태 확인
nvidia-smi
# 2. NCCL 디버그 활성화하여 재시작
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=ALL
# 3. 프로세스 상태 확인
ps aux | grep torchrun
복구 방법
# 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}"
최근 체크포인트 자동 감지 스크립트
#!/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% | 🔴 실패 | 재학습 필요 |
파라미터 조정으로 해결 시도 (재학습 없이)
# 추론 시 적용
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%
- 원인 분석:
- 데이터 내 반복 패턴:
data_quality_audit.py로 재확인 - Epoch 과다: 5+ epoch은 반복 패턴 암기 유발 → 3-4 epoch이 적정
- 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: 모델 출력 직접 확인
# 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에서 달성하기 어려움
데이터 추가 수집 방향
- Korean instruction-following 데이터: KoAlpaca, KULLM 등에서 format-constrained 샘플
- Multi-turn 한국어 대화: 지시 따르기 능력 강화
- 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
오래된 체크포인트 자동 삭제
#!/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
디스크 모니터링
# 학습 중 주기적 확인
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반환 → 이어서 학습
재시작 명령어
# 방법 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