#!/usr/bin/env bash # ============================================================================= # finish_korean_pipeline.sh # 한국어 LLM 데이터 파이프라인 Step 6~7 재개 스크립트 # # - 완료된 단계(출력 파일 존재)는 자동으로 건너뜀 # - --from-step N 지정 시 해당 스텝부터 강제 재실행 # - 상세 로그를 파일 + 터미널에 동시 출력 # # 스텝 번호: # 61 = Step 6a : c4_ko 토크나이징 # 62 = Step 6b : namuwiki_ko 토크나이징 # 63 = Step 6c : ko_wiki 토크나이징 # 70 = Step 7 : 병합 (korean_train.bin / korean_val.bin) # ============================================================================= set -euo pipefail # ----------------------------------------------------------------------------- # 프로젝트 루트로 이동 (스크립트 위치 기준으로 한 단계 위) # ----------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" cd "${PROJECT_ROOT}" # ----------------------------------------------------------------------------- # 인자 파싱 # ----------------------------------------------------------------------------- FROM_STEP=0 LOG_FILE="data/finish_korean_pipeline.log" DRY_RUN=false while [[ $# -gt 0 ]]; do case $1 in --from-step) FROM_STEP="$2" shift 2 ;; --log-file) LOG_FILE="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; *) echo "알 수 없는 인자: $1" echo "사용법: bash data/finish_korean_pipeline.sh [--from-step N] [--log-file PATH] [--dry-run]" exit 1 ;; esac done # ----------------------------------------------------------------------------- # 로그 설정: 이후 모든 stdout/stderr를 파일 + 터미널로 동시 출력 # (--dry-run 시에도 로그 파일 생성) # ----------------------------------------------------------------------------- mkdir -p "$(dirname "${LOG_FILE}")" exec > >(tee -a "${LOG_FILE}") 2>&1 # ----------------------------------------------------------------------------- # 유틸리티 함수 # ----------------------------------------------------------------------------- log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } log_sep() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ================================================================" } log_skip() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [SKIP] $*" } log_start() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [START] $*" } log_done() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [DONE] $*" } log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2 } # 명령 실행 (dry-run 시 출력만) # PYTHONUNBUFFERED=1: Python stdout 즉시 flush → tee 경유 로그 파일에 실시간 반영 run_cmd() { if $DRY_RUN; then echo "[DRY-RUN] $*" else PYTHONUNBUFFERED=1 "$@" fi } # 파일 크기를 사람이 읽기 쉬운 형식으로 출력 human_size() { local file="$1" if [[ ! -f "${file}" ]]; then echo "N/A" return fi local bytes bytes=$(stat -c%s "${file}" 2>/dev/null || echo 0) if (( bytes >= 1073741824 )); then awk "BEGIN { printf \"%.2f GB\", ${bytes}/1073741824 }" elif (( bytes >= 1048576 )); then awk "BEGIN { printf \"%.2f MB\", ${bytes}/1048576 }" elif (( bytes >= 1024 )); then awk "BEGIN { printf \"%.2f KB\", ${bytes}/1024 }" else echo "${bytes} B" fi } # .bin 파일의 토큰 수 추정 (uint16 = 2바이트/토큰) token_count() { local file="$1" if [[ ! -f "${file}" ]]; then echo "N/A" return fi local bytes bytes=$(stat -c%s "${file}" 2>/dev/null || echo 0) local tokens=$(( bytes / 2 )) if (( tokens >= 1000000000 )); then awk "BEGIN { printf \"%.2fB\", ${tokens}/1000000000 }" elif (( tokens >= 1000000 )); then awk "BEGIN { printf \"%.2fM\", ${tokens}/1000000 }" elif (( tokens >= 1000 )); then awk "BEGIN { printf \"%.2fK\", ${tokens}/1000 }" else echo "${tokens}" fi } # 스텝 실행 여부 결정 # 인자: step_num output_file # 반환: 0 = 실행해야 함, 1 = 건너뜀 should_skip() { local step_num="$1" local output_file="$2" # --from-step 이 지정되어 있고, 현재 스텝이 그 이상이면 강제 실행 if (( FROM_STEP > 0 && step_num >= FROM_STEP )); then return 1 # 건너뛰지 않음 (실행) fi # 출력 파일이 이미 존재하면 건너뜀 if [[ -f "${output_file}" ]]; then return 0 # 건너뜀 fi return 1 # 실행 } # ----------------------------------------------------------------------------- # 경로 상수 # ----------------------------------------------------------------------------- TOKENIZER="tokenizer/korean_sp/tokenizer.json" RAW_C4="data/raw/c4_ko" RAW_NAMU="data/raw/namuwiki_ko" RAW_WIKI_PATTERN="data/raw/ko_wiki_*.txt" OUT_C4_TRAIN="data/korean_c4_train.bin" OUT_C4_VAL="data/korean_c4_val.bin" OUT_NAMU_TRAIN="data/korean_namuwiki_train.bin" OUT_NAMU_VAL="data/korean_namuwiki_val.bin" OUT_WIKI_TRAIN="data/korean_wiki_train.bin" OUT_WIKI_VAL="data/korean_wiki_val.bin" OUT_TRAIN="data/korean_train.bin" OUT_VAL="data/korean_val.bin" # ----------------------------------------------------------------------------- # 시작 메시지 # ----------------------------------------------------------------------------- log_sep log "한국어 LLM 데이터 파이프라인 (Step 6~7) 재개" log "프로젝트 루트 : ${PROJECT_ROOT}" log "로그 파일 : ${LOG_FILE}" log "FROM_STEP : ${FROM_STEP} (0=자동감지)" log "DRY_RUN : ${DRY_RUN}" log_sep # ----------------------------------------------------------------------------- # 사전 검사 # ----------------------------------------------------------------------------- log "사전 검사 시작..." # 토크나이저 존재 확인 if [[ ! -f "${TOKENIZER}" ]]; then log_error "토크나이저를 찾을 수 없습니다: ${TOKENIZER}" exit 1 fi log "토크나이저 확인: ${TOKENIZER} ($(human_size "${TOKENIZER}"))" # CC-100은 비어있으므로 건너뜀 알림 if [[ -d "data/raw/cc100_ko" ]]; then local_files=$(find "data/raw/cc100_ko" -type f 2>/dev/null | wc -l) if (( local_files == 0 )); then log "CC-100: data/raw/cc100_ko 디렉토리가 비어있음 → CC-100 처리 건너뜀" fi fi # 입력 데이터 존재 확인 c4_files=$(find "${RAW_C4}" -name "*.txt" -type f 2>/dev/null | wc -l) namu_files=$(find "${RAW_NAMU}" -name "*.txt" -type f 2>/dev/null | wc -l) wiki_files=$(find "data/raw" -name "ko_wiki_*.txt" -type f 2>/dev/null | wc -l) log "입력 데이터 현황:" log " c4_ko : ${c4_files}개 .txt 파일 (${RAW_C4})" log " namuwiki_ko: ${namu_files}개 .txt 파일 (${RAW_NAMU})" log " ko_wiki : ${wiki_files}개 .txt 파일 (data/raw/ko_wiki_*.txt)" if (( c4_files == 0 )); then log_error "c4_ko 데이터 없음: ${RAW_C4} 에 .txt 파일이 없습니다" exit 1 fi if (( namu_files == 0 )); then log_error "namuwiki 데이터 없음: ${RAW_NAMU} 에 .txt 파일이 없습니다" exit 1 fi if (( wiki_files == 0 )); then log_error "ko_wiki 데이터 없음: data/raw/ko_wiki_*.txt 파일이 없습니다" exit 1 fi log "사전 검사 완료" log_sep # ============================================================================= # Step 6a: c4_ko 토크나이징 # ============================================================================= STEP_NUM=61 if should_skip ${STEP_NUM} "${OUT_C4_TRAIN}"; then log_skip "Step 6a (c4_ko 토크나이징): ${OUT_C4_TRAIN} 이미 존재 → 건너뜀" log " 크기: $(human_size "${OUT_C4_TRAIN}"), 토큰: $(token_count "${OUT_C4_TRAIN}")" else log_start "Step 6a: c4_ko 토크나이징 시작" log " 입력: ${RAW_C4}/*.txt (${c4_files}개 파일)" log " 출력: ${OUT_C4_TRAIN}, ${OUT_C4_VAL}" log " 토크나이저: ${TOKENIZER}" # 강제 재실행 시 기존 파일 제거 if (( FROM_STEP > 0 && STEP_NUM >= FROM_STEP )); then if [[ -f "${OUT_C4_TRAIN}" ]]; then log " 기존 파일 삭제 (강제 재실행): ${OUT_C4_TRAIN}" run_cmd rm -f "${OUT_C4_TRAIN}" "${OUT_C4_VAL}" fi fi STEP6A_START=$(date +%s) run_cmd python data/prepare.py \ --input "${RAW_C4}/*.txt" \ --output "${OUT_C4_TRAIN}" \ --tokenizer "${TOKENIZER}" \ --val_split 0.002 \ --seed 42 if ! $DRY_RUN; then STEP6A_END=$(date +%s) STEP6A_ELAPSED=$(( STEP6A_END - STEP6A_START )) log_done "Step 6a 완료 (소요: ${STEP6A_ELAPSED}초)" log " ${OUT_C4_TRAIN} : $(human_size "${OUT_C4_TRAIN}"), 토큰: $(token_count "${OUT_C4_TRAIN}")" log " ${OUT_C4_VAL} : $(human_size "${OUT_C4_VAL}"), 토큰: $(token_count "${OUT_C4_VAL}")" else log_done "Step 6a (dry-run 완료)" fi fi log_sep # ============================================================================= # Step 6b: namuwiki_ko 토크나이징 # ============================================================================= STEP_NUM=62 if should_skip ${STEP_NUM} "${OUT_NAMU_TRAIN}"; then log_skip "Step 6b (namuwiki 토크나이징): ${OUT_NAMU_TRAIN} 이미 존재 → 건너뜀" log " 크기: $(human_size "${OUT_NAMU_TRAIN}"), 토큰: $(token_count "${OUT_NAMU_TRAIN}")" else log_start "Step 6b: namuwiki_ko 토크나이징 시작" log " 입력: ${RAW_NAMU}/*.txt (${namu_files}개 파일)" log " 출력: ${OUT_NAMU_TRAIN}, ${OUT_NAMU_VAL}" log " 토크나이저: ${TOKENIZER}" # 강제 재실행 시 기존 파일 제거 if (( FROM_STEP > 0 && STEP_NUM >= FROM_STEP )); then if [[ -f "${OUT_NAMU_TRAIN}" ]]; then log " 기존 파일 삭제 (강제 재실행): ${OUT_NAMU_TRAIN}" run_cmd rm -f "${OUT_NAMU_TRAIN}" "${OUT_NAMU_VAL}" fi fi STEP6B_START=$(date +%s) run_cmd python data/prepare.py \ --input "${RAW_NAMU}/*.txt" \ --output "${OUT_NAMU_TRAIN}" \ --tokenizer "${TOKENIZER}" \ --val_split 0.002 \ --seed 42 if ! $DRY_RUN; then STEP6B_END=$(date +%s) STEP6B_ELAPSED=$(( STEP6B_END - STEP6B_START )) log_done "Step 6b 완료 (소요: ${STEP6B_ELAPSED}초)" log " ${OUT_NAMU_TRAIN} : $(human_size "${OUT_NAMU_TRAIN}"), 토큰: $(token_count "${OUT_NAMU_TRAIN}")" log " ${OUT_NAMU_VAL} : $(human_size "${OUT_NAMU_VAL}"), 토큰: $(token_count "${OUT_NAMU_VAL}")" else log_done "Step 6b (dry-run 완료)" fi fi log_sep # ============================================================================= # Step 6c: ko_wiki 토크나이징 # ============================================================================= STEP_NUM=63 if should_skip ${STEP_NUM} "${OUT_WIKI_TRAIN}"; then log_skip "Step 6c (ko_wiki 토크나이징): ${OUT_WIKI_TRAIN} 이미 존재 → 건너뜀" log " 크기: $(human_size "${OUT_WIKI_TRAIN}"), 토큰: $(token_count "${OUT_WIKI_TRAIN}")" else log_start "Step 6c: ko_wiki 토크나이징 시작" log " 입력: data/raw/ko_wiki_*.txt (${wiki_files}개 파일)" log " 출력: ${OUT_WIKI_TRAIN}, ${OUT_WIKI_VAL}" log " 토크나이저: ${TOKENIZER}" # 강제 재실행 시 기존 파일 제거 if (( FROM_STEP > 0 && STEP_NUM >= FROM_STEP )); then if [[ -f "${OUT_WIKI_TRAIN}" ]]; then log " 기존 파일 삭제 (강제 재실행): ${OUT_WIKI_TRAIN}" run_cmd rm -f "${OUT_WIKI_TRAIN}" "${OUT_WIKI_VAL}" fi fi STEP6C_START=$(date +%s) run_cmd python data/prepare.py \ --input "data/raw/ko_wiki_*.txt" \ --output "${OUT_WIKI_TRAIN}" \ --tokenizer "${TOKENIZER}" \ --val_split 0.002 \ --seed 42 if ! $DRY_RUN; then STEP6C_END=$(date +%s) STEP6C_ELAPSED=$(( STEP6C_END - STEP6C_START )) log_done "Step 6c 완료 (소요: ${STEP6C_ELAPSED}초)" log " ${OUT_WIKI_TRAIN} : $(human_size "${OUT_WIKI_TRAIN}"), 토큰: $(token_count "${OUT_WIKI_TRAIN}")" log " ${OUT_WIKI_VAL} : $(human_size "${OUT_WIKI_VAL}"), 토큰: $(token_count "${OUT_WIKI_VAL}")" else log_done "Step 6c (dry-run 완료)" fi fi log_sep # ============================================================================= # Step 7: 병합 (korean_train.bin / korean_val.bin) # ============================================================================= STEP_NUM=70 if should_skip ${STEP_NUM} "${OUT_TRAIN}"; then log_skip "Step 7 (병합): ${OUT_TRAIN} 이미 존재 → 건너뜀" log " 크기: $(human_size "${OUT_TRAIN}"), 토큰: $(token_count "${OUT_TRAIN}")" else log_start "Step 7: 병합 시작" # 병합 대상 파일 확인 (dry-run이 아닐 경우에만 존재 확인) if ! $DRY_RUN; then MISSING_TRAINS=() for f in "${OUT_C4_TRAIN}" "${OUT_NAMU_TRAIN}" "${OUT_WIKI_TRAIN}"; do if [[ ! -f "${f}" ]]; then MISSING_TRAINS+=("${f}") fi done if (( ${#MISSING_TRAINS[@]} > 0 )); then log_error "병합에 필요한 train 파일이 없습니다:" for f in "${MISSING_TRAINS[@]}"; do log_error " - ${f}" done exit 1 fi MISSING_VALS=() for f in "${OUT_C4_VAL}" "${OUT_NAMU_VAL}" "${OUT_WIKI_VAL}"; do if [[ ! -f "${f}" ]]; then MISSING_VALS+=("${f}") fi done if (( ${#MISSING_VALS[@]} > 0 )); then log_error "병합에 필요한 val 파일이 없습니다:" for f in "${MISSING_VALS[@]}"; do log_error " - ${f}" done exit 1 fi fi # 강제 재실행 시 기존 병합 파일 제거 if (( FROM_STEP > 0 && STEP_NUM >= FROM_STEP )); then if [[ -f "${OUT_TRAIN}" ]]; then log " 기존 파일 삭제 (강제 재실행): ${OUT_TRAIN}" run_cmd rm -f "${OUT_TRAIN}" "${OUT_VAL}" fi fi log " [train] 병합:" log " 입력: ${OUT_C4_TRAIN}, ${OUT_NAMU_TRAIN}, ${OUT_WIKI_TRAIN}" log " 출력: ${OUT_TRAIN}" STEP7_START=$(date +%s) run_cmd python data/merge_bins.py \ "${OUT_C4_TRAIN}" \ "${OUT_NAMU_TRAIN}" \ "${OUT_WIKI_TRAIN}" \ "${OUT_TRAIN}" log " [val] 병합:" log " 입력: ${OUT_C4_VAL}, ${OUT_NAMU_VAL}, ${OUT_WIKI_VAL}" log " 출력: ${OUT_VAL}" run_cmd python data/merge_bins.py \ "${OUT_C4_VAL}" \ "${OUT_NAMU_VAL}" \ "${OUT_WIKI_VAL}" \ "${OUT_VAL}" if ! $DRY_RUN; then STEP7_END=$(date +%s) STEP7_ELAPSED=$(( STEP7_END - STEP7_START )) log_done "Step 7 완료 (소요: ${STEP7_ELAPSED}초)" log " ${OUT_TRAIN} : $(human_size "${OUT_TRAIN}"), 토큰: $(token_count "${OUT_TRAIN}")" log " ${OUT_VAL} : $(human_size "${OUT_VAL}"), 토큰: $(token_count "${OUT_VAL}")" else log_done "Step 7 (dry-run 완료)" fi fi log_sep # ============================================================================= # 최종 상태 요약 # ============================================================================= log "=== 파이프라인 완료 요약 ===" print_file_info() { local label="$1" local file="$2" if [[ -f "${file}" ]]; then printf "[$(date '+%Y-%m-%d %H:%M:%S')] %-45s 크기: %10s 토큰: %10s\n" \ "${label}" "$(human_size "${file}")" "$(token_count "${file}")" else printf "[$(date '+%Y-%m-%d %H:%M:%S')] %-45s [파일 없음]\n" "${label}" fi } print_file_info "korean_c4_train.bin" "${OUT_C4_TRAIN}" print_file_info "korean_c4_val.bin" "${OUT_C4_VAL}" print_file_info "korean_namuwiki_train.bin" "${OUT_NAMU_TRAIN}" print_file_info "korean_namuwiki_val.bin" "${OUT_NAMU_VAL}" print_file_info "korean_wiki_train.bin" "${OUT_WIKI_TRAIN}" print_file_info "korean_wiki_val.bin" "${OUT_WIKI_VAL}" print_file_info "korean_train.bin [최종]" "${OUT_TRAIN}" print_file_info "korean_val.bin [최종]" "${OUT_VAL}" # 총 학습 토큰 계산 if [[ -f "${OUT_TRAIN}" ]] && ! $DRY_RUN; then TRAIN_BYTES=$(stat -c%s "${OUT_TRAIN}" 2>/dev/null || echo 0) TRAIN_TOKENS=$(( TRAIN_BYTES / 2 )) TRAIN_TOKENS_B=$(awk "BEGIN { printf \"%.2fB\", ${TRAIN_TOKENS}/1000000000 }") log "" log "총 학습 토큰: ${TRAIN_TOKENS_B} (${TRAIN_TOKENS} tokens)" fi log_sep log "모든 단계 완료" # ============================================================================= # 실행 안내 (스크립트 첫 실행 시에도 볼 수 있도록 출력) # ============================================================================= cat <<'EOF' 실행 방법: # 자동 감지 (완료된 스텝 건너뜀) bash data/finish_korean_pipeline.sh # 백그라운드 실행 (권장) nohup bash data/finish_korean_pipeline.sh > data/finish_korean_pipeline.log 2>&1 & tail -f data/finish_korean_pipeline.log # 특정 스텝부터 재시작 bash data/finish_korean_pipeline.sh --from-step 62 # dry-run (실제 실행 없이 명령 확인) bash data/finish_korean_pipeline.sh --dry-run EOF