Files
frankenstallm/source/scripts/monitor_training.sh

245 lines
7.5 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# =============================================================================
# monitor_training.sh — SFT 학습 실시간 모니터링 + 이상 감지
#
# Usage:
# bash scripts/monitor_training.sh # 기본 로그 경로
# bash scripts/monitor_training.sh /path/to/train.log # 커스텀 경로
# bash scripts/monitor_training.sh --check-once # 1회 검사 후 종료
#
# 감시 항목:
# 🔴 loss = 0.0000 (3 step 연속) → Labels 버그
# 🔴 gnorm > 50.0 → 발산 직전
# 🔴 로그 5분 이상 멈춤 → Hang
# 🟠 loss spike (3× 이동평균) → Bad batch / LR
# 🟠 gnorm > 10.0 → 불안정
# 🟠 디스크 > 80% → 정리 필요
# 🟡 GPU util < 50% → 병목
# =============================================================================
set -euo pipefail
# ---- Configuration ----------------------------------------------------------
LOG_FILE="${1:-checkpoints/korean_1b_sft/train.log}"
CHECK_INTERVAL=30 # 초 단위 폴링 간격
ZERO_LOSS_THRESHOLD=3 # N회 연속 loss=0이면 경고
GNORM_WARN=10.0
GNORM_CRITICAL=50.0
LOSS_SPIKE_FACTOR=3.0 # 이동평균 대비 N배 이상이면 spike
STALL_TIMEOUT=300 # 초 (5분) 로그 멈춤 감지
DISK_WARN_PCT=80
GPU_UTIL_WARN=50
CHECK_ONCE=false
if [[ "${1:-}" == "--check-once" ]]; then
CHECK_ONCE=true
LOG_FILE="${2:-checkpoints/korean_1b_sft/train.log}"
fi
# ---- Colors -----------------------------------------------------------------
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
# ---- Helper -----------------------------------------------------------------
timestamp() { date '+%Y-%m-%d %H:%M:%S'; }
alert() {
local level="$1" msg="$2"
case "$level" in
CRITICAL) echo -e "${RED}🔴 [$(timestamp)] [CRITICAL] ${msg}${NC}" ;;
WARNING) echo -e "${YELLOW}🟠 [$(timestamp)] [WARNING] ${msg}${NC}" ;;
INFO) echo -e "${CYAN}🟡 [$(timestamp)] [INFO] ${msg}${NC}" ;;
OK) echo -e "${GREEN}✅ [$(timestamp)] [OK] ${msg}${NC}" ;;
esac
}
# ---- Parse last N log lines -------------------------------------------------
parse_metrics() {
# 로그 형식: [timestamp] [INFO] step XXXX | loss X.XXXX | lr X.XXe-XX | gnorm X.XXX | ...
local n="${1:-20}"
if [[ ! -f "$LOG_FILE" ]]; then
echo ""
return
fi
tail -n "$n" "$LOG_FILE" | grep "step.*loss.*gnorm" || true
}
extract_field() {
# $1=line, $2=field name (loss, gnorm, lr)
echo "$1" | grep -oP "${2}\s+\K[0-9]+\.[0-9e+\-]+" | head -1
}
# ---- Check functions --------------------------------------------------------
check_loss_zero() {
local lines
lines=$(parse_metrics "$ZERO_LOSS_THRESHOLD")
if [[ -z "$lines" ]]; then return; fi
local zero_count=0
while IFS= read -r line; do
local loss
loss=$(extract_field "$line" "loss")
if [[ -n "$loss" ]]; then
# loss < 0.001
if (( $(echo "$loss < 0.001" | bc -l 2>/dev/null || echo 0) )); then
((zero_count++))
fi
fi
done <<< "$lines"
if [[ $zero_count -ge $ZERO_LOSS_THRESHOLD ]]; then
alert CRITICAL "Loss가 ${zero_count}회 연속 ~0! Labels 버그 가능성. 즉시 학습 중단!"
return 1
fi
return 0
}
check_loss_spike() {
local lines
lines=$(parse_metrics 20)
if [[ -z "$lines" ]]; then return 0; fi
local losses=()
while IFS= read -r line; do
local loss
loss=$(extract_field "$line" "loss")
[[ -n "$loss" ]] && losses+=("$loss")
done <<< "$lines"
local count=${#losses[@]}
if [[ $count -lt 5 ]]; then return 0; fi
# 마지막 값과 이전 평균 비교
local last_loss="${losses[$((count-1))]}"
local sum=0
for ((i=0; i<count-1; i++)); do
sum=$(echo "$sum + ${losses[$i]}" | bc -l 2>/dev/null || echo "$sum")
done
local avg=$(echo "$sum / ($count - 1)" | bc -l 2>/dev/null || echo "0")
if [[ "$avg" != "0" ]]; then
local ratio=$(echo "$last_loss / $avg" | bc -l 2>/dev/null || echo "1")
if (( $(echo "$ratio > $LOSS_SPIKE_FACTOR" | bc -l 2>/dev/null || echo 0) )); then
alert WARNING "Loss spike 감지! 현재=${last_loss}, 평균=${avg}, 비율=${ratio}x"
fi
fi
return 0
}
check_gnorm() {
local lines
lines=$(parse_metrics 5)
if [[ -z "$lines" ]]; then return 0; fi
local last_line
last_line=$(echo "$lines" | tail -1)
local gnorm
gnorm=$(extract_field "$last_line" "gnorm")
if [[ -z "$gnorm" ]]; then return 0; fi
if (( $(echo "$gnorm > $GNORM_CRITICAL" | bc -l 2>/dev/null || echo 0) )); then
alert CRITICAL "GNorm=${gnorm} > ${GNORM_CRITICAL}! 발산 직전. 학습 중단 고려."
elif (( $(echo "$gnorm > $GNORM_WARN" | bc -l 2>/dev/null || echo 0) )); then
alert WARNING "GNorm=${gnorm} > ${GNORM_WARN}. 불안정 징후."
fi
return 0
}
check_stall() {
if [[ ! -f "$LOG_FILE" ]]; then
alert INFO "로그 파일 없음: ${LOG_FILE}"
return 0
fi
local last_modified
last_modified=$(stat -c %Y "$LOG_FILE" 2>/dev/null || echo 0)
local now
now=$(date +%s)
local diff=$((now - last_modified))
if [[ $diff -gt $STALL_TIMEOUT ]]; then
alert CRITICAL "로그가 ${diff}초 ($(( diff/60 ))분) 동안 업데이트 없음! Hang 가능성."
fi
return 0
}
check_disk() {
local usage
usage=$(df /PROJECT 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%')
if [[ -n "$usage" && "$usage" -gt "$DISK_WARN_PCT" ]]; then
alert WARNING "디스크 사용률 ${usage}% > ${DISK_WARN_PCT}%. 체크포인트 정리 필요."
fi
return 0
}
check_gpu() {
if ! command -v nvidia-smi &>/dev/null; then return 0; fi
local low_util=0
local total_gpus=0
while IFS= read -r util; do
((total_gpus++))
if [[ "$util" -lt "$GPU_UTIL_WARN" ]]; then
((low_util++))
fi
done < <(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>/dev/null)
if [[ $total_gpus -gt 0 && $low_util -gt 0 ]]; then
alert INFO "${low_util}/${total_gpus} GPU utilization < ${GPU_UTIL_WARN}%. 데이터 로딩 병목?"
fi
return 0
}
# ---- Status summary ---------------------------------------------------------
print_status() {
local lines
lines=$(parse_metrics 1)
if [[ -n "$lines" ]]; then
echo -e "${GREEN}최근 로그:${NC} $lines"
fi
if command -v nvidia-smi &>/dev/null; then
echo -e "${CYAN}GPU 메모리:${NC}"
nvidia-smi --query-gpu=index,memory.used,memory.total,utilization.gpu \
--format=csv,noheader 2>/dev/null | head -8
fi
local disk
disk=$(df -h /PROJECT 2>/dev/null | awk 'NR==2 {print "사용: "$3"/"$2" ("$5")"}')
echo -e "${CYAN}디스크:${NC} ${disk}"
}
# ---- Main loop --------------------------------------------------------------
echo "=================================================================="
echo " SFT Training Monitor"
echo " Log file: ${LOG_FILE}"
echo " Check interval: ${CHECK_INTERVAL}s"
echo " Press Ctrl+C to stop"
echo "=================================================================="
run_all_checks() {
check_loss_zero || true
check_loss_spike || true
check_gnorm || true
check_stall || true
check_disk || true
check_gpu || true
echo "---"
print_status
echo ""
}
if $CHECK_ONCE; then
run_all_checks
exit 0
fi
while true; do
run_all_checks
sleep "$CHECK_INTERVAL"
done