frankenstallm / source /scripts /quality_gate.sh
pathcosmos's picture
Upload folder using huggingface_hub (#17)
48ecd01
#!/usr/bin/env bash
# =============================================================================
# quality_gate.sh β€” Phase μ™„λ£Œ μžλ™ ν’ˆμ§ˆ 게이트 검증
#
# Usage:
# bash scripts/quality_gate.sh <phase>
#
# Phases:
# pretrain β€” μ‚¬μ „ν•™μŠ΅ 게이트 (val_loss, loss 단쑰 κ°μ†Œ)
# sft β€” SFT 게이트 (val_loss 수렴, 반볡λ₯ , KoBEST)
# orpo β€” ORPO 게이트 (반볡λ₯ , KoBEST, chosen > rejected)
# deploy β€” 배포 게이트 (GGUF perplexity, Ollama 응닡)
# all β€” λͺ¨λ“  게이트 순차 μ‹€ν–‰
#
# Exit codes:
# 0 β€” 게이트 톡과
# 1 β€” 게이트 μ‹€νŒ¨ (κΈ°μ€€ 미달)
# 2 β€” ν•„μˆ˜ 파일 / μ˜μ‘΄μ„± μ—†μŒ (μ‹€ν–‰ λΆˆκ°€)
# =============================================================================
set -uo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# ---------------------------------------------------------------------------
# 색상 좜λ ₯ 헬퍼
# ---------------------------------------------------------------------------
_RED='\033[0;31m'
_GREEN='\033[0;32m'
_YELLOW='\033[1;33m'
_BLUE='\033[0;34m'
_NC='\033[0m'
log_info() { echo -e "${_BLUE}[INFO]${_NC} $*"; }
log_ok() { echo -e "${_GREEN}[PASS]${_NC} $*"; }
log_warn() { echo -e "${_YELLOW}[WARN]${_NC} $*"; }
log_fail() { echo -e "${_RED}[FAIL]${_NC} $*"; }
log_skip() { echo -e " [SKIP] $*"; }
# ---------------------------------------------------------------------------
# μœ ν‹Έλ¦¬ν‹°: Python ν•œ 쀄 ν‘œν˜„μ‹ 평가 (λΆ€λ™μ†Œμˆ˜μ  비ꡐ)
# ---------------------------------------------------------------------------
py_eval() {
python3 -c "import sys; sys.exit(0 if ($1) else 1)"
}
py_value() {
python3 -c "print($1)"
}
# ---------------------------------------------------------------------------
# μœ ν‹Έλ¦¬ν‹°: JSONμ—μ„œ κ°’ μΆ”μΆœ
# ---------------------------------------------------------------------------
json_get() {
local file="$1" key="$2"
python3 -c "
import json, sys
try:
d = json.load(open('$file'))
keys = '$key'.split('.')
for k in keys:
d = d[k]
print(d)
except Exception as e:
print('NOT_FOUND')
sys.exit(1)
"
}
# ---------------------------------------------------------------------------
# 게이트 κ²°κ³Ό 집계
# ---------------------------------------------------------------------------
GATE_PASS=0
GATE_FAIL=0
GATE_SKIP=0
record_pass() { GATE_PASS=$((GATE_PASS + 1)); log_ok "$*"; }
record_fail() { GATE_FAIL=$((GATE_FAIL + 1)); log_fail "$*"; }
record_skip() { GATE_SKIP=$((GATE_SKIP + 1)); log_skip "$*"; }
# =============================================================================
# Gate 1: Pretrain
# =============================================================================
gate_pretrain() {
echo ""
echo "=================================================================="
echo " Gate: PRETRAIN"
echo " κΈ°μ€€: val_loss < 2.5 | loss 단쑰 κ°μ†Œ 확인"
echo "=================================================================="
# μ΅œμ‹  체크포인트 디렉토리 탐색
CKPT_BASE="$PROJECT_DIR/checkpoints"
METRICS_FILE=""
# metrics.json λ˜λŠ” train_log.jsonl 탐색
for candidate in \
"$CKPT_BASE/korean_3b_fp8_pretrain/metrics.json" \
"$CKPT_BASE/korean_3b_pretrain/metrics.json" \
"$PROJECT_DIR/outputs/pretrain_metrics.json" \
"$PROJECT_DIR/logs/pretrain_metrics.json"
do
if [[ -f "$candidate" ]]; then
METRICS_FILE="$candidate"
break
fi
done
if [[ -z "$METRICS_FILE" ]]; then
log_warn "μ‚¬μ „ν•™μŠ΅ λ©”νŠΈλ¦­ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."
log_warn "μ°ΎλŠ” 경둜: $CKPT_BASE/korean_3b_*/metrics.json"
log_warn "λ©”νŠΈλ¦­ 파일이 μ—†μœΌλ©΄ ν•™μŠ΅ μŠ€ν¬λ¦½νŠΈμ—μ„œ μ•„λž˜ ν˜•μ‹μœΌλ‘œ μ €μž₯ν•˜μ„Έμš”:"
log_warn ' {"val_loss": 2.3, "loss_history": [3.1, 2.8, 2.5, 2.3]}'
record_skip "λ©”νŠΈλ¦­ 파일 μ—†μŒ β€” 게이트 κ±΄λ„ˆλœ€"
return 0
fi
log_info "λ©”νŠΈλ¦­ 파일: $METRICS_FILE"
# val_loss 확인
VAL_LOSS=$(json_get "$METRICS_FILE" "val_loss" 2>/dev/null || echo "NOT_FOUND")
if [[ "$VAL_LOSS" == "NOT_FOUND" ]]; then
record_skip "val_loss ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
log_info "val_loss = $VAL_LOSS (κΈ°μ€€: < 2.5)"
if py_eval "$VAL_LOSS < 2.5" 2>/dev/null; then
record_pass "val_loss $VAL_LOSS < 2.5"
else
record_fail "val_loss $VAL_LOSS >= 2.5 (κΈ°μ€€ 미달)"
fi
fi
# loss 단쑰 κ°μ†Œ 확인 (loss_history)
python3 - "$METRICS_FILE" <<'PYEOF'
import json, sys
metrics_file = sys.argv[1]
try:
d = json.load(open(metrics_file))
history = d.get("loss_history", [])
except Exception as e:
print(f"[SKIP] loss_history 읽기 μ‹€νŒ¨: {e}")
sys.exit(0)
if len(history) < 2:
print(f"[SKIP] loss_history 데이터 λΆ€μ‘± ({len(history)}개)")
sys.exit(0)
# 전체 μΆ”μ„Έκ°€ κ°μ†Œν•˜λŠ”μ§€ 확인 (처음 1/4 vs λ§ˆμ§€λ§‰ 1/4 평균 비ꡐ)
n = len(history)
q = max(1, n // 4)
early_avg = sum(history[:q]) / q
late_avg = sum(history[-q:]) / q
if late_avg < early_avg:
print(f"[PASS] loss 단쑰 κ°μ†Œ 확인: 초기 avg={early_avg:.4f} β†’ 졜근 avg={late_avg:.4f}")
sys.exit(0)
else:
print(f"[FAIL] loss κ°μ†Œ 미확인: 초기 avg={early_avg:.4f}, 졜근 avg={late_avg:.4f}")
sys.exit(1)
PYEOF
local mono_exit=$?
if [[ $mono_exit -eq 0 ]]; then
GATE_PASS=$((GATE_PASS + 1))
elif [[ $mono_exit -eq 1 ]]; then
GATE_FAIL=$((GATE_FAIL + 1))
fi
# exit 0 (SKIP) λŠ” 이미 처리됨
}
# =============================================================================
# Gate 2: SFT
# =============================================================================
gate_sft() {
echo ""
echo "=================================================================="
echo " Gate: SFT"
echo " κΈ°μ€€: val_loss 수렴 | 반볡λ₯  < 15% | KoBEST > 55%"
echo "=================================================================="
METRICS_FILE=""
for candidate in \
"$PROJECT_DIR/outputs/sft_metrics.json" \
"$PROJECT_DIR/logs/sft_metrics.json" \
"$PROJECT_DIR/checkpoints/sft/metrics.json"
do
if [[ -f "$candidate" ]]; then
METRICS_FILE="$candidate"
break
fi
done
if [[ -z "$METRICS_FILE" ]]; then
log_warn "SFT λ©”νŠΈλ¦­ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."
log_warn ' {"val_loss": 1.8, "rep_rate": 0.08, "kobest_score": 0.62}'
record_skip "SFT λ©”νŠΈλ¦­ 파일 μ—†μŒ β€” 게이트 κ±΄λ„ˆλœ€"
return 0
fi
log_info "λ©”νŠΈλ¦­ 파일: $METRICS_FILE"
# val_loss 수렴 (μƒλŒ€ λ³€ν™”μœ¨ < 1% β€” λ§ˆμ§€λ§‰ 두 체크포인트)
python3 - "$METRICS_FILE" <<'PYEOF'
import json, sys
metrics_file = sys.argv[1]
try:
d = json.load(open(metrics_file))
history = d.get("val_loss_history", [])
except Exception as e:
print(f"[SKIP] val_loss_history 읽기 μ‹€νŒ¨: {e}")
sys.exit(0)
if len(history) < 2:
# 단일 val_loss만 있으면 λ‹¨μˆœ 확인
val_loss = d.get("val_loss")
if val_loss is not None:
print(f"[INFO] val_loss = {val_loss} (수렴 νžˆμŠ€ν† λ¦¬ μ—†μŒ β€” 단일 κ°’ 확인 κ±΄λ„ˆλœ€)")
sys.exit(0)
last = history[-1]
second = history[-2]
rel_change = abs(last - second) / max(abs(second), 1e-9)
if rel_change < 0.01:
print(f"[PASS] val_loss 수렴 (μƒλŒ€λ³€ν™”μœ¨ {rel_change*100:.3f}% < 1%): {second:.4f} β†’ {last:.4f}")
sys.exit(0)
else:
print(f"[FAIL] val_loss 미수렴 (μƒλŒ€λ³€ν™”μœ¨ {rel_change*100:.3f}% >= 1%): {second:.4f} β†’ {last:.4f}")
sys.exit(1)
PYEOF
local conv_exit=$?
[[ $conv_exit -eq 0 ]] && GATE_PASS=$((GATE_PASS + 1)) || GATE_FAIL=$((GATE_FAIL + 1))
# 반볡λ₯  확인
REP_RATE=$(json_get "$METRICS_FILE" "rep_rate" 2>/dev/null || echo "NOT_FOUND")
if [[ "$REP_RATE" == "NOT_FOUND" ]]; then
record_skip "rep_rate ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
REP_PCT=$(py_value "$REP_RATE * 100")
log_info "반볡λ₯  = ${REP_PCT}% (κΈ°μ€€: < 15%)"
if py_eval "$REP_RATE < 0.15" 2>/dev/null; then
record_pass "반볡λ₯  ${REP_PCT}% < 15%"
else
record_fail "반볡λ₯  ${REP_PCT}% >= 15% (κΈ°μ€€ 미달)"
fi
fi
# KoBEST 확인
KOBEST=$(json_get "$METRICS_FILE" "kobest_score" 2>/dev/null || echo "NOT_FOUND")
if [[ "$KOBEST" == "NOT_FOUND" ]]; then
record_skip "kobest_score ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
KOBEST_PCT=$(py_value "$KOBEST * 100")
log_info "KoBEST = ${KOBEST_PCT}% (κΈ°μ€€: > 55%)"
if py_eval "$KOBEST > 0.55" 2>/dev/null; then
record_pass "KoBEST ${KOBEST_PCT}% > 55%"
else
record_fail "KoBEST ${KOBEST_PCT}% <= 55% (κΈ°μ€€ 미달)"
fi
fi
}
# =============================================================================
# Gate 3: ORPO
# =============================================================================
gate_orpo() {
echo ""
echo "=================================================================="
echo " Gate: ORPO"
echo " κΈ°μ€€: 반볡λ₯  < 5% | KoBEST > 60% | chosen > rejected 90%+"
echo "=================================================================="
METRICS_FILE=""
for candidate in \
"$PROJECT_DIR/outputs/orpo_metrics.json" \
"$PROJECT_DIR/logs/orpo_metrics.json" \
"$PROJECT_DIR/checkpoints/orpo/metrics.json"
do
if [[ -f "$candidate" ]]; then
METRICS_FILE="$candidate"
break
fi
done
if [[ -z "$METRICS_FILE" ]]; then
log_warn "ORPO λ©”νŠΈλ¦­ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."
log_warn ' {"rep_rate": 0.03, "kobest_score": 0.63, "chosen_win_rate": 0.92}'
record_skip "ORPO λ©”νŠΈλ¦­ 파일 μ—†μŒ β€” 게이트 κ±΄λ„ˆλœ€"
return 0
fi
log_info "λ©”νŠΈλ¦­ 파일: $METRICS_FILE"
# 반볡λ₯  (더 엄격: < 5%)
REP_RATE=$(json_get "$METRICS_FILE" "rep_rate" 2>/dev/null || echo "NOT_FOUND")
if [[ "$REP_RATE" == "NOT_FOUND" ]]; then
record_skip "rep_rate ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
REP_PCT=$(py_value "$REP_RATE * 100")
log_info "반볡λ₯  = ${REP_PCT}% (κΈ°μ€€: < 5%)"
if py_eval "$REP_RATE < 0.05" 2>/dev/null; then
record_pass "반볡λ₯  ${REP_PCT}% < 5%"
else
record_fail "반볡λ₯  ${REP_PCT}% >= 5% (κΈ°μ€€ 미달)"
fi
fi
# KoBEST (더 엄격: > 60%)
KOBEST=$(json_get "$METRICS_FILE" "kobest_score" 2>/dev/null || echo "NOT_FOUND")
if [[ "$KOBEST" == "NOT_FOUND" ]]; then
record_skip "kobest_score ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
KOBEST_PCT=$(py_value "$KOBEST * 100")
log_info "KoBEST = ${KOBEST_PCT}% (κΈ°μ€€: > 60%)"
if py_eval "$KOBEST > 0.60" 2>/dev/null; then
record_pass "KoBEST ${KOBEST_PCT}% > 60%"
else
record_fail "KoBEST ${KOBEST_PCT}% <= 60% (κΈ°μ€€ 미달)"
fi
fi
# Chosen win rate (chosen log-prob > rejected log-prob λΉ„μœ¨)
CHOSEN_WIN=$(json_get "$METRICS_FILE" "chosen_win_rate" 2>/dev/null || echo "NOT_FOUND")
if [[ "$CHOSEN_WIN" == "NOT_FOUND" ]]; then
record_skip "chosen_win_rate ν‚€ μ—†μŒ β€” κ±΄λ„ˆλœ€"
else
WIN_PCT=$(py_value "$CHOSEN_WIN * 100")
log_info "Chosen win rate = ${WIN_PCT}% (κΈ°μ€€: >= 90%)"
if py_eval "$CHOSEN_WIN >= 0.90" 2>/dev/null; then
record_pass "Chosen win rate ${WIN_PCT}% >= 90%"
else
record_fail "Chosen win rate ${WIN_PCT}% < 90% (κΈ°μ€€ 미달)"
fi
fi
}
# =============================================================================
# Gate 4: Deploy
# =============================================================================
gate_deploy() {
echo ""
echo "=================================================================="
echo " Gate: DEPLOY"
echo " κΈ°μ€€: Q4_K_M perplexity < F16 Γ— 1.05 | Ollama 5개 ν”„λ‘¬ν”„νŠΈ 응닡"
echo "=================================================================="
local MODEL_NAME="frankenstallm-3b"
local GGUF_DIR="$PROJECT_DIR/outputs/gguf"
local F16_GGUF="$GGUF_DIR/${MODEL_NAME}-f16.gguf"
local Q4KM_GGUF="$GGUF_DIR/${MODEL_NAME}-Q4_K_M.gguf"
# --- GGUF 파일 쑴재 확인 ---
if [[ ! -f "$Q4KM_GGUF" ]]; then
log_warn "Q4_K_M GGUF 파일 μ—†μŒ: $Q4KM_GGUF"
log_warn "λ¨Όμ € μ‹€ν–‰: bash scripts/convert_3b_gguf.sh"
record_skip "GGUF 파일 μ—†μŒ β€” perplexity 게이트 κ±΄λ„ˆλœ€"
else
# perplexity μΈ‘μ • (llama-perplexity λ˜λŠ” Python fallback)
LLAMA_PPL_BIN="$PROJECT_DIR/outputs/llama.cpp/build/bin/llama-perplexity"
if [[ ! -f "$LLAMA_PPL_BIN" ]]; then
log_warn "llama-perplexity λ°”μ΄λ„ˆλ¦¬ μ—†μŒ β€” λΉŒλ“œ μ‹œλ„ 쀑 ..."
cmake --build "$PROJECT_DIR/outputs/llama.cpp/build" \
--target llama-perplexity -j "$(nproc)" &>/dev/null || true
fi
# μƒ˜ν”Œ ν…μŠ€νŠΈλ‘œ perplexity 비ꡐ
SAMPLE_TEXT="$PROJECT_DIR/outputs/gguf/ppl_sample.txt"
if [[ ! -f "$SAMPLE_TEXT" ]]; then
# 짧은 ν•œκ΅­μ–΄ μƒ˜ν”Œ 생성
cat > "$SAMPLE_TEXT" <<'SAMPLE'
인곡지λŠ₯은 ν˜„λŒ€ μ‚¬νšŒμ—μ„œ 맀우 μ€‘μš”ν•œ 기술둜 자리작고 μžˆμŠ΅λ‹ˆλ‹€.
기계 ν•™μŠ΅κ³Ό λ”₯λŸ¬λ‹μ˜ λ°œμ „μœΌλ‘œ 인해 λ‹€μ–‘ν•œ λΆ„μ•Όμ—μ„œ ν˜μ‹ μ΄ 이루어지고 μžˆμŠ΅λ‹ˆλ‹€.
μžμ—°μ–΄ 처리 기술의 λ°œμ „μ€ 인간과 μ»΄ν“¨ν„°μ˜ μƒν˜Έμž‘μš© 방식을 근본적으둜 λ³€ν™”μ‹œν‚€κ³  μžˆμŠ΅λ‹ˆλ‹€.
ν•œκ΅­μ–΄λŠ” κ΅μ°©μ–΄λ‘œμ„œ 특유의 ν˜•νƒœλ‘ μ  νŠΉμ„±μ„ κ°€μ§€κ³  μžˆμ–΄ μžμ—°μ–΄ μ²˜λ¦¬μ— λ…νŠΉν•œ 도전을 μ œμ‹œν•©λ‹ˆλ‹€.
λŒ€κ·œλͺ¨ μ–Έμ–΄ λͺ¨λΈμ˜ λ“±μž₯으둜 기계 λ²ˆμ—­, ν…μŠ€νŠΈ μš”μ•½, μ§ˆμ˜μ‘λ‹΅ λ“±μ˜ μ„±λŠ₯이 크게 ν–₯μƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.
SAMPLE
fi
if [[ -f "$LLAMA_PPL_BIN" && -f "$F16_GGUF" ]]; then
log_info "Perplexity μΈ‘μ • 쀑 (F16 vs Q4_K_M) ..."
PPL_F16=$(timeout 120 "$LLAMA_PPL_BIN" -m "$F16_GGUF" -f "$SAMPLE_TEXT" 2>&1 \
| grep -oP "Perplexity: \K[0-9.]+" | head -1 || echo "0")
PPL_Q4=$(timeout 120 "$LLAMA_PPL_BIN" -m "$Q4KM_GGUF" -f "$SAMPLE_TEXT" 2>&1 \
| grep -oP "Perplexity: \K[0-9.]+" | head -1 || echo "0")
if [[ "$PPL_F16" == "0" || "$PPL_Q4" == "0" ]]; then
record_skip "Perplexity μΈ‘μ • μ‹€νŒ¨ β€” κ±΄λ„ˆλœ€"
else
THRESHOLD=$(py_value "$PPL_F16 * 1.05")
log_info "F16 PPL = $PPL_F16 | Q4_K_M PPL = $PPL_Q4 | κΈ°μ€€: < $THRESHOLD"
if py_eval "$PPL_Q4 < $PPL_F16 * 1.05" 2>/dev/null; then
record_pass "Q4_K_M PPL $PPL_Q4 < F16 PPL Γ— 1.05 ($THRESHOLD)"
else
record_fail "Q4_K_M PPL $PPL_Q4 >= F16 PPL Γ— 1.05 ($THRESHOLD)"
fi
fi
else
record_skip "llama-perplexity λ˜λŠ” F16 GGUF μ—†μŒ β€” perplexity 게이트 κ±΄λ„ˆλœ€"
fi
fi
# --- Ollama 응닡 ν…ŒμŠ€νŠΈ ---
if ! command -v ollama &>/dev/null; then
record_skip "ollama μ—†μŒ β€” 응닡 ν…ŒμŠ€νŠΈ κ±΄λ„ˆλœ€"
return 0
fi
if ! ollama list 2>/dev/null | grep -q "$MODEL_NAME"; then
log_warn "Ollama에 $MODEL_NAME λͺ¨λΈμ΄ λ“±λ‘λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
log_warn "λ¨Όμ € μ‹€ν–‰: bash scripts/deploy_3b_ollama.sh"
record_skip "Ollama λͺ¨λΈ 미등둝 β€” 응닡 ν…ŒμŠ€νŠΈ κ±΄λ„ˆλœ€"
return 0
fi
log_info "Ollama 응닡 ν…ŒμŠ€νŠΈ (5개 ν”„λ‘¬ν”„νŠΈ) ..."
declare -a PROMPTS=(
"μ•ˆλ…•ν•˜μ„Έμš”."
"1 λ”ν•˜κΈ° 1은 λ¬΄μ—‡μΈκ°€μš”?"
"νŒŒμ΄μ¬μ΄λž€ λ¬΄μ—‡μΈκ°€μš”?"
"ν•œκ΅­μ˜ μˆ˜λ„λŠ” μ–΄λ””μΈκ°€μš”?"
"였늘 날씨가 μ’‹λ„€μš”."
)
local PASS=0 FAIL=0
for i in "${!PROMPTS[@]}"; do
local PROMPT="${PROMPTS[$i]}"
local NUM=$((i + 1))
if RESP=$(timeout 45 ollama run "$MODEL_NAME" "$PROMPT" 2>&1) && [[ -n "$RESP" ]]; then
log_ok " ν”„λ‘¬ν”„νŠΈ $NUM 응닡 OK (${#RESP}자)"
PASS=$((PASS + 1))
else
log_fail " ν”„λ‘¬ν”„νŠΈ $NUM 응닡 μ‹€νŒ¨"
FAIL=$((FAIL + 1))
fi
done
log_info "Ollama 응닡: $PASS/5 성곡"
if [[ $FAIL -eq 0 ]]; then
record_pass "Ollama 5개 ν”„λ‘¬ν”„νŠΈ λͺ¨λ‘ 응닡 성곡"
else
record_fail "Ollama 응닡 μ‹€νŒ¨ $FAIL/5"
fi
}
# =============================================================================
# μ΅œμ’… μš”μ•½ 좜λ ₯
# =============================================================================
print_summary() {
local phase="$1"
local TOTAL=$((GATE_PASS + GATE_FAIL + GATE_SKIP))
echo ""
echo "=================================================================="
echo " Quality Gate κ²°κ³Ό: $phase"
echo " PASS: $GATE_PASS | FAIL: $GATE_FAIL | SKIP: $GATE_SKIP | TOTAL: $TOTAL"
echo "=================================================================="
if [[ $GATE_FAIL -eq 0 ]]; then
echo -e "${_GREEN} [GATE PASSED]${_NC} λͺ¨λ“  검증 κΈ°μ€€ 톡과"
echo ""
return 0
else
echo -e "${_RED} [GATE FAILED]${_NC} ${GATE_FAIL}개 검증 κΈ°μ€€ 미달"
echo " μ‹€νŒ¨ ν•­λͺ©μ„ μˆ˜μ •ν•œ ν›„ λ‹€μ‹œ μ‹€ν–‰ν•˜μ„Έμš”."
echo ""
return 1
fi
}
# =============================================================================
# μ§„μž…μ 
# =============================================================================
PHASE="${1:-}"
if [[ -z "$PHASE" ]]; then
echo "Usage: bash scripts/quality_gate.sh <phase>"
echo " phase: pretrain | sft | orpo | deploy | all"
exit 2
fi
echo ""
echo "=================================================================="
echo " Quality Gate 검증 μ‹œμž‘: $PHASE"
echo " ν”„λ‘œμ νŠΈ: $PROJECT_DIR"
echo " μ‹œκ° : $(date '+%Y-%m-%d %H:%M:%S')"
echo "=================================================================="
case "$PHASE" in
pretrain)
gate_pretrain
print_summary "pretrain"
;;
sft)
gate_sft
print_summary "sft"
;;
orpo)
gate_orpo
print_summary "orpo"
;;
deploy)
gate_deploy
print_summary "deploy"
;;
all)
gate_pretrain
gate_sft
gate_orpo
gate_deploy
print_summary "all"
;;
*)
echo "ERROR: μ•Œ 수 μ—†λŠ” phase: $PHASE"
echo "Usage: bash scripts/quality_gate.sh <pretrain|sft|orpo|deploy|all>"
exit 2
;;
esac