2026-10-17 15:38:00 +00:00
#!/usr/bin/env bash
# 一条龙:bash ml/pipeline.sh <阶段> ;阶段按顺序:
# setup 安装 Python/Node 依赖(torch 需先按 CUDA 版本装好)
# kin 导出运动学/碰撞模型 + JS↔PyTorch 对拍(必须 PASS)
2026-10-17 18:57:48 +00:00
# r4 浏览器推理延迟(随机初始化模型导出到 ml/runs/r4/,无头 Edge/Chrome 自动测 → ml/runs/r4.json;不过就停,改模型规模)
2026-10-17 15:38:00 +00:00
# gate1 老师一致性门禁(决定生成时是否加 --consensus 4)
2026-10-17 16:35:52 +00:00
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
# prep 打包训练样本 + R2 标签一致性检查
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
2026-10-17 18:57:48 +00:00
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型,不过自动训 WTA 头再判
2026-10-17 16:35:52 +00:00
# apg APG 首轮微调(从选定模型开始)
2026-10-17 15:38:00 +00:00
# mine 挖错循环(3–6 轮,含门禁回滚)
2026-10-17 16:35:52 +00:00
# export 导出 ONNX( fp32)到 webapp/models/
2026-10-17 20:28:00 +00:00
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)+ 查看器里同一批贴柱题的整次规划耗时(R9 → ml/runs/r9_browser.json)
2026-10-17 16:35:52 +00:00
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
2026-10-17 18:57:48 +00:00
# all 一键无人值守 kin → accept:已完成阶段自动跳过(断了再跑一次即续跑)、阶段超时、门槛自动分支,并在后台打开训练监控
2026-10-17 20:28:00 +00:00
# selftest 本机自检约 5 分钟(Windows 进程生命周期 + 断点续跑),写 ml/runs/selftest.json( bash ml/selftest.sh)
# smoke 全流程冒烟约 40–70 分钟:在仓库旁的副本里用极小模型把 kin → accept 跑通,并出 2000 条任务的数据统计,写 ml/runs/smoke.json( bash ml/smoke.sh)
2026-10-17 17:15:16 +00:00
# monitor 训练控制台网页 http://localhost:8100:点「开始训练」一键跑全流程、看进度、可停止(也可双击 ml/console.bat / ml/console.sh)
2026-10-17 16:35:52 +00:00
# 可调环境变量:PYTHON(默认 python; Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
# CFG( ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
2026-10-17 18:57:48 +00:00
# 可调(第五轮新增):FRESH=1 从头重跑(旧产物移到 ml/archive/<时间>/,不删;改了生成/模型/挖错参数时只作废受影响的阶段);MIN_GAP_MM(默认 3, gen/prep 共用);
# R2_STRICT=1 R2 不过即停;R4_STRICT=0 R4 不过也继续;AUTO_WTA=0 M5 不过不自动训 WTA; TIMEOUT_SCALE 阶段超时倍数
2026-10-17 16:35:52 +00:00
# 2026-10-18:按另一台机器的审查改——头分目录不互相覆盖、统一 $PYTHON( Windows Git Bash 可跑)、
# gen 自动补量、m5 双口径、export 默认 fp32、新增 accept。
2026-10-17 18:57:48 +00:00
# 2026-10-18 第五轮(审查第四轮 N-7/N-10/N-11/N-16 与「无人值守跑完」四个缺口):
# 互斥锁(任何入口同一把)、`all` 自动续跑已完成阶段、阶段超时、R2 默认警告继续、M5 不过自动转 WTA、
# r4 自动探针(独立文件,不碰 webapp/models)、启动前自检 torch。
set -e -o pipefail # pipefail: `node ... | tee` 里 node 失败也算失败
2026-10-17 15:38:00 +00:00
cd " $( dirname " $0 " ) /.."
2026-10-17 20:28:00 +00:00
# 2026-10-18 第六轮:所有 Python 子进程按 UTF-8 读写文件(Windows 默认 GBK,读 ensure_ascii=False 写出的中文 json 会 UnicodeDecodeError,
# 第六轮本机实测 :110 即此因;逐处补 encoding 容易漏,统一用 UTF-8 模式兜住)
export PYTHONUTF8 = 1 PYTHONIOENCODING = utf-8
2026-10-17 16:35:52 +00:00
PY = ${ PYTHON :- python }
command -v " $PY " >/dev/null 2>& 1 && " $PY " -c "import sys" 2>/dev/null || { echo "找不到可用的 Python( $PY ),请设 PYTHON=..." ; exit 1; }
export PYTHON = $PY
2026-10-17 18:57:48 +00:00
export NPROC = ${ NPROC :-$( nproc) }
export N_TASKS = ${ N_TASKS :- 50000 }
export TARGET_SAMPLES = ${ TARGET_SAMPLES :- 1700000 }
export CFG = ${ CFG :- ml /configs/base.json }
export MIN_GAP_MM = ${ MIN_GAP_MM :- 3 } # 老师轨迹认证间隙下限:stats.cjs(补量口径)与 build_dataset.cjs(训练集)必须同值(审查 N-1)
export ROUNDS = ${ ROUNDS :- 6 } APG_STEPS = ${ APG_STEPS :- 1000 } MINE_N = ${ MINE_N :- 50000 }
2026-10-17 16:35:52 +00:00
TAG = $( basename " $CFG " .json)
SEL = ml/runs/selected_ckpt
ckpt() { if [ -n " ${ CKPT :- } " ] ; then echo " $CKPT " ; elif [ -f $SEL ] ; then cat $SEL ; else echo ml/runs/$TAG /best.pt; fi ; }
2026-10-17 15:38:00 +00:00
stage = ${ 1 :- help }
2026-10-18 07:23:29 +08:00
run() { echo "== $* " >& 2; " $@ " ; } # 回显走 stderr: gate1 用 tee 把 stdout 存成 consistency.json,回显混进去会让后面读它时 JSONDecodeError
2026-10-17 18:57:48 +00:00
ALL_STAGES = "setup kin r4 gate1 gen prep train m5 apg mine export bench accept"
2026-10-17 17:15:16 +00:00
stage_info() { case $1 in
gen) echo "target_tasks= $(( ( N_TASKS + NPROC - 1 ) / NPROC * NPROC )) " ;;
train) echo "tag= $TAG bench_steps=2000" ;;
m5) echo "tag= $TAG " ;;
2026-10-17 18:57:48 +00:00
mine) echo "rounds= $ROUNDS apg_steps= $APG_STEPS " ;;
2026-10-17 17:15:16 +00:00
esac ; }
2026-10-17 18:57:48 +00:00
# ---- 互斥锁(审查 N-7):网页、终端、`all` 同一把锁;`all` 内部调用的子阶段继承(PIPE_LOCKED)
OWN_LOCK = 0; BEAT = ; IN_STAGE = 0
case " $ALL_STAGES all " in *" $stage " *)
if [ -z " ${ PIPE_LOCKED :- } " ] ; then
# Windows Git Bash 的 $$ 是 msys pid, taskkill 要 Windows pid( /proc/$$/winpid)
$PY -m ml.pstate lock acquire " ${ PIPE_OWNER :- 终端 } : $stage " " $( cat /proc/$$ /winpid 2>/dev/null || echo $$ ) " || exit 9
export PIPE_LOCKED = 1; OWN_LOCK = 1
PARENT = $$
( while sleep 60; do kill -0 $PARENT 2>/dev/null || exit 0; $PY -m ml.pstate lock beat >/dev/null 2>& 1 || true; done ) &
BEAT = $!
fi ;;
2026-10-17 17:15:16 +00:00
esac
2026-10-17 18:57:48 +00:00
case " $ALL_STAGES " in *" $stage " *)
$PY -m ml.pstate begin $stage $( stage_info $stage ) ; IN_STAGE = 1 ;;
esac
on_exit() {
rc = $?
[ $IN_STAGE = 1 ] && { $PY -m ml.pstate end $stage $rc >/dev/null 2>& 1 || true; }
if [ $OWN_LOCK = 1 ] ; then [ -n " $BEAT " ] && kill $BEAT 2>/dev/null; $PY -m ml.pstate lock release || true; fi
exit $rc
}
trap on_exit EXIT
trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返回码、放锁
# ---- 训练与门禁的单元(m5 自动分支要对两种头各调一次)
train_one() { # $1 = 配置文件
local cfg = $1 tag; tag = $( basename " $1 " .json)
if [ -f ml/runs/bench_$tag /last.pt ] ; then echo "吞吐摸底 ml/runs/bench_ $tag 已有,跳过"
2026-10-17 20:28:00 +00:00
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps ${ BENCH_STEPS :- 2000 } --no-eval --config $cfg ; fi
2026-10-17 18:57:48 +00:00
local res = "" ; [ -f ml/runs/$tag /last.pt ] && { res = "--resume ml/runs/ $tag /last.pt" ; echo "发现 ml/runs/ $tag /last.pt → 续训" ; }
run $PY -m ml.train --samples ml/data/samples --out ml/runs/$tag --config $cfg $res
}
m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过
local tag = $1 ck = ${ CKPT :- ml /runs/ $1 /best.pt } o
[ -f " $ck " ] || { echo "M5: $ck 不存在(训练发散或没训完)→ 按不通过处理" ; return 3; }
# 断点续跑:结果文件比检查点新 → 这一半已经评过,跳过
2026-10-17 20:28:00 +00:00
o = ml/runs/m5_${ tag } _policy.json; [ -f $o ] && [ $o -nt " $ck " ] && echo "M5: $o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${ M5_N :- 1000 } --out $o
o = ml/runs/m5_${ tag } _safety.json; [ -f $o ] && [ $o -nt " $ck " ] && echo "M5: $o 已有,跳过" || run $PY -m ml.sim.closed_loop --ckpt $ck --tasks ml/runs/nc_gate.jsonl --n ${ M5_N :- 1000 } --safety --out $o
2026-10-17 18:57:48 +00:00
$PY - " $tag " " $ck " <<'PY'
2026-10-17 16:35:52 +00:00
import json, sys
tag, ck = sys.argv[1], sys.argv[2]
2026-10-18 03:21:53 +08:00
p = json.load(open(f'ml/runs/m5_{tag}_policy.json', encoding='utf-8')); s = json.load(open(f'ml/runs/m5_{tag}_safety.json', encoding='utf-8'))
2026-10-17 16:35:52 +00:00
ok = p['collision_rate'] <= 0.02
out = {'head': tag, 'ckpt': ck, 'pass': ok,
'policy_only': {k: p[k] for k in ('n', 'success', 'collision_rate', 'start_fail')},
'with_sim_safety': {k: s[k] for k in ('n', 'success', 'collision_rate', 'brakes_mean', 'start_fail')},
'note': '门禁只看 policy_only(纯策略穿柱率);with_sim_safety 为仿真近似安全层,部署口径以 bench 为准'}
json.dump(out, open(f'ml/runs/m5_gate_{tag}.json', 'w'), indent=1)
print('[%s] 纯策略穿柱率 %.2f%%、到达 %.2f%%;加仿真安全层 穿柱 %.2f%%、到达 %.2f%%' % (
tag, 100 * p['collision_rate'], 100 * p['success'], 100 * s['collision_rate'], 100 * s['success']))
2026-10-17 18:57:48 +00:00
if ok: open('ml/runs/selected_ckpt', 'w').write(ck); print('M5 门禁:通过 → 后续阶段使用', ck)
sys.exit(0 if ok else 3)
2026-10-17 15:38:00 +00:00
PY
2026-10-17 18:57:48 +00:00
}
case $stage in
2026-10-17 20:28:00 +00:00
setup) run $PY -m pip install -r ml/requirements.txt; ( cd ml && npm install --no-audit --no-fund) ; run bash ml/prep/vendor_ort.sh ;;
2026-10-17 18:57:48 +00:00
kin) run node ml/prep/export_kin.cjs; run node ml/eval/dump_parity.cjs 2000; run $PY -m ml.eval.check_parity ;;
r4) # 浏览器推理延迟(方案 R4):随机初始化模型导出到独立文件(不碰 webapp/models,审查 N-5),无头浏览器自动测
mkdir -p ml/runs/r4
2026-10-17 22:12:53 +00:00
[ -f webapp/vendor/ort/ort.min.js ] || [ " ${ ORT_LOCAL :- 1 } " = 0 ] || { echo "webapp/vendor/ort 缺失(ort 已入库,先 git pull);确要用 CDN 设 ORT_LOCAL=0" ; exit 3; } # 第八轮:不再静默退回 CDN
2026-10-17 18:57:48 +00:00
run $PY -m ml.export_onnx --random $CFG --out ml/runs/r4/traj_policy_r4.onnx
2026-10-17 20:28:00 +00:00
run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json ${ R4_ARGS :- }
2026-10-18 03:21:53 +08:00
r = $( $PY -c "import json;p=json.load(open('ml/runs/r4.json', encoding='utf-8')).get('pass');print({True:'pass',False:'fail'}.get(p,'null'))" )
2026-10-17 18:57:48 +00:00
if [ " $r " = fail ] && [ " ${ R4_STRICT :- 1 } " = 1 ] ; then
echo "R4 不通过(见 ml/runs/r4.json):部署延迟超标,先别花几十小时训练这个规模的模型。按方案改 CFG=ml/configs/small.json( d=128)重跑;确认要继续设 R4_STRICT=0" ; exit 5
fi
2026-10-18 03:21:53 +08:00
if [ " $r " = null ] ; then echo "R4 未判定(没找到浏览器或页面未回报),继续;验收汇总会标红" ; fi ;;
2026-10-17 18:57:48 +00:00
gate1) mkdir -p ml/data; run node ml/gen/consistency.cjs 300 4242 | tee ml/data/consistency.json.tmp; mv ml/data/consistency.json.tmp ml/data/consistency.json ;; # 中断时不留残缺结果
gen) per = $(( ( N_TASKS + NPROC - 1 ) / NPROC ))
if [ -z " ${ CONSENSUS :- } " ] && [ -f ml/data/consistency.json ] ; then
2026-10-18 03:21:53 +08:00
CONSENSUS = $( $PY -c "import json;print(1 if json.load(open('ml/data/consistency.json', encoding='utf-8'))['consistentByGateRule']<0.9 else 0)" )
2026-10-17 18:57:48 +00:00
echo "一致性门禁结果 → CONSENSUS= $CONSENSUS "
fi
extra = "" ; [ " ${ CONSENSUS :- 0 } " = "1" ] && extra = "--consensus 4"
run bash ml/gen/run_parallel.sh $per $NPROC 1 " $extra "
# 补量:共识过滤与间隙过滤都会降低保留率,样本不足 TARGET_SAMPLES 时按实测"样本/任务"补生成。
# 最多 4 次(第五轮由 2 改 4:两种过滤叠加时 2 次可能补不满);已写完的补量 shard 续跑时跳过。
s0 = $(( 1 + NPROC))
for k in 1 2 3 4; do
2026-10-18 03:21:53 +08:00
need = $( $PY -c "import json,math;s=json.load(open('ml/data/stats.json', encoding='utf-8'));d= $TARGET_SAMPLES -s['samplesEvery3'];print(0 if d<=0 else math.ceil(d/(s['samplesEvery3']/max(1,s['tasks']))*1.05))" )
2026-10-17 18:57:48 +00:00
[ " $need " = "0" ] && break
echo "样本不足目标 $TARGET_SAMPLES ,补生成 $need 条任务(第 $k 次)"
per2 = $(( ( need + NPROC - 1 ) / NPROC ))
old = $( $PY -m ml.pstate get gen target_tasks) # 读不到直接失败(审查 N-13:此前空值会被算成 56 这类数)
$PY -m ml.pstate set gen target_tasks = $(( old + per2 * NPROC ))
run bash ml/gen/run_parallel.sh $per2 $NPROC $s0 " $extra "
s0 = $(( s0 + NPROC))
done
2026-10-18 03:21:53 +08:00
$PY -c "import json;s=json.load(open('ml/data/stats.json', encoding='utf-8'));import sys;sys.exit(0 if s['samplesEvery3']>=0.9* $TARGET_SAMPLES else 7)" || echo "警告:补 4 次后样本仍不足目标的 90%,按现有样本继续(stats.json 见 samplesEvery3) " ;;
2026-10-17 18:57:48 +00:00
prep) # 间隙过滤口径必须与生成时一致(审查 N-1)
2026-10-18 03:21:53 +08:00
$PY -c "import json,sys;m=json.load(open('ml/data/stats.json', encoding='utf-8')).get('minGapFilterMm');sys.exit(0 if m is None or float(m)==float( $MIN_GAP_MM ) else 8)" \
2026-10-17 18:57:48 +00:00
|| { echo "MIN_GAP_MM= $MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep" ; exit 8; }
# 已打包完的 shard 跳过(断点续跑,见 run_prep.sh);参数变了由 pstate reset 把旧 samples 移到 ml/archive/
run bash ml/prep/run_prep.sh $NPROC
2026-10-17 20:28:00 +00:00
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json ${ R2_ARGS :- }
2026-10-17 18:57:48 +00:00
# 第五轮(审查 N-2):R2 判据与搜索池大小有关、阈值未经实测校准 → 默认只警告继续,R2_STRICT=1 才拦
2026-10-18 03:21:53 +08:00
if ! $PY -c "import json,sys;sys.exit(4 if json.load(open('ml/data/r2.json', encoding='utf-8')).get('pass') is False else 0)" ; then
2026-10-17 18:57:48 +00:00
if [ " ${ R2_STRICT :- 0 } " = 1 ] ; then echo "R2 不通过(R2_STRICT=1):停在这里,把 ml/data/r2.json 发我" ; exit 4; fi
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
fi ;;
train) train_one $CFG ;;
2026-10-17 20:28:00 +00:00
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate ${ M5_N :- 1000 } --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
2026-10-17 18:57:48 +00:00
if m5_one $TAG ; then :
elif [ " $TAG " != wta ] && [ " ${ AUTO_WTA :- 1 } " = 1 ] && [ -z " ${ CKPT :- } " ] ; then
# 第五轮(缺口 3):base 头不过 → 流程内自动训 WTA 头再门禁一次,两种头都不过才停
echo "M5: $TAG 头不通过 → 自动训练 WTA 回退头(约 3 小时)"
$PY -m ml.pstate set m5 branch = wta
train_one ml/configs/wta.json
m5_one wta || { echo "M5:两种头都未通过,把 ml/runs/m5_gate_*.json 发我" ; exit 3; }
2026-10-17 20:28:00 +00:00
elif [ " ${ M5_STRICT :- 1 } " = 0 ] ; then echo "M5:不通过,M5_STRICT=0(冒烟)→ 照常继续" ; echo ml/runs/$TAG /best.pt > $SEL
2026-10-17 18:57:48 +00:00
else echo "M5:不通过,把 ml/runs/m5_gate_*.json 发我" ; exit 3; fi ;;
apg) ck = $( ckpt) ; res = "" ; [ -f ml/runs/apg/last.pt ] && { res = "--resume ml/runs/apg/last.pt" ; echo "发现 ml/runs/apg/last.pt → 续跑" ; }
2026-10-17 20:28:00 +00:00
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg ${ APG_FIRST_STEPS :+--steps $APG_FIRST_STEPS } $res ; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
2026-10-17 18:57:48 +00:00
mine) run bash ml/mining/loop.sh $ROUNDS $APG_STEPS $MINE_N ;;
2026-10-17 17:15:16 +00:00
monitor) run $PY -m ml.monitor ${ MONITOR_ARGS :- } ;;
2026-10-17 20:28:00 +00:00
selftest) run bash ml/selftest.sh ;;
smoke) run bash ml/smoke.sh ;;
2026-10-17 16:35:52 +00:00
export ) ck = ml/runs/current.pt; [ -f $ck ] || ck = $( ckpt)
run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;;
2026-10-17 20:28:00 +00:00
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 ${ BENCH_ARGS :- --nc1000 } --out ml/runs/policy_bench.json
# R9(第六轮,审查 B-1/B-2):查看器里跑同一批 30 道贴柱题,整次规划 P99 < 45 ms、饥饿节拍 < 1%;结果由 accept 判定,这里不拦
run $PY -m ml.eval.r4_probe --viewer --model webapp/models/traj_policy.onnx --out ml/runs/r9_browser.json --viewer-n ${ VIEWER_N :- 30 } ;;
2026-10-17 16:35:52 +00:00
accept) ck = ml/runs/current.pt; [ -f $ck ] || ck = $( ckpt)
run $PY -m ml.eval.acceptance --ckpt $ck --samples ml/data/samples --out ml/runs/acceptance.json ;;
2026-10-17 18:57:48 +00:00
all) # 默认 kin → accept;已完成(rc=0)的阶段自动跳过,从第一个没完成的继续。FRESH=1 从头来;FROM/TO 可只跑一段
2026-10-17 17:15:16 +00:00
FROM = ${ FROM :- kin } ; TO = ${ TO :- accept } ; PLAN = "" ; on = 0
2026-10-17 18:57:48 +00:00
for s in $ALL_STAGES ; do
2026-10-17 17:15:16 +00:00
[ " $s " = " $FROM " ] && on = 1; [ $on = 1 ] && PLAN = " $PLAN $s " ; [ " $s " = " $TO " ] && break
done
PLAN = ${ PLAN # } ; [ -n " $PLAN " ] || { echo "FROM= $FROM / TO= $TO 不是有效阶段" ; exit 1; }
2026-10-17 18:57:48 +00:00
# 启动前自检(审查 N-10:不能等到 train 才发现这个 Python 没有 torch)
$PY -c "import torch" 2>/dev/null || { echo "当前 Python( $( $PY -c 'import sys;print(sys.executable)' ) )没有 torch:请先激活训练环境或设 PYTHON=..." ; exit 1; }
command -v node >/dev/null || { echo "找不到 node" ; exit 1; }
fl = "" ; [ " ${ FRESH :- 0 } " = 1 ] && fl = "--fresh"
$PY -m ml.pstate reset ${ PLAN // /, } $fl # 续跑 / 参数变了作废受影响阶段(旧产物移到 ml/archive/<时间>/)
TODO = $( $PY -m ml.pstate todo)
echo "计划: $PLAN " ; echo "待跑: ${ TODO :- (全部已完成) } "
2026-10-17 17:15:16 +00:00
# 监控网页随 all 自动在后台启动(MONITOR=0 关闭);端口已被占用说明已经开着,不重复启动
if [ " ${ MONITOR :- 1 } " = "1" ] && $PY -c "import socket,sys;s=socket.socket();sys.exit(0 if s.connect_ex(('127.0.0.1',8100)) else 1)" ; then
mkdir -p ml/runs; $PY -m ml.monitor ${ MONITOR_ARGS :- } > ml/runs/monitor.log 2>& 1 &
2026-10-17 18:57:48 +00:00
echo "训练监控已在后台启动:http://localhost:8100/"
2026-10-17 17:15:16 +00:00
fi
2026-10-17 18:57:48 +00:00
for s in $TODO ; do
to = $( $PY -m ml.pstate timeout $s )
if command -v timeout >/dev/null; then
# 第五轮(缺口 4):每阶段超时 = 估时 ×3;超时整组进程收到 TERM,阶段记失败、锁释放
timeout -k 120 $to bash ml/pipeline.sh $s || { rc = $? ; [ $rc = 124 ] && { echo "阶段 $s 超过 $(( to/60)) 分钟上限,判定卡死" ; $PY -m ml.pstate set $s timed_out = 1; } ; exit $rc ; }
else bash ml/pipeline.sh $s ; fi
done
echo "全流程完成:ml/runs/acceptance.json" ;;
*) sed -n '2,30p' " $0 " ;;
2026-10-17 15:38:00 +00:00
esac