#!/bin/bash # test_lab_train.sh — lab_train.sh (依頼 49) を, Pod を借りずに確かめる # # ./test_lab_train.sh 全部流す (最後に「OK n / NG m」. NG があれば終了コード 1) # # lab_train.sh --dry-run を場合ごとに流す. --dry-run は lab_train_fakes.sh で API・SSH・hf・通知を差し替え, # 書くのは一時フォルダ (LAB_DRY_ROOT) だけ. 場合は LAB_FAKE_SCENARIO で決める. # Pod の中で流すコマンド (学習の様子・見張り) は, この Mac の bash で実際に流して書き方を確かめる set -u HERE="$(cd "$(dirname "$0")" && pwd)" OK=0; NG=0 ok() { OK=$((OK + 1)); echo " ok $1"; } ng() { NG=$((NG + 1)); echo " NG $1"; } check() { if eval "$2"; then ok "$1"; else ng "$1"; fi; } run_to() { # run_to 秒 コマンド... (macOS には timeout がない. 時間が来たら TERM で止める → 143) local s="$1"; shift "$@" & local p=$! ( sleep "$s"; kill "$p" 2>/dev/null ) & local w=$! wait "$p"; local rc=$? kill "$w" 2>/dev/null; wait "$w" 2>/dev/null return $rc } run() { # run 場合 [lab_train.sh の引数...] local sc="$1"; shift T=$(mktemp -d) (cd "$HERE" && LAB_DRY_ROOT="$T" LAB_FAKE_SCENARIO="$sc" ./lab_train.sh --log LogT --dry-run "$@" < /dev/null > "$T/out.txt" 2>&1) RC=$? TD="$T/data/lab/LogT/train" CK="$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v1" } has() { grep -qF -- "$2" "$T/$1"; } n_call() { grep -c -- "^$1" "$T/calls.log"; } line_of() { grep -nF -- "$2" "$T/$1" | head -1 | cut -d: -f1; } no_secret() { ! grep -rqE 'DRY-RUNPOD-SECRET-KEY|hf_DRYSECRETTOKEN|10\.9\.8\.7|40123' "$T/out.txt" "$T/data/lab" "$T/notify.log" 2>/dev/null; } common_wrap() { # どの終わり方でも: ログを持ち帰ってから消し, 消えたのを確かめ, 費用を出し, 通知する check "ログを持ち帰ってから DELETE" '[ -f "$TD/train_trim_corr_v3.log" ] && [ -f "$TD/gpu_train.csv" ] && [ $(n_call DELETE) = 1 ] && [ $(line_of scp.log train_trim_corr_v3.log) -ge 1 ]' check "消えたのを API で確かめた・費用 (ドルと円)" 'has out.txt "が消えたことを API で確かめた" && grep -qE "約 [0-9.]+ ドル \(約 [0-9]+ 円" "$T/out.txt"' check "記録に消した時刻と gone_confirmed" '[ "$(jq -r .gone_confirmed "$TD/lab_pod.json")" = true ] && [ -n "$(jq -r ".terminated_at // empty" "$TD/lab_pod.json")" ]' check "通知を1回出す・まとめ result_*.txt" '[ $(wc -l < "$T/notify.log") = 1 ] && ls "$TD"/result_*.txt >/dev/null 2>&1' check "鍵・IP・ポートを画面・記録・通知に出さない" 'no_secret' check "印 (.lab_train.lock) を外す" '[ ! -e "$TD/.lab_train.lock" ]' } echo "== 1. 学習が最後まで終わる" run success check "終了コード 0" '[ $RC = 0 ]' check "A40 で p2m-train-LogT を1つ作る" '[ $(n_call "POST gpu=NVIDIA A40 name=p2m-train-LogT") = 1 ] && [ $(n_call "POST /pods") = 1 ]' check "Pod 側に見張りを置く (上限+60分の時刻を書く)" 'has ssh.log ": lt=watchdog" && has ssh.log "cat > /workspace/r2r2r/lab_watchdog.sh" && [ "$(jq -r ".watchdog_epoch - .created_epoch" "$TD/lab_pod.json")" = 21600 ]' check "準備は lab_start.sh 3b〜3f と同じコマンド" 'has ssh.log "hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 --local-dir /workspace/ckpt/3999" && has ssh.log "git checkout -q 215abfb" && has ssh.log "uv sync" && has ssh.log "install_addon.py" && has ssh.log "add_trim_config.py /workspace/openpi"' check "サーバーは起動しない" '! has ssh.log "serve_policy.py"' check "pi05/・add_trim_config.py・tar を送る" 'has scp.log "/pi05 root@" && has scp.log "add_trim_config.py root@" && grep -q "_raw_episodes.tar root@.*:/workspace/data/" "$T/scp.log"' check "pod_prep_trim_corr.sh 1500 /workspace/ckpt/3999/params" 'has ssh.log "bash /workspace/r2r2r/pi05/openpi_addon/pod_prep_trim_corr.sh 1500 /workspace/ckpt/3999/params"' check "学習は setsid nohup・exp trim_corr_v3・ログ train_trim_corr_v3.log・GPU 30 秒おき" 'has ssh.log "setsid nohup bash /workspace/r2r2r/train_run.sh" && has ssh.log "uv run scripts/train.py pi05_mecharm_trim_corr_lora --exp-name=trim_corr_v3 --overwrite > /workspace/logs/train_trim_corr_v3.log" && has ssh.log "--format=csv -l 30 > /workspace/logs/gpu_train.csv"' check "進み (step・loss) を出す・寝てよいと出す" 'has out.txt "step 100/1500 loss 0.0412" && has out.txt "ここまで来れば寝てかまいません"' check "Hugging Face へ上げる (train_state は上げない)" 'has ssh.log "hf_upload_ckpt.py --dir /workspace/openpi/checkpoints/pi05_mecharm_trim_corr_lora/trim_corr_v3/1499 --repo PixelstoMotors/pi05-mecharm-trim-corr-lora-v1"' check "上げる → ログ → DELETE → KIOXIA の順" '[ $(line_of ssh.log hf_upload_ckpt.py) -lt $(line_of ssh.log ": lt=ls_all") ] && [ $(line_of calls.log "DELETE") -lt $(line_of calls.log "hf download") ]' check "KIOXIA の checkpoints/pi05_mecharm_trim_corr_lora_v1/1499 に置く (.cache・.gitattributes・一時フォルダは残さない)" '[ -f "$CK/1499/_CHECKPOINT_METADATA" ] && [ -f "$CK/1499/params/manifest.ocdbt" ] && [ ! -e "$CK/1499/.cache" ] && [ ! -e "$CK/1499/.gitattributes" ] && [ -z "$(ls -A "$CK" | grep -v "^1499$")" ] && [ -f "$CK/1499/._params" ]' check "Pod 側の一覧 (pod_files_1499.tsv・pod_checkpoints.tsv) を持ち帰る" '[ $(wc -l < "$TD/pod_files_1499.tsv") = 5 ] && [ -s "$TD/pod_checkpoints.tsv" ]' check "通知は「成功」" 'grep -q "成功: 学習が終わりました (step 1499" "$T/notify.log"' check "lab_start.sh の pod/ には触らない" '[ ! -e "$T/data/lab/LogT/pod" ]' common_wrap OUT1="$T" echo "== 2. 学習が落ちる (終了コード 1)" run crash check "終了コード 1" '[ $RC = 1 ]' check "Hugging Face に上げない・KIOXIA に置かない" '! has ssh.log "hf_upload_ckpt.py" && [ ! -e "$CK" ]' check "わけを出す" 'has out.txt "終了コード 1" && grep -q "^\[NOTIFY\].*失敗: 学習が終わったが, 終了コード 1" "$T/notify.log"' common_wrap rm -rf "$T" echo "== 3. 学習のプロセスが終わりの印を書かずに消える" run killed check "終了コード 1・わけ" '[ $RC = 1 ] && has notify.log "終わりの印を書かずに消えた"' common_wrap rm -rf "$T" echo "== 4. 上限の時間を超える (--limit-hours 0.001 = 3.6 秒)" run timeout --limit-hours 0.001 check "終了コード 1・上限を超えたと出す" '[ $RC = 1 ] && has notify.log "上限 0.001 時間を超えた"' check "学習を止めてからログを持ち帰る" 'has ssh.log ": lt=kill_train" && [ $(line_of ssh.log ": lt=kill_train") -lt $(line_of ssh.log ": lt=ls_all") ]' check "Hugging Face に上げない" '! has ssh.log "hf_upload_ckpt.py"' common_wrap rm -rf "$T" echo "== 5. 学習のログが長く書かれない (止まった)" run stall check "終了コード 1・止まったと出す" '[ $RC = 1 ] && has notify.log "分書かれていない (止まった"' common_wrap rm -rf "$T" echo "== 6. Hugging Face に上げられない → Pod から scp で KIOXIA へ取ってから消す" run hf_fail check "終了コード 1 (全部はそろっていない)" '[ $RC = 1 ]' check "KIOXIA に scp で置く (Pod を消す前)" '[ -f "$CK/1499/_CHECKPOINT_METADATA" ] && grep -q "checkpoints/pi05_mecharm_trim_corr_lora/trim_corr_v3/1499/params" "$T/scp.log" && [ $(line_of out.txt "Pod から scp で取ります") -lt $(line_of out.txt "削除 (Terminate) を送った") ]' check "Mac の hf download はしない" '[ $(n_call "hf download") = 0 ]' check "通知に「Hugging Face に上げられなかった」「KIOXIA には置いた」" 'has notify.log "Hugging Face に上げられなかった" && has notify.log "KIOXIA には置いた (scp)" && has notify.log "Pod は消しました"' common_wrap rm -rf "$T" echo "== 7. Hugging Face から取ったものが Pod 側と合わない" run dl_mismatch check "終了コード 1・1499 は作らず一時フォルダのまま残す" '[ $RC = 1 ] && [ ! -e "$CK/1499" ] && ls -d "$CK"/.incoming_1499_* >/dev/null 2>&1' check "どのファイルが違うかを出す" 'has out.txt "params/manifest.ocdbt 119" && has notify.log "KIOXIA に置けなかった"' rm -rf "$T" echo "== 8. KIOXIA に 1499 がもとからある → 上書きしない" T=$(mktemp -d); mkdir -p "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v1/1499"; echo old > "$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v1/1499/keep.txt" (cd "$HERE" && LAB_DRY_ROOT="$T" LAB_FAKE_SCENARIO=success ./lab_train.sh --log LogT --dry-run < /dev/null > "$T/out.txt" 2>&1); RC=$? CK="$T/data/checkpoints/pi05_mecharm_trim_corr_lora_v1" check "前もって知らせる" 'has out.txt "がもうあります. 学習のあと上書きはせず"' check "もとの中身はそのまま・終了コード 1・一時フォルダも作らない" '[ "$(cat "$CK/1499/keep.txt")" = old ] && [ $RC = 1 ] && [ $(n_call "hf download") = 0 ] && has notify.log "上書きしなかった"' rm -rf "$T" echo "== 9. A40 が借りられない → RTX A6000 で借りる" run gpu_busy check "A40 を 5 回試し, RTX A6000 で作る" '[ $(n_call "POST gpu=NVIDIA A40") = 5 ] && [ $(n_call "POST gpu=NVIDIA RTX A6000") = 1 ] && [ "$(jq -r .gpu "$TD/lab_pod.json")" = "NVIDIA RTX A6000" ]' check "終了コード 0・費用の行に GPU" '[ $RC = 0 ] && has out.txt "GPU=NVIDIA RTX A6000"' rm -rf "$T" echo "== 10. どの GPU も借りられない → 借りずに通知して終わる" run all_busy check "終了コード 1・A40 5 回 + A6000 2 回 + L40S 2 回" '[ $RC = 1 ] && [ $(n_call "POST gpu=NVIDIA A40") = 5 ] && [ $(n_call "POST gpu=NVIDIA RTX A6000") = 2 ] && [ $(n_call "POST gpu=NVIDIA L40S") = 2 ]' check "DELETE しない・通知に「Pod は借りていません」" '[ $(n_call DELETE) = 0 ] && has notify.log "Pod は借りていません"' rm -rf "$T" run all_busy --no-fallback check "--no-fallback は A40 だけ" '[ $(n_call "POST gpu=NVIDIA A40") = 5 ] && [ $(n_call "POST gpu=NVIDIA RTX A6000") = 0 ]' rm -rf "$T" echo "== 11. Pod を消せない → 残っているかもしれないと通知する" run delete_fail check "終了コード 1・--stop の案内" '[ $RC = 1 ] && has notify.log "残っているかもしれません" && has notify.log "--stop"' check "記録は gone_confirmed=false" '[ "$(jq -r .gone_confirmed "$TD/lab_pod.json")" = false ]' rm -rf "$T" echo "== 11b. 1 回目の DELETE だけ失敗 → やり直して消す" run delete_flaky check "終了コード 0・DELETE 2 回・消えたのを確かめた" '[ $RC = 0 ] && [ $(n_call DELETE) = 2 ] && has out.txt "(1/3) Pod を消せない" && [ "$(jq -r .gone_confirmed "$TD/lab_pod.json")" = true ]' rm -rf "$T" echo "== 11c. 上限を超えた直後の確かめで学習が終わっていた → 消さずに持ち帰る (codex R1)" run late_finish --limit-hours 0.002 check "終了コード 0・成功・上限で止めない" '[ $RC = 0 ] && has notify.log "成功: 学習が終わりました" && ! has out.txt "上限 0.002 時間を超えた" && [ -f "$CK/1499/_CHECKPOINT_METADATA" ]' rm -rf "$T" echo "== 11d. Pod 側の見張りが働かない → 続けるが, 画面・まとめで目立たせる" run wd_missing check "終了コード 0・警告・寝てよいの案内に注意・まとめに注意" '[ $RC = 0 ] && has out.txt "Pod 側の見張りが働かないかもしれない (WATCHDOG run=1 pod=1 key=0 ctl=0)" && has out.txt "※ Pod 側の見張りがない" && grep -q "Pod 側の見張りは働いていなかった" "$TD"/result_*.txt' rm -rf "$T" echo "== 11e. Pod 側のチェックポイントの一覧が取れない → 上げずに, ログを持ち帰って消す" run list_empty check "終了コード 1・上げない・DELETE" '[ $RC = 1 ] && ! has ssh.log "hf_upload_ckpt.py" && [ $(n_call DELETE) = 1 ] && has notify.log "一覧を取れない"' rm -rf "$T" echo "== 11f. 見守りに入る前に上限を過ぎた (学習は終わっている) → 消さずに持ち帰る (codex R2)" run late_start --limit-hours 0.002 check "終了コード 0・成功" '[ $RC = 0 ] && has notify.log "成功: 学習が終わりました" && ! has out.txt "時間を超えた"' rm -rf "$T" echo "== 11g. Pod 側の一覧が途中で失敗 (一部の行・終了コード 1) → 合ったとせず, 上げない (codex R2)" run list_partial check "終了コード 1・上げない・一覧を取れない" '[ $RC = 1 ] && ! has ssh.log "hf_upload_ckpt.py" && has notify.log "一覧を取れない"' check "一覧のコマンドは pipefail 付き" 'has ssh.log ": lt=list_ckpt; set -o pipefail;"' rm -rf "$T" echo "== 12. 前の学習の Pod が動いている → 作らずに止まる / --stop で持ち帰って消す" T=$(mktemp -d); mkdir -p "$T/data/lab/LogT/train" "$T/fake" jq -n '{id:"dryrunpod0001", kind:"train", gpu:"NVIDIA A40", costPerHr:0.44, created_epoch:1793449178, created_at:"2026-10-31 21:19:38", exp:"trim_corr_v3", train_started_epoch:1793449300}' > "$T/data/lab/LogT/train/lab_pod.json" echo "NVIDIA A40" > "$T/fake/created" (cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run < /dev/null > "$T/out.txt" 2>&1); RC=$? TD="$T/data/lab/LogT/train" check "終了コード 1・POST しない・--stop の案内" '[ $RC = 1 ] && [ $(n_call POST) = 0 ] && has out.txt "前の学習の Pod (id dryrunpod0001・RUNNING) が残っている" && [ $(n_call DELETE) = 0 ]' (cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run --stop < /dev/null > "$T/out.txt" 2>&1); RC=$? check "--stop: 学習を止め, ログを持ち帰って DELETE" '[ $(n_call DELETE) = 1 ] && has ssh.log ": lt=kill_train" && [ -f "$TD/train_trim_corr_v3.log" ] && has out.txt "--stop で止めた"' check "--stop の出力は lab_train_*_stop.txt に残る" 'ls "$TD"/lab_train_*_stop.txt >/dev/null 2>&1' (cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run < /dev/null > "$T/out2.txt" 2>&1) check "消したあとは前の記録を名前を変えて残し, 新しく作る" '[ $(n_call "POST /pods") = 1 ] && ls "$TD"/lab_pod_*.json >/dev/null 2>&1' rm -rf "$T" echo "== 13. 見守る途中で止められても Pod を残さない (TERM. Ctrl-C・HUP も同じ扱い)" T=$(mktemp -d) (cd "$HERE" && LAB_DRY_ROOT="$T" LAB_FAKE_SCENARIO=timeout LAB_DRY_POLL_SEC=1 ./lab_train.sh --log LogT --dry-run --limit-hours 1 < /dev/null > "$T/out.txt" 2>&1) & bg=$! for _ in $(seq 1 40); do grep -q "ここまで来れば寝て" "$T/out.txt" 2>/dev/null && break; sleep 0.5; done pk=$(pgrep -f "lab_train.sh --log LogT --dry-run --limit-hours 1" | head -1) kill -TERM "$pk" 2>/dev/null # 裏で動かした bash は INT を受けない (始まる前から無視) ので TERM で確かめる. INT も同じ on_signal wait $bg; TD="$T/data/lab/LogT/train" check "止められたら, ログを持ち帰って DELETE・通知" '[ $(n_call DELETE) = 1 ] && [ -f "$TD/train_trim_corr_v3.log" ] && grep -q "TERM (kill) で止めた" "$T/notify.log"' rm -rf "$T" echo "== 14. 引数" for a in "--limit-hours 0" "--limit-hours x" "--steps 0" "--exp-name a;b" "--init /a b"; do T=$(mktemp -d) # shellcheck disable=SC2086 (cd "$HERE" && LAB_DRY_ROOT="$T" ./lab_train.sh --log LogT --dry-run $a < /dev/null > "$T/out.txt" 2>&1); RC=$? check "$a は終了コード 2" '[ $RC = 2 ]' rm -rf "$T" done echo "== 15. Pod の中で流すコマンドを, この Mac の bash で流す" . "$HERE/lab_lib.sh" d=$(mktemp -d); L="$d/train.log"; E="$d/exit"; C="$d/ck" st() { bash -c "$(lab_train_status_cmd "$L" "$E" "$C" 1500)" 2>/dev/null; } check "ログがない: STATUS run=0 exit=- age=-" '[[ "$(st)" == "STATUS run=0 exit=- age=- bar=- logstep=- loss=- ckpt=- cm=0" ]]' printf 'loading\r 5%%|▌ | 75/1500 [08:00<2:30:00, 6.6s/it]\rStep 0: grad_norm=1.2000, loss=0.0800, param_norm=300.1\n 13%%|█▎ | 200/1500 [22:00<2:23:00, 6.6s/it]\rStep 100: grad_norm=0.5000, loss=0.0412, param_norm=300.2\n\r 14%%|█▍ | 210/1500 [23:00<2:22:00, 6.6s/it]' > "$L" mkdir -p "$C/1000" "$C/1499" "$C/1499.orbax-checkpoint-tmp-123"; touch "$C/1499/_CHECKPOINT_METADATA" check "tqdm と Step の行・チェックポイント" '[[ "$(st)" == "STATUS run=0 exit=- age="[0-9]*" bar=210 logstep=100 loss=0.0412 ckpt=1499 cm=1" ]]' echo 0 > "$E" check "終わりの印 → exit=0" '[[ "$(st)" == *" exit=0 "* ]]' lab_train_parse_status "$(st)" check "読み分け (S_EXIT=0 S_BAR=210 S_LOSS=0.0412 S_CKPT=1499 S_CM=1)" '[ "$S_EXIT" = 0 ] && [ "$S_BAR" = 210 ] && [ "$S_LOSS" = 0.0412 ] && [ "$S_CKPT" = 1499 ] && [ "$S_CM" = 1 ]' mkdir -p "$d/gnu"; printf '#!/bin/bash\n[ "$1" = -c ] && [ "$2" = %%Y ] && { echo 1000; exit 0; }; exit 1\n' > "$d/gnu/stat"; chmod +x "$d/gnu/stat" check "Pod (GNU) では stat -c %Y で書いた時刻を読む" '[ $(PATH="$d/gnu:$PATH" st | sed -E "s/.* age=([0-9]+) .*/\1/") -gt 1000000000 ]' printf '#!/bin/bash\nexit 1\n' > "$d/gnu/stat"; mkdir -p "$d/nodate"; printf '#!/bin/bash\n[ "$1" = -r ] && exit 1; exec /bin/date "$@"\n' > "$d/nodate/date"; chmod +x "$d/gnu/stat" "$d/nodate/date" check "書いた時刻が読めなければ age=- (止まったとしない)" '[[ "$(PATH="$d/gnu:$d/nodate:$PATH" st)" == *" age=- "* ]]' check "STATUS の行がなければ読めない" '! lab_train_parse_status "ssh: connect to host"' touch -t 202601010000 "$L" check "古いログ → age が大きい" '[ $(st | sed -E "s/.* age=([0-9]+) .*/\1/") -gt 100000 ]' mkdir -p "$d/x/.cache/huggingface" "$d/x/params" "$d/x/train_state" printf 'abc' > "$d/x/params/a"; printf '12345' > "$d/x/_CHECKPOINT_METADATA"; touch "$d/x/._a" "$d/x/.gitattributes" "$d/x/.DS_Store" "$d/x/.cache/huggingface/m" "$d/x/train_state/t" mkdir -p "$d/x/params/locked"; chmod 000 "$d/x/params/locked" check "Mac の一覧は読めないフォルダがあれば失敗する (黙って飛ばさない)" '! lab_list_files "$d/x" >/dev/null 2>&1' chmod 755 "$d/x/params/locked"; rmdir "$d/x/params/locked" check "Mac の一覧は ._・.cache・.gitattributes・train_state を数えない" '[ "$(lab_list_files "$d/x")" = "$(printf "_CHECKPOINT_METADATA\t5\nparams/a\t3")" ]' # 見張り: 上限の時刻を過ぎたら Pod 用の鍵で DELETE を送る (偽物の curl). 鍵は書き出さない mkdir -p "$d/bin"; printf '#!/bin/bash\nfor a in "$@"; do case "$a" in @/dev/fd/*|@/proc/*) cat "${a#@}" > "%s/hdr" ;; esac; done\necho "$*" > "%s/curl_args"; printf 204\n' "$d" "$d" > "$d/bin/curl"; chmod +x "$d/bin/curl" lab_watchdog_script > "$d/wd.sh" echo $(( $(date +%s) + 2 )) > "$d/deadline" run_to 20 env PATH="$d/bin:$PATH" LAB_WD_DEADLINE="$d/deadline" LAB_WD_LOG="$d/wd.log" LAB_WD_POLL=1 LAB_WD_API=http://fake \ RUNPOD_POD_ID=pod123 RUNPOD_API_KEY=podscoped-SECRET bash "$d/wd.sh"; wrc=$? check "見張り: 時刻まで待ってから DELETE (Pod 用の鍵をヘッダーで)・0 で終わる" '[ $wrc = 0 ] && grep -q -- "-X DELETE .*http://fake/pods/pod123" "$d/curl_args" && grep -q "Bearer podscoped-SECRET" "$d/hdr" && grep -q "HTTP 204" "$d/wd.log"' check "見張り: 鍵をログ・引数に出さない" '! grep -q SECRET "$d/wd.log" "$d/curl_args"' # 1 回目は 500 → やり直して 204 printf '#!/bin/bash\nn=$(cat "%s/cn" 2>/dev/null || echo 0); echo $((n+1)) > "%s/cn"; [ "$n" = 0 ] && printf 500 || printf 204\n' "$d" "$d" > "$d/bin/curl" echo 1 > "$d/deadline"; : > "$d/wd.log" run_to 20 env PATH="$d/bin:$PATH" LAB_WD_DEADLINE="$d/deadline" LAB_WD_LOG="$d/wd.log" LAB_WD_RETRY=0 RUNPOD_POD_ID=p RUNPOD_API_KEY=k bash "$d/wd.sh"; wrc=$? check "見張り: DELETE が一時だめでもやり直して消す" '[ $wrc = 0 ] && grep -q "(1) DELETE → HTTP 500" "$d/wd.log" && grep -q "(2) DELETE → HTTP 204" "$d/wd.log"' printf '#!/bin/bash\nprintf 500\n' > "$d/bin/curl"; : > "$d/wd.log" run_to 20 env PATH="$d/bin:$PATH" LAB_WD_DEADLINE="$d/deadline" LAB_WD_LOG="$d/wd.log" LAB_WD_RETRY=0 LAB_WD_TRIES=3 RUNPOD_POD_ID=p RUNPOD_API_KEY=k bash "$d/wd.sh"; wrc=$? check "見張り: ずっとだめなら 3 回試して「消せなかった」・終了コード 1" '[ $wrc = 1 ] && grep -q "(3) DELETE" "$d/wd.log" && grep -q "消せなかった" "$d/wd.log"' # runpodctl が固まっても, 時間で切って次へ進む (Mac には timeout がないので偽物) printf '#!/bin/bash\ns=$1; shift; "$@" & p=$!; ( sleep "$s"; kill $p 2>/dev/null ) & w=$!; wait $p; r=$?; kill $w 2>/dev/null; exit $r\n' > "$d/bin/timeout" printf '#!/bin/bash\necho call >> "%s/ctl"; sleep 30\n' "$d" > "$d/bin/runpodctl"; chmod +x "$d/bin/timeout" "$d/bin/runpodctl"; : > "$d/wd.log" run_to 25 env PATH="$d/bin:$PATH" LAB_WD_DEADLINE="$d/deadline" LAB_WD_LOG="$d/wd.log" LAB_WD_RETRY=0 LAB_WD_TRIES=2 LAB_WD_CTL_TIMEOUT=1 RUNPOD_POD_ID=p RUNPOD_API_KEY=k bash "$d/wd.sh"; wrc=$? check "見張り: runpodctl が固まっても時間で切ってやり直し, 最後まで行く" '[ $wrc = 1 ] && grep -q "(2) DELETE" "$d/wd.log" && [ $(wc -l < "$d/ctl") = 3 ] && grep -q "消せなかった" "$d/wd.log"' rm -f "$d/bin/timeout" "$d/bin/runpodctl" echo $(( $(date +%s) + 3600 )) > "$d/deadline"; : > "$d/wd.log" run_to 3 env PATH="$d/bin:$PATH" LAB_WD_DEADLINE="$d/deadline" LAB_WD_LOG="$d/wd.log" LAB_WD_POLL=1 RUNPOD_POD_ID=p RUNPOD_API_KEY=k \ bash "$d/wd.sh"; wrc=$? check "見張り: 時刻の前は何もしない (延ばした時刻を読む)" '[ $wrc = 143 ] && [ ! -s "$d/wd.log" ]' bash -n <(lab_watchdog_start_cmd 123) && ok "見張りを起動するコマンドの書き方" || ng "見張りを起動するコマンドの書き方" check "見張りを起動するコマンドは鍵の中身を出さない (有無だけ)" '! lab_watchdog_start_cmd 1 | grep -qE "echo .*\\\$RUNPOD_API_KEY|printenv RUNPOD_API_KEY\$"' rm -rf "$d" echo "== 15b. pgrep / pkill の形が, そのコマンドを流す bash -c 自身に当たらない (依頼 47 の codex R1)" . "$HERE/lab_lib.sh" all_cmds="$(lab_train_start_cmd c e /w/l /w/g /w/r /w/x) $(lab_watchdog_start_cmd 1) $(lab_train_kill_cmd) $(lab_train_status_cmd /w/l /w/x /w/c 1500)" self_hit() { # self_hit 形 → コマンドの中身を引数に持つ bash -c の中から pgrep し, 自分に当たれば 0 bash -c "true <<'X_EOF' $all_cmds X_EOF pgrep -a -f '$1' >/dev/null; r=\$?; exit \$r" 2>/dev/null # Mac の pgrep は自分の親を数えない (-a で数える). Pod (Linux) の pgrep は数えるので, -a で Pod と同じにする. # pgrep を最後に置くと bash が exec で pgrep に置き換わるので, 後ろに 1 つ置く } for pat in "$LAB_RUN_PAT" "$LAB_WD_PAT" "$LAB_GPU_PAT"; do check "「${pat}」は自分に当たらない" '! self_hit "$pat"' done check "(確かめの確かめ) ^ のない形なら自分に当たる" 'self_hit "[n]vidia-smi --query-gpu=timestamp"' check "学習の Pod のコマンドに ^ のない train_run・lab_watchdog・nvidia-smi の pgrep/pkill がない" '! grep -E "(pgrep|pkill)( -9)? -f .\[?(t\]?rain_run|l\]?ab_watchdog|n\]?vidia-smi)" <<<"$all_cmds" | grep -q .' mkdir -p /tmp/lt_$$/r2r2r && LC_ALL=C sed -e 's#/workspace/r2r2r/train_run.sh#/tmp/lt_'$$'/r2r2r/train_run.sh#g' <<<"$(lab_train_start_cmd c e /w/l /w/g /w/r /w/x)" | LC_ALL=C sed -n "/^cat > /,/^RUN_EOF\$/p" | bash bash -n "/tmp/lt_$$/r2r2r/train_run.sh" && ok "Pod に置く train_run.sh の書き方" || ng "Pod に置く train_run.sh の書き方" check "train_run.sh は終了コードを終わりの印に書く・\$HOME を Pod 側で読む" 'grep -q "^echo \$rc > /w/x$" /tmp/lt_$$/r2r2r/train_run.sh && grep -q "source \$HOME/.local/bin/env" /tmp/lt_$$/r2r2r/train_run.sh' rm -rf /tmp/lt_$$ echo "== 16. lab_start.sh が lab_lib.sh の準備のコマンドで同じものを流す" . "$HERE/lab_lib.sh" check "3c は lab_start.sh の時と同じ中身" '[ "$(lab_cmd_ckpt PixelstoMotors/pi05-mecharm-trim-lora-v1 /workspace/ckpt/3999)" = "rm -f /workspace/ckpt/3999/.lab_download_ok && hf download PixelstoMotors/pi05-mecharm-trim-lora-v1 --local-dir /workspace/ckpt/3999 && touch /workspace/ckpt/3999/.lab_download_ok && du -sh /workspace/ckpt/3999" ]' check "3e は \$HOME を Pod 側で読む" '[[ "$(lab_cmd_uv_sync)" == *"source \$HOME/.local/bin/env"* ]]' rm -rf "$OUT1" echo echo "OK $OK / NG $NG" [ $NG = 0 ]