Files
Robot_Arm/ml/pipeline.sh
T

195 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 一条龙:bash ml/pipeline.sh <阶段> ;阶段按顺序:
# setup 安装 Python/Node 依赖(torch 需先按 CUDA 版本装好)
# kin 导出运动学/碰撞模型 + JS↔PyTorch 对拍(必须 PASS)
# r4 浏览器推理延迟(随机初始化模型导出到 ml/runs/r4/,无头 Edge/Chrome 自动测 → ml/runs/r4.json;不过就停,改模型规模)
# gate1 老师一致性门禁(决定生成时是否加 --consensus 4)
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
# prep 打包训练样本 + R2 标签一致性检查
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型,不过自动训 WTA 头再判
# apg APG 首轮微调(从选定模型开始)
# mine 挖错循环(3–6 轮,含门禁回滚)
# export 导出 ONNX(fp32)到 webapp/models/
# bench 部署形态评测(Node,策略 + 安全层,含 R8 对抗集)
# accept 验收汇总(方案 §9.2 各项 + R2/R7/R8/R10)→ ml/runs/acceptance.json
# all 一键无人值守 kin → accept:已完成阶段自动跳过(断了再跑一次即续跑)、阶段超时、门槛自动分支,并在后台打开训练监控
# monitor 训练控制台网页 http://localhost:8100:点「开始训练」一键跑全流程、看进度、可停止(也可双击 ml/console.bat / ml/console.sh)
# 可调环境变量:PYTHON(默认 python;Linux 无 python 时设 PYTHON=python3)、NPROC、N_TASKS、TARGET_SAMPLES、
# CFG(ml/configs/base.json | wta.json)、CKPT(覆盖后续阶段的起点模型)、CONSENSUS
# 可调(第五轮新增):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-18:按另一台机器的审查改——头分目录不互相覆盖、统一 $PYTHON(Windows Git Bash 可跑)、
# gen 自动补量、m5 双口径、export 默认 fp32、新增 accept。
# 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 失败也算失败
cd "$(dirname "$0")/.."
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
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}
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; }
stage=${1:-help}
run() { echo "== $*"; "$@"; }
ALL_STAGES="setup kin r4 gate1 gen prep train m5 apg mine export bench accept"
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" ;;
mine) echo "rounds=$ROUNDS apg_steps=$APG_STEPS" ;;
esac; }
# ---- 互斥锁(审查 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 ;;
esac
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 已有,跳过"
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps 2000 --no-eval --config $cfg; fi
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; }
# 断点续跑:结果文件比检查点新 → 这一半已经评过,跳过
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 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 1000 --safety --out $o
$PY - "$tag" "$ck" <<'PY'
import json, sys
tag, ck = sys.argv[1], sys.argv[2]
p = json.load(open(f'ml/runs/m5_{tag}_policy.json')); s = json.load(open(f'ml/runs/m5_{tag}_safety.json'))
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']))
if ok: open('ml/runs/selected_ckpt', 'w').write(ck); print('M5 门禁:通过 → 后续阶段使用', ck)
sys.exit(0 if ok else 3)
PY
}
case $stage in
setup) run $PY -m pip install -r ml/requirements.txt; (cd ml && npm install --no-audit --no-fund) ;;
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
run $PY -m ml.export_onnx --random $CFG --out ml/runs/r4/traj_policy_r4.onnx
run $PY -m ml.eval.r4_probe --model ml/runs/r4/traj_policy_r4.onnx --out ml/runs/r4.json
r=$($PY -c "import json;p=json.load(open('ml/runs/r4.json')).get('pass');print({True:'pass',False:'fail'}.get(p,'null'))")
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
[ "$r" = null ] && echo "R4 未判定(没找到浏览器或页面未回报),继续;验收汇总会标红" ;;
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
CONSENSUS=$($PY -c "import json;print(1 if json.load(open('ml/data/consistency.json'))['consistentByGateRule']<0.9 else 0)")
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
need=$($PY -c "import json,math;s=json.load(open('ml/data/stats.json'));d=$TARGET_SAMPLES-s['samplesEvery3'];print(0 if d<=0 else math.ceil(d/(s['samplesEvery3']/max(1,s['tasks']))*1.05))")
[ "$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
$PY -c "import json;s=json.load(open('ml/data/stats.json'));import sys;sys.exit(0 if s['samplesEvery3']>=0.9*$TARGET_SAMPLES else 7)" || echo "警告:补 4 次后样本仍不足目标的 90%,按现有样本继续(stats.json 见 samplesEvery3)" ;;
prep) # 间隙过滤口径必须与生成时一致(审查 N-1)
$PY -c "import json,sys;m=json.load(open('ml/data/stats.json')).get('minGapFilterMm');sys.exit(0 if m is None or float(m)==float($MIN_GAP_MM) else 8)" \
|| { 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
run $PY -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json
# 第五轮(审查 N-2):R2 判据与搜索池大小有关、阈值未经实测校准 → 默认只警告继续,R2_STRICT=1 才拦
if ! $PY -c "import json,sys;sys.exit(4 if json.load(open('ml/data/r2.json')).get('pass') is False else 0)"; then
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 ;;
m5) [ -f ml/runs/nc_gate.jsonl ] || run $PY -m ml.mining.mine_round --make-gate 1000 --seed 4321 --mix 0,0,1,0 --name nc_gate.jsonl --out ml/runs
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; }
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 → 续跑"; }
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples --out ml/runs/apg $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
mine) run bash ml/mining/loop.sh $ROUNDS $APG_STEPS $MINE_N ;;
monitor) run $PY -m ml.monitor ${MONITOR_ARGS:-} ;;
export) ck=ml/runs/current.pt; [ -f $ck ] || ck=$(ckpt)
run $PY -m ml.export_onnx --ckpt $ck --out webapp/models/traj_policy.onnx ;;
bench) run node tests/policy_bench.cjs webapp/models/traj_policy.onnx nc,route,r8 --nc1000 --out ml/runs/policy_bench.json ;;
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 ;;
all) # 默认 kin → accept;已完成(rc=0)的阶段自动跳过,从第一个没完成的继续。FRESH=1 从头来;FROM/TO 可只跑一段
FROM=${FROM:-kin}; TO=${TO:-accept}; PLAN=""; on=0
for s in $ALL_STAGES; do
[ "$s" = "$FROM" ] && on=1; [ $on = 1 ] && PLAN="$PLAN $s"; [ "$s" = "$TO" ] && break
done
PLAN=${PLAN# }; [ -n "$PLAN" ] || { echo "FROM=$FROM / TO=$TO 不是有效阶段"; exit 1; }
# 启动前自检(审查 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:-(全部已完成)}"
# 监控网页随 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 &
echo "训练监控已在后台启动:http://localhost:8100/"
fi
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" ;;
esac