12 KiB
SFT 품질 위기 분석 및 의사결정 보고서
작성일: 2026-02-26
작성자: Optimus Prime (AI)
판결 유형: 중립적 판사 — 모든 보고서 종합 후 최종 결론
1. 현재 상황 요약
| 항목 | 값 |
|---|---|
| 모델 | Korean 1B SFT (1.19B params) |
| 학습 | 5,000 steps, ~39분, 8× B200 |
| Final Loss | 1.9677 (수렴 근접, 아직 미세 하강 중) |
| 반복률 (잘못된 포맷) | 57% → 근본 원인: 프롬프트 포맷 불일치 |
| 반복률 (올바른 포맷) | 30.7% → +rep_penalty 적용 시 17.7% |
| 반복률 (올바른 포맷 + rep_penalty=1.1만) | ~5% (실험 결과) |
| 반복률 (올바른 포맷 + rep_penalty=1.1 + no_repeat_3gram) | 0.0% |
| SFT 데이터 | 159,125 샘플, ~2 epochs |
| Epoch 수 | ~2 (업계 표준 3-5 대비 부족) |
핵심 사실: 원래 보고된 57% 반복률의 대부분은 추론 시 프롬프트 포맷 불일치 때문이었다. 학습은 <|user|>/<|assistant|> 포맷인데 평가는 ### 질문:/### 답변: 포맷으로 수행됨. 이 포맷만 맞추면 57% → 5%로 급감하고, rep_penalty=1.1 추가 시 0%까지 도달.
2. 발견된 문제들 전체 목록
🔴 Critical (학습 품질에 직접 영향)
| # | 문제 | 심각도 | 상태 |
|---|---|---|---|
| 1 | 추론 프롬프트 포맷 불일치 (학습≠평가) | 🔴 Critical | ✅ 수정됨 |
| 2 | Static Padding — Dynamic padding이 사실상 무효화 (4096 고정) | 🔴 Critical | ❌ 미수정 |
| 3 | 트렁케이션 시 EOS 손실 — 잘린 샘플에서 EOS 미학습 | 🔴 Critical | ❌ 미수정 (0.04%만 해당) |
| 4 | Epoch 부족 — ~2 epochs (업계 표준 3-5) | 🔴 Critical | ❌ 미수정 |
| 5 | Validation split 없음 — 과적합 모니터링 불가 | 🔴 Critical | ❌ 미수정 |
🟡 Important (데이터 품질)
| # | 문제 | 영향 |
|---|---|---|
| 6 | Output 내 </s> 리터럴 113건 |
EOS 학습 혼란 |
| 7 | Output 내 Q/A 마커 ~550건 | 자체 Q/A 루프 패턴 학습 |
| 8 | 자체 반복 패턴 57건 | 반복 생성 직접 학습 |
| 9 | 짧은 output (<50자) 16,519건 (10.4%) | EOS 타이밍 불안정 |
| 10 | OpenOrca 5배 업샘플링 | 과적합 위험, 다양성 부족 |
| 11 | <|user|>/<|assistant|> 특수토큰 미등록 |
서브워드 분할 (경미) |
🟢 Minor
| # | 문제 | 영향 |
|---|---|---|
| 12 | 한국어 비율 30% 미만 샘플 13.7% | 일관성 저하 |
| 13 | Label shift 마지막 position 미학습 | EOS 이후 생성 경향 |
3. 고쳐서 가는 시나리오 (Fix & Continue)
시나리오 상세
현재 checkpoint-5000 위에서 추가 학습 (resume 또는 lr=1e-5로 continuation):
| 단계 | 작업 | 소요 시간 |
|---|---|---|
| 1 | 데이터 필터링 (품질 문제 샘플 제거) | 30분 |
| 2 | Val split 생성 | 10분 |
| 3 | 추가 학습 5,000 steps (lr=1e-5, epoch 3-4) | ~40분 |
| 4 | 평가 | 30분 |
| 합계 | ~2시간 |
예상 개선 효과
| 지표 | 현재 | 예상 |
|---|---|---|
| Loss | 1.97 | 1.90-1.93 |
| 반복률 (올바른 포맷 + rep_penalty) | 17.7% | 10-15% |
| ko_ifeval | 미측정 (15-28% 추정) | +3-7%p |
리스크
- ⚠️ Static padding 미수정: 학습 속도 3-8x 낭비 지속 → 40분이면 괜찮지만 비효율
- ⚠️ 오염된 가중치 위에 쌓기: EOS 경계 혼란 + 반복 패턴이 이미 가중치에 학습됨 → 추가 학습으로 완전히 "잊을" 수 있는가 불확실
- ⚠️ cosine schedule 문제: 기존 5000 steps 기준으로 LR이 이미 2e-6까지 decay → resume 시 LR 재설정 필요
- 🟡 천장 효과: 오염된 가중치의 한계가 어디인지 모름
4. 처음부터 다시 시나리오 (Restart from Base)
시나리오 상세
base checkpoint (pretrained korean_1b_fp8_run1/checkpoint-0034000)에서 깨끗한 데이터로 SFT 재시작:
| 단계 | 작업 | 소요 시간 |
|---|---|---|
| 1 | 데이터 필터링 (159K → ~120-130K) | 30분 |
| 2 | sft_dataset.py 수정 (dynamic padding 실제 작동, EOS 보존) | 30분 |
| 3 | Val split 생성 | 10분 |
| 4 | launch_sft.sh 수정 (10,000 steps, val_data, 가중치 조정) | 10분 |
| 5 | 학습 실행 (10,000 steps, dynamic padding 적용 시 기존보다 빠를 수 있음) | ~40-80분 |
| 6 | 평가 | 30분 |
| 합계 | ~2.5-3시간 |
예상 품질
| 지표 | 예상 |
|---|---|
| Loss | 1.85-1.92 |
| 반복률 (올바른 포맷, rep_penalty=1.1) | <5% |
| ko_ifeval | 20-30% (1B 한계 내 최적) |
리스크
- 🟢 리스크 낮음: 이미 데이터/코드가 모두 준비되어 있음
- 🟢 결과 예측 가능: 깨끗한 데이터 + 올바른 패딩 + 충분한 epoch → 표준적 결과 기대
- ⚠️ 유일한 리스크: 코드 수정(sft_dataset.py) 시 새로운 버그 도입 가능성 → 작은 subset으로 sanity check 필요
5. 최종 판결 및 근거
판결: 🟢 처음부터 다시 (Restart) — 즉시 재학습
핵심 논거
1. 17.7% 반복률은 "고쳐야 할 수준"인가?
결론: 배포 불가, 그러나 위기는 아니다.
- 17.7%는 rep_penalty + no_repeat_3gram 적용 후 수치. 이 기법 없이는 30.7%
- 상업적 서비스 기준: 반복률 <5%가 업계 표준. 17.7%는 사용자 10명 중 2명이 반복 문장을 목격
- 그러나 올바른 포맷 + rep_penalty=1.1만으로 이미 ~5% 달성 → 모델 자체는 나쁘지 않음
- 진짜 문제는 반복률보다 코드/데이터 파이프라인의 다수 미수정 버그
2. 현재 가중치는 구제 가능한가?
결론: 구제 가능하나, 비용 대비 비효율적.
- EOS truncation은 0.04%만 해당 → 가중치 오염 경미
- Static padding은 가중치 품질에는 영향 없음 (학습 속도만 낭비)
- 데이터 품질 문제 ( 리터럴, Q/A 마커, 짧은 output)는 가중치에 이미 학습됨
- 추가 학습으로 "잊기"는 가능하지만, 깨끗하게 다시 학습하는 것과 시간 차이가 크지 않음
3. 재시작 비용은?
결론: 매우 낮음. Fix 대비 추가 비용 ~1시간.
| Fix (Continue) | Restart | |
|---|---|---|
| 데이터 준비 | 30분 | 30분 (동일) |
| 코드 수정 | 0분 | 40분 (sft_dataset.py) |
| 학습 | 40분 | 40-80분 |
| 평가 | 30분 | 30분 (동일) |
| 합계 | ~2시간 | ~2.5-3시간 |
| 결과 품질 | 개선되지만 한계 있음 | 깨끗한 최적 결과 |
추가 비용 1시간으로 깨끗한 기반을 확보할 수 있다. 이 1시간은 이후 3B 전환, ORPO/DPO 적용 시 "오염된 가중치에서 시작해야 하나?"라는 고민을 완전히 제거한다.
4. 어느 경로가 목표 달성이 빠른가?
목표: 반복률 <5%, ko_ifeval 25%
- Fix 경로: 17.7% → 추가 학습 → 10-15% → 여전히 >5%. ORPO 추가 필요 → +6시간. 총 ~8시간
- Restart 경로: 깨끗한 재학습 → <5% (추론 파라미터 포함) + ko_ifeval 20-30%. 총 ~3시간
- Restart가 2.5배 빠름
결정적 수치 근거
재학습 추가 비용: +1시간 (Fix 대비)
반복률 예상 개선: 17.7% → <5% (3.5배 개선)
미수정 버그 해소: 5개 → 0개 (static padding, EOS 보존, epoch, val split, 데이터 필터)
향후 3B/ORPO 기반: 오염 가중치 → 깨끗한 가중치
ROI: 1시간 투자 → 모든 기술 부채 청산
6. 실행 계획 (구체적 Next Steps)
Step 1: 데이터 필터링 (30분)
cd /PROJECT/0325120031_A/ghong/taketimes/llm-bang
python eval/data_quality_audit.py # 또는 enhanced_quality_filter.py 실행
# 159K → ~120-130K 예상
수행 내용:
</s>,<|endoftext|>,EOS리터럴 포함 샘플 제거 (161건)- Q/A 마커 포함 샘플 제거 (~550건)
- Output <80자 샘플 제거 (~16K건)
- N-gram 반복 샘플 제거 (57건)
- 한국어 비율 <40% 샘플 제거
성공 기준: 필터링 후 120K-135K 샘플 남음. 제거된 샘플 spot check 시 실제 저품질 확인.
Step 2: 코드 수정 (40분)
2-1. sft_dataset.py — Dynamic padding 실제 작동 (가장 중요)
__getitem__에서 고정 4096 패딩 제거- 실제 길이 텐서만 반환
dynamic_collate_fn이 배치별 패딩 수행
2-2. sft_dataset.py — EOS 보존
response_ids = response_ids[:allowed_response - 1] + [self.eos_token_id]
2-3. 데이터 가중치 조정
- OpenOrca: 5.0 → 2.0
- kovast: 0.8 → 0.5
성공 기준: 수정 후 작은 subset (1000 샘플, 100 steps)으로 학습이 정상 실행되는지 확인. Loss가 합리적 범위 (2.0-2.5)에서 시작.
Step 3: Val Split + Config 수정 (10분)
# 90/10 split
python -c "
import json, random
random.seed(42)
with open('data/sft/train_cleaned.jsonl') as f:
lines = f.readlines()
random.shuffle(lines)
split = int(len(lines) * 0.9)
with open('data/sft/train_split.jsonl', 'w') as f:
f.writelines(lines[:split])
with open('data/sft/val_split.jsonl', 'w') as f:
f.writelines(lines[split:])
"
launch_sft.sh 수정:
--max_steps 10000(3-4 epochs)--val_data data/sft/val_split.jsonl--lr 2e-5(초기 학습이므로 유지)--warmup_steps 300
성공 기준: Config 파일 변경 확인, val split 크기 ~12-13K 확인.
Step 4: 재학습 실행 (~40-80분)
bash scripts/launch_sft.sh
모니터링:
- Loss curve: 지속적 하강 확인
- Val loss: 매 500 steps 체크, 상승 시 early stop
- GNorm: 1.5 미만 유지
성공 기준:
- Train loss < 1.90
- Val loss가 train loss의 1.1배 이내 (과적합 없음)
- 학습 속도: dynamic padding으로 기존 대비 2x+ 향상 확인
Step 5: 평가 (30분)
# 1. 반복률 측정 (올바른 포맷)
python eval/test_generation_params.py # 수정된 포맷
# 2. 다양한 rep_penalty에서 반복률
# rep_penalty=1.0 (없음): 목표 <10%
# rep_penalty=1.1: 목표 <3%
# 3. ko_ifeval (가능하면)
lm_eval --model hf --tasks ko_ifeval ...
성공 기준:
| 지표 | 목표 | 실패 기준 |
|---|---|---|
| 반복률 (rep_penalty 없이) | <10% | >20% |
| 반복률 (rep_penalty=1.1) | <3% | >10% |
| Train loss | <1.90 | >2.00 |
| ko_ifeval | >20% | <15% |
Step 6 (Optional): 3B 전환 준비
재학습 성공 시, 동일한 깨끗한 파이프라인으로 3B pretrain → SFT 진행 가능. 재학습 실패 시, 문제 원인 분석 후 데이터/아키텍처 수준에서 재검토.
7. 성공 기준 (각 단계별 체크포인트)
Step 1 ✅ 데이터 필터링
□ 120K-135K 샘플 남음
□ 제거된 샘플이 실제 저품질임을 spot check
Step 2 ✅ 코드 수정
□ 100 steps sanity check 통과
□ 배치 내 시퀀스 길이가 가변적 (4096 고정 아님)
□ 트렁케이션 샘플에서 마지막 토큰이 EOS
Step 3 ✅ Config
□ Val split ~12-13K 샘플
□ max_steps=10000, val_data 경로 설정
Step 4 ✅ 학습
□ Train loss < 1.90
□ Val loss ≤ Train loss × 1.1
□ 학습 속도 ≥ 2x 기존 대비 (dynamic padding 효과)
Step 5 ✅ 평가
□ 반복률 < 10% (rep_penalty 없이)
□ 반복률 < 3% (rep_penalty=1.1)
□ ko_ifeval > 20%
최종 ✅ 목표 달성
□ 반복률 < 5% (실용적 설정)
□ ko_ifeval > 25% (1B 한계 내 최적)
□ 깨끗한 가중치 → 3B/ORPO 기반으로 사용 가능
부록: 왜 "제3의 선택지"는 아닌가
"1B 고쳐서 재학습 후 바로 3B 전환" 옵션도 고려했으나:
- 1B 재학습 자체가 3시간이면 끝남 → 별도 "고쳐서" 단계가 필요 없음
- 3B 전환은 1B 결과와 무관하게 진행 가능 (sft_dataset.py 수정은 3B에도 그대로 적용)
- 따라서 "깨끗하게 재학습" = "3B 전환 준비"가 자연스럽게 포함됨
결론: Restart가 Fix의 상위 호환이다. Fix로 할 수 있는 모든 것을 Restart가 포함하면서, 추가로 코드 버그까지 수정한다. 비용 차이는 1시간.
"40분 아끼려고 기술 부채를 안고 가지 마라. 3시간 투자해서 깨끗한 기반을 만들어라."