Author SHA1 Message Date
cbb ee7fe829ac fix+exp: 轮次35(补)——gaps/gm_groups 拒绝行数不符的 xf;撤回两次不存在的"合并调用"收益
- 探测脚本把认证点当成与特征无关的已知量,据此测出的 共享cum_xf −4.3ms / 合并一次调用 −8.1ms
  都不成立:真实顺序是 特征(q)→推理→候选→认证点,同一次规划内既不能共享 xf 也不能合并 gaps
  只有轮次34 已落地的 gm_groups 复用同姿态 xf(−3.4ms,22项指标逐位不变)是真的
- 守卫:xf 行数与 q 不符时抛错(之前会静默按别的行做宽相位,实测触发 CUDA device-side assert)
- 测试 34→35 项;check_parity 仍 PASS(2000样本,间隙差 2.52e-13 mm、碰撞布尔差 0、特征差 5.96e-08)
2026-10-20 21:06:35 +08:00
cbb 296cd17f98 exp: 轮次35——FP16/BF16与流水线复用都被实测否掉,f32认证需先抬eps到2e-4
- 显卡精度档(B=1,K=8,steps=1):推理 FP32 7.68 / FP16 8.31 / BF16 8.58 ms,整周期不变而FP16 P99 46.6→55.6;
  动作偏差 FP16 0.0050°、BF16 0.0377° ⇒ 该批量下不是算力受限,保持 FP32
- float32 认证:48段×9点×15组对的间隙差 max 1.32e-4 mm(有1例高估)→ 现 eps=1e-4 已被越界,要用需 ≥2e-4;
  判据结果两边一致(各24接受、0 仅f32安全、0 符号翻转),暂不放宽 dtype 守卫
- 流水线复用特征间隙:第一版42.1→31.8ms 但等价性检查不过(我取缓存行索引取错,差3.19mm);
  修正后只剩 −3.1ms 且缓存行与新算差 6.3e-5mm(不同 batch 的结合序)⇒ 放弃
- 顺带修掉轮次34日志里重复的章节标题
2026-10-20 21:01:04 +08:00
cbb 34019c1a34 perf+exp: 轮次34——复用cum_xf进库(22项指标逐位不变)、批量认证压最坏尾部(80→37ms)、FP16/BF16实测无收益
- 撤回一条自己的假收益:先前"合并几何查询→24.5ms"不成立。①第一版把 xf= 传给含5行的 gaps,
  只有第0行走完宽相位,认证点全部退回截顶40.0(少干活);②真实顺序 特征→推理→候选→认证,
  认证点依赖网络输出,故"特征间隙与认证点合并一次调用"实现上做不到
- 可实现并已落地:gm_groups(..., xf=) 复用 features 已算的 cum_xf,B=1 交错实测 P50 −3.4ms;
  256题重跑 22 项指标与轮次33 base 逐项相同
- 新增 --cert_batch(默认0不变):K条候选并成1次 gaps 调用。合成夹具(B=2)断言选择/轨迹/证书逐位一致,
  变异检查(换回错误索引)2项测试立即失败;256题重跑同样 22 项指标逐位相同
  实测交换:典型 33.5→37.6ms,最坏(前面候选被拒)80.2→37.1ms,P99 64.7→48.3ms
- FP16/BF16 实测不比 FP32 快(推理 7.68→8.31→8.58ms),整周期不变而 P99 更差,动作偏差 0.005°/0.038°
  → 小批量下不是算力受限,半精度不划算,推理保持 FP32
- 仍不达 20ms:下一刀是流水线复用(下次规划的特征间隙=本次已认证的末点),可整块省掉特征 ~13ms
- 测试 32→34 项
2026-10-20 20:53:43 +08:00
cbb 48cc566c3f exp: 轮次33——限幅实测要付5.1pp到达(57.4→52.3%);GPU真实周期36.7ms≈27Hz,瓶颈是重复几何而非推理
- 四组单变量对照(同256题同seed):base / inc_cap / gap_margin 0.5 / 两者同开
  限幅后 到达 0.5742→0.5234、time_ok 0.3906→0.2656、2s误差比 0.210→0.279、超VMAX 1.0→0.0、刹停几乎不变
  → 之前那部分到达率是超速换来的;损失在逼近末段,不在刹停
- 0.5mm 余量门只付 3 题(0.5742→0.5625);但 plans['gaps'] 在 0.5mm 截顶,5mm 护栏可达性需抬 cap 重算
- 前提更正:eval_cl 不传 steps、rollout 默认 steps=1(policy.py:156 一步生成),先前按 steps=16 得到的
  "当前配方 116ms≈8.6Hz" 作废;真实配方 36.68ms P50 / 49.17ms P99 ≈ 27.3Hz
- 归因更正:空算子 0.028ms、空 sync 0.005ms、encode 2.27ms → 不是启动/同步地板;
  单步推理仅 7.2ms 且 K=1→8 免费,周期里 ~27ms 是 cum_xf+宽相位的重复计算
- certify_uniform 挂上后 36.7→45.3ms(n=8)/49.6ms(n=16)
- kin_torch 补 a/b 形状守卫(脚本里广播不一致会算错 view),测试 30→31 项
2026-10-20 20:32:19 +08:00
cbb 6ecd33db0c exp: 轮次33(上)——实际执行速度做成一等指标并自证;默认路径逐位复现第30轮,256题全部超VMAX
- rollout 逐题记录 max|Δq|/STEP(1.0=VMAX)与每步速度变化比;eval_cl 汇总 p50/p95/max、over_vmax_rate
- 自证:用 round30_after trace 独立重算得 1.660004×VMAX、超速度占比 1.0,与新指标逐位一致
- 默认参数下 base 复现第30轮 after 的每个旧指标(0.57421875/0.390625/19.25/2.444952726/0.209975064 等)
- 新开关 inc_cap(反馈叠加后再按每轴 VMAX·DT 限幅)、gap_margin(余量门限+自动抬宽相位 cap,
  否则现 cap=0.5 会让 >0.5 的余量恒假);col 仍按 gap<=0 判真实碰撞,不与余量混用
- 只读核查:ik-policy.js:355 同样在 ±1·STEP 之后叠加反馈;JS 的 5 mm 余量是投影式护栏,非同机制
- 口径提醒:round30 plans['gaps'] 在 cap=0.5 截顶,不能据此算"≥1mm 占比";中位最小间隙 0.046 mm 可用
- 测试 26→30 项(限幅灵敏度、余量拒/受、cap 抬升生效)
2026-10-20 19:48:23 +08:00
cbb c6b46b8bb1 exp: 轮次32——均匀细分认证一次调用可证段内碰撞,界经720项检验未证伪,但P50≤20ms的档只能证出54~63%
- certify_uniform:固定 n 个子段一次 gaps 调用,判据与二分同源(子段界=整段界/n),证不出即拒
- 确定界反证:48段×121点×15组对=720项检验 0 违例;有信息量仅 61 项,最紧仍高出 0.035 mm
  (是"未能在真实运动上证伪",不是证明;浮点误差仍未形式化)
- 第180题 1.08-1.10s 漏检段在每个 n>=8 均判碰撞;二次加密(2n+1)反证 0 误放行
- 更正轮次31的"自适应cap无收益":旧公式 bound/2+eps 会把本可通过的子段钳成不可证;
  改为 bound+2eps 后交错实测决策不变、快 1.36~1.65x(n=8 13.13→9.68,n=128 98.46→59.76 ms)
- 负结论:只有 n<=16 能把认证压进 20 ms,但该档仅证出 26-30/48 根,其余必须刹停;
  成本=log(bound/间隙) 决定,故下一步要在反馈后限幅并让安全层要求正余量(现 cap=0.5 会截掉余量)
- 测试 23→26 项:n=5拒/n=6受的解析对照、cap_required 公式、n 张量类型门
2026-10-20 19:30:02 +08:00
cbb 6de085fd37 exp+fix: 轮次31——确定界连续认证抓到段内碰撞,但单段P50 22.9ms进不了20ms;自适应cap收益经交错复测撤回
- Kin.cert_lever 用三角不等式给全姿态位移界(替代 JS 随机采样×1.05),界量级 5.93–48.86 mm/整步
- Kin.certify_segments 批量前沿二分+每根查询预算:48 固定段中预算32判安全37/碰撞1/未知拒10,
  第180题1.08–1.10s漏检段以5次查询判碰撞;未知只拒不放
- 修 seg_seg_dist 第二条退化成点不投影(模型内退化胶囊686受影响);但7586个加密点的组最小间隙
  逐位不变、2000样本JS对拍仍 PASS → 不解释轮次30漏检、不改既有成绩
- 负结论:B=1 单段认证 P50 22.93/P90 58.43/P99 101.57 ms,432次中225次>20ms;
  自适应宽相位 cap 交错复测耗时无收益,先前1.6×为GPU顺序漂移,已在日志撤回
- 成本模型 L=ceil(log2(bound/(ga+gb)))、查询=2^L+1 与实测对齐(安全段2–17次,未知段33–65次)
- 新增8项段认证测试+1项回放测试(14→23项通过);变异实测证明张量 max_depth 回绕会误放行
2026-10-20 19:13:40 +08:00
cbb 95da49193f fix+exp: 轮次30——统一认证与执行限位,到达57.4%不变;加密回放仍检出段内碰撞 2026-10-20 17:26:20 +08:00
cbb 9eaff7341f exp: 轮次29——全拒后减半反馈到达55.1%,补齐无碰撞指标与首撞回放
512题无碰撞到达54.7%、碰撞1.8%;9次首撞均在J5限位且双精度复现。保留小样本消融,撤回未经实际调度验证的50Hz达标结论,附六项回归测试及完整配置结果。
2026-10-20 17:06:53 +08:00
cbb ce9edd7934 exp: 端点进度选择——commit2 到达 52.1%/time_ok 35.2%(n=512),ratio2s 0.343→0.285;附带 50Hz 饥饿实测
- select='endpoint'(候选块末态离目标最近)优于 consist:n=512 到达 +1.3pp、time_ok +6.3pp、ratio −0.058
- 负结论:safety_try 1/2(41.4/44.9%)、K=4(47.3%)、DLS 兜底(52.3%/碰撞+1.1pp)、增益调度(47.3%)
- JS Runner:endpoint 选择落地、冷启动跳前瞻;starveTicks 指标;commit2 平均空转 0.8–1.3 tick,commit3 可覆盖
- route40 endpoint:12/20 ok、13/20 到达、刹车 40.4%
2026-10-20 15:48:31 +08:00
cbb d977be321b exp+fix: commit2 增益重扫(gain2.2 到达 50.8%,n=512)+ 浏览器安全层三点修复(限位夹紧/部分前缀/离散认证)
- gain 1.8/2.2/2.5/2.8 = 50.4/52.0/49.6/47.7%(n=256),2.2 最佳;n=512 复核 50.8%/1.4% 碰撞
- 积分反馈与反馈隔次复用均为负结论(−4.0/−5.1 pp)
- JS:tick/候选投影夹 LIM(原越限后 inBox 永久拒绝→刹停);safety_partial 移植;segCert 开关(默认严格)
- bench nc10 --seg 0:刹车率 79.5→48.1%、planP50 56.1→33.5ms、ok 0→5/10
2026-10-20 14:23:59 +08:00
cbb 46fe24365b perf+exp(closed_loop): 解析雅可比 DLS(消除 float32 差分数值噪声)——commit3 到达 36.3→46.5%,commit2 49.2%(n=512),DLS n1 9.3→7.1ms
- Kin.jac 几何雅可比,与 autograd 逐项一致(FD 自身 rel 误差 3.58%)
- dls_steps(analytic=True) 默认;每步少 6 次批量 FK
- 新增 ml/eval/eval_cl.py(部署口径小规模评估)与 check_dls.py(对拍+延迟)
- 积分反馈 fb_ki 脚手架(默认 0,未启用)
2026-10-20 13:33:23 +08:00
cbb 7e99b333ea exp: 轮次25——实时节奏(commit3)准确率:候选平均29.7%/冻结编码器34.4%/commit4 28.9%,均低于基线36.3%;效率:ORT CPU推理3.9ms+gm_groups,单次规划~32ms,50Hz异步可行 2026-10-20 12:49:37 +08:00
cbb 79519b042c perf+exp: gm_groups 只算特征 8 组(数值一致,features 10.4→7.1ms);混合节奏负结论(执行反馈无提升);浏览器部署 dag2 ONNX + fbGain/commit 参数(bench 刹车 47% 待调);实时口径改为异步规划+tick 2026-10-20 12:38:56 +08:00
cbb 3b748c811c perf(closed_loop): DLS 批量雅可比(25→10-17ms,数值差≤0.013°)+ 单题安全层早停;实时评估:单次规划 40-80ms 超 50Hz 预算 2-4 倍;实时节奏下每3步 36.3%、每6步 14.8%(每步 42.2% 不可实时) 2026-10-20 12:20:31 +08:00
cbb 26eb0815c2 exp(dagger-deploy): 轮次22——冻结编码器微调(开环保住15.8mm vs 19.5)但部署到达41.8%仍低于dag2 43.8%;训练侧四种方法均未超基线,训练探索到此平台 2026-10-20 10:10:25 +08:00
cbb 7a8e5aab63 exp(dagger-deploy): 轮次21——DAgger-3(部署配置采集500题+15000步微调)负结论:部署口径 39.5% vs dag2 43.8%,开环退化到19.5mm;下一步试冻结编码器只调解码器 2026-10-20 09:41:44 +08:00
cbb bb919d33c9 exp(dagger-deploy): 轮次20——反馈蒸馏(17.7万样本+3000步微调)负结论:gain=0 时到达仅0.8%(反馈未内化),gain1.8 与基线持平(43.4 vs 43.8) 2026-10-20 08:42:29 +08:00
cbb ff160f576e exp(dagger-deploy): 轮次19——APG 修复两个既有 bug(_cat_gaps in-place、段边界 queue.detach 无梯度)后可跑,但碰撞铰链尺度失控(40万)且与部署形态不一致,暂不采用;转反馈蒸馏 2026-10-20 07:56:58 +08:00
cbb 5675159987 docs(探索日志): 分支清理记录——fb-mix 已并入 exp/dagger-deploy 后删除;abs-target 打 archive tag 后删远端分支 2026-10-20 07:40:16 +08:00
cbb 39c870c1cf exp(dagger-deploy): 轮次18收尾——n=512 gain1.8 到达42.2%/碰撞2.9%/ratio0.270/time_ok29.1%/870ms(历史最佳);保守版 gain1.5 40.8%/2.1%;探索日志统计更新 2026-10-20 07:23:16 +08:00
cbb 7be41cb1ad exp(dagger-deploy): 轮次17——每步规划下 gain1.8 到达 43.8%/碰撞1.6%/ratio0.216/time_ok28.9%(新最佳);gain2.2/3.0 时间效率更高但到达降、刹停暴涨(3.0:123次/题过冲) 2026-10-20 07:12:59 +08:00
cbb 6c2af53d18 exp(dagger-deploy): 轮次16——n=512 稳定:每步规划+部分前缀 到达40.8%/碰撞2.1%/ratio0.309/time_ok25.0%/852ms(历史最佳,较基线 0% +40.8pp) 2026-10-20 06:57:25 +08:00
cbb f5f394a4bf exp(dagger-deploy): 轮次15——每步规划(commit_n=1):到达 41.8%、time_ok 21.5%(+57%)、ratio 0.259、碰撞 2.3%、967ms(新历史最佳);修复 ok6 视图硬编码 2026-10-20 06:47:00 +08:00
cbb 76ac563d21 exp(dagger-deploy): 部分安全前缀(执行到首个不安全步)——刹停 27.4→8.6、到达 38.3%、碰撞 1.6%;新增 commit_n 参数(每步规划实验);更正 replan=empty 实为 r=0 规划非每步 2026-10-20 06:26:31 +08:00
cbb de009bd516 exp(dagger-deploy): 轮次12——n=512 稳定:gain1.5+每步重规划 到达33.4%/碰撞1.0%/ratio0.399/刹停28.1/738ms(历史最佳);gain1.2 保守版 32.6%/1.0%/18.6 刹停;监控页回写已验证 2026-10-20 05:51:11 +08:00
cbb 29f3887be1 feat(monitor): 控制台新增「闭环探索日志」卡片——/api/explore 读取 docs/探索日志.md,Edge headless 截图验证渲染正常(http://127.0.0.1:8100) 2026-10-20 05:35:42 +08:00
cbb fe48b4e777 exp(dagger-deploy): 轮次11——n=256 复核(前128题偏难):gain1.5+每步重规划 到达36.7%/碰撞1.6%/ratio0.323/刹停27.4/760ms(新最佳);gain1.0 更保守 31.6%/0.8% 2026-10-20 05:34:40 +08:00
cbb 4de5e03a0b exp(dagger-deploy): 轮次10——每步重规划下 gain1.5 达 28.9%/0.8%/0.411/刹停28.5/844ms(新最佳);gain1.0 碰撞 0.0% 但到达 22.7%;待 n=256 复核 2026-10-20 05:24:20 +08:00
cbb 5f2cbd5179 exp(dagger-deploy): 轮次9新历史最佳——每步重规划+gain1.2:到达 26.6%、碰撞 0.8%、ratio 0.453、刹停 20.6、845ms(K=16 无益淘汰) 2026-10-20 05:15:01 +08:00
cbb 2ff02eb9a8 exp(dagger-deploy): 轮次8——部署分布 DAgger 把刹停降 18-40%、时延 953→922ms,到达 22.7%(未破 23% 平台);历史最佳到达仍 dag1+gain1.2 23.4% 2026-10-20 05:05:47 +08:00
cbb dc27781a72 exp(dagger-deploy): 采集支持 fb_gain+safety(训练分布=部署分布);新增 base_dag2 配置(同 base) 2026-10-20 03:40:15 +08:00
cbb 8e5431c460 exp(fb-mix): 轮次7负结论——衰减增益到达 23.4→9.4%、刹停兜底碰撞 1.6→57.8%(绕过安全层);参数默认关闭,历史最佳仍为 gain1.2 无衰减无兜底(23.4%/1.6%/0.454/973ms) 2026-10-20 03:09:21 +08:00
cbb b06662c152 exp(fb-mix): 目标反馈混合(inc += gain·DLS 朝目标)——DAgger 模型 + gain 1.0 + 安全层 reach 20.3%/碰撞 2.3%(历史最佳,此前 0%);建立 docs/探索日志.md 2026-10-20 02:40:12 +08:00
cbb 70654e45f7 docs(本机执行清单): 方案 B 初版实测(feat/abs-target 分支)——绝对目标标签 + 训练/执行适配完成并修复前缀增广污染;实测一步生成误差 31°(4/8 段 2.3°/2.8°)、闭环 0%/100%,过预热 5000 步无下降趋势——此配方不成立,B 若继续需改 p_edge/FK 权重/加权或换输出头 2026-10-20 01:03:45 +08:00
cbb fc17ae10d0 docs(本机执行清单): 方案 B 判别——偏移 3.19° 下模型预测的 16 步累计目标偏离原轨迹 4.86°(ρ=1.54;未扰动开环仅 0.46°),扰动微调后 ρ 更差(1.9-2.0);结论:模型是从当前状态外推增量而非指向参考位置,B 必须改训练标签(绝对目标)重训,改执行层无效 2026-10-19 23:14:30 +08:00
cbb 2f5a72a6ab docs(本机执行清单): 方案 A 实测(两轮)——合成 1.5° 扰动状态样本 36k + 微调 3000 步:碰撞率 99.6%→48%/60%,但 success 仍 0、开环精度 6→15/16mm 退化 2.5×;小步微调在两种分布间折中带偏主分布,此配方不成立 2026-10-19 23:11:06 +08:00
cbb 7a194dd4a7 docs(本机执行清单): 回答「加大数据能否救闭环」——闭环状态到训练集最近邻中位 21.5°(起点 13.2° 的 1.6×,p90 32.5°);训练集本身稀疏(老师起点最近邻 13.2°);6 维密度缩放数据翻 4 倍只降 ~21%,与 0.02° 误差尺度差三个量级——扩大同类数据救不了,需 DAgger 覆盖分布外状态 2026-10-19 22:45:39 +08:00
cbb a41a5440c6 docs(本机执行清单): DAgger 小样本试跑通过——collect 16 题 12.7s/29 起点,64 题显存峰值 7.98GB 无 OOM;老师重规划 15 起点 7.6s、打包 2.3s(453 样本);提醒 build_dataset 默认 act_scale=1.25 与旧主数据 1.0 不一致,全量前须显式 ACT_SCALE=1.0 或先重打主数据 2026-10-19 22:19:24 +08:00
cbb 864b21f894 docs(本机执行清单): 发散题分组——三组(r2>1 / 0.7-1 / ≤0.7)碰撞率均 79-83%、到达全 0,无「一小撮坏题」可筛;r2 与起点误差秩相关 -0.384 是分母效应(短任务比值波动大),与老师时长/行程无关;误差放大是全体普遍行为 2026-10-19 22:16:39 +08:00
cbb b396298819 docs(本机执行清单): 补充实验——开环误差无轴向偏置(|mean|/std≤0.17)但 lag-1 自相关 0.93-0.97(慢变误差,非白噪声);闭环 p50 2s 缩 41%、p90 发散到 1.318(≥10% 题误差复利);结论:慢变误差 × 闭环低容忍,训练侧需闭环状态训练 2026-10-19 20:31:48 +08:00
cbb 53a50ecab9 docs(本机执行清单): 本地自主排查——闭环噪声容忍上限 0.01-0.02×STEP(σ 扫描 0→0.05 时 reach 1.0→0.218);8 段采样 reach 仍 0(flip 已=老师、无抖)→ 策略误差是系统偏差非白噪声,降噪已到天花板;need 头恒 <0.5(>0.5 占比 0、召回 0/43)=闭环子目标通道从不激活(H3 三变体逐位相同的直接原因),训练 80% sf=1 vs 闭环 100% sf=0 2026-10-19 20:21:02 +08:00
cbb 425229f412 docs(本机执行清单): 第六轮执行回传——H1_oracle 全正常(仿真无误,闭环结果可信);σ=0.05 噪声 reach 1.0→0.218(对残差噪声极敏感);前缀照抄、子目标规则均排除;H3 三变体逐位相同=子目标通道零响应(新异常);4 段采样消抖后仍 0% 到达 2026-10-19 19:18:07 +08:00
cbb 7e063eb3d5 本机改动:归档早期闭环碰撞定位脚本 diag_collision.py(已被 diag_cl 取代) 2026-10-19 18:46:39 +08:00
Hark 406f180cab diag: H 段(老师 oracle 走 rollout / 前缀消融 / 子目标越过顶点规则 / 分轴与分类别指标);训练:EMA、选模不落在 p_neq 预热、闭环分数选模、24000 步余弦、p_neq 0.5 + p_edge 0.1、子目标头切断抄输入、前缀/dq 增广;仿真:last 记真正提交的候选、碰撞与到达按每题时限、回放门禁读 pass+指纹并走 rollout;数据:动作标签按 1.25·STEP 归一化(act_scale 随 meta/ckpt/ONNX 传递,旧模型 1.0)、收回亏空结转、DAgger 带 qPrev 与起步速度、打包签名变更自动重打;ik-policy.js 同步(actScale、子目标规则、DLS 时清 lastPlan);本机执行清单与 README 更新 2026-10-19 10:43:05 +00:00
cbb 86323f1691 docs(本机执行清单): 第五轮执行回传——一致性错位修复未改变闭环结果(C 段仍 0%/99.6%,2s 误差比 0.755);G 段:consist/value/first 三选择器几乎一样(0%/94-96%)——问题在候选本身不在选择;1step 速度比 2.31×、4step+safety 碰撞降到 0.78% 但到达仍 0% 2026-10-19 12:40:16 +08:00
Hark 0f5b84b919 fix(closed_loop): 一致性打分错位一步(对齐 last[3]→last[4],原来每次重规划奖励比上一计划慢一步的候选;Python 闭环与 ik-policy.js 同步修正);rollout 加 select;diag_cl 删 F 段、加 G 段(1/4 段采样 × consist/value/first、4 段+安全层);本机执行清单更新 2026-10-19 03:42:46 +00:00
cbb 0a19b29fd3 docs(本机执行清单): 第四轮执行回传——F 段证实一步采样残留噪声(抖动 41.9×、残差-噪声相关 −0.861),2/4/8 段分别把 16 步误差降到 0.92/0.86/0.56°;但闭环 4 段仍 0% 到达(碰撞 93.0%、2s 误差比 0.610)、零噪声 0%/100%/0.724——残留噪声真实存在但非主因 2026-10-19 03:37:00 +08:00
Hark 1bf59e7175 diag: F 段——MeanFlow 一步采样残留噪声(开环 1/2/4/8 段与零噪声的幅度/抖动/残差-噪声相关;闭环 4 段、零噪声);sample_k 加 steps、rollout 加 steps/z_scale;删 E 段;本机执行清单更新 2026-10-18 19:10:32 +00:00
cbb cafa038110 docs(本机执行清单): 第三轮执行回传——gaps 三行全 1.05 GiB 通过(默认不拼胶囊对生效,峰值=单粗块水平);E0 排除抄速度(抹 dq 幅度仅降 2.7%,且开环/起点幅度系统性 1.44-1.52×);E1 中段带老师速度与静止起步都 0% 到达/96-98% 碰撞(误差比 0.627 vs 0.829)——非起步问题;新观察:幅度系统性偏大 1.4-1.6× 2026-10-19 03:07:49 +08:00
Hark 527bc25c11 fix(kin/diag): gaps 默认不拼胶囊对(pairs=True 仅 APG 用;实测 4096×40 峰值 4.46 GiB 全在拼接 8.6e7 对);diag_cl 删 D 段(子目标已排除)、加 E 段(开环幅度与抹 dq 敏感度、老师中段带速度/静止起步闭环);rollout 加 qp0;本机执行清单更新 2026-10-18 18:53:39 +00:00
cbb 9a7404856e docs(本机执行清单): 第二轮执行回传——gaps 1.05/1.40/4.46 GiB(前两条过线;4096x40 峰值全在顶层 _cat_gaps 拼接,parts 与输出 8.63e7 对同时存活 2.25+2.25 GiB);diag_cl 含 D 段:D1 no_sub 与 D2 oracle_sub 都是 0% 到达/99.6% 碰撞,与基线一致——子目标不是元凶;C.goal_err_ratio 2s 仅靠近 18.5% 2026-10-19 02:43:26 +08:00
cbb 1f2f2a2bb1 monitor: 开环指标两张图;pstate: train 归档加 review_* 2026-10-19 02:33:46 +08:00
Hark 0653cf0d8d fix(kin/diag): kin.gaps 粗筛段也按点分块(COARSE_ENTRIES=3e7,≤446 点/块;实测上一版粗筛 4096 点仍 9.3 GiB);diag_cl 加 D 段(子目标头精度 / 不用子目标 / 老师真实途经点)与目标误差进度比;rollout 加 use_sub、sub_override;本机执行清单更新 2026-10-18 18:32:45 +00:00
cbb 95ba43a1c7 docs(本机执行清单): 执行回传——第1步 gaps 显存三条 1.20/9.30/9.30 GiB 未过(峰值全在粗筛段,精算分块有效但救不了粗筛;旧版同测 4.59/16.98/36.89);第2步闭环定位 A/B 全过(特征口径差≤3.6e-6)、C 段 0% 到达/99.6% 碰撞、主碰撞对 J3-J5 90次、开安全层碰撞压至 5.5% 但到达仍 0.4% 2026-10-19 02:29:51 +08:00
cbb 5365add657 Merge commit '573082d' 2026-10-19 02:02:55 +08:00
Hark 573082df26 fix: 闭环失败定位与显存修复——新增 ml/eval/diag_cl.py(Python/JS 特征口径、同噪声动作差、碰撞时刻/组对、开安全层到达率);kin.gaps 按幸存胶囊对数(PAIR_BUDGET=2e6)自动分块(实测原 4096 点 cap 40 峰值约 30 GB);dagger/collect.py cap=gap_min+1 分块;新增 docs/本机执行清单.md(本机计算唯一要求清单) 2026-10-18 18:02:14 +00:00
Hark 5a87c0655a diag: ml/eval/diag_cl.py 闭环失败定位 + fix(dagger): collect.py kin.gaps 分块 1024 点、cap=gap_min+1(原 4096 点 cap 40 吃满显存/内存) 2026-10-18 17:36:52 +00:00
cbb 0b47905a1c feat(prep/monitor): 打包每 10% 打一行进度 + 监控按实测算进度/ETA;prep 估时 0.5→2.0 h
- build_dataset.cjs:每 10% 打 `[prep] shard_xxxx a/b`(只加日志,不改计算)。新增广
  (40%/3°/12,每个增广样本 6 次碰撞检查)让这批打包从上次的 25 分钟涨到约 1 小时,
  而原来整片完成前一行输出都没有,看起来像卡死。
- monitor.py prep 分支:解析这些行,按 (已完成分片 + 进行中分片的任务比例)/总分片 算 frac,
  按实测速率外推 eta;首批完成前显示"首波处理中,第一批完成后显示预估"。
- pstate/monitor.html:prep 估时 0.5 h → 2.0 h(超时 1.5 h → 6 h),与实际耗时一致。
2026-10-19 00:33:11 +08:00
cbb 0d522dd782 feat(monitor): 开始训练弹窗视觉重做 + 进度卡片「重新训练」按钮 + 记住上次设置
- 弹窗:头部图标/标题/说明、分区标题(运行方式 / 模型与数据)、下拉统一自绘箭头与焦点环、
  每个控件加提示行、警告条改成左侧色条的信息块、取消改 ghost 按钮;宽度 560→780。
- 进度卡片加「⟳ 重新训练」(运行中禁用并提示先停止),点开即预设"从 train 重训"。
- 记住上次设置(localStorage mlmon.pref.v1):起始阶段/CFG/共识/N_TASKS/NPROC/TARGET_SAMPLES/
  ROUNDS/MIN_GAP_MM/平滑/对数轴。运行方式不记忆——记成"全部重跑"会把数据一起归档,风险不对等。
- ?start=<阶段> 直接打开重训弹窗(便于收藏与脚本化验证)。
- 端到端验证(CDP 驱动无头 Edge):改 fT/fFrom/smooth → 刷新 → 三个值都保留,fMode 复位 resume。
2026-10-19 00:11:13 +08:00
Hark d60cbcdb3c fix(monitor): FRESH_FROM 白名单加 dagger 2026-10-18 16:01:09 +00:00
Hark 979a5814c1 fix(train): 首次正式预训练(21850 步闭环到达 0%)排查后修正——FK/平滑改加在配对去噪估计上(原加在随机噪声一步采样上=回归条件均值,多峰塌成单峰,实测候选标准差 0.030/幅度 0.151);时间嵌入最高角频率 1000→50、MF 分支 fp32、p_neq 前 5000 步为 0(实测 du/dt 均方根 6–12 vs v 1.1);增广 40%/3°/12 步收回;新增 dagger 阶段(策略闭环状态请老师重规划 ×2 轮微调);开训前老师轨迹回放自检;评估加开环误差与候选多样性,best/早停按到达率+开环误差 2026-10-18 15:56:39 +00:00
cbb c2732fb3cd feat(train/monitor): 评估峰值不再撑爆整卡 + 从指定阶段重训 + 闭环误差诊断
- train.py:evaluate 改 bs=64,结束后 empty_cache()。WDDM 下 15.9 GiB 的评估峰值用完不退,
  整卡顶到 96% 后驱动把训练张量换出(实测 Local 18.8 GB / Shared 1.9 GB),吞吐从 5.5k
  掉到 444 samples/s、8000 步 eval 卡死 68 分钟;改后峰值 5.32 GiB、eval 106 s、吞吐 6.4–7.0k。
- pstate/pipeline:新增 --fresh-from=<阶段>:只把该阶段及之后的产物移进 ml/archive/(不删),
  与参数失效取并集;配合 FROM 实现"只重训某一阶段,之后全部重做"(沙盒实测选 train 只归档 ml/runs/base)。
- monitor:运行方式加「从指定阶段重训」+ 起始阶段下拉;TARGET_SAMPLES 默认改 200 万;
  「最好 X%」改标最好那次 eval 的步数(原来错标当前步,18,000 步显示成 0.2% 的出处)。
  连同工作区已有的停止补扫 sweep_nt(按命令行补杀 git-bash 下逃逸的孙进程)一并提交。
- eval/diag_tail.py:闭环末位误差 / 是否进过 5 mm 窗口的逐题诊断(手动运行,不进流水线)。
2026-10-18 23:15:53 +08:00
cbb 7fe1e5e0b3 feat(monitor): 失败提示做醒目——顶部红色横幅(阶段名 + 返回码 + 终端里最后一行真实报错,如 OutOfMemory 那条)+ 右上角胶囊变红「失败 · 停在「预训练」返回码 1」+ 步骤条那一格红字加粗;并补三层"永不空白"保险:正文脚本前装看门狗(3 秒没跑完就显示"卡片空白不是训练出问题")、window.onerror 显示出错文件与行号、渲染调用 try/catch 让单个面板出错不拖垮整页;同时修掉我上一轮写坏的 JS 字符串(Python 转义把 \n 写成真实换行 → 整个脚本不执行 → 页面全空,就是用户看到的"失败时什么都不显示") 2026-10-18 19:43:24 +08:00
cbb 3c4bc6c8ab fix(vram): 上限不再从 nvidia-smi 的"已用"里扣——WDDM 下那部分驱动可换出,按它扣会把本进程上限压到 14.45 GiB,预训练第一次 evaluate 峰值 ~15 GiB 就自己 OOM(实测当时卡上还有 9.98 GiB 空闲);改成上限 = CUDA 可用 − 1 GiB(实测 21.8 GiB),别人占多少只打印不扣;实测分配 15.5 GiB 通过;顺带删掉 monitor.html 里被塞进模板字符串、当成文字渲染出来的 JS 注释(剩余时间那格) 2026-10-18 19:18:41 +08:00
cbb 4a59ca84f6 feat(monitor): 每个阶段都按实时数据算剩余时间,用时精确到秒——打包按已打包分片数、预训练用脚本自报 eta_h、APG 用 it/min、挖错用本轮 APG + 已完成轮次平均时长、头门禁用已完成那次评估、一致性门禁读日志里的 k/300 行(只读日志尾部,不占 CPU);修 mine 的当前轮判断(原来 len(cur)>k 在第二轮起永远判不到在跑)与 APG 漏加的 eta;总用时/本步用时/步骤条用时改为带秒,预估仍只到分钟 2026-10-18 17:59:46 +08:00
cbb faf6dd2748 feat(monitor): CPU 卡加温度和功耗——走 MSI Afterburner 的共享内存(MAHMSharedMemory):Windows 上免驱动的唯一口子(psutil 不支持、ACPI 热区只有板温、Power Meter 无实例);实测 CPU 98-100 °C / 222-227 W,用 GPU1 power 对 nvidia-smi 校准过(125.29 vs 124.57);数据比 10 秒旧就当读不到、Afterburner 没开就自动隐藏这两行;28 核条悬停可看每核温度 2026-10-18 17:31:05 +08:00
cbb b1cf4126fd feat(vram): 显存上限改为浮动——本进程上限 = min(整卡 − nvidia-smi 已用 − 1 GB 余量, CUDA 空闲 − 余量),别人占多少自动扣掉,不再写死 0.78(实测当前口径 14.1 GiB;VRAM_FRAC 仍可显式对拍旧口径);网页显存行改成"8.7 / 23 GB · 空 14.3"并把解释放悬停(长文本不再把行挤成两行,.gr 行加了省略号保护);README/审查文档同步口径 2026-10-18 17:23:57 +08:00
cbb e431862793 feat(gen): 补量轮次上限 4 → 5(应用户要求:共识 + 间隙两道过滤叠加时 4 轮余量偏紧);顺带把末尾那句"补 4 次后仍不足"的提示同步成 5 次 2026-10-18 17:16:55 +08:00
cbb c65b9986ca feat(monitor): 当前阶段进度放大——「当前步骤 · X」的百分比 20px 加粗、阶段进度条 14px 高(原来 12.5px/8px 太小);顶部步骤条当前格的百分比也放大加色;顺带把已完成格子的「用时」改用累计值(续跑的阶段不再只显示最后一段) 2026-10-18 17:13:35 +08:00
cbb db0bbbc15d feat(monitor): 用时改为按阶段累计——"总用时"=各阶段之和、"本步用时"=该阶段累计,停止后再"继续训练"不再归零(训练跑 5 万步停一次,之前那段的时间会保留);只有产物被归档、要从头重做的阶段才清零;本次改动前完成的老记录用 start/end 兜底;停止与流水线退出两处记账幂等(实测 180 s 不翻倍) 2026-10-18 16:59:21 +08:00
cbb 21ad209747 feat(monitor): CPU 卡显示型号(psutil 只给核数,型号走注册表 ProcessorNameString / Linux /proc/cpuinfo),加在卡底与 GPU 同名同位置 2026-10-18 16:15:44 +08:00
cbb c143b56232 fix(gen): 被停止的种子不再报"失败"——按日志尾部有无报错分类,中断走 143(网页显示"已停止"),只有 node 真报错才 exit 6;顺便修补量分母累加:target_tasks 改用 stats.json 的已写完条数 + 本轮补量,此前每次点停止再"继续训练"分母都再加一截(实测 67368→84728),进度条越续越少 2026-10-18 15:24:43 +08:00
cbb 83dcc1c3ee fix(monitor): CPU 占用读数改为后台每秒独立采样(此前在请求里算差值,网页 1 秒轮询会把窗口压成几毫秒 → 恒为 0);CPU 并进 GPU 同一个卡(上 GPU 环+显存/温度/功耗,下 CPU 环+28 核逐核条)
实测:空闲 16.8% → 14 个满载循环 71.7% → 结束 19.1%,随负载单调;截图复核合并卡排版正常。
2026-10-18 15:13:37 +08:00
cbb 9ed213417b feat(monitor): 网页加 CPU 占用卡(总占用环 + 28 核逐核条),psutil 采样缓存 1 秒;顺便重启控制台让之前的补量进度口径与停止补杀生效 2026-10-18 13:57:48 +08:00
cbb e6418da084 fix(monitor): 补量时进度条显示全量而不是从 0 开始;停止时补杀 Git Bash 逃掉的 node 孙进程
- gen 进度:分子改为统计全部分片(含之前轮次),分母仍是累计 target_tasks,补量时显示 50,008/79,688 并标注「本轮补 N 条」。此前只统计本轮开始后改过的文件,看起来像从零重跑。
- 停止:taskkill /T 在 Git Bash 下只杀到 bash,node 孙进程会逃(实测点了停止还有 28 个 gen_dataset 在跑)。按命令行补扫 gen_dataset.cjs / run_parallel.sh / teacher.cjs / policy_bench.cjs 并 Stop-Process;正则已验证命中 gen、不误伤普通 node。
2026-10-18 13:54:16 +08:00
cbb 241333c213 fix(pstate): 改 TARGET_SAMPLES 不再作废 gen 的已有分片——只重跑 gen 补量(run_parallel 跳过写完的 shard、按差额补量),prep 及之后仍作废;此前把它当"从 gen 作废"会让 5 小时生成的 5 万条分片被整体移进 archive,用户改目标样本量就得从头跑 2026-10-18 13:50:12 +08:00
cbb c128288c1b fix: gen 判定改按产物而不是 wait 退出码——实测 28 个种子跑 5 小时后全部 wait 报失败,但每个 .part 都是完整的 1786 行(node 打了 done、mv 没执行),Git Bash 长跑后 wait 返回码不可靠;现在只要 .part 行数达标就补改名并记成功,只有产物缺失/不全才算失败。另:监控页 1 秒轮询(并行取 status/job/runs + nvidia-smi 缓存),GPU 数字 2 秒缓存。 2026-10-18 13:48:30 +08:00
cbb b25a7a8bfa fix: pipeline.sh run() 回显改走 stderr——gate1 用 tee 把 stdout 存成 consistency.json,回显混入让 gen 读 CONSENSUS 时 JSONDecodeError 停在 gen(第一次真跑实测:kin/r4/gate1 rc=0、gen rc=1);文档记门 1 实测 0.838<0.9 → 自动走共识分支 2026-10-18 07:23:29 +08:00
cbb c175357b72 docs: 训练定名「Transformer 多峰策略端到端训练与浏览器部署」——写入 ml/README 标题与控制台顶栏(含浏览器标签),长跑时打开网页即见;截图复核排版未挤坏控件 2026-10-18 06:40:26 +08:00
Hark d03f34822c 第八轮:monitor 状态三项被注释吞掉的 bug;R9 改判稳态 planWarmP99 + 运动中饥饿 + 认证超时占比;APG steps 8000(batch 48 保持总任务量);r4 不再静默退回 CDN;自检副本含未跟踪文件;显存文案按 19–20 GB;审查文档精简为实测/判据/待数据/三段训练 2026-10-18 06:40:26 +08:00
cbb 8ad6b58fca ml: 显存预算按 19–20 GB 落地 + ort 本地副本入库 + R4 复跑(feat_dyn)通过
- 显存:APG batch 128→48(实测 d=192 下 128 无论 plans 60/150 都 OOM,崩前已分配 17.85 GiB;48/plans150=13.08、48/60=7.67);新增 ml/common/vram.py 每进程硬帽 VRAM_FRAC=0.78 并把 vram 峰值写进 train/APG 日志、监控页加「显存峰值」卡与按 20 GB 预算的显存条;smoke.json 补 apg 段并让自检带 --config。
- 自检修复:find_bash 兼容便携版 git 的 mingw64 布局(本机原返回 None → 网页「开始训练」起不动);selftest_proc.alive 读中文 tasklist 的 GBK 崩溃;rec 值非法 JSON 时静默丢项;gen_resume 判据剔除 plan_ms/cert.ms 两个墙钟字段(旧判据恒 false)。整轮重跑 7/8 通过、3.5 分钟,仅 train_term 为 Windows 真实限制(TERM 送不到原生 python)。
- vendor: onnxruntime-web 1.20.1 本地副本入库(约 33 MB),查看器与 R4/R9 探针不再依赖 npm/CDN;.gitignore 同步移除该目录。
- docs: 第八节 R4 复跑(feat_dyn 新模型):部署口径 P99 12.67 通过;WebGPU 首次实测 27.09(有界面窗口,adapter nvidia ampere);viewer_ref 随机模型 planP99 131 / starve 32.7% / lookaheadP99 170 / certFailWhy time 948 / tickGapMax 736,三条判读留给实现方。
2026-10-18 06:40:25 +08:00
Hark 591678703a 第六、七轮:离线 ort/WebGPU、R9 查看器整周期、安全层截止下沉、enc_once 前瞻 bug(feat 动态轴)、UTF-8、断点 RNG 续跑与 --stop-at、R2 池无关性自检、R8 出题缓存、本机自检 selftest、冒烟 smoke;遗留清单并入审查文档第六节 2026-10-18 06:40:25 +08:00
cbb b6141d237c fix: R4 探针页 tensor 复用(proxy 下 buffer 被 detach)+ pipeline.sh 7 处 GBK 读取 + r4 分支返回码;r4 端到端 rc=0
- webapp/r4_probe.html:feat 移进 run() 每次新建。proxy=1 时 ORT 把输入 transfer 给 Worker,旧写法第二次 run 报 Tensor size(83) vs data length(0),两种 proxy 配置全挂、r4.json pass=null。
- ml/pipeline.sh:7 处 json.load(open(...)) 补 encoding='utf-8'(:90 :110 :118 :127 :136 :138 :144)。r4.json 以 ensure_ascii=False 写出,Windows GBK 下 :110 实测 UnicodeDecodeError,命令替换为空会让上层分支走错。
- ml/pipeline.sh r4:末行 `[ "$r" = null ] && echo` 在 r=pass 时整体返回 1,set -e + EXIT trap 把通过的 r4 记成 rc=1(all 会因此停)→ 改 if/fi。
- 实测复跑两次:pass=true,部署口径(16 线程+proxy+enc_once)P99 10.22 / 11.25 ms,4 线程 13.58/13.64,无 proxy 9.28/10.92,webgpu 仍 backend not found。
- 文档同步:A 组两处标记已修,仅留 A-3(ortLocal+proxy 离线路径)交实现方。
2026-10-18 06:40:25 +08:00
cbb a1ee7d1a4d docs: ml 审查精简版(763→85 行)——删去前五轮已解决条目,只留:R4 实测基线(部署口径 P99 11.07 通过、查看器 infer/safety 30.4/61.6+63.5/248.8)、A 组三处会断流程(r4_probe.html 复用 detached tensor、pipeline.sh 7 处 GBK json.load、ortLocal+proxy 离线路径失效)、B 组三项待判(R4 双口径、safetyMs Node vs 浏览器两把尺/R9 无数据、WebGPU 零数据)、C 组六项未实测(6–13 步、gapDrop、R2 池无关性、R8 出题耗时、Windows 生命周期、续跑本机复核)、D 组 webapp/models/README 过时 2026-10-18 06:40:25 +08:00
Hark f40a40c56b 第五轮:锁/续跑/超时/自动分支/R4 探针与 encode-once/控制台加固;阶段内断点续跑(gen 按条、prep 按 shard、训练与 APG 定时原子存档+体面停止);分歧以实现方意见为准 2026-10-18 06:40:25 +08:00
cbb 3675fbbd9b docs: ml 审查第四轮(对 14d72a9 与训练控制台)+ R4 实测——控制台 N-7 并发无锁(守卫只看 log.jsonl,gen/prep/m5/bench 阶段失灵,二次生成用同名 shard 互相截断)、N-8 开始/停止无 Origin 校验(本机网页即可停掉长跑)、N-9 每次刷新全量重读 shard、N-5 r4 与真模型同名同路径、N-10 console.bat 用 where python 判存在且不带 conda 环境、N-16 gen 节拍被重定向吞掉且 C 类采样 for(;;) 无上限;修法按完整流程分层(状态机可续跑/入口互斥且脱离控制台/门槛改自动分支/阶段超时心跳);R4 随机模型实测 batch=8:4 线程 P50 33.2 P99 37.0(要求 <15,差 2.4×),16 线程 13.7/19.5,24 线程反而劣化,闭环倍率未采到(主线程已挤到 CDP 取不到槽),故 K 与重复 encoder 是必改项;附 .gitignore 忽略 20 MB 级模型产物 2026-10-18 06:40:24 +08:00
Hark 5591adb39a ml: 第二轮审查 B-1~B-8 处置 + 一键训练控制台
- B-1 运行时投影护栏恢复 5 mm;老师轨迹按认证最小间隙 < 3 mm 剔除(stats/build_dataset,gapDrop)
- B-2 APG 端点+中点 hinge 取均值;B-3 check_r2 空匹配保护、5000×50000、分块 256,pass=false 时 prep 停下
- B-4 R7 按全部 B/C 题统计;B-5 r8 起点贴柱;B-7 pipeline r4(export_onnx --random,训练前测延迟)
- B-8 起步失败 ≤0.2%、column_gap 分块、pipefail、门禁结果先写 .tmp
- 训练控制台:ml/console.bat|sh → ml/monitor.py(开始/停止/进度/日志接口)+ monitor.html;
  pipeline.sh all 支持 FROM/TO,阶段状态写 ml/runs/pipeline.json(ml/pstate.py);训练/APG 日志加 total、t
- 文档:README 一键运行与训练控制台;审查文档追加第三轮处置;方案余量决策与 R4 时点
2026-10-18 06:40:24 +08:00
cbb 5348bc6f6f docs: ml 运行手册审查追加第二轮(对修订版 d3b7c7b 的可行性评估,只读代码未跑):13 条处置表(R10 阈值/共识补量/头分目录/$PYTHON/三口径/exec_s 置 None/池只用本轮/fp32/验收汇总 已落地);新引入问题 B1~B8——3 mm 是靠放松运行时投影迁就数据分布(正解在认证侧过滤老师样本,属需拍板的安全决定)、APG 端点+中点 hinge 使碰撞项约 ×2 而 w_col 未重标且 plans 60→150 单步代价上界 ~4× 而 §8 估时未改、check_r2 空匹配取 quantile 会抛且默认 20万×2万 池峰值 2–3 GB 显存、gate R7 左右比例以到达为条件会漏报多模态被压缩、r8 集合起点未查贴柱「两侧都要绕远」名不副实、§2「实测 7.6 s」是 300 条口径而流程跑 2000 条、R4 wasm batch=8 P99 至今没测;结论:1–6 步可执行、7–13 步零证据,第 7 步前先结 R4 并单独拍板 margin 2026-10-18 06:40:24 +08:00
Hark f8cdabb781 ml: 按运行手册审查修订——头分目录不互相覆盖(ml/runs/<CFG>、selected_ckpt/CKPT);统一 $PYTHON(Windows Git Bash 可跑);gen 按实测样本/任务自动补量;R10 改终态判据(≥5k 样本/s≈60k 步≤3.5h);m5 纯策略/仿真安全层双口径;无老师题定长 6 s 口径;APG 展开随批时长、端点+中点铰链、余量 3 mm(投影同步);挖错池只用本轮;门禁加 R7;bench 加 R8 对抗集与刹停率、--nc-n/--route-n/--r8-n;新增 check_r2 与 acceptance;export 默认 fp32;出题分块防 OOM;val 为空保护 2026-10-18 06:40:24 +08:00
cbb 8800a68cad docs: ml 运行手册审查(本机 3090 实测):训练吞吐 6208 样本/s 与方案 R10「≥2万/s」冲突;共识过滤分支缺期望数字、WTA 回退覆盖 ml/runs/base 丢对比物、run_prep 用 python3 在 Windows 第5步末失败;仿真安全层≠ik-policy 运行时(M5 穿柱率与 bench collisionFrames 两把尺)、挖错题 exec_s 写死 3.0 致到达率不可比、APG 仅 3.6 s 定长且只查端点、老师 minGap p50 4.4 mm 压在 5 mm 训练余量下;含复现命令与按优先级改动清单 2026-10-18 06:40:23 +08:00
Hark 2a0a2e55ad ml: 数据生成 + 可微运动学对拍 + MeanFlow/WTA 策略与闭环仿真 + APG/挖错/ONNX 导出与运行手册
- M1 数据生成器:采样/老师/距离下界认证/时间律/统计/一致性门禁;确定性老师(碰撞次数计时 + resetSession)、pos 任务冻结 J6、--consensus 邻域共识过滤、Dr 类。
- 全流程代码与运行手册:数据打包/可微运动学与对拍/MeanFlow+WTA 模型/闭环仿真/APG/挖错循环/ONNX 导出;webapp 策略运行时 ik-policy.js、?policy=1 接入、tests/policy_bench.cjs;ik-solver 增 pairList/clusterExport/criticalPairs/pairGap/leverTable。
2026-10-18 06:40:23 +08:00
Hark cc43b3d3be docs: Transformer 拟合避障轨迹规划训练方案(最终版):无老师闭环部署、MeanFlow 动作头(WTA K=8 回退)、距离下界安全认证、APG 后训练与挖错循环、里程碑 M1–M8;附 HTML 版 2026-10-18 06:40:22 +08:00
cbb 67964496be fix(ik): 贴立柱目标不再误判为立柱区内:候选表分层保留 + 姿态放宽兜底 + 实时管线跨帧坏边/目标解优先/预算递增/半格复验;新增贴立柱基准 tests/near_column_bench.cjs 2026-10-18 06:40:22 +08:00
Hark 46bdb98dd8 feat(ik): 实时避障版:离线路网 PRM + 在线 Lazy A* + 终身增广 + 拖拽热启动;实时管线全程细验(拖拽穿模帧 430→0,重规划 P50 5 ms);前端每帧碰撞守护 2026-10-18 06:40:22 +08:00
Hark 318ad0e264 feat(ik): 避障路径改为多目标 Lazy RRT-Connect + 规划提速 32%
- 避障路径改为多目标 Lazy RRT-Connect + 捷径平滑;碰撞窄相位预测提速。
- 规划提速 32%:(2,4) 零件对类离线间隙表、撞点前移、判定记忆、routeFrom 惰性回退、平滑预算收紧。
2026-10-18 06:40:22 +08:00
82 changed files with 16013 additions and 405 deletions
+22 -2
View File
@@ -25,7 +25,7 @@
|---|---|
| R9(真查看器、训练好的模型、30 道贴柱题) | 稳态 `planWarmP99` < 45 ms、运动中饥饿 < 1%、认证超时占比 < 5%;冷启动 P99、刹停率、`tickGapMax` 只报 |
| R9 不过 | 几何查询(特征、投影、认证)搬进 Worker,主线程只执行 |
| APG | batch 48、steps 8000(总任务量同原 128×3000),进程显存硬帽 `VRAM_FRAC=0.78` |
| APG | batch 48、steps 8000(总任务量同原 128×3000),本进程显存上限浮动(整卡 − 已用 − 1 GB 余量,见 `ml/common/vram.py`) |
| 离线 ort | 只用入库本地副本,缺失即报错,不退回 CDN |
| 训练余量 3 mm | `stats.json` 间隙 p10 ≥ 3 mm 保持,否则降到 p10 |
| `R2_STRICT` | `pool_check.independent=true` 且截距 < 0.10 时打开 |
@@ -40,12 +40,32 @@
4. APG 收敛与机时(batch 48 下首测)→ APG 日志。
5. 真模型 R9 → `r9_browser.json`。
## 三′、首次正式预训练失败(2026-10-18,21850 步)与修正
现象:闭环到达 0–0.2%、穿柱 77–97%;MF 残差 2 万步停在 7–8、17k–19k 尖峰到 96;梯度 100% 被裁剪。用 last.pt 实测定位:
1. **多峰头塌成单峰(主因)**:同一状态 8 个候选间标准差 0.030、动作幅度 0.151。FK / 平滑损失加在 `one_step(随机噪声)` 上去对齐这一条老师 Y,最优解是条件均值 E[Y|X],左绕 / 右绕取平均正好穿柱。→ 改加在 r = t、t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上。
2. **MeanFlow 目标失控**:∂u/∂t 均方根 6–12,而 v 约 1.1,目标被自激的 JVP 项主导。时间嵌入 sin(1000·t) 太陡,JVP 又在 bf16 下算。→ 最高角频率 50;MF 分支 fp32;前 5000 步 p_neq = 0。
3. **闭环分布偏移被低估(推断)**:增广只有 1°。→ 增广 40% / 3° / 12 步收回;新增 DAgger 阶段(策略走到的状态请老师重规划,2 轮);开训前加老师轨迹回放自检;每次评估加开环误差与候选多样性。
## 四、正式训练(三段,每段跑完回传)
| 段 | 命令 | 耗时 | 回传 |
|---|---|---|---|
| 数据 | `FROM=kin TO=prep bash ml/pipeline.sh all` | 数小时 | `ml/data/stats.json`、`ml/data/r2.json`、`ml/runs/r4.json` |
| 训练 | `FROM=train TO=m5 bash ml/pipeline.sh all` | 约 3–6 小时 | `ml/runs/m5_gate_*.json` |
| 训练(含 DAgger) | `FRESH_FROM=prep FROM=prep TO=m5 bash ml/pipeline.sh all`(增广改了,样本要重打包;gen 不重跑) | 约 9–11 小时(未实测:prep 0.5 + 预训练约 5(fp32 变慢)+ DAgger 约 3.5 + m5 0.3) | `ml/runs/replay_teacher.json`、`ml/runs/base/log.jsonl`、`ml/runs/m5_gate_*.json` |
| 后训练与评测 | `FROM=apg TO=accept bash ml/pipeline.sh all` | 约 10–20 小时 | `ml/runs/acceptance.json`、`policy_bench.json`、`r9_browser.json` |
中途出错:把出错信息发我,修完用 `FROM=<出错阶段>` 接着跑,前面的不重做。
---
## 七、第一次真跑(2026-10-18,网页「开始训练」)暴露的一处阻塞 + 门 1 实测
**阻塞(已修,一行)**:`pipeline.sh` 的 `run() { echo "== $*"; "$@"; }` 把命令回显写进 **stdout**,而 `gate1` 是 `run node ml/gen/consistency.cjs … | tee ml/data/consistency.json.tmp` → 回显被一起存进 `consistency.json`,文件首行变成 `== node ml/gen/consistency.cjs 300 4242`。`gen` 阶段读它判 CONSENSUS 时 `json.load` 当场 `JSONDecodeError: Expecting value: line 1 column 1 (char 0)`,`set -e` 下整条 `all` 停在 gen(阶段 rc:kin 0、r4 0、gate1 0、gen 1)。
- 与平台无关,沙箱 Linux 同样会中,只是没跑到这一步。
- 修法:回显改走 stderr(`echo "== $*" >&2`)。全仓只有 `gate1` 这一处把 `run` 的输出重定向进文件(`check_r2` 等用 `--out` 自写,不受影响)。
- 已写坏的 `ml/data/consistency.json` 就地剥掉首行复用,**没有重跑 25–30 分钟的门禁**。
- 建议实现方顺手加一条自检:任何被 `tee`/`>` 落盘成 JSON 的产物,读之前先确认首字符是 `{`。
**门 1 实测(第一次真数据)**:`tasks 300 / pairs 1133 / consistentRate 0.838 / consistentByGateRule 0.838 / gate FAIL / deterministic 300/300`,分类看 `C 0.824`、`B 0.844`,`devP50 3.6`、`devP90 145.1`。
→ 0.838 < 0.90,**自动走共识分支**(`--consensus 4`),与方案 R1 首测 78% 同一侧;`worstAxisOfInconsistent [43,4,42,40,16,39]` 说明跳变在各轴上分布均匀,不是某一根轴的系统性问题。gen 因此按 4–5 h 那一档估,不是 1 h。
+62
View File
@@ -0,0 +1,62 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 512,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round29_rows.json",
"out": "docs/experiment_round29.json",
"trace": "ml/runs/round29_trace.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "1fd72f71eaf8170b45782beede0d91544e1f7142e98c0642560af814382f01ef",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 512,
"n_ref": 512,
"success": 0.55078125,
"collision_free_success": 0.546875,
"time_ok_1p3": 0.361328125,
"collision_rate": 0.017578125,
"collision_steps_mean": 0.1015625,
"travel_ratio_median": 2.425084352493286,
"time_mean_s": 2.322056770324707,
"brakes_mean": 19.228515625,
"start_fail": 0.0,
"goal_ratio_2s": 0.25053170323371887,
"err_fail_med": 194.51763916015625,
"err_success_med": 0.1961677223443985,
"dls_share": 0.634765625,
"time_budget": "teacher×2(≥3 s)"
},
"simulation_wall_s": 488.4280448999925
}
+779
View File
@@ -0,0 +1,779 @@
{
"trace": "ml/runs/round29_trace.pt",
"n": 512,
"reported_collision_tasks": 9,
"replayed_first_hits32": 9,
"replayed_first_hits64": 9,
"scope": "Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.",
"rows": [
{
"i": 21,
"task": {
"tid": 1223280991,
"id": "s0001_000564",
"cat": "A",
"mode": "pose",
"q0": [
178.767,
101.917,
25.283,
31.588,
-12.179,
-6.464
],
"T": [
320.779,
79.294,
350.853
],
"R": [
-0.5548626629953249,
-0.1382477148217311,
-0.820374910976869,
-0.29956278115972873,
-0.8867511636570162,
0.35204334093514156,
-0.7761375943462505,
0.44108949556693955,
0.4506112421399604
],
"exec_s": 2.86,
"travel": 581.023,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.659999847412109,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.35770511627197266,
-0.3957242965698242
],
"gap64_mm": [
0.5,
0.5,
0.3577231796451059,
-0.39572636491366353
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
139.78468322753906,
87.256591796875,
6.928672790527344,
103.78778839111328,
-94.0,
19.4688720703125
],
"endpoint": [
139.50450134277344,
87.4175796508789,
6.71871280670166,
104.29376983642578,
-94.0,
21.218488693237305
],
"dense_nsegments": 175,
"dense_gap64_min_mm": -0.39572636491366353
},
{
"i": 84,
"task": {
"tid": 1268963905,
"id": "s0002_000538",
"cat": "B",
"mode": "pose",
"q0": [
-49.198,
52.395,
108.796,
75.534,
82.302,
-121.823
],
"T": [
-85.062,
275.378,
888.929
],
"R": [
0.5139412462799992,
-0.4285630099475821,
-0.7431003578769356,
-0.5533754886400825,
-0.8275489878032067,
0.09454227286372051,
-0.6554692700230204,
0.36262435010704513,
-0.6624678231921397
],
"exec_s": 3.2600000000000002,
"travel": 349.09,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.980000019073486,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.030836105346679688,
-0.1565418243408203,
-0.11790847778320312
],
"gap64_mm": [
0.5,
0.030838235678215753,
-0.15655898804830848,
-0.11791701257236653
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-69.87329864501953,
36.559112548828125,
2.094588279724121,
102.22724151611328,
89.0,
-212.1793212890625
],
"endpoint": [
-69.79582214355469,
37.351837158203125,
-0.033895254135131836,
102.45509338378906,
89.0,
-214.75440979003906
],
"dense_nsegments": 258,
"dense_gap64_min_mm": -0.16882067924099076
},
{
"i": 145,
"task": {
"tid": 593213945,
"id": "s0003_000414",
"cat": "B",
"mode": "pose",
"q0": [
-49.395,
84.783,
2.744,
14.841,
-86.783,
81.932
],
"T": [
5.391,
506.723,
383.367
],
"R": [
-0.5269859333114162,
-0.7554822816510317,
-0.38927156099983334,
0.6402732880654818,
-0.05172298162073713,
-0.7664038424760652,
0.5588702377848656,
-0.6531242265324508,
0.5109724082909718
],
"exec_s": 2.96,
"travel": 560.55,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.599999904632568,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.3180522918701172,
-0.19827747344970703
],
"gap64_mm": [
0.5,
0.5,
0.318071359587325,
-0.19825289671655533
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
-36.05080032348633,
49.852294921875,
22.054914474487305,
67.3380126953125,
-94.0,
79.87274932861328
],
"endpoint": [
-35.60316848754883,
48.9223747253418,
21.386226654052734,
67.87915802001953,
-94.0,
79.27398681640625
],
"dense_nsegments": 93,
"dense_gap64_min_mm": -0.19825289671655533
},
{
"i": 164,
"task": {
"tid": 1743026973,
"id": "s0003_000814",
"cat": "C",
"mode": "pos",
"q0": [
-65.982,
5.595,
-64.669,
-72.912,
85.732,
-86.377
],
"T": [
-189.354,
221.034,
302.457
],
"R": null,
"exec_s": 2.3000000000000003,
"travel": 306.824,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.0399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.12672901153564453
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.12673094091899273
],
"pair32": [
0,
0,
0,
5
],
"pair64": [
0,
0,
0,
5
],
"boundary_joints": [
5
],
"previous": [
-33.6672248840332,
39.63850021362305,
-60.174774169921875,
-108.2870864868164,
88.27082061767578,
-74.56200408935547
],
"endpoint": [
-32.8936882019043,
39.478729248046875,
-58.639915466308594,
-107.65913391113281,
89.0,
-74.92655944824219
],
"dense_nsegments": 154,
"dense_gap64_min_mm": -0.12673094091899273
},
{
"i": 175,
"task": {
"tid": 444406422,
"id": "s0003_001100",
"cat": "B",
"mode": "pos",
"q0": [
-28.854,
43.33,
-137.874,
-6.895,
-88.001,
-43.745
],
"T": [
379.105,
293.88,
653.541
],
"R": null,
"exec_s": 2.58,
"travel": 465.057,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 0.8399999737739563,
"dls_it": 144,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.4698514938354492,
-0.6612262725830078,
-0.9359521865844727
],
"gap64_mm": [
0.5,
0.4698356780126254,
-0.6612412101671552,
-0.9359581587234
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-42.48794937133789,
11.756612777709961,
-124.7530288696289,
-46.12489700317383,
-94.0,
-54.57075500488281
],
"endpoint": [
-42.73639678955078,
10.6148681640625,
-124.47537231445312,
-48.52891159057617,
-94.0,
-55.451263427734375
],
"dense_nsegments": 241,
"dense_gap64_min_mm": -0.9544276230224238
},
{
"i": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 1.1399999856948853,
"dls_it": 102,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.06517887115478516,
-0.0172576904296875,
0.013133049011230469
],
"gap64_mm": [
0.5,
0.06521150921177998,
-0.017227151210720493,
0.013145878955246104
],
"pair32": [
0,
5,
5,
5
],
"pair64": [
0,
5,
5,
5
],
"boundary_joints": [
5
],
"previous": [
1.8212896585464478,
-9.327469825744629,
-150.87704467773438,
-23.00735855102539,
-94.0,
93.73664855957031
],
"endpoint": [
1.3598560094833374,
-9.66404914855957,
-150.52992248535156,
-23.200042724609375,
-94.0,
92.69589233398438
],
"dense_nsegments": 105,
"dense_gap64_min_mm": -0.020246891242656773
},
{
"i": 215,
"task": {
"tid": 169730876,
"id": "s0004_000544",
"cat": "B",
"mode": "pos",
"q0": [
47.051,
-1.179,
-94.31,
-43.128,
67.067,
-119.867
],
"T": [
-142.696,
-139.159,
726.163
],
"R": null,
"exec_s": 2.16,
"travel": 461.684,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.5399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.08817481994628906
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.08817155079043282
],
"pair32": [
0,
0,
0,
6
],
"pair64": [
0,
0,
0,
6
],
"boundary_joints": [
5
],
"previous": [
34.57734298706055,
32.23564910888672,
-71.76512908935547,
10.783411979675293,
88.44934844970703,
-105.16633605957031
],
"endpoint": [
34.31901931762695,
33.24732208251953,
-72.12022399902344,
10.171412467956543,
89.0,
-106.89080810546875
],
"dense_nsegments": 173,
"dense_gap64_min_mm": -0.08817155079043282
},
{
"i": 266,
"task": {
"tid": 458057184,
"id": "s0005_000271",
"cat": "B",
"mode": "pos",
"q0": [
44.394,
32.402,
-145.074,
-48.37,
-85.543,
13.576
],
"T": [
184.38,
-306.445,
433.207
],
"R": null,
"exec_s": 2.52,
"travel": 376.205,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 1.2200000286102295,
"dls_it": 304,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.2372283935546875,
-0.05915641784667969
],
"gap64_mm": [
0.5,
0.5,
0.23719947210205383,
-0.05918908805933398
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
20.49032974243164,
10.35391902923584,
-172.87489318847656,
-73.93492126464844,
-93.70915985107422,
14.725995063781738
],
"endpoint": [
20.34522819519043,
10.012239456176758,
-172.91363525390625,
-74.4600601196289,
-94.0,
13.266871452331543
],
"dense_nsegments": 146,
"dense_gap64_min_mm": -0.05918908805933398
},
{
"i": 325,
"task": {
"tid": 1206824904,
"id": "s0005_001755",
"cat": "B",
"mode": "pose",
"q0": [
-68.225,
58.481,
-31.386,
57.2,
-31.831,
-80.259
],
"T": [
30.805,
353.752,
661.419
],
"R": [
0.5493387160259536,
0.13571391775309405,
0.8245051289125273,
-0.09525700289583322,
-0.9701190077749987,
0.22314841283987508,
0.8301524429129026,
-0.2011239500451364,
-0.5199962290670785
],
"exec_s": 2.34,
"travel": 659.756,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.0999999046325684,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.3628082275390625,
0.004294395446777344,
-0.6861848831176758
],
"gap64_mm": [
0.5,
0.3628058862147938,
0.004307730814957722,
-0.686172816414107
],
"pair32": [
0,
18,
5,
5
],
"pair64": [
0,
18,
5,
5
],
"boundary_joints": [
5
],
"previous": [
-39.524147033691406,
54.68175506591797,
-18.574026107788086,
103.78845977783203,
-94.0,
-53.657958984375
],
"endpoint": [
-39.42676544189453,
55.778987884521484,
-18.88759994506836,
103.97470092773438,
-94.0,
-54.84250259399414
],
"dense_nsegments": 119,
"dense_gap64_min_mm": -0.686172816414107
}
]
}
+62
View File
@@ -0,0 +1,62 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round30_after_rows.json",
"out": "docs/experiment_round30_after.json",
"trace": "ml/runs/round30_after.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.57421875,
"collision_free_success": 0.57421875,
"time_ok_1p3": 0.390625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.4449527263641357,
"time_mean_s": 2.3076188564300537,
"brakes_mean": 19.25,
"start_fail": 0.0,
"goal_ratio_2s": 0.20997506380081177,
"err_fail_med": 229.68746948242188,
"err_success_med": 0.18899640440940857,
"dls_share": 0.625,
"time_budget": "teacher×2(≥3 s)"
},
"simulation_wall_s": 248.83432349999202
}
+62
View File
@@ -0,0 +1,62 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round30_before_rows.json",
"out": "docs/experiment_round30_before.json",
"trace": "ml/runs/round30_before.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.57421875,
"collision_free_success": 0.56640625,
"time_ok_1p3": 0.390625,
"collision_rate": 0.02734375,
"collision_steps_mean": 0.0625,
"travel_ratio_median": 2.4449527263641357,
"time_mean_s": 2.2993197441101074,
"brakes_mean": 19.23828125,
"start_fail": 0.0,
"goal_ratio_2s": 0.20997506380081177,
"err_fail_med": 229.68746948242188,
"err_success_med": 0.1950554996728897,
"dls_share": 0.625,
"time_budget": "teacher×2(≥3 s)"
},
"simulation_wall_s": 247.51454629999353
}
+880
View File
@@ -0,0 +1,880 @@
{
"trace": "ml/runs/round30_before.pt",
"n": 256,
"reported_collision_tasks": 7,
"replayed_first_hits32": 7,
"replayed_first_hits64": 7,
"scope": "Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.",
"rows": [
{
"i": 21,
"task": {
"tid": 1223280991,
"id": "s0001_000564",
"cat": "A",
"mode": "pose",
"q0": [
178.767,
101.917,
25.283,
31.588,
-12.179,
-6.464
],
"T": [
320.779,
79.294,
350.853
],
"R": [
-0.5548626629953249,
-0.1382477148217311,
-0.820374910976869,
-0.29956278115972873,
-0.8867511636570162,
0.35204334093514156,
-0.7761375943462505,
0.44108949556693955,
0.4506112421399604
],
"exec_s": 2.86,
"travel": 581.023,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.659999847412109,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.35770511627197266,
-0.3957242965698242
],
"gap64_mm": [
0.5,
0.5,
0.3577231796451059,
-0.39572636491366353
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
139.78468322753906,
87.256591796875,
6.928672790527344,
103.78778839111328,
-94.0,
19.4688720703125
],
"endpoint": [
139.50450134277344,
87.4175796508789,
6.71871280670166,
104.29376983642578,
-94.0,
21.218488693237305
],
"dense_nsegments": 175,
"dense_gap64_min_mm": -0.39572636491366353,
"certificate": {
"plan_start_it": 282,
"step": 0,
"checked": true,
"endpoint": [
139.50450134277344,
87.4175796508789,
6.71871280670166,
104.29376983642578,
-95.717529296875,
21.218488693237305
],
"midpoint": [
139.64459228515625,
87.33708190917969,
6.823692798614502,
104.04077911376953,
-94.8587646484375,
20.34368133544922
],
"recorded_gap32_mm": [
0.1606760025024414,
0.5
],
"replayed_gap64_mm": [
0.16066912536394184,
0.5
],
"command_delta": [
-0.2801789343357086,
0.16098570823669434,
-0.20995977520942688,
0.5059784054756165,
-1.717526912689209,
1.7496168613433838
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 1.717529296875
}
},
{
"i": 84,
"task": {
"tid": 1268963905,
"id": "s0002_000538",
"cat": "B",
"mode": "pose",
"q0": [
-49.198,
52.395,
108.796,
75.534,
82.302,
-121.823
],
"T": [
-85.062,
275.378,
888.929
],
"R": [
0.5139412462799992,
-0.4285630099475821,
-0.7431003578769356,
-0.5533754886400825,
-0.8275489878032067,
0.09454227286372051,
-0.6554692700230204,
0.36262435010704513,
-0.6624678231921397
],
"exec_s": 3.2600000000000002,
"travel": 349.09,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.980000019073486,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.030836105346679688,
-0.1565418243408203,
-0.11790847778320312
],
"gap64_mm": [
0.5,
0.030838235678215753,
-0.15655898804830848,
-0.11791701257236653
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-69.87329864501953,
36.559112548828125,
2.094588279724121,
102.22724151611328,
89.0,
-212.1793212890625
],
"endpoint": [
-69.79582214355469,
37.351837158203125,
-0.033895254135131836,
102.45509338378906,
89.0,
-214.75440979003906
],
"dense_nsegments": 258,
"dense_gap64_min_mm": -0.16882067924099076,
"certificate": {
"plan_start_it": 298,
"step": 0,
"checked": true,
"endpoint": [
-69.79582214355469,
37.351837158203125,
-0.033895254135131836,
102.45509338378906,
89.79981231689453,
-214.75440979003906
],
"midpoint": [
-69.83456420898438,
36.955474853515625,
1.0303465127944946,
102.34117126464844,
89.39990234375,
-213.4668731689453
],
"recorded_gap32_mm": [
0.5,
0.20721912384033203
],
"replayed_gap64_mm": [
0.5,
0.2071966639119509
],
"command_delta": [
0.07747504115104675,
0.7927232980728149,
-2.128483533859253,
0.22785267233848572,
0.7998088598251343,
-2.5750951766967773
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 0.7998123168945312
}
},
{
"i": 145,
"task": {
"tid": 593213945,
"id": "s0003_000414",
"cat": "B",
"mode": "pose",
"q0": [
-49.395,
84.783,
2.744,
14.841,
-86.783,
81.932
],
"T": [
5.391,
506.723,
383.367
],
"R": [
-0.5269859333114162,
-0.7554822816510317,
-0.38927156099983334,
0.6402732880654818,
-0.05172298162073713,
-0.7664038424760652,
0.5588702377848656,
-0.6531242265324508,
0.5109724082909718
],
"exec_s": 2.96,
"travel": 560.55,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 5.599999904632568,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.3180522918701172,
-0.19827747344970703
],
"gap64_mm": [
0.5,
0.5,
0.318071359587325,
-0.19825289671655533
],
"pair32": [
0,
0,
5,
5
],
"pair64": [
0,
0,
5,
5
],
"boundary_joints": [
5
],
"previous": [
-36.05080032348633,
49.852294921875,
22.054914474487305,
67.3380126953125,
-94.0,
79.87274932861328
],
"endpoint": [
-35.60316848754883,
48.9223747253418,
21.386226654052734,
67.87915802001953,
-94.0,
79.27398681640625
],
"dense_nsegments": 93,
"dense_gap64_min_mm": -0.19825289671655533,
"certificate": {
"plan_start_it": 278,
"step": 1,
"checked": true,
"endpoint": [
-35.60316848754883,
48.9223747253418,
21.3862247467041,
67.87915802001953,
-97.17477416992188,
79.27399444580078
],
"midpoint": [
-35.82698440551758,
49.387332916259766,
21.720569610595703,
67.60858154296875,
-96.70620727539062,
79.57337188720703
],
"recorded_gap32_mm": [
0.2614564895629883,
0.5
],
"replayed_gap64_mm": [
0.2614702651611598,
0.5
],
"command_delta": [
0.4476320445537567,
-0.929920494556427,
-0.6686887741088867,
0.5411425828933716,
-0.9371238946914673,
-0.5987589359283447
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 3.174774169921875
}
},
{
"i": 164,
"task": {
"tid": 1743026973,
"id": "s0003_000814",
"cat": "C",
"mode": "pos",
"q0": [
-65.982,
5.595,
-64.669,
-72.912,
85.732,
-86.377
],
"T": [
-189.354,
221.034,
302.457
],
"R": null,
"exec_s": 2.3000000000000003,
"travel": 306.824,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.0399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.12672901153564453
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.12673094091899273
],
"pair32": [
0,
0,
0,
5
],
"pair64": [
0,
0,
0,
5
],
"boundary_joints": [
5
],
"previous": [
-33.6672248840332,
39.63850021362305,
-60.174774169921875,
-108.2870864868164,
88.27082061767578,
-74.56200408935547
],
"endpoint": [
-32.8936882019043,
39.478729248046875,
-58.639915466308594,
-107.65913391113281,
89.0,
-74.92655944824219
],
"dense_nsegments": 154,
"dense_gap64_min_mm": -0.12673094091899273,
"certificate": {
"plan_start_it": 150,
"step": 1,
"checked": true,
"endpoint": [
-32.89369201660156,
39.478729248046875,
-58.639915466308594,
-107.65913391113281,
89.33978271484375,
-74.92655944824219
],
"midpoint": [
-33.28045654296875,
39.55861282348633,
-59.407344818115234,
-107.97311401367188,
88.8052978515625,
-74.7442855834961
],
"recorded_gap32_mm": [
0.1356372833251953,
0.5
],
"replayed_gap64_mm": [
0.13560383338968585,
0.5
],
"command_delta": [
0.773534893989563,
-0.15977251529693604,
1.534859299659729,
0.6279491186141968,
1.068959355354309,
-0.3645569384098053
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 0.33978271484375
}
},
{
"i": 175,
"task": {
"tid": 444406422,
"id": "s0003_001100",
"cat": "B",
"mode": "pos",
"q0": [
-28.854,
43.33,
-137.874,
-6.895,
-88.001,
-43.745
],
"T": [
379.105,
293.88,
653.541
],
"R": null,
"exec_s": 2.58,
"travel": 465.057,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 0.8399999737739563,
"dls_it": 144,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.4698514938354492,
-0.6612262725830078,
-0.9359521865844727
],
"gap64_mm": [
0.5,
0.4698356780126254,
-0.6612412101671552,
-0.9359581587234
],
"pair32": [
0,
6,
6,
6
],
"pair64": [
0,
6,
6,
6
],
"boundary_joints": [
5
],
"previous": [
-42.48794937133789,
11.756612777709961,
-124.7530288696289,
-46.12489700317383,
-94.0,
-54.57075500488281
],
"endpoint": [
-42.73639678955078,
10.6148681640625,
-124.47537231445312,
-48.52891159057617,
-94.0,
-55.451263427734375
],
"dense_nsegments": 241,
"dense_gap64_min_mm": -0.9544276230224238,
"certificate": {
"plan_start_it": 40,
"step": 1,
"checked": true,
"endpoint": [
-42.73639678955078,
10.6148681640625,
-124.47537231445312,
-48.52891159057617,
-98.31655883789062,
-55.45126724243164
],
"midpoint": [
-42.61217498779297,
11.18574047088623,
-124.61420440673828,
-47.326904296875,
-97.59821319580078,
-55.011009216308594
],
"recorded_gap32_mm": [
0.5,
0.5
],
"replayed_gap64_mm": [
0.5,
0.5
],
"command_delta": [
-0.24844767153263092,
-1.1417450904846191,
0.27765607833862305,
-2.4040162563323975,
-1.4366930723190308,
-0.8805096745491028
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 4.316558837890625
}
},
{
"i": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 1.1399999856948853,
"dls_it": 102,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.06517887115478516,
-0.0172576904296875,
0.013133049011230469
],
"gap64_mm": [
0.5,
0.06521150921177998,
-0.017227151210720493,
0.013145878955246104
],
"pair32": [
0,
5,
5,
5
],
"pair64": [
0,
5,
5,
5
],
"boundary_joints": [
5
],
"previous": [
1.8212896585464478,
-9.327469825744629,
-150.87704467773438,
-23.00735855102539,
-94.0,
93.73664855957031
],
"endpoint": [
1.3598560094833374,
-9.66404914855957,
-150.52992248535156,
-23.200042724609375,
-94.0,
92.69589233398438
],
"dense_nsegments": 105,
"dense_gap64_min_mm": -0.020246891242656773,
"certificate": {
"plan_start_it": 56,
"step": 0,
"checked": true,
"endpoint": [
1.3598560094833374,
-9.66404914855957,
-150.52992248535156,
-23.200042724609375,
-95.86970520019531,
92.69589233398438
],
"midpoint": [
1.5905728340148926,
-9.495759010314941,
-150.70347595214844,
-23.103700637817383,
-94.93485260009766,
93.21627044677734
],
"recorded_gap32_mm": [
0.5,
0.20821666717529297
],
"replayed_gap64_mm": [
0.5,
0.20822553884000428
],
"command_delta": [
-0.46143364906311035,
-0.33657926321029663,
0.3471294939517975,
-0.19268345832824707,
-1.8697090148925781,
-1.0407572984695435
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 1.8697052001953125
}
},
{
"i": 215,
"task": {
"tid": 169730876,
"id": "s0004_000544",
"cat": "B",
"mode": "pos",
"q0": [
47.051,
-1.179,
-94.31,
-43.128,
67.067,
-119.867
],
"T": [
-142.696,
-139.159,
726.163
],
"R": null,
"exec_s": 2.16,
"travel": 461.684,
"reached": true,
"err": 0,
"split": "val"
},
"first_hit_s": 3.5399999618530273,
"dls_it": -1,
"point_order": [
"q0",
"previous",
"midpoint",
"endpoint"
],
"gap32_mm": [
0.5,
0.5,
0.5,
-0.08817481994628906
],
"gap64_mm": [
0.5,
0.5,
0.5,
-0.08817155079043282
],
"pair32": [
0,
0,
0,
6
],
"pair64": [
0,
0,
0,
6
],
"boundary_joints": [
5
],
"previous": [
34.57734298706055,
32.23564910888672,
-71.76512908935547,
10.783411979675293,
88.44934844970703,
-105.16633605957031
],
"endpoint": [
34.31901931762695,
33.24732208251953,
-72.12022399902344,
10.171412467956543,
89.0,
-106.89080810546875
],
"dense_nsegments": 173,
"dense_gap64_min_mm": -0.08817155079043282,
"certificate": {
"plan_start_it": 176,
"step": 0,
"checked": true,
"endpoint": [
34.31901931762695,
33.24732208251953,
-72.12022399902344,
10.171412467956543,
89.79435729980469,
-106.89080810546875
],
"midpoint": [
34.44818115234375,
32.741485595703125,
-71.94267272949219,
10.477412223815918,
89.12185668945312,
-106.02857208251953
],
"recorded_gap32_mm": [
0.2809743881225586,
0.5
],
"replayed_gap64_mm": [
0.28098007564065064,
0.5
],
"command_delta": [
-0.2583238482475281,
1.011674404144287,
-0.3550943434238434,
-0.6119998693466187,
1.3450126647949219,
-1.7244750261306763
],
"clamped_reconstruction_max_abs_deg": 0.0,
"certified_vs_executed_max_abs_deg": 0.7943572998046875
}
}
]
}
+65
View File
@@ -0,0 +1,65 @@
{
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"same_config_except_output_paths": true,
"instrumentation_vs_round29_first_batch_max_abs_deg": 0.0,
"reached_only_after": [],
"reached_only_before": [],
"collision_tasks_before": [
21,
84,
145,
164,
175,
180,
215
],
"collision_tasks_after": [],
"statistics": {
"before": {
"active_certified_steps": 34069,
"certificate_endpoint_mismatch_gt_0p001deg": 5298,
"certificate_endpoint_max_abs_deg": 7.587158203125,
"certificate_midpoint_max_abs_deg": 5.78558349609375,
"uncertified_moving_steps": 0,
"executed_speed_ratio_max": 1.660003662109375,
"executed_speed_limit_violating_tasks": 256,
"executed_speed_max_deg_s": [
99.6002197265625,
99.60002899169922,
149.40032958984375,
149.400146484375,
199.20005798339844,
298.80029296875
],
"executed_travel_ratio_median_success": 2.4250845909118652,
"commanded_travel_ratio_median_success": 2.4449527263641357
},
"after": {
"active_certified_steps": 34107,
"certificate_endpoint_mismatch_gt_0p001deg": 0,
"certificate_endpoint_max_abs_deg": 0.0,
"certificate_midpoint_max_abs_deg": 0.0,
"uncertified_moving_steps": 0,
"executed_speed_ratio_max": 1.660003662109375,
"executed_speed_limit_violating_tasks": 256,
"executed_speed_max_deg_s": [
99.6002197265625,
99.60002899169922,
149.40032958984375,
149.400146484375,
199.20005798339844,
298.80029296875
],
"executed_travel_ratio_median_success": 2.4250845909118652,
"commanded_travel_ratio_median_success": 2.4449527263641357
}
},
"cpu_prediction_microbenchmark": {
"scope": "B=1, commit=2, 100 warmups then 1000 serial calls, CPU one thread; not full planning latency",
"before_p50_ms": 0.0388,
"before_p99_ms": 0.069102,
"after_p50_ms": 0.0518,
"after_p99_ms": 0.093933
}
}
+485
View File
@@ -0,0 +1,485 @@
{
"trace": "ml/runs/round30_after.pt",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"indices": [
0,
21,
84,
128,
145,
164,
175,
180,
197,
215,
224,
255
],
"max_step_deg": 0.05,
"dtype": "float64",
"device": "cuda",
"scope": "Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.",
"collision_tasks": 1,
"rows": [
{
"i": 0,
"task": {
"tid": 919549409,
"id": "s0001_000001",
"cat": "B",
"mode": "pos",
"q0": [
-129.897,
46.68,
43.494,
-97.461,
-85.102,
-139.71
],
"T": [
-116.172,
268.408,
179.282
],
"R": null,
"exec_s": 3.48,
"travel": 345.324,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 174,
"sampled_points": 6211,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.02673023799951224,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 21,
"task": {
"tid": 1223280991,
"id": "s0001_000564",
"cat": "A",
"mode": "pose",
"q0": [
178.767,
101.917,
25.283,
31.588,
-12.179,
-6.464
],
"T": [
320.779,
79.294,
350.853
],
"R": [
-0.5548626629953249,
-0.1382477148217311,
-0.820374910976869,
-0.29956278115972873,
-0.8867511636570162,
0.35204334093514156,
-0.7761375943462505,
0.44108949556693955,
0.4506112421399604
],
"exec_s": 2.86,
"travel": 581.023,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 286,
"sampled_points": 6059,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.009057896844284485,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 84,
"task": {
"tid": 1268963905,
"id": "s0002_000538",
"cat": "B",
"mode": "pose",
"q0": [
-49.198,
52.395,
108.796,
75.534,
82.302,
-121.823
],
"T": [
-85.062,
275.378,
888.929
],
"R": [
0.5139412462799992,
-0.4285630099475821,
-0.7431003578769356,
-0.5533754886400825,
-0.8275489878032067,
0.09454227286372051,
-0.6554692700230204,
0.36262435010704513,
-0.6624678231921397
],
"exec_s": 3.2600000000000002,
"travel": 349.09,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 326,
"sampled_points": 6251,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.006206321604070197,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 128,
"task": {
"tid": 408952743,
"id": "s0002_001588",
"cat": "A",
"mode": "pos",
"q0": [
-153.539,
100.454,
108.434,
3.379,
70.254,
-179.848
],
"T": [
-324.009,
70.778,
919.477
],
"R": null,
"exec_s": 0.8200000000000001,
"travel": 121.056,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 63,
"sampled_points": 2403,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.5,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 145,
"task": {
"tid": 593213945,
"id": "s0003_000414",
"cat": "B",
"mode": "pose",
"q0": [
-49.395,
84.783,
2.744,
14.841,
-86.783,
81.932
],
"T": [
5.391,
506.723,
383.367
],
"R": [
-0.5269859333114162,
-0.7554822816510317,
-0.38927156099983334,
0.6402732880654818,
-0.05172298162073713,
-0.7664038424760652,
0.5588702377848656,
-0.6531242265324508,
0.5109724082909718
],
"exec_s": 2.96,
"travel": 560.55,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 296,
"sampled_points": 4509,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.002145835308116517,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 164,
"task": {
"tid": 1743026973,
"id": "s0003_000814",
"cat": "C",
"mode": "pos",
"q0": [
-65.982,
5.595,
-64.669,
-72.912,
85.732,
-86.377
],
"T": [
-189.354,
221.034,
302.457
],
"R": null,
"exec_s": 2.3000000000000003,
"travel": 306.824,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 230,
"sampled_points": 3985,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.008857213584009926,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 175,
"task": {
"tid": 444406422,
"id": "s0003_001100",
"cat": "B",
"mode": "pos",
"q0": [
-28.854,
43.33,
-137.874,
-6.895,
-88.001,
-43.745
],
"T": [
379.105,
293.88,
653.541
],
"R": null,
"exec_s": 2.58,
"travel": 465.057,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 143,
"sampled_points": 5547,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.18812628629569694,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 95,
"sampled_points": 3153,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.1492959159660998,
"collision_segments": 1,
"first_collision_s": 1.0915789473684212
},
{
"i": 197,
"task": {
"tid": 1433345185,
"id": "s0004_000020",
"cat": "B",
"mode": "pose",
"q0": [
20.112,
-20.013,
-177.85,
32.746,
-70.888,
-30.111
],
"T": [
-288.364,
63.606,
672.54
],
"R": [
-0.5335375732231524,
-0.7509294450860695,
0.38915630081225366,
-0.8443344656004893,
0.4997538915602813,
-0.19324947107169785,
-0.049365657681080755,
-0.4316839311036288,
-0.9006730902323173
],
"exec_s": 2.14,
"travel": 634.022,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 214,
"sampled_points": 4641,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.0010684324191032601,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 215,
"task": {
"tid": 169730876,
"id": "s0004_000544",
"cat": "B",
"mode": "pos",
"q0": [
47.051,
-1.179,
-94.31,
-43.128,
67.067,
-119.867
],
"T": [
-142.696,
-139.159,
726.163
],
"R": null,
"exec_s": 2.16,
"travel": 461.684,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 216,
"sampled_points": 6605,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.2112382958686112,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 224,
"task": {
"tid": 1884410379,
"id": "s0004_000721",
"cat": "A",
"mode": "pose",
"q0": [
-127.447,
7.845,
-124.228,
-20.968,
-21.51,
-35.505
],
"T": [
-233.158,
-59.78,
648.987
],
"R": [
0.3964618999442966,
-0.020495209950317686,
0.9178223729358808,
-0.8179090164279511,
0.4461640898622928,
0.3632663564978371,
-0.41694460392049726,
-0.8947164641865291,
0.1601238457417738
],
"exec_s": 1.48,
"travel": 356.988,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 128,
"sampled_points": 4707,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.0041216155493710716,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 255,
"task": {
"tid": 712677274,
"id": "s0004_001686",
"cat": "C",
"mode": "pos",
"q0": [
-65.548,
71.865,
27.462,
-75.048,
60.325,
151.872
],
"T": [
-53.154,
50.204,
397.489
],
"R": null,
"exec_s": 1.84,
"travel": 238.182,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 184,
"sampled_points": 2495,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.004616787795093558,
"collision_segments": 0,
"first_collision_s": null
}
],
"audit_wall_s": 15.056756100006169
}
+485
View File
@@ -0,0 +1,485 @@
{
"trace": "ml/runs/round30_before.pt",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"indices": [
0,
21,
84,
128,
145,
164,
175,
180,
197,
215,
224,
255
],
"max_step_deg": 0.05,
"dtype": "float64",
"device": "cuda",
"scope": "Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.",
"collision_tasks": 7,
"rows": [
{
"i": 0,
"task": {
"tid": 919549409,
"id": "s0001_000001",
"cat": "B",
"mode": "pos",
"q0": [
-129.897,
46.68,
43.494,
-97.461,
-85.102,
-139.71
],
"T": [
-116.172,
268.408,
179.282
],
"R": null,
"exec_s": 3.48,
"travel": 345.324,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 174,
"sampled_points": 6211,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.02673023799951224,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 21,
"task": {
"tid": 1223280991,
"id": "s0001_000564",
"cat": "A",
"mode": "pose",
"q0": [
178.767,
101.917,
25.283,
31.588,
-12.179,
-6.464
],
"T": [
320.779,
79.294,
350.853
],
"R": [
-0.5548626629953249,
-0.1382477148217311,
-0.820374910976869,
-0.29956278115972873,
-0.8867511636570162,
0.35204334093514156,
-0.7761375943462505,
0.44108949556693955,
0.4506112421399604
],
"exec_s": 2.86,
"travel": 581.023,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 286,
"sampled_points": 6099,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.39572636491366353,
"collision_segments": 2,
"first_collision_s": 5.655
},
{
"i": 84,
"task": {
"tid": 1268963905,
"id": "s0002_000538",
"cat": "B",
"mode": "pose",
"q0": [
-49.198,
52.395,
108.796,
75.534,
82.302,
-121.823
],
"T": [
-85.062,
275.378,
888.929
],
"R": [
0.5139412462799992,
-0.4285630099475821,
-0.7431003578769356,
-0.5533754886400825,
-0.8275489878032067,
0.09454227286372051,
-0.6554692700230204,
0.36262435010704513,
-0.6624678231921397
],
"exec_s": 3.2600000000000002,
"travel": 349.09,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 326,
"sampled_points": 6037,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.16878710314368206,
"collision_segments": 3,
"first_collision_s": 5.961153846153847
},
{
"i": 128,
"task": {
"tid": 408952743,
"id": "s0002_001588",
"cat": "A",
"mode": "pos",
"q0": [
-153.539,
100.454,
108.434,
3.379,
70.254,
-179.848
],
"T": [
-324.009,
70.778,
919.477
],
"R": null,
"exec_s": 0.8200000000000001,
"travel": 121.056,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 63,
"sampled_points": 2403,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.5,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 145,
"task": {
"tid": 593213945,
"id": "s0003_000414",
"cat": "B",
"mode": "pose",
"q0": [
-49.395,
84.783,
2.744,
14.841,
-86.783,
81.932
],
"T": [
5.391,
506.723,
383.367
],
"R": [
-0.5269859333114162,
-0.7554822816510317,
-0.38927156099983334,
0.6402732880654818,
-0.05172298162073713,
-0.7664038424760652,
0.5588702377848656,
-0.6531242265324508,
0.5109724082909718
],
"exec_s": 2.96,
"travel": 560.55,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 296,
"sampled_points": 4489,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.19825289671655533,
"collision_segments": 5,
"first_collision_s": 5.597
},
{
"i": 164,
"task": {
"tid": 1743026973,
"id": "s0003_000814",
"cat": "C",
"mode": "pos",
"q0": [
-65.982,
5.595,
-64.669,
-72.912,
85.732,
-86.377
],
"T": [
-189.354,
221.034,
302.457
],
"R": null,
"exec_s": 2.3000000000000003,
"travel": 306.824,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 230,
"sampled_points": 3865,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.12673094091895543,
"collision_segments": 4,
"first_collision_s": 3.0387500000000003
},
{
"i": 175,
"task": {
"tid": 444406422,
"id": "s0003_001100",
"cat": "B",
"mode": "pos",
"q0": [
-28.854,
43.33,
-137.874,
-6.895,
-88.001,
-43.745
],
"T": [
379.105,
293.88,
653.541
],
"R": null,
"exec_s": 2.58,
"travel": 465.057,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 145,
"sampled_points": 5769,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.954427674326439,
"collision_segments": 2,
"first_collision_s": 0.8236
},
{
"i": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 103,
"sampled_points": 3385,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.1492959159660998,
"collision_segments": 2,
"first_collision_s": 1.0915789473684212
},
{
"i": 197,
"task": {
"tid": 1433345185,
"id": "s0004_000020",
"cat": "B",
"mode": "pose",
"q0": [
20.112,
-20.013,
-177.85,
32.746,
-70.888,
-30.111
],
"T": [
-288.364,
63.606,
672.54
],
"R": [
-0.5335375732231524,
-0.7509294450860695,
0.38915630081225366,
-0.8443344656004893,
0.4997538915602813,
-0.19324947107169785,
-0.049365657681080755,
-0.4316839311036288,
-0.9006730902323173
],
"exec_s": 2.14,
"travel": 634.022,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 214,
"sampled_points": 4317,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.0010684324191032601,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 215,
"task": {
"tid": 169730876,
"id": "s0004_000544",
"cat": "B",
"mode": "pos",
"q0": [
47.051,
-1.179,
-94.31,
-43.128,
67.067,
-119.867
],
"T": [
-142.696,
-139.159,
726.163
],
"R": null,
"exec_s": 2.16,
"travel": 461.684,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 216,
"sampled_points": 6341,
"initial_gap64_mm": 0.5,
"min_gap64_mm": -0.08817155079042394,
"collision_segments": 3,
"first_collision_s": 3.538888888888889
},
{
"i": 224,
"task": {
"tid": 1884410379,
"id": "s0004_000721",
"cat": "A",
"mode": "pose",
"q0": [
-127.447,
7.845,
-124.228,
-20.968,
-21.51,
-35.505
],
"T": [
-233.158,
-59.78,
648.987
],
"R": [
0.3964618999442966,
-0.020495209950317686,
0.9178223729358808,
-0.8179090164279511,
0.4461640898622928,
0.3632663564978371,
-0.41694460392049726,
-0.8947164641865291,
0.1601238457417738
],
"exec_s": 1.48,
"travel": 356.988,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 128,
"sampled_points": 4707,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.0041216155493710716,
"collision_segments": 0,
"first_collision_s": null
},
{
"i": 255,
"task": {
"tid": 712677274,
"id": "s0004_001686",
"cat": "C",
"mode": "pos",
"q0": [
-65.548,
71.865,
27.462,
-75.048,
60.325,
151.872
],
"T": [
-53.154,
50.204,
397.489
],
"R": null,
"exec_s": 1.84,
"travel": 238.182,
"reached": true,
"err": 0,
"split": "val"
},
"steps": 184,
"sampled_points": 2495,
"initial_gap64_mm": 0.5,
"min_gap64_mm": 0.004616787795093558,
"collision_segments": 0,
"first_collision_s": null
}
],
"audit_wall_s": 14.869853199998033
}
@@ -0,0 +1,71 @@
{
"task_index": 180,
"task": {
"tid": 737760595,
"id": "s0003_001280",
"cat": "B",
"mode": "pos",
"q0": [
41.213,
49.353,
-149.808,
-8.981,
-88.566,
83.45
],
"T": [
118.145,
2.733,
266.082
],
"R": null,
"exec_s": 1.78,
"travel": 228.67,
"reached": true,
"err": 0,
"split": "val"
},
"segment_start_it": 54,
"start_s": 1.08,
"end_s": 1.1,
"original_discrete_col": 0,
"certified": true,
"recorded_endpoint_midpoint_gap32_mm": [
0.0651998519897461,
0.11720466613769531
],
"previous_midpoint_endpoint_gap64_mm": [
0.5,
0.11723191087430074,
0.06521150921177998
],
"previous": [
3.0118656158447266,
-7.4926042556762695,
-151.498291015625,
-22.7498836517334,
-94.0,
93.50477600097656
],
"endpoint": [
1.8212896585464478,
-9.327469825744629,
-150.87704467773438,
-23.00735855102539,
-94.0,
93.73664855957031
],
"dense_max_step_deg": 0.01,
"dense_points": 185,
"min_gap64_mm": -0.14927288174659914,
"min_at_fraction": 0.7663043478260869,
"min_at_q": [
2.09952208334985,
-8.898669719696045,
-151.0222272458284,
-22.947187786516935,
-94.0,
93.68246095076852
],
"scope": "One executed segment; finite sampling demonstrates a collision between certified checkpoints."
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,658 @@
{
"segments": 48,
"kin_fp": "2ebcbf17",
"root_gap_cap_mm": 20.0,
"formula": "L = ceil(log2(bound_pair/(ga+gb)))\uff0c\u67e5\u8be2\u6570 = 2^L + 1\uff1bbound_pair \u53d6\u6700\u7d27\u7ec4\u5bf9\uff0c\u4e0d\u7528\u5168\u7ec4\u5bf9\u6700\u5927\u503c",
"levels_needed_by_status": {
"safe": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
2,
2,
2,
2,
3,
3,
3,
3,
3,
4,
4,
4,
4,
4
],
"unresolved": [
5,
5,
5,
5,
5,
6,
6,
6,
6,
6
],
"collision": [
4
]
},
"queries_needed_safe": [
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
3,
3,
3,
5,
5,
5,
5,
9,
9,
9,
9,
9,
17,
17,
17,
17,
17
],
"model_vs_measured_max_excess": 20,
"unresolved_needed_queries": [
33,
33,
33,
33,
33,
65,
65,
65,
65,
65
],
"collision_dense_check": [
{
"i": 180,
"start_it": 54,
"status": "collision",
"tight_group_pair": 5,
"bound_pair_mm": 29.09,
"endpoint_gap_sum_mm": 1.98854,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 5
}
],
"note": "\u6a21\u578b\u662f\u201c\u754c\u53ea\u968f\u4e8c\u5206\u51cf\u534a\u201d\u4e0b\u7684\u4e0b\u754c\uff1b\u5b9e\u6d4b\u67e5\u8be2\u53ef\u80fd\u66f4\u5c0f\uff08\u591a\u5c42\u91cc\u6709\u7684\u7ec4\u5bf9\u5148\u6ee1\u8db3\uff09\u6216\u66f4\u5927\uff08\u78b0\u649e\u6bb5\u63d0\u524d\u7ec8\u6b62\uff09\u3002",
"rows": [
{
"i": 0,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.382,
"endpoint_gap_sum_mm": 17.77168,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 0,
"start_it": 8,
"status": "unresolved",
"tight_group_pair": 13,
"bound_pair_mm": 8.2833,
"endpoint_gap_sum_mm": 0.25216,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 0,
"start_it": 91,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.459,
"endpoint_gap_sum_mm": 14.45459,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 0,
"start_it": 173,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.8776,
"endpoint_gap_sum_mm": 15.48695,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 21,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.7095,
"endpoint_gap_sum_mm": 12.81844,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 21,
"start_it": 100,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 1.3371,
"endpoint_gap_sum_mm": 0.03365,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 21,
"start_it": 127,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5104,
"endpoint_gap_sum_mm": 0.68046,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 4
},
{
"i": 21,
"start_it": 282,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 3.5692,
"endpoint_gap_sum_mm": 1.72327,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 4
},
{
"i": 84,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 11.7042,
"endpoint_gap_sum_mm": 13.79742,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 84,
"start_it": 170,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.9891,
"endpoint_gap_sum_mm": 0.3999,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 18
},
{
"i": 84,
"start_it": 234,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 7.5939,
"endpoint_gap_sum_mm": 0.28698,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 31
},
{
"i": 84,
"start_it": 325,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.5738,
"endpoint_gap_sum_mm": 10.95173,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.4648,
"endpoint_gap_sum_mm": 16.52712,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 31,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.1155,
"endpoint_gap_sum_mm": 14.24909,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 128,
"start_it": 62,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 0.3619,
"endpoint_gap_sum_mm": 12.62201,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 145,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5691,
"endpoint_gap_sum_mm": 11.60111,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 145,
"start_it": 32,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 4.6053,
"endpoint_gap_sum_mm": 0.67129,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 28
},
{
"i": 145,
"start_it": 138,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 9.6062,
"endpoint_gap_sum_mm": 1.11765,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 15
},
{
"i": 145,
"start_it": 294,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 8.2956,
"endpoint_gap_sum_mm": 0.61574,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 17
},
{
"i": 164,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 12.0298,
"endpoint_gap_sum_mm": 11.50625,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 164,
"start_it": 84,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 18.0143,
"endpoint_gap_sum_mm": 1.7857,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 18
},
{
"i": 164,
"start_it": 182,
"status": "unresolved",
"tight_group_pair": 5,
"bound_pair_mm": 14.1522,
"endpoint_gap_sum_mm": 0.38976,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 164,
"start_it": 227,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 5.6457,
"endpoint_gap_sum_mm": 1.13586,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 10
},
{
"i": 175,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.5456,
"endpoint_gap_sum_mm": 15.18067,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 175,
"start_it": 46,
"status": "unresolved",
"tight_group_pair": 6,
"bound_pair_mm": 30.8668,
"endpoint_gap_sum_mm": 1.26529,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 32
},
{
"i": 175,
"start_it": 75,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 17.8885,
"endpoint_gap_sum_mm": 10.74989,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 175,
"start_it": 142,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 0.8185,
"endpoint_gap_sum_mm": 10.77208,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 180,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.4654,
"endpoint_gap_sum_mm": 14.04707,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 180,
"start_it": 14,
"status": "unresolved",
"tight_group_pair": 4,
"bound_pair_mm": 17.6609,
"endpoint_gap_sum_mm": 0.46562,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 180,
"start_it": 52,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 27.7231,
"endpoint_gap_sum_mm": 3.97272,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 15
},
{
"i": 180,
"start_it": 54,
"status": "collision",
"tight_group_pair": 5,
"bound_pair_mm": 29.09,
"endpoint_gap_sum_mm": 1.98854,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 5
},
{
"i": 180,
"start_it": 94,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.1032,
"endpoint_gap_sum_mm": 12.22495,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 197,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.0839,
"endpoint_gap_sum_mm": 6.87225,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 197,
"start_it": 80,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 3.3325,
"endpoint_gap_sum_mm": 0.50053,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 29
},
{
"i": 197,
"start_it": 82,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 5.7899,
"endpoint_gap_sum_mm": 0.16354,
"levels_needed": 6,
"queries_needed": 65,
"queries_used_batch32": 17
},
{
"i": 197,
"start_it": 211,
"status": "safe",
"tight_group_pair": 5,
"bound_pair_mm": 18.6183,
"endpoint_gap_sum_mm": 5.55945,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 11
},
{
"i": 215,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.4256,
"endpoint_gap_sum_mm": 12.47554,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 215,
"start_it": 104,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.7208,
"endpoint_gap_sum_mm": 14.00267,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 215,
"start_it": 178,
"status": "unresolved",
"tight_group_pair": 6,
"bound_pair_mm": 25.7963,
"endpoint_gap_sum_mm": 1.07014,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 30
},
{
"i": 215,
"start_it": 215,
"status": "safe",
"tight_group_pair": 6,
"bound_pair_mm": 9.2152,
"endpoint_gap_sum_mm": 8.57509,
"levels_needed": 1,
"queries_needed": 3,
"queries_used_batch32": 3
},
{
"i": 224,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 6.492,
"endpoint_gap_sum_mm": 10.02154,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 224,
"start_it": 52,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 3.5989,
"endpoint_gap_sum_mm": 0.12354,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 32
},
{
"i": 224,
"start_it": 61,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 2.7788,
"endpoint_gap_sum_mm": 0.78961,
"levels_needed": 2,
"queries_needed": 5,
"queries_used_batch32": 5
},
{
"i": 224,
"start_it": 127,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.4884,
"endpoint_gap_sum_mm": 7.29024,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 255,
"start_it": 0,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 1.5332,
"endpoint_gap_sum_mm": 11.184,
"levels_needed": 0,
"queries_needed": 2,
"queries_used_batch32": 2
},
{
"i": 255,
"start_it": 25,
"status": "unresolved",
"tight_group_pair": 18,
"bound_pair_mm": 2.9831,
"endpoint_gap_sum_mm": 0.14423,
"levels_needed": 5,
"queries_needed": 33,
"queries_used_batch32": 29
},
{
"i": 255,
"start_it": 50,
"status": "safe",
"tight_group_pair": 4,
"bound_pair_mm": 7.2365,
"endpoint_gap_sum_mm": 0.61112,
"levels_needed": 4,
"queries_needed": 17,
"queries_used_batch32": 25
},
{
"i": 255,
"start_it": 181,
"status": "safe",
"tight_group_pair": 18,
"bound_pair_mm": 4.2961,
"endpoint_gap_sum_mm": 0.74979,
"levels_needed": 3,
"queries_needed": 9,
"queries_used_batch32": 12
}
]
}
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"baseline_commit": "95da491",
"poses": "first 64 existing parity poses",
"n_poses": 64,
"kin_fp": "2ebcbf17",
"dtype": "float64",
"degenerate_capsules": [
686
],
"pairs_with_second_degenerate": 670,
"evaluated_pairs": 42880,
"changed_pairs_over_1e_8": 20661,
"old_minus_new_min_mm": 0.0,
"old_minus_new_max_mm": 86.00309853133399,
"new_reverse_max_abs_mm": 0.0,
"worst": {
"pose": 29,
"first_capsule": 430,
"second_capsule": 686,
"old_distance_mm": 493.354753665718,
"new_distance_mm": 407.35165513438403
},
"scope": "Raw capsule distances; does not imply these pairs win the group minimum."
}
@@ -0,0 +1,16 @@
{
"baseline_commit": "95da491",
"sample_source": "docs/experiment_round31_certificates.json",
"segments": 48,
"dense_points": 7586,
"max_step_deg": 0.01,
"cap_mm": 0.5,
"dtype": "float64",
"device": "cuda",
"kin_fp": "2ebcbf17",
"max_group_gap_difference_mm": 0.0,
"group_gaps_changed_over_1e_8": 0,
"minimum_gaps_changed_over_1e_8": 0,
"collision_bool_mismatches": 0,
"wall_s": 4.18259759999637
}
+10
View File
@@ -0,0 +1,10 @@
{
"n": 2000,
"fk_max_abs_mm": 4.547473508864641e-13,
"orient_max_abs": 3.3306690738754696e-16,
"gap_max_abs_mm": 2.5224267119483557e-13,
"collide_bool_mismatch": 0,
"feat_max_abs": 5.960463655974024e-08,
"grad_rel_err_max": 1.8474076070450906e-06,
"PASS": true
}
@@ -0,0 +1,22 @@
{
"scope": "全部 48 段 × 15 组对 = 720 项检验;观察值按 cap=2.0 mm 钳制,故可检验命题取 min(预测下限, cap)。违例=实测最小间隙低于该下限。",
"checks": 720,
"violations": [],
"worst_margin_mm": 0.0,
"min_slack_mm": 1e-06,
"dense_cap_mm": 2.0,
"grid_points_per_segment": 121,
"supersedes": "docs/experiment_round32_uniform_validation.json 里 bound_checks=61 的部分覆盖版本",
"kin_fp": "2ebcbf17",
"wall_s": 1.66,
"nontrivial": {
"definition": "只有预测下限 < 观察 cap 才是有信息量的检验;其余组对被钳制恒等式自动满足",
"checks": 61,
"min_slack_mm": 0.035347,
"p10_slack_mm": 0.544274,
"violations": 0,
"pred_mm_median_all": 15.8639,
"pred_mm_max_all": 19.9582
},
"note": "720 项总检验 0 违例,其中有信息量的是 61 项;预测下限的中位数 15.9 mm 远大于观察 cap 2 mm,所以大部分组对无法在本网格上证伪"
}
@@ -0,0 +1,190 @@
{
"device": "cuda float64",
"torch": "2.10.0+cu130",
"kin_fp": "2ebcbf17",
"roots_per_call": 2,
"pairs": 24,
"warm": 3,
"reps": 7,
"segment_sample": "docs/experiment_round31_certificates.json 的 48 段两两成对",
"design": "每对根轮流按正序/逆序测完全部配置(3次预热+7次计时);uniform_auto 的宽相位=最大子段界+2eps,uniform_cap20 固定 20 mm,bisect 为第31轮逐层二分口径",
"results": [
{
"kind": "uniform_auto",
"n_or_budget": 8,
"points": 18,
"cap_used_mm": 5.9191,
"calls": 168,
"p50_ms": 9.684,
"p90_ms": 13.027,
"p99_ms": 15.157,
"max_ms": 17.537,
"over_20ms": 0,
"roots": 48,
"safe": 26,
"collision": 1,
"rejected": 21
},
{
"kind": "uniform_auto",
"n_or_budget": 16,
"points": 34,
"cap_used_mm": 2.9597,
"calls": 168,
"p50_ms": 14.137,
"p90_ms": 18.454,
"p99_ms": 20.866,
"max_ms": 21.632,
"over_20ms": 5,
"roots": 48,
"safe": 30,
"collision": 1,
"rejected": 17
},
{
"kind": "uniform_auto",
"n_or_budget": 32,
"points": 66,
"cap_used_mm": 1.4799,
"calls": 168,
"p50_ms": 20.209,
"p90_ms": 27.88,
"p99_ms": 32.268,
"max_ms": 35.563,
"over_20ms": 87,
"roots": 48,
"safe": 34,
"collision": 1,
"rejected": 13
},
{
"kind": "uniform_auto",
"n_or_budget": 64,
"points": 130,
"cap_used_mm": 0.7401,
"calls": 168,
"p50_ms": 31.429,
"p90_ms": 48.751,
"p99_ms": 57.697,
"max_ms": 62.084,
"over_20ms": 165,
"roots": 48,
"safe": 36,
"collision": 1,
"rejected": 11
},
{
"kind": "uniform_auto",
"n_or_budget": 128,
"points": 258,
"cap_used_mm": 0.3701,
"calls": 168,
"p50_ms": 59.761,
"p90_ms": 89.063,
"p99_ms": 109.086,
"max_ms": 117.345,
"over_20ms": 168,
"roots": 48,
"safe": 38,
"collision": 1,
"rejected": 9
},
{
"kind": "uniform_cap20",
"n_or_budget": 8,
"points": 18,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 13.129,
"p90_ms": 16.44,
"p99_ms": 20.963,
"max_ms": 24.67,
"over_20ms": 3,
"roots": 48,
"safe": 26,
"collision": 1,
"rejected": 21
},
{
"kind": "uniform_cap20",
"n_or_budget": 32,
"points": 66,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 28.415,
"p90_ms": 39.579,
"p99_ms": 53.023,
"max_ms": 59.983,
"over_20ms": 153,
"roots": 48,
"safe": 34,
"collision": 1,
"rejected": 13
},
{
"kind": "uniform_cap20",
"n_or_budget": 64,
"points": 130,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 50.85,
"p90_ms": 75.512,
"p99_ms": 88.355,
"max_ms": 91.492,
"over_20ms": 168,
"roots": 48,
"safe": 36,
"collision": 1,
"rejected": 11
},
{
"kind": "uniform_cap20",
"n_or_budget": 128,
"points": 258,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 98.46,
"p90_ms": 139.982,
"p99_ms": 167.042,
"max_ms": 177.743,
"over_20ms": 168,
"roots": 48,
"safe": 38,
"collision": 1,
"rejected": 9
},
{
"kind": "bisect",
"n_or_budget": 32,
"points": null,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 54.885,
"p90_ms": 82.151,
"p99_ms": 117.205,
"max_ms": 118.716,
"over_20ms": 133,
"roots": 48,
"safe": 37,
"collision": 1,
"rejected": 10
},
{
"kind": "bisect",
"n_or_budget": 65,
"points": null,
"cap_used_mm": 20.0,
"calls": 168,
"p50_ms": 71.825,
"p90_ms": 108.559,
"p99_ms": 122.533,
"max_ms": 127.319,
"over_20ms": 133,
"roots": 48,
"safe": 41,
"collision": 1,
"rejected": 6
}
],
"wall_s": 108.63
}
@@ -0,0 +1,62 @@
{
"segments": 48,
"kin_fp": "2ebcbf17",
"dense_cap_mm": 2.0,
"eps_mm": 0.0001,
"bound_falsification": "每条组对:预测下限 (ga+gb-bound)/2 应 ≤ 网格上该组对的最小实测间隙",
"per_segment": [
{
"n": 8,
"safe": 26,
"collision": 1,
"unproven": 21,
"outside_box": 0,
"false_accept_vs_dense": 0,
"cap_required_mm": 5.919107314806348,
"known_missed_segment_collision": true
},
{
"n": 16,
"safe": 30,
"collision": 1,
"unproven": 17,
"outside_box": 0,
"false_accept_vs_dense": 0,
"cap_required_mm": 2.959653657403174,
"known_missed_segment_collision": true
},
{
"n": 32,
"safe": 34,
"collision": 1,
"unproven": 13,
"outside_box": 0,
"false_accept_vs_dense": 0,
"cap_required_mm": 1.479926828701587,
"known_missed_segment_collision": true
},
{
"n": 64,
"safe": 36,
"collision": 1,
"unproven": 11,
"outside_box": 0,
"false_accept_vs_dense": 0,
"cap_required_mm": 0.7400634143507935,
"known_missed_segment_collision": true
},
{
"n": 128,
"safe": 38,
"collision": 1,
"unproven": 9,
"outside_box": 0,
"false_accept_vs_dense": 0,
"cap_required_mm": 0.37013170717539673,
"known_missed_segment_collision": true
}
],
"bound_checks": 61,
"bound_violations": [],
"worst_margin_mm": 0.0
}
+70
View File
@@ -0,0 +1,70 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round33_base_rows.json",
"out": "docs/experiment_round33_base.json",
"trace": "ml/runs/round33_base.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 0,
"gap_margin": 0.0,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.57421875,
"collision_free_success": 0.57421875,
"time_ok_1p3": 0.390625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.4449527263641357,
"time_mean_s": 2.3076188564300537,
"brakes_mean": 19.25,
"start_fail": 0.0,
"goal_ratio_2s": 0.20997506380081177,
"err_fail_med": 229.68746948242188,
"err_success_med": 0.18899640440940857,
"dls_share": 0.625,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.6600004434585571,
"speed_ratio_p95": 1.6600035429000854,
"speed_ratio_max": 1.660003662109375,
"over_vmax_rate": 1.0,
"accel_ratio_p95": 1.5835107564926147,
"accel_ratio_max": 1.6600004434585571
},
"simulation_wall_s": 296.2702505999914
}
+70
View File
@@ -0,0 +1,70 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round33_both_rows.json",
"out": "docs/experiment_round33_both.json",
"trace": "ml/runs/round33_both.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 1,
"gap_margin": 0.5,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.51953125,
"collision_free_success": 0.51953125,
"time_ok_1p3": 0.26171875,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.5444939136505127,
"time_mean_s": 2.4729323387145996,
"brakes_mean": 19.97265625,
"start_fail": 0.0,
"goal_ratio_2s": 0.2868920564651489,
"err_fail_med": 202.12567138671875,
"err_success_med": 0.22085000574588776,
"dls_share": 0.62109375,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.0000016689300537,
"speed_ratio_p95": 1.0000101327896118,
"speed_ratio_max": 1.0000101327896118,
"over_vmax_rate": 0.0,
"accel_ratio_p95": 1.0000016689300537,
"accel_ratio_max": 1.0000101327896118
},
"simulation_wall_s": 277.54961390000244
}
+70
View File
@@ -0,0 +1,70 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round33_cap_rows.json",
"out": "docs/experiment_round33_cap.json",
"trace": "ml/runs/round33_cap.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 1,
"gap_margin": 0.0,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.5234375,
"collision_free_success": 0.5234375,
"time_ok_1p3": 0.265625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.615795850753784,
"time_mean_s": 2.486567258834839,
"brakes_mean": 19.5546875,
"start_fail": 0.0,
"goal_ratio_2s": 0.27920001745224,
"err_fail_med": 204.71360778808594,
"err_success_med": 0.21751396358013153,
"dls_share": 0.61328125,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.0000016689300537,
"speed_ratio_p95": 1.0000101327896118,
"speed_ratio_max": 1.0000101327896118,
"over_vmax_rate": 0.0,
"accel_ratio_p95": 1.0000016689300537,
"accel_ratio_max": 1.0000101327896118
},
"simulation_wall_s": 325.18856850000157
}
+202
View File
@@ -0,0 +1,202 @@
{
"device": "NVIDIA GeForce RTX 3090",
"torch": "2.10.0+cu130",
"act_scale": 1.0,
"cfg": {},
"B": 1,
"commit_n": 2,
"fp64_note": "3090 fp64 \u541e\u5410\u7ea6 fp32 \u7684 1/54\uff1b\u8ba4\u8bc1\u76ee\u524d\u8981\u6c42 fp64",
"inference": {
"sample_K1_steps1": {
"p50": 7.453,
"p95": 9.912,
"p99": 10.766,
"max": 11.109,
"reps": 120
},
"sample_K8_steps1": {
"p50": 8.208,
"p95": 10.685,
"p99": 11.22,
"max": 15.224,
"reps": 120
},
"sample_K1_steps2": {
"p50": 13.149,
"p95": 17.621,
"p99": 18.438,
"max": 19.296,
"reps": 120
},
"sample_K8_steps2": {
"p50": 14.167,
"p95": 19.241,
"p99": 24.77,
"max": 25.703,
"reps": 120
},
"sample_K1_steps4": {
"p50": 19.193,
"p95": 22.338,
"p99": 23.666,
"max": 25.787,
"reps": 120
},
"sample_K8_steps4": {
"p50": 21.509,
"p95": 24.732,
"p99": 35.655,
"max": 41.514,
"reps": 120
},
"sample_K1_steps8": {
"p50": 40.658,
"p95": 55.162,
"p99": 60.394,
"max": 60.398,
"reps": 120
},
"sample_K8_steps8": {
"p50": 40.416,
"p95": 47.997,
"p99": 62.49,
"max": 71.976,
"reps": 120
},
"sample_K1_steps16": {
"p50": 72.429,
"p95": 90.188,
"p99": 107.536,
"max": 112.674,
"reps": 120
},
"sample_K8_steps16": {
"p50": 79.893,
"p95": 99.933,
"p99": 105.746,
"max": 106.078,
"reps": 120
}
},
"encode_once": {
"p50": 2.265,
"p95": 4.001,
"p99": 6.086,
"max": 6.136,
"reps": 120
},
"tiny_op_ms": 0.028,
"sync_only_ms": 0.005,
"components": {
"features_f32": {
"p50": 12.443,
"p95": 14.227,
"p99": 19.678,
"max": 20.531,
"reps": 120
},
"endpoint_score_K8_f32": {
"p50": 4.278,
"p95": 5.841,
"p99": 6.322,
"max": 8.883,
"reps": 120
},
"dls_fb_1step_f64": {
"p50": 7.191,
"p95": 12.26,
"p99": 14.642,
"max": 15.384,
"reps": 120
},
"dls_fb_1step_f32": {
"p50": 7.619,
"p95": 10.142,
"p99": 11.597,
"max": 12.022,
"reps": 120
},
"discrete_cert_4pts_f32": {
"p50": 8.214,
"p95": 13.365,
"p99": 14.235,
"max": 14.991,
"reps": 120
},
"cert_n8_f64_2roots": {
"p50": 11.265,
"p95": 14.64,
"p99": 16.788,
"max": 17.761,
"reps": 120
},
"cert_n16_f64_2roots": {
"p50": 14.574,
"p95": 16.731,
"p99": 18.459,
"max": 22.346,
"reps": 120
},
"cert_n32_f64_2roots": {
"p50": 21.528,
"p95": 24.746,
"p99": 28.037,
"max": 28.066,
"reps": 120
}
},
"cycle_K8_steps16_discrete": {
"p50": 116.474,
"p95": 134.474,
"p99": 142.593,
"max": 152.768,
"reps": 120
},
"cycle_K8_steps4_discrete": {
"p50": 55.919,
"p95": 65.767,
"p99": 74.809,
"max": 78.059,
"reps": 120
},
"cycle_K1_steps4_discrete": {
"p50": 51.087,
"p95": 58.614,
"p99": 62.326,
"max": 63.919,
"reps": 120
},
"cycle_K1_steps1_discrete": {
"p50": 34.493,
"p95": 42.078,
"p99": 43.906,
"max": 50.268,
"reps": 120
},
"cycle_K8_steps16_cert_n8": {
"p50": 132.027,
"p95": 150.463,
"p99": 160.83,
"max": 192.586,
"reps": 120
},
"cycle_K8_steps4_cert_n8": {
"p50": 64.812,
"p95": 79.192,
"p99": 89.629,
"max": 95.837,
"reps": 120
},
"cycle_K1_steps4_cert_n16": {
"p50": 61.967,
"p95": 73.219,
"p99": 79.053,
"max": 83.18,
"reps": 120
},
"f32_vs_f64": {
"points": 3,
"max_abs_err_mm": 0.0,
"sign_flips": 0
}
}
@@ -0,0 +1,143 @@
{
"device": "NVIDIA GeForce RTX 3090",
"B": 1,
"steps_used_by_eval": 1,
"note": "eval_cl \u4ece\u4e0d\u4f20 steps\uff0crollout \u9ed8\u8ba4 steps=1\uff08MeanFlow \u4e00\u6b65\u751f\u6210\uff09\uff1b\u6240\u4ee5\u771f\u5b9e\u5468\u671f\u6309 steps=1 \u6d4b",
"parts": {
"features_total": {
"p50": 12.589,
"p95": 18.334,
"p99": 20.217,
"max": 23.649,
"reps": 120
},
"features_gm_groups_only": {
"p50": 6.817,
"p95": 8.771,
"p99": 10.254,
"max": 17.169,
"reps": 120
},
"features_gaps40_all_groups": {
"p50": 7.245,
"p95": 10.501,
"p99": 13.794,
"max": 14.508,
"reps": 120
},
"features_fk_only": {
"p50": 3.708,
"p95": 5.024,
"p99": 5.991,
"max": 6.301,
"reps": 120
},
"sample_k_K8_steps1": {
"p50": 7.799,
"p95": 11.655,
"p99": 13.342,
"max": 16.212,
"reps": 120
},
"sample_k_K1_steps1": {
"p50": 7.205,
"p95": 8.935,
"p99": 9.615,
"max": 13.92,
"reps": 120
},
"sample_k_K4_steps1": {
"p50": 7.834,
"p95": 10.354,
"p99": 14.744,
"max": 15.241,
"reps": 120
},
"dls_fb_f64": {
"p50": 6.729,
"p95": 9.297,
"p99": 11.186,
"max": 12.064,
"reps": 120
},
"discrete_cert_4pts": {
"p50": 8.603,
"p95": 11.181,
"p99": 12.464,
"max": 15.799,
"reps": 120
},
"cert_uniform_n8_2roots_f64": {
"p50": 11.479,
"p95": 19.221,
"p99": 20.942,
"max": 21.183,
"reps": 120
},
"cert_uniform_n16_2roots_f64": {
"p50": 16.138,
"p95": 21.972,
"p99": 23.898,
"max": 25.793,
"reps": 120
}
},
"cycles": {
"cycle_K8_full_discrete": {
"p50": 40.676,
"p95": 51.742,
"p99": 59.782,
"max": 62.074,
"reps": 120
},
"cycle_K4_full_discrete": {
"p50": 43.997,
"p95": 56.145,
"p99": 59.905,
"max": 65.882,
"reps": 120
},
"cycle_K1_full_discrete": {
"p50": 37.87,
"p95": 45.754,
"p99": 49.394,
"max": 53.139,
"reps": 120
},
"cycle_K8_noscore_discrete": {
"p50": 37.427,
"p95": 49.602,
"p99": 60.777,
"max": 70.139,
"reps": 120
},
"cycle_K8_full_cert_n8": {
"p50": 43.984,
"p95": 65.795,
"p99": 75.53,
"max": 95.452,
"reps": 120
},
"cycle_K8_full_cert_n16": {
"p50": 51.054,
"p95": 74.862,
"p99": 95.768,
"max": 106.48,
"reps": 120
},
"cycle_K8_fused_estimate": {
"p50": 24.054,
"p95": 26.595,
"p99": 30.17,
"max": 33.284,
"reps": 120
},
"cycle_K8_fused_cert_n8": {
"p50": 34.274,
"p95": 46.329,
"p99": 72.737,
"max": 79.022,
"reps": 120
}
}
}
+70
View File
@@ -0,0 +1,70 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": "ml/runs/round33_margin_rows.json",
"out": "docs/experiment_round33_margin.json",
"trace": "ml/runs/round33_margin.pt",
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 0,
"gap_margin": 0.5,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.5625,
"collision_free_success": 0.5625,
"time_ok_1p3": 0.37890625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.448458433151245,
"time_mean_s": 2.2555553913116455,
"brakes_mean": 20.046875,
"start_fail": 0.0,
"goal_ratio_2s": 0.18239997327327728,
"err_fail_med": 225.54278564453125,
"err_success_med": 0.18544802069664001,
"dls_share": 0.62890625,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.6600004434585571,
"speed_ratio_p95": 1.660003662109375,
"speed_ratio_max": 1.660003662109375,
"over_vmax_rate": 1.0,
"accel_ratio_p95": 1.5889430046081543,
"accel_ratio_max": 1.6600035429000854
},
"simulation_wall_s": 298.24517190000915
}
@@ -0,0 +1,71 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": null,
"out": "docs/experiment_round34_cert_batch_check.json",
"trace": null,
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 0,
"gap_margin": 0.0,
"cert_batch": 1,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.57421875,
"collision_free_success": 0.57421875,
"time_ok_1p3": 0.390625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.4449527263641357,
"time_mean_s": 2.3076188564300537,
"brakes_mean": 19.25,
"start_fail": 0.0,
"goal_ratio_2s": 0.20997506380081177,
"err_fail_med": 229.68746948242188,
"err_success_med": 0.18899640440940857,
"dls_share": 0.625,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.6600004434585571,
"speed_ratio_p95": 1.6600035429000854,
"speed_ratio_max": 1.660003662109375,
"over_vmax_rate": 1.0,
"accel_ratio_p95": 1.5835107564926147,
"accel_ratio_max": 1.6600004434585571
},
"simulation_wall_s": 306.9490996999957
}
+41
View File
@@ -0,0 +1,41 @@
{
"kin_fp": "2ebcbf17",
"segments": 48,
"n_subsegments": 8,
"wide_cap_mm": 2.0,
"gap_abs_err_mm": {
"p50": 0.0,
"p99": 3.661949063912929e-05,
"p999": 7.4360332600075e-05,
"max": 0.00013247109886016517
},
"signed_err_mm": {
"min": -0.00010937313134817828,
"max": 0.00013247109886016517,
"overestimate_count": 1,
"overestimate_gt_1e-3": 0
},
"sign_flips": 0,
"accepted_f64": 24,
"accepted_f32": 24,
"unsafe_direction_f32_only": 0,
"conservative_f64_only": 0,
"per_group_pair_err_p99": [
0.0,
7.6e-05,
0.0001,
7e-05,
7.1e-05,
0.0,
0.0,
0.0,
0.000132,
6.7e-05,
5.1e-05,
0.0,
0.0,
0.0,
0.0
],
"note": "wide_cap 2.0 mm\uff1a\u8ba4\u8bc1\u5b9e\u9645\u7528 bound+2eps \u7684\u81ea\u9002\u5e94 cap\uff0c\u8fd9\u91cc\u53d6\u56fa\u5b9a 2 mm \u4ee5\u8986\u76d6\u88ab\u68c0\u9a8c\u7684\u6bb5\uff1b\u82e5 max \u8bef\u5dee\u8fdc\u5c0f\u4e8e 1e-4 \u7684 eps\uff0c\u5224\u636e\u5728 f32 \u4e0b\u4ecd\u6210\u7acb\u9700\u518d\u786e\u8ba4 f32 \u7684\u95f4\u9699\u672c\u8eab\u8bef\u5dee\u7b26\u53f7\u65b9\u5411\u3002"
}
+629
View File
@@ -0,0 +1,629 @@
{
"device": "NVIDIA GeForce RTX 3090",
"B": 1,
"K": 8,
"steps": 1,
"reps": 90,
"design": "\u540c\u4e00\u6279 4 \u4e2a\u53d8\u4f53\u9010\u8f6e\u4ea4\u66ff\uff08\u5947\u6570\u8f6e\u53cd\u5e8f\uff09\uff0c\u6d88\u9664 GPU \u65f6\u949f\u6f02\u79fb\uff1b\u539f\u59cb\u8017\u65f6\u5168\u90e8\u843d\u76d8",
"equivalence": {
"features_reuse_bitwise_equal": true,
"gm_groups_vs_full_table_max_abs_mm": 0.0,
"cert_matrix_bitwise_equal_at_same_cap": true,
"shared_xf_cert_bitwise_equal_at_cap1": true,
"max_abs_shared_diff_mm": 0.0,
"max_abs_cert_diff_mm": 0.0,
"decision_equal_by_threshold": {
"thr_0.0": true,
"thr_0.5": true,
"thr_1.0": true
},
"per_point_min_cur_mm": [
8.833421,
8.849185,
8.859405,
8.83829
],
"per_point_min_fused_mm": [
8.833421,
8.849185,
8.859405,
8.83829
]
},
"results": {
"current_discrete": {
"p50": 32.932,
"p95": 42.589,
"p99": 55.568,
"max": 55.568,
"hz_p50": 30.4,
"raw_ms": [
30.458,
30.624,
30.789,
31.17,
31.229,
31.283,
31.371,
31.394,
31.454,
31.473,
31.564,
31.58,
31.719,
31.772,
31.78,
31.785,
31.859,
31.875,
31.906,
31.973,
31.979,
32.002,
32.017,
32.063,
32.099,
32.113,
32.123,
32.127,
32.13,
32.137,
32.191,
32.286,
32.308,
32.324,
32.33,
32.388,
32.48,
32.508,
32.564,
32.63,
32.64,
32.702,
32.78,
32.822,
32.834,
32.932,
32.964,
32.975,
32.996,
33.071,
33.163,
33.191,
33.192,
33.402,
33.477,
33.509,
33.583,
33.956,
34.014,
34.04,
34.047,
34.047,
34.154,
34.302,
34.595,
34.658,
34.943,
35.296,
35.522,
35.67,
35.757,
35.828,
35.859,
36.459,
36.503,
36.562,
36.956,
37.047,
37.067,
37.142,
37.442,
38.632,
39.285,
41.269,
41.932,
42.589,
43.199,
43.329,
54.407,
55.568
]
},
"fused_discrete": {
"p50": 24.515,
"p95": 30.02,
"p99": 42.085,
"max": 42.085,
"hz_p50": 40.8,
"raw_ms": [
22.146,
22.266,
22.409,
22.653,
22.66,
22.695,
22.76,
22.827,
22.848,
23.001,
23.033,
23.087,
23.249,
23.36,
23.488,
23.573,
23.607,
23.625,
23.649,
23.651,
23.652,
23.652,
23.656,
23.676,
23.716,
23.737,
23.745,
23.795,
23.871,
23.883,
23.92,
23.922,
23.945,
23.959,
24.046,
24.13,
24.184,
24.282,
24.291,
24.313,
24.318,
24.319,
24.32,
24.34,
24.399,
24.515,
24.532,
24.541,
24.552,
24.592,
24.618,
24.731,
24.741,
24.745,
24.766,
24.807,
24.813,
24.898,
25.066,
25.088,
25.137,
25.18,
25.223,
25.376,
25.474,
25.603,
25.68,
25.744,
25.82,
25.936,
25.95,
25.953,
26.002,
26.174,
26.312,
26.88,
27.036,
27.209,
27.329,
27.424,
27.715,
28.796,
28.824,
29.427,
29.662,
30.02,
31.515,
31.839,
36.18,
42.085
]
},
"shared_xf_discrete": {
"p50": 26.864,
"p95": 34.544,
"p99": 67.243,
"max": 67.243,
"hz_p50": 37.2,
"raw_ms": [
24.433,
24.441,
24.585,
24.777,
24.862,
24.935,
25.054,
25.08,
25.105,
25.193,
25.313,
25.44,
25.466,
25.483,
25.516,
25.554,
25.577,
25.689,
25.702,
25.708,
25.854,
25.896,
25.898,
26.011,
26.097,
26.175,
26.255,
26.289,
26.297,
26.306,
26.328,
26.336,
26.346,
26.375,
26.397,
26.456,
26.502,
26.523,
26.563,
26.613,
26.634,
26.658,
26.708,
26.752,
26.798,
26.864,
26.869,
26.874,
27.315,
27.341,
27.668,
27.67,
27.732,
27.774,
27.812,
27.84,
27.882,
27.921,
28.24,
28.302,
28.421,
28.694,
28.722,
28.739,
28.817,
28.914,
28.944,
28.998,
29.12,
29.122,
29.259,
29.292,
29.334,
29.658,
29.847,
30.181,
30.206,
30.292,
30.492,
30.782,
31.168,
31.222,
31.607,
31.614,
32.187,
34.544,
35.274,
35.565,
35.915,
67.243
]
},
"current_cert_n8": {
"p50": 42.624,
"p95": 49.434,
"p99": 57.771,
"max": 57.771,
"hz_p50": 23.5,
"raw_ms": [
38.546,
38.666,
39.152,
39.265,
39.344,
39.441,
39.584,
39.626,
39.738,
39.841,
40.228,
40.306,
40.514,
40.805,
40.838,
40.875,
40.884,
40.907,
40.952,
40.971,
41.185,
41.193,
41.193,
41.277,
41.279,
41.422,
41.448,
41.463,
41.465,
41.473,
41.504,
41.551,
41.684,
41.733,
41.841,
41.947,
41.967,
42.011,
42.077,
42.241,
42.249,
42.269,
42.301,
42.314,
42.388,
42.624,
42.661,
42.767,
42.773,
42.837,
42.865,
42.87,
42.913,
43.343,
43.35,
43.356,
43.376,
43.501,
43.695,
43.708,
43.842,
43.923,
43.972,
44.243,
44.476,
44.536,
44.648,
44.745,
44.894,
45.034,
45.144,
45.236,
45.697,
45.859,
45.944,
46.039,
46.126,
46.467,
46.575,
46.887,
46.983,
47.234,
47.621,
47.685,
48.547,
49.434,
49.683,
50.646,
54.754,
57.771
]
},
"fused_cert_n8": {
"p50": 24.826,
"p95": 31.721,
"p99": 49.398,
"max": 49.398,
"hz_p50": 40.3,
"raw_ms": [
22.127,
22.553,
22.598,
22.624,
22.789,
22.798,
22.878,
22.983,
23.002,
23.134,
23.192,
23.273,
23.323,
23.345,
23.377,
23.445,
23.466,
23.549,
23.611,
23.613,
23.861,
23.868,
23.885,
23.885,
23.906,
23.918,
23.919,
23.933,
23.947,
24.052,
24.064,
24.067,
24.08,
24.103,
24.13,
24.164,
24.249,
24.301,
24.429,
24.515,
24.603,
24.609,
24.7,
24.748,
24.812,
24.826,
24.855,
24.858,
24.954,
24.99,
25.052,
25.097,
25.195,
25.209,
25.241,
25.252,
25.264,
25.298,
25.63,
25.659,
25.767,
25.804,
25.928,
25.976,
26.494,
26.871,
26.886,
26.988,
26.995,
27.004,
27.307,
27.322,
27.352,
27.937,
28.144,
28.165,
28.216,
28.288,
28.331,
29.37,
29.611,
30.6,
30.704,
31.026,
31.552,
31.721,
32.875,
34.001,
40.223,
49.398
]
},
"shared_xf_cert_n8": {
"p50": 36.185,
"p95": 41.136,
"p99": 64.287,
"max": 64.287,
"hz_p50": 27.6,
"raw_ms": [
33.051,
33.153,
33.201,
33.406,
33.819,
33.845,
33.94,
34.083,
34.145,
34.235,
34.3,
34.372,
34.605,
34.697,
34.714,
34.755,
34.817,
34.855,
34.925,
34.957,
34.963,
34.967,
34.986,
35.167,
35.239,
35.314,
35.325,
35.402,
35.444,
35.518,
35.544,
35.582,
35.594,
35.662,
35.675,
35.724,
35.775,
35.874,
35.908,
35.929,
35.965,
36.019,
36.04,
36.126,
36.164,
36.185,
36.2,
36.259,
36.263,
36.266,
36.301,
36.324,
36.383,
36.393,
36.609,
36.667,
36.696,
36.779,
36.799,
36.836,
36.881,
36.978,
37.001,
37.033,
37.043,
37.078,
37.183,
37.206,
37.278,
37.3,
37.411,
37.485,
37.722,
37.923,
37.965,
38.197,
38.588,
38.871,
39.082,
39.099,
39.135,
39.351,
39.505,
40.204,
40.892,
41.136,
41.449,
42.611,
44.687,
64.287
]
}
}
}
+630
View File
@@ -0,0 +1,630 @@
{
"device": "NVIDIA GeForce RTX 3090",
"B": 1,
"K": 8,
"commit_n": 2,
"reps": 80,
"design": "5 \u4e2a\u53d8\u4f53\u9010\u8f6e\u4ea4\u66ff\uff08\u5947\u6570\u8f6e\u53cd\u5e8f\uff09\uff1b\u53ea\u6d4b\u53ef\u5b9e\u73b0\u7684\u53e3\u5f84\uff1a\u7279\u5f81/\u63a8\u7406/\u8ba4\u8bc1\u6309\u771f\u5b9e\u987a\u5e8f",
"equivalence": {
"batch_cert_matrix_equal_to_loop": true,
"features_xf_reuse_bitwise_equal": true
},
"results": {
"A_today": {
"p50": 33.494,
"p95": 46.143,
"p99": 64.693,
"max": 64.693,
"raw_ms": [
28.984,
29.251,
29.361,
29.549,
29.899,
29.944,
30.181,
30.605,
30.624,
30.744,
30.815,
30.838,
30.862,
30.9,
31.024,
31.092,
31.115,
31.262,
31.433,
31.463,
31.52,
31.572,
31.642,
31.741,
31.871,
31.902,
31.924,
31.933,
31.976,
32.001,
32.03,
32.06,
32.285,
32.384,
32.624,
32.825,
32.919,
33.063,
33.168,
33.197,
33.494,
33.589,
33.624,
33.632,
33.666,
33.708,
33.727,
33.735,
33.754,
33.773,
33.869,
33.907,
34.116,
34.219,
34.314,
34.337,
34.897,
34.957,
35.436,
35.469,
35.999,
36.106,
36.523,
36.559,
36.846,
36.866,
36.922,
37.354,
37.375,
37.581,
37.933,
38.026,
40.541,
40.882,
44.161,
44.233,
46.143,
54.047,
58.36,
64.693
]
},
"B_xf_reuse": {
"p50": 30.122,
"p95": 42.349,
"p99": 66.368,
"max": 66.368,
"raw_ms": [
26.698,
26.958,
26.984,
27.007,
27.018,
27.158,
27.308,
27.369,
27.987,
28.066,
28.219,
28.288,
28.362,
28.517,
28.537,
28.539,
28.593,
28.611,
28.638,
28.735,
28.828,
28.871,
28.922,
28.945,
28.978,
29.088,
29.202,
29.226,
29.226,
29.31,
29.497,
29.506,
29.531,
29.549,
29.649,
29.875,
29.978,
29.992,
30.025,
30.102,
30.122,
30.243,
30.245,
30.263,
30.407,
30.578,
30.665,
30.717,
30.863,
30.884,
30.889,
30.95,
31.023,
31.314,
31.525,
31.537,
31.543,
31.549,
31.62,
32.011,
32.098,
32.168,
32.188,
32.22,
32.578,
32.818,
32.851,
32.929,
32.991,
33.76,
33.805,
33.831,
33.94,
34.124,
34.891,
39.001,
42.349,
43.636,
62.045,
66.368
]
},
"C_batch_cert": {
"p50": 38.048,
"p95": 50.171,
"p99": 59.86,
"max": 59.86,
"raw_ms": [
33.625,
33.736,
33.848,
34.235,
34.49,
34.917,
34.918,
35.108,
35.407,
35.919,
36.023,
36.035,
36.087,
36.263,
36.318,
36.531,
36.627,
36.638,
36.722,
36.778,
36.791,
36.801,
36.816,
36.859,
36.97,
36.998,
37.029,
37.055,
37.057,
37.093,
37.208,
37.318,
37.408,
37.438,
37.463,
37.501,
37.503,
37.605,
37.63,
37.984,
38.048,
38.283,
38.347,
38.524,
38.547,
38.667,
38.816,
38.995,
39.028,
39.34,
39.617,
39.679,
39.845,
39.852,
39.871,
39.894,
39.923,
40.195,
40.514,
41.421,
41.684,
41.804,
41.879,
41.933,
42.011,
42.127,
42.483,
42.551,
42.656,
43.038,
43.248,
43.269,
45.68,
46.514,
46.755,
48.909,
50.171,
50.906,
55.33,
59.86
]
},
"D_both": {
"p50": 34.87,
"p95": 39.811,
"p99": 48.258,
"max": 48.258,
"raw_ms": [
30.044,
30.934,
31.005,
31.007,
31.064,
31.492,
31.675,
31.758,
32.114,
32.153,
32.338,
32.374,
32.442,
32.494,
32.681,
32.882,
32.942,
32.991,
33.167,
33.18,
33.302,
33.305,
33.342,
33.449,
33.58,
33.694,
33.739,
33.784,
33.832,
33.835,
33.866,
33.961,
34.037,
34.135,
34.315,
34.395,
34.465,
34.556,
34.656,
34.819,
34.87,
34.98,
35.047,
35.176,
35.19,
35.37,
35.461,
35.528,
35.557,
35.59,
35.643,
35.716,
35.742,
35.804,
35.991,
36.018,
36.063,
36.105,
36.293,
36.312,
36.326,
36.537,
36.541,
36.602,
36.717,
36.934,
36.945,
37.088,
37.141,
37.46,
37.561,
37.608,
37.765,
37.904,
37.974,
39.253,
39.811,
40.076,
46.504,
48.258
]
},
"E_both_cont_n8": {
"p50": 43.725,
"p95": 51.084,
"p99": 65.681,
"max": 65.681,
"raw_ms": [
37.733,
39.087,
39.154,
39.238,
39.36,
39.454,
39.514,
40.167,
40.44,
40.837,
40.86,
40.868,
41.237,
41.276,
41.346,
41.408,
41.551,
41.815,
41.824,
41.912,
41.932,
41.973,
42.074,
42.296,
42.341,
42.365,
42.435,
42.505,
42.592,
42.598,
42.637,
42.85,
43.045,
43.048,
43.18,
43.209,
43.359,
43.361,
43.373,
43.411,
43.725,
43.728,
43.787,
43.833,
43.985,
44.133,
44.185,
44.401,
44.458,
44.585,
44.689,
44.819,
44.841,
44.86,
45.081,
45.087,
45.55,
45.701,
45.939,
46.06,
46.128,
46.264,
46.368,
46.43,
46.957,
47.145,
47.231,
47.256,
47.463,
47.637,
47.819,
47.863,
48.382,
48.87,
49.676,
50.884,
51.084,
51.17,
53.859,
65.681
]
},
"F_worst_loop8": {
"p50": 80.224,
"p95": 88.436,
"p99": 89.553,
"max": 89.553,
"raw_ms": [
67.204,
70.008,
70.436,
71.432,
72.454,
72.503,
73.006,
73.12,
73.492,
73.971,
74.203,
74.358,
74.756,
74.816,
74.893,
75.536,
75.98,
76.049,
76.164,
76.25,
76.547,
76.669,
76.843,
77.268,
77.534,
77.658,
77.872,
78.149,
78.343,
78.377,
78.445,
78.483,
78.86,
78.863,
78.905,
79.048,
79.315,
79.857,
80.035,
80.041,
80.224,
80.602,
80.644,
80.677,
80.848,
81.061,
81.094,
81.155,
81.243,
81.384,
81.628,
81.679,
81.75,
82.117,
82.148,
82.363,
82.444,
82.511,
82.651,
82.828,
82.921,
83.371,
83.592,
83.894,
83.926,
84.386,
84.519,
84.608,
85.06,
85.078,
85.094,
85.26,
87.212,
87.744,
87.847,
88.098,
88.436,
88.461,
88.919,
89.553
]
},
"G_worst_batch": {
"p50": 37.139,
"p95": 46.06,
"p99": 52.215,
"max": 52.215,
"raw_ms": [
32.718,
33.447,
33.569,
33.692,
33.81,
34.236,
34.28,
34.475,
34.496,
34.746,
35.19,
35.392,
35.435,
35.564,
35.569,
35.669,
35.693,
35.734,
35.741,
35.812,
35.828,
35.934,
36.015,
36.036,
36.097,
36.144,
36.197,
36.205,
36.487,
36.637,
36.71,
36.775,
36.79,
36.83,
36.831,
36.885,
36.894,
36.903,
36.947,
37.122,
37.139,
37.154,
37.198,
37.225,
37.249,
37.478,
37.615,
37.617,
38.114,
38.164,
38.288,
38.637,
38.651,
38.724,
38.813,
39.108,
39.15,
39.848,
39.866,
39.946,
39.969,
39.992,
40.04,
40.28,
40.35,
40.697,
40.727,
40.893,
40.972,
41.029,
41.138,
41.479,
41.51,
41.711,
45.036,
45.784,
46.06,
48.597,
50.186,
52.215
]
}
}
}
+58
View File
@@ -0,0 +1,58 @@
{
"device": "NVIDIA GeForce RTX 3090",
"torch": "2.10.0+cu130",
"K": 8,
"steps": 1,
"B": 1,
"act_scale": 1.0,
"inference": {
"fp32": {
"p50": 7.681,
"p95": 9.426,
"p99": 10.631,
"max": 15.837
},
"fp16": {
"p50": 8.309,
"p95": 10.791,
"p99": 12.031,
"max": 12.322
},
"bf16": {
"p50": 8.577,
"p95": 11.057,
"p99": 14.174,
"max": 17.1
}
},
"cycle": {
"fp32": {
"p50": 36.507,
"p95": 43.688,
"p99": 46.568,
"max": 50.598
},
"fp16": {
"p50": 36.162,
"p95": 50.43,
"p99": 55.598,
"max": 57.043
}
},
"output_deviation": {
"fp16": {
"act_max_abs": 0.002096,
"act_p99_abs": 0.001059,
"deg_max_abs": 0.005,
"deg_p99_abs": 0.00219,
"would_change_execution_order": false
},
"bf16": {
"act_max_abs": 0.017171,
"act_p99_abs": 0.012661,
"deg_max_abs": 0.03766,
"deg_p99_abs": 0.03008,
"would_change_execution_order": true
}
}
}
@@ -0,0 +1,70 @@
{
"config": {
"ckpt": "ml/runs/base_dag2_dag1/best.pt",
"samples": "ml/data/samples",
"tasks": null,
"split": "val",
"n": 256,
"K": 8,
"commit": 2,
"fb": 2.2,
"ki": 0.0,
"fb_scale": 0.0,
"fb_near": 0.2,
"fb_fallback": 0.0,
"kdecay": 0.9,
"kimax": 0.5,
"fb_every": 1,
"safety_try": 0,
"sel_w": 0.1,
"dls_safety": 1,
"fb_clear": 0.0,
"fb_cmin": 0.3,
"sub_refresh": 0,
"fb_block_gain": 0.5,
"dump": null,
"out": "docs/experiment_round34_xf_reuse_check.json",
"trace": null,
"select": "endpoint",
"replan": "empty",
"safety": 1,
"partial": 1,
"inc_cap": 0,
"gap_margin": 0.0,
"max_sec": null,
"seed": 0,
"dls": "analytic",
"device": "cuda"
},
"batch_size": 256,
"batch_seed_rule": "same seed per batch",
"checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1",
"tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772",
"kin_fp": "2ebcbf17",
"act_scale": 1.0,
"metrics": {
"n": 256,
"n_ref": 256,
"success": 0.57421875,
"collision_free_success": 0.57421875,
"time_ok_1p3": 0.390625,
"collision_rate": 0.0,
"collision_steps_mean": 0.0,
"travel_ratio_median": 2.4449527263641357,
"time_mean_s": 2.3076188564300537,
"brakes_mean": 19.25,
"start_fail": 0.0,
"goal_ratio_2s": 0.20997506380081177,
"err_fail_med": 229.68746948242188,
"err_success_med": 0.18899640440940857,
"dls_share": 0.625,
"time_budget": "teacher×2(≥3 s)",
"speed_ratio_p50": 1.6600004434585571,
"speed_ratio_p95": 1.6600035429000854,
"speed_ratio_max": 1.660003662109375,
"over_vmax_rate": 1.0,
"accel_ratio_p95": 1.5835107564926147,
"accel_ratio_max": 1.6600004434585571
},
"simulation_wall_s": 317.96790459999465
}
@@ -0,0 +1,23 @@
{
"device": "NVIDIA GeForce RTX 3090",
"K": 8,
"commit_n": 2,
"design": "\u4e24\u6b21\u89c4\u5212\u4e3a\u4e00\u4e2a\u5355\u5143\uff0c\u5947\u6570\u8f6e\u53cd\u5e8f\uff1b\u6d41\u6c34\u7ebf\u90a3\u6b21\u7684\u8ba4\u8bc1\u8c03\u7528\u88ab\u8feb\u7528 cap=GAP_CAP(\u7279\u5f81\u9700\u8981)\uff0c\u8fd9\u662f\u590d\u7528\u7684\u771f\u5b9e\u4ee3\u4ef7\uff0c\u4e0d\u662f\u514d\u8d39",
"equivalence": {
"cached_gap_row_bitwise_equal": false,
"features_bitwise_equal": false,
"max_row_diff_mm": 6.29425048828125e-05
},
"fresh": {
"p50_per_plan": 34.779,
"p95": 39.271,
"p99": 40.98,
"reps": 60
},
"pipelined": {
"p50_per_plan": 31.655,
"p95": 34.483,
"p99": 38.208,
"reps": 60
}
}
+746
View File
@@ -0,0 +1,746 @@
# 探索日志:Transformer 机械臂闭环控制
长期锚点:`docs/Transformer轨迹拟合训练方案.md` §9.2(部署形态 = 策略 + 安全层)。
验收口径(§9.2):实际执行碰撞 = 0;held-out 可达任务到达率 ≥ 98%;到达用时 ≤ 老师 ×1.3;关节行程 ≤ 现有规划器 ×1.10;冷启动 < 10 ms;每次推理 + 安全认证 P99 < 20 ms;刹停发生率 < 2% 的推理周期;策略单独(关安全层)碰撞率 < 3%。
评测约定:val 任务、部署节奏(r = 3 重规划、1 段采样、K=8);「纯策略」= 关安全层;「部署」= 开安全层。时延:桌面 RTX 3090 上 `sample_k(B=64, K=8)` 墙钟均值(均值,非 P99);浏览器 ONNX P99 以 §9.2 R4 的独立测量为准。
**口径更正(2026-10-20,轮次 29 复核;下列历史原始数值保留):**
- 轮次 24–28 的“50 Hz 可行/连续无饥饿”结论撤回。同步 `await plan(); tick()` 不是实际 20 ms 调度;`starveTicks` 只是耗时除以 20 ms 的估算。组件分位数相加不等于整次规划 P99,实测 62–68 ms 也超过 commit=3 的 60 ms。仍须严格认证下的真查看器 R9 测量,且不放松 §9.2 的 P99 < 20 ms。
- Python 端点/中点检查不等于连续认证;JS `--seg 0` 是放松安全的消融,不是部署验收。开安全层碰撞必须为 0,不能套用“纯策略 <3%”判据。既有到达率允许途中碰撞,新指标 `collision_free_success` 才是时限内无碰撞到达率。
- `replan=empty` 是同步仿真的零前缀配方,不是已验证的异步执行配置。所有任务(含 D)继续保留在历史对照分母内;不能与 held-out 可达任务 ≥98% 直接比较。
- 轮次 26 的几何角速度雅可比不等于远离目标时 skew 姿态残差的精确导数,因而既改变控制律又消除差分误差;不能把全部收益归因于去噪。3.58% 是单个位置雅可比样本对拍,不是普遍噪声界。`check_dls.py` 中标 B=256 的测量实际仅有 64 行,不能引用为 B=256 性能。
- `err_fail_med` 是批次结束时未按时到达任务的末端误差,不是每题截止时误差;仿真墙钟/题也不是单次规划延迟。查看器默认选择器仍为 consist,endpoint 仅是可选实验。
## 轮次 0:基线(2026-10-18,main)
- **假设**:模型开环已拟合老师(末端 ~6 mm),闭环失败另有原因。
- **判据**:reach = 0 且碰撞 ≈ 100% 即失败;需同时报告碰撞 / 到达 / 精度 / 时延。
- **配方**:`ml/runs/base/last.pt`(step 14000,旧配方、act_scale=1.0),n=256。
- **对照**:老师回放 reach 1.0 / 碰撞 0;H1_oracle(老师提线走 rollout)reach 1.0、碰撞 0、轨迹偏差 p99 4.6e-4°。
- **指标**:到达 **0%**;碰撞 **99.6%**;精度 ol_fk 5.98 mm / ol_best 2.92 mm / ol_div 0.655,2 s 误差比 0.755;时延未测。
- **结论**:仿真/滚动无误,失败在策略输出。闭环对每步残差极敏感(老师动作加 σ=0.05×STEP 即 reach 1.0→0.218)。
## 轮次 1:数据规模能否救闭环(main,负结论)
- **假设**:训练数据不足导致分布外状态无覆盖。
- **判据**:若闭环状态到训练集最近邻距离大,且密度缩放无法在误差尺度内缩小差距,则加大同类数据无效。
- **配方**:191 万老师状态最近邻分析;6 维密度缩放 d∝N^(−1/6)。
- **对照**:起点(老师分布内)最近邻。
- **指标**:闭环状态最近邻 p50 **21.5°**(起点 13.2°);数据翻 4 倍仅降 ~21%,与 0.02° 误差尺度差三个量级。
- **结论**:加大同类老师数据无效;需要「不同的数据」(闭环状态标注)。
- **下一步**:DAgger 或状态扰动数据。
## 轮次 2:合成扰动状态微调(main,负结论)
- **假设**:偏移 1.5° 状态的老师标注可补足闭环状态覆盖。
- **判据**:若 success 仍 0 或开环精度退化,则配方不成立。
- **配方**:2000 个扰动起点(σ 混合 0.5/1/2/4°)→ 老师重规划(2000/2000 到达)→ 36k 样本 → 从 base/last.pt 微调 3000 步(lr 3e-4 / 1e-4 两轮)。
- **对照**:base/last.pt。
- **指标**:碰撞 99.6%→**48.4% / 60.4%**;到达仍 **0%**;开环 6→**15.3 / 16.4 mm**(退化 2.5×);ratio2s 0.927/0.893。
- **结论**:只学会了「保守避撞」,开环精度被带偏,success 仍 0;此配方不成立。
- **下一步**:改训练标签(绝对目标)或真实 DAgger。
## 轮次 3:绝对目标参数化(exp: `feat/abs-target`,负结论)
- **假设**:模型从当前状态外推增量(判别 ρ=1.54),改为绝对目标标签可让输出指向参考位置。
- **判据**:一步采样开环误差需回到 <10 mm,闭环 success > 0;否则参数化不可行。
- **配方**:`ABS_TARGET=1` 重打主数据(201 万样本,Y=16 步绝对目标/180),train/rollout/ckpt 适配;从零训练 9950 步(p_neq 预热 5000)。过程中修复前缀噪声增广把 eps 直接加到绝对目标(≈18° 污染)的 bug。
- **对照**:base/last.pt(ol_fk 5.98 mm、reach 0%)。
- **指标**:一步采样动作误差 **31°**(4/8 段 2.3°/2.8°);ol_fk 4000/6000/8000 步 = 74.6/140.4/139.0 mm;闭环 **0% / 100%**(1 段与 4 段皆然);时延未测。
- **结论**:绝对目标的一步流匹配在 MeanFlow 头上学不动(过预热后 5000 步无下降趋势);此配方不成立。分支已推送远端 `217a70b`(本地分支可删,远端保留可复现)。
- **下一步**:回归增量口径,上真实 DAgger。
## 轮次 4:真实 DAgger 1 轮(main,部分有效)
- **假设**:策略自己走到的状态(离训练集 21.5°)+ 老师重标注,可让模型学会在偏移状态上行动。
- **判据**:reach 是否从 0 抬头;同时看碰撞、开环精度、时延。
- **配方**:`ACT_SCALE=1.0 DAG_ROUNDS=1 DAG_N=2000 bash ml/dagger/run.sh base ml/runs/base/last.pt`(采集 2000 题 → 老师重规划 → 打包 → 微调 15000 步,lr 3e-4;主数据 + DAgger 混合)。
- **对照**:base/last.pt。
- **指标**:碰撞 99.6%→**40.2%**(8000 步 36.9%,10000 步 58.8%);到达 **0%**,仅微调 12000 步时出现 **0.4%(2/512)**;开环退化 6→**15.7 mm**;ratio2s 0.87–0.92;时延未测。
- **结论**:DAgger 数据确实教会避撞,但微调把主分布带偏(开环退化),到达仍近 0;单独 DAgger 不是终点。
- **下一步**:在 DAgger 模型上加目标反馈(模型前馈 + DLS 反馈)。
## 轮次 5:模型前馈 + DLS 目标反馈(exp: `feat/fb-mix`,历史最佳)
- **假设**:模型擅长开环轨迹/避撞,但不指向目标;把 DLS 朝目标的反馈叠加到候选动作上,可把「走偏」拉回目标。
- **判据**:部署口径(+安全层)reach 显著 > 0 且碰撞 < 3%(§9.2 策略单独碰撞线)、刹停可控;否则不成立。
- **配方**:`closed_loop.rollout(fb_gain=...)`:`inc += fb_gain · DLS(qe→T,R, n=1)`(加在所有候选上,安全层评估带反馈的候选)。模型 `ml/runs/base_dag1/best.pt`(DAgger 轮 4 的 best,12000 步)。n=128,val。
- **对照**:base/last.pt + 同反馈;DAgger 模型 + 无反馈。
- **指标(n=128)**:
| 配置 | 到达 | 碰撞 | 精度 ratio2s | 刹停/题 | 时延 |
|---|---|---|---|---|---|
| DAgger 模型, gain 0 | 0% | 44.5% | 0.873 | 0 | — |
| DAgger 模型, gain 1.0, 关安全层 | 29.7% | 73.4% | 0.370 | 0 | — |
| **DAgger 模型, gain 1.0, 开安全层** | **20.3%** | **2.3%** | **0.480** | **21.9** | **12.58 ms/次**(B=64,K=8,桌面 3090) |
| base 模型, gain 1.0, 关安全层 | 0% | 94.5% | 0.554 | 0 | — |
(另测 n=32 复核:gain 1.0 + safety → reach 25.0%、碰撞 6.2%、ratio2s 0.588。)
- **结论**:**反馈混合是首个把到达率从 0 拉到 20%+ 的机理**;安全层把碰撞从 73% 压到 2.3%(部署口径),代价是刹停 21.9 次/题(远超 §9.2 的 <2%/周期)。精度 ratio2s 0.48(2 s 时误差缩一半),时延 12.58 ms/次接近 §9.2 的 20 ms(推理+认证)门槛。
- **与历史最佳/基线对比**:reach 0→20.3%、碰撞 99.6→2.3%、ratio2s 0.755→0.480;开环精度仍退化(DAgger 微调导致 15.7 mm vs 基线 5.98 mm)。
- **漂移检查**:main = 70654e4(无新提交);工作区仅用户个人 html 未跟踪;数据口径 act_scale=1.0 与 ckpt 一致;`feat/abs-target` 已推送远端;本次实验在独立分支 `feat/fb-mix`。
- **下一步**:扫更大 gain(1.2–2.0)与增益衰减;目标是把刹停次数降下来、到达率再提升,并测 ONNX/浏览器时延。
## 轮次 6:反馈增益扩展扫描(exp: `feat/fb-mix`,小幅进展)
- **假设**:更大增益能把到达率继续推高。
- **判据**:reach 显著高于 20.3%(>25%)且碰撞 ≤3%、刹停不失控。
- **配方**:`rollout(fb_gain∈{1.2,1.5,2.0}, safety=True)`,模型同轮次 5,n=128。
- **对照**:轮次 5(gain 1.0:reach 20.3% / 碰撞 2.3% / ratio2s 0.480 / 刹停 21.9)。
- **指标**:
| gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|
| 1.2 | **23.4%** | 1.6% | **0.454** | 31.4 | 973 ms |
| 1.5 | 21.9% | 2.3% | 0.498 | 43.0 | 1151 ms |
| 2.0 | 23.4% | 0.8% | 0.530 | 57.6 | 1100 ms |
- **结论**:到达率封顶在 ~23%,增益再大只增刹停(31→58 次/题)。当前瓶颈从「到不了目标」变成「安全层频繁刹停 + 近目标过冲」。
- **下一步**:距离衰减增益(远离目标全增益、贴近目标减弱)+ 刹停兜底;目标 reach > 30% 且刹停显著下降。
## 轮次 7:距离衰减增益 + 刹停兜底(exp: `feat/fb-mix`,负结论)
- **假设**:贴近目标减弱反馈可减少过冲/刹停;安全层全拒时用「半速朝目标一步」(单点安全认证)可减少原地停滞。
- **判据**:reach > 23.4%(轮次 6)且碰撞 ≤3%、刹停下降。
- **配方**:`fb_scale=100`(>100 mm 全增益,近目标最低 0.2 倍);`fb_fallback=0.5`(全拒时半速 DLS 一步,认证 `qe+fb1` 单点无碰)。n=128,safety=True。
- **对照**:轮次 6(gain 1.2、无衰减、无兜底:23.4% / 1.6% / 0.454 / 31.4 / 973 ms)。
- **指标**:
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|
| decay + fallback (gain 1.2) | 7.0% | 57.8% | 0.506 | 45.1 | 1166 ms |
| decay only (gain 1.2) | 9.4% | 1.6% | 0.454 | 29.6 | 1106 ms |
| no-decay + fallback (gain 1.2) | 21.1% | 57.8% | 0.506 | 46.8 | 1063 ms |
| gain 1.0 + decay + fallback | 4.7% | 55.5% | 0.515 | 37.2 | 939 ms |
- **结论**:两个改动都失败。① 衰减使接近目标时的收敛变慢,到达从 23.4% 掉到 9.4%;② 兜底绕过安全层的多候选选择,碰撞从 1.6% 拉到 **57.8%**(仅单点认证、且执行 3 步同向动作,危险),必须清除。保留参数但默认关闭(`fb_scale=0, fb_fallback=0`),历史最佳配置(轮次 6)可复现。
- **过程 bug(自造)**:衰减代码曾用变量名 `g` 覆盖 rollout 的随机数生成器,导致 `randn` 报错;改名 `gdec` 后正常——记录以免误判为模型问题。
- **下一步**:DAgger 第 2 轮——采集时加入 fb_gain + safety,让训练分布 = 部署分布(1 轮,15000 步微调,约 1 小时)。按目标要求先向用户确认。
## 轮次 8:部署分布 DAgger 第 2 轮(exp: `exp/dagger-deploy`,边际改善)
- **假设**:第 1 轮 DAgger 用纯策略轨迹采集,与部署分布(反馈 + 安全层)不一致;用部署分布采集可让模型学到刹停/反馈下的正确动作。
- **判据**:部署口径 reach > 23.4%(轮次 6)或刹停显著下降且到达不降。
- **配方**:`ml.dagger.collect --fb_gain 1.2 --safety 1`(2000 题 → 637 起点,部署分布)→ 老师重规划 → 打包 → 从 `base_dag1/best.pt` 微调 15000 步(配置 base_dag2,lr 3e-4)。
- **对照**:base_dag1/best.pt(轮次 6 历史最佳);纯策略 eval 见训练日志。
- **指标(部署口径,n=128,safety=True)**:
| 模型 | gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| dag2 best | 1.0 | 21.9% | 1.6% | 0.485 | **18.9** | **905 ms** |
| dag2 best | 1.2 | 22.7% | 1.6% | 0.491 | 25.9 | 922 ms |
| dag1 best | 1.0 | 20.3% | 2.3% | 0.480 | 21.9 | 944 ms |
| dag1 best(轮次 6) | 1.2 | **23.4%** | 1.6% | **0.454** | 31.4 | 953 ms |
- **结论**:部署分布 DAgger 把刹停降 18–40%、时延降 ~31 ms,但到达率未突破 23% 平台(-0.7 pp),属边际权衡改善;历史最佳到达仍是 dag1+gain1.2(23.4%)。
- **下一步**:试 replan=empty(r=0 规划,让反馈逐步生效)与安全层候选 K=16(减少全拒刹停)。
## 轮次 9:replan=empty(队列空时规划、r=0)/ K=16 安全候选(exp: `exp/dagger-deploy`,新历史最佳)
- **假设**:replan='empty'(每次队列空时规划、无前缀 r=0)比默认 r=3 更适合反馈混合。K=16 给安全层更多候选、减少全拒。
(更正:replan='empty' 仍是每 3 步规划一次——队列空才规划,commit 3 步;收益来自 r=0 无前缀,不是规划频率。)
- **判据**:部署口径 reach > 23.4% 且碰撞 ≤1.6%、刹停/时延不恶化。
- **配方**:dag2 best;`replan='empty'`(每次 r=0 规划);`K=16`。n=128,safety=True。
- **对照**:轮次 8(dag2+gain1.2:22.7% / 1.6% / 0.491 / 25.9 / 922 ms)。
- **指标**:
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|
| **gain1.2 + replan=empty** | **26.6%** | **0.8%** | **0.453** | **20.6** | **845 ms** |
| gain1.2 + K=16 | 21.1% | 1.6% | 0.451 | 19.4 | 1629 ms |
| gain1.0 + K=16 | 21.9% | 4.7% | 0.506 | 14.6 | 1588 ms |
- **结论**:**replan=empty(r=0)全面改善**(到达 +3.9 pp、碰撞 1.6→0.8%、刹停 25.9→20.6、时延 922→845 ms),成为**新历史最佳**。K=16 无收益且时延翻倍(1629 ms),淘汰。
- **下一步**:replan=empty 下重扫增益(0.8/1.0/1.5/2.0),找新最优。
## 轮次 10:replan=empty(r=0)下的增益重扫(exp: `exp/dagger-deploy`,新历史最佳)
- **假设**:r=0 规划下,最优增益会上移。
- **判据**:reach > 26.6%(轮次 9)且碰撞 ≤1.6%;同时比较刹停/时延。
- **配方**:dag2 best,`replan='empty'`,gain ∈ {0.8, 1.0, 1.2, 1.5, 2.0},safety=True,n=128。
- **对照**:轮次 9(gain 1.2:26.6% / 0.8% / 0.453 / 20.6 / 845 ms)。
- **指标**:
| gain | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|
| 0.8 | 21.9% | 2.3% | 0.505 | 11.3 | 801 ms |
| 1.0 | 22.7% | **0.0%** | 0.489 | 15.2 | 810 ms |
| 1.2 | 26.6% | 0.8% | 0.453 | 20.6 | 845 ms |
| **1.5** | **28.9%** | 0.8% | **0.411** | 28.5 | 844 ms |
| 2.0 | 28.9% | 0.8% | 0.414 | 44.9 | 914 ms |
- **结论**:到达在 gain 1.5 封顶(28.9%),2.0 无收益且刹停翻倍(44.9)。**按到达优先/碰撞 ≤3%:历史最佳 = gain 1.5 + replan=empty(28.9% / 0.8% / 0.411 / 28.5 / 844 ms)**;按碰撞优先:gain 1.0(22.7% / 0.0% / 0.489 / 15.2 / 810 ms)。n=128 的差异约 3–4 题,需 n=256 复核。
- **下一步**:n=256 复核 gain 1.0 / 1.2 / 1.5;若平台稳定,再决定是否 DAgger 第 3 轮(采集用新部署配置)。
## 轮次 11:n=256 复核(发现前 128 题偏难)
- **假设**:轮次 10 的 n=128 差异含样本偏差,需扩大样本复核。
- **判据**:reach 排名与轮次 10 一致、碰撞 ≤3%;若差异大则以 n=256 为准。
- **配方**:dag2 best,`replan='empty'`,safety=True,gain ∈ {1.0, 1.2, 1.5},n=256。
- **对照**:轮次 10(n=128:22.7/26.6/28.9%)。
- **指标**:
| gain | n=128 到达 | **n=256 到达** | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| 1.0 | 22.7% | **31.6%** | 0.8% | 0.428 | 15.0 | 748 ms |
| 1.2 | 26.6% | **34.8%** | 1.2% | 0.392 | 18.6 | 752 ms |
| 1.5 | 28.9% | **36.7%** | 1.6% | 0.323 | 27.4 | 760 ms |
- **结论**:前 128 题显著偏难;n=256 上 **gain 1.5 + replan=empty = 到达 36.7% / 碰撞 1.6% / ratio2s 0.323 / 刹停 27.4 / 760 ms**(新历史最佳)。gain 1.0 更保守(31.6% / 碰撞 0.8% / 刹停 15.0)。排名与 n=128 一致(1.5 > 1.2 > 1.0)。
- **下一步**:n=512 复核 gain 1.2/1.5;随后评估 DAgger 第 3 轮(部署分布采集)与监控页回写。
## 轮次 12:n=512 复核(稳定结论)
- **假设**:n=256 的 34.8–36.7% 需在更大样本上确认。
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
- **配方**:dag2 best,`replan='empty'`,safety=True,gain ∈ {1.2, 1.5},n=512。
- **对照**:轮次 11(n=256:34.8 / 36.7%)。
- **指标**:
| gain | n=256 到达 | **n=512 到达** | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| 1.2 | 34.8% | **32.6%** | 1.0% | 0.435 | 18.6 | 722 ms |
| 1.5 | 36.7% | **33.4%** | 1.0% | 0.399 | 28.1 | 738 ms |
- **结论**:n=512 稳定在 **33% 左右**(n=256 略乐观);**历史最佳(n=512,部署口径)= gain 1.5 + replan=empty:到达 33.4% / 碰撞 1.0% / ratio2s 0.399 / 刹停 28.1 / 738 ms**。保守版 gain 1.2:32.6% / 1.0% / 0.435 / 18.6 / 722 ms(刹停少 1/3)。
- **监控页回写**:`ml/monitor.py` 新增 `/api/explore`(读 docs/探索日志.md),`ml/monitor.html` 新增「闭环探索日志」卡片;Edge headless 截图确认渲染正常(http://127.0.0.1:8100)。
- **下一步**:候选选择器(consist/value/first)与 K=4 在反馈+每步重规划下是否影响到达/刹停。
## 轮次 13:选择器 / K / 失败模式(exp: `exp/dagger-deploy`)
- **假设**:反馈混合下,候选选择器与候选数 K 会影响到达/时延;失败模式可指导下一步。
- **判据**:reach > 36.7% 或时延显著下降;同时报告 time_ok_1p3。
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,n=256。
- **对照**:轮次 11/12(consist K=8:36.7%)。
- **指标**:
| 配置 | 到达 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|
| consist K=8 | 36.7% | 1.6% | 0.323 | 27.4 | 764 ms |
| value K=8 | 35.9% | 1.6% | 0.334 | 29.4 | 1063 ms |
| first K=8 | 36.3% | 1.2% | 0.338 | 28.1 | 780 ms |
| K=6 | 35.9% | 1.6% | 0.353 | 28.6 | 627 ms |
| K=4 | 31.6% | 1.2% | 0.396 | 32.6 | 482 ms |
- **失败模式(n=256,consist K=8)**:失败 149 题中 **碰撞仅 3 题(1.2%)、超时无碰撞 146 题(57.0%)**;失败题终点误差 p50 140 mm;`time_ok_1p3` 仅 **13.3%**;成功题 t_done p50 2.74 s(时限 p50 3.4 s);刹停 27.4 次/题。
- **结论**:选择器无差别(consist 略优);K=8 到达最佳,K=6 省 18% 时延(到达 -0.8 pp)。**当前真正的瓶颈是时间效率(频繁刹停→超时),不是碰撞**。§9.2 的「用时 ≤ 老师×1.3」当前只有 13.3%。
- **下一步**:每步重规划下,安全层只需认证第 1 步(现在验 3 步,过度保守→全拒→刹停);改 `safety_steps=1` 验证能否在保持低碰撞的同时大幅减少刹停。
## 轮次 14:部分安全前缀(exp: `exp/dagger-deploy`,刹停大降)
- **假设**:安全层现在要求 3 步全安全才执行,候选只要第 2–3 步有碰撞就全拒刹停;改为「执行到第一个不安全步为止」可减少刹停。
- **判据**:刹停显著下降、到达不降、碰撞 ≤3%。
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,`safety_partial=1`;n=256。
- **对照**:全窗口(轮次 11/13:36.7% / 1.6% / 0.323 / 27.4 / 775 ms)。
- **指标**:
| 安全层 | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| 3 步全安全 | 36.7% | 13.3% | 1.6% | 0.323 | 27.4 | 775 ms |
| **部分安全前缀** | **38.3%** | 13.7% | 1.6% | **0.308** | **8.6** | 770 ms |
- **结论**:刹停 **27.4→8.6(-69%)**、到达 +1.6 pp、碰撞不变——**部分安全前缀有效**(新最佳:38.3% / 1.6% / 0.308 / 8.6 / 770 ms)。但 `time_ok_1p3` 仍 13.7%:说明超时主因不是刹停,而是**执行粒度**——部分前缀的填充零步仍在消耗时间。
- **下一步**:commit_n=1(每次只提交 1 步、每步规划,无填充步),验证时间效率。
## 轮次 15:每步规划(commit_n=1)(exp: `exp/dagger-deploy`,新历史最佳)
- **假设**:部分前缀的填充零步仍在消耗时间;每次只提交 1 步、每步重新规划可消除填充、提升时间效率。
- **判据**:time_ok_1p3 显著上升、到达不降、碰撞 ≤3%。
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,`safety_partial=1, commit_n=1`;n=256。
- **对照**:commit_n=3 部分前缀(轮次 14:38.3% / 13.7% / 1.6% / 0.308 / 8.6 / 770 ms)。
- **指标**:
| commit_n | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| 3 | 38.3% | 13.7% | 1.6% | 0.308 | 8.6 | 770 ms |
| **1** | **41.8%** | **21.5%** | 2.3% | **0.259** | 26.9 | 967 ms |
- **结论**:每步规划把到达推到 **41.8%**、时间效率 **13.7→21.5%**、精度 ratio2s **0.259**;代价:碰撞 1.6→2.3%(仍 <3%)、刹停计数回升(每步都判断)、时延 +26%。**新历史最佳(n=256,部署口径):41.8% / 2.3% / 0.259 / 21.5% / 967 ms**。
- **注**:commit_n=1 的推理频率是 3 倍,真机需核对 §9.2 的 P99 <20 ms 预算(当前桌面 12.58 ms/次)。
- **下一步**:n=512 复核;若稳定则收尾(更新最佳、监控页、分支清理)。
## 轮次 16:n=512 复核每步规划(稳定结论)
- **假设**:轮次 15 的 41.8%(n=256)需更大样本确认。
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
- **配方**:dag2 best,replan='empty',gain 1.5,safety=True,safety_partial=1,commit_n=1;n=512。
- **对照**:轮次 15(n=256:41.8% / 21.5% / 2.3% / 0.259 / 967 ms)。
- **指标**:到达 **40.8%**、time_ok_1p3 **25.0%**、碰撞 **2.1%**、ratio2s **0.309**、刹停 25.3、墙钟 **852 ms/题**。
- **结论**:稳定。**历史最佳(n=512,部署口径)= 40.8% 到达 / 2.1% 碰撞 / ratio2s 0.309 / time_ok 25.0% / 852 ms/题**;从基线 0% 到达提升 40.8 pp。相对 §9.2(98%、碰撞 0、用时≤老师×1.3)仍有大差距,但机理已成立。
- **下一步**:每步规划下重扫增益(1.8/2.2/3.0),看时间效率是否继续提升;随后收尾。
## 轮次 17:每步规划下的更大增益(exp: `exp/dagger-deploy`)
- **假设**:每步规划下更大的反馈增益可进一步提升到达与时间效率。
- **判据**:time_ok 上升、到达不降、碰撞 ≤3%。
- **配方**:dag2 best,replan='empty',commit_n=1,safety_partial=1,gain ∈ {1.8, 2.2, 3.0},n=256。
- **对照**:轮次 15(gain 1.5:41.8% / 21.5% / 2.3% / 0.259 / 26.9 / 967 ms)。
- **指标**:
| gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|
| **1.8** | **43.8%** | 28.9% | 1.6% | 0.216 | 40.0 | 956 ms |
| 2.2 | 41.0% | **32.4%** | 2.0% | 0.174 | 64.4 | 976 ms |
| 3.0 | 36.3% | 32.8% | 0.8% | **0.153** | **123.2** | 994 ms |
- **结论**:gain 1.8 到达最高(43.8%);2.2/3.0 精度与时间效率更好但到达下降、刹停暴涨(3.0 达 123 次/题,过冲)。**历史最佳按到达优先(碰撞 ≤3%):gain 1.8 = 到达 43.8% / 碰撞 1.6% / ratio2s 0.216 / time_ok 28.9% / 956 ms(n=256)**。保守/时间优先可选 2.2。
- **下一步**:n=512 复核 gain 1.8;随后收尾。
## 轮次 18:n=512 复核 gain 1.8(收尾)
- **假设**:轮次 17 的 43.8%(n=256)需更大样本确认。
- **判据**:reach 与 n=256 差异 <5 pp、碰撞 ≤3%。
- **配方**:dag2 best,replan='empty',commit_n=1,safety_partial=1,gain 1.8;n=512。
- **对照**:轮次 17(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms);轮次 16(gain 1.5 n=512:40.8% / 25.0% / 2.1% / 0.309 / 25.3 / 852 ms)。
- **指标**:到达 **42.2%**、time_ok_1p3 **29.1%**、碰撞 **2.9%**、ratio2s **0.270**、刹停 37.4、墙钟 **870 ms/题**。
- **结论**:gain 1.8 在 n=512 稳定(42.2%),到达/时间效率优于 gain 1.5(+1.4 pp / +4.1 pp),但碰撞 2.1→2.9%(贴近 3% 线)、刹停 25.3→37.4。**历史最佳(到达优先,n=512)= gain 1.8;碰撞优先 = gain 1.5**。
- **下一步**:本轮探索收尾(监控页已回写);进入大规模训练需用户确认 §9.2 锚点。
## 轮次统计
- 连续无实质进展轮数:0(轮次 9–17 持续有实质进展)。
- **历史最佳(部署口径,n=512)**:`ml/runs/base_dag2_dag1/best.pt` + `K=8, safety=True, replan='empty', fb_gain=1.8, safety_partial=1, commit_n=1` → **到达 42.2%、碰撞 2.9%、ratio2s 0.270、time_ok_1p3 29.1%、刹停 37.4/题、仿真墙钟 870 ms/题**;推理 12.58 ms/次(B=64,K=8,桌面 3090 均值,P99 未测)。
- 保守版(碰撞优先):gain 1.5 → 40.8% / 2.1% / 0.309 / 25.0% / 25.3 / 852 ms。
- 与基线对比:到达 **0→42.2%**、碰撞 **99.6→2.9%**、ratio2s **0.755→0.270**;代价:DAgger 微调使开环 ol_fk 5.98→15.7 mm。
- 相对 §9.2:到达 42.2% vs ≥98%(差距大);碰撞 2.9% vs 0(接近策略单独 <3% 线);用时 ≤老师×1.3 为 29.1%;推理+认证 P99 未测(桌面均值 12.58 ms,预算 20 ms)。
- 未进入大规模训练(需用户确认 §9.2 锚点)。
- 训练方法探索(轮次 19–22):APG(管线修复、尺度失控)、反馈蒸馏(未内化)、DAgger-3(39.5%)、冻结编码器(41.8%)——均未超过 dag2 基线 43.8%。
## 轮次 19:APG 闭环策略梯度试跑(exp: `exp/dagger-deploy`,管线修复但暂不采用)
- **假设**:用闭环目标/碰撞损失直接微调策略(把"朝目标走"内化),减少对执行层反馈的依赖。
- **判据**:纯策略闭环 success 上升、碰撞铰链下降;部署口径(+反馈/安全层)不降。
- **配方**:`ml.apg.apg_finetune --init base_dag2_dag1/best.pt --steps 2`(先冒烟;原计划 500 步)。
- **修复的两个既有 bug**(APG 此前从未实测):
1. `kin_torch._cat_gaps` 的 `p['b'] += s` 是 in-place,而 `b` 被 autograd 索引反向图引用 → backward 报 version 错;改为 `p['b'] = p['b'] + s`。
2. APG 段边界 `queue.detach()` 让下一段执行的动作无梯度;改为只 detach 状态并重置队列(下一 plan 走 r=0 重新提交)。
- **指标(2 步冒烟)**:EVAL0(纯策略)success 0 / 碰撞 31.8%;第 0 步 col(碰撞铰链均值)**409508**、gnorm 473119(被裁剪)、tgt 206.6、smooth 645;EVAL@2 success 0 / 碰撞 26.0%。
- **结论**:管线已能跑,但**损失尺度失控**(碰撞铰链均值 40 万,远大于 margin 3mm 的合理量级);且 APG 展开是纯策略口径(无反馈/安全层/每步规划),与当前部署形态不一致。**暂不采用**,转「反馈蒸馏」。
- **下一步**:反馈蒸馏——用最终部署配置收集 (特征, 复合动作=模型输出+目标反馈) 样本,微调模型把反馈内化。
## 轮次 20:反馈蒸馏(exp: `exp/dagger-deploy`,负结论)
- **假设**:用最终部署配置收集 (特征, 复合动作 = 模型输出 + 目标反馈) 样本,微调后模型可内化反馈(部署时降低/去掉反馈仍能到目标)。
- **判据**:部署口径下 `fb_gain=0~0.9` 时 success 接近/超过基线(43.8%,n=256),且 time_ok 不降。
- **配方**:`rollout` 加 collect 钩子(与部署一致:safety、replan='empty'、safety_partial=1、commit_n=1、fb_gain=1.8)收集 500 题 → **17.7 万样本**;`train.py --config distill.json`(lr 1e-4、3000 步、w_value=0、w_sub=0、w_fk=0.1、w_smooth=0.01)+ 主数据 BC 锚定,从 dag2 best 微调。
- **对照**:dag2 + gain 1.8(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
- **指标(n=256,部署口径)**:
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|---|
| distill | 0.0 | **0.8%** | — | 0.0% | 0.708 | 2.1 | 762 ms |
| distill | 0.9 | 36.7% | 10.5% | 1.2% | 0.421 | 12.4 | 916 ms |
| distill | 1.8 | 43.4% | 27.7% | 2.0% | 0.228 | 47.4 | 925 ms |
| dag2(基线) | 1.8 | 43.8% | 28.9% | 1.6% | 0.216 | 40.0 | 956 ms |
- **结论**:**反馈未内化**——`gain=0` 时到达仅 0.8%(与基线纯策略一样);`gain=1.8` 时与基线持平(43.4 vs 43.8,差约 1 题),碰撞/刹停略差。蒸馏样本占比仅 8.5%、标签仍以主数据增量为主,模型没有学会"看目标修正"。此配方不成立。
- **下一步**:与最终部署配置一致的 DAgger 第 3 轮(采集时已用 safety+fb 1.2,但仍是 3 步提交/全窗口;现在部署是每步规划+部分前缀)。
## 轮次 21:DAgger-3(最终部署配置采集)(exp: `exp/dagger-deploy`,负结论)
- **假设**:用最终部署配置(每步规划 + 部分安全前缀 + 反馈 gain1.8 + 安全层)采集,训练分布与部署完全一致,可继续提升。
- **判据**:部署口径 success > 43.8%(dag2 基线,n=256)。
- **配方**:`collect --fb_gain 1.8 --safety 1 --replan empty --commit_n 1 --safety_partial 1`(500 题、73 起点)→ 老师重规划 → 打包 → 从 dag2 best 微调 15000 步(lr 3e-4,配置 base_dag3)。
- **对照**:dag2 + gain1.8(n=256:43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
- **指标(n=256,部署口径)**:
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 |
|---|---|---|---|---|---|---|---|
| dag3 | 1.5 | 39.8% | 16.0% | 1.2% | 0.360 | 14.7 | 912 ms |
| dag3 | 1.8 | 39.5% | 21.1% | 1.2% | 0.320 | 24.5 | 935 ms |
| dag2(基线) | 1.8 | **43.8%** | **28.9%** | 1.6% | **0.216** | 40.0 | 956 ms |
- **结论**:DAgger-3 到达 **-4.3 pp**(39.5 vs 43.8),time_ok -7.8 pp;碰撞/刹停略好但主指标下降。开环进一步退化(ol_fk 19.5 mm vs dag2 15.7)。**负结论**:继续微调(15000 步 lr 3e-4)会退化主分布,训练侧收益递减。
- **下一步**:冻结编码器、只微调解码器(假设:微调退化来自表征漂移),用同一份数据与预算验证。
## 轮次 22:冻结编码器微调(exp: `exp/dagger-deploy`,负结论)
- **假设**:微调导致的分布退化来自表征漂移;冻结编码器(enc + enc_norm)、只调解码器/头,可保留开环精度同时吸收 DAgger-3 的部署分布样本。
- **判据**:部署口径 success > 43.8%(dag2)且 ol_fk 不退化(≤16 mm)。
- **配方**:`train.py --freeze-enc`(优化器只含 requires_grad 参数),数据 = 主数据 + DAgger-3 样本,lr 3e-4、5000 步,从 dag2 best 初始化;n=256 部署口径。
- **对照**:dag2 + gain1.8(43.8% / 28.9% / 1.6% / 0.216 / 40.0 / 956 ms)。
- **指标**:
| 模型 | gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 | 墙钟/题 | ol_fk |
|---|---|---|---|---|---|---|---|---|
| 冻结编码器 | 1.5 | 39.8% | 22.3% | 1.2% | 0.293 | 25.4 | 936 ms | 15.8 mm |
| 冻结编码器 | 1.8 | 41.8% | 26.2% | 2.3% | 0.274 | 41.4 | 940 ms | 15.8 mm |
| dag2(基线) | 1.8 | **43.8%** | **28.9%** | 1.6% | **0.216** | 40.0 | 956 ms | 15.7 mm |
- **结论**:冻结编码器**确实减缓了开环退化**(ol_fk 15.8 vs 全微调 19.5),但部署到达仍比 dag2 低 2.0 pp——**无提升**。至此训练侧四种方法(APG、反馈蒸馏、DAgger-3、冻结编码器)均未超过 dag2 基线(43.8%),训练侧在当前数据/预算下已到平台。
- **下一步(超出小规模范围,需确认)**:更大规模训练(24000 步从头 + 8000×2 轮 DAgger)或改特征/架构(目标条件化、状态扰动训练需重算特征)。
## 轮次 23:实时性口径评估(exp: `exp/dagger-deploy`)
- **背景**:870 ms/题是仿真里整条轨迹的总时长(150–300 次规划累加),不是控制延迟。实时口径按 §9.2:每次「推理 + 安全认证」P99 < 20 ms(50 Hz,DT=0.02 s)。
- **假设**:单次规划延迟可以压到 20 ms 以内。
- **判据**:B=1 单题、单次规划(features + sample_k K=8 + 目标反馈 + 安全层)P99 < 20 ms。
- **优化**:① DLS 目标反馈的 6 次串行有限差分改为一次批量 FK(数值差 ≤0.013°,DLS 25→10–17 ms);② 单题安全层按候选早停(首个全安全候选即停,16 点→2–4 点)。
- **指标(桌面 RTX 3090,B=1,Python;GPU 时钟抖动使 p50 在 40–65 ms 间波动)**:单次规划 **p50 40–65 ms / p99 70–80 ms**;分解:features 10–17 ms、推理 8–14 ms、DLS 反馈 10–17 ms、安全层 10–17 ms。
- **实时频率扫描(n=256,部署口径,gain1.8 + safety + partial)**:
| 规划节奏 | 控制周期 | 到达 | time_ok_1p3 | 碰撞 | 刹停/题 |
|---|---|---|---|---|---|
| 每步(commit_n=1) | 20 ms | **42.2%(n=512)** | 29.1% | 2.9% | 37.4 |
| 每 3 步(commit_n=3) | 60 ms | 36.3% | 16.4% | 1.6% | 14.3 |
| 每 6 步(commit_n=6) | 120 ms | 14.8% | 3.9% | 2.0% | 6.1 |
| 每 10 步(commit_n=10) | 200 ms | 2.0% | — | 1.2% | 3.7 |
- **结论**:**当前方案不满足 50 Hz 实时**(单次 40–80 ms,超预算 2–4 倍);42.2% 的成绩依赖每步规划(20 ms 节奏)。实时可行节奏下:每 3 步 → 36.3%(p50 有余量、p99 需继续优化),每 6 步 → 14.8%。**实时性是当前方案的硬约束,不是仿真耗时问题**。
- **下一步(需确认)**:① 浏览器 ONNX 路径实测(推理 P99 历史 10–12 ms;特征/安全层在 JS 端需 `tests/policy_bench.cjs` 实测);② 特征缓存 + 安全层粗筛,目标单次 P99 < 20 ms;③ 或接受「每 3 步规划」的 60 ms 周期(36.3%)。
## 轮次 24:实时性优化与新方法(exp: `exp/dagger-deploy`)
- **目标**:准确率更高 + 效率满足 50 Hz(20 ms 控制步)。
- **① 混合节奏(规划每 3 步 + 执行时每步反馈)——负结论**:plan fb1.8 + exec fb1.8 → 36.3%(与纯规划反馈的 commit_n=3 完全相同);plan fb0 + exec fb1.8 → 仅 1.6%。执行时反馈不带来额外收益(规划时的反馈已是主要贡献),此捷径不成立。
- **② 特征几何优化(有效)**:特征只用 8 组间隙(GP),但原实现算全部 49 组(67256 簇对)。新增 `Kin.gm_groups(q, GP, cap)`:按 7 连杆组对过滤簇对(簇索引 → `cl_g` → 组对 min/max),只算 8 组。
- 数值验证:与 `gaps()['gm'][:, a*7+b]` 逐项一致(diff 0)。
- 延迟:gm 计算 10.4→**7.1 ms**(p50),单次规划省约 3 ms。
- **③ 浏览器部署**:导出 `webapp/models/traj_policy.onnx`(dag2 best,19.8 MB,对拍误差 3.1e-6,act_scale 1.0);`ik-policy.js` 加全程目标反馈(`fbGain`)+ `commit` 可配;`ik-policy-glue.js` 支持 `?fb=&commit=`(默认 fb=1.8、commit=3)。
- JS bench(Node,10 题,fb1.8+commit1):到达 0、**刹车率 47.3%**(每 2 次规划刹停一次)、planP50 43.8 / P99 70.5 ms。JS 安全层(5 mm 护栏 + 距离认证)比 Python(0.5 mm gap)保守,反馈+每步规划被大量拒绝。
- **④ 实时口径的关键认识**:部署形态是**异步规划**(tick 20 ms 只执行队列;`plan()` 后台跑),所以 50 Hz 的约束是「tick < 20 ms + 规划在队列耗尽前完成」,不是「单次规划 < 20 ms」。
- JS 的 tick 只做队列执行 + 投影/碰撞检查(~1.5–3 ms)→ 50 Hz 可行;
- 规划 P99 70 ms → 需要 commit ≥ 4(80 ms 队列)才不饥饿;commit=3(60 ms)会偶发饥饿。
- 准确率与规划频率的 trade-off:commit1 42.2%(不可 50Hz)、commit3 36.3%、commit6 14.8%。
- **下一步**:① 在 commit=3/4 的实时节奏下找准确率提升(候选平均、dag3_fe 冻结编码器、更高 gain);② 把 gm_groups 同步到 JS(gapMatrix 只算 8 组);③ 降低 JS 安全层拒绝率(margin/认证预算调参)。
## 轮次 25:实时节奏下的准确率探索 + 推理提速(exp: `exp/dagger-deploy`)
- **目标**:在 50 Hz 可行的节奏(commit=3,60 ms 周期)下提升准确率。
- **效率优化(有效)**:
- `Kin.gm_groups`:特征只算 8 组间隙(数值 diff 0),features 10.4→**7.1 ms**;
- **onnxruntime CPU 推理 3.9 ms**(PyTorch 11.1 ms,部署端本来就是 ONNX);
- 单次规划 ≈ 7.1(features)+3.9(推理)+11(DLS)+8(安全)+2 ≈ **32 ms p50 / ~45 ms p99**;
- 异步部署下 tick(执行步 + 碰撞检查)2–3 ms → **50 Hz 控制可行**;commit=3(60 ms 队列)可覆盖规划 p99。
- **准确率尝试(n=256,commit=3,gain1.8,safety_partial)**:
| 方法 | 到达 | time_ok | 碰撞 | ratio2s | 刹停 |
|---|---|---|---|---|---|
| dag2 + consist(基线) | **36.3%** | 16.4% | 1.6% | 0.277 | 14.3 |
| dag2 + 候选平均(select=average) | 29.7% | 20.3% | 0.4% | 0.432 | 55.9 |
| 冻结编码器模型(dag3_fe) | 34.4% | 14.8% | 2.0% | 0.332 | 15.8 |
| dag2 + commit=4 | 28.9% | 8.2% | 3.5% | 0.339 | 10.5 |
- **结论**:候选平均、冻结编码器、更稀疏规划**均未超过 36.3%**。**50 Hz 节奏下的最佳配置 = dag2 + commit=3 + consist + gain1.8 + safety_partial:到达 36.3% / 碰撞 1.6% / ratio2s 0.277**(相对非实时的 42.2% 差 5.9 pp)。
- **下一步**:若要把实时准确率再拉高,需要把单次规划再压到 ~22 ms 以支持 commit=2(40 ms 周期,预计 ~39%);或提升模型本身精度(训练侧)。
## 轮次 26:解析雅可比 DLS——去掉差分噪声,准确率大幅提升(exp: `exp/dagger-deploy`,新历史最佳)
- **背景**:DLS 目标反馈此前用 6 次批量有限差分(ε=1e-3°)求雅可比。float32 下旋转项信噪比只有 ~1%,位置项 ~3.6%(与 autograd 对拍,见下),相当于每次规划往反馈方向注入 2–4% 的随机扰动。
- **假设**:改用几何雅可比(一次 FK)可同时去噪 + 提速;闭环准确率应不降、延迟应下降。
- **判据**:① 雅可比与 autograd 一致(rel < 1e-4);② n=256 commit=3 到达 ≥ 36.3%(轮次 25 基线,不劣化);③ DLS 单次延迟下降。
- **配方**:`Kin.jac` 几何雅可比(Jv_k = a_k×(p−o_k),Jw_k = a_k,度单位);`dls_steps(analytic=True)` 默认;其余同轮次 25(dag2、K=8、gain1.8、safety_partial、replan=empty)。
- **数值对拍**(`ml/eval/check_dls.py`):
- 位置项:解析 J 与 autograd 逐项一致(rel 0.00000);有限差分 J 自身 rel 误差 **3.58%**。
- 姿态项:解析 J 与差分 DLS 输出 max 0.20°/rel 5%(差的部分是 rotvec 的二阶项 Jr(e),非噪声)。
- DLS 输出:pos-only max 0.12°,pos+rot max 0.20°。
- **延迟**(B=1,3090):n=1 **9.3→7.1 ms**,n=3 **27.7→19.5 ms**(每次 DLS 少 6 次批量 FK)。
- **闭环(replan=empty,analytic,n=256)**:
| commit | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|---|---|---|---|---|---|
| 3(FD 对照,完全复现轮次 25) | 36.3% | 16.4% | 1.6% | 0.277 | 14.3 |
| 3(解析 J) | 46.5% | 16.4% | 1.6% | 0.337 | 13.5 |
| 2(解析 J) | **50.4%** | 27.0% | 2.0% | 0.287 | 22.1 |
| 1(解析 J) | **57.4%** | **31.6%** | 3.1% | 0.251 | 39.1 |
- **n=512 复核(commit=2,解析 J)**:到达 **49.2%** / time_ok **26.2%** / 碰撞 **2.3%** / ratio2s 0.351 / 刹停 21.0(n=256 的 50.4% 在噪声内)。
- **结论**:
- 解析雅可比**既快又准**:commit=3 到达 36.3→46.5%(+10.2 pp),碰撞不变;commit=2 达 **49.2%**(历史最佳 42.2% 是 commit=1 + FD,现在 commit=2 就超过),碰撞 2.3%。
- 机理:DLS 差分雅可比在 float32 下的 2–4% 噪声被慢变误差闭环放大(轮次 4/闭环根因结论:σ>0.02×STEP 即崩);解析 J 去掉噪声后失败题里"能到 50 mm 却到不了"的那批能收敛了(err_fail 中位数 51→217 mm:失败集两极分化,成功集变大)。
- ratio2s 在 commit=3 变差(0.277→0.337)但到达更多:慢收敛题现在"最终到得了",不是"更快"。
- **新历史最佳(50 Hz 目标节奏,commit=2)= 49.2% / 碰撞 2.3% / ratio2s 0.351 / time_ok 26.2%**;非实时上限(commit=1)= 57.4%(n=256,待 n=512 复核)。
- **下一步**:① n=512 复核 commit=1;② 积分反馈(fb_ki,针对慢变偏差)在 commit=2 上叠加;③ 浏览器 JS 端同步解析 J;④ commit=2 的 40 ms 周期实测规划 P99(Python/JS)。
## 轮次 27:解析 J 后重扫增益 + 积分/缓存反馈(负结论)+ 浏览器安全层对齐(exp: `exp/dagger-deploy`)
- **目标**:解析雅可比改变了反馈噪声结构,重扫增益;同时修浏览器部署路径的安全层(轮次 25 遗留:bench 刹车 47%、到达 0)。
- **增益重扫(commit=2,empty,analytic,n=256)**:
| gain | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|---|---|---|---|---|---|
| 1.8 | 50.4% | 27.0% | 2.0% | 0.287 | 22.1 |
| **2.2** | **52.0%** | 29.7% | 1.6% | 0.329 | 35.3 |
| 2.5 | 49.6% | 34.4% | 1.2% | 0.334 | 47.9 |
| 2.8 | 47.7% | **34.4%** | 2.0% | 0.362 | 59.8 |
- **n=512 复核 gain 2.2(commit=2)**:**到达 50.8% / time_ok 28.9% / 碰撞 1.4% / ratio2s 0.343 / 刹停 35.9**。**新历史最佳(50 Hz 节奏)= gain 2.2 + commit=2**。
- **负结论**:
- 积分反馈(fb_ki=0.3,decay0.9,限幅0.5·STEP):到达 48.0%(gain2.2 单独 52.0%),−4.0 pp。慢变偏差用积分项补偿会过冲,不采用。
- 反馈隔次复用(fb_every=2,省 ~3.5 ms/次规划):到达 46.9%(−5.1 pp)。误差方向变化比预期快,缓存不成立。
- **浏览器部署修复(有效)**:三个与 Python 仿真不一致的 bug 修掉后(`webapp/js/ik-policy.js`):
1. `tick` 不夹关节限位(Python `clamp_box` 每步夹)→ q 一旦越限,认证的 `inBox` 永远拒绝所有候选,原地刹停到超时(certWhy.box 2650 次);现执行与候选投影都夹 LIM。
2. 安全层整段拒绝 → 刹停;移植 `safety_partial`:整段不过时依次缩短前缀,执行到第一个不安全步。
3. 新增 `segCert` 开关(默认 true):连续区间认证(距离下界递归)~40 ms/次,是 50 Hz 预算的主要超支项;关闭后改为离散判定「每步端点+中点 collideFree」(与 Python 安全层同口径),默认仍走严格认证,bench 用 `--seg 0` 测部署口径。
- JS bench(nc10,fb2.2,commit2;`node tests/policy_bench.cjs ... --seg 0`):
| 集合 | 修复前 ok/reached | 修复后 ok/reached | 刹车率 | safety P50 | plan P50 / P99 |
|---|---|---|---|---|---|
| near_column_pos | 0/—(轮次 25) | **5/7** | 79.5→**48.1%** | 41.6→**19.8 ms** | 56.1→**33.5 / 61.1 ms** |
| near_column_pose | 0/— | **3/3** | 80.4→**31.6%** | 43.4→**22.8 ms** | 57.3→**37.6 / 66.3 ms** |
- **结论**:实时配置定为 `dag2 + 解析 J + gain2.2 + commit2 + safety_partial + replan=empty`:n=512 到达 **50.8%**、碰撞 **1.4%**。浏览器端从「0 到达 + 47% 刹车」恢复到 nc 集可达,plan P50 33.5/37.6 ms;P99 仍 61–66 ms,commit=2 的 40 ms 队列会偶发饥饿,需继续压 P99(K/安全候选数/JS 特征)。
- **下一步**:① Python 测 `safety_try=1`(只认证首选候选)对准确率的影响,若无损则 JS 同步(省多次 collideFree);② K=4/6 的准确率-时延权衡;③ JS `gapMatrix` 同步只算 8 组特征;④ commit=2 异步部署的饥饿率实测。
## 轮次 28:端点进度选择——时间效率大幅提升(exp: `exp/dagger-deploy`,新历史最佳)
- **假设**:一致性选择奖励"与上一条计划一致",但失败主因是超时(轮次 13:失败 57% 是超时)。改成直接按候选块末态离目标的距离选(`select='endpoint'`),应提升时间效率与精度。
- **判据**:time_ok_1p3 显著上升、到达不降、碰撞 ≤3%。
- **配方**:dag2 + 解析 J,gain2.2,commit=2,replan=empty,safety_partial;n=256 → n=512。选择器用 `goal_err(fut[-1])`(只做一次 K 端点 FK,不额外推理)。
- **指标(n=256,commit=2)**:
| 选择器 | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|---|---|---|---|---|---|
| consist(轮次 27) | 52.0% | 29.7% | 1.6% | 0.329 | 35.3 |
| **endpoint** | **52.7%** | **37.5%** | 2.3% | **0.210** | 34.6 |
| mix(consist − 0.1·端点mm) | 50.0% | 30.5% | 2.3% | 0.300 | 35.1 |
- **n=512 复核(endpoint,commit=2)**:**到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285 / 刹停 35.7**(consist n=512:50.8% / 28.9% / 1.4% / 0.343 / 35.9)。
- **节奏曲线(endpoint,n=256)**:commit=1 → **59.4% / 42.6% / 3.9% / 0.133**;commit=2 → 52.7% / 37.5% / 2.3% / 0.210;commit=3 → 47.7% / 27.0% / 1.2% / 0.245。
- **负结论**:
- `safety_try=1/2`(只认证前 1/2 条候选):41.4% / 44.9%(全 K 52.0%)——候选兜底对到达贡献 ~10 pp,不能省。
- K=4:47.3%(K=8 52.0%)——少采样同样损失到达。
- DLS 兜底半步(全拒时朝目标走一步并认证):52.3%(基线 52.0%),碰撞 1.6→2.7%,无收益;重复多步版本碰撞 45.7%(第二步未认证),已回退为单步且默认关闭。
- 增益调度(远 2.8/近 2.2):47.3% / time_ok 32.8%,等同 gain2.8 常数;高增益在远处就造成过冲,按距离衰减无效。
- **浏览器同步**:`select='endpoint'` 进 Runner/bench(`--select endpoint`),冷启动跳过 8 份前瞻(省 ~80 ms P99,冷启动价值评分被端点排序替代)。nc10 bench(--seg 0 --select endpoint):pos ok 4/5 reached、pose ok 2/4,刹车率 32.1%/25.5%(consist 48.1%/31.6%),planP50 31.9/36.5 ms、P99 63.5/67.5 ms。
- **同步 bench 耗时与队列耗尽估算(不是 50 Hz 调度实测)**:Runner 的 `starveTicks = max(0,ceil(planMs/20) − 规划时队列步数)`,不含真实 tick 时间戳。
| 集合(endpoint, commit=2, --seg 0) | 到达 ok/reached | 刹车率 | planP50/P99 | 估算饥饿 tick/次规划 |
|---|---|---|---|---|
| near_column_pos (10) | 4/5 | 32.7% | 32.0/62.1 ms | 0.87 |
| near_column_pose (10) | 2/3 | 25.6% | 36.8/68.0 ms | 0.78 |
| route40 (20) | 12/13 | 40.4% | 44.3/68.1 ms | 1.26 |
- commit=2 对应 40 ms 队列,route 集 planP50 44 ms 已超过;估算提示可能饥饿,但该 bench 等规划完成才 tick,没有观测实际空转。
- commit=3 对应 60 ms,仍低于上述 P99,不能据此宣称连续 50 Hz。其 Python 同步结果:endpoint n=256 → 47.7% / 27.0% / 1.2% / 0.245;gain2.5 为 45.7% / 24.6%。
- **结论(经轮次 29 更正)**:端点选择 + 解析 J + gain2.2 + commit=2 + safety_partial,在 n=512 同步仿真为 **到达 52.1% / time_ok 35.2% / 碰撞 2.0% / ratio2s 0.285**。历史 42.2% 使用 commit=1 与 FD,非单变量对照。commit=1 到达 59.4% 但碰撞 3.9%;无论 2.0% 还是 3.9%,开安全层均未通过零碰撞要求,实时性亦未通过。
- **下一步**:先查碰撞来源,保持严格认证和查看器默认;再测真实异步调度及认证成本,不因 Python 收益直接切换部署默认。
## 轮次 29:安全层全拒后减弱目标反馈(exp: `exp/dagger-deploy`)
- **假设**:目标反馈将绕障候选推向障碍,连续全拒会反复停在同处;仅在上一计划被全拒时减小下一计划反馈,可能保留正常段收敛速度并减少重复刹停。
- **判据**:同任务、同 seed、同 K、同认证下到达和 time_ok 不下降,碰撞不增加;部署门槛仍是零碰撞及真实时延达标。所有实验不重训、不扩大数据。
- **固定配方**:`python -m ml.eval.eval_cl --n 256 --K 8 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --seed 0`;dag2 checkpoint、几何雅可比。下表仅改列出的参数,默认 `fb_block_gain=1,dls_safety=0`。512 题按两个 256 批、每批 seed=0,沿用轮次 28 顺序。
| 改动(n=256) | 到达 | time_ok_1p3 | 碰撞 | ratio2s | 刹停/题 |
|---|---|---|---|---|---|
| 原配置复跑 | 52.7% | 37.5% | 2.3% | 0.210 | 34.6 |
| `--dls_safety 1` | 52.7% | 37.5% | 2.0% | 0.210 | 34.6 |
| `--fb_clear 20 --fb_cmin 0.3` | 32.0% | 5.9% | 1.2% | 0.323 | 3.2 |
| `--sub_refresh 1` | 51.6% | 35.5% | 2.0% | 0.279 | 33.8 |
| `--select adapt` | 52.7% | 37.9% | 2.3% | 0.210 | 34.6 |
| `--fb_block_gain 0.3` | 56.2% | 38.7% | 3.1% | 0.178 | 17.2 |
| `--fb_block_gain 0.5` | **57.4%** | **39.1%** | 3.1% | 0.210 | 19.3 |
| `--fb_block_gain 0.7` | 55.5% | 38.3% | 2.3% | 0.189 | 25.0 |
| `--fb_block_gain 0.5 --dls_safety 1` | **57.4%** | **39.1%** | 2.7% | 0.210 | 19.2 |
- **单独减半反馈 n=512**:到达 55.1% / time_ok 36.1% / 碰撞 2.5% / ratio2s 0.251 / 刹停 19.5;相对轮次 28 同规模基线 52.1% / 35.2% / 2.0% / 0.285 / 35.7,到达 +3.0 pp、碰撞也 +0.5 pp,不满足“碰撞不增加”判据。
- **为何不采用其他变体**:所有近障碍状态都衰减反馈会明显拖慢到达;子目标重预测无收益;adapt 全拒后旧计划本来就失效,一致性分数全为 0,实质变成等分选首条而非“继续绕行”。
- **原配置失败分解(n=256)**:135 题按时到达,121 题失败(其中 20 题后来才到,101 题始终没到)。A/B/C/D/Dr 分别 128/74/32/13/9 题,B/C 失败题刹停中位数约 96/93 次;D 留在分母,不用删题提高成绩。
- **碰撞定位**:原配置共 6 题碰撞,只有 224 号首次碰撞紧跟 DLS 接管;另外 5 题首次碰撞早于接管或根本未接管。为 DLS 加端点/中点检查仅减少 1 题,不能解释剩余碰撞。候选认证的未夹紧累加点与执行的逐步夹紧点不同是待验证假设,尚不能断言根因。
- **计时**:n=256 减半反馈+DLS检查整批墙钟 243.6 s,单独减半 n=512 为 443.4 s;这些不是规划时延,不能证明 50 Hz。反馈条件分支未增加几何查询,但其实际延迟收益未测。
- **记录改进**:评估输出完整参数、checkpoint/任务 SHA256、批大小和随机种子规则;增加时限内无碰撞到达率;修正 n_ref、参考时间占比和条件指标的跨批汇总。可保存实际轨迹供不重新采样的回放,NaN 在 JSON 中写为 null,执行进度每 50 步输出。
- **回归检查**:`python -m unittest tests.test_eval_cl -v` 六项通过,覆盖跨批分母、空条件集、非有限数、首撞探针正对照、精修被拒后保留安全候选、只有全拒后才减弱反馈。精修对照在关闭检查时确实碰撞,打开后保留候选,证明门会执行;这是合成测试,不替代真实几何验收。
- **仍未修复的边界**:`fb_fallback=0` 保持关闭;非部分前缀分支仍有“只验一步却重复执行”问题,不能开启部署。速度限制在反馈叠加前而非叠加后,尚未验证速度/加速度符合硬约束。
- **减半反馈 + DLS 检查的 n=512 复核(完整输出 `docs/experiment_round29.json`)**:到达 **282/512 = 55.1%**、时限内无碰撞到达 **280/512 = 54.7%**、time_ok **36.1%**、碰撞 **9/512 = 1.8%**、ratio2s **0.251**、刹停 **19.23/题**。到达题末端误差中位数 **0.196 mm**;有参考且到达题的行程比中位数 **2.425**(远高于 1.10 要求)。整批墙钟 **488.4 s**,非控制延迟。
- 完整命令:`python -u -m ml.eval.eval_cl --n 512 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --fb_block_gain 0.5 --dls_safety 1 --out docs/experiment_round29.json --dump ml/runs/round29_rows.json --trace ml/runs/round29_trace.pt`。
- 与轮次 28 同规模端点基线相比,到达 +3.0 pp、time_ok +0.9 pp、碰撞少 1 题(10→9),不足以宣称稳定安全改善;n=256 碰撞仍由 6→7 题。
- **首撞回放(`docs/experiment_round29_collisions.json`)**:`python -m ml.eval.diag_collision --trace ml/runs/round29_trace.pt --out docs/experiment_round29_collisions.json`。9 题首次碰撞在 CPU float32/float64 均复现,均位于 **J5 限位**;q0 与首次碰撞前一点均为正间隙,首撞采样点负间隙约 **−0.017 至 −0.936 mm**。这排除了这 9 次碰撞由起点本就碰撞或 float32 单独翻号造成,但还需要记录原认证点才能证明夹紧的因果作用;0.01° 加密仅覆盖首撞段,不是整条轨迹安全审计。
- 双精度运动学/间隙另与已有 JS 2000 个样本对拍:`python -m ml.eval.check_parity` PASS,碰撞布尔差 0、间隙最大差 2.53e−13 mm。首撞回放仍共用碰撞模型,不替代真机或独立连续认证。
- **结论与下一步**:减少连续刹停确有小样本收益,但碰撞、行程和真实时延仍未过门槛;保留为实验选项,不更改查看器默认。下一轮记录原认证点与 J5 夹紧后实际点,修复后用同批同 seed A/B 和独立细采样复核,禁止靠关掉认证提速。
## 轮次 30:认证与执行统一逐步限位(exp: `exp/dagger-deploy`,局部修复有效,仍未零碰撞)
- **假设与判据**:安全层认证的是未夹紧累加轨迹,执行器却逐步夹紧 J5,导致认证通过后碰撞。必须同时证明原证书安全、执行点碰撞、记录命令经执行算式能准确重建实际点;修复后保持同任务、同种子、同模型、同增益和同排序,到达不下降,并用不依赖原 `col` 标记的加密回放检验。
- **固定配方**:`python -u -m ml.eval.eval_cl --n 256 --commit 2 --fb 2.2 --partial 1 --replan empty --select endpoint --fb_block_gain 0.5 --dls_safety 1`;K=8、seed=0、analytic DLS、dag2 checkpoint、fallback=0,其余参数默认;分别附 `--out docs/experiment_round30_before.json --trace ml/runs/round30_before.pt --dump ml/runs/round30_before_rows.json` 和对应的 `after` 路径。未训练、未扩题、未改 JS 默认。
- **可比性**:取证版 before 与轮次29首批256题完整轨迹逐元素完全相同(最大差 0°),ok/col/brakes/err/t_done/first_hit 也完全相同;before/after 配置仅输出路径不同。任务 SHA256=`249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772`,checkpoint SHA256=`f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1`,kin_fp=`2ebcbf17`。
- **直接证据**(`docs/experiment_round30_certificates.json`):旧版7次首撞的原认证端点/中点在 float32 与 float64 均为正间隙,实际执行段均有负间隙;`clamp(previous + recorded_command)` 与实际端点逐位一致。原证书与实际点主要差在 J5,首撞段差 0.340°–4.317°;其他轴仅有最高约 7.6e-6° 的累加舍入差,不足以解释毫米级穿入。全部有效认证步中,5298/34069 步偏差 >0.001°,最大 7.587°。
- **修复范围**:`execution_points` 按执行器相同顺序逐步夹紧,端点和中点都由实际路径计算;整段、部分前缀和近目标 DLS 共用。r3 的旧队列终点和特征前缀改为真实执行结果。fallback 保持实验默认关闭,但两分支统一为“认证一步后补零”,不再把只验过一步的动作重复执行;端点和中点都认证。候选排序和反馈增益未改,0.5 仍只是间隙截顶,正间隙判据没有放宽。
| 指标(同256题) | 修复前 | 修复后 |
|---|---|---|
| 按时到达 | 147/256 = 57.4% | 同147题,57.4% |
| 原端点/中点口径无碰撞到达 | 145/256 = 56.6% | 147/256 = 57.4%(下述加密审计否定其“全程无碰撞”含义) |
| time_ok_1p3 | 39.1% | 39.1% |
| 原端点/中点口径碰撞题 | 7/256 | 0/256 |
| 成功题末端误差中位数 | 0.1951 mm | 0.1890 mm |
| 成功题平均时间 | 2.2993 s | 2.3076 s |
| 2 s 误差比中位数 | 0.2100 | 0.2100 |
| 刹停/题 | 19.2383 | 19.2500 |
| 成功题指令行程比中位数 | 2.4450 | 2.4450 |
| 从实际轨迹重算成功题行程比 | 2.4251 | 2.4251 |
| 整批同步仿真墙钟 | 247.515 s | 248.834 s |
- **执行一致性复核**(`docs/experiment_round30_comparison.json`):修复后34107个有效认证步,认证端点/中点与实际执行点均逐位一致,最大差 0°;有效时段没有未经认证的移动步。这里“认证”仍仅指离散端点/中点,不能推出连续安全。CPU B=1、commit=2 的预测函数单独测量:100次预热、1000次采样、单线程,P50 0.0388→0.0518 ms,P99 0.0691→0.0939 ms;不是完整规划延迟,不能证明50 Hz。
- **独立加密审计的正反对照**:预先固定12题 `0,21,84,128,145,164,175,180,197,215,224,255`,其中包含全部7题旧首撞及5题其他任务;不按新版 `col` 过滤。逐题回放至自身时限或到达,含起点,每个插值段最大轴步距≤0.05°并包含中点,float64、每批64点。命令:`python -u -m ml.eval.diag_collision --trace ml/runs/round30_before.pt --dense_indices 0,21,84,128,145,164,175,180,197,215,224,255 --max_step_deg 0.05 --device cuda --out docs/experiment_round30_dense_before.json`,after 同理;串行运行。
- before:56118点,**7/12题检出碰撞**,14.870 s;证明探针确实能抓旧碰撞。
- after:56566点,**仍有1/12题碰撞**,15.057 s;其余11题采样点间隙为正。这是小样本有限采样,不是完整256题的连续认证。
- 剩余第180题,执行段1.08–1.10 s:上一点/中点/端点 float64 间隙为 **0.5 / 0.11723 / 0.06521 mm**,都安全,但段内约76.6%处为 **−0.14927 mm**;该段0.01°再加密185点仍检出,见 `docs/experiment_round30_missed_segment.json`。因此这次残余是端点/中点漏检,而非认证与执行错位;它比旧离散首次碰撞1.14 s还早。不得把内部计数0当成修好了;已知真实无碰撞到达至多146/256=57.0%,其余未加密任务仍未知。
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl -v` 14项通过。覆盖整段/部分限位拒绝、只保留真实安全前缀、DLS被拒后保留候选、r3旧队列起点和特征前缀及逐点证书、fallback单步/中点拒绝。把预测函数在内存中替回旧算式,限位、DLS和r3测试重新失败。加密探针另有合成正对照:原端点/中点全安全且 `col=0`,仍检出四分之一处薄障碍;安全负对照、截止后/到达后碰撞排除均通过。
- **新增硬约束证据**:before/after 两组256题实际轨迹都出现超速,最大 **1.660004×VMAX**(来源是动作限幅后叠加0.3×gain2.2反馈)。实际各轴最大速度约 `[99.600,99.600,149.400,149.400,199.200,298.800]°/s`,并非只看原始指令推测。行程仍约参考2.425倍;速度、加速度和真实控制时延均未达部署验收。历史 `travel` 累积的是指令而非夹紧后位移,本轮已同时列出实际轨迹重算值,不悄悄换分母。
- **结论与下一步**:逐步限位一致性修复有效,未牺牲本批到达率;但加密审计仍检出段内碰撞,**不能部署、不能宣称零碰撞或50 Hz合格**。下一轮先验证更强的段内认证,再单独评估反馈后的速度/加速度约束与真实端到端调度;每次保持小样本单变量对照,继续保留失败证据。
## 轮次 31:连续段认证的确定界与成本实测(exp: `exp/dagger-deploy`,判据变强,实测进不了 20 ms)
- **假设与判据**:轮次 30 的端点/中点检查漏掉段内碰撞(第180题 1.08–1.10 s,段内 −0.14927 mm,端点/中点均正)。若用可证明的全姿态运动界对整段做 Lipschitz 判据,应当(a) 在同一个漏检段上给出碰撞或拒绝,(b) 预算不足时只会拒绝而不会放行,(c) 不放宽 `>0` 判据。判据失败或耗时超预算都算负结论,必须实测不能外推。
- **确定界替代随机采样界**:JS 侧 `leverTable(4000)`/`leverBounds(20000)` 是随机姿态最大值再 ×1.05,不是全姿态上界。本轮按三角不等式给闭式界:组 g 的胶囊端点到关节 k(<g) 的最大位移 = (Σ_{j=k}^{g-2}‖P[j+1]−P[j]‖ + max_e‖e−P[g−1]‖)·|Δq_k|(k=g−1 时末项换成端点到该轴的垂直距离),组对 (ga,gb) 只累加 ga..gb−1,取组内最小仍保留 Lipschitz 性质。Python 端 `Kin.cert_lever`(15×6)即此表,未改 `lever`,也未改 JS。
- **界的量级**(STEP=[1.2,1.2,1.8,1.8,2.4,3.6]°):单整步在各组对上的位移界 **5.93–48.86 mm**,0 层通过要求端点间隙之和 > 界,即单端间隙至少 3.0–24.4 mm。实际执行段的间隙普遍只有 0.001–0.5 mm,所以“必须细分”是结构性的,不是实现缺陷。
- **基础求距 bug 与影响范围**:模型里有 1 个退化胶囊(索引 686,组 3)。旧 `seg_seg_dist` 在第二条线段退化成点时固定取该点为第二条起点,没把点投影到第一条线段上——[0,2] 段对点 1 的实测距离旧值 1 mm、反向调用约 1e−9 mm、真值 0。修复后在已有 2000 个对拍姿态的前 64 个姿态上,670 个“退化胶囊作第二条”的细对里 **20661/42880 个距离变小(旧值偏大,最大 86.003 mm)**,正反调用一致。但这批变化没有进入判定:把轮次 30 修复后 48 段的 7586 个 0.01° 加密点分别用修复前(95da491)与修复后内核重算,**组最小间隙逐位相同(最大差 0.0 mm)、碰撞布尔差 0**;`python -m ml.eval.check_parity` 仍 PASS(2000 样本,fk 4.55e−13 mm、间隙 2.52e−13 mm、特征 5.96e−08、梯度相对误差 1.85e−06、碰撞布尔差 0)。即这轮修复是正确的,但不解释轮次 30 的漏检,也不改变既有成绩。
- **认证实现**:`Kin.certify_segments` 为 float64 批量前沿二分,每根段独立查询预算,端点间隙用 `min(gap,cap)` 故钳制只会更保守;判据 `ga+gb > bound+2eps` 且两端 >eps,任一子段碰撞即整根判碰撞;超深度、超预算、非有限间隙、越出关节盒一律拒绝(`unresolved`/`outside_box`),不放行未知。宽相位 cap 可按本层界收到 `bound/2+eps`(`adaptive_cap`)。**尚未接入 rollout,未改查看器默认。**
- **48 段固定样本**(`docs/experiment_round31_certificates.json`,沿用轮次 30 的 12 题,每题取首/中/末移动段 + 原证书最小间隙段 + 显式指定 180:54;cap=20、eps=1e−4、max_depth=12):
| 每根查询预算 | 判安全 | 判碰撞 | 未知拒绝 | 查询数 p50/max |
|---|---|---|---|---|
| 2 | 20 | 0 | 28 | 2 / 2 |
| 8 | 26 | 1 | 21 | 4 / 8 |
| 32 | 37 | 1 | 10 | 4 / 32 |
- 已知漏检段(180 题执行段 54)**判碰撞,仅用 5 次查询**;该段原证书 `certified=true`、原内部 `col=0`。48 段里 0.01° 加密采样检出碰撞 1 段,认证接受该类段 **0 段**;整批与逐根调用结果逐位一致。
- 拒绝不等于安全:预算 2 时 28 段全部落 `unresolved`,实现不会把未知当通过。
- **成本模型对上实测**(`docs/experiment_round31_cert_cost_model.json`):按最紧组对的界,所需层数 `L=ceil(log2(bound_pair/(ga+gb)))`、查询数 `2^L+1`。37 个判安全段需要 2–17 次(L=0..4),10 个未知段需要 **33 或 65 次**(L=5..6),与预算 32 恰好卡在中间一致;碰撞段因提前撞到负间隙只用 5 次。结论:认证成本由 `bound/间隙` 的对数决定,**降低界或提高间隙余量**才是可行杠杆,加预算只是线性买单。
- **耗时负结论**(CUDA float64、B=1、驻留输入、每段4次预热+9次计时、变体按段交错,`docs/experiment_round31_adaptive_cap.json`):单段认证 **P50 22.93 ms、P90 58.43 ms、P99 101.57 ms,432 次里 225 次 > 20 ms**;自适应 cap 交错重测 P50 23.21 ms、P99 99.10 ms,**决策完全相同、耗时无收益**。先前顺序测得的“1.6×、整批 72→14.8 ms”经交错复测判定为 GPU 时钟/顺序漂移,不作为结论(48 根整批复测 260→212 ms 仍受同一偏差影响,且部署不会出现 48 根同时认证)。因此**连续认证按当前实现不能进 20 ms,更不能宣称 50 Hz**。
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl` **23 项通过**(轮次 30 为 14 项)。新增段认证 8 项覆盖:薄障碍段内碰撞被界抓到而清晰段被接受、预算/深度耗尽只拒不放、恒间隙合成段上预算 4/8/9 对应查询 3/5/9(验证细分层数与查询数的解析关系)、批量与逐根及乱序一致、空批与全拒前沿、NaN/±Inf 间隙拒绝、越界拒绝、退化距离对称性与梯度。**变异实测**:把类型门去掉后 `max_depth=torch.tensor(255,dtype=torch.uint8)` 会回绕成 `range(0)`、整段判据被跳过,真实间隙 −0.05 mm 的段直接返回 `safe=true`;加门后同一调用报 `certification budgets must be plain ints`。这是本轮抓到的第二个误放行路径(第一个是轮次 30 的未夹紧认证)。
- **口径与边界**:认证只在 float64 下进行,浮点求距误差没有形式上界(`seg_seg_dist` 距离下限约 1e−9 mm),eps=1e−4 mm 是工程容差不是证明;48 段取自轮次 30 修复后的执行轨迹,是固定样本不是新策略闭环;连续认证未接入 rollout,实际执行仍走端点/中点离散检查;速度/加速度超限(轮次 30 实测最大 1.66×VMAX)本轮未处理。
- **结论与下一步**:判据确实变强(抓到离散检查漏掉的段内碰撞、未知只拒不放),但成本与 `bound/间隙` 的对数绑定,**当前实现不能部署,也不得把“认证通过”说成零碰撞**。下一步两条:(1) 把逐层依赖的二分改成**一次均匀细分**(固定层数、单个 gaps 调用覆盖全部子段),其实时可行性取决于几何查询成本随批大小是否近似平坦;(2) 把余量要求移到规划/速度侧——实际轨迹贴着 0.001–0.02 mm 间隙走,任何认证方案都会退化成几十次查询,而反馈叠加后的速度约束本来就是未完成项。
## 轮次 32:均匀细分认证与确定界的反证(exp: `exp/dagger-deploy`,认证仍进不了 20 ms,但定位到真正的成本杠杆)
- **本轮假设**:第31轮的二分认证把成本花在**多次依赖的 gaps 调用**上(B=1 时每次约 7 ms 起步)。若改成固定层数的一次均匀细分,同样的判据应只付一次调用;同时需要单独证明"这个确定界本身没被低估",否则判据再快也没意义。
- **实现**:`Kin.certify_uniform(a, b, n, eps, cap=None)`——每根段取 n 个子段、一次 gaps 调用查 n+1 个点,对**每个子段**用与二分版完全相同的判据(子段两端间隙之和 > 子段位移界 + 2eps;界对 Δq 线性,故子段界 = 整段界/n)。没有"未决"状态:证不出就是 `unproven`(拒绝),不存在把未知当安全。未接入 rollout,未改查看器默认。
- **确定界的反证**(`docs/experiment_round32_bound_falsification.json`):对 48 段每段取 121 个网格点,逐组对比"实测最小间隙"与"预测下限 (ga+gb−bound)/2"。720 项检验**0 违例**;但只有 **61 项有信息量**(预测下限 < 观察 cap 2 mm 才可能被证伪;其余组对的预测下限中位数 15.9 mm、最大 19.96 mm,都被钳制恒等式自动满足)。61 项里最紧的一项仍比预测下限高 **0.035 mm**,p10 高 0.544 mm。因此这是"未能在真实运动上证伪该界",不是"证明了该界";浮点求距误差仍未做形式化上界。
- **接受率曲线**(48 段单根,`docs/experiment_round32_uniform_validation.json`):
| 子段数 n | 判安全 | 判碰撞 | 证不出 | 二次加密(2n+1点)反证的误放行 |
|---|---|---|---|---|
| 8 | 26 | 1 | 21 | 0 |
| 16 | 30 | 1 | 17 | 0 |
| 32 | 34 | 1 | 13 | 0 |
| 64 | 36 | 1 | 11 | 0 |
| 128 | 38 | 1 | 9 | 0 |
第180题 1.08–1.10 s 那段(第30轮加密回放检出 −0.14927 mm、原离散证书 `certified=true`)在**每个 n≥8 都判碰撞**。
- **宽相位 cap 的正确公式**(对第31轮一条负结论的更正):第31轮用 `bound/2 + eps`,那个值恰好**会把本可通过的子段钳成不可证**,所以既没提速也可能少接受;本轮改成"最小不挡住判定的宽相位"= `最大子段界 + 2eps`,n=4 时是 5.92 mm、n=128 时 0.37 mm。第31轮那句"自适应 cap 无收益"只在旧公式下成立,新公式的实测收益见下表(原记录保留,不回改)。
- **计时 A/B**(`docs/experiment_round32_uniform_latency.json`;B=2 根=部署 commit2 口径,24 对根逐对按正序/逆序**交替**测全部配置,每配置 3 次预热 + 7 次计时 = 168 次;CUDA float64):
| 配置 | 宽相位 mm | P50 | P90 | P99 | >20 ms 次数 | 安全/碰撞/拒(48根) |
|---|---|---|---|---|---|---|
| 均匀 n=8 auto | 5.92 | **9.68** | 13.03 | 15.16 | 0/168 | 26/1/21 |
| 均匀 n=16 auto | 2.96 | **14.14** | 18.45 | 20.87 | 5/168 | 30/1/17 |
| 均匀 n=32 auto | 1.48 | 20.21 | 27.88 | 32.27 | 87/168 | 34/1/13 |
| 均匀 n=64 auto | 0.74 | 31.43 | 48.75 | 57.70 | 165/168 | 36/1/11 |
| 均匀 n=128 auto | 0.37 | 59.76 | 89.06 | 109.09 | 168/168 | 38/1/9 |
| 均匀 n=8 cap20 | 20 | 13.13 | 16.44 | 20.96 | 3/168 | 26/1/21 |
| 均匀 n=32 cap20 | 20 | 28.41 | 39.58 | 53.02 | 153/168 | 34/1/13 |
| 均匀 n=64 cap20 | 20 | 50.85 | 75.51 | 88.36 | 168/168 | 36/1/11 |
| 均匀 n=128 cap20 | 20 | 98.46 | 139.98 | 167.04 | 168/168 | 38/1/9 |
| 二分 budget32 | 20 | 54.88 | 82.15 | 117.20 | 133/168 | 37/1/10 |
| 二分 budget65 | 20 | 71.83 | 108.56 | 122.53 | 133/168 | 41/1/6 |
- auto 与 cap20 的**决策列完全相同**,只是快 1.36–1.65×(n=8 13.13→9.68、n=128 98.46→59.76):这次是交错实测的真收益,不是第31轮那种顺序漂移。
- 单次调用的均匀细分在同样接受率下比二分便宜 5 倍以上(n=8 9.68 ms vs 二分 54.88 ms),但二分在同样时间能多证出 11 根——两条路的取舍由下一段的结论决定,不单看耗时。
- **负结论(部署口径)**:只有 n≤16 能把认证本身压进 20 ms(P50 9.68–14.14 ms),而这档只能证出 48 根里的 26–30 根(54–63%),其余必须刹停;n=32 起 P50 就已经吃满 20 ms,还没算推理。所以**连续段认证按现有几何口径不能同时满足"P99<20 ms"和"不牺牲到达"**。这些数是 CUDA float64 的单次调用,不能外推到浏览器 CPU float32 的 50 Hz 链路。
- **机理定位与下一步**:成本 = 单次调用固定开销 × 由 `log(bound/间隙)` 决定的层数。要同时拿到"证得出"和"证得起",必须把**间隙抬起来**(安全层要求正余量,而不是 `gap>0`)并把**每步位移压回去**(反馈叠加在 ±1 限幅之后,导致第30轮实测最大 1.66×VMAX,界也随之变大)。顺带记下一个坑:安全层现在是 `kin.gaps(allq, 0.5)`,截顶值 0.5 mm 低于任何 >0.5 mm 的余量门限,直接把余量判据变成恒假——加余量必须同时抬这个 cap。
- **回归与判据灵敏度**:`python -m unittest tests.test_eval_cl` **26 项通过**(新增 3 项均匀细分)。含解析对照:合成恒间隙段(界 1 mm/度、间隙 0.1 mm)在 n=5 必判证不出、n=6 必判安全,且 `cap_required_mm` 等于 `1/n+2eps`;薄障碍在 n=4/8/64 判碰撞而 n=2 只判证不出(不误放行);`n` 给成张量被类型门拒绝。二次加密反证在 n=8..128 全部 0 误放行。
## 轮次 33:把速度做成可观测的硬约束,再测限幅与余门的代价(exp: `exp/dagger-deploy`)
- **仪表先行**:`rollout()` 现在逐题记录**实际执行**的速度比 `max_j|Δq_j|/STEP`(STEP=VMAX·DT,所以 1.0 = VMAX)和按控制步的速度变化比;`eval_cl` 汇总 `speed_ratio_p50/p95/max`、`over_vmax_rate`、`accel_ratio_p95/max`,并把逐题值放进 `per`(极值/分位不进按 n 加权的批聚合)。
- **仪表自证**:用第30轮 after 的 trace 独立重算得 max **1.660004×VMAX**、超速度题占比 **1.0**,与新指标在同一批上逐位一致(`speed_ratio_max` 1.660003662、`over_vmax_rate` 1.0);这是"先证判据有灵敏度",不是拿新数当结论。
- **默认路径未变**:`--inc_cap 0 --gap_margin 0`(新默认)跑出的每个旧指标与第30轮 after 完全相同——success 0.57421875、无碰撞到达 0.57421875、time_ok 0.390625、碰撞 0、刹停 19.25、行程比 2.4449527263641357、ratio2s 0.20997506380081177、成功题误差 0.18899640440940857、dls_share 0.625(`docs/experiment_round33_base.json`)。整批墙钟 296.3 s 与第30轮 248.8 s 不可比:这期间我在同机跑了 CPU 分析,属自我污染,仪表开销需独占另测,不作为结论。
- **两个新开关**:`inc_cap=1` 在**反馈叠加之后**再按每轴 VMAX·DT 限幅(此前 `inc=clamp(act,±1)·astep` 之后又加 `gain×fb`,所以 1.66× 是结构性的);`gap_margin>0` 让安全层要求间隙 > 余量而非 >0,并把宽相位 cap 自动抬到 `margin+0.5`——不抬的话 `gaps(allq, 0.5)` 的截顶值 0.5 会让任何 >0.5 的余量门限恒假。`col`(真实碰撞计数)仍按 `gap≤0` 判,不随余量改变,所以两者不会互相顶替。
- **部署侧同病**(只读核查,未改 JS):`webapp/js/ik-policy.js:355` 也是 `inc[s][j] += fbGain*fbStep[j]` 在 ±1·STEP 之后叠加,所以浏览器控制器同样能超 VMAX;JS 的余量是 `o.margin=5` 的**投影式**护栏(`project()` 每轴 ≤projMax,只处理 gap<margin 的近距对),与本轮的"低于余量就刹停"不是同一机制;`ACC=5.0` 只用于刹停段长度,不是每步加速度上限(§9.2 也没给数值化的加速度门槛,所以 `accel_ratio` 只作变体间比较)。
- **间隙分布的口径坑**:第30轮 `plans['gaps']` 是在 cap=0.5 下记录的,所以"最小间隙 ≥1 mm 的题占 0"是截顶假象。可说的是:逐题最小记录间隙 p5 **0.0005**、p25 0.0064、中位 **0.046** mm,57.4% 的题曾低于 0.5 mm——即策略实际贴障碍飞,而查看器护栏是 5 mm。要把 0.5 以上那段分开,必须抬 cap 重算(本轮后续做)。
- **四组单变量对照**(同 256 题、同 seed、其余按第30轮配方):base / `inc_cap=1` / `gap_margin=0.5` / 两者同开。结果见下表。
- **四组单变量对照结果**(同 256 题、同 seed、K=8、commit 2、fb 2.2、fb_block_gain 0.5、dls_safety 1、select endpoint、replan empty、partial 1;`docs/experiment_round33_{base,cap,margin,both}.json`):
| 变体 | 到达 | 无碰撞到达 | time_ok_1p3 | 真实碰撞 | 刹停/题 | 速度比 p50 | 超 VMAX 题占比 | 2 s 误差比 |
|---|---|---|---|---|---|---|---|---|
| base | 147/256 = 0.5742 | 0.5742 | 0.3906 | 0 | 19.25 | 1.660 | 1.000 | 0.2100 |
| `--inc_cap 1` | 134/256 = 0.5234 | 0.5234 | 0.2656 | 0 | 19.55 | 1.000 | 0.000 | 0.2790 |
| `--gap_margin 0.5` | 144/256 = 0.5625 | 0.5625 | 0.3789 | 0 | 20.05 | 1.660 | 1.000 | 0.1817 |
| 两者同开 | 133/256 = 0.5195 | 0.5195 | 0.2617 | 0 | 19.97 | 1.000 | 0.000 | 0.2870 |
- **速度限幅是有代价的硬约束**:每轴增量限回 VMAX 后到达 −5.1 pp、time_ok −12.5 pp、2 s 误差比 0.210→0.279。也就是说 base 的 57.4% 里**有一部分是用超速换来的**:反馈在限幅之外叠加,实际替模型补了速度。不能把 base 当作"满足速度约束的成绩"。
- **机理**:base 速度比 p50=1.660 恰等于 `1+0.3×fb_gain 2.2`;限幅后刹停几乎不变(19.25→19.55),但 time_ok 掉得比到达更多,说明损失集中在**逼近目标末段被剥夺了超速修正**,不是更容易刹停。所以出路不是调低增益(第29/30轮已测:增益小到达就掉),而是把该由前馈承担的速度还给模型——在限幅后的分布上重采数据(DAgger,需批准),或用不违反速度约束的前馈(#14 的余量投影 + 按时限打分)。
- **0.5 mm 余量门很便宜**:只少 3 题、刹停 +0.8/题,2 s 误差比反而更好(0.182)。但这不等于裕度够用——它只说明"要求 0.5 mm 不明显拖慢"。5 mm(查看器护栏)是否同样便宜,取决于策略能否找到 ≥5 mm 的路径,必须抬 cap 重算真实间隙分布才能回答(第30轮 `plans['gaps']` 在 0.5 mm 截顶,算不出这一条)。
- 四组 `collision_rate` 都是 0,但这仍是端点/中点离散口径;`gap_margin` 不改 `col` 判据(仍 `gap≤0`),余量与碰撞计数是两条独立的线,不互相顶替。
- **回归**:`python -m unittest tests.test_eval_cl` **30 项通过**(新增 4 项):未限幅时实测速度比确实 =3.4/1.2>1(灵敏度)、限幅后恰为 1.0、0.3 mm 间隙在余量 0 时通过而余量 0.5 时刹停、5 mm 间隙在余量 0.5 时仍通过(证明 cap 抬升生效,否则该测试会恒假)。
- **GPU 上的一个控制周期(按 GPU 口径实测)**:诊断档与逐项分解见 `docs/experiment_round33_gpu_cycle.json`,真实配方(steps=1)见 `docs/experiment_round33_gpu_cycle_steps1.json`,RTX 3090、B=1、float32 推理 + float64 认证,每项 20 次预热 + 120 次计时并 `cuda.synchronize`。
- **前提更正(重要)**:`eval_cl` 从不传 `steps`,`rollout` 默认 `steps=1`,而 `ml/model/policy.py:156` 写明"steps=1 即一步生成,steps>1 是诊断用"。所以第27轮以来的所有评估成绩一直是**单步推理**;本节先用 steps=16 测出的"当前配方 116 ms ≈ 8.6 Hz"是口径错误,已作废并在下面用真实配方重测。
- **真实配方(steps=1, K=8)的分解**:
| 环节 | P50 | P95 | P99 |
|---|---|---|---|
| 推理 `sample_k` steps=1,K=1 / 4 / 8 | 7.11 / 7.08 / 7.19 | 9.45 / 10.17 / 9.95 | 11.91 / 14.00 / 11.34 |
| 特征 83 维(总 / 其中宽相位间隙 / 其中 FK) | 13.11 / 8.18 / 4.00 | 19.57 / 16.57 / 7.51 | 21.17 / 19.43 / 8.47 |
| 目标反馈 DLS 1 步 | 6.53 | 9.32 | 12.62 |
| 现有离散安全层 4 点(cap 1.0) | 7.41 | 11.34 | 13.80 |
| `certify_uniform` 2 根 n=8 / n=16 | 9.70 / 16.07 | 14.40 / 20.95 | 15.75 / 24.42 |
| 组合周期(同一位姿,顺序测;跨行比较受漂移影响,只作量级) | P50 | P95 | P99 |
|---|---|---|---|
| K=8 + 离散安全层(真实配方) | **36.68 → 27.3 Hz** | 43.27 | 49.17 |
| K=4 / K=1 + 离散安全层 | 37.71 / 40.36 | 55.17 / 48.46 | 76.53 / 52.06 |
| K=8 + 连续认证 n=8 | 45.34 → 22.1 Hz | 49.68 | 52.20 |
| K=8 + 连续认证 n=16 | 49.55 → 20.2 Hz | 56.85 | 59.06 |
| (对照)诊断档 steps=16 的同一周期 | 116.5 | 134.5 | 142.6 |
- **归因更正(先证仪表再下结论)**:空算子 P50 0.028 ms、空 `synchronize` 0.005 ms、一次 `encode` 2.27 ms,所以**不是启动/同步地板**。先前口算的"5–10 ms 是 GPU 启动开销"错了,在此更正:特征 13.1 ms、认证 7.4 ms、DLS 6.5 ms 是 B=1 下真实的小算子工作量——即 **GPU 在单臂批量=1 时利用率极低**,而不是被框架拖慢。
- **推理不是瓶颈,几何才是**:单步推理 7.2 ms,且候选数几乎免费(K 1→8 不涨);一个周期里几何类工作合计约 27 ms(特征宽相位 8.2 + 特征 FK 4.0 + 认证 7.4 + DLS 6.5),并且这几次调用各自重复做 `cum_xf` 与宽相位筛选。所以 50 Hz 的路径是**合并几何查询**(`features()` 已支持 `gm=`/`xf=` 复用,`gm_groups` 文档保证与整表 `gaps` 数值一致),而不是继续压推理。
- **连续认证的增量成本可接受**:在同一位姿上 n=8 只比现有离散层贵约 8.7 ms(整周期 36.7→45.3 ms),n=16 贵约 12.9 ms。也就是说"段内连续证明"在 GPU 上不是奢侈项,真正卡住 20 ms 的是那 ~27 ms 的重复几何。合并后再看是否进得了 20 ms——这一步正在做交错复测(跨行顺序测的数字受漂移影响,不作最终结论)。
- **50 Hz 结论(当前状态)**:真实配方一个规划周期 **36.7 ms(P99 49.2)≈ 27 Hz**,仍不满足"P99<20 ms";且必须记住这是在**不限幅**(可超速 1.66×)的策略分布下测的——限幅后的到达率代价见上表,两者不能混着谈。
- f32 与 f64 的间隙在 3 个点上完全相同(最大偏差 0.0 mm、无符号翻转),但 **3 个点不足以支持"认证可以改用 float32"**,需按真实轨迹批量对比才能下结论。
## 轮次 34:GPU 周期的可实现优化——复用 FK 与压认证最坏情况(exp: `exp/dagger-deploy`)
- **起因与一次自我撤回**:轮次 33 记下"合并几何查询看能不能进 20 ms"后,先按 `docs/experiment_round34_fused_cycle.json` 测出"整周期 41.9→24.5 ms"。这个数**作废**,两个独立原因:
1. 第一版的"合并"把 `xf=` 传给了包含 5 行的 `gaps` 调用,而 `gaps` 的 `xf` 是按行对应的 → 只有第 0 行走完宽相位,认证点 4 行全部退回截顶值 40.0 mm,即**少干了活**(表现为认证矩阵差 31.17 mm)。修好后同一 cap 下矩阵逐位一致(差 0.0 mm)。
2. 更根本:真实管线顺序是 特征 → 推理 → 候选 → 认证,**认证点依赖网络输出**,所以"特征用的 q 间隙与认证点间隙合成同一次调用"在实现上做不到。该文件保留作记录,但不能当作可实现收益。
- **只测可实现的口径**(`docs/experiment_round34_geom_opt.json`;B=1、K=8、commit 2;7 个变体逐轮交替、奇数轮反序,80 轮;等价性两项都 `True`:批量认证矩阵与逐条循环逐位一致、features 复用 xf 前后逐位一致):
| 变体 | 含义 | P50 | P95 | P99 |
|---|---|---|---|---|
| A_today | 现口径 | 33.49 | 46.14 | 64.69 |
| B_xf_reuse | `gm_groups` 复用 features 已算的 `cum_xf` | **30.12** | 42.35 | 66.37 |
| C_batch_cert | K 条候选的认证点并成 1 次 gaps | 38.05 | 50.17 | 59.86 |
| D_both | B + C | 34.87 | 39.81 | 48.26 |
| E_both_cont_n8 | B + C + 连续认证 n=8 | 43.73 | 51.08 | 65.68 |
| F_worst_loop8 | **最坏情况**:所有候选被拒(今天逐条串行 K 次调用) | 80.22 | 88.44 | 89.55 |
| G_worst_batch | 同一最坏情况但并成 1 次调用 | **37.14** | 46.06 | 52.22 |
- **复用 FK 是无条件收益**:省一次 `cum_xf`(约 3.4–3.7 ms),特征输出逐位一致,已直接实现进 `ml/common/kin_torch.py` 的 `gm_groups(..., xf=)` 与 `ml/common/features.py`(默认行为不变,新参数仅在调用方已算过 xf 时传)。
- **批量认证是"典型换尾部"**:常见路径(第一条候选就通过)贵 4.6 ms;但今天一旦前面的候选被拒就要串行调用,实测最坏 **80.2 ms(12.5 Hz)→ 37.1 ms**,P99 64.7→48.3 ms(D vs A)。硬实时看的是最坏情况,所以这条值得做,但要按"典型略变慢、尾部大幅变稳"来汇报,不能只报 P50。
- **仍未到 20 ms**:即便 D 档 P50 34.9 ms、P99 48.3 ms。连续认证挂在 D 上是 43.7 ms(E)。所以 GPU 侧下一步只能是更结构性的:认证降 float32(需先批量量化 f32/f64 判据差异,不能凭 3 个点下结论)、把宽相位做成可增量复用的核、或 `torch.compile`/CUDA Graph 把碎 kernel 合批。
- **等价性闸门(纯效率改动不许改变任何结果)**:
- 复用 FK:同一 256 题、同 seed、同配方重跑(`docs/experiment_round34_xf_reuse_check.json`),**22 个指标逐项与轮次 33 base 完全相同**(含 success 0.57421875、time_ok 0.390625、brakes 19.25、speed_ratio_max 1.660003662 等)。整批墙钟 296.3 → 318.0 s,是两次独立运行的差,**不能当作开销结论**:省下的那次 `cum_xf` 只在 B=1 交错实测里量到 −3.4 ms,批量评估里一次调用已覆盖 256 行,摊薄后不足以抵消运行间波动。
- 批量认证(`--cert_batch 1`,默认 0 不变):同一 256 题重跑 **22 项指标与 base 逐项相同**(`docs/experiment_round34_cert_batch_check.json`,success 0.57421875、time_ok 0.390625、speed_ratio_max 1.660003662 等全部一致),整批墙钟 296.3 → 306.9 s(批量口径把 8 次串行调用换成 1 次 K×256 点的大调用,均值略涨符合预期,与 B=1 交错实测方向一致)。合成夹具里也断言两条路径选到同一条候选、同一前缀长度、逐位相同的轨迹与证书。
- **测试 32→34 项**:`CertBatchEquivalenceTest` 两条(候选 0 前缀不完整 → 选候选 1;所有候选第 2 步都不安全 → 两条路径都停在第 1 步)。写夹具时踩到一个自己的坑:桩里掩码写成 `q[:, :1]` 再 `[:, None]` 变成 3 维,广播出 (4,4,49) 才报"size 16",是**测试桩错**不是库错——先证形状再改代码。
## 轮次 35:半精度、流水线复用与 float32 认证的实测(exp: `exp/dagger-deploy`,三条路两条被自己否掉)
- **按用户指定的方向先量显卡精度档**(FP32 / FP16 / BF16,`docs/experiment_round34_precision_ab.json`;B=1、K=8、steps=1,25 次预热 + 120 次计时):
| 精度 | 推理 P50 | 推理 P99 | 整周期 P50 | 整周期 P99 | 动作最大偏差 |
|---|---|---|---|---|---|
| FP32 | 7.68 ms | 10.63 | 36.51 | 46.57 | — |
| FP16 | 8.31 ms(慢 8%) | 12.03 | 36.16 | **55.60(更差)** | 2.1e−3 归一化 = **0.0050°** |
| BF16 | 8.58 ms(慢 12%) | 14.17 | — | — | 1.7e−2 = **0.0377°** |
结论:**这个网络在这个批量下不是算力受限**,半精度换不来时间,还要付动作偏差与更差的尾部 ⇒ 推理保持 FP32。(未测更大 batch,那里的结论不下。)
- **float32 能不能做认证**(`docs/experiment_round34_f32_cert.json`,48 段 × 9 网格点 × 15 组对,宽相位 cap 2 mm):f32 与 f64 间隙差 p50 **0**、p99 3.66e−5、p999 7.44e−5、**max 1.32e−4 mm**;有符号误差 max +1.32e−4(确有 1 例高估)、min −1.09e−4。同一判据下 f64/f32 都接受 24/48 段,"仅 f32 判安全"= 0、符号翻转 = 0。⇒ f32 不是不能用,但**现用 eps=1e−4 已被实测越界**,要用就得把 eps 抬到 ≥2e−4 mm;且这只量了间隙值误差,没覆盖 f32 下 FK/界自身的误差,因此暂不放宽 dtype 守卫。
- **流水线复用特征间隙——否掉**(`docs/experiment_round35_pipeline_gaps.json`):想法是"下一次规划的 q 正好是本次认证已算过的末点"。第一版给出 42.1→31.8 ms,但**等价性检查不通过**(缓存行与新算差 3.19 mm)——是我取行索引用错(`full[CN-1]` 取到第 2 条候选的第 1 步,而非第 1 条候选走完 commit 步)。修好后收益只剩 **34.8→31.7 ms(−3.1 ms)**:复用要求认证调用改用 cap=40 的宽相位(特征所需),省下的被吃回大半;且缓存行与新算**不是逐位相同**(差 6.3e−5 mm,源于不同 batch 下矩阵乘的结合序),会扰动特征输入。⇒ 不为这 3 ms 改口径,放弃。
- **剩下的量级判断**:一个周期 ≈ 推理 7.2 + 特征 13.1(宽相位 8.2 + FK 4.0)+ 末态打分 4.3 + DLS 6.5 + 认证 7.4 ≈ 33 ms,其中 **4–5 次调用各自重做同一个宽相位筛选**(67,256 个簇对)。下一步是把一次筛选的半径按位移界(`cert_lever`)扩到整个 commit 地平线,供同周期所有点共用——这才是有量级的候选,而不是再抠精度。
- **再一次自我撤回(方法论)**:为了找"合并几何调用"的收益,我写了三个变体的探测:①两次独立调用 17.62 ms、②共享一次 `cum_xf` 13.32 ms、③合成一次 cap=40 调用 9.51 ms(交错 90 轮,特征行逐位一致)。**②③ 都不成立**:探测里认证点直接取自轨迹、与特征无关,而真实循环的顺序是 特征(q) → 推理 → 候选 → 认证点(q+Δ),认证点在特征算完之前不存在 ⇒ 同一次规划内既不能共享 `cum_xf`,也不能把两次 `gaps` 合并。只有 ①→轮次 34 已落地的 `gm_groups` 复用 `xf`(同一姿态内部,−3.4 ms)是真的。教训:**效率探测必须按真实数据依赖排布,否则测的是不存在的调用图**。
- **循环内还剩的可复用点**:`replan='empty'` 且队列为空时 `qe == q`,此时目标反馈 DLS 与末态打分的 FK 和特征用的是同一姿态,可以共享一份 `cum_xf`(部署 `r=3` 时不成立,要按 r 分支)。这条待测(#19)。
- **守卫补一个**:`gaps()` / `gm_groups()` 现在拒绝行数与 `q` 不符的 `xf`(之前会静默按别的行做宽相位,实测触发 CUDA device-side assert 或给出错间隙),测试 34→35 项;`python -m ml.eval.check_parity` 仍 PASS(2000 样本,间隙差 2.52e−13 mm、碰撞布尔差 0、特征差 5.96e−08)。
## 分支清理(2026-10-20)
- `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。
- `feat/abs-target`(217a70b):绝对目标参数化负结论分支(轮次 3);打 tag `archive/abs-target` 保留可复现实体后,删除远端分支(本地此前已删)。
- `backup/pre-deep-squash`、`backup/pre-squash`:历史备份分支,非实验方案,保留未动。
- 保留:`main`(70654e4)、`exp/dagger-deploy`(39c870c,历史最佳)。
+245
View File
@@ -0,0 +1,245 @@
# 本机执行清单(给本地 AI 逐条执行)
本文件是 Hark 对本机计算的唯一要求清单。按顺序执行,每一步把「回传」里的文件或输出原样交给用户,不要跳步,不要自行改代码。
所有命令在仓库根目录(G:\Robot_v2\机器人手臂)的 Git Bash 里运行。完成的条目由 Hark 在下一次提交里删掉。
## 已知结论(截至 2026-10-19 05:35 回传)
- 现有模型 `ml/runs/base/last.pt` 闭环 0% 到达:1 段采样碰撞 99.6%;4 段采样 94%;4 段 + 仿真安全层碰撞 0.78%,到达仍 0%。consist / value / first 三种选法结果几乎一样,问题出在候选本身,不在挑哪条。
- 三份代码审查逐行核对了滚动索引、前缀、提交、标签与执行的对齐,没有找到错位;但之前没有任何测试真正走过 `rollout()`。下面第 1 步的 H 段把「仿真错了」「前缀照抄」「子目标切换规则」「多峰」几种解释逐个分开。
- 之前「前 1 s 速度比 2.31×」按 6 轴角度直接相加,被 J5/J6 抖动和 pos 任务冻结的 J6 放大。H 段改为分轴、净位移、TCP 三种口径。
## 第 0 步:同步代码
```bash
git status --short # 有未提交的改动先 commit(git add -A && git commit -m "本机改动"),不要丢
git pull --rebase gitea main
git log --oneline -1 # 应是 Hark 的「diag: H 段(老师 oracle / 前缀消融 / 子目标规则 / 分轴指标)…」
```
- 训练控制台若开着,先点「停止」,确认没有 python 训练进程在跑。
- 不要启动训练、DAgger、M5。
## 第 1 步:闭环失败定位 H 段(约 35–50 分钟)
```bash
PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json
```
- 专用显存超过 22 GB 就 Ctrl+C,回传终端最后 30 行和已写出的 `ml/runs/diag_cl.json`(每段跑完就写一次,中断也保留已完成的段)。
- 现有 checkpoint 没有 EMA、没有 `act_scale`:脚本按原始权重、动作尺度 1.0 载入(输出里 `weights`=`model`、`act_scale_ckpt`=1.0),与之前各轮同口径。
- 回传:`ml/runs/diag_cl.json`。
### 输出键
| 键 | 内容 |
|---|---|
| `A_*`、`B_act_diff_deg` | 起点特征 Python 与数据集逐组差;两份特征下的动作差(与之前相同) |
| `C` | 旧部署口径(1 段采样、子目标旧规则 timeout)的失败形态。到达与碰撞改为只计每题自己的时限内,数字可能比上一轮略低 |
| `H1_oracle` | 老师当策略走完整 `rollout()`。`oracle` 字段:`max_dev_deg_before_dls`(DLS 接管前与老师轨迹的最大关节偏差,p50 / p99 / max)、`reach`、`collision`、`finish_minus_teacher_s`(完成时刻 − 老师时长)、`dls_used` |
| `H1_oracle_noise_0.05` / `_0.1` / `_0.2` | 老师动作每次规划加 N(0, σ²)(σ 以 STEP 为单位) |
| `H2_open_loop` | r = 3 的 val 行:`s{1,4}_prefix_orig`(前缀原样,执行窗口 3..5 的误差,另含 `prefix_copy_err_deg_mean`)vs `s{1,4}_prefix_zeroed`(前缀置零) |
| `H2_replan_empty_s1` / `_s4` | 队列空了才规划(每次 r = 0),其余同新部署默认 |
| `H3_headsub_cross_s{1,4}` | 子目标头 + 越过顶点规则(新部署默认) |
| `H3_headsub_timeout_s{1,4}` | 子目标头 + 旧规则(< 5° 或 1.5 s 超时,旧部署) |
| `H3_teachersub_cross_s{1,4}` / `H3_teachersub_timeout_s{1,4}` | 老师真实途经点全表,两种切换规则 |
| `H3_nosub_s{1,4}` | 不用子目标 |
| `H4_open_loop` | 按前缀长度 r = 0..3 分组的执行窗口误差 \|Σ(act − Y)·STEP\|(度),1 段与 4 段采样 |
每个闭环变体(H1–H3)都带同一组指标:`reach`、`collision`(cat ≠ D、时限内)、`goal_ratio_2s_p50`、`end_goal_err_mm_p50`、`first_hit_s_p50`、`t_done_over_teacher_p50`、`brakes_p50`、`joint_amp_ratio`(6 轴前 1 s 关节行程 策略/老师)、`joint_net_ratio`(6 轴前 1 s 净位移 策略/老师)、`j6_pos_amp_deg_p50`、`tcp_path_ratio_p50`、`tcp_net_ratio_p50`、`sign_flip_rate` 与 `sign_flip_rate_teacher`(6 轴前 2 s 相邻 3 步块变号比例)、`by_cat_mode`(各类别 n / reach / collision / goal_ratio_2s_p50)、`D`(D 类单列)。`_s1` = 1 段采样(部署),`_s4` = 4 段采样。
### 判读
| 现象 | 结论 |
|---|---|
| `H1_oracle` 的 reach < 0.95、collision > 0.01、或 `max_dev_deg_before_dls.p99` > 0.1° | 仿真 / 滚动本身有错,之前所有闭环 0% 都不可信。先修仿真,不看下面各项 |
| `H1_oracle` 全部正常 | 仿真没问题,失败在策略输出。再对照 `H1_oracle_noise_*`:σ = 0.05 时 reach 就大幅下降,说明闭环对残差噪声极其敏感,单靠降噪不够 |
| `H2_open_loop` 的 `prefix_zeroed` 执行窗口误差明显小于 `prefix_orig`,或 `H2_replan_empty` 明显好于同段数的 `H3_headsub_cross`(reach > 0,或 `goal_ratio_2s_p50` 明显更低、`sign_flip_rate` 明显更低) | 前缀照抄(copycat)是主因 |
| `H3_teachersub_cross` 明显好于 `H3_teachersub_timeout`(或 `H3_headsub_cross` 好于 `H3_headsub_timeout`) | 子目标切换规则是主因之一 |
| `H3_teachersub_*` 明显好于 `H3_headsub_*` | 子目标头给的途经点不可用 |
| `H3_nosub` 不比 `H3_headsub_cross` 差 | 子目标通道可以整体去掉 |
| `by_cat_mode` 里 `A/pos`(老师就是一条直线)reach 也是 0 | 失败与多峰无关 |
| `joint_amp_ratio` 远大于 `joint_net_ratio`,且 `sign_flip_rate` 远高于 `sign_flip_rate_teacher` | 「动得多、走得少」是来回抖,不是速度律学快了 |
重新打包和重训(`FRESH_FROM=prep`)要等 Hark 看完这一步的结果再定,这一步之后不要自行启动。
## 回传汇总
| 步骤 | 回传内容 |
|---|---|
| 1 | ml/runs/diag_cl.json |
| 任一步出错 | 终端最后 30 行输出 |
---
## 执行回传(2026-10-19 19:55,本地 AI)
### 第 0 步:同步代码
`ml/eval/diag_collision.py` 归档提交(本机改动 `7e063eb`);根目录用户个人 html 为个人文件未入仓,原位保留。log 顶部 = `406f180`「diag: H 段 …」✓。无训练进程。
### 第 1 步:H 段(完整 JSON 已更新到 ml/runs/diag_cl.json;weights=model、act_scale_ckpt=1.0,同口径;H_tasks 256、missing_teacher 0)
**判读表逐行核对:**
1. **`H1_oracle`:仿真完全正常。** reach **1.0**(243/243)、collision **0.0**、`max_dev_deg_before_dls` p50 6.9e-5° / p99 4.6e-4°(阈值 0.1°)、`finish−teacher` p50 −0.02 s、`dls_used` 94.9%。→ **滚动/仿真没有错,之前所有闭环 0% 都可信,失败在策略输出本身。**
2. **`H1_oracle_noise`:对残差噪声极其敏感。** σ=0.05 时 reach 1.0→**0.218**(碰撞 9.1%);σ=0.1→0.058;σ=0.2→0.012。→ 单靠降采样噪声不够(判读表第二行命中)。
3. **`H2_open_loop`:前缀照抄排除。** prefix_orig vs zeroed:s1 2.315° vs **2.375**(置零不更好);s4 0.251° vs **0.705**(置零明显更差)。`H4` 按 r=0..3 分组误差持平(2.31–2.33°)。`H2_replan_empty`:s1 0%/79.4%/0.836、s4 0%/84.8%/0.603——不比同段数 `H3_headsub_cross` 好。
4. **`H3`:子目标三个变体逐位相同(新异常)。** `headsub_cross` / `headsub_timeout` / `nosub` 在 s1、s4 下从 reach 到 end_err 到 flip 数组**完全一致**(s1: 0% / 96.3% / 0.7504 / 414.2091;s4: 0% / 79.8% / 0.5869 / 428.0032)。`teachersub_*`(真值途经点)仅微动(s1 95.9%/0.726、s4 79.4%/0.578)。→ **子目标头通道在这条闭环里零响应**——比「可以整体去掉」更强;可能是 need 概率全程 <0.5 从不触发,或子目标输入对旧模型输出无影响,建议核一下(D0 曾测过 need 标签占比 28.1%)。
5. **`by_cat_mode`:A/pos(老师直线)reach 也是 0**(s1 碰撞 94%)→ **失败与多峰无关**。
6. **分轴指标:s1 的"动得多"含抖、s4 后基本消抖。** J5/J6 幅度/净位移:s1 3.08/2.57、2.55/2.16;s4 1.83/1.84、0.69/0.69,`sign_flip_rate` 从 0.026–0.062 降到 0.0006–0.0013(≈老师 0.0005–0.004 水平)。→ 4 段采样把来回抖消掉了,但净位移仍 J5 1.8×,且到达仍 0%。
**C 段**(新时限口径):0% 到达 / 96.5% 碰撞 / 2 s 误差比 0.755。
### 本轮无出错,无终端报错段。
---
## 本地自主排查(2026-10-19 20:40,用户授权「继续排查」)
### 1. 闭环噪声容忍阈值(老师动作 + N(0,σ²),σ 以 STEP 为单位,n=256,其余同 H1_oracle)
| σ | 0 | 0.005 | 0.01 | 0.02 | 0.03 | 0.04 | 0.05 |
|---|---|---|---|---|---|---|---|
| reach | 1.0 | 1.0 | 0.971 | 0.716 | 0.457 | 0.309 | 0.218 |
| collision | 0 | 0.8% | 2.5% | 4.9% | 6.2% | 7.4% | 9.1% |
| ratio2s | 0.047 | 0.049 | 0.049 | 0.054 | 0.059 | 0.063 | — |
→ **闭环能容忍的每步噪声上限 ≈ 0.01–0.02×STEP**(约 0.02–0.04°/步)。策略 s1 残差 ~0.77°/步、s4 ~0.05–0.08°/步,都在崩溃区右侧。
### 2. 8 段采样闭环(steps=8,同 H3_headsub_cross 口径)
reach **0%** / collision **79.0%** / ratio2s 0.574 / end 408 mm;`sign_flip_rate` 0.0004–0.0015(≈老师 0.0005–0.004,抖已完全消掉)、`amp ≈ net`(1.00/1.19/0.97/1.19/1.36/0.76,无抖动成分)。
→ 对比 s4(reach 0 / 79.8% / 0.587):**s4→s8 几乎无改善**。按上表的白噪声口径,s8 的残差幅度(16 步 0.56° ≈ 0.035°/步,σ_eff≈0.0175)应给出 reach≈0.75——实测 0。**策略误差不是白噪声,是有方向的系统偏差**:幅度不大但同向、闭环里逐步累积,重规划抵消不掉。降采样噪声(1→4→8 段)已到天花板。
### 3. 子目标通道:need 头恒不触发(新发现,修正「子目标排除」的理由)
- 冷启动(子目标特征抹零,与闭环同口径)need 概率:mean 0.0048、p50 0.0030、p90 0.0079、**max 0.0819**、>0.5 占比 **0**;43 道 need=True 题召回 **0/43**。(D0 的 0.71875 准确率恰等于"全预测 False"基线,当时已有此暗示。)
- 因此闭环里 `sub_flag` 恒 0、子目标从不激活——这才是 H3 三变体逐位相同的直接原因。
- 但模型对子目标特征**高度敏感**(同一噪声下真值子目标 vs 置零:|Δact| mean 0.177、max 3.09)——训练时 80% 样本 sf=1,而闭环 100% 是 sf=0,属训练/闭环分布错配。
- 不过 H3_teachersub(强制真值 need + 真值途经点)仍 0% / 95.9%——**子目标不是闭环全灭的主因**,但 need 头失效 + 闭环恒走少数分支值得修。
### 4. 汇总判断
- 仿真正常(H1_oracle 已证);采样噪声已压到老师抖动水平仍 0% 到达 → **剩余障碍是策略的系统性动作偏差 + 闭环对误差的极低容忍度(误差复利)**。
- 继续调采样段数、子目标规则、候选选择都无解;正确方向是**在闭环状态上训练(DAgger)**,让老师纠正分布偏移,或提高闭环鲁棒性。
- 未自行启动 DAgger / 重训,等指示。
### 5. 补充:系统偏差的方向结构(4096 行开环 + 256 题闭环,4 段采样)
**开环动作误差(deg,有符号)**:
- 每轴均值 [0.010, −0.012, −0.001, −0.029, 0.034, 0.018],|mean|/std ≤ 0.17 → **无固定轴向偏置**(不是某轴恒偏)。
- 每轴标准差 0.16–0.21°;每步平均 |误差| 0.016–0.018°,**16 步内不随步数增长**。
- **lag-1 自相关 0.931–0.967(极高)** → 误差是**慢变/连贯**的,不是白噪声。这解释了为什么白噪声 σ 扫描会低估策略误差的危害:闭环重规划抵消不掉持续同向的偏差。
- 误差与老师动作方向:cos mean 0.046 / p50 0.072(不超调也不欠调);投影比 e·Y/|Y|² mean −0.038 / p50 0.006(无系统性缩放)。
**闭环误差增长(s4,n=256,done=0)**,goal_err / 起点误差:
| t (s) | 0.25 | 0.50 | 0.75 | 1.00 | 1.50 | 2.00 |
|---|---|---|---|---|---|---|
| p50 | 0.959 | 0.880 | 0.811 | 0.741 | 0.645 | 0.591 |
| p90 | 1.024 | 1.072 | 1.119 | 1.162 | 1.222 | **1.318** |
→ p50 缓慢靠近(2 s 缩 41%),**p90 持续发散**——≥10% 的题误差随时间增长(误差复利进碰撞轨道),其余也远不够快。
**补充结论**:策略每步误差很小(~0.017°)但**时间相关性极强(0.93–0.97)**,闭环对"慢变误差"的敏感度远超等幅白噪声;失败是「慢变误差 × 闭环误差复利」。推理侧降噪(段数/选择器)已无空间,训练侧需要降低慢变误差(DAgger / 闭环状态训练)。
### 6. 「2 s 误差比 >1 的发散题」能否提前筛掉(per-task 分组,s4,n=256;明细 ml/runs/task_divergence.json)
| 组(2 s 误差比) | 题数 | 碰撞 | 到达 | cat/mode | 起点误差 p50 | 老师时长 p50 | need 率 | 老师到达 |
|---|---|---|---|---|---|---|---|---|
| 发散 r2>1.0 | 48 | 81% | 0% | A/pos 14、A/pose 14、C/pose 5、C/pos 5、B/pos 3、B/pose 3、D/pos 2、Dr/pos 2 | 374 mm | 1.80 s | 21% | 96% |
| 平 0.7–1.0 | 48 | 83% | 0% | 混合 | 539 mm | 2.12 s | 27% | 90% |
| 收敛 ≤0.7 | 160 | 79% | 0% | 混合 | 642 mm | 2.18 s | 31% | 96% |
- **三组碰撞率相同(79–83%)、到达全 0**——不存在"一小撮可提前去掉的坏题";发散题散布在所有类别,老师都能到达。
- r2 与起点误差秩相关 **−0.384**(起点误差小 → 比值易大),是**分母效应**:短任务(老师 1.8 s、e0 374 mm)比值波动大,不代表题目特殊;r2 与老师时长/行程几乎无关(−0.09 / −0.03)。
- 结论:误差放大是**全体题目上的普遍行为**,筛题无效(只让指标好看),必须训练侧修正。
### 7. DAgger 小样本试跑(用户授权;未跑全量、未微调)
**链路实测**(ckpt `ml/runs/base/last.pt`,旧口径 act_scale=1.0):
| 步骤 | 规模 | 实测 |
|---|---|---|
| `ml.dagger.collect`(16 题,parts=2) | 16 题 | **12.7 s**(含加载),29 个起点(全撞,拒 1 个 gap) |
| `ml.dagger.collect --n 64`(显存采样) | 64 题 | 成功,112 个起点(拒 10 gap),**显存峰值 7.98 GB**(24 GB 卡,无 OOM) |
| `node ml/gen/gen_dataset.cjs --tasks tasks_01.jsonl` | 15 个起点 | **7.6 s**,shard 含 qPrev / v0 / traj / cert |
| `ACT_SCALE=1.0 node ml/prep/build_dataset.cjs …` | 13 题 | **2.3 s**,453 样本(175 增广),meta `act_scale:1, data_ver:2` |
**关键口径提醒**:`build_dataset.cjs` 现在默认 **ACT_SCALE=1.25**(DATA_VER 2),而现有主数据 `ml/data/samples` 无 meta → 训练按 **1.0** 读。两者不一致时训练直接报错(README 第 120 行)。全量 DAgger 前二选一:
1. 打包 DAgger 时显式 `ACT_SCALE=1.0`(与旧主数据、旧 ckpt 兼容);
2. 先按新流程 `FRESH_FROM=prep` 重打主数据为 1.25、重训后再跑 DAgger。
本次试跑全部用 1.0;产物在 `ml/data/dag_try*/`(gitignored)。
**全量耗时外推(估算,以首次实测为准)**:collect 8000 题约 0.8–1 小时(单进程);老师重规划 1.6 万起点 / NPROC≈12 约 10 分钟;打包约 3 分钟;微调 15000 步未试。
**结论**:DAgger 收集链路可用、显存安全(峰值 8 GB),可以全量启动——但先定 act_scale 口径。
### 8. 「加大数据能否力挽狂澜」——闭环状态到训练分布的距离(s4,n=256,9408 个闭环状态)
方法:闭环轨迹每 10 步取一个 q(9408 点),对训练集 191 万老师状态求最近邻(6 维关节角 L∞,±360 回绕);对照 = 每题起点(老师分布内)。
| 状态 | p10 | p50 | p90 | p99 |
|---|---|---|---|---|
| 闭环轨迹状态 | 12.10° | **21.46°** | 32.50° | 44.56° |
| 每题起点(老师) | 8.71° | 13.21° | 17.91° | 21.53° |
- 闭环状态离最近老师状态中位 **21.5°**(起点 13.2° 的 1.6×;p90 32.5°、p99 44.6°)。
- 训练集本身在关节空间稀疏:连老师起点最近邻都有 13.2°——数据是"轨迹线"而非密集体。
- 按密度缩放(6 维 d∝N^(−1/6)):数据翻 4 倍最近邻仅降 ~21%(21.5°→~17°),而策略误差尺度是 0.02°;差距三个数量级,且同类数据不改变误差的时间相关结构(0.94)。
- **结论:扩大同类老师数据救不了闭环**;需要「不同的数据」——在策略实际到达的状态上标注(DAgger),把 21.5° 这些分布外点变成训练样本。
### 9. 方案 A 实测:合成扰动状态样本微调(两轮配方)
做法:从训练任务老师轨迹的 15–85% 处取状态,加 N(0,σ²) 关节扰动(σ 混合 0.5/1/2/4°;实测起点偏移 p50 1.5°),老师重规划(**2000/2000 到达**、cert 1951/2000),打包 36k 样本(act_scale=1.0,为主数据的 1.9%),从 `base/last.pt` 微调 3000 步,2000 步时以 EMA 权重做 512 题闭环评估。
| 配方 | success | collision | goal_ratio_2s | ol_fk_mm | ol_best_mm | ol_div |
|---|---|---|---|---|---|---|
| base/last.pt(基线) | 0 | 0.996 | 0.755 | 5.98 | 2.92 | 0.655 |
| +扰动样本 lr 3e-4 | 0 | **0.484** | 0.927 | **15.27** | 10.32 | 0.966 |
| +扰动样本 lr 1e-4 | 0 | 0.604 | 0.893 | **16.42** | 11.45 | 0.980 |
- 碰撞率首次大幅下降(99.6%→48–60%),但 **success 仍 0**;开环精度退化 2.5–2.7 倍(6→15/16 mm)、多样性升高——小步微调在两种分布间折中,把主分布带偏了。
- **结论**:1.5° 量级的合成扰动 + 3000 步微调不能解决闭环。若继续这条线,需从**预训练开始**按课程混入扰动样本(并监控开环不退化),成本高;或转向结构改动(方案 B:绝对目标参数化/参考轨迹跟踪)或真实 DAgger。
- 产物:`ml/data/samples_perturb/`、`ml/runs/base_perturb/`、`ml/runs/base_perturb2/`(均在 gitignored 目录)。
### 10. 方案 B 判别实验:模型预测的"累计目标"是否指向参考轨迹
方法:2000 个扰动起点(原轨迹状态 + σ 0.5–4° 扰动,L∞ 实测 p50 **3.19°**),模型预测 16 步增量并累积为绝对目标 `Z_model = q_off + cumsum(â·STEP)`,对比原老师轨迹未来 16 步绝对位置 `Z_ref`;`ρ = |Z_model − Z_ref| / |q_off − q_ref|`(ρ≈1 = 目标随状态平移、无纠偏;ρ≈0 = 目标稳定指向轨迹)。
| 模型 | 状态偏移 p50 | 目标误差 p50 | ρ p50 |
|---|---|---|---|
| base/last.pt | 3.19° | 4.86° | **1.54** |
| base_perturb(扰动微调 lr 3e-4) | 3.19° | 5.85° | 1.90 |
| base_perturb2(lr 1e-4) | 3.19° | 6.25° | 1.97 |
| 对照:未扰动行开环 | — | **0.46°** | — |
- 未扰动时 16 步累计目标误差仅 0.46°;状态偏移 3.19° 后目标偏离 4.86°(ρ=1.54)——**当前模型是"从当前状态外推增量",不是"指向参考位置"**。扰动微调让 ρ 更差(1.9–2.0),与开环退化一致。
- **结论:方案 B 不能靠改执行层实现,必须改训练标签**——把标签从"16 步增量"改为"16 步绝对目标关节角(参考轨迹位置)",让模型学"目标在哪"而不是"从这怎么走"。执行层每步朝目标走,偏移状态下即天然纠偏。
- 实施成本:改 `build_dataset.cjs` 标签口径(Y = 绝对目标),同步 FK/平滑/价值等基于增量的辅助损失、rollout 与 `ik-policy.js` 的增量假设,然后从头训练(约 1 小时)。属结构性改动,未擅自开始。
### 11. 方案 B 初版实测(分支 `feat/abs-target`,保存点 tag `pre-b-abs-target`)
**改动**(全部在分支,main 未动):
- `build_dataset.cjs`:`ABS_TARGET=1` → Y = 16 步绝对目标关节角(/180 归一化),prefix 仍存增量(度),meta 记录 `abs_target`、签名加 `a1`。
- `train.py`:读 `abs_target`;每单位动作=180°;FK 损失用绝对位置;平滑项换算到增量量纲;开环指标用第 16 步绝对位置;ckpt 记录 `abs_target`。
- `closed_loop.py`:模型输出绝对目标 → 队列存目标序列、执行 `q=clamp(target)`、前缀由目标差分、安全层中点与 DLS 增量转绝对目标。
- `ckpt.py` / `data.py`:abs_target 透传与一致性检查。
- **过程 bug(已修)**:前缀噪声增广把 eps 直接加到绝对目标标签上(≈18° 污染,一半样本);改为累计偏移后第 r 步扣回。
**数据**:84 shard 重打 → 201 万样本、abs_target=1;标签连续(步间差 p50 0.30°、p99 1.8°、无 ±180 跳变)。
**训练结果**(从零 9950 步;p_neq 预热 5000 步):
| step | ol_fk_mm | ol_best_mm | 闭环 success | 碰撞 |
|---|---|---|---|---|
| 4000 | 74.6 | 33.4 | 0 | 100% |
| 6000 | 140.4 | 70.3 | 0 | 100% |
| 8000 | 139.0 | 68.5 | 0 | 100% |
- 一步采样动作误差 **31°**(4/8 段采样 2.3°/2.8°)——**绝对目标的一步流匹配映射没学会**;闭环 1 段 0%/100%、4 段 0%/100%(ratio2s 1.045)。
- 过预热后 5000 步无下降趋势。可能原因:一步样本仅 `p_edge=10%`、`ada_p=1` 逆误差加权压制难样本、FK/平滑辅助项在绝对位置空间主导。
- **结论:此配方不成立**。B 若继续,需改训练配方(p_edge↑、w_fk↓、ada_p=0)或换输出头(确定性回归);当前证据不支持继续投入。分支代码保留可复现。
+34 -8
View File
@@ -24,7 +24,8 @@
| 3 老师门禁 | `bash ml/pipeline.sh gate1` | 25–30 分钟(单核) | `ml/data/consistency.json` |
| 4 生成数据 | `bash ml/pipeline.sh gen` | 不共识约 1 小时;共识约 4–5 小时 | `ml/data/shard_*.jsonl`、`stats.json` |
| 5 打包样本 | `bash ml/pipeline.sh prep` | 15–30 分钟 | `ml/data/samples/`、`ml/data/r2.json` |
| 6 预训练 | `bash ml/pipeline.sh train` | 测速 2000 步约 6 分钟,正式约 3 小时 | `ml/runs/base/best.pt` |
| 6 预训练 | `bash ml/pipeline.sh train` | 测速 2000 步约 6 分钟,正式 24000 步约 1.5–2.5 小时(未实测:fp32 MF 分支后的吞吐以测速为准) | `ml/runs/base/best.pt` |
| 6′ DAgger 纠偏 | `bash ml/pipeline.sh dagger` | 未实测,估 3–4 小时:2 轮 ×(采起点 5 分钟 + 老师重规划约 30 分钟 + 打包 + 微调 15000 步) | `ml/runs/base_dagger.ckpt`、`ml/runs/base_dag2/best.pt` |
| 7 M5 门禁 | `bash ml/pipeline.sh m5` | 估算 10–20 分钟 | `ml/runs/m5_gate_base.json` |
| 8 APG 微调 | `bash ml/pipeline.sh apg` | 未实测:展开长度 60–150 次推理随批变化,以监控页前 50 步换算的 ETA 为准 | `ml/runs/apg/best.pt` |
| 9 挖错循环 | `bash ml/pipeline.sh mine` | 未实测:每轮 = 5 万题作答 + APG,以第 1 轮实际耗时 × 轮数为准 | `ml/runs/current.pt` |
@@ -114,7 +115,9 @@ bash ml/pipeline.sh gen # 默认 5 万条、进程数 = CPU
bash ml/pipeline.sh prep
```
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算),30% 样本加 σ = 1° 噪声增广(加噪后间隙 < 3 mm 的不用)。
每条轨迹按 50 Hz 时间律重放,每 3 步取一个样本(特征与部署同一份 JS 代码计算)。40% 样本对 q 加 σ = 3° 噪声增广,标签改为 12 步内线性收回老师轨迹(加噪后间隙 < 3 mm 或收回途中有碰撞的不用)。
- **动作标签**:Y = 每步增量 / (act_scale · STEP),夹在 ±1,`act_scale` = 1.25(`ACT_SCALE`)。老师巡航时主导轴每步正好 1 个 STEP,标签最大 0.8,执行端的 ±1 只防越界;被 ±1 截掉的收回量在下一步补上。运行时增量 = clamp(act, ±1) · act_scale · STEP,`act_scale` 写在 checkpoint 和 ONNX meta 里,仿真、APG、浏览器运行时都按它反归一化;没有这个字段的旧模型按 1.0 处理。
- 每个 shard 打包产物带 `<shard>.meta.json`(数据版本 `data_ver`=2、`act_scale`、增广参数、签名 `sig`)。`ACT_SCALE`、`AUG_P`、`AUG_SIGMA`、`AUG_BACK`、`MIN_GAP_MM` 任一变化,签名就变,`run_prep.sh` 只重打签名不符的 shard;没有 meta 的旧打包一律重打。训练读 meta 取 `act_scale`,同一次训练的各目录(主数据 + DAgger)必须一致,否则报错。
认证最小间隙 < 3 mm 的老师轨迹不进训练集(`MIN_GAP_MM=3`,与 `stats.json` 的 `gapDrop` 同一口径):模型只学离障碍 ≥ 3 mm 的走法,运行时投影护栏仍保持 5 mm。
检查 `ml/data/samples/index.json`:`train_samples` 约 170 万、`val_samples` 约 9 万(5% 任务划为 val)。
`MIN_GAP_MM` 必须与生成阶段相同(`stats.json` 的 `minGapFilterMm`),不一致时这一步直接报错(返回码 8)。
@@ -127,11 +130,32 @@ bash ml/pipeline.sh prep
bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/bench_base/、ml/runs/base/
```
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练到 60000 步或闭环到达率连续 5 次不升为止。
开训前先跑老师轨迹回放自检(`ml/runs/replay_teacher.json`,约 1–3 分钟),两部分都过才开训:
- 运动学口径:老师 50 Hz 轨迹直接放进 PyTorch 运动学 / 碰撞 / 到达判据,到达 ≥ 95%、碰撞 ≤ 1%;
- 滚动口径:老师当策略(`TeacherOracle`)走完整的闭环仿真 `rollout()`(队列、前缀、提交 3 步、标签 clamp、DLS、时限都与策略同一条路径),时限内到达 ≥ 95%、碰撞 ≤ 1%、DLS 接管前与老师轨迹的最大关节偏差 p99 ≤ 0.1°。
流程读 json 的 `pass` 字段,并核对 `fp`(当前 `kin.json`)与 `data_fp`(数据 shard 记录的碰撞模型指纹);任一不符就重跑一次回放,仍不过 rc 5 停。
- **通过线(R10):`samples_per_s` ≥ 5000,即 60000 步 ≤ 3.5 小时**。参考:3090、batch 1024 实测 6.2 k 样本/s ≈ 2.8 小时。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48,仍不够再把 `steps` 降到 40000
- 训练中每 2000 步打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`time_ok_1p3`、`start_fail`。时限 = 老师时长 × 2(至少 3 s)
- 显存预算按 **19–20 GB** 算(Windows + 4K 桌面常驻占 4–5 GB),训练与 APG 进程启动时自设硬帽 `VRAM_FRAC=0.78`(≈18.7 GiB),日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。
闭环指标长期 0% 而开环正常时,跑 `PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256`(约 35–50 分钟)→ `ml/runs/diag_cl.json`:A 起点特征 Python 与 JS 逐组差、B 两份特征下动作差、C 旧部署口径的失败形态、H 老师 oracle / 前缀消融 / 子目标规则 / 分轴与分类别指标(各键含义见 `ml/eval/diag_cl.py` 文件头与 `docs/本机执行清单.md`)。
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练 24000 步(`configs/base.json` 的 `steps`,余弦学习率按这个预算走完)。
- **通过线(R10):`samples_per_s` ≥ 5000**(24000 步 × batch 1024 ≈ 1.4 小时)。MeanFlow 的 JVP 双前向使训练受算力限制,加大 batch 不会提高样本/s;低于通过线时把 `configs/base.json` 的 `fk_caps` 从 96 降到 48
- **EMA**:训练同时维护权重的指数滑动平均(`ema` 0.999),checkpoint 里存 `model`(原始权重)与 `ema`。评估、DAgger、APG 起点、M5、挖错、验收、导出一律用 EMA 权重(没有 `ema` 的旧 checkpoint 用 `model`)
- 训练中每 2000 步用 EMA 权重打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`goal_ratio_2s`(2 s 时目标误差 / 起点误差的中位数)、`time_ok_1p3`、`start_fail`,时限 = 老师时长 × 2(至少 3 s),碰撞与到达只计时限内;开环三项(val 样本、老师状态上):`ol_fk_mm` 候选 0 的 16 步末端误差、`ol_best_mm` 8 个候选里最好的一条、`ol_div` 候选多样性。
- **选模**:`score` = success + 0.05·(1 − collision_rate) + 0.05·(1 − min(1, goal_ratio_2s)) − 1e-4·min(ol_best_mm, 500),到达率全 0 时闭环指标仍能排序。p_neq 预热期内和预热后第一次评估之前不存 best.pt(一步映射还没训过);早停只在 max(预热 + 2 次评估, 总步数一半) 之后计数,连续 5 次不升即停
- 损失:FK / 平滑只加在 MF 分支 r = t 且 t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上;时间嵌入最高角频率 50;MF 分支与 JVP 走 fp32(TF32 矩阵乘);前 5000 步 p_neq = 0,之后 p_neq = 0.5,另有 10% 样本固定在部署用的 (r = 0, t = 1)(`p_edge`)
- 子目标头只用把 sub 特征置零的输入训练(sub 存在时子目标标签与输入是同一个量,原样抄就能答对);运行时子目标头只在冷启动、sub 为空时使用,口径一致。价值头用完整输入
- 条件输入增广(训练时现做,`aug_pre_drop` 0.15 / `aug_pre_sigma` 0.1 / `aug_dq_sigma` 0.3):前缀随机清空(变成 r = 0 样本,标签不变);前缀加噪声,标签前 r 步同步加、第 r 步扣回;速度特征 dq 加噪声。目的是让模型不能靠照抄上一块和上一帧速度出动作
## 6′. DAgger 纠偏
```bash
bash ml/pipeline.sh dagger
```
起点模型:预训练 best.pt 的闭环 success > 0 时用它,否则用 last.pt(`python -m ml.common.ckpt pick ml/runs/<头>`),都取 EMA 权重;之后每轮按同一规则取上一轮的结果。微调时只有起点已走过自己的 p_neq 预热才不再预热。
用起点模型在 train 任务上闭环跑(关安全层),每题取「首次碰撞前 0.2 s」和一个随机时刻的状态做起点(须无碰、间隙 > `MIN_GAP_MM`、离目标 > 5 mm),同时记下该时刻的上一帧。老师从这些起点重新规划(`gen_dataset.cjs --tasks`,与主数据同认证),时间律从当前速度在首段方向上的分量起步(`v0`),t = 0 样本的速度特征用真实上一帧;打包后与主数据一起微调。微调的 val 只取主数据,DAgger 样本全部算 train。默认 2 轮(`DAG_ROUNDS`),每轮 8000 题(`DAG_N`)、微调 15000 步(`DAG_STEPS`,lr 3e-4)。老师只用于训练标注,部署仍无老师。M5 门禁读 `ml/runs/<头>_dagger.ckpt` 指向的模型;APG / 挖错的 BC 项也用 DAgger 样本。可断点续跑(采集、老师 shard、打包、微调各有完成标记)。
- 显存预算**浮动**:整卡留 1 GB 给系统(`VRAM_RESERVE_GIB`,默认 1),训练与 APG 进程启动时按"整卡 − nvidia-smi 已用 − 余量"和"CUDA 空闲 − 余量"两者取小自设本进程上限(`ml/common/vram.py`)。占显存的应用多,上限就自动变小,不用改配置;想按定值对拍给 `VRAM_FRAC`(旧口径 0.78)。日志 `vram` 字段记峰值;预训练 `batch=1024` 实测峰值 2.76 GiB、APG `batch=48` 13.08 GiB。显存不够把 `batch` 改成 512。中断后再跑 `bash ml/pipeline.sh train`(或 `all`)会自动从 `ml/runs/<头>/last.pt` 续训(每 10 分钟存一次),测速那 2000 步做过就跳过。训练发散时不会用发散的权重覆盖 `last.pt` / `best.pt`。
## 7. M5 门禁:选定动作头
@@ -177,7 +201,7 @@ bash ml/pipeline.sh mine # 默认 6 轮;ROUNDS=3 APG_STEPS=1000 MIN
## 10. 导出
```bash
bash ml/pipeline.sh export # fp32 ONNX + meta.json → webapp/models/(约 20 MB)
bash ml/pipeline.sh export # fp32 ONNX + meta.json(含 act_scale)→ webapp/models/(约 20 MB),导出 EMA 权重
```
默认 fp32:部署走 wasm 与 Node CPU,fp16 在 CPU 后端上不保证更快。只有第 13 步用 `&ep=webgpu` 实测 `__POLICY.latency()` 明显受益时,才另导出 fp16 做对比:
@@ -220,6 +244,7 @@ python ml/serve.py # 带跨源隔离头的本地服务,wasm
- 控制台 `__POLICY.latency()` 查看推理、前瞻、安全层、整次规划的 P50/P99 与节拍;`__POLICY.stats()` 查看刹停、换向、精修次数
- `&autotest=30` 自动跑与 bench 同一批贴柱题,结果在 `__POLICY.autotest`
- 运行时投影护栏 5 mm:候选动作离障碍 < 5 mm 时沿间隙梯度外推(训练数据的过滤线是 3 mm,护栏比数据更保守)
- 动作尺度读模型 meta 的 `act_scale`;子目标按「越过顶点」切换(与训练标签同口径:沿拐角平分面越过当前途经点即换下一个,没有超时),与仿真 `closed_loop.sub_advance` 同一规则
- 工具偏移 ≠ 0 时自动交回原规划器
- 离线环境:`bash ml/prep/vendor_ort.sh` 把 onnxruntime-web 放到 `webapp/vendor/ort/`,地址加 `&ortLocal=1`(WebGPU 同样可用)
@@ -300,7 +325,8 @@ ml/
sim/closed_loop.py 部署节奏闭环仿真(验证 / 挖错 / 门禁)
apg/apg_finetune.py APG 可微微调
mining/ tasks.py(无老师出题)、mine_round.py、gate.py(含 R7)、loop.sh
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、r4_probe.py(R4 无头浏览器探针)、acceptance.py(验收汇总)
eval/ dump_parity.cjs + check_parity.py(JS↔PyTorch 对拍)、check_r2.py(R2)、r4_probe.py(R4 无头浏览器探针)、acceptance.py(验收汇总)、
replay_teacher.py(开训前回放自检,含老师 oracle 走 rollout)、diag_cl.py(闭环失败定位 A/B/C/H)
toy/toy_bimodal.py 2D 双模态对照实验(动作头选型依据)
export_onnx.py serve.py
configs/ base.json(MeanFlow)、wta.json(回退)、small.json(R4 不过时的 d=128)、apg.json
+31 -22
View File
@@ -6,7 +6,10 @@
2026-10-18:展开长度不再定长 60 次推理(3.6 s,会截掉老师时长 > 3.6 s 的绕远题),改为覆盖本批 exec_s p90×1.2,上限 plans_max;
"到达"仍是每 seg_plans 段末的距离项(无到达即停),属有意设计:段末项给出持续的接近梯度。
margin_train 3 mm:与训练数据过滤线一致(认证最小间隙 < 3 mm 的老师轨迹不进训练集),碰撞项不与 BC 标签对抗;
运行时投影护栏独立保持 5 mm。端点与中点两个铰链取均值,与只查端点同量纲,w_col 无需重标。"""
运行时投影护栏独立保持 5 mm。端点与中点两个铰链取均值,与只查端点同量纲,w_col 无需重标。
2026-10-19:起点取 EMA 权重(ckpt.load_policy);动作反归一化乘 act_scale(与数据同一尺度,不一致报错),产物存 act_scale;
子目标推进改用 closed_loop.sub_advance(默认「越过顶点」,与标签同口径);子目标头 BC 项用 sub 特征置零的输入(同 train.py);
val 只取主数据目录(第一个 --samples)。"""
import argparse, json, math, os, random, sys, time
import torch
@@ -15,7 +18,7 @@ from ml.common.features import features, STEP, H, wrap180
from ml.data import Samples, load_tasks
from ml.model.policy import build, wta_loss
from ml.model.meanflow import meanflow_loss
from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err
from ml.sim.closed_loop import evaluate, tasks_to_tensors, goal_err, sub_advance
from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
from ml.common import vram
@@ -41,18 +44,23 @@ def main():
dev = a.device; torch.manual_seed(a.seed); random.seed(a.seed)
vram.limit()
os.makedirs(a.out, exist_ok=True)
ck = torch.load(a.init, map_location=dev)
mcfg = ck['cfg']
model = build(mcfg).to(dev); model.load_state_dict(ck['model'])
from ml.common.ckpt import load_policy
model, ck = load_policy(a.init, dev); mcfg = ck['cfg']
kin = Kin(device=dev)
data = Samples(a.samples, device=dev, split='train')
data = Samples(a.samples, device=dev, split='train', val_dirs=a.samples[:1])
act_scale = float(ck.get('act_scale', 1.0))
if abs(act_scale - data.act_scale) > 1e-9:
raise SystemExit(f'起点 act_scale={act_scale} 与样本 act_scale={data.act_scale} 不一致(BC 项标签尺度不同),换同一版数据训练的模型')
model.act_scale = act_scale
all_tasks = load_tasks(a.samples)
train_tasks = [t for t in all_tasks if t['split'] == 'train'] + load_pool(a.pool)
val_tasks = [t for t in all_tasks if t['split'] == 'val'][:C['eval_tasks']]
main_tid = {t['tid'] for t in load_tasks(a.samples[:1]) if t['split'] == 'val'}
train_tasks = [t for t in all_tasks if t['tid'] not in main_tid] + load_pool(a.pool)
val_tasks = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'val'][:C['eval_tasks']]
w = [C['cat_weights'].get(t.get('cat', 'A'), 1.0) * (2.0 if t.get('hard') else 1.0) for t in train_tasks]
opt = torch.optim.AdamW(model.parameters(), lr=C['lr'], betas=(0.9, 0.95), weight_decay=0.0)
amp = dev.startswith('cuda') and C.get('amp') == 'bf16'
step = torch.tensor(STEP, device=dev)
astep = step * act_scale # 动作 → 度
it0 = 0
if a.resume and os.path.exists(a.resume):
rk = torch.load(a.resume, map_location=dev); model.load_state_dict(rk['model'])
@@ -68,11 +76,11 @@ def main():
base, _ = evaluate(model.eval(), kin, val_tasks, dev)
print('EVAL0', json.dumps(base), flush=True); log.write(json.dumps({'eval': base, 'step': 0}) + '\n')
best = base['success']
atomic_save({'model': model.state_dict(), 'cfg': mcfg, 'step': 0, 'fp': kin.fp, 'eval': base}, os.path.join(a.out, 'best.pt'))
atomic_save({'model': model.state_dict(), 'cfg': mcfg, 'step': 0, 'fp': kin.fp, 'eval': base, 'act_scale': act_scale}, os.path.join(a.out, 'best.pt'))
M = C['margin_train_mm']; t0 = time.time()
saver = Saver(C.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(不再只在每 500 次验证时存)
def save_last(n, ev=None):
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev, 'rng': rng_state()}, os.path.join(a.out, 'last.pt')); saver.mark()
atomic_save({'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': mcfg, 'step': n, 'fp': kin.fp, 'eval': ev, 'rng': rng_state(), 'act_scale': act_scale}, os.path.join(a.out, 'last.pt')); saver.mark()
for it in range(it0, C['steps']):
model.train()
batch = random.choices(train_tasks, weights=w, k=C['batch'])
@@ -81,7 +89,7 @@ def main():
z = torch.randn(B, H, 6, device=dev)
q, qp = q0.clone(), q0.clone()
queue = torch.zeros(B, 0, 6, device=dev)
subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev)
subs = torch.zeros(B, 2, 6, device=dev); nsub = torch.zeros(B, dtype=torch.long, device=dev); sub_t = torch.zeros(B, device=dev); sub_prev = q0.clone()
opt.zero_grad(set_to_none=True)
stats = {'col': 0.0, 'tgt': 0.0, 'smooth': 0.0}
# 展开长度随本批任务时长:覆盖本批 exec_s 的 p90 × 1.2(无老师参考题按 6 s),夹在 [plans, plans_max]
@@ -95,11 +103,7 @@ def main():
pre = torch.zeros(B, 3, 6, device=dev); pre[:, :r] = queue[:, :r]
qe = q + queue.sum(1)
with torch.no_grad():
if (nsub > 0).any():
cur = subs[:, 0]; dd = torch.where(kin.cont, wrap180(cur - qe), cur - qe).abs().max(-1).values
adv = (nsub > 0) & ((dd < 5) | (tnow - sub_t > 1.5))
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, 1:]], 1), subs)
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
subs, nsub, sub_prev, sub_t = sub_advance(kin, subs, nsub, sub_prev, sub_t, qe.detach(), tnow)
feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=dev), subs[:, 0], (nsub > 0).float())
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
act, val, sub = model.sample_k(feat, 1, z)
@@ -108,8 +112,8 @@ def main():
with torch.no_grad():
need = sub[:, 12].float() > 0.5
subs = q.detach()[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub))
inc = act.clamp(-1, 1) * step # [B,16,6]
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub)); sub_prev = q.detach().clone()
inc = act.clamp(-1, 1) * astep # [B,16,6]
commit = inc[:, r:r + 3]
if r == 0: # 冷启动:先提交一块,下一轮才开始执行
queue = commit; continue
@@ -118,7 +122,7 @@ def main():
d = queue[:, k]
qp = q; q = kin.clamp_box(q + d)
# 碰撞铰链:端点 + 中点(与仿真/运行时的离散检查一致)
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M)
G = kin.gaps(torch.cat([q, 0.5 * (q + qp)], 0), M, pairs=True)
hinge = torch.relu(M - G['gap'])
col = torch.zeros(2 * B, device=dev).index_add(0, G['b'], hinge).view(2, B).mean(0) # 端点与中点取均值:与只查端点时同量纲,w_col 无需重标
sm = ((d / step) ** 2).sum(-1) + (((d - prev_d) / step) ** 2).sum(-1)
@@ -135,13 +139,18 @@ def main():
stats['tgt'] += lt.mean().item()
(seg_loss / nseg).backward()
seg_loss = torch.zeros((), device=dev)
q, qp, queue, prev_d = q.detach(), qp.detach(), queue.detach(), prev_d.detach()
# 只 detach 状态并重置队列:段边界下一 plan 走 r=0 重新提交(新图有梯度);
# 沿用旧 queue 会跨过已 backward 的图(二次 backward 报错),也避免整段无梯度
q, qp, prev_d = q.detach(), qp.detach(), prev_d.detach()
queue = torch.zeros(B, 0, 6, device=dev)
# BC 锚定
X, Y, V, SG = data.batch(C['batch'] * 4)
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
mem = model.encode(X)
l_bc = wta_loss(model, mem, Y)[0] if model.head == 'wta' else meanflow_loss(model, mem, Y)[0]
v, s = model.heads(mem)
v, _ = model.heads(mem)
Xs = X.clone(); Xs[:, 68:75] = 0
_, s = model.heads(model.encode(Xs))
l_bc = l_bc + 0.1 * (v.float() - V).abs().mean() + 0.1 * (s.float() - SG).abs().mean()
(C['w_bc'] * l_bc).backward()
gn = torch.nn.utils.clip_grad_norm_(model.parameters(), C['grad_clip'])
@@ -160,7 +169,7 @@ def main():
res, _ = evaluate(model.eval(), kin, val_tasks, dev)
res['it'] = it + 1; res['t'] = time.time()
print('EVAL', json.dumps(res), flush=True); log.write(json.dumps({'eval': res}) + '\n'); log.flush()
sd = {'model': model.state_dict(), 'cfg': mcfg, 'step': it + 1, 'fp': kin.fp, 'eval': res}
sd = {'model': model.state_dict(), 'cfg': mcfg, 'step': it + 1, 'fp': kin.fp, 'eval': res, 'act_scale': act_scale}
save_last(it + 1, res) # last 带优化器状态供续跑;best 不带(导出/门禁用)
if res['success'] > best + 1e-4:
best = res['success']; atomic_save(sd, os.path.join(a.out, 'best.pt'))
+30
View File
@@ -60,3 +60,33 @@ def set_rng_state(d):
try: torch.cuda.set_rng_state_all([t.cpu() for t in d['cuda']])
except Exception: pass
return True
def load_policy(path, device, prefer_ema=True):
"""载入策略(2026-10-19,审查 review_model_train P0-2):优先取 EMA 权重 ck['ema'](train.py 起存),没有就用 ck['model'](旧 checkpoint、APG 产物)。
model.act_scale = ck['act_scale'](动作标签归一化倍数,见 ml/prep/build_dataset.cjs ACT_SCALE);旧 checkpoint 缺省 1.0 = 原口径。
闭环仿真 / DAgger / APG 起点 / 挖错 / 门禁 / 验收 / 导出都经这里载入,保证同一份权重、同一个动作尺度。返回 (model.eval(), ck)。"""
from ml.model.policy import build
ck = torch.load(path, map_location=device)
m = build(ck['cfg']).to(device)
sd = ck.get('ema') if prefer_ema and ck.get('ema') is not None else ck['model']
m.load_state_dict(sd)
m.act_scale = float(ck.get('act_scale', 1.0))
return m.eval(), ck
def pick_start(run_dir):
"""DAgger 起点(2026-10-19,审查 P0-2):best.pt 的闭环 success > 0 才用 best.pt;否则用 last.pt
(success 全 0 时 best.pt 只由开环误差决定,可能落在 p_neq 预热期,一步映射没训过)。载入时取 EMA。"""
best, last = os.path.join(run_dir, 'best.pt'), os.path.join(run_dir, 'last.pt')
if os.path.exists(best):
ev = torch.load(best, map_location='cpu').get('eval') or {}
if (ev.get('success') or 0) > 0 or not os.path.exists(last): return best
return last if os.path.exists(last) else best
if __name__ == '__main__':
# python -m ml.common.ckpt pick <运行目录> → 打印 DAgger 起点 checkpoint 路径
import sys
if len(sys.argv) == 3 and sys.argv[1] == 'pick': print(pick_start(sys.argv[2]))
else: raise SystemExit('usage: python -m ml.common.ckpt pick <run_dir>')
+3 -1
View File
@@ -52,7 +52,9 @@ def features(kin, q, q_prev, T, R, prefix, prefix_len, sub, sub_flag, gm=None, x
out.append(sd)
out.append(sub_flag[:, None].to(dt))
if gm is None:
gm = kin.gaps(q, GAP_CAP, xf)['gm']
out.append(kin.gm_groups(q, GP, GAP_CAP, xf=xf) / GAP_CAP) # 2026-10-20:只算特征用到的 8 组;xf 复用不重复算 FK
gm = None
if gm is not None:
out.append(torch.stack([gm[:, a * 7 + b].clamp(max=GAP_CAP) / GAP_CAP for a, b in GP], -1))
f = torch.cat(out, -1)
assert f.shape[1] == F_DIM, f.shape
+225 -7
View File
@@ -38,7 +38,8 @@ def seg_seg_dist(p1, q1, p2, q2, eps=1e-12):
s = torch.where(t_lo, (-c / a.clamp_min(eps)).clamp(0, 1), s)
s = torch.where(t_hi, ((b - c) / a.clamp_min(eps)).clamp(0, 1), s)
t = t.clamp(0, 1)
# 退化(线段长度≈0)
# 第二条退化成点时,仍须在第一条上投影,而不是固定取其起点。
s = torch.where(e <= eps, (-c / a.clamp_min(eps)).clamp(0, 1), s)
s = torch.where(a <= eps, torch.zeros_like(s), s)
t = torch.where(e <= eps, torch.zeros_like(t), t)
c1 = p1 + d1 * s.unsqueeze(-1)
@@ -85,8 +86,36 @@ class Kin:
gi, gj = self.cap_g[self.pi], self.cap_g[self.pj]
self.pair_gp = torch.minimum(gi, gj) * 7 + torch.maximum(gi, gj)
self.lever = T(K['lever'])
table = [[0.0] * 7 for _ in range(6)]
for group in range(1, 7):
endpoints = [c[key] for c in caps if c['g'] == group for key in ('a', 'b')]
if not endpoints:
continue
pivot, axis = K['P'][group - 1], K['D'][group - 1]
unit = [v / math.hypot(*axis) for v in axis]
relative = [[e[j] - pivot[j] for j in range(3)] for e in endpoints]
radius = max(math.hypot(*v) for v in relative)
radial = max(math.hypot(v[1] * unit[2] - v[2] * unit[1],
v[2] * unit[0] - v[0] * unit[2],
v[0] * unit[1] - v[1] * unit[0]) for v in relative)
for joint in range(group):
chain = sum(math.dist(K['P'][j], K['P'][j + 1]) for j in range(joint, group - 1))
table[joint][group] = (chain + (radial if joint == group - 1 else radius)) * D2R
self.cert_groups = torch.tensor([a * 7 + b for a in range(7) for b in range(a + 2, 7)], device=device)
# 三角不等式给出全姿态界,不用随机姿态最大值冒充上界。
self.cert_lever = T([[table[k][b] if a <= k < b else 0.0 for k in range(6)]
for a in range(7) for b in range(a + 2, 7)])
# ---------------- 运动学 ----------------
def _checked_xf(self, q, xf):
"""xf 是按行对应 q 的位姿数组,行数不匹配会静默广播出错(宽相位筛到别的行 ⇒ device assert 或错间隙)。"""
if xf is None:
return self.cum_xf(q)
Rs, Ts = xf
if Rs.shape[0] != q.shape[0] or Ts.shape[0] != q.shape[0]:
raise ValueError('xf 的行数 %d 与 q 的行数 %d 不符' % (Rs.shape[0], q.shape[0]))
return xf
def cum_xf(self, q):
"""q: [B,6] 度 → Rs [B,7,3,3], Ts [B,7,3]"""
B = q.shape[0]
@@ -110,6 +139,19 @@ class Kin:
Rs, _ = xf if xf is not None else self.cum_xf(q)
return Rs[:, 6]
def jac(self, q, xf=None):
"""几何雅可比(对度):返回 (Jv [B,3,6] mm/deg, Jw [B,3,6] 单位轴/deg)"""
Rs, Ts = xf if xf is not None else self.cum_xf(q)
p = self.fk(q, (Rs, Ts))
Jv, Jw = [], []
for k in range(6):
d = torch.tensor(self.D[k], dtype=q.dtype, device=q.device)
a = Rs[:, k] @ d
o = torch.einsum('bij,j->bi', Rs[:, k], self.P[k].to(q.dtype)) + Ts[:, k]
Jv.append(torch.linalg.cross(a, p - o, dim=-1) * D2R)
Jw.append(a * D2R)
return torch.stack(Jv, -1), torch.stack(Jw, -1)
def caps_world(self, q, xf=None):
Rs, Ts = xf if xf is not None else self.cum_xf(q)
Rg, Tg = Rs[:, self.cap_g], Ts[:, self.cap_g] # [B,C,3,3], [B,C,3]
@@ -118,19 +160,73 @@ class Kin:
return a, b
# ---------------- 间隙 ----------------
def gaps(self, q, cap=40.0, xf=None, max_pairs=None):
# 2026-10-19(用户实测 dagger/诊断吃满 24 GB 显存):本模型 1936 条胶囊 / 491 组 / 67256 粗对 / 104.7 万细对。
# 显存有两段峰值,都随点数线性涨:
# 粗筛段 cc[:,cp_a]-cc[:,cp_b] 的广播中间量 ≈ 36 B × 点数 × 粗对数(4096 点实测 9.3 GiB,与 cap 无关);
# 精算段 ≈ 每对几百字节 × 幸存细对数(4096 点 cap 40 幸存 2.4 亿对;旧版不分块实测 36.9 GiB)。
# 所以先按 COARSE_ENTRIES 把点切块(粗筛每块 ≤ ~1 GiB),块内再按幸存细对数 PAIR_BUDGET 切(精算每块 ≤ ~1 GiB)。
# 两段不同时存在,峰值与调用方 batch 大小无关。结果与不分块逐位一致(梯度经 cat 保留)。
COARSE_ENTRIES = 30_000_000
PAIR_BUDGET = 2_000_000
@staticmethod
def _cat_gaps(parts, starts):
out = {'gm': torch.cat([p.pop('gm') for p in parts], 0)}
if 'b' in parts[0]:
# 非 in-place:b 会被 autograd 的索引反向图引用(APG 的碰撞铰链梯度路径),+= 会让 backward 报 version 错
for p, s in zip(parts, starts): p['b'] = p['b'] + s
for k in ('b', 'i', 'j', 'gap'): out[k] = torch.cat([p.pop(k) for p in parts]) # 逐键拼、拼完即释放
return out
def gaps(self, q, cap=40.0, xf=None, max_pairs=None, pairs=False):
"""返回 dict:
gm [B,49]:组对最小间隙 min(gap, cap)(gA*7+gB,gA<gB;其余为 cap)
b, i, j, gap:幸存胶囊对(gap 可微)
pairs=True 时另有 b, i, j, gap:幸存胶囊对(gap 可微;只有 APG 的 hinge 与 check_parity 需要)
2026-10-19(用户实测 4096 点 cap 40 峰值 4.46 GiB 全在拼接 8.6e7 对的 b/i/j/gap):
默认只返回 gm,每块算完就丢掉胶囊对,峰值只剩单块。
"""
xf = xf if xf is not None else self.cum_xf(q)
xf = self._checked_xf(q, xf)
Rs, Ts = xf
B = q.shape[0]
step = max(1, self.COARSE_ENTRIES // int(self.cp_a.shape[0]))
if B <= step:
return self._gaps_split(q, cap, xf, pairs)
st = list(range(0, B, step))
return self._cat_gaps([self._gaps_split(q[s:s + step], cap, (Rs[s:s + step], Ts[s:s + step]), pairs) for s in st], st)
def _coarse(self, q, cap, xf):
Rs, Ts = xf
cc = torch.einsum('bkij,kj->bki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g]
d2 = (cc[:, self.cp_a] - cc[:, self.cp_b]).pow(2).sum(-1) # [B,M]
del cc
return d2 <= (self.cp_lim + cap).pow(2)
def _gaps_split(self, q, cap, xf, pairs):
"""粗筛已在显存预算内:按幸存细对数再切块做精算"""
Rs, Ts = xf
B = q.shape[0]
with torch.no_grad():
cc = torch.einsum('bkij,kj->bki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g]
d2 = (cc[:, self.cp_a] - cc[:, self.cp_b]).pow(2).sum(-1) # [B,M]
keep = d2 <= (self.cp_lim + cap).pow(2)
per = (self._coarse(q, cap, xf).long() * self.cp_cnt).sum(1) # [B] 每点幸存细对数
if B <= 1 or int(per.sum()) <= self.PAIR_BUDGET:
bounds = [0, B]
else:
cs = torch.cumsum(per, 0).tolist(); bounds = [0]; base = 0
for k, c in enumerate(cs):
if c - base > self.PAIR_BUDGET and k > bounds[-1]:
bounds.append(k); base = cs[k - 1]
bounds.append(B)
del per
if len(bounds) == 2:
return self._gaps_one(q, cap, xf, pairs)
return self._cat_gaps([self._gaps_one(q[s:e], cap, (Rs[s:e], Ts[s:e]), pairs) for s, e in zip(bounds[:-1], bounds[1:])], bounds[:-1])
def _gaps_one(self, q, cap, xf, pairs):
Rs, Ts = xf
B = q.shape[0]
with torch.no_grad():
keep = self._coarse(q, cap, xf)
bb, mm = keep.nonzero(as_tuple=True)
del keep
cnt = self.cp_cnt[mm]
rep_b = torch.repeat_interleave(bb, cnt)
off = torch.repeat_interleave(self.cp_start[mm] - torch.cumsum(cnt, 0) + cnt, cnt) + torch.arange(int(cnt.sum()), device=q.device)
@@ -145,8 +241,130 @@ class Kin:
gap = d - self.pair_need[pidx].to(q.dtype)
gm = torch.full((B * 49,), float(cap), dtype=q.dtype, device=q.device)
gm = gm.scatter_reduce(0, rep_b * 49 + self.pair_gp[pidx], gap.clamp(max=cap), reduce='amin', include_self=True)
if not pairs: return {'gm': gm.view(B, 49)}
return {'gm': gm.view(B, 49), 'b': rep_b, 'i': pi, 'j': pj, 'gap': gap}
def gm_groups(self, q, gp, cap=40.0, xf=None):
"""只算指定组对 gp=[(a,b),...] 的最小间隙 [B,len(gp)](特征用,避免 49 组全量)。
数值与 gaps(q,cap)['gm'][:, a*7+b] 一致;组对在碰撞粗对里不存在时返回 cap。
xf 可传入调用者已算好的 cum_xf(q),避免同一姿态重复算 FK(特征路径每周期省一次)。"""
key = tuple(gp)
if getattr(self, '_gp_key', None) != key:
m = torch.full((49,), -1, dtype=torch.long, device=q.device)
for i, (a_, b_) in enumerate(gp): m[min(a_, b_) * 7 + max(a_, b_)] = i
ga, gb = self.cl_g[self.cp_a], self.cl_g[self.cp_b]
cg = torch.minimum(ga, gb) * 7 + torch.maximum(ga, gb) # 簇对 → 7 连杆组对
sel = torch.isin(cg, torch.tensor([min(a, b) * 7 + max(a, b) for a, b in gp], device=q.device))
self._gp = (m, sel, sel.nonzero(as_tuple=True)[0]); self._gp_key = key
self._gp49, sel, idx = self._gp
xf = self._checked_xf(q, xf); Rs, Ts = xf; B = q.shape[0]
with torch.no_grad():
cc = torch.einsum('bkij,kj->bki', Rs[:, self.cl_g], self.cl_c.to(q.dtype)) + Ts[:, self.cl_g]
d2 = (cc[:, self.cp_a[idx]] - cc[:, self.cp_b[idx]]).pow(2).sum(-1)
keep = d2 <= (self.cp_lim[idx] + cap).pow(2)
bb, mm = keep.nonzero(as_tuple=True)
del keep, d2, cc
cnt = self.cp_cnt[idx][mm]
rep_b = torch.repeat_interleave(bb, cnt)
off = torch.repeat_interleave(self.cp_start[idx][mm] - torch.cumsum(cnt, 0) + cnt, cnt) + torch.arange(int(cnt.sum()), device=q.device)
pidx = off
a, b = self.caps_world(q, xf)
with torch.no_grad():
pi, pj = self.pi[pidx], self.pj[pidx]
mid = 0.5 * (a[rep_b, pi] + b[rep_b, pi] - a[rep_b, pj] - b[rep_b, pj])
k2 = mid.pow(2).sum(-1) <= (self.pair_bl[pidx] + cap).pow(2)
rep_b, pidx, pi, pj = rep_b[k2], pidx[k2], pi[k2], pj[k2]
d = seg_seg_dist(a[rep_b, pi], b[rep_b, pi], a[rep_b, pj], b[rep_b, pj])
gap = d - self.pair_need[pidx].to(q.dtype)
gm = torch.full((B * len(gp),), float(cap), dtype=q.dtype, device=q.device)
gm = gm.scatter_reduce(0, rep_b * len(gp) + self._gp49[self.pair_gp[pidx]], gap.clamp(max=cap), reduce='amin', include_self=True)
return gm.view(B, len(gp))
@torch.no_grad()
def certify_segments(self, a, b, max_queries=32, max_depth=12, eps=1e-4, cap=20.0):
"""逐层二分认证:每根段独立查询预算,未知(超预算/超深度/非有限/越界)一律拒绝。
判据对每条组对:子段两端间隙之和 > 该子段位移界 + 2eps(间隙取 min(gap,cap),钳制只会更保守)。
cap 必须 ≥ 该层子段界 + 2eps 才不会挡住本可通过的判定,故不做自适应收紧——同批其他段会改变
钳制程度、让结果随批次而变(实测也无耗时收益,见探索日志轮次 31)。
"""
if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64:
raise ValueError('segment certification requires float64')
if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6:
raise ValueError('a and b must be [count,6] with identical shape')
# 预算给成张量时 max_depth + 1 会在 uint8 这类窄类型上回绕成 0,循环整个跳过,把碰撞段当安全返回。
if type(max_queries) is not int or type(max_depth) is not int:
raise ValueError('certification budgets must be plain ints')
if max_queries < 2 or max_depth < 0 or eps <= 0 or cap <= eps:
raise ValueError('invalid certification budget or tolerance')
count = a.shape[0]
status = torch.zeros(count, dtype=torch.long, device=a.device)
inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1)
status[~inside] = 3
root = torch.arange(count, device=a.device)
gaps = self.gaps(torch.cat([a, b]), cap)['gm'][:, self.cert_groups]
ga, gb = gaps[:count], gaps[count:]
queries = torch.full((count,), 2, dtype=torch.long, device=a.device)
bound = (b - a).abs() @ self.cert_lever.T
for depth in range(max_depth + 1):
finite = torch.isfinite(ga).all(-1) & torch.isfinite(gb).all(-1)
collision = (ga <= 0).any(-1) | (gb <= 0).any(-1)
status[root[~finite]] = 2
status[root[collision]] = 1
positive = (ga > eps).all(-1) & (gb > eps).all(-1)
proven = positive & (ga + gb > bound + 2 * eps).all(-1)
pending = (status[root] == 0) & ~proven
if not bool(pending.any()):
break
a, b, ga, gb, root, bound = a[pending], b[pending], ga[pending], gb[pending], root[pending], bound[pending]
if depth == max_depth:
status[root] = 2
break
requested = torch.bincount(root, minlength=count)
status[(status == 0) & (queries + requested > max_queries)] = 2
keep = status[root] == 0
a, b, ga, gb, root, bound = (t[keep] for t in (a, b, ga, gb, root, bound))
if not root.numel():
break
mid = a + 0.5 * (b - a)
gm = self.gaps(mid, cap)['gm'][:, self.cert_groups]
queries += torch.bincount(root, minlength=count)
a, b = torch.cat([a, mid]), torch.cat([mid, b])
ga, gb = torch.cat([ga, gm]), torch.cat([gm, gb])
root = torch.cat([root, root])
bound = torch.cat([bound, bound]) / 2
return {'safe': status == 0, 'collision': status == 1, 'unresolved': status == 2,
'outside_box': status == 3, 'queries': queries}
@torch.no_grad()
def certify_uniform(self, a, b, n=64, eps=1e-4, cap=None):
"""固定层数的均匀细分认证:n 个子段只需一次 gaps 调用(n+1 个点),没有逐层依赖的往返。
判据与 certify_segments 完全相同;界对 Δq 线性,所以子段界 = 整段界 / n。未通过即 unproven
(拒绝),不存在“未知当安全”。cap=None 时取“最小不挡住可通过判定的宽相位”= 最大子段界 + 2eps:
小于它钳制会把本可证明的子段压成不可证(偏保守),大于它只是多算对(偏慢),不改正确性。
"""
if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64:
raise ValueError('segment certification requires float64')
if type(n) is not int or n < 1 or eps <= 0 or (cap is not None and cap <= eps):
raise ValueError('invalid subdivision count or tolerance')
if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6:
raise ValueError('a and b must be [count,6] with identical shape')
count = a.shape[0]
inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1)
bound = (b - a).abs() @ self.cert_lever.T / n # [count,15] 子段界
required = float(bound.max()) + 2 * eps if count else 2 * eps
qc = required if cap is None else cap
alpha = torch.arange(n + 1, dtype=self.dtype, device=a.device) / n
points = (a[:, None] + (b - a)[:, None] * alpha[None, :, None]).reshape(-1, 6)
gm = self.gaps(points, qc)['gm'][:, self.cert_groups].view(count, n + 1, -1)
adjacent = gm[:, :-1] + gm[:, 1:] > bound[:, None, :] + 2 * eps
collision = (gm <= 0).any((1, 2))
safe = inside & (gm > eps).all((1, 2)) & adjacent.all((1, 2)) & ~collision
return {'safe': safe, 'collision': collision, 'unproven': (~safe & ~collision & inside),
'outside_box': ~inside, 'points': n + 1, 'queries': n + 1,
'cap_required_mm': required, 'cap_used_mm': qc}
def min_gap(self, q, cap=40.0):
return self.gaps(q, cap)['gm'].min(-1).values
+36 -9
View File
@@ -1,19 +1,46 @@
"""显存预算(2026-10-18 本机实测后加)。
用户定的预算:这台 3090 只有 19–20 GB 能给训练用——Windows + 4K 桌面常驻就占掉 4–5 GB(空载 nvidia-smi 4.4 GB)。
limit() 给本进程设硬帽(VRAM_FRAC,默认 0.78 ≈ 18.7 GiB)。超限时在自己的反向里早失败,
而不是把桌面挤到没有显存(整机卡顿、别的应用崩),这也是 APG batch=128 原先 OOM 时把显存顶到 23.6 GB 的原因。
peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对预算,不用再去猜。
实测参考(d=192 同结构、1 步即到位):预训练 batch=1024 → 2.76;APG batch=48/plans=150 → 13.08、batch=128 → OOM。"""
"""显存预算(2026-10-18 本机实测后加;同日两次修正)。
口径:整卡留 1 GB 给系统(VRAM_RESERVE_GIB),本进程上限 = CUDA 可用 − 余量。
为什么不能按"整卡 − nvidia-smi 已用"扣:WDDM 下 nvidia-smi 的"已用"含**驱动可以换出**的量,
把它当硬占用会把自己卡死——实测预训练第一次 evaluate 峰值要 ~15 GiB,那样算出的上限只有
14.45 GiB,torch 就报 OOM,而同一时刻卡上其实还有 9.98 GiB 空闲(train.py step 2000 的评估里挂掉)。
CUDA 的 mem_get_info 才是"我实际能拿到多少":它已经把可换出的部分算进去了。
limit() 上限 = min(CUDA 可用, 整卡) − 余量;显式给 VRAM_FRAC 时再取较小值(跟旧口径 0.78 对拍用)。
别的应用占多少照样打印,但只作参考、不从上限里扣。
peak_gib() 记进日志的 vram 字段:网页指标卡与 ml/selftest.sh 都按它核对,不用再去猜。
实测参考(d=192 同结构):预训练 batch=1024 的训练步峰值只有 2.76 GiB,但第一次 evaluate 就要 ~15 GiB
(kin.gaps 一次开 2.35 GiB);APG batch=48/plans=150 → 13.08 GiB。"""
import os
import subprocess
import torch
def _smi():
"""(整卡已用 GiB, 整卡总量 GiB);拿不到就 (None, None)。"""
try:
q = 'memory.used,memory.total'
r = subprocess.run(['nvidia-smi', f'--query-gpu={q}', '--format=csv,noheader,nounits'],
capture_output=True, text=True, timeout=3)
u, t = [float(x) for x in r.stdout.strip().splitlines()[0].split(',')[:2]]
return u / 1024, t / 1024
except Exception:
return None, None
def limit():
if not torch.cuda.is_available(): return False
f = float(os.environ.get('VRAM_FRAC', '0.78'))
tot = torch.cuda.get_device_properties(0).total_memory / 2**30
torch.cuda.set_per_process_memory_fraction(f)
print('显存上限:本进程 %.1f GiB(整卡 %.1f GiB × %.2f,其余留给桌面)' % (f * tot, tot, f), flush=True)
torch.empty(1, device='cuda') # 让本进程的 CUDA 上下文先落地,再读可用(否则会把自家上下文算进可用)
free = torch.cuda.mem_get_info()[0] / 2**30
res = float(os.environ.get('VRAM_RESERVE_GIB', '1'))
cap = min(free, tot) - res
if os.environ.get('VRAM_FRAC'): # 显式硬帽(对拍旧口径用)
cap = min(cap, tot * float(os.environ['VRAM_FRAC']))
cap = max(1.0, min(cap, tot - res))
torch.cuda.set_per_process_memory_fraction(cap / tot)
used, _ = _smi()
note = '' if used is None else ';别的应用当前占 %.1f GiB,需要时驱动自行换出,不从上限里扣' % used
print('显存上限:本进程 %.1f GiB(CUDA 可用 %.1f − 系统余量 %.1f,= 整卡 %.1f 的 %.0f%%)%s'
% (cap, free, res, tot, 100 * cap / tot, note), flush=True)
return True
+3 -2
View File
@@ -1,10 +1,11 @@
{
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
"train": {
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "wd": 0.01, "betas": [0.9, 0.95],
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "wd": 0.01, "betas": [0.9, 0.95],
"amp": "bf16", "grad_clip": 1.0,
"w_mf": 1.0, "w_fk": 0.5, "w_smooth": 0.05, "w_value": 0.1, "w_sub": 0.1,
"p_neq": 0.25, "fk_caps": 96,
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
"train": {
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "wd": 0.01, "betas": [0.9, 0.95],
"amp": "bf16", "grad_clip": 1.0,
"w_mf": 1.0, "w_fk": 0.5, "w_smooth": 0.05, "w_value": 0.1, "w_sub": 0.1,
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4},
"train": {
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "wd": 0.01, "betas": [0.9, 0.95],
"amp": "bf16", "grad_clip": 1.0,
"w_mf": 1.0, "w_fk": 0.5, "w_smooth": 0.05, "w_value": 0.1, "w_sub": 0.1,
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
}
}
+41
View File
@@ -0,0 +1,41 @@
{
"model": {
"d": 192,
"heads": 8,
"ff": 768,
"enc": 4,
"dec": 4
},
"train": {
"batch": 1024,
"lr": 0.0001,
"warmup": 1000,
"steps": 3000,
"wd": 0.01,
"betas": [
0.9,
0.95
],
"amp": "bf16",
"grad_clip": 1.0,
"w_mf": 1.0,
"w_fk": 0.1,
"w_smooth": 0.01,
"w_value": 0.0,
"w_sub": 0.0,
"p_neq": 0.5,
"p_edge": 0.1,
"p_neq_warm": 0,
"fk_tmax": 0.3,
"fk_caps": 96,
"ema": 0.999,
"aug_pre_drop": 0.15,
"aug_pre_sigma": 0.1,
"aug_dq_sigma": 0.3,
"ckpt_min": 10,
"eval_every": 1000,
"eval_tasks": 256,
"log_every": 50,
"patience_evals": 3
}
}
+3 -2
View File
@@ -1,10 +1,11 @@
{
"model": {"d": 128, "heads": 8, "ff": 512, "enc": 4, "dec": 4},
"train": {
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "wd": 0.01, "betas": [0.9, 0.95],
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "wd": 0.01, "betas": [0.9, 0.95],
"amp": "bf16", "grad_clip": 1.0,
"w_mf": 1.0, "w_fk": 0.5, "w_smooth": 0.05, "w_value": 0.1, "w_sub": 0.1,
"p_neq": 0.25, "fk_caps": 96,
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
}
}
+27 -6
View File
@@ -29,15 +29,36 @@
"eval_every": 100,
"eval_tasks": 16,
"log_every": 10,
"patience_evals": 5
"patience_evals": 5,
"p_neq_warm": 100,
"fk_tmax": 0.3
},
"apg": {
"batch": 8, "lr": 0.0001, "steps": 10, "grad_clip": 1.0, "amp": "bf16",
"plans": 20, "plans_max": 30, "seg_plans": 5,
"batch": 8,
"lr": 0.0001,
"steps": 10,
"grad_clip": 1.0,
"amp": "bf16",
"plans": 20,
"plans_max": 30,
"seg_plans": 5,
"margin_train_mm": 3.0,
"w_tgt": 1.0, "w_rot": 0.3, "w_col": 2.0, "w_smooth": 0.02, "w_len": 0.001, "w_bc": 0.5,
"cat_weights": {"A": 0.4, "B": 1.0, "C": 1.6, "D": 0.6, "Dr": 0.6},
"ckpt_min": 10, "eval_every": 1000, "eval_tasks": 4
"w_tgt": 1.0,
"w_rot": 0.3,
"w_col": 2.0,
"w_smooth": 0.02,
"w_len": 0.001,
"w_bc": 0.5,
"cat_weights": {
"A": 0.4,
"B": 1.0,
"C": 1.6,
"D": 0.6,
"Dr": 0.6
},
"ckpt_min": 10,
"eval_every": 1000,
"eval_tasks": 4
},
"_note": "冒烟与自检专用(bash ml/pipeline.sh selftest / smoke):只验证流程能跑通,数值无意义。apg 段必须有:selftest.sh 第 6 项原先不带 --config,走的是正式 apg.json 的 batch=128,本机实测第一次反向就 OOM;改成正式值 48 也要约 13 GiB,自检不必吃这么多。"
}
+3 -2
View File
@@ -1,10 +1,11 @@
{
"model": {"d": 192, "heads": 8, "ff": 768, "enc": 4, "dec": 4, "head": "wta", "K": 8},
"train": {
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "wd": 0.01, "betas": [0.9, 0.95],
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 24000, "wd": 0.01, "betas": [0.9, 0.95],
"amp": "bf16", "grad_clip": 1.0,
"w_mf": 1.0, "w_fk": 0.5, "w_smooth": 0.05, "w_value": 0.1, "w_sub": 0.1,
"p_neq": 0.25, "fk_caps": 96,
"p_neq": 0.5, "p_edge": 0.1, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
"ema": 0.999, "aug_pre_drop": 0.15, "aug_pre_sigma": 0.1, "aug_dq_sigma": 0.3,
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
}
}
View File
+92
View File
@@ -0,0 +1,92 @@
"""DAgger 状态采集(2026-10-18 首次正式预训练后新增):用当前策略在 train 任务上闭环跑(关安全层),
从它自己走到的状态里挑起点,交给老师重新规划 → 这些"策略会去、老师数据里没有"的状态拿到正确标签。
python -m ml.dagger.collect --ckpt ml/runs/base/best.pt --samples ml/data/samples --n 8000 --seed 1 --parts 28 --out ml/data/dag1
每题最多取 2 个起点:
- 撞了的题:首次碰撞前 0.2 s 的状态(策略"快要撞上"的地方,最有用);
- 另取一个 [0.3 s, 首撞前或结束] 内均匀随机时刻的状态(偏离老师的一般状态)。
起点必须无碰且间隙 > MIN_GAP_MM(老师从碰撞态出发规划不出来,与 build_dataset 的增广接受同一地板),且离目标 > 5 mm(否则 DLS 接管,不需要老师)。
输出 tasks_XX.jsonl(按 --parts 均分,供 gen_dataset.cjs --tasks 并行),每行 {q0, qPrev, T, R, cat, src}。
2026-10-19(审查 F7 / C10):同时记录策略在该时刻的上一帧 qPrev;老师重规划的时间律从当前速度(v0)起步,
t=0 样本的 dq 特征用真实上一帧——原来一律按静止起步标注,DAgger 样本不是策略真正到过的 (q, dq) 状态。
为什么需要:纯行为克隆在闭环里会偏出老师分布(首次 21850 步闭环到达 0%),原方案指望 APG/挖错修,但它们没法从 0% 起步。
老师只用于训练标注,部署仍然无老师。"""
import argparse, json, os, random
import torch
from ml.common.kin_torch import Kin
from ml.data import load_tasks
from ml.model.policy import build
from ml.sim.closed_loop import rollout, goal_err, tasks_to_tensors, DT
GAP_CHUNK = 1024 # 每次 kin.gaps 的查询点数上限
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', nargs='+', default=['ml/data/samples'])
ap.add_argument('--n', type=int, default=8000); ap.add_argument('--seed', type=int, default=1)
ap.add_argument('--parts', type=int, default=8); ap.add_argument('--out', required=True)
ap.add_argument('--bs', type=int, default=64)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
ap.add_argument('--fb_gain', type=float, default=0.0, help='部署分布采集:目标反馈增益(与 rollout 一致)')
ap.add_argument('--safety', type=int, default=0, help='部署分布采集:是否开安全层')
ap.add_argument('--replan', default='r3'); ap.add_argument('--commit_n', type=int, default=3)
ap.add_argument('--safety_partial', type=int, default=0)
a = ap.parse_args()
gap_min = float(os.environ.get('MIN_GAP_MM') or 3)
os.makedirs(a.out, exist_ok=True)
kin = Kin(device=a.device)
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
pool = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'train' and t.get('cat') not in ('D',)]
rng = random.Random(a.seed); rng.shuffle(pool); tasks = pool[:a.n]
out, stat = [], {'tasks': len(tasks), 'collided': 0, 'states': 0, 'rej_gap': 0, 'rej_near': 0}
for i in range(0, len(tasks), a.bs):
tb = tasks[i:i + a.bs]
with torch.no_grad():
_, per = rollout(model, kin, tb, a.device, K=8, safety=bool(a.safety), seed=a.seed * 100003 + i, return_traj=True,
fb_gain=a.fb_gain, replan=a.replan, commit_n=a.commit_n, safety_partial=a.safety_partial)
tr = per['traj'].to(a.device) # [B,T,6]
B, Tn, _ = tr.shape
_, T, R, _, _ = tasks_to_tensors(tb, a.device)
# 2026-10-19 修(用户实测):原来一次喂 64 题 × 64 步 = 4096 点、cap=40 mm 给 kin.gaps,40 mm 内幸存的胶囊对太多,
# 峰值把显存 / 内存吃满。这里只需判断"≤0(碰撞)"和"> gap_min(起点地板)",cap 取 gap_min+1 即可,
# 且按 GAP_CHUNK 个点分块,用完即释放。
cap = gap_min + 1.0
flat = tr.reshape(-1, 6)
with torch.no_grad():
g = torch.cat([kin.gaps(flat[s:s + GAP_CHUNK], cap)['gm'].min(-1).values for s in range(0, flat.shape[0], GAP_CHUNK)]).view(B, Tn)
del flat
for b, t in enumerate(tb):
hit = (g[b] <= 0).nonzero()
c = int(hit[0]) if hit.numel() else Tn
if hit.numel(): stat['collided'] += 1
cand = []
if hit.numel(): cand.append(max(0, c - int(round(0.2 / DT))))
lo = int(round(0.3 / DT))
if c - 1 > lo: cand.append(rng.randint(lo, c - 1))
for k in cand:
if g[b, k] <= gap_min: stat['rej_gap'] += 1; continue
q = tr[b, k:k + 1]; qprev = tr[b, max(0, k - 1)]
e, oe = goal_err(kin, q, T[b:b + 1], R[b:b + 1])
if e.item() < 5 and (not t.get('R') or oe.item() < 5): stat['rej_near'] += 1; continue
out.append({'q0': [round(v, 3) for v in q[0].tolist()], 'qPrev': [round(v, 3) for v in qprev.tolist()], 'T': t['T'], 'R': t.get('R'), 'cat': t.get('cat', 'A'),
'src': {'tid': t['tid'], 'k': k, 'col': bool(hit.numel())}})
del tr, g, per
if a.device.startswith('cuda'): torch.cuda.empty_cache()
print(f'[dagger] {min(i + a.bs, len(tasks))}/{len(tasks)} 题,起点 {len(out)}', flush=True)
stat['states'] = len(out)
rng.shuffle(out)
per_part = (len(out) + a.parts - 1) // max(1, a.parts)
for p in range(a.parts):
chunk = out[p * per_part:(p + 1) * per_part]
if not chunk: continue
fn = os.path.join(a.out, 'tasks_%02d.jsonl' % (p + 1))
open(fn + '.tmp', 'w', encoding='utf-8').write(''.join(json.dumps(x) + '\n' for x in chunk)); os.replace(fn + '.tmp', fn)
json.dump(stat, open(os.path.join(a.out, 'collect.json'), 'w', encoding='utf-8'), indent=1) # 最后写:它存在 = 采集完成
print(json.dumps(stat, ensure_ascii=False), flush=True)
if __name__ == '__main__':
main()
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python
"""反馈蒸馏:用最终部署配置(目标反馈 + 安全层 + 部分前缀 + 每步规划)跑闭环,
收集 (特征, 复合动作 = 模型输出 + 目标反馈) 样本,供 train.py 微调把反馈内化进策略。
PYTHONUTF8=1 python -m ml.dagger.distill_fb --ckpt ml/runs/base_dag2_dag1/best.pt --n 500 \
--fb_gain 1.8 --out ml/data/samples_fbdistill
输出可直接被 ml.data.Samples 读取(collect.X/Y/V/SG/TID.npy;无 meta = act_scale 1.0,与主数据一致)。
动作标签 = 候选动作(含反馈)的归一化 act(/STEP,clamp ±1),与主数据同口径。
"""
import argparse, os, random
import numpy as np
import torch
from ml.common.ckpt import load_policy
from ml.common.features import STEP
from ml.common.kin_torch import Kin
from ml.data import load_tasks
from ml.sim.closed_loop import rollout
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--n', type=int, default=500)
ap.add_argument('--bs', type=int, default=64); ap.add_argument('--fb_gain', type=float, default=1.8)
ap.add_argument('--samples', default='ml/data/samples'); ap.add_argument('--out', required=True)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=1)
a = ap.parse_args()
dev = a.device
model, _ = load_policy(a.ckpt, dev)
kin = Kin(device=dev)
pool = [t for t in load_tasks([a.samples]) if t['split'] == 'train' and t.get('cat') not in ('D',)]
random.Random(a.seed).shuffle(pool); tasks = pool[:a.n]
stp = torch.tensor(STEP) # collect 返回 CPU 张量,标签换算也在 CPU
X, Y, TID = [], [], []
for i in range(0, len(tasks), a.bs):
ch = tasks[i:i + a.bs]
coll = []
with torch.no_grad():
rollout(model, kin, ch, dev, K=8, safety=True, replan='empty', fb_gain=a.fb_gain,
safety_partial=1, commit_n=1, seed=a.seed * 100003 + i, collect=coll)
for f, ac in coll:
X.append(f.numpy()); Y.append((ac / stp).clamp(-1, 1).numpy())
ncol = sum(f.shape[0] for f, _ in coll)
TID.append(np.arange(ncol, dtype=np.int32) + i * 100000)
print(f'[distill] {i + len(ch)}/{len(tasks)} tasks, samples {sum(x.shape[0] for x in X)}', flush=True)
os.makedirs(a.out, exist_ok=True)
X = np.concatenate(X); Y = np.concatenate(Y); TID = np.concatenate(TID)
n = X.shape[0]
np.save(os.path.join(a.out, 'collect.X.npy'), X.astype(np.float32))
np.save(os.path.join(a.out, 'collect.Y.npy'), Y.astype(np.float32))
np.save(os.path.join(a.out, 'collect.V.npy'), np.full(n, 0.5, np.float32))
np.save(os.path.join(a.out, 'collect.SG.npy'), np.zeros((n, 13), np.float32))
np.save(os.path.join(a.out, 'collect.TID.npy'), TID)
print(f'done: {n} samples, X {X.shape}, Y {Y.shape}', flush=True)
if __name__ == '__main__':
main()
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# DAgger 纠偏(2026-10-18 首次正式预训练后新增):bash ml/dagger/run.sh <头标签> <起点 ckpt>
# 每轮:策略闭环采起点(ml.dagger.collect)→ 老师从这些起点重新规划(gen_dataset.cjs --tasks,NPROC 并行)
# → 打包成样本(build_dataset.cjs,与主数据同口径)→ 主数据 + 历轮 DAgger 样本一起微调 DAG_STEPS 步。
# 环境变量:DAG_ROUNDS(默认 2)、DAG_N(每轮采样题数,默认 8000,每题最多 2 个起点)、DAG_STEPS(每轮微调步数,默认 15000)、
# DAG_LR(默认 3e-4)、NPROC、MIN_GAP_MM。
# 产物:ml/data/dag_<tag>_r<k>/(起点与老师 shard)、ml/data/samples_dag_<tag>_r<k>/、ml/runs/<tag>_dag<k>/;
# 最终模型路径写进 ml/runs/<tag>_dagger.ckpt(m5 读它)。
# 断点续跑:采集以 collect.json 为完成标记;老师 shard 先写 .part 再改名、未写完的从断点续写;打包以 .tasks.jsonl 为完成标记;
# 微调用该轮 last.pt 续训;整轮完成打 ml/runs/<tag>_dag<k>/done。
# 2026-10-19(审查 review_model_train P0-2):每轮起点 = python -m ml.common.ckpt pick <目录>:best.pt 的闭环 success > 0 才用它,
# 否则用 last.pt(success 全 0 时 best.pt 只由开环误差决定、可能落在 p_neq 预热期);train.py --init 载入 EMA 权重,
# 只有起点已走过自己的 p_neq 预热时才不再预热。微调的 val 只取主数据(DAgger 样本全部算 train)。
set -e -o pipefail
cd "$(dirname "$0")/../.."
PY=${PYTHON:-python}
TAG=$1; CK=$2
[ -n "$TAG" ] && [ -f "$CK" ] || { echo "usage: run.sh <tag> <ckpt>($CK 不存在)"; exit 1; }
R=${DAG_ROUNDS:-2}; N=${DAG_N:-8000}; STEPS=${DAG_STEPS:-15000}; LR=${DAG_LR:-3e-4}; P=${NPROC:-$(nproc)}
CFGF=ml/configs/$TAG.json
SAMPLES="ml/data/samples"
for ((r=1; r<=R; r++)); do
D=ml/data/dag_${TAG}_r$r; SD=ml/data/samples_dag_${TAG}_r$r; OUT=ml/runs/${TAG}_dag$r
SAMPLES="$SAMPLES $SD"
if [ -f $OUT/done ]; then echo "[dagger] 第 $r 轮已完成,跳过"; CK=$($PY -m ml.common.ckpt pick $OUT); continue; fi
if [ -f $D/collect.json ]; then echo "[dagger] 第 $r 轮起点已采集,跳过"
else echo "[dagger] 第 $r 轮:用 $CK 闭环采起点($N 题)"; $PY -m ml.dagger.collect --ckpt $CK --samples ml/data/samples --n $N --seed $r --parts $P --out $D --fb_gain ${FB_GAIN:-0} --safety ${DAG_SAFETY:-0} --replan ${DAG_REPLAN:-r3} --commit_n ${DAG_COMMIT_N:-3} --safety_partial ${DAG_PARTIAL:-0}; fi
cat $D/collect.json
pids=()
for f in $D/tasks_*.jsonl; do
[ -e "$f" ] || continue
k=$(basename $f .jsonl); k=${k#tasks_}; sh=$D/shard_dag_${TAG}_r${r}_$k.jsonl
[ -f $sh ] && continue
( node ml/gen/gen_dataset.cjs --tasks $f --seed $((9000 + 100 * r + 10#$k)) --out $sh.part 2>> $D/log_$k.txt && mv $sh.part $sh ) &
pids+=($!)
done
if [ ${#pids[@]} -gt 0 ]; then
echo "[dagger] 第 $r 轮:老师重新规划(${#pids[@]} 进程)"
t0=$(date +%s)
while :; do
alive=0; for p in "${pids[@]}"; do kill -0 $p 2>/dev/null && alive=$((alive+1)); done
[ $alive = 0 ] && break; sleep 30
echo "[dagger 节拍] $(( ($(date +%s) - t0) / 60 )) 分钟 运行中 $alive/${#pids[@]} 已写 $(cat $D/*.part $D/shard_*.jsonl 2>/dev/null | wc -l) 条"
done
for p in "${pids[@]}"; do wait $p || true; done # 判定按产物(同 run_parallel.sh:Git Bash 长跑后 wait 返回码不可靠)
for f in $D/tasks_*.jsonl; do
k=$(basename $f .jsonl); k=${k#tasks_}; sh=$D/shard_dag_${TAG}_r${r}_$k.jsonl
if [ ! -f $sh ]; then
n=0; [ -f $sh.part ] && n=$(wc -l < $sh.part); need=$(wc -l < $f)
if [ "$n" -ge "$need" ]; then mv $sh.part $sh; else echo "[dagger] $sh 未写完($n/$need),见 $D/log_$k.txt;重跑本阶段从断点续写"; exit 6; fi
fi
done
fi
mkdir -p $SD
SIG=$(node ml/prep/build_dataset.cjs --sig) # 2026-10-19:与主数据同一套打包签名,参数变了重打(见 run_prep.sh)
todo=(); for sh in $D/shard_dag_*.jsonl; do [ -e "$sh" ] || continue; b=$(basename $sh .jsonl); grep -qF "\"sig\":\"$SIG\"" $SD/$b.meta.json 2>/dev/null && [ -f $SD/$b.tasks.jsonl ] || todo+=("$sh"); done
[ ${#todo[@]} = 0 ] || printf '%s\n' "${todo[@]}" | xargs -P "$P" -I{} node ml/prep/build_dataset.cjs {} $SD
$PY -m ml.data --index $SD | tail -n 3
res=""; [ -f $OUT/last.pt ] && res="--resume $OUT/last.pt"
echo "[dagger] 第 $r 轮:微调 $STEPS 步(样本:$SAMPLES)"
$PY -m ml.train --samples $SAMPLES --out $OUT --config $CFGF --init $CK --steps $STEPS --lr $LR --warmup 500 $res
touch $OUT/done; CK=$($PY -m ml.common.ckpt pick $OUT)
done
echo "$CK" > ml/runs/${TAG}_dagger.ckpt
echo "[dagger] 完成:后续阶段使用 $CK"
+66 -7
View File
@@ -1,12 +1,17 @@
"""样本加载:ml/data/samples/*.{X,Y,V,SG,TID}.npy + *.tasks.jsonl(ml/prep/build_dataset.cjs 产出)
"""样本加载:ml/data/samples/*.{X,Y,V,SG,TID}.npy + *.tasks.jsonl + *.meta.json(ml/prep/build_dataset.cjs 产出)
python -m ml.data --index ml/data/samples # 汇总样本/任务数,写 index.json
全部载入内存(200 万样本约 1.5 GB),按任务 split 字段划分 train / val。"""
全部载入内存(200 万样本约 1.5 GB),按任务 split 字段划分 train / val。
动作尺度(2026-10-19):Y = 每步增量 / (act_scale·STEP)。act_scale 取自各 shard 的 meta.json(DATA_VER 2 起写;
旧数据没有 meta → act_scale 1.0、data_ver 1,即原口径)。同一次训练的所有目录(主数据 + DAgger)必须同一个 act_scale。"""
import argparse, glob, json, os
import numpy as np
import torch
PREFIX_SL, MASK_SL, DQ_SL, SUB_SL = slice(47, 65), slice(65, 68), slice(15, 21), slice(68, 75) # 特征分组(features.py 拼接顺序)
def load_tasks(dirs):
if isinstance(dirs, str): dirs = [dirs]
tasks = []
for d in dirs:
for f in sorted(glob.glob(os.path.join(d, '*.tasks.jsonl'))):
@@ -16,10 +21,28 @@ def load_tasks(dirs):
return tasks
def read_meta(d):
"""目录内各 shard 的打包参数(data_ver、act_scale、增广参数、sig)。没有 meta 的旧打包 → {'data_ver': 1, 'act_scale': 1.0}。
一部分有、一部分没有,或 act_scale 不一致 → 拒绝(新旧标签尺度混在一起会让动作幅度错 25%)。"""
xs = sorted(glob.glob(os.path.join(d, '*.X.npy')))
ms = [json.load(open(f[:-6] + '.meta.json', encoding='utf-8')) for f in xs if os.path.exists(f[:-6] + '.meta.json')]
if not ms: return {'data_ver': 1, 'act_scale': 1.0}
if len(ms) != len(xs): raise SystemExit(f'{d}:{len(xs) - len(ms)} 个 shard 缺 meta.json(新旧打包混在一起),重跑 prep')
sc = {float(m.get('act_scale', 1.0)) for m in ms}
if len(sc) != 1: raise SystemExit(f'{d}:各 shard 的 act_scale 不一致 {sorted(sc)},重跑 prep')
return ms[0]
class Samples:
def __init__(self, dirs, device='cpu', split=None):
def __init__(self, dirs, device='cpu', split=None, val_dirs=None):
"""val_dirs:只用这些目录的 val 任务划 val(默认 = 全部 dirs)。2026-10-19(审查 review_closedloop F11):
DAgger 目录的任务起点带速度、来自策略状态,不该混进预训练的 val;train.py / apg 传主数据目录,DAgger 样本全部算 train。"""
if isinstance(dirs, str): dirs = [dirs]
xs, ys, vs, ss, ts = [], [], [], [], []
metas = [read_meta(d) for d in dirs]
sc = {float(m['act_scale']) for m in metas}
if len(sc) > 1: raise SystemExit(f'样本目录的 act_scale 不一致 {sorted(sc)}({dirs}):DAgger 样本须与主数据同一版打包')
self.act_scale = sc.pop(); self.data_ver = int(metas[0].get('data_ver', 1)) if metas else 1
for d in dirs:
for fx in sorted(glob.glob(os.path.join(d, '*.X.npy'))):
b = fx[:-6]
@@ -27,7 +50,7 @@ class Samples:
ss.append(np.load(b + '.SG.npy')); ts.append(np.load(b + '.TID.npy'))
if not xs: raise SystemExit(f'没有样本:{dirs}(先跑 ml/prep/run_prep.sh)')
self.tasks = load_tasks(dirs)
val_tid = {t['tid'] for t in self.tasks if t['split'] == 'val'}
val_tid = {t['tid'] for t in load_tasks(val_dirs if val_dirs is not None else dirs) if t['split'] == 'val'}
X, Y, V, SG, TID = map(np.concatenate, (xs, ys, vs, ss, ts))
is_val = np.isin(TID, np.fromiter(val_tid, dtype=np.int64)) if val_tid else np.zeros(len(TID), bool)
sel = {'train': ~is_val, 'val': is_val, None: np.ones(len(TID), bool)}[split]
@@ -35,9 +58,43 @@ class Samples:
self.X, self.Y, self.V, self.SG, self.TID = t(X), t(Y), t(V), t(SG), t(TID)
self.n = self.X.shape[0]
def batch(self, bs, gen=None):
def batch(self, bs, gen=None, aug=None):
"""aug:训练配置(train 段),按 aug_pre_drop / aug_pre_sigma / aug_dq_sigma 做条件输入增广(见 augment);None = 原样。"""
i = torch.randint(0, self.n, (bs,), device=self.X.device, generator=gen)
return self.X[i], self.Y[i], self.V[i], self.SG[i]
X, Y = self.X[i], self.Y[i]
if aug: X, Y = self.augment(X.clone(), Y.clone(), aug, gen)
return X, Y, self.V[i], self.SG[i]
def augment(self, X, Y, cfg, gen=None):
"""条件输入增广(2026-10-19,审查 review_data_actions C3 / review_model_train P1-4:前缀与 dq 的 copycat 捷径)。
训练集里 r=3 时要执行的 Y[3:6] 几乎就是前缀第 3 步的延续,dq 又永远是老师的干净速度,模型容易学成「照抄上一块」,
闭环里自己的误差经前缀 / dq 原样带回。三种扰动,标签都保持自洽:
- 前缀丢弃(aug_pre_drop):r>0 的行以该概率清空前缀与掩码 → 变成 r=0 样本。Y 本来就从当前 q 起算,r=0 时标签不变;
- 前缀噪声(aug_pre_sigma,STEP 为单位):r>0 的行一半加噪声 ε。前缀 = 已提交、一定会执行的步,所以标签前 r 步同样 +ε,
第 r 步减去 Σε 把位置拉回老师轨迹(亏空在下一步补齐,与增广收回同理);
- dq 噪声(aug_dq_sigma,STEP 为单位):一半的行给速度特征加噪声,状态与标签不变。"""
B, dev = X.shape[0], X.device
rnd = lambda *s: torch.rand(*s, device=dev, generator=gen)
rn = lambda *s: torch.randn(*s, device=dev, generator=gen)
pl = X[:, MASK_SL].sum(-1).round().long()
pd = float(cfg.get('aug_pre_drop', 0.0))
if pd > 0:
m = (rnd(B) < pd) & (pl > 0)
X[:, 47:68] = torch.where(m[:, None], torch.zeros_like(X[:, 47:68]), X[:, 47:68]); pl = torch.where(m, torch.zeros_like(pl), pl)
ps = float(cfg.get('aug_pre_sigma', 0.0))
if ps > 0:
m = (rnd(B) < 0.5) & (pl > 0)
km = (torch.arange(3, device=dev)[None] < pl[:, None]) & m[:, None] # [B,3] 前缀有效步
eps = rn(B, 3, 6) * ps * km[..., None].to(X.dtype) # STEP 为单位
X[:, PREFIX_SL] = X[:, PREFIX_SL] + eps.reshape(B, 18)
Y[:, :3] = Y[:, :3] + eps / self.act_scale # Y 以 act_scale·STEP 为单位
Y[torch.arange(B, device=dev), pl.clamp(max=Y.shape[1] - 1)] -= eps.sum(1) / self.act_scale
Y.clamp_(-1, 1)
ds = float(cfg.get('aug_dq_sigma', 0.0))
if ds > 0:
m = (rnd(B) < 0.5).to(X.dtype)
X[:, DQ_SL] = X[:, DQ_SL] + rn(B, 6) * ds * m[:, None]
return X, Y
def main():
@@ -46,7 +103,9 @@ def main():
tasks = load_tasks([a.index])
cats = {}
for t in tasks: cats[t['cat'] + '/' + t['mode']] = cats.get(t['cat'] + '/' + t['mode'], 0) + 1
info = {'train_samples': tr.n, 'val_samples': va.n, 'tasks': len(tasks), 'val_tasks': sum(t['split'] == 'val' for t in tasks), 'by_cat_mode': cats}
meta = read_meta(a.index)
info = {'train_samples': tr.n, 'val_samples': va.n, 'tasks': len(tasks), 'val_tasks': sum(t['split'] == 'val' for t in tasks), 'by_cat_mode': cats,
'data_ver': int(meta.get('data_ver', 1)), 'act_scale': float(meta.get('act_scale', 1.0)), 'sig': meta.get('sig')}
json.dump(info, open(os.path.join(a.index, 'index.json'), 'w'), indent=1)
print(json.dumps(info, indent=1))
+4 -4
View File
@@ -4,7 +4,7 @@
其余项读已有文件(缺文件记为 missing,不算通过):
ml/runs/policy_bench.json 部署形态:各集合穿模帧 = 0;near 30/30(pos、pose);route 全部到达;nc1000 0 失败;刹停率 < 2%;R8 恢复 ≥ 95%、活锁 = 0
ml/data/r2.json R2 近邻标签 std < 10% 步长
ml/runs/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(≈ 60000 步 ≤ 3.5 h)
ml/runs/bench_*/log.jsonl R10 吞吐 ≥ 5000 样本/s(24000 步 batch 1024 ≈ 1.4 h)
ml/runs/m5_gate_*.json M5 头门禁
ml/runs/mine/r*/gate.json R7 最后一轮左右比例 / 行程门禁
ml/runs/r4.json R4 浏览器推理 batch=8 P99 < 15 ms(r4 探针)
@@ -37,8 +37,8 @@ def main():
items.append({'item': name, 'value': value, 'pass': (None if ok is None else bool(ok)), 'rule': rule})
# 1) held-out 闭环(仿真安全层)
ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
kin = Kin(device=a.device)
val = [t for t in load_tasks([a.samples]) if t['split'] == 'val']
reach = [t for t in val if t.get('reached') and t['cat'] != 'D']
@@ -97,7 +97,7 @@ def main():
for f in sorted(glob.glob('ml/runs/bench_*/log.jsonl')):
recs = [json.loads(l) for l in open(f) if '"samples_per_s"' in l]
if recs: sps = max(sps or 0, recs[-1]['samples_per_s'])
add('R10 训练吞吐(样本/s)', sps, sps is not None and sps >= 5000, '≥ 5000(≈ 60000 步 ≤ 3.5 h)')
add('R10 训练吞吐(样本/s)', sps, sps is not None and sps >= 5000, '≥ 5000(24000 步 ≈ 1.4 h)')
m5 = [jload(f) for f in sorted(glob.glob('ml/runs/m5_gate_*.json'))]
m5 = [m for m in m5 if m]
add('M5 头门禁', [{'head': m['head'], 'collision_rate': m['policy_only']['collision_rate']} for m in m5] or 'missing',
+54
View File
@@ -0,0 +1,54 @@
"""DLS 解析雅可比对拍 + 延迟(python -m ml.eval.check_dls)"""
import time
import torch
from ml.common.kin_torch import Kin
from ml.sim.closed_loop import dls_steps
def main():
dev = 'cuda' if torch.cuda.is_available() else 'cpu'
kin = Kin(device=dev)
g = torch.Generator(device=dev); g.manual_seed(0)
B = 64
lo, hi = kin.lo + 20, kin.hi - 20
q = lo + (hi - lo) * torch.rand(B, 6, device=dev, generator=g)
xf = kin.cum_xf(q)
p = kin.fk(q, xf)
T = p + 6 * torch.randn(B, 3, device=dev, generator=g)
R = torch.zeros(B, 9, device=dev)
a = dls_steps(kin, q, T, R, n=3, analytic=True)
b = dls_steps(kin, q, T, R, n=3, analytic=False)
d = (a - b).abs()
print('pos-only max|Δdq| %.6f° mean %.6f° rel %.4f' % (d.max().item(), d.mean().item(), (d.norm() / b.norm()).item()))
# 带姿态目标:绕随机轴 ±8°
axis = torch.randn(B, 3, device=dev, generator=g); axis = axis / axis.norm(dim=-1, keepdim=True)
ang = (torch.rand(B, 1, device=dev, generator=g) * 16 - 8) * torch.pi / 180
Kx = torch.zeros(B, 3, 3, device=dev)
Kx[:, 0, 1] = -axis[:, 2]; Kx[:, 0, 2] = axis[:, 1]; Kx[:, 1, 0] = axis[:, 2]
Kx[:, 1, 2] = -axis[:, 0]; Kx[:, 2, 0] = -axis[:, 1]; Kx[:, 2, 1] = axis[:, 0]
I = torch.eye(3, device=dev)
Rd = I + torch.sin(ang)[..., None] * Kx + (1 - torch.cos(ang))[..., None] * (Kx @ Kx)
Rt = kin.orient(q, xf) @ Rd
R9 = Rt.reshape(B, 9)
a = dls_steps(kin, q, T, R9, n=3, analytic=True)
b = dls_steps(kin, q, T, R9, n=3, analytic=False)
d = (a - b).abs()
print('pos+rot max|Δdq| %.6f° mean %.6f° rel %.4f' % (d.max().item(), d.mean().item(), (d.norm() / b.norm()).item()))
for n_ in (1, 3):
for Bn in (1, 256):
qq = q[:Bn]; TT = T[:Bn]; RR = R9[:Bn]
for an in (False, True):
for _ in range(3):
dls_steps(kin, qq, TT, RR, n=n_, analytic=an)
torch.cuda.synchronize() if dev == 'cuda' else None
t0 = time.perf_counter()
for _ in range(20):
dls_steps(kin, qq, TT, RR, n=n_, analytic=an)
torch.cuda.synchronize() if dev == 'cuda' else None
print('n=%d B=%3d analytic=%d %.2f ms' % (n_, Bn, an, (time.perf_counter() - t0) / 20 * 1e3))
if __name__ == '__main__':
main()
+3 -2
View File
@@ -60,14 +60,15 @@ def measure(d, a, pool):
# 与池无关的口径:全部有效近邻对(取距离较近的一半,排除远处非线性段)上回归 std ~ a + b·dist,截距 a
dd = best_d[fin]; jj = best_j[fin]; qf = torch.nonzero(fin).squeeze(1)
half = dd <= torch.quantile(dd, 0.5)
Ya, Yb = d.Y[qi][qf[half], :3].float(), d.Y[pi][jj[half], :3].float()
# 2026-10-19:Y 以 act_scale·STEP 为单位(DATA_VER 2 起 1.25),乘回 act_scale 让判据仍是「STEP 的比例」,与旧数据可比
Ya, Yb = d.Y[qi][qf[half], :3].float() * d.act_scale, d.Y[pi][jj[half], :3].float() * d.act_scale
sa = (Ya - Yb).pow(2).mean(-1).mean(-1).sqrt() / math.sqrt(2)
A = torch.stack([torch.ones_like(dd[half]), dd[half]], 1)
coef = torch.linalg.lstsq(A.cpu(), sa.cpu()[:, None]).solution.squeeze(1)
std_norm = max(0.0, coef[0].item())
thr = torch.quantile(best_d[fin], a.frac)
m = fin & (best_d <= thr)
Yq, Yp = d.Y[qi][m, :3].float(), d.Y[pi][best_j[m], :3].float()
Yq, Yp = d.Y[qi][m, :3].float() * d.act_scale, d.Y[pi][best_j[m], :3].float() * d.act_scale
rms = (Yq - Yp).pow(2).mean(-1).mean(-1).sqrt() # 每对、6 轴 × 3 步平均
std = (rms / math.sqrt(2))
return {'pairs': int(m.sum()), 'pool': P, 'queries': Q, 'nn_dist_threshold': thr.item(),
+316
View File
@@ -0,0 +1,316 @@
#!/usr/bin/env python
"""闭环失败定位(只读,手动运行,n=256 约 35–50 分钟,显存约 3–6 GB)。
PYTHONUTF8=1 python -m ml.eval.diag_cl --ckpt ml/runs/base/last.pt --n 256 --out ml/runs/diag_cl.json
任务 = val 集前 n 题;老师轨迹按任务 id 从 ml/data/shard_*.jsonl 取(取不到的题不进 H 段,数量记在 H_tasks_missing_teacher)。
模型经 ckpt.load_policy 载入(有 EMA 用 EMA;act_scale 随 checkpoint,旧 checkpoint = 1.0)。
段落:
A. 特征口径:val 任务起点(非增广、老师状态)上,Python features() 与数据集 X 的逐组最大差 / 平均差。
B. 同一噪声下两份特征得到的动作差(16 步末端关节差,度)。
C. 旧部署口径(1 段采样、一致性选择、子目标旧规则 timeout)的失败形态:首撞时刻、碰撞步数、首撞组对、最小目标误差、
目标误差进度比、开安全层的到达 / 碰撞 / 刹停。到达 = 时限内到达,碰撞只计时限内(2026-10-19 起)。
H(2026-10-19,三份审查 review_closedloop / review_model_train / review_data_actions 的决定性实验):
H1 老师 oracle 走完整 rollout()(closed_loop.TeacherOracle;select='first'、use_sub=False):
H1_oracle:额外含 oracle 字段——DLS 接管前与老师轨迹的最大关节偏差、到达 / 碰撞、完成时刻 − 老师时长;
H1_oracle_noise_<σ>(σ = 0.05 / 0.1 / 0.2,STEP 为单位,每次规划重采):闭环对残差噪声的敏感度。
H2 前缀消融:H2_open_loop(r=3 的 val 行,前缀 47:68 原样 vs 置零,执行窗口 3..5 的误差);
H2_replan_empty_s{1,4}:队列空了才规划(每次 r=0、提交 3 步),其余同部署。
H3 子目标:H3_headsub_cross_s{1,4}(子目标头 + 越过顶点规则 = 新部署默认)、H3_headsub_timeout_s{1,4}(旧规则 = 旧部署)、
H3_teachersub_cross_s{1,4} / H3_teachersub_timeout_s{1,4}(老师真实途经点全表)、H3_nosub_s{1,4}(不用子目标)。
H4 指标(上面每个闭环变体都输出同一组键,见 metrics());H4_open_loop:按前缀长度 r=0..3 的执行窗口误差。
_s1 / _s4 = MeanFlow 1 段(部署)/ 4 段采样。"""
import argparse, json, math, os
import torch
from ml.common.ckpt import load_policy
from ml.common.kin_torch import Kin
from ml.common.features import features, STEP, H
from ml.data import Samples, load_tasks, read_meta
from ml.sim.closed_loop import rollout, goal_err, DT, TeacherOracle
from ml.eval.replay_teacher import load_teacher, pad_traj, oracle_raw, oracle_summary
# 83 维特征分组(与 features.py 拼接顺序一致)
GROUPS = [('sincos', 0, 12), ('joint_norm', 12, 15), ('dq', 15, 21), ('p', 21, 24), ('Rq', 24, 30), ('T', 30, 33),
('R', 33, 39), ('T-p', 39, 42), ('T-p_clamp', 42, 45), ('mode', 45, 47), ('prefix', 47, 65), ('mask', 65, 68),
('sub', 68, 74), ('sub_flag', 74, 75), ('gaps', 75, 83)]
PROG_T = (0.5, 1.0, 2.0) # 目标误差 / 起点误差 的取样时刻(s)
K1, K2 = int(round(1.0 / DT)), int(round(2.0 / DT)) # 1 s / 2 s 对应的步数
def pct(v, p):
v = sorted(x for x in v if x is not None and x == x)
return v[min(len(v) - 1, int(len(v) * p))] if v else None
def qt(t, p):
t = t[torch.isfinite(t)]
return round(float(t.kthvalue(max(1, min(len(t), int(math.ceil(p * len(t)))))).values), 4) if len(t) else None
def rnd(x, n=4):
if isinstance(x, list): return [rnd(v, n) for v in x]
return None if x is None or x != x else round(float(x), n)
def pad_len(tr, n):
"""轨迹 [B,T,6] 末帧重复补到至少 n 帧(到达后冻结,补帧与冻结等价)。"""
if tr.shape[1] >= n: return tr
return torch.cat([tr, tr[:, -1:].expand(-1, n - tr.shape[1], -1)], 1)
def batch_stats(kin, ch, per, TR):
"""一批的逐题量(CPU)。窗口:幅度 / 净位移 / TCP 用前 1 s,变号率用前 2 s 的 3 步块。"""
dev = TR.device
stp = torch.tensor(STEP, device=dev)
tr = pad_len(per['traj'].to(dev), K2 + 1); te = pad_len(TR, K2 + 1)
B = tr.shape[0]
amp = lambda x: (x[:, 1:K1 + 1] - x[:, :K1]).abs().sum(1) # [B,6] 前 1 s 关节行程
net = lambda x: (x[:, K1] - x[:, 0]).abs() # [B,6] 前 1 s 净位移
def tcp(x):
p = kin.fk(x[:, :K1 + 1].reshape(-1, 6)).view(B, K1 + 1, 3)
return (p[:, 1:] - p[:, :-1]).norm(dim=-1).sum(1), (p[:, -1] - p[:, 0]).norm(dim=-1)
def flips(x):
idx = torch.arange(0, K2 + 1, 3, device=dev)
b = x[:, idx[1:]] - x[:, idx[:-1]] # [B,nb,6] 3 步块增量
big = b.abs() > 0.1 * stp
v = big[:, 1:] & big[:, :-1]
f = v & (torch.sign(b[:, 1:]) != torch.sign(b[:, :-1]))
return f.sum(1).float(), v.sum(1).float()
tl_p, tn_p = tcp(tr); tl_t, tn_t = tcp(te)
fp_, vp_ = flips(tr); ft_, vt_ = flips(te)
ex = torch.tensor([t.get('exec_s') or float('nan') for t in ch])
return {'ok': per['ok'], 'col': per['col'] > 0, 'isD': torch.tensor([t.get('cat') == 'D' for t in ch]),
'pose': torch.tensor([bool(t.get('R')) for t in ch]), 'key': [t.get('cat', '?') + '/' + t.get('mode', '?') for t in ch],
'r2': per['goal_ratio_2s'], 'err': per['err'], 'first_hit': per['first_hit'], 'brakes': per['brakes'],
'tratio': per['t_done'] / ex,
'amp_p': amp(tr).cpu(), 'amp_t': amp(te).cpu(), 'net_p': net(tr).cpu(), 'net_t': net(te).cpu(),
'tl_p': tl_p.cpu(), 'tl_t': tl_t.cpu(), 'tn_p': tn_p.cpu(), 'tn_t': tn_t.cpu(),
'fl_p': fp_.cpu(), 'fv_p': vp_.cpu(), 'fl_t': ft_.cpu(), 'fv_t': vt_.cpu()}
def metrics(stats):
"""H4 指标(每个闭环变体同一组键):
reach / collision:cat≠D、时限内(D 类目标不可达,单列在 D);
goal_ratio_2s_p50:2 s 时目标误差 / 起点误差;end_goal_err_mm_p50;first_hit_s_p50(撞了的题);t_done_over_teacher_p50(到达的题);
joint_amp_ratio[6]:前 1 s 各轴关节行程 Σ|Δq| 策略 / 老师(全体题合计;J6 只算 pose 题,pos 题老师冻结 J6);
joint_net_ratio[6]:前 1 s 各轴净位移 |q(1s) − q(0)| 策略 / 老师(同上);j6_pos_amp_deg_p50:pos 题策略 J6 前 1 s 行程(度,老师为 0);
tcp_path_ratio_p50 / tcp_net_ratio_p50:前 1 s TCP 路程 / 净位移 策略 / 老师(逐题比,老师 > 1 mm 的题);
sign_flip_rate[6] / sign_flip_rate_teacher[6]:前 2 s 相邻 3 步块增量变号的比例(两块都 > 0.1·STEP 才计);
by_cat_mode:各 cat/mode 的 n、reach、collision、goal_ratio_2s_p50(不含 D);D:n、collision、end_goal_err_mm_p50、goal_ratio_2s_p50。"""
c = {k: (sum((s[k] for s in stats), []) if isinstance(stats[0][k], list) else torch.cat([s[k] for s in stats])) for k in stats[0]}
keys = c['key']
m = ~c['isD']; pose = c['pose']
def jr(a, b):
out = []
for j in range(6):
sel = m & pose if j == 5 else m
den = float(b[sel, j].sum()); out.append(float(a[sel, j].sum()) / den if den > 1e-6 else None)
return rnd(out)
tl_ok = m & (c['tl_t'] > 1); tn_ok = m & (c['tn_t'] > 1)
out = {'n': int(m.sum()),
'reach': rnd(c['ok'][m].float().mean()) if m.any() else None,
'collision': rnd(c['col'][m].float().mean()) if m.any() else None,
'goal_ratio_2s_p50': qt(c['r2'][m], .5), 'end_goal_err_mm_p50': qt(c['err'][m], .5),
'first_hit_s_p50': qt(c['first_hit'][m], .5), 't_done_over_teacher_p50': qt(c['tratio'][m & c['ok']], .5),
'brakes_p50': qt(c['brakes'][m], .5),
'joint_amp_ratio': jr(c['amp_p'], c['amp_t']), 'joint_net_ratio': jr(c['net_p'], c['net_t']),
'j6_pos_amp_deg_p50': qt(c['amp_p'][m & ~pose, 5], .5),
'tcp_path_ratio_p50': qt(c['tl_p'][tl_ok] / c['tl_t'][tl_ok], .5), 'tcp_net_ratio_p50': qt(c['tn_p'][tn_ok] / c['tn_t'][tn_ok], .5),
'sign_flip_rate': rnd((c['fl_p'][m].sum(0) / c['fv_p'][m].sum(0).clamp(min=1)).tolist()),
'sign_flip_rate_teacher': rnd((c['fl_t'][m].sum(0) / c['fv_t'][m].sum(0).clamp(min=1)).tolist())}
by = {}
for i, k in enumerate(keys):
if not bool(m[i]): continue
by.setdefault(k, []).append(i)
out['by_cat_mode'] = {k: {'n': len(ix), 'reach': rnd(c['ok'][ix].float().mean()), 'collision': rnd(c['col'][ix].float().mean()),
'goal_ratio_2s_p50': qt(c['r2'][ix], .5)} for k, ix in sorted(by.items())}
d = c['isD']
out['D'] = {'n': int(d.sum()), 'collision': rnd(c['col'][d].float().mean()) if d.any() else None,
'end_goal_err_mm_p50': qt(c['err'][d], .5), 'goal_ratio_2s_p50': qt(c['r2'][d], .5)}
return out
def teacher_subs(recs, device):
"""老师真实途经点全表(path 去掉起终点)→ (subs [B,M,6], n [B]),供 rollout(sub_override=...)。"""
vs = [r['path'][1:-1] for r in recs]
M = max(1, max(len(v) for v in vs))
subs = torch.zeros(len(vs), M, 6, device=device); n = torch.zeros(len(vs), dtype=torch.long, device=device)
for i, v in enumerate(vs):
if v:
x = torch.tensor(v, dtype=torch.float32, device=device); subs[i, :len(v)] = x; subs[i, len(v):] = x[-1]; n[i] = len(v)
return subs, n
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--shards', default='ml/data/shard_*.jsonl')
ap.add_argument('--n', type=int, default=256); ap.add_argument('--bs', type=int, default=64)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default='ml/runs/diag_cl.json')
a = ap.parse_args()
dev = a.device
model, ck = load_policy(a.ckpt, dev)
kin = Kin(device=dev)
astep = torch.tensor(STEP, device=dev) * model.act_scale # 动作 → 度
data_scale = float(read_meta(a.samples).get('act_scale', 1.0))
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'weights': 'ema' if ck.get('ema') is not None else 'model',
'act_scale_ckpt': model.act_scale, 'act_scale_data': data_scale}
tasks = [t for t in load_tasks([a.samples]) if t['split'] == 'val'][:a.n]
by_tid = {t['tid']: t for t in tasks}
def save():
s = json.dumps(out, indent=1, ensure_ascii=False)
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
open(a.out + '.tmp', 'w', encoding='utf-8').write(s); os.replace(a.out + '.tmp', a.out)
return s
# ---------- A / B:起点特征口径 ----------
vd = Samples(a.samples, device=dev, split='val')
tid = vd.TID.long()
first = torch.ones_like(tid, dtype=torch.bool); first[1:] = tid[1:] != tid[:-1] # 每题第一行 = t=0 样本
idx = [i for i in first.nonzero().flatten().tolist() if int(tid[i]) in by_tid]
X0 = vd.X[idx].float()
tk = [by_tid[int(tid[i])] for i in idx]
q0 = torch.tensor([t['q0'] for t in tk], dtype=torch.float32, device=dev)
qx = torch.atan2(X0[:, 0:12:2], X0[:, 1:12:2]) * 180 / torch.pi
dqx = torch.remainder(qx - q0 + 180, 360) - 180
clean = dqx.abs().max(-1).values < 1e-2 # 未被噪声增广的起点
out['A_rows'] = int(clean.sum()); out['A_rows_aug_skipped'] = int((~clean).sum())
if clean.any():
Xc = X0[clean]; qc = q0[clean]; tc = [t for t, c in zip(tk, clean.tolist()) if c]
T = torch.tensor([t['T'] for t in tc], dtype=torch.float32, device=dev)
R = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in tc], dtype=torch.float32, device=dev)
B = qc.shape[0]
qpc = torch.tensor([t.get('qp0') or t['q0'] for t in tc], dtype=torch.float32, device=dev)
pre = Xc[:, 47:65].view(B, 3, 6) * torch.tensor(STEP, device=dev) # 前缀 / 子目标直接取数据集里的,只比状态相关部分
pl = Xc[:, 65:68].sum(-1).round()
sf = Xc[:, 74]
sub = qc + Xc[:, 68:74] * 180
Fp = features(kin, qc, qpc, T, R, pre, pl, sub, sf)
out['A_maxdiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().max().item() for g, s, e in GROUPS}
out['A_meandiff'] = {g: (Fp[:, s:e] - Xc[:, s:e]).abs().mean().item() for g, s, e in GROUPS}
with torch.no_grad():
z = torch.randn(B * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(7))
a1, _, _ = model.sample_k(Xc, 8, z); a2, _, _ = model.sample_k(Fp, 8, z)
d = ((a1.clamp(-1, 1) - a2.clamp(-1, 1)) * astep).sum(2).abs().max(-1).values # [B,K] 16 步末端关节差(度)
mag = (a1.clamp(-1, 1) * astep).sum(2).abs().max(-1).values
out['B_act_diff_deg'] = {'mean': d.mean().item(), 'p90': d.flatten().kthvalue(max(1, int(0.9 * d.numel()))).values.item(),
'ref_motion_deg_mean': mag.mean().item()}
# ---------- H4 / H2 开环:执行窗口误差(val 行,老师状态) ----------
# 闭环稳态 r=3,真正执行的是 act[r:r+3];原来的 ol_fk_mm 是 16 步终点误差,前缀那几步几乎不会错,看不出执行窗口。
with torch.no_grad():
pl_all = vd.X[:, 65:68].sum(-1).round().long()
gsel = torch.Generator(device=dev); gsel.manual_seed(321)
def rows_with(r, n=1024):
ix = (pl_all == r).nonzero().flatten()
return ix[torch.randperm(len(ix), device=dev, generator=gsel)[:n]] if len(ix) else ix
def win_err(X, Y, r, steps, k0=None):
k0 = r if k0 is None else k0
z = torch.randn(X.shape[0] * 8, H, 6, device=dev, generator=torch.Generator(device=dev).manual_seed(11))
act, _, _ = model.sample_k(X, 8, z, steps=steps); act = act.float().clamp(-1, 1)
e = ((act[:, :, k0:k0 + 3] - Y[:, None, k0:k0 + 3]) * astep).sum(2).abs() # [N,K,6] 窗口累计误差(度)
emax = e.max(-1).values.flatten()
res = {'n_rows': int(X.shape[0]), 'deg_mean': rnd(emax.mean()), 'deg_p90': qt(emax, .9), 'per_joint_deg_mean': rnd(e.mean((0, 1)).tolist())}
if r > 0 and k0 == r:
ce = ((act[:, :, :r] - Y[:, None, :r]) * astep).abs().max(-1).values.max(-1).values.flatten()
res['prefix_copy_err_deg_mean'] = rnd(ce.mean())
return res
ol4, ol2 = {}, {}
for steps in (1, 4):
for r in range(4):
ix = rows_with(r)
if len(ix): ol4['s%d_r%d' % (steps, r)] = win_err(vd.X[ix].float(), vd.Y[ix].float(), r, steps)
ix = rows_with(3)
if len(ix):
X3 = vd.X[ix].float(); Y3 = vd.Y[ix].float(); X3z = X3.clone(); X3z[:, 47:68] = 0
ol2['s%d_prefix_orig' % steps] = win_err(X3, Y3, 3, steps)
ol2['s%d_prefix_zeroed' % steps] = win_err(X3z, Y3, 0, steps, k0=3)
out['H4_open_loop'] = ol4; out['H2_open_loop'] = ol2
del vd
if dev.startswith('cuda'): torch.cuda.empty_cache()
# ---------- C:旧部署口径的闭环失败形态(子目标旧规则,便于与前几轮对比) ----------
first_t, col_steps, pairs, min_err, reach = [], [], {}, [], 0
prog = {ts: [] for ts in PROG_T}
reach_s, brakes_s, col_s = 0, [], 0
for i in range(0, len(tasks), a.bs):
ch = tasks[i:i + a.bs]
with torch.no_grad():
_, per = rollout(model, kin, ch, dev, K=8, safety=False, seed=i, return_traj=True, sub_rule='timeout')
tr = per['traj'].to(dev) # [B,Tn,6]
B, Tn, _ = tr.shape
flat = tr.reshape(-1, 6) # 分块 ≤1024 点(同 dagger/collect.py 的显存修复)
gm = torch.cat([kin.gaps(flat[s:s + 1024], 0.5)['gm'] for s in range(0, flat.shape[0], 1024)]).view(B, Tn, 49)
inlim = (torch.arange(Tn, device=dev)[None] * DT) <= per['limit'].to(dev)[:, None] + 1e-6
hit = (gm.min(-1).values <= 0) & inlim # [B,Tn] 只计时限内
Tt = torch.tensor([t['T'] for t in ch], dtype=torch.float32, device=dev)
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in ch], dtype=torch.float32, device=dev)
e, _ = goal_err(kin, tr.reshape(-1, 6), Tt.repeat_interleave(Tn, 0), Rt.repeat_interleave(Tn, 0))
e = e.view(B, Tn)
for b in range(B):
for ts in PROG_T:
k = min(Tn - 1, int(round(ts / DT))); prog[ts].append((e[b, k] / e[b, 0].clamp(min=1e-3)).item())
for b in range(B):
if per['ok'][b]: reach += 1
min_err.append(e[b].min().item()); col_steps.append(int(per['col'][b]))
h = hit[b].nonzero().flatten()
if len(h):
k = int(h[0]); first_t.append(k * DT)
pr = int(gm[b, k].argmin()); key = '%d-%d' % (pr // 7, pr % 7); pairs[key] = pairs.get(key, 0) + 1
rs, ps = rollout(model, kin, ch, dev, K=8, safety=True, seed=i, sub_rule='timeout')
reach_s += int(ps['ok'].sum()); col_s += int((ps['col'] > 0).sum()); brakes_s += ps['brakes'].tolist()
n = len(tasks)
out['C'] = {'n': n, 'reach': reach / n, 'collided': len(first_t) / n,
'first_hit_s': {'p10': pct(first_t, .1), 'p50': pct(first_t, .5), 'p90': pct(first_t, .9),
'within_0.1s': sum(t <= 0.1 for t in first_t) / max(1, len(first_t))},
'col_steps': {'p50': pct(col_steps, .5), 'p90': pct(col_steps, .9)},
'first_hit_pair(group a-b)': dict(sorted(pairs.items(), key=lambda kv: -kv[1])[:8]),
'min_goal_err_mm': {'p10': pct(min_err, .1), 'p50': pct(min_err, .5), 'p90': pct(min_err, .9)},
'safety_on': {'reach': reach_s / n, 'collided': col_s / n, 'brakes_p50': pct(brakes_s, .5)}}
out['C']['goal_err_ratio_p50'] = {'%.1fs' % ts: pct(v, .5) for ts, v in prog.items()} # <1 = 在靠近目标
print(save(), flush=True)
# ---------- H:闭环变体(同一批 val 题,老师轨迹按 id 取) ----------
teach = load_teacher(a.shards, [t['id'] for t in tasks])
tH = [t for t in tasks if t['id'] in teach]
out['H_tasks'] = len(tH); out['H_tasks_missing_teacher'] = len(tasks) - len(tH)
def variant(oracle_sigma=None, teacher_sub=False, **kw):
stats, raws = [], []
for i in range(0, len(tH), a.bs):
ch = tH[i:i + a.bs]; recs = [teach[t['id']] for t in ch]
TR = pad_traj([r['traj'] for r in recs], dev)
kw2 = dict(kw)
if oracle_sigma is not None:
kw2.update(oracle=TeacherOracle(TR, act_scale=data_scale, sigma=oracle_sigma), select='first', use_sub=False)
if teacher_sub: kw2['sub_override'] = teacher_subs(recs, dev)
with torch.no_grad():
_, per = rollout(None if oracle_sigma is not None else model, kin, ch, dev, K=8, seed=i, return_traj=True, **kw2)
stats.append(batch_stats(kin, ch, per, TR))
if oracle_sigma is not None: raws.append(oracle_raw(per, TR, [len(r['traj']) for r in recs], ch))
del per
res = metrics(stats)
if oracle_sigma is not None and oracle_sigma == 0: res['oracle'] = oracle_summary(raws)
if dev.startswith('cuda'): torch.cuda.empty_cache()
return res
plan = [('H1_oracle', dict(oracle_sigma=0.0))]
plan += [('H1_oracle_noise_%g' % s, dict(oracle_sigma=s)) for s in (0.05, 0.1, 0.2)]
for st in (1, 4):
plan += [('H3_headsub_cross_s%d' % st, dict(steps=st, sub_rule='cross')),
('H3_headsub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout')),
('H3_teachersub_cross_s%d' % st, dict(steps=st, sub_rule='cross', teacher_sub=True)),
('H3_teachersub_timeout_s%d' % st, dict(steps=st, sub_rule='timeout', teacher_sub=True)),
('H3_nosub_s%d' % st, dict(steps=st, use_sub=False)),
('H2_replan_empty_s%d' % st, dict(steps=st, sub_rule='cross', replan='empty'))]
for name, kw in plan:
out[name] = variant(**kw)
save(); print('[diag] %s 完成' % name, flush=True)
print(save(), flush=True)
if __name__ == '__main__':
main()
+400
View File
@@ -0,0 +1,400 @@
#!/usr/bin/env python
"""闭环碰撞定位:第一次碰撞发生在第几秒、碰撞时最近的是哪两条连杆(J几-J几)。
用法(仓库根目录):
PYTHONUTF8=1 python -m ml.eval.diag_collision --ckpt ml/runs/base/last.pt --n 512 --out ml/runs/diag_collision.json
口径与 closed_loop.rollout 一致:每步同时检查当前状态 q 与相邻状态中点 mid 的间隙(cap=0.5 mm);
间隙组对索引 = gA*7+gB(gA<gB,g 为连杆组 0..5 → J1..J6)。
"""
import argparse, json, math, os, sys, time
from collections import Counter
import torch
from ml.common.kin_torch import Kin
from ml.data import load_tasks
from ml.model.policy import build
from ml.sim.closed_loop import DT, rollout
def pair_names():
# kin.json 有 7 个连杆组:g=0 是固定基座/立柱,g=1..6 是 J1..J6
def nm(g):
return '基座' if g == 0 else f'J{g}'
return {a * 7 + b: f'{nm(a)}-{nm(b)}' for a in range(7) for b in range(a + 1, 7)}
@torch.no_grad()
def replay_first_hits(path, out=None):
saved = torch.load(path, map_location='cpu', weights_only=True)
k32, k64 = Kin(), Kin(dtype=torch.float64)
rows, offset = [], 0
for _, per, count in saved['batches']:
for b in (per['col'] > 0).nonzero().flatten().tolist():
it = int(round(float(per['first_hit'][b]) / DT))
prev, q = per['traj'][b, it - 1:it + 1]
mid = prev + 0.5 * (q - prev)
points = torch.stack([per['traj'][b, 0], prev, mid, q])
gm32 = k32.gaps(points, 0.5)['gm']
gm64 = k64.gaps(points.double(), 0.5)['gm']
gap32, gp32 = gm32.min(-1)
gap64, gp64 = gm64.min(-1)
nseg = max(1, math.ceil(float((q - prev).abs().max()) / 0.01))
dense = prev.double() + torch.linspace(0, 1, nseg + 1, dtype=torch.float64)[:, None] * (q.double() - prev.double())
dense_gap = torch.cat([k64.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in dense.split(32)])
boundary = (~k32.cont) & ((q - k32.lo).abs().lt(1e-5) | (q - k32.hi).abs().lt(1e-5))
row = {
'i': offset + b, 'task': saved['tasks'][offset + b],
'first_hit_s': float(per['first_hit'][b]), 'dls_it': int(per['dls_it'][b]),
'point_order': ['q0', 'previous', 'midpoint', 'endpoint'],
'gap32_mm': gap32.tolist(), 'gap64_mm': gap64.tolist(),
'pair32': gp32.tolist(), 'pair64': gp64.tolist(),
'boundary_joints': (boundary.nonzero().flatten() + 1).tolist(),
'previous': prev.tolist(), 'endpoint': q.tolist(),
'dense_nsegments': nseg, 'dense_gap64_min_mm': float(dense_gap.min()),
}
if 'plans' in per:
plans = per['plans']
pi = int(torch.searchsorted(plans['it'], it - 1, right=True)) - 1
step = it - 1 - int(plans['it'][pi])
ncommit = plans['commit'].shape[2]
command = plans['commit'][b, pi, step]
cp, cm = plans['points'][b, pi, step], plans['midpoints'][b, pi, step]
checked = bool(plans['certified'][b, pi, step])
predicted = k32.clamp_box((prev + command)[None])[0]
row['certificate'] = {
'plan_start_it': int(plans['it'][pi]), 'step': step, 'checked': checked,
'endpoint': cp.tolist() if checked else None,
'midpoint': cm.tolist() if checked else None,
'recorded_gap32_mm': plans['gaps'][b, pi, [step, ncommit + step]].tolist() if checked else None,
'replayed_gap64_mm': k64.gaps(torch.stack([cp, cm]).double(), 0.5)['gm'].min(-1).values.tolist() if checked else None,
'command_delta': command.tolist(),
'clamped_reconstruction_max_abs_deg': float((predicted - q).abs().max()),
'certified_vs_executed_max_abs_deg': float((cp - q).abs().max()) if checked else None,
}
rows.append(row)
print('task %d first_hit %.2fs gap32 %s gap64 %s boundary %s' %
(row['i'], row['first_hit_s'], row['gap32_mm'], row['gap64_mm'], row['boundary_joints']), flush=True)
offset += count
result = {
'trace': path, 'n': offset, 'reported_collision_tasks': len(rows),
'replayed_first_hits32': sum(min(r['gap32_mm'][2:]) <= 0 for r in rows),
'replayed_first_hits64': sum(min(r['gap64_mm'][2:]) <= 0 for r in rows),
'scope': 'Replay reported first hits only; same geometry kernel at float32/float64, not a full safety audit.',
'rows': rows,
}
if out:
with open(out, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, allow_nan=False)
print('replayed first hits: %d/%d float32, %d/%d float64' %
(result['replayed_first_hits32'], len(rows), result['replayed_first_hits64'], len(rows)), flush=True)
return result
@torch.no_grad()
def replay_dense(path, indices, max_step_deg=0.05, device='cpu', out=None):
saved = torch.load(path, map_location='cpu', weights_only=True)
kin = Kin(device=device, dtype=torch.float64)
if saved.get('kin_fp') != kin.fp:
raise ValueError('trace/kin fingerprint mismatch')
if not indices or min(indices) < 0 or max(indices) >= len(saved['tasks']) or max_step_deg <= 0:
raise ValueError('valid task indices and positive max_step_deg required')
started = time.perf_counter()
rows, offset = [], 0
for _, per, count in saved['batches']:
for index in indices:
if not offset <= index < offset + count:
continue
b = index - offset
nstep = min(per['traj'].shape[1] - 1, math.floor((float(per['limit'][b]) + 1e-6) / DT))
if torch.isfinite(per['t_done'][b]):
nstep = min(nstep, round(float(per['t_done'][b]) / DT))
trajectory = per['traj'][b, :nstep + 1].double()
points, segment_ids, fractions = [trajectory[:1]], [torch.tensor([-1])], [torch.zeros(1)]
for it in range(nstep):
prev, end = trajectory[it:it + 2]
nseg = max(2, 2 * math.ceil(float((end - prev).abs().max()) / (2 * max_step_deg)))
alpha = torch.arange(1, nseg + 1, dtype=torch.float64) / nseg
points.append(prev + alpha[:, None] * (end - prev))
segment_ids.append(torch.full((nseg,), it))
fractions.append(alpha)
points = torch.cat(points)
segments, alpha = torch.cat(segment_ids), torch.cat(fractions)
print('dense task %d steps %d points %d start' % (index, nstep, len(points)), flush=True)
gaps = []
for start in range(0, len(points), 64):
gaps.append(kin.gaps(points[start:start + 64].to(device), 0.5)['gm'].min(-1).values.cpu())
if (start // 64 + 1) % 64 == 0:
print('dense task %d points %d/%d wall %.1fs' %
(index, min(start + 64, len(points)), len(points), time.perf_counter() - started), flush=True)
gaps = torch.cat(gaps)
hit = gaps <= 0
first = int(hit.nonzero()[0]) if hit.any() else None
rows.append({
'i': index, 'task': saved['tasks'][index], 'steps': nstep, 'sampled_points': len(points),
'initial_gap64_mm': float(gaps[0]), 'min_gap64_mm': float(gaps.min()),
'collision_segments': int(segments[hit & (segments >= 0)].unique().numel()),
'first_collision_s': max(0.0, float(segments[first] + alpha[first]) * DT) if first is not None else None,
})
print('dense task %d min_gap %.9f mm collision_segments %d' %
(index, rows[-1]['min_gap64_mm'], rows[-1]['collision_segments']), flush=True)
offset += count
result = {
'trace': str(path), 'tasks_sha256': saved.get('tasks_sha256'), 'kin_fp': kin.fp,
'indices': indices, 'max_step_deg': max_step_deg, 'dtype': 'float64', 'device': device,
'scope': 'Executed trajectory through each task deadline or arrival, including initial state; finite sampling, not continuous certification.',
'collision_tasks': sum(r['min_gap64_mm'] <= 0 for r in rows), 'rows': rows,
'audit_wall_s': time.perf_counter() - started,
}
if out:
with open(out, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, allow_nan=False)
return result
@torch.no_grad()
def replay_certificates(path, indices, device='cpu', out=None, repeats=5, extra_segments=()):
saved = torch.load(path, map_location='cpu', weights_only=True)
kin = Kin(device=device, dtype=torch.float64)
if saved.get('kin_fp') != kin.fp:
raise ValueError('trace/kin fingerprint mismatch')
if not indices or min(indices) < 0 or max(indices) >= len(saved['tasks']) or repeats < 1:
raise ValueError('valid task indices and positive repeats required')
rows, offset = [], 0
for _, per, count in saved['batches']:
for index in indices:
if not offset <= index < offset + count:
continue
b = index - offset
nstep = min(per['traj'].shape[1] - 1, math.floor((float(per['limit'][b]) + 1e-6) / DT))
if torch.isfinite(per['t_done'][b]):
nstep = min(nstep, round(float(per['t_done'][b]) / DT))
traj = per['traj'][b, :nstep + 1]
moving = ((traj[1:] - traj[:-1]).abs().max(-1).values > 1e-6).nonzero().flatten().tolist()
if not moving:
continue
plans = per['plans']
nearest, nearest_gap = None, float('inf')
for it in moving:
pi = int(torch.searchsorted(plans['it'], it, right=True)) - 1
step = it - int(plans['it'][pi])
if 0 <= step < plans['commit'].shape[2] and plans['certified'][b, pi, step]:
gap = float(plans['gaps'][b, pi, [step, plans['commit'].shape[2] + step]].min())
if gap < nearest_gap:
nearest, nearest_gap = it, gap
selected = {moving[0], moving[len(moving) // 2], moving[-1]}
if nearest is not None:
selected.add(nearest)
for task_index, it in extra_segments:
if task_index == index:
if it not in moving:
raise ValueError('extra segment must be a moving segment before deadline or arrival')
selected.add(it)
for it in sorted(selected):
rows.append({'i': index, 'start_it': it, 'a': traj[it].tolist(), 'b': traj[it + 1].tolist()})
offset += count
if not rows:
raise ValueError('no moving segments in selected tasks')
a = torch.tensor([r['a'] for r in rows], dtype=torch.float64, device=device)
b = torch.tensor([r['b'] for r in rows], dtype=torch.float64, device=device)
started = time.perf_counter()
print('certificate replay: %d tasks, %d fixed moving segments' % (len(indices), len(rows)), flush=True)
summaries = {}
for budget in (2, 8, 32):
result = kin.certify_segments(a, b, max_queries=budget)
summaries[str(budget)] = {key: int(result[key].sum()) for key in ('safe', 'collision', 'unresolved', 'outside_box')}
summaries[str(budget)]['queries_p50'] = float(result['queries'].double().quantile(0.5))
summaries[str(budget)]['queries_max'] = int(result['queries'].max())
for j, row in enumerate(rows):
row.setdefault('budgets', {})[str(budget)] = {key: value[j].item() for key, value in result.items()}
print('budget %d: %s' % (budget, summaries[str(budget)]), flush=True)
latencies = []
for j, row in enumerate(rows):
aa, bb = a[j:j + 1], b[j:j + 1]
discrete = kin.gaps(torch.cat([aa, aa + 0.5 * (bb - aa), bb]), 0.5)['gm'].min(-1).values
row['previous_midpoint_endpoint_gap_mm'] = discrete.tolist()
nseg = max(2, 2 * math.ceil(float((bb - aa).abs().max()) / 0.02))
alpha = torch.linspace(0, 1, nseg + 1, dtype=torch.float64, device=device)
points = aa + alpha[:, None] * (bb - aa)
dense = torch.cat([kin.gaps(chunk, 0.5)['gm'].min(-1).values for chunk in points.split(64)])
row['dense_points'] = len(points)
row['dense_min_gap_mm'] = float(dense.min())
row['dense_min_fraction'] = float(alpha[dense.argmin()])
for _ in range(3):
kin.certify_segments(aa, bb)
elapsed = []
for _ in range(repeats):
if a.is_cuda:
torch.cuda.synchronize(a.device)
t0 = time.perf_counter()
result = kin.certify_segments(aa, bb)
if a.is_cuda:
torch.cuda.synchronize(a.device)
elapsed.append((time.perf_counter() - t0) * 1000)
row['batch_single_match'] = all(value.item() == row['budgets']['32'][key] for key, value in result.items())
row['latency_ms'] = elapsed
latencies.extend(elapsed)
print('segment %d/%d task %d it %d dense %.6f mm queries %d latency %.2f ms' %
(j + 1, len(rows), row['i'], row['start_it'], row['dense_min_gap_mm'],
row['budgets']['32']['queries'], sum(elapsed) / len(elapsed)), flush=True)
elapsed = torch.tensor(latencies, dtype=torch.float64)
result = {
'trace': str(path), 'tasks_sha256': saved.get('tasks_sha256'), 'kin_fp': kin.fp,
'device': str(device), 'gpu': torch.cuda.get_device_name(a.device) if a.is_cuda else None,
'torch_version': str(torch.__version__), 'cpu_threads': torch.get_num_threads(), 'dtype': 'float64',
'indices': indices, 'extra_segments': list(extra_segments),
'selection': 'First/middle/last moving segment and minimum original certified gap per task; plus explicit extra_segments.',
'cap_mm': 20.0, 'eps_mm': 1e-4, 'max_depth': 12, 'dense_max_step_deg': 0.01,
'timing': {'scope': 'B=1 certificate only, resident inputs, 3 warmups per segment; synchronized wall time, not policy or scheduling latency.',
'repeats_per_segment': repeats, 'calls': len(latencies),
'p50_ms': float(elapsed.quantile(0.5)), 'p99_ms': float(elapsed.quantile(0.99)),
'max_ms': float(elapsed.max()), 'over_20ms': int((elapsed > 20).sum())},
'budgets': summaries,
'dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 for r in rows),
'accepted_dense_collision_segments': sum(r['dense_min_gap_mm'] <= 0 and r['budgets']['32']['safe'] for r in rows),
'batch_single_match': all(r['batch_single_match'] for r in rows),
'scope': 'Fixed recorded segments, not a new rollout. Dense replay is finite sampling; floating-point certification error is not formally bounded.',
'wall_s': time.perf_counter() - started, 'rows': rows,
}
if out:
with open(out, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, allow_nan=False)
return result
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt')
ap.add_argument('--trace')
ap.add_argument('--dense_indices', help='逗号分隔的任务下标;回放实际轨迹,不依赖原 col 标记')
ap.add_argument('--cert_indices', help='固定实际段的连续认证、加密对照和单段计时')
ap.add_argument('--cert_segments', default='', help='额外指定任务号:零基段号,逗号分隔')
ap.add_argument('--repeats', type=int, default=5)
ap.add_argument('--max_step_deg', type=float, default=0.05)
ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--split', default='val')
ap.add_argument('--n', type=int, default=512)
ap.add_argument('--bs', type=int, default=64)
ap.add_argument('--chunk', type=int, default=64, help='回放轨迹分块步数(显存小就调小)')
ap.add_argument('--K', type=int, default=8)
ap.add_argument('--cap', type=float, default=0.5)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
ap.add_argument('--out', default=None)
a = ap.parse_args()
if a.dense_indices and a.cert_indices:
ap.error('--dense_indices and --cert_indices are mutually exclusive')
if a.cert_segments and not a.cert_indices:
ap.error('--cert_segments requires --cert_indices')
if a.trace:
if a.cert_indices:
extra = [tuple(map(int, segment.split(':'))) for segment in a.cert_segments.split(',') if segment]
indices = [int(i) for i in a.cert_indices.split(',')]
if any(len(segment) != 2 or segment[0] not in indices for segment in extra):
ap.error('--cert_segments requires task:segment pairs from --cert_indices')
replay_certificates(a.trace, indices, a.device, a.out, a.repeats, extra)
elif a.dense_indices:
replay_dense(a.trace, [int(i) for i in a.dense_indices.split(',')], a.max_step_deg, a.device, a.out)
else:
replay_first_hits(a.trace, a.out)
return
if a.dense_indices or a.cert_indices:
ap.error('--dense_indices and --cert_indices require --trace')
if not a.ckpt:
ap.error('--ckpt or --trace is required')
ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device)
model.load_state_dict(ck['model'])
model.eval()
kin = Kin(device=a.device)
if ck.get('fp') and ck['fp'] != kin.fp:
sys.exit(f'checkpoint 指纹 {ck["fp"]} 与 kin.json {kin.fp} 不符')
tasks = [t for t in load_tasks([a.samples]) if t['split'] == a.split][:a.n]
PN = pair_names()
rows = []
tau = [0, 0.5, 1.0, 2.0, 3.0, 5.0, 1e9]
hist = Counter()
pair_first = Counter(); pair_steps = Counter()
with torch.no_grad():
for i in range(0, len(tasks), a.bs):
chunk = tasks[i:i + a.bs]
res, per = rollout(model, kin, chunk, a.device, K=a.K, safety=False, seed=i, return_traj=True)
traj = per['traj'].to(a.device) # [B,T,6]
B, Tn, _ = traj.shape
first_t = torch.full((B,), float('nan'), device=a.device)
first_pair = torch.full((B,), -1, dtype=torch.long, device=a.device)
first_gap = torch.full((B,), float('nan'), device=a.device)
min_gap = torch.full((B,), float('inf'), device=a.device)
col_steps = torch.zeros(B, device=a.device)
for k in range(Tn):
qk = traj[:, k]
mid = 0.5 * (traj[:, max(0, k - 1)] + qk)
pts = torch.stack([qk, mid], 1).reshape(2 * B, 6) # 每帧只喂 2B 个点(与 rollout 同量级;一次 2048 点会爆显存)
gm = kin.gaps(pts, a.cap)['gm'].view(B, 2, 49)
gmin, gpair = gm.min(-1) # [B,2]
g1 = gmin.min(1).values
min_gap = torch.minimum(min_gap, g1)
newly = (g1 <= 0) & torch.isnan(first_t)
if bool(newly.any()):
for b in newly.nonzero().flatten().tolist():
pos = 0 if float(gmin[b, 0]) <= 0 else 1
first_t[b] = k * DT
first_pair[b] = gpair[b, pos]
first_gap[b] = gmin[b, pos]
if not bool(torch.isnan(first_t).any()):
break
for b, t in enumerate(chunk):
ft = float(first_t[b])
fp = int(first_pair[b])
name = PN.get(fp, '—')
if not math.isnan(ft):
pair_first[name] += 1
for j in range(len(tau) - 1):
if tau[j] <= ft < tau[j + 1]:
hist[f'{tau[j]}–{tau[j + 1]} s' if tau[j + 1] < 1e8 else '> 5 s'] += 1
break
rows.append({
'tid': t.get('tid'), 'cat': t.get('cat'), 'mode': t.get('mode'),
'collided': bool(per['col'][b] > 0),
'first_collision_s': None if math.isnan(ft) else round(ft, 2),
'first_pair': None if name == '—' else name, 'first_pair_idx': None if fp < 0 else fp,
'first_gap_mm': None if math.isnan(float(first_gap[b])) else round(float(first_gap[b]), 3),
'min_gap_mm': round(float(min_gap[b]), 3),
'collision_steps': int(per['col'][b]),
})
print(f'[diag] {min(i + a.bs, len(tasks))}/{len(tasks)} 已回放', flush=True)
collided = [r for r in rows if r['collided']]
def pct(v, p):
v = sorted(v); return v[min(len(v) - 1, int(len(v) * p))] if v else None
times = [r['first_collision_s'] for r in collided]
out = {
'ckpt': a.ckpt, 'step': ck.get('step'), 'n': len(rows), 'n_collided': len(collided),
'collision_rate': len(collided) / max(1, len(rows)),
'first_time_s': {'p10': pct(times, .1), 'p50': pct(times, .5), 'p90': pct(times, .9)},
'first_time_hist': dict(hist),
'first_pair_top': pair_first.most_common(),
'min_gap_mm_p10/p50': [pct([r['min_gap_mm'] for r in rows], .1), pct([r['min_gap_mm'] for r in rows], .5)],
'by_cat': {},
'tasks': rows,
}
for k in sorted({f"{r['cat']}/{r['mode']}" for r in rows}):
g = [r for r in rows if f"{r['cat']}/{r['mode']}" == k]
out['by_cat'][k] = {'n': len(g), 'first_t_p50': pct([r['first_collision_s'] for r in g if r['collided']], .5),
'first_pairs': Counter([r['first_pair'] for r in g if r['first_pair']]).most_common(3)}
s = json.dumps(out, indent=1, ensure_ascii=False)
if a.out:
open(a.out + '.tmp', 'w', encoding='utf-8').write(s); os.replace(a.out + '.tmp', a.out)
print('== 汇总 ==')
print('题数 %d,撞过 %d(%.1f%%)' % (len(rows), len(collided), 100 * out['collision_rate']))
print('首次碰撞时间 p10/p50/p90 = %s / %s / %s s' % (
out['first_time_s']['p10'], out['first_time_s']['p50'], out['first_time_s']['p90']))
print('首次碰撞时间分布:', out['first_time_hist'])
print('首次碰撞的连杆对 Top:', pair_first.most_common(8))
print('全程最小间隙 p10/p50 = %s / %s mm' % tuple(out['min_gap_mm_p10/p50']))
if a.out: print('已写', a.out)
if __name__ == '__main__':
main()
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python
"""闭环末位误差诊断(只读,手动运行;不在流水线里)。
回答一个问题:策略"差一点没进 1 mm 门"还是"根本没朝目标走"。
输出每题:
- 最终位置误差 err_mm / 最终姿态误差 oerr_deg
- 全程最小位置误差 min_err_mm、最小综合误差时是否进过 5 mm & 5° 窗口(DLS 接管门槛)
- done(是否到达)、碰撞步数、用时
以及按 cat/mode 分组的汇总与分位数。
用法(仓库根目录):
PYTHONUTF8=1 python ml/eval/diag_tail.py --ckpt ml/runs/base/last.pt --n 512 --out ml/runs/diag_tail.json
"""
import argparse, json, math, os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import torch
def pct(v, p):
if not v:
return None
v = sorted(v)
return v[min(len(v) - 1, int(len(v) * p))]
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True)
ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--tasks', default=None, help='任务 jsonl(覆盖 --samples/--split)')
ap.add_argument('--split', default='val')
ap.add_argument('--n', type=int, default=512)
ap.add_argument('--bs', type=int, default=64, help='每批题数(显存小就调小)')
ap.add_argument('--K', type=int, default=8)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
ap.add_argument('--out', default=None)
a = ap.parse_args()
from ml.common.kin_torch import Kin, D2R
from ml.data import load_tasks
from ml.model.policy import build
from ml.sim.closed_loop import rollout, goal_err
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
kin = Kin(device=a.device)
if ck.get('fp') and ck['fp'] != kin.fp:
sys.exit('checkpoint 的碰撞模型指纹 %s 与当前 kin.json %s 不符' % (ck['fp'], kin.fp))
if a.tasks:
tasks = [json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()]
else:
tasks = [t for t in load_tasks([a.samples]) if t['split'] == a.split]
tasks = tasks[:a.n]
per_task = []
with torch.no_grad():
for i in range(0, len(tasks), a.bs):
chunk = tasks[i:i + a.bs]
res, per = rollout(model, kin, chunk, a.device, K=a.K, safety=False, seed=i, return_traj=True)
traj = per['traj'] # [B, T, 6]
B, Tn, _ = traj.shape
Tt = torch.tensor([t['T'] for t in chunk], dtype=torch.float32, device=a.device)
Rt = torch.tensor([t['R'] if t.get('R') else [0.] * 9 for t in chunk], dtype=torch.float32, device=a.device)
min_e = torch.full((B,), float('inf'), device=a.device)
min_o = torch.full((B,), float('inf'), device=a.device)
entered = torch.zeros(B, dtype=torch.bool, device=a.device) # 曾同时满足 e<5mm & (无姿态要求或 oe<5°)
step = 256
for t0 in range(0, Tn, step):
q = traj[:, t0:t0 + step].reshape(-1, 6)
n = q.shape[0] // B
e, oe = goal_err(kin, q, Tt.repeat_interleave(n, 0), Rt.repeat_interleave(n, 0))
e = e.view(B, n); oe = oe.view(B, n)
min_e = torch.minimum(min_e, e.min(1).values)
min_o = torch.minimum(min_o, oe.min(1).values)
pose = (Rt.abs().sum(-1) > 0)[:, None]
window = (e < 5) & (~pose | (oe < 5))
entered |= window.any(1)
done = per['done'].to(a.device); col = per['col'].to(a.device); td = per['t_done'].to(a.device)
end_e = per['err'].to(a.device)
# 末态姿态误差:traj 最后一帧
qend = traj[:, -1]
ee, eo = goal_err(kin, qend, Tt, Rt)
for j, t in enumerate(chunk):
per_task.append({
'tid': t.get('tid'), 'cat': t.get('cat'), 'mode': t.get('mode'),
'err_mm': float(end_e[j]), 'oerr_deg': float(eo[j]),
'min_err_mm': float(min_e[j]), 'min_oerr_deg': float(min_o[j]),
'entered_5mm5deg': bool(entered[j]),
'done': bool(done[j]), 'collision_steps': float(col[j]),
't_done_s': None if math.isnan(float(td[j])) else float(td[j]),
})
print('[diag] %d/%d done=%d entered=%d min_err<=5mm=%d' % (
min(i + a.bs, len(tasks)), len(tasks), int(done.sum()), int(entered.sum()),
sum(1 for p in per_task if p['min_err_mm'] <= 5)), flush=True)
def summ(rows):
if not rows:
return {'n': 0}
e = [r['err_mm'] for r in rows]; me = [r['min_err_mm'] for r in rows]
o = [r['oerr_deg'] for r in rows]
return {'n': len(rows),
'done': sum(r['done'] for r in rows),
'entered_5mm5deg': sum(r['entered_5mm5deg'] for r in rows),
'min_err_mm<=5': sum(1 for v in me if v <= 5),
'min_err_mm<=10': sum(1 for v in me if v <= 10),
'err_mm_p10/p50/p90': [pct(e, .1), pct(e, .5), pct(e, .9)],
'min_err_mm_p10/p50/p90': [pct(me, .1), pct(me, .5), pct(me, .9)],
'oerr_deg_p50/p90': [pct(o, .5), pct(o, .9)],
'collision_steps_mean': sum(r['collision_steps'] for r in rows) / len(rows)}
out = {'ckpt': a.ckpt, 'step': ck.get('step'), 'fp': ck.get('fp'), 'n': len(per_task),
'overall': summ(per_task),
'by_cat_mode': {k: summ([r for r in per_task if '%s/%s' % (r['cat'], r['mode']) == k])
for k in sorted({('%s/%s' % (r['cat'], r['mode'])) for r in per_task})},
'tasks': per_task}
s = json.dumps(out, indent=1, ensure_ascii=False)
print(s[:2000])
if a.out:
open(a.out + '.tmp', 'w', encoding='utf-8').write(s)
os.replace(a.out + '.tmp', a.out)
print('[diag] 已写', a.out)
if __name__ == '__main__':
main()
+142
View File
@@ -0,0 +1,142 @@
"""小规模同步闭环评估;仿真墙钟不等于控制延迟。"""
import argparse, functools, hashlib, json, math, time
from pathlib import Path
import torch
from ml.common.ckpt import load_policy
from ml.common.kin_torch import Kin
from ml.data import load_tasks
from ml.sim.closed_loop import rollout, tasks_to_tensors
import ml.sim.closed_loop as clmod
def run(model, kin, tasks, device, bs=256, **kw):
per = []
for i in range(0, len(tasks), bs):
t0 = time.perf_counter()
print('batch %d-%d/%d start' % (i + 1, min(i + bs, len(tasks)), len(tasks)), flush=True)
def progress(step, total):
print('batch %d step %d/%d wall %.1fs' % (i // bs + 1, step, total, time.perf_counter() - t0), flush=True)
r, p = rollout(model, kin, tasks[i:i + bs], device, progress=progress, **kw)
per.append((r, p, len(tasks[i:i + bs])))
print('batch %d done wall %.1fs' % (i // bs + 1, time.perf_counter() - t0), flush=True)
flat = {k: torch.cat([p[k] for _, p, _ in per]) for k in per[0][1] if k not in ('traj', 'plans')}
_, _, _, ex, tr = tasks_to_tensors(tasks, 'cpu')
ok, col = flat['ok'], flat['col'] > 0
has_ref = torch.isfinite(ex)
okr = ok & has_ref & torch.isfinite(tr)
median = lambda x: x.median().item() if x.numel() else None
out = {
'n': len(tasks), 'n_ref': int(has_ref.sum()),
'success': ok.float().mean().item(),
'collision_free_success': (ok & ~col).float().mean().item(),
'time_ok_1p3': (ok & has_ref & (flat['t_done'] <= 1.3 * ex)).sum().item() / int(has_ref.sum()) if has_ref.any() else None,
'collision_rate': col.float().mean().item(),
'collision_steps_mean': flat['col'].mean().item(),
'travel_ratio_median': median(flat['travel'][okr] / tr[okr].clamp(min=1)),
'time_mean_s': flat['t_done'][ok].mean().item() if ok.any() else None,
'brakes_mean': flat['brakes'].mean().item(),
'start_fail': flat['start_fail'].float().mean().item(),
'goal_ratio_2s': median(flat['goal_ratio_2s']),
'err_fail_med': median(flat['err'][~ok]),
'err_success_med': median(flat['err'][ok]),
'dls_share': flat['dls_it'].ge(0).float().mean().item(),
'time_budget': ('fixed %.3f s' % kw['max_sec']) if kw.get('max_sec') is not None else
(per[0][0]['time_budget'] if len({r['time_budget'] for r, _, _ in per}) == 1 else 'mixed'),
}
if 'speed_ratio' in flat: # 实际执行速度:每题时限内 max|Δq|/STEP(1.0 = VMAX),加速度同口径按每控制步的速度变化
v, acc = flat['speed_ratio'], flat['accel_ratio']
out.update({'speed_ratio_p50': median(v), 'speed_ratio_p95': v.quantile(0.95).item(),
'speed_ratio_max': v.max().item(), 'over_vmax_rate': (v > 1.001).float().mean().item(),
'accel_ratio_p95': acc.quantile(0.95).item(), 'accel_ratio_max': acc.max().item()})
return out, per
def json_finite(value):
if isinstance(value, dict): return {k: json_finite(v) for k, v in value.items()}
if isinstance(value, list): return [json_finite(v) for v in value]
if isinstance(value, float) and not math.isfinite(value): return None
return value
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', default='ml/runs/base_dag2_dag1/best.pt')
ap.add_argument('--samples', default='ml/data/samples')
ap.add_argument('--tasks', default=None)
ap.add_argument('--split', default='val')
ap.add_argument('--n', type=int, default=256)
ap.add_argument('--K', type=int, default=8)
ap.add_argument('--commit', type=int, default=3)
ap.add_argument('--fb', type=float, default=1.8)
ap.add_argument('--ki', type=float, default=0.0)
ap.add_argument('--fb_scale', type=float, default=0.0)
ap.add_argument('--fb_near', type=float, default=0.2)
ap.add_argument('--fb_fallback', type=float, default=0.0)
ap.add_argument('--kdecay', type=float, default=0.9)
ap.add_argument('--kimax', type=float, default=0.5)
ap.add_argument('--fb_every', type=int, default=1)
ap.add_argument('--safety_try', type=int, default=0)
ap.add_argument('--sel_w', type=float, default=0.1)
ap.add_argument('--dls_safety', type=int, default=0)
ap.add_argument('--fb_clear', type=float, default=0.0)
ap.add_argument('--fb_cmin', type=float, default=0.3)
ap.add_argument('--sub_refresh', type=int, default=0)
ap.add_argument('--fb_block_gain', type=float, default=1.0)
ap.add_argument('--dump', default=None)
ap.add_argument('--out', default=None)
ap.add_argument('--trace', default=None)
ap.add_argument('--select', default='consist')
ap.add_argument('--replan', default='r3')
ap.add_argument('--safety', type=int, default=1, help='1=安全层+部分前缀;0=关')
ap.add_argument('--partial', type=int, default=1)
ap.add_argument('--inc_cap', type=int, default=0, help='1=反馈叠加后再按每轴 VMAX·DT 限幅')
ap.add_argument('--gap_margin', type=float, default=0.0, help='安全层要求的间隙余量 mm(>0 时宽相位 cap 自动抬到 margin+0.5)')
ap.add_argument('--cert_batch', type=int, default=0, help='1=一次 gaps 调用覆盖全部候选(压最坏情况,典型路径略贵)')
ap.add_argument('--max_sec', type=float, default=None)
ap.add_argument('--seed', type=int, default=0)
ap.add_argument('--dls', default='analytic', choices=['analytic', 'fd'])
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
print('config ' + json.dumps(vars(a), ensure_ascii=False), flush=True)
if a.dls == 'fd':
clmod.dls_steps = functools.partial(clmod.dls_steps, analytic=False)
model, ck = load_policy(a.ckpt, a.device)
kin = Kin(device=a.device)
assert ck.get('fp') in (None, kin.fp)
tasks = ([json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()] if a.tasks
else [t for t in load_tasks([a.samples]) if t['split'] == a.split])
tasks = tasks[:a.n]
if not tasks: ap.error('no tasks selected')
meta = {
'config': vars(a), 'batch_size': 256, 'batch_seed_rule': 'same seed per batch',
'checkpoint_sha256': hashlib.sha256(Path(a.ckpt).read_bytes()).hexdigest(),
'tasks_sha256': hashlib.sha256(json.dumps(tasks, sort_keys=True).encode()).hexdigest(),
'kin_fp': kin.fp, 'act_scale': float(model.act_scale),
}
t0 = time.perf_counter()
r, pers = run(model, kin, tasks, a.device, K=a.K, safety=bool(a.safety), seed=a.seed, max_sec=a.max_sec,
safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit,
select=a.select, replan=a.replan, fb_gain=a.fb, fb_scale=a.fb_scale, fb_near=a.fb_near, fb_fallback=a.fb_fallback, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax,
fb_every=a.fb_every, safety_try=a.safety_try, sel_w=a.sel_w, dls_safety=a.dls_safety, fb_clear=a.fb_clear, fb_cmin=a.fb_cmin, sub_refresh=a.sub_refresh, fb_block_gain=a.fb_block_gain,
inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, return_traj=bool(a.trace))
wall = time.perf_counter() - t0
result = {**meta, 'metrics': r, 'simulation_wall_s': wall}
print(json.dumps(json_finite(result), ensure_ascii=False, allow_nan=False), flush=True)
if a.out:
Path(a.out).write_text(json.dumps(json_finite(result), indent=2, ensure_ascii=False, allow_nan=False) + '\n', encoding='utf-8')
if a.dump:
rows = []
keys = ['done', 'ok', 't_done', 'limit', 'col', 'brakes', 'err', 'first_hit', 'dls_it', 'goal_ratio_2s']
flat = {k: torch.cat([p[k] for _, p, _ in pers]).tolist() for k in keys}
for i, t in enumerate(tasks):
rows.append({**t, 'i': i, **{k: flat[k][i] for k in keys}})
Path(a.dump).write_text(json.dumps(json_finite(rows), allow_nan=False), encoding='utf-8')
print('dump', a.dump, flush=True)
if a.trace:
torch.save({**meta, 'tasks': tasks, 'batches': pers}, a.trace)
print('trace', a.trace, flush=True)
if __name__ == '__main__':
main()
+144
View File
@@ -0,0 +1,144 @@
"""老师轨迹回放自检(train 阶段开训前自动跑,约 1–3 分钟):
python -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json
两部分,都过才 pass(pipeline.sh 读 pass 字段与指纹,不过就停 rc 5):
1. 运动学口径(2026-10-18):gen 写出的老师 50 Hz 轨迹(keep=true、cat≠D)直接放进 PyTorch 运动学 / 碰撞 / 到达判据:
到达率 ≥ 0.95、碰撞率 ≤ 0.01。验证的是「仿真口径(kin.json、间隙、到达门槛)与 JS 认证过的数据一致」。
2. 滚动口径(2026-10-19,审查 review_closedloop F1/D1、review_data_actions C1):上面那条完全不经过 rollout(),
队列 / 前缀 / 提交 3 步 / qe / 标签 clamp / DLS / done / 时限上的错位它都发现不了。这里让老师当策略(closed_loop.TeacherOracle:
每次规划从当前时间下标取老师 16 步增量,按标签口径 clamp)原样走 rollout()(select='first'、use_sub=False):
到达(时限内)≥ 0.95、碰撞 ≤ 0.01、DLS 接管前与老师轨迹的最大关节偏差 p99 ≤ 0.1°(shard 轨迹按 1e-3° 取整,标签 clamp 会截掉取整的余量)。
输出另带 fp(当前 kin.json)、data_fp(shard 里记录的碰撞模型指纹)、ver=2;pipeline 三者都核对。
load_teacher / pad_traj / oracle_raw / oracle_summary 供 ml/eval/diag_cl.py 的 H1 段复用。"""
import argparse, glob, json, math, os, sys
import torch
from ml.common.kin_torch import Kin
from ml.sim.closed_loop import goal_err, rollout, TeacherOracle, DT
DEV_TOL_DEG = 0.1
def load_teacher(shards, ids):
"""按任务 id 从 shard jsonl 取老师记录(traj、path、exec_s…)。只解析 id 命中的行。"""
want, out = set(ids), {}
for f in sorted(glob.glob(shards)):
for line in open(f, encoding='utf-8'):
k = line.find('"id":"')
if k < 0: continue
rid = line[k + 6:line.find('"', k + 6)]
if rid in want and rid not in out:
r = json.loads(line)
if r.get('traj'): out[rid] = r
if len(out) == len(want): break
return out
def pad_traj(trajs, device, extra=20):
"""[B,Tmax+extra,6]:各题老师轨迹末帧重复补齐(oracle 在轨迹末尾之后给零增量)。"""
Tm = max(len(t) for t in trajs) + extra
out = torch.zeros(len(trajs), Tm, 6, device=device)
for i, t in enumerate(trajs):
x = torch.tensor(t, dtype=torch.float32, device=device)
out[i, :len(t)] = x; out[i, len(t):] = x[-1]
return out
def oracle_raw(per, TR, lens, tasks):
"""oracle 滚动的逐题量:DLS 接管前(且到达前、老师轨迹结束前)与老师的最大关节偏差 mdev(度)、时限内到达 ok、
时限内碰撞 col、是否计入到达统计(cat≠D)、完成时刻 − 老师时长 dt(s)、是否用过 DLS、2 s 目标误差比。"""
tr = per['traj'].to(TR.device); B, Tn, _ = tr.shape
L = min(Tn, TR.shape[1])
dev = (tr[:, :L] - TR[:, :L]).abs().max(-1).values # [B,L]
k = torch.arange(L, device=TR.device)[None]
kd = torch.tensor(lens, device=TR.device)[:, None] - 1
dls = per['dls_it'].to(TR.device)[:, None]
td = per['t_done'].to(TR.device)
kdone = torch.where(torch.isfinite(td), (td / DT).round(), torch.full_like(td, 1e9)).long()[:, None]
valid = (k <= kd) & ((dls < 0) | (k <= dls)) & (k <= kdone)
ex = torch.tensor([t.get('exec_s') or float('nan') for t in tasks])
return {'mdev': torch.where(valid, dev, torch.zeros_like(dev)).max(-1).values.cpu(), 'ok': per['ok'], 'col': per['col'] > 0,
'reach_m': torch.tensor([t.get('cat') != 'D' for t in tasks]), 'dt': per['t_done'] - ex,
'dls': per['dls_it'] >= 0, 'r2': per['goal_ratio_2s']}
def _q(v, p):
v = v[torch.isfinite(v)]
return float(v.kthvalue(max(1, min(len(v), int(math.ceil(p * len(v)))))).values) if len(v) else None
def oracle_summary(raws):
c = {k: torch.cat([r[k] for r in raws]) for k in raws[0]}
m = c['reach_m']; ok = c['ok']
dt = c['dt'][ok & m]
return {'n': int(len(m)), 'n_reach_tasks': int(m.sum()),
'reach': float(ok[m].float().mean()) if m.any() else None,
'collision': float(c['col'][m].float().mean()) if m.any() else None,
'D': {'n': int((~m).sum()), 'collision': float(c['col'][~m].float().mean()) if (~m).any() else None},
'max_dev_deg_before_dls': {'p50': _q(c['mdev'], .5), 'p99': _q(c['mdev'], .99), 'max': float(c['mdev'].max())},
'dls_used': float(c['dls'].float().mean()),
'finish_minus_teacher_s': {'p50': _q(dt, .5), 'p90': _q(dt, .9), 'max': float(dt.max()) if len(dt) else None},
'goal_ratio_2s_p50': _q(c['r2'], .5)}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--shards', default='ml/data/shard_*.jsonl'); ap.add_argument('--n', type=int, default=512)
ap.add_argument('--samples', default='ml/data/samples', help='读打包 meta 的 act_scale(oracle 按同一标签口径 clamp);没有就按 1.0')
ap.add_argument('--bs', type=int, default=128)
ap.add_argument('--out', default='ml/runs/replay_teacher.json')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
a = ap.parse_args()
kin = Kin(device=a.device)
recs = []
for f in sorted(glob.glob(a.shards)):
for line in open(f, encoding='utf-8'):
r = json.loads(line)
if r.get('keep') and r.get('traj') and r.get('cat') != 'D': recs.append(r)
if len(recs) >= a.n: break
if len(recs) >= a.n: break
if not recs: sys.exit('没有可回放的老师轨迹(先跑 gen)')
# ---- 1. 运动学口径
reach = col = 0; worst_gap = []
for r in recs:
q = torch.tensor(r['traj'], dtype=torch.float32, device=a.device)
mid = 0.5 * (q[1:] + q[:-1])
g = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
hit = bool((g <= 0).any()); col += hit; worst_gap.append(g.min().item())
T = torch.tensor([r['T']], dtype=torch.float32, device=a.device)
R = torch.tensor([r['R'] if r.get('R') else [0.] * 9], dtype=torch.float32, device=a.device)
e, oe = goal_err(kin, q[-1:], T, R)
reach += bool((e < 1).item() and (oe < 0.5).item())
n = len(recs)
kin_ok = reach / n >= 0.95 and col / n <= 0.01
# ---- 2. 滚动口径:老师 oracle 走完整 rollout()
try:
from ml.data import read_meta
act_scale = float(read_meta(a.samples).get('act_scale', 1.0)) if os.path.isdir(a.samples) else 1.0
except SystemExit:
act_scale = 1.0
tasks = [{'q0': r['q0'], 'T': r['T'], 'R': r.get('R'), 'exec_s': r.get('exec_s') or (len(r['traj']) - 1) * DT,
'travel': r.get('travel_deg'), 'cat': r.get('cat')} for r in recs]
raws = []
for i in range(0, n, a.bs):
tb, rb = tasks[i:i + a.bs], recs[i:i + a.bs]
TR = pad_traj([r['traj'] for r in rb], a.device)
_, per = rollout(None, kin, tb, a.device, K=1, select='first', use_sub=False, return_traj=True, seed=i,
oracle=TeacherOracle(TR, act_scale=act_scale))
raws.append(oracle_raw(per, TR, [len(r['traj']) for r in rb], tb))
orc = oracle_summary(raws); orc['act_scale'] = act_scale
orc_ok = (orc['reach'] or 0) >= 0.95 and (orc['collision'] or 0) <= 0.01 and orc['max_dev_deg_before_dls']['p99'] <= DEV_TOL_DEG
data_fps = {r.get('fp') for r in recs}
data_fp = data_fps.pop() if len(data_fps) == 1 else 'mixed'
res = {'ver': 2, 'n': n, 'reach': reach / n, 'collision': col / n, 'min_gap_mm_p10': sorted(worst_gap)[n // 10], 'kin_pass': kin_ok,
'oracle': orc, 'oracle_pass': orc_ok, 'fp': kin.fp, 'data_fp': data_fp,
'pass': bool(kin_ok and orc_ok and data_fp in (None, kin.fp))}
os.makedirs(os.path.dirname(a.out) or '.', exist_ok=True)
json.dump(res, open(a.out, 'w', encoding='utf-8'), indent=1, ensure_ascii=False)
print('老师轨迹回放:到达 %.1f%%、碰撞 %.1f%%(%d 条);oracle 走 rollout:到达 %.1f%%、碰撞 %.1f%%、DLS 前最大偏差 p99 %.4f°;指纹 %s' % (
100 * res['reach'], 100 * res['collision'], n, 100 * orc['reach'], 100 * orc['collision'],
orc['max_dev_deg_before_dls']['p99'], '一致' if data_fp in (None, kin.fp) else '不符'), flush=True)
sys.exit(0 if res['pass'] else 5)
if __name__ == '__main__':
main()
+6 -4
View File
@@ -1,7 +1,7 @@
"""导出部署包(方案 §8.4):
python -m ml.export_onnx --ckpt ml/runs/current.pt --out webapp/models/traj_policy.onnx [--fp16]
输出 traj_policy.onnx(输入 feat[1 或 K,83]、z[K,16,6];输出 act[K,16,6] 归一化增量、value[K]、sub[K,13])
与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once)。导出后用 onnxruntime 与 PyTorch 对拍。
与 traj_policy.meta.json(碰撞模型指纹、特征版本、维度、enc_once、act_scale)。导出后用 onnxruntime 与 PyTorch 对拍。
2026-10-18 第五轮:改为"编码一次、解码 K 次"(Policy.sample_shared),meta.enc_once=true;运行时据此只送 1 行特征。
2026-10-18 第六轮:feat 轴改为动态(1 行 = 主推理编码一次;K 行 = 前瞻逐行编码),meta.feat_dyn=true。
第五轮的 enc_once 模型 feat 固定 1 行,运行时前瞻只能逐行推(ik-policy.js ortInfer 已兼容),重新导出即可。
@@ -32,8 +32,8 @@ def main():
torch.manual_seed(0); ck = {'cfg': json.load(open(a.random)), 'random_init': True}
m = build(ck['cfg']); m.eval()
elif a.ckpt:
ck = torch.load(a.ckpt, map_location='cpu')
m = build(ck['cfg']); m.load_state_dict(ck['model']); m.eval()
from ml.common.ckpt import load_policy # 2026-10-19:导出 EMA 权重(审查 P1-5:原来导的是原始权重)
m, ck = load_policy(a.ckpt, 'cpu')
else: raise SystemExit('需要 --ckpt 或 --random <配置>')
kin = Kin()
if ck.get('fp') and ck['fp'] != kin.fp: raise SystemExit(f"checkpoint 指纹 {ck['fp']} ≠ 当前碰撞模型 {kin.fp}:须重训")
@@ -62,8 +62,10 @@ def main():
err_rows = max(float(np.abs(o8[i] - r8[i].numpy()).max()) for i in range(3))
assert err_rows < 1e-3, f'feat 为 K 行时与 sample 不一致:{err_rows}'
err = max(err, err_rows)
# 2026-10-19:act_scale = 动作标签归一化倍数,运行时 inc = clamp(act, ±1)·act_scale·STEP(ik-policy.js Runner 的 actScale 读它)
meta = {'fp': kin.fp, 'tcp0': kin.tcp0.tolist(), 'head': m.head, 'feat_ver': FEAT_VER, 'f_dim': F_DIM, 'h': H, 'step': STEP, 'fp16': a.fp16,
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'feat_dyn': True, 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err}
'ckpt': os.path.abspath(a.ckpt) if a.ckpt else None, 'random_init': bool(a.random), 'enc_once': True, 'feat_dyn': True,
'act_scale': float(ck.get('act_scale', 1.0)), 'eval': ck.get('eval'), 'max_abs_err_vs_torch': err}
json.dump(meta, open(a.out.replace('.onnx', '.meta.json'), 'w'), indent=1)
print(json.dumps({'onnx': a.out, 'MB': os.path.getsize(a.out) / 1e6, 'max_abs_err_vs_torch': err}))
+9 -3
View File
@@ -5,7 +5,9 @@
* 每行一条任务:采样 → 老师 → 距离下界认证过滤 → 局部时间律(50 Hz)→ 写出。
* 认证不过的样本也写出(keep=false + 原因),供 stats 统计丢弃率;训练只用 keep=true。
* 2026-10-18 断点续写:第 i 条任务的随机数只由 (seed, i) 决定(sampler.reseed);输出文件已存在时保留其中完整的行、
* 截掉写了一半的最后一行,从下一条接着写 —— 生成几小时被中断,重跑只补剩下的,不重来、不重复。 */
* 截掉写了一半的最后一行,从下一条接着写 —— 生成几小时被中断,重跑只补剩下的,不重来、不重复。
* 2026-10-19:--tasks 的任务带 qPrev(DAgger 采集时策略的上一帧)时,记录 qPrev 与起步速度 v0(timelaw.initSpeed),
* 时间律从 v0 起步;build_dataset.cjs 用它们做 t=0 样本的 dq 特征与标签。 */
'use strict';
const fs = require('fs'), path = require('path');
const args = {}; for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
@@ -19,7 +21,7 @@ const K_CONS = +args.consensus || 0;
const GIVEN = args.tasks ? fs.readFileSync(args.tasks, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l)) : null;
const NT = GIVEN ? GIVEN.length : N;
const cert = require('./cert.cjs')(S, {});
const { timeParam } = require('./timelaw.cjs');
const { timeParam, initSpeed } = require('./timelaw.cjs');
const fp = S.modelFingerprint ? S.modelFingerprint() : null;
fs.mkdirSync(path.dirname(OUT), { recursive: true });
let done = 0;
@@ -63,7 +65,11 @@ for (let i = done; i < NT; i++) {
if (valid >= 2 && agree * 2 < valid) { rec.keep = false; rec.why = 'consensus'; }
}
if (t.poseRelaxed && task.mode === 'pose') { rec.mode = 'pos'; rec.R = null; rec.demoted = true; rec.reached = t.err < 1; }
if (rec.keep) { const tp = timeParam(t.pts); rec.traj = tp.traj.map(q => q.map(r3)); rec.exec_s = r3(tp.steps * 0.02); }
if (rec.keep && task.qPrev) {
rec.qPrev = task.qPrev.map(r3);
rec.v0 = r3(initSpeed(t.pts, task.qPrev, S.wrap180, S.ORDER.map(id => !!S.LIM[id].cont)));
}
if (rec.keep) { const tp = timeParam(t.pts, { v0: rec.v0 || 0 }); rec.traj = tp.traj.map(q => q.map(r3)); rec.exec_s = r3(tp.steps * 0.02); }
fs.writeSync(fd, JSON.stringify(rec) + '\n');
if ((i + 1) % 25 === 0) process.stderr.write(`[seed ${SEED}] ${i + 1}/${NT} ${((performance.now() - t00) / 1000 / (i + 1 - done)).toFixed(2)} s/条\n`);
}
+20 -3
View File
@@ -29,10 +29,27 @@ while :; do
echo "[gen 节拍] $(( (tl-t0)/60 )) 分钟 运行中 $alive/${#pids[@]} 进程 本批已写 $n 条 $last"
fi
done
fail=0
# 判定按产物而不是 wait 的退出码:2026-10-18 实测 28 个种子跑了 5 小时后全部 wait 报失败,
# 但每个 .part 都是完整的 1786 行(node 打了 done、mv 却没执行)——Git Bash 长跑后 wait 的返回码不可靠。
# 短产物分两种:网页点「停止」把 node 杀掉(.part 是断点,续跑接着写,不是失败)和 node 自己报错。
# 只有日志尾部有报错特征才算失败;143 让网页显示「已停止」而不是「失败(返回码 6)」(monitor.html:508 同一口径)。
crashed=0; stopped=0; sn=0
for k in "${!pids[@]}"; do
if ! wait ${pids[$k]}; then echo "[gen] 种子 ${seeds[$k]} 失败,最后几行:"; tail -n 5 ml/data/log_${seeds[$k]}.txt || true; fail=1; fi
s=${seeds[$k]}; f=ml/data/shard_$(printf %04d $s).jsonl
w=0; wait ${pids[$k]} || w=$? # 记下来但不当判据:长跑后 Git Bash 的 wait 返回码不可靠
if [ -f "$f" ]; then continue; fi
n=0; [ -f "$f.part" ] && n=$(wc -l < "$f.part")
if [ "$n" -ge "$N" ]; then mv "$f.part" "$f" && echo "[gen] 种子 $s 产物完整($n 行),补改名" && continue; fi
if tail -n 4 ml/data/log_$s.txt 2>/dev/null | grep -qE '^(Error|.*Error:|.*SyntaxError|.*heap out of memory|Traceback|.*throw )'; then
echo "[gen] 种子 $s 失败:.part 只有 $n/$N 行,最后几行:"; tail -n 5 ml/data/log_$s.txt || true
crashed=1
else
echo "[gen] 种子 $s 已停止:写好 $n/$N 条,断点保留在 $f.part,点「继续训练」从第 $((n + 1)) 条接着写(子进程返回码 $w)"
stopped=1; sn=$((sn + n))
fi
done
[ $fail = 0 ] || exit 6
[ $crashed = 0 ] || exit 6
# 全被停止时不跑 stats.cjs:分片还没改名,统计口径没变,重算一遍只会让「补量」看起来原地不动
[ $stopped = 0 ] || { echo "[gen] 本轮是被停止的,没有报错:$stopped 个种子共保住 $sn 条,续跑不重复生成"; exit 143; }
node ml/gen/stats.cjs ml/data/shard_*.jsonl > ml/data/stats.json
cat ml/data/stats.json
+20 -3
View File
@@ -1,7 +1,10 @@
/* 局部可观测时间律(2026-10-17,方案 §4.5)
* 折线按"巡航时间" τ 参数化:每段 τ = max_j |Δq_j| / VMAX_j(各轴同步、主导轴满速)。
* 归一化速度 v∈[0,1](1 = 巡航):v ← min(1, v + A·dt, sqrt(2·A·剩余 τ)),只依赖当前速度与剩余路程。
* 输出 50 Hz(dt = 20 ms)状态序列,末点精确落在终点。 */
* 输出 50 Hz(dt = 20 ms)状态序列,末点精确落在终点。
* 2026-10-19(审查 review_closedloop F7 / review_data_actions C10):opt.v0 = 起步归一化速度(默认 0 = 静止起步)。
* DAgger 重新标注的起点来自策略闭环、本来就在运动,原来一律从静止加速,标签与策略真正到达的 (q, dq) 状态不符。
* initSpeed(P, qPrev) 把当前速度投影到首段方向上得到 v0(gen_dataset.cjs 用)。 */
'use strict';
const VMAX = [60, 60, 90, 90, 120, 180]; // °/s,与 tests/route_bench.cjs 同(轴速比 0.6/0.6/0.9/0.9/1.2/1.8)
const A = 5.0; // 1/s²:0→巡航约 0.2 s(a_max 待真机参数)
@@ -19,7 +22,7 @@ function timeParam(P, opt) {
const u = segT[i] > 0 ? (tau - cum[i]) / segT[i] : 1;
return P[i].map((v, j) => v + (P[i + 1][j] - v) * u);
};
const out = [P[0].slice()], taus = [0]; let tau = 0, v = 0, guard = 0;
const out = [P[0].slice()], taus = [0]; let tau = 0, v = Math.max(0, Math.min(1, opt.v0 || 0)), guard = 0;
while (tau < total - 1e-9 && guard++ < 100000) {
const rem = total - tau;
const v1 = Math.min(1, v + a * dt, Math.sqrt(2 * a * Math.max(0, rem - 0.5 * v * dt)) + 1e-9);
@@ -30,4 +33,18 @@ function timeParam(P, opt) {
}
return { traj: out, taus, cum, total, steps: out.length - 1 };
}
module.exports = { timeParam, VMAX, A, DT };
/* 起步速度:w_j = 当前每步增量 / STEP_j(qPrev → P[0]),u_j = 巡航时首段每步增量 / STEP_j(max|u_j| = 1),
v0 = clamp(⟨w,u⟩/⟨u,u⟩, 0, 1)——当前速度在首段方向上的分量;反向或首段为零时 0。wrap:连续轴差值的最短弧函数(可缺省)。 */
function initSpeed(P, qPrev, wrap, cont) {
if (!qPrev || !P || P.length < 2) return 0;
const dd = (a, b, j) => (wrap && cont && cont[j]) ? wrap(a - b) : a - b;
let seg = 0; for (let j = 0; j < 6; j++) seg = Math.max(seg, Math.abs(P[1][j] - P[0][j]) / VMAX[j]);
if (seg < 1e-9) return 0;
let wu = 0, uu = 0;
for (let j = 0; j < 6; j++) {
const u = (P[1][j] - P[0][j]) / VMAX[j] / seg, w = dd(P[0][j], qPrev[j], j) / (VMAX[j] * DT);
wu += w * u; uu += u * u;
}
return Math.max(0, Math.min(1, wu / uu));
}
module.exports = { timeParam, initSpeed, VMAX, A, DT };
+2 -2
View File
@@ -12,8 +12,8 @@ from ml.sim.closed_loop import evaluate
def load(path, dev):
ck = torch.load(path, map_location=dev)
m = build(ck['cfg']).to(dev); m.load_state_dict(ck['model']); return m.eval()
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
return load_policy(path, dev)[0]
def per_task_success(pers):
+4 -3
View File
@@ -12,13 +12,14 @@ ROUNDS=${1:-6}; STEPS=${2:-1000}; N=${3:-50000}
mkdir -p ml/runs/mine
[ -f ml/runs/gate_tasks.jsonl ] || $PY -m ml.mining.mine_round --make-gate ${MINE_GATE_N:-2000} --seed 777 --out ml/runs
[ -f ml/runs/current.pt ] || cp ml/runs/apg/best.pt ml/runs/current.pt
SAMPLES="ml/data/samples"; FIXED=""; small=0
BASE_S="ml/data/samples $(ls -d ml/data/samples_dag_* 2>/dev/null | tr '\n' ' ')" # 2026-10-18:BC 正则也用 DAgger 样本
SAMPLES="$BASE_S"; FIXED=""; small=0
for ((r=1; r<=ROUNDS; r++)); do
D=ml/runs/mine/r$r; mkdir -p $D
if [ -f $D/gate.json ]; then
gain=$($PY -c "import json;print(json.load(open('$D/gate.json'))['gain_pp'])")
echo "round $r 已完成(gain ${gain} pp),跳过"; FIXED="--fixed $D/gate_fixed.jsonl"
[ -s ml/data/e2/shard_e2_r$r.jsonl ] && SAMPLES="ml/data/samples ml/data/samples_e2"
[ -s ml/data/e2/shard_e2_r$r.jsonl ] && SAMPLES="$BASE_S ml/data/samples_e2"
if $PY -c "import sys;sys.exit(0 if $gain < 0.5 else 1)"; then small=$((small+1)); else small=0; fi
if [ $small -ge 2 ]; then echo "连续 2 轮提升 < 0.5 pp,停止"; break; fi
continue
@@ -35,7 +36,7 @@ for ((r=1; r<=ROUNDS; r++)); do
if [ -s $D/e2_tasks.jsonl ]; then
[ -f $E2F ] || { node ml/gen/gen_dataset.cjs --tasks $D/e2_tasks.jsonl --seed $((5000+r)) --out $E2F.part && mv $E2F.part $E2F; }
node ml/prep/build_dataset.cjs ml/data/e2/shard_e2_r$r.jsonl ml/data/samples_e2
SAMPLES="ml/data/samples ml/data/samples_e2"
SAMPLES="$BASE_S ml/data/samples_e2"
fi
res=""; [ -f ml/runs/apg_r$r/last.pt ] && res="--resume ml/runs/apg_r$r/last.pt"
$PY -m ml.apg.apg_finetune --init ml/runs/current.pt --samples $SAMPLES --pool $D/pool.jsonl --out ml/runs/apg_r$r --steps $STEPS $res
+2 -2
View File
@@ -28,8 +28,8 @@ def main():
with open(os.path.join(a.out, a.name), 'w') as f:
for t in ts: f.write(json.dumps(t) + '\n')
print('gate tasks', len(ts)); return
ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
from ml.common.ckpt import load_policy # 2026-10-19:EMA 权重 + act_scale
model, ck = load_policy(a.ckpt, a.device)
tasks = sample_tasks(kin, a.n, a.seed)
g = torch.Generator(); g.manual_seed(a.seed)
cls = {k: [] for k in ('E1', 'E2', 'E3', 'E4', 'E5', 'ok')}
+22 -5
View File
@@ -1,22 +1,36 @@
"""MeanFlow 损失(Geng et al. 2025;MP1 配方:25% 样本 r≠t,自适应损失加权)。
约定:t=0 为数据、t=1 为噪声;z_t = (1−t)·x + t·ε,瞬时速度 v = ε − x;网络输出平均速度 u(z,r,t)。
目标 u_tgt = v − (t − r)·du/dt,du/dt = v·∂_z u + ∂_t u 用 torch.func.jvp 一次前向得到(切向量 (v, 0, 1))。"""
目标 u_tgt = v − (t − r)·du/dt,du/dt = v·∂_z u + ∂_t u 用 torch.func.jvp 一次前向得到(切向量 (v, 0, 1))。
2026-10-18 首次正式预训练后改:
- 整个 MF 分支(解码器前向 + JVP)在 fp32 下算:目标里含网络自己的导数,bf16 下 JVP 误差直接变成目标噪声;
- return_aux=True 时返回 (z, t, r, u),训练用 r==t 且 t 小的样本做"配对去噪估计" x̂ = z − t·u 来加 FK / 平滑损失。
原来把 FK 加在 one_step(随机噪声) 上、对齐这一条样本的老师 Y:对任意噪声都回归同一个 Y,最优解是条件均值 E[Y|X],
多峰头被训成单峰(实测 last.pt 8 个候选间标准差 0.030,动作幅度 0.151),左绕/右绕取平均正好穿柱。
x̂ 由 z_t 里带着的那一条 x 决定(t 小时 z_t 几乎就是 x),不会把不同模式拉到一起。"""
import torch
from torch.func import jvp
def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0):
def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0, p_edge=0.0):
"""2026-10-19(审查 review_model_train P0-3):部署只在 (r=0, t=1) 推理一次,而 logit-normal 取 max/min 后
P(t>0.95) 约 0.08%、P(r<0.05) 约 0.5%,这个角几乎从未被训练到(一步残差与噪声相关 −0.86 即外推失准)。
p_neq > 0 时另以 p_edge 的概率把样本直接放在 (r=0, t=1);预热期(p_neq=0)不放,那时一步映射本就不训。"""
t = torch.sigmoid(torch.randn(B, device=device, dtype=dtype) * sigma + mu)
r = torch.sigmoid(torch.randn(B, device=device, dtype=dtype) * sigma + mu)
t, r = torch.maximum(t, r), torch.minimum(t, r)
eq = torch.rand(B, device=device) >= p_neq
r = torch.where(eq, t, r)
if p_neq > 0 and p_edge > 0:
edge = torch.rand(B, device=device) < p_edge
t = torch.where(edge, torch.ones_like(t), t); r = torch.where(edge, torch.zeros_like(r), r)
return r, t
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3, return_aux=False, p_edge=0.0):
mem, x = mem.float(), x.float()
with torch.autocast('cuda', enabled=False):
B = x.shape[0]
r, t = sample_rt(B, x.device, x.dtype, p_neq)
r, t = sample_rt(B, x.device, x.dtype, p_neq, p_edge=p_edge)
e = torch.randn_like(x)
z = (1 - t)[:, None, None] * x + t[:, None, None] * e
v = e - x
@@ -25,7 +39,10 @@ def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
u_tgt = (v - (t - r)[:, None, None] * dudt).detach()
err = (u - u_tgt).pow(2).mean(dim=(1, 2))
w = 1.0 / (err.detach() + ada_c).pow(ada_p)
return (w * err).mean(), err.mean().detach()
loss = (w * err).mean()
if return_aux:
return loss, err.mean().detach(), {'z': z, 't': t, 'r': r, 'u': u}
return loss, err.mean().detach()
def one_step(model, mem, e):
+16 -5
View File
@@ -55,10 +55,17 @@ class DecBlock(nn.Module):
return x + self.ff(self.n3(x))
TEMB_WMAX = 50.0 # 时间嵌入最高角频率(rad / 单位 t)
def time_emb(t, dim):
"""2026-10-18 首次正式预训练后改(step 21850 闭环到达 0%):原实现照扩散模型写成 sin(1000·t·f),f∈[1e-3, 1],
最高角频率 1000 —— t∈[0,1] 上网络对 t 极其敏感,实测 last.pt 的 ∂u/∂t 均方根 6–12,而 v 只有约 1.1,
MeanFlow 目标 u_tgt = v − (t−r)·du/dt 被自激的 JVP 项主导:MF 残差 2 万步一直停在 7–8、17k–19k 尖峰到 96、梯度 100% 被裁剪。
改为角频率在 [0.5, 50] 几何分布(最快一维在 [0,1] 上约转 8 圈,足够分辨 t,导数量级降 20 倍)。"""
half = dim // 2
fr = torch.exp(-math.log(1000.0) * torch.arange(half, device=t.device, dtype=t.dtype) / half)
a = t[:, None] * 1000.0 * fr[None]
w = torch.exp(torch.linspace(math.log(0.5), math.log(TEMB_WMAX), half, device=t.device, dtype=t.dtype))
a = t[:, None] * w[None]
return torch.cat([torch.sin(a), torch.cos(a)], -1)
@@ -144,8 +151,9 @@ class Policy(nn.Module):
x = z - self.decode(m, z, torch.zeros_like(one), one)
return x, v.expand(K), s.expand(K, -1)
def sample_k(self, feat, K, z=None):
"""仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13]"""
def sample_k(self, feat, K, z=None, steps=1):
"""仿真 / APG 用:每个观测 K 条候选 → act [B,K,16,6], value [B], sub [B,13]
steps>1(诊断用,2026-10-19):把 [0,1] 均分成 steps 段,x ← x − (t−r)·u(x, r, t) 逐段走;steps=1 即一步生成。"""
mem = self.encode(feat)
v, s = self.heads(mem)
B = feat.shape[0]
@@ -158,7 +166,10 @@ class Policy(nn.Module):
z = torch.randn(B * K, self.h, 6, device=feat.device, dtype=feat.dtype) if z is None else z.reshape(B * K, self.h, 6)
m = mem.repeat_interleave(K, 0)
one = torch.ones(B * K, device=feat.device, dtype=feat.dtype)
x = z - self.decode(m, z, torch.zeros_like(one), one)
x = z
for i in range(steps):
t, r = 1 - i / steps, 1 - (i + 1) / steps
x = x - (t - r) * self.decode(m, x, one * r, one * t)
return x.view(B, K, self.h, 6), v, s
+231 -66
View File
@@ -56,7 +56,16 @@
.step.done .ic { background: #34d39922; border-color: var(--ok); color: var(--ok); }
.step.active .ic { background: #5aa9ff22; border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 4px #5aa9ff1f; }
.step.fail .ic { background: #f8717122; border-color: var(--bad); color: var(--bad); }
.step.fail .ds { color: var(--bad); font-weight: 650; font-size: 12px; }
.alert { border-radius: 12px; padding: 13px 16px; margin: 2px 0 0; border: 1px solid; }
.alert.bad { background: linear-gradient(180deg,#7f1d1d66,#450a0a55); border-color: #f8717199; box-shadow: 0 0 0 1px #f8717122, 0 8px 26px #00000066; }
.alert.warn { background: #78350f55; border-color: #fbbf2488; }
.alert .alerth { font-size: 17px; font-weight: 700; color: #ffe3e3; letter-spacing: .2px; }
.alert.warn .alerth { color: #ffeccd; }
.alert .alerterr { font: 500 12.5px/1.5 var(--mono); color: #ffb4b4; margin-top: 7px; word-break: break-all; }
.alert .alerttip { font-size: 12.5px; color: #f0d9d9cc; margin-top: 7px; }
.step .nm { font-size: 12.5px; color: var(--text); white-space: nowrap; } .step .ds { font-size: 11px; color: var(--dim2); white-space: nowrap; }
.step .ds.big { font-size: 14px; font-weight: 650; color: var(--accent); }
.step.pending .nm { color: var(--dim); }
.step.active .ds { color: var(--accent); }
@@ -84,7 +93,9 @@
.ring .c { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; }
.ring .c b { font: 700 24px/1 var(--mono); display: block; } .ring .c span { font-size: 11px; color: var(--dim); }
.gpurows { flex: 1; display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.gr .l { display: flex; justify-content: space-between; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); }
.cores { display: flex; gap: 2px; height: 26px; margin-top: 2px; background: #1b2433; border-radius: 3px; padding: 2px; }
.gr .l { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--dim); margin-bottom: 4px; } .gr .l b { color: var(--text); font: 500 12.5px var(--mono); }
.gr .l span { flex: none; white-space: nowrap; } .gr .l b { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } /* 值太长就省略号,别把整行挤成两行 */
.mini { height: 6px; background: #0d131d; border-radius: 99px; overflow: hidden; } .mini > div { height: 100%; border-radius: 99px; transition: width .6s; }
.gname { font-size: 12px; color: var(--dim2); margin-top: 12px; }
@@ -126,25 +137,42 @@
button.danger { color: var(--bad); border-color: #f8717166; } button.danger:hover { background: #f8717114; }
.overall { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.overall .now { font-size: 13px; color: var(--dim); } .overall .now b { display: block; color: var(--text); font-size: 18px; font-weight: 650; margin: 2px 0 2px; }
.sublabel { font-size: 12.5px; color: var(--dim); display: flex; justify-content: space-between; margin-top: 4px; }
.bar.thin { height: 8px; margin: 8px 0 4px; } .bar.ind .fill { width: 30% !important; animation: ind 1.6s ease-in-out infinite; }
.sublabel { font-size: 15px; color: var(--dim); display: flex; justify-content: space-between; align-items: baseline; margin-top: 6px; }
.sublabel b { font: 700 20px/1 var(--mono); color: var(--text); }
.bar.thin { height: 14px; margin: 10px 0 4px; } .bar.ind .fill { width: 30% !important; animation: ind 1.6s ease-in-out infinite; }
@keyframes ind { 0% { margin-left: 0; } 50% { margin-left: 70%; } 100% { margin-left: 0; } }
.ctlrow { display: flex; gap: 10px; margin-top: 14px; align-items: center; flex-wrap: wrap; }
.ctlrow .hint { font-size: 12px; color: var(--dim2); }
.term { background: #070a10; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; height: 260px; overflow: auto; font: 12px/1.55 var(--mono); color: #c9d4e3; white-space: pre-wrap; word-break: break-all; }
.explore { background: #0b0f16; border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; max-height: 560px; overflow: auto; font: 12px/1.7 var(--mono); color: #c9d4e3; white-space: pre-wrap; }
.term .e { color: var(--bad); } .term .c { color: var(--accent); } .term .g { color: var(--ok); }
.modal { position: fixed; inset: 0; background: #0009; backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; z-index: 40; }
.modal { position: fixed; inset: 0; background: #05080ed9; backdrop-filter: blur(7px); display: none; align-items: flex-start; justify-content: center; z-index: 40; padding: 5vh 16px 24px; overflow: auto; }
.modal.show { display: flex; }
.dlg { width: min(560px, 92vw); background: #131a27; border: 1px solid var(--line2); border-radius: 16px; padding: 20px 22px; box-shadow: 0 30px 80px #000a; }
.dlg h3 { margin: 0 0 4px; font-size: 16px; } .dlg .d { color: var(--dim); font-size: 12.5px; margin-bottom: 14px; }
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
.form label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; color: var(--dim); }
.form input, .form select { background: #0f1520; color: var(--text); border: 1px solid var(--line2); border-radius: 8px; padding: 7px 9px; font: 13px var(--mono); }
.dlg { width: min(780px, 96vw); background: linear-gradient(180deg, #161f2e 0%, #121926 100%); border: 1px solid var(--line2); border-radius: 18px; box-shadow: 0 40px 100px #000c, 0 1px 0 #ffffff0a inset; overflow: hidden; }
.dlg .hd { display: flex; gap: 13px; align-items: flex-start; padding: 18px 22px 15px; border-bottom: 1px solid var(--line); background: #ffffff04; }
.dlg .hd .ic { flex: none; width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: #fff; background: linear-gradient(135deg, var(--accent), var(--accent2)); box-shadow: 0 4px 16px #5aa9ff33; font-size: 13px; }
.dlg .hd h3 { margin: 2px 0 0; font-size: 16px; } .dlg .hd .d2 { color: var(--dim2); font-size: 12px; margin-top: 4px; line-height: 1.55; }
.dlg .hd .d2 code { color: var(--dim); }
.dlg .bd { padding: 16px 22px 4px; }
.sec { display: flex; align-items: center; gap: 10px; margin: 17px 0 11px; font-size: 11px; letter-spacing: .1em; color: var(--dim2); }
.bd .sec:first-child { margin-top: 2px; }
.sec:after { content: ''; flex: 1; height: 1px; background: var(--line); }
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 13px 16px; }
.form label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--dim); min-width: 0; }
.form .hint { font-size: 11px; color: var(--dim2); line-height: 1.45; }
.form input, .form select { width: 100%; background: #0c1320; color: var(--text); border: 1px solid var(--line2); border-radius: 9px; padding: 9px 11px; font: 13px/1.35 var(--mono); outline: none; transition: border-color .12s, box-shadow .12s, background .12s; }
.form input:hover, .form select:hover { border-color: #3d4c68; }
.form input:focus, .form select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px #5aa9ff22; background: #0e1626; }
.form select { appearance: none; -webkit-appearance: none; padding-right: 32px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1.5l5 5 5-5' fill='none' stroke='%238c99ad' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/></svg>"); background-repeat: no-repeat; background-position: right 11px center; }
.form select:disabled { opacity: .4; cursor: not-allowed; background-color: #0a0f18; }
.form .full { grid-column: 1 / -1; }
.ckline { margin-top: 12px; font-size: 12px; color: var(--dim); } .ckline.bad { color: var(--bad); } .ckline .warn { color: var(--warn); }
.demobar { background: #fbbf2418; border: 1px solid #fbbf2455; color: var(--warn); border-radius: 10px; padding: 8px 12px; margin: 10px 0; font-size: 13px; }
.dlg .acts { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.dlg .warnbox { margin-top: 12px; font-size: 12px; color: var(--warn); background: #fbbf2410; border: 1px solid #fbbf2433; border-radius: 8px; padding: 8px 10px; display: none; }
.dlg .acts { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 16px 22px 19px; }
.dlg .acts .sp { flex: 1; }
button.ghost { background: transparent; }
button.ghost:hover { background: #ffffff0a; border-color: #3d4c68; }
.dlg .warnbox { margin: 15px 0 0; font-size: 12.5px; line-height: 1.6; color: var(--warn); background: #fbbf240d; border: 1px solid #fbbf2430; border-left: 3px solid #fbbf24; border-radius: 10px; padding: 10px 13px; display: none; }
.toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #1b2433; border: 1px solid var(--line2); padding: 9px 16px; border-radius: 10px; font-size: 13px; z-index: 50; display: none; box-shadow: 0 10px 30px #0008; }
@media (max-width: 900px) { .hero { grid-template-columns: 1fr; } .charts { grid-template-columns: 1fr; } main { padding: 14px; } }
</style>
@@ -161,17 +189,18 @@
<span class="sp"></span>
<label class="ctl">平滑 <input id="smooth" type="range" min="0" max="0.99" step="0.01" value="0.85"><span id="smoothv" style="font-family:var(--mono);width:30px">0.85</span></label>
<button id="logy" title="损失图纵轴取对数">对数轴</button>
<button id="auto" class="on" title="每 5 秒刷新"><span>●</span>自动刷新</button>
<button id="auto" class="on" title="每 1 秒刷新"><span>●</span>自动刷新</button>
<button id="btnStart" class="primary" title="一键运行流水线">▶ 开始训练</button>
<button id="btnStop" class="danger" style="display:none" title="停止正在运行的流水线">■ 停止</button>
</header>
<main>
<div id="alert"></div>
<section class="card"><h2><span>流水线</span><span class="r" id="stepnote"></span></h2><div class="steps" id="steps"></div></section>
<section class="hero">
<div class="card" id="pipe"></div>
<div class="card"><h2><span>GPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
<div class="card"><h2><span>GPU / CPU</span><span class="r" id="gputime"></span></h2><div id="gpu"></div></div>
</section>
<section class="card" id="prog"></section>
@@ -185,28 +214,66 @@
<div class="card"><h2><span>部署形态评测</span><span class="r">策略 + 安全层</span></h2><div id="bench"></div></div>
<div class="card"><h2><span>验收汇总</span><span class="r">方案 §9.2</span></h2><div id="accept"></div></div>
</section>
<section class="card"><h2><span>闭环探索日志</span><span class="r">docs/探索日志.md</span></h2><div class="explore" id="explore">加载中…</div></section>
<section class="card"><h2><span>终端输出</span><span class="r" id="termnote">ml/runs/console.log</span></h2><div class="term" id="term"></div></section>
<footer id="foot"></footer>
</main>
<div class="tip" id="tip"></div>
<div class="toast" id="toast"></div>
<div class="modal" id="modal"><div class="dlg">
<h3>开始训练</h3>
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
<div class="hd">
<div class="ic">▶</div>
<div>
<h3 id="mdTitle">开始训练</h3>
<div class="d2">一键跑 <code>bash ml/pipeline.sh all</code>:已完成的步骤自动跳过,训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
</div>
</div>
<div class="bd">
<div class="sec">运行方式</div>
<div class="form">
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="from">从指定阶段重训(该阶段及之后全部重做)</option><option value="fresh">全部重跑(数据也重新生成)</option></select><span class="hint">续跑 = 从上次断点接着来;重训 = 把该阶段及之后的旧产物移进 ml/archive/(不删)</span></label>
<label>起始阶段<select id="fFrom"></select><span class="hint">按流水线顺序排列;选「预训练」只重训模型:数据、打包样本、一致性门禁都保留</span></label>
</div>
<div class="sec">模型与数据</div>
<div class="form">
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="fresh">从头重跑(旧产物移到 ml/archive/)</option></select></label>
<label>动作头 CFG<select id="fCfg"><option value="ml/configs/base.json">MeanFlow(base,M5 不过自动转 WTA)</option><option value="ml/configs/wta.json">WTA(wta)</option><option value="ml/configs/small.json">小模型 d=128(R4 不过时用)</option></select></label>
<label>共识过滤 CONSENSUS<select id="fCons"><option value="">按老师门禁自动</option><option value="0">关</option><option value="1">开(生成约慢 5 倍)</option></select></label>
<label>任务数 N_TASKS<input id="fN" value="50000"></label>
<label>生成进程数 NPROC<input id="fP" placeholder="默认 = CPU 核数"></label>
<label>目标样本 TARGET_SAMPLES<input id="fT" value="1700000"></label>
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
<label>目标样本 TARGET_SAMPLES<input id="fT" value="2000000"></label>
<label>老师间隙下限 MIN_GAP_MM<input id="fG" value="3"></label>
<label>挖错轮数 ROUNDS<input id="fR" value="6"></label>
</div>
<div class="warnbox" id="fWarn"></div>
<div class="acts"><button id="fCancel">取消</button><button id="fGo" class="primary">▶ 开始</button></div>
</div>
<div class="acts">
<span class="sp"></span>
<button class="ghost" id="fCancel">取消</button>
<button id="fGo" class="primary">▶ 开始</button>
</div>
</div></div>
<script>
/* 兜底:正文脚本一旦有语法错误或抛异常,整页会空白(2026-10-18 真踩过:一行字符串写断 → 页面什么都不显示)。
这段在正文之前跑,负责"坏也坏得看得见":看门狗 + 全局错误显示。 */
window.__boot = { ok: false };
function __pageFail(msg) {
const m = document.querySelector('main') || document.body;
let d = document.getElementById('pagefail');
if (!d) {
d = document.createElement('div'); d.id = 'pagefail';
d.style.cssText = 'margin:10px 24px 0;padding:14px 16px;border-radius:12px;background:#7f1d1dcc;border:1px solid #f87171;' +
'color:#ffe3e3;font:600 15px/1.6 system-ui;position:sticky;top:8px;z-index:99;box-shadow:0 8px 26px #00000088';
m.prepend(d);
}
d.textContent = msg;
}
window.addEventListener('error', e => __pageFail('页面脚本出错:' + (e.message || '未知错误') +
'(' + (e.filename || '').split('/').pop() + ':' + (e.lineno || '?') + ')— 面板可能不完整,但训练本身没受影响;按 F12 看 Console'));
setTimeout(() => { if (!window.__boot.ok) __pageFail('页面脚本没有启动(多半是 ml/monitor.html 有语法错误):卡片空白不是训练出问题。' +
'F12 → Console 看第一条报错,或把该文件回滚到上一个提交。'); }, 3000);
</script>
<script>
'use strict';
/* =====================================================================
@@ -220,7 +287,7 @@ const $ = id => document.getElementById(id);
const isNum = v => typeof v === 'number' && isFinite(v);
const fmt = (v, d = 3) => !isNum(v) ? '—' : (Math.abs(v) >= 1000 ? Math.round(v).toLocaleString() : Math.abs(v) >= 100 ? v.toFixed(1) : Math.abs(v) >= 1 ? v.toFixed(Math.min(d, 3)) : v === 0 ? '0' : v.toPrecision(3));
const pct = (v, d = 1) => !isNum(v) ? '—' : (100 * v).toFixed(d) + '%';
const hms = s => { if (!isNum(s)) return '—'; s = Math.max(0, s); const h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60); return h ? `${h} 小时 ${m} 分` : m ? `${m} 分 ${Math.floor(s % 60)} 秒` : `${Math.floor(s)} 秒`; };
const hms = s => { if (!isNum(s)) return '—'; s = Math.max(0, s); const h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60); return h ? `${h} 小时 ${m} 分 ${Math.floor(s % 60)} 秒` : m ? `${m} 分 ${Math.floor(s % 60)} 秒` : `${Math.floor(s)} 秒`; }; // 用时精确到秒(页面 1 秒轮询,秒位是有意义的)
const hmsShort = s => !isNum(s) ? '—' : s >= 3600 ? `${(s / 3600).toFixed(1)} h` : s >= 60 ? `${Math.round(s / 60)} 分` : `${Math.round(s)} 秒`;
const ago = t => { const s = Date.now() / 1000 - t; return s < 60 ? `${Math.max(0, Math.floor(s))} 秒前` : s < 3600 ? `${Math.floor(s / 60)} 分钟前` : `${(s / 3600).toFixed(1)} 小时前`; };
const ICON_WAIT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>';
@@ -343,13 +410,26 @@ let logyOn = false, job = null, userPicked = false, conOff = 0;
下面这份只给示例数据用,数值与 pstate.py 一致。 */
let STAGE_META = [
['setup', '安装依赖', 0.2], ['kin', '对拍', 0.2], ['r4', '推理延迟', 0.2], ['gate1', '一致性门禁', 0.5], ['gen', '生成老师轨迹', 5.0],
['prep', '打包样本', 0.5], ['train', '预训练', 3.0], ['m5', '头门禁', 0.5], ['apg', 'APG 微调', 4.0], ['mine', '挖错循环', 9.0],
['prep', '打包样本', 2.0], ['train', '预训练', 4.5], ['dagger', 'DAgger 纠偏', 3.5], ['m5', '头门禁', 0.5], ['apg', 'APG 微调', 4.0], ['mine', '挖错循环', 9.0],
['export', '导出', 0.1], ['bench', '部署评测', 1.0], ['accept', '验收汇总', 0.5]];
let SM = Object.fromEntries(STAGE_META.map(([k, n, w]) => [k, { n, w, to: Math.max(1, 3 * w) }]));
/* 起始阶段下拉的编号按 README 步骤表(1 环境、2 对拍、2′ R4、3 门禁、…、12 验收);setup 不进下拉(建议手动装依赖) */
const FROM_NO = { setup: '1', kin: '2', r4: '2′', gate1: '3', gen: '4', prep: '5', train: '6', dagger: '6′', m5: '7', apg: '8', mine: '9', export: '10', bench: '11', accept: '12' };
const FROM_NOTE = { gen: '(约 7 小时)' };
function fillFrom() {
const sel = $('fFrom'); if (!sel) return;
const want = STAGE_META.map(x => x[0]).filter(k => k !== 'setup');
if ([...sel.options].map(o => o.value).join() === want.join()) return; // 阶段表没变就别重建,免得每秒轮询把已选项重置
const keep = sel.value;
sel.innerHTML = want.map(k => `<option value="${k}">${FROM_NO[k] ? FROM_NO[k] + ' ' : ''}${SM[k].n} ${k}${FROM_NOTE[k] || ''}</option>`).join('');
sel.value = want.includes(keep) ? keep : want.includes('train') ? 'train' : want[0];
}
function applyStageMeta(m) {
if (!m || !m.length) return;
STAGE_META = m.map(x => [x.id, x.name, x.est_h]); SM = Object.fromEntries(m.map(x => [x.id, { n: x.name, w: x.est_h, to: x.timeout_h }]));
fillFrom();
}
fillFrom();
/* 开始 / 停止请求带自定义头:跨站网页发不出这种请求(会触发 CORS 预检,控制台不应答),见 monitor.py 审查 N-8 */
const post = (path, body) => fetch(path, { method: 'POST', headers: { 'X-Console': '1', 'Content-Type': 'application/json' }, body: body || '{}' }).then(x => x.json()).catch(e => ({ error: String(e) }));
const smoothv = () => +$('smooth').value;
@@ -372,6 +452,10 @@ function buildCharts(kind) {
b: new Chart(c, '损失分量', { logy: () => logyOn, xmax: total, xmin0: true, hidden: ['smooth'] }),
e: new Chart(c, '闭环评估(val,部署节奏,关安全层)', { percent: true, range: [0, 1], xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : '每 2000 步',
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
o: new Chart(c, '开环误差(mm)', { xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : 'val 样本、老师状态 · 16 步末端误差 · 越低越好',
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
d: new Chart(c, '候选多样性', { xmax: total, xmin0: true, note: kind === 'bench' ? '测速运行不评估' : '候选间标准差 / 动作幅度 · 塌成单峰时接近 0',
emptyText: kind === 'bench' ? '测速运行不做闭环评估' : '第一次评估在第 2000 步' }),
t: new Chart(c, '吞吐(样本/s)', { xmax: total, xmin0: true, zero: true, refs: [{ y: 5000, label: 'R10 通过线 5000', color: css('--bad') }] }),
};
}
@@ -393,6 +477,11 @@ function render() {
}
const E = (k, label, best) => { const pts = evals.filter(e => isNum(e[k])).map(e => [e.x, e[k]]); let b = null; if (best && pts.length) b = pts.reduce((m, p) => p[1] > m[1] ? p : m); return { name: k, label, dots: true, pts, best: b }; };
charts.e.set([E('success', '到达率', true), E('collision_rate', '碰撞率'), E('time_ok_1p3', '≤ 老师时长×1.3'), E('start_fail', '起步失败')]);
if (charts.o) { // 开环指标只随 train.py 的评估写进日志;APG 分支没建这两张图
const Emin = (k, label) => { const pts = evals.filter(e => isNum(e[k])).map(e => [e.x, e[k]]); let b = null; if (pts.length) b = pts.reduce((m, p) => p[1] < m[1] ? p : m); return { name: k, label, dots: true, pts, best: b }; };
charts.o.set([Emin('ol_fk_mm', '候选 0'), { name: 'ol_best_mm', label: '8 候选最好', dots: true, pts: evals.filter(e => isNum(e.ol_best_mm)).map(e => [e.x, e.ol_best_mm]) }]);
charts.d.set([{ name: 'ol_div', label: '多样性', dots: true, pts: evals.filter(e => isNum(e.ol_div)).map(e => [e.x, e.ol_div]) }]);
}
renderProgress(); renderKpis();
}
@@ -416,7 +505,7 @@ function renderProgress() {
<div class="facts">
${fact('步数', `${step.toLocaleString()}${total ? ' / ' + total.toLocaleString() : ''}`)}
${fact('已用时间', hms(elapsed))}
${fact('剩余时间', finished ? '—' : hms(eta))}
${fact('剩余时间', finished ? '—' : hmsShort(eta))}
${fact('预计完成', fin)}
${kind === 'apg' ? fact('本批展开', r.plans ? `${r.plans} 次推理` : '—') : fact('学习率', isNum(r.lr) ? r.lr.toExponential(2) : '—')}
${fact('日志更新', lastMtime ? ago(lastMtime) : '—', running ? '' : 'warn')}
@@ -426,6 +515,7 @@ function renderProgress() {
function renderKpis() {
const r = recs[recs.length - 1] || {}, e = evals[evals.length - 1] || {}, pe = evals[evals.length - 2] || {}, kind = cur.kind;
const best = evals.length ? Math.max(...evals.map(v => v.success ?? -1)) : null;
const bestE = evals.length ? evals.reduce((m, v) => ((v.success ?? -1) > (m.success ?? -1) ? v : m)) : null;
const dlt = (k, good = 1) => { if (!isNum(e[k]) || !isNum(pe[k])) return ''; const d = (e[k] - pe[k]) * 100; if (Math.abs(d) < 0.05) return ''; const up = d > 0; return `<span class="delta ${(up ? 1 : -1) * good > 0 ? 'ok' : 'bad'}">${up ? '▲' : '▼'} ${Math.abs(d).toFixed(1)}pp</span>`; };
const last = k => recs.filter(v => isNum(v[k])).slice(-300).map(v => [xOf(v), v[k]]);
const evs = k => evals.filter(v => isNum(v[k])).map(v => [v.x, v[k]]);
@@ -441,7 +531,7 @@ function renderKpis() {
const sps = r.samples_per_s;
tile('总损失', fmt(r.loss, 4), `MF ${fmt(r.mf)} · FK ${fmt(r.fk_cm)} cm`, '', ema(last('loss'), 0.9), COLORS[0]);
tile('吞吐', sps ? `${(sps / 1000).toFixed(2)}<small>k 样本/s</small>` : '—', sps ? (sps >= 5000 ? 'R10 达标(≥ 5k/s)' : 'R10 未达标(< 5k/s)') : 'R10 通过线 5k/s', sps ? (sps >= 5000 ? 'ok' : 'bad') : '', last('samples_per_s'), COLORS[5]);
tile('闭环到达率', pct(e.success) + dlt('success'), evals.length ? `最好 ${pct(best)} · @${e.x.toLocaleString()} 步` : '尚未评估', '', evs('success'), COLORS[2]);
tile('闭环到达率', pct(e.success) + dlt('success'), bestE ? `最好 ${pct(best)} · @${bestE.x.toLocaleString()} 步` : '尚未评估', '', evs('success'), COLORS[2]);
tile('碰撞率', pct(e.collision_rate, 2) + dlt('collision_rate', -1), '关安全层,部署另有护栏', e.collision_rate > 0.03 ? 'warn' : '', evs('collision_rate'), COLORS[3]);
tile('梯度范数', fmt(r.gnorm), '裁剪阈值 1.0', r.gnorm > 5 ? 'warn' : '', ema(last('gnorm'), 0.9), COLORS[4]);
tile('显存峰值', isNum(r.vram) ? `${r.vram.toFixed(2)}<small>GiB</small>` : '—', '预算 19–20 GB(整卡 24,桌面占 4–5)', r.vram > 17 ? 'bad' : r.vram > 14 ? 'warn' : '', last('vram'), COLORS[1]);
@@ -464,10 +554,12 @@ async function loadRuns() {
if (!r) { cur = null; $('charts').innerHTML = ''; $('kpis').innerHTML = ''; $('prog').innerHTML = '<h2><span>当前运行 · 训练曲线</span></h2>' + empty('还没有训练日志:预训练开始后这里出现损失、吞吐和评估曲线。'); }
}
let polling = false;
async function poll() {
if (polling) return; polling = true; // 1 秒一轮:上一轮没回来就跳过,避免请求堆叠
try {
lastStatus = await api('/api/status'); applyStageMeta(lastStatus.stage_meta);
job = await api('/api/job').catch(() => null);
const [st, jb] = await Promise.all([api('/api/status'), api('/api/job').catch(() => null)]);
lastStatus = st; applyStageMeta(st.stage_meta); job = jb;
await loadRuns();
if (cur) {
const d = await api(`/api/log?run=${encodeURIComponent(cur.name)}&offset=${offset}`);
@@ -482,26 +574,36 @@ async function poll() {
if (DEMO) { setPill('demo', '示例数据'); if (!$('demobar')) $('pipe').closest('.hero').insertAdjacentHTML('beforebegin', `<div class="demobar" id="demobar">这是示例数据,不是你的训练。${location.protocol === 'file:' ? '直接双击 monitor.html 只能看示例;' : ''}看真实训练请双击 ml/console.bat(或 python -m ml.monitor --open)。</div>`); }
else if (busy) setPill('live', `运行中 · ${P && P.current ? SM[P.current]?.n || P.current : '启动中'}`);
else if (cur && Date.now() / 1000 - lastMtime < 120) setPill('live', `运行中 · ${ago(lastMtime)}更新`);
else if (P && P.plan && P.plan.some(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0)) {
const k = P.plan.find(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0), rc = P.stages[k].rc, nm = SM[k]?.n || k;
setPill(rc === -15 || rc === 143 ? 'stale' : 'dead', `${rc === -15 || rc === 143 ? '已停止' : '失败'} · 停在「${nm}」返回码 ${rc}`);
}
else setPill('', '空闲');
renderStatus(lastStatus); renderPipe(lastStatus); renderJob();
try { renderStatus(lastStatus); renderPipe(lastStatus); renderJob(); }
catch (e) { __pageFail('渲染面板出错:' + e.message + ' —— 其余面板可能不更新;训练本身不受影响'); }
await pollConsole();
} catch (err) {
setPill('dead', '连不上控制台(双击 ml/console.bat 或 python -m ml.monitor)'); console.error(err);
}
} finally { polling = false; }
}
function setPill(cls, text) { const st = $('state'); st.className = 'pill ' + cls; st.lastElementChild.textContent = text; }
/* ===================== 流水线 / GPU / 各阶段面板 ===================== */
/* 「全流程」进度与上方步骤条同一口径:步骤 = 本次计划 ∪ 记录里已有的阶段。
从中间阶段重训时,前面已完成的阶段不在本次 plan 里,但记录还在(步骤条就靠这个显示打勾),
总进度/剩余时间/完成计数也必须算上它们,否则总进度会归零。 */
const pipeKeys = (P, plan) => STAGE_META.map(x => x[0]).filter(k => plan.includes(k) || P.stages[k]);
function stages(s) {
const P = s.pipeline;
if (P) { // 有流水线记录:以它为准
const plan = P.plan && P.plan.length ? P.plan : STAGE_META.map(x => x[0]).filter(k => k !== 'setup');
const keys = STAGE_META.map(x => x[0]).filter(k => plan.includes(k) || P.stages[k]);
const keys = pipeKeys(P, plan);
const el = P.elapsed || {}; // 累计用时(跨停止/续跑),没有就退回本段
return keys.map(k => {
const r = P.stages[k], nm = SM[k].n;
if (!r) return [nm, 'pending', '—'];
if (P.current === k) return [nm, 'active', P.progress && P.progress.frac != null ? pct(P.progress.frac, 0) : '进行中'];
if (r.rc === 0) return [nm, 'done', '用时 ' + hmsShort(r.end - r.start)];
if (r.rc === 0) return [nm, 'done', '用时 ' + hms(el[k] != null ? el[k] : r.end - r.start)];
if (r.rc === -15 || r.rc === 143) return [nm, 'fail', '已停止'];
if (r.rc != null) return [nm, 'fail', `失败(返回码 ${r.rc})`];
return [nm, 'fail', '中断'];
@@ -528,24 +630,41 @@ function stages(s) {
}
function renderStatus(s) {
const L = stages(s);
$('steps').innerHTML = L.map(([nm, st, ds], i) => `<div class="step ${st}"><div class="ic">${st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1}</div><div class="nm">${nm}</div><div class="ds">${ds}</div></div>`).join('');
$('steps').innerHTML = L.map(([nm, st, ds], i) => `<div class="step ${st}"><div class="ic">${st === 'done' ? '✓' : st === 'fail' ? '!' : i + 1}</div><div class="nm">${nm}</div><div class="ds${st === 'active' ? ' big' : ''}">${ds}</div></div>`).join('');
const nDone = L.filter(x => x[1] === 'done').length; $('stepnote').textContent = `${nDone} / ${L.length} 完成`;
// GPU
const G = s.gpu || [];
const BUD = (s.vram_budget_gb || 24) * 1024; // 显存预算(MB):桌面常驻占 4–5 GB,不能按整卡 24 GB 看
$('gputime').textContent = G.length ? '实时' : '';
$('gpu').innerHTML = G.length ? G.map(g => {
// GPU + CPU(同一个框:上面是 GPU 环 + 显存/温度/功耗,下面接 CPU 总占用与每核条)
const G = s.gpu || [], CP = s.cpu || {};
const RESV = s.vram_reserve_gb ?? 1; // 给系统留的余量(GB):训练进程启动时按"整卡 − 已用 − 余量"自设上限(浮动)
const hr = '<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">';
const gr = (k, v, p, c, t) => `<div class="gr"><div class="l"><span${t ? ` title="${t}"` : ''}>${k}</span><b>${v}</b></div><div class="mini"><div style="width:${Math.min(100, p)}%;background:${c}"></div></div></div>`;
$('gputime').textContent = (G.length || CP.n) ? '实时' : '';
const gpuBlk = G.length ? G.map(g => {
const u = g.util ?? 0, C = 2 * Math.PI * 50, col = u > 85 ? css('--ok') : u > 50 ? css('--accent') : css('--warn');
const BUD = (s.vram_budget_gb || (g.mem_total / 1024 - RESV)) * 1024, avail = BUD / 1024 - g.mem_used / 1024;
const memP = 100 * g.mem_used / BUD, tc = g.temp > 83 ? css('--bad') : g.temp > 75 ? css('--warn') : css('--c6');
const row = (k, v, p, c) => `<div class="gr"><div class="l"><span>${k}</span><b>${v}</b></div><div class="mini"><div style="width:${Math.min(100, p)}%;background:${c}"></div></div></div>`;
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
<circle cx="58" cy="58" r="50" stroke="${col}" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="${C}" stroke-dashoffset="${C * (1 - u / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b>${u}%</b><span>利用率</span></div></div></div>
<div class="gpurows">${row('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB 预算(整卡 ${(g.mem_total / 1024).toFixed(0)})`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'))}
${row('温度', `${g.temp} °C`, g.temp, tc)}${row('功耗', g.power != null ? `${Math.round(g.power)} W` : '—', g.power ? g.power / 3.5 : 0, css('--c7'))}</div></div>
<div class="gpurows">${gr('显存', `${(g.mem_used / 1024).toFixed(1)} / ${(BUD / 1024).toFixed(0)} GB · 空 ${avail.toFixed(1)}`, memP, memP > 100 ? css('--bad') : memP > 85 ? css('--warn') : css('--accent2'), `可用 ${(BUD / 1024).toFixed(0)} GB = 整卡 ${(g.mem_total / 1024).toFixed(0)} GB − 给系统的余量 ${RESV} GB;训练进程启动时按「可用 − 那一刻已用」再自设本进程上限,别人占多少自动扣掉(ml/common/vram.py)`)}
${gr('温度', `${g.temp} °C`, g.temp, tc)}${gr('功耗', g.power != null ? `${Math.round(g.power)} W` : '—', g.power ? g.power / 3.5 : 0, css('--c7'))}</div></div>
<div class="gname">${g.name}</div>`;
}).join('<hr style="border:none;border-top:1px solid var(--line);margin:14px 0">') : empty('未检测到 nvidia-smi');
}).join(hr) : '';
const cpuBlk = CP.n ? (() => {
const tot = CP.total ?? 0, C = 2 * Math.PI * 50, col = tot > 85 ? css('--bad') : tot > 60 ? css('--warn') : css('--ok');
const ct = CP.coreT || [], ctMax = ct.length ? Math.max(...ct) : null;
const tcol = v => v > 98 ? css('--bad') : v > 90 ? css('--warn') : css('--c6');
const bars = (CP.per || []).map((v, i) => `<div title="核 ${i + 1}: ${v}%${ct[i] != null ? ' · ' + ct[i] + ' °C' : ''}" style="flex:1;min-width:0;height:100%;border-radius:2px;background:${v > 85 ? css('--bad') : v > 60 ? css('--warn') : css('--accent2')};opacity:${0.18 + 0.82 * Math.min(1, v / 100)}"></div>`).join('');
return `<div class="gpuwrap"><div class="ring"><svg width="116" height="116"><circle cx="58" cy="58" r="50" stroke="#1b2433" stroke-width="10" fill="none"/>
<circle cx="58" cy="58" r="50" stroke="${col}" stroke-width="10" fill="none" stroke-linecap="round" stroke-dasharray="${C}" stroke-dashoffset="${C * (1 - tot / 100)}" style="transition:stroke-dashoffset .6s"/></svg>
<div class="c"><div><b>${tot}%</b><span>CPU</span></div></div></div>
<div class="gpurows">${gr('占用', `${tot}% · ${CP.n} 核`, tot, col)}
${CP.temp != null ? gr('温度', `${CP.temp} °C${ctMax != null && ctMax !== CP.temp ? `(最高核 ${ctMax})` : ''}`, CP.temp, tcol(CP.temp), 'MSI Afterburner 的共享内存;鼠标停在核条上还能看每核温度') : ''}
${CP.power != null ? gr('功耗', `${Math.round(CP.power)} W`, 100 * CP.power / 253, css('--c7'), 'CPU 封装功耗(满标度按 14700K 的 PL2 253 W 画)') : ''}
<div class="cores">${bars}</div></div></div>
<div class="gname">${CP.model || 'CPU'}${CP.temp == null ? ' · 开 MSI Afterburner 才能读 CPU 温度/功耗' : ''} · 每核占用(深色=忙)</div>`;
})() : '';
$('gpu').innerHTML = (gpuBlk + (gpuBlk && cpuBlk ? hr : '') + cpuBlk) || empty('未检测到 nvidia-smi / psutil');
// 数据
const D = s.data || {}, st = D.stats, ix = D.index;
@@ -596,32 +715,39 @@ function renderPipe(s) {
: ck.python ? `<div class="ckline">Python ${ck.python} · torch ${ck.torch}${ck.gpu ? ' · ' + ck.gpu : ''}${ck.warn ? ` · <span class="warn">${ck.warn}</span>` : ''}</div>` : '';
const who = job && job.external ? `由${(lk && lk.owner || '').startsWith('网页') ? '上一个控制台窗口' : '终端'}启动(pid ${lk && lk.pid},${Math.round(lk && lk.age_s || 0)} 秒前心跳)` : job && job.pid ? `PID ${job.pid}` : '';
const btns = `${ckline}<div class="ctlrow">${running || (P && P.current)
? `<button class="danger" onclick="stopJob()">■ 停止</button><span class="hint">${who}</span>`
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
? `<button class="danger" onclick="stopJob()">■ 停止</button><button class="ghost" disabled title="训练进行中:先点「停止」,再点「重新训练」">⟳ 重新训练</button><span class="hint">${who}</span>`
: `<button class="primary" onclick="openStart()" ${ck.problems && ck.problems.length ? 'disabled' : ''}>▶ ${P ? '继续训练' : '开始训练'}</button><button class="ghost" onclick="openStart({mode:'from',from:'train',title:'重新训练'})" ${ck.problems && ck.problems.length ? 'disabled' : ''} title="从 train 阶段重做:旧模型与评测移到 ml/archive/(不删),生成的数据、打包样本、门禁都保留">⟳ 重新训练</button><span class="hint">${job && job.rc != null ? (job.stopped ? '上次已手动停止,再点即从停下的那步继续' : job.rc === 0 ? '上次运行已全部完成' : `上次运行失败(返回码 ${job.rc}),看终端输出;修好后再点即续跑`) : '已完成的步骤会自动跳过'}</span>`}</div>`;
if (!P) { $('pipe').innerHTML = `<h2><span>全流程</span><span class="r">尚未开始</span></h2>${empty('还没运行过流水线。点「开始训练」从对拍一直跑到验收;中途每一步的进度都显示在这里。')}${btns}`; return; }
const plan = P.plan && P.plan.length ? P.plan : Object.keys(P.stages);
const keys = pipeKeys(P, plan); // 含重训时保留的已完成阶段,与上方步骤条同口径
let tot = 0, got = 0;
for (const k of plan) { const w = SM[k]?.w || 0.1, r = P.stages[k]; tot += w; if (r && r.rc === 0 && P.current !== k) got += w; else if (P.current === k) got += w * ((P.progress && P.progress.frac) || 0); }
for (const k of keys) { const w = SM[k]?.w || 0.1, r = P.stages[k]; tot += w; if (r && r.rc === 0 && P.current !== k) got += w; else if (P.current === k) got += w * ((P.progress && P.progress.frac) || 0); }
const frac = tot ? got / tot : 0, now = Date.now() / 1000;
const cur = P.current, cr = cur && P.stages[cur], pr = P.progress || {};
const doneN = plan.filter(k => P.stages[k] && P.stages[k].rc === 0 && P.current !== k).length;
const doneN = keys.filter(k => P.stages[k] && P.stages[k].rc === 0 && P.current !== k).length;
const failed = plan.find(k => P.stages[k] && P.stages[k].rc != null && P.stages[k].rc !== 0);
const allDone = !cur && doneN === plan.length;
let remain = 0; for (const k of plan) { const r = P.stages[k]; if (!r || r.rc == null) remain += (SM[k]?.w || 0.1) * (k === cur ? 1 - (pr.frac || 0) : 1); }
const allDone = !cur && doneN === keys.length;
let remain = 0, calib = false; for (const k of keys) { const r = P.stages[k]; if (!r || r.rc == null) { if (k === cur && pr.eta_s != null) { remain += pr.eta_s / 3600; calib = true; } else remain += (SM[k]?.w || 0.1) * (k === cur ? 1 - (pr.frac || 0) : 1); } }
const head = cur ? `<div class="now">正在运行<b>${SM[cur]?.n || cur}</b>${pr.label || ''}</div>`
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${plan.length} 步</div>`
: allDone ? `<div class="now">状态<b class="ok">全部完成</b>共 ${keys.length} 步</div>`
: failed ? `<div class="now">状态<b class="bad">停在「${SM[failed]?.n || failed}」</b>${(P.stages[failed].info || {}).timed_out || P.stages[failed].rc === 124 ? '超过时间上限,判定卡死' : P.stages[failed].rc === -15 || P.stages[failed].rc === 143 ? '已停止' : '失败,看终端输出'}</div>`
: `<div class="now">状态<b>未在运行</b>已完成 ${doneN} / ${plan.length} 步</div>`;
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${plan.length} 步完成</span></h2>
: `<div class="now">状态<b>未在运行</b>已完成 ${doneN} / ${keys.length} 步</div>`;
const esc2 = x => String(x).replace(/&/g, '&amp;').replace(/</g, '&lt;');
const fr = failed && P.stages[failed], stopped = !!fr && (fr.rc === -15 || fr.rc === 143);
const timedOut = !!fr && ((fr.info || {}).timed_out || fr.rc === 124);
const why = !fr ? '' : stopped ? '已手动停止 —— 点「继续训练」从停下的地方接着跑' : timedOut ? '超过时间上限,判定卡死' : `失败(返回码 ${fr.rc})`;
const lastErr = (() => { const t = $('term'); if (!t) return ''; const ls = (t.innerText || '').split(/\r?\n/).filter(x => /Error|error:|Traceback|不通过|失败|超过时间上限/.test(x)); return ls.length ? ls[ls.length - 1].slice(0, 300) : ''; })();
$('alert').innerHTML = failed ? `<div class="alert ${stopped ? 'warn' : 'bad'}"><div class="alerth">✕ 停在「${SM[failed]?.n || failed}」 · ${why}</div>${lastErr ? `<div class="alerterr">${esc2(lastErr)}</div>` : ''}<div class="alerttip">完整报错见下方「终端输出」;修好后点「继续训练」,已完成的步骤会自动跳过${stopped ? '、从停下的那一步接着跑' : ''}</div></div>` : '';
$('pipe').innerHTML = `<h2><span>全流程</span><span class="r">${doneN} / ${keys.length} 步完成</span></h2>
<div class="overall">${head}<div class="bigpct">${(100 * frac).toFixed(1)}<small>%</small></div></div>
<div class="bar ${cur ? '' : 'idle'}"><div class="fill" style="width:${100 * frac}%"></div></div>
${cur ? `<div class="sublabel"><span>当前步骤 · ${SM[cur]?.n}</span><span>${pr.frac != null ? pct(pr.frac) : '无法计量,按用时估'}</span></div>
${cur ? `<div class="sublabel"><span>当前步骤 · ${SM[cur]?.n}</span>${pr.frac != null ? `<b>${pct(pr.frac)}</b>` : '<span>无法计量,按用时估</span>'}</div>
<div class="bar thin ${pr.frac == null ? 'ind' : ''}"><div class="fill" style="width:${pr.frac != null ? 100 * pr.frac : 30}%"></div></div>` : ''}
<div class="facts">
<div class="fact"><div class="k">总用时</div><div class="v">${hms((cur ? now : Math.max(...Object.values(P.stages).map(r => r.end || r.start))) - P.started)}</div></div>
<div class="fact"><div class="k">本步用时</div><div class="v">${cr ? hms(now - cr.start) : '—'}</div></div>
<div class="fact"><div class="k">本步粗估 / 上限</div><div class="v">${cur ? hmsShort((SM[cur]?.w || 0) * 3600) + ' / ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600) : '—'}</div></div>
<div class="fact"><div class="k">全流程剩余(粗估)</div><div class="v">${allDone ? '—' : '约 ' + hmsShort(remain * 3600)}</div></div>
<div class="fact"><div class="k">总用时</div><div class="v">${hms(keys.reduce((a, k) => a + ((P.elapsed || {})[k] || 0), 0))}</div></div>
<div class="fact"><div class="k">本步用时</div><div class="v">${cur ? hms((P.elapsed || {})[cur] || 0) : '—'}</div></div>
<div class="fact"><div class="k">${cur && pr.eta_s != null ? '本步预计剩余(实测速率)' : '本步粗估 / 上限'}</div><div class="v">${cur ? (pr.eta_s != null ? hmsShort(pr.eta_s) + ' / 上限 ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600) : hmsShort((SM[cur]?.w || 0) * 3600) + ' / ' + hmsShort((cr && cr.timeout_h || SM[cur]?.to || 0) * 3600)) : '—'}</div></div>
<div class="fact"><div class="k">全流程剩余(${calib ? '实测外推' : '粗估'})</div><div class="v">${allDone ? '—' : '约 ' + hmsShort(remain * 3600)}</div></div>
</div>${btns}`;
}
function renderJob() {
@@ -641,20 +767,28 @@ async function pollConsole() {
conOff = d.offset; if (atBottom || d.reset) t.scrollTop = t.scrollHeight;
}
function toast(msg) { const t = $('toast'); t.textContent = msg; t.style.display = 'block'; clearTimeout(toast.h); toast.h = setTimeout(() => t.style.display = 'none', 3500); }
function openStart() {
function openStart(preset) {
const P = lastStatus && lastStatus.pipeline, ck = lastStatus && lastStatus.check;
if (ck && ck.problems && ck.problems.length) { toast('环境自检未通过:' + ck.problems.join(';')); return; }
const pre = preset || {};
if (pre.mode) $('fMode').value = pre.mode;
if (pre.from) $('fFrom').value = pre.from;
$('mdTitle').textContent = pre.title || (P ? '继续训练' : '开始训练');
const w = $('fWarn'); const upd = () => { const msgs = [];
const done = P ? (P.plan || []).filter(k => P.stages[k] && P.stages[k].rc === 0) : [];
if ($('fMode').value === 'resume' && done.length) msgs.push(`将跳过已完成的 ${done.length} 步:${done.map(k => SM[k]?.n || k).join('、')}。改了参数会自动改为从头重跑。`);
if ($('fMode').value === 'fresh' && P) msgs.push('从头重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。');
if ($('fMode').value === 'from' && P) { const nm = SM[$('fFrom').value]?.n || $('fFrom').value; msgs.push(`从「${nm}」开始重训:该阶段及之后的旧产物移到 ml/archive/<时间>/(不删除),之前的阶段保持不动。${$('fFrom').value === 'gen' ? '注意:生成的数据会被归档,重跑约 7 小时。' : ''}`); }
if ($('fMode').value === 'fresh' && P) msgs.push('全部重跑:上次的数据、模型、评测结果整体移到 ml/archive/<时间>/(不删除)。生成数据要重跑(约 7 小时)。');
$('fFrom').disabled = $('fMode').value !== 'from';
$('fGo').textContent = $('fMode').value === 'from' ? '▶ 开始重训' : ($('fMode').value === 'fresh' ? '▶ 全部重跑' : '▶ 开始');
w.style.display = msgs.length ? 'block' : 'none'; w.innerHTML = msgs.join('<br>'); };
$('fMode').onchange = upd; upd();
$('modal').classList.add('show');
}
async function startJob() {
const env = { CFG: $('fCfg').value, CONSENSUS: $('fCons').value, N_TASKS: $('fN').value.trim(), NPROC: $('fP').value.trim(), TARGET_SAMPLES: $('fT').value.trim(), ROUNDS: $('fR').value.trim(), MIN_GAP_MM: $('fG').value.trim() };
const body = JSON.stringify({ fresh: $('fMode').value === 'fresh', env });
const mode = $('fMode').value, from = mode === 'from' ? $('fFrom').value : 'kin';
const env = { CFG: $('fCfg').value, CONSENSUS: $('fCons').value, N_TASKS: $('fN').value.trim(), NPROC: $('fP').value.trim(), TARGET_SAMPLES: $('fT').value.trim(), ROUNDS: $('fR').value.trim(), MIN_GAP_MM: $('fG').value.trim(), FRESH_FROM: mode === 'from' ? from : '' };
const body = JSON.stringify({ fresh: mode === 'fresh', from, to: 'accept', env });
const r = DEMO ? await api('/api/start', body) : await post('/api/start', body);
if (r.error) { toast('没能开始:' + r.error); return; }
$('modal').classList.remove('show'); toast('已开始'); conOff = 0; userPicked = false; poll();
@@ -668,14 +802,43 @@ async function stopJob() {
$('btnStart').onclick = openStart; $('btnStop').onclick = stopJob;
$('fCancel').onclick = () => $('modal').classList.remove('show'); $('fGo').onclick = startJob;
$('modal').onclick = e => { if (e.target === $('modal')) $('modal').classList.remove('show'); };
/* 记住上次的设置(localStorage,按站点保存):下次开页面直接是这套参数。
注意「运行方式」不记忆——万一记成"全部重跑",点开始会把数据和模型全归档,风险不对等。 */
const PREF = 'mlmon.pref.v1', PREF_IDS = ['fFrom', 'fCfg', 'fCons', 'fN', 'fP', 'fT', 'fR', 'fG'];
function prefSave() {
try {
const o = {};
for (const id of PREF_IDS) o[id] = $(id).value;
o.smooth = $('smooth').value; o.logy = logyOn ? 1 : 0;
localStorage.setItem(PREF, JSON.stringify(o));
} catch (e) { }
}
function prefLoad() {
let o = null; try { o = JSON.parse(localStorage.getItem(PREF) || 'null'); } catch (e) { }
if (!o) return;
for (const id of PREF_IDS) {
const v = o[id]; if (v == null || v === '') continue;
const el = $(id); if (!el) continue;
if (el.tagName === 'SELECT') { if ([...el.options].some(x => x.value === v)) el.value = v; }
else if (/^[0-9]*\.?[0-9]*$/.test(String(v))) el.value = v; // 参数框只接受数字,脏值不落地
}
if (o.smooth != null) { $('smooth').value = o.smooth; $('smoothv').textContent = Number(o.smooth).toFixed(2); }
if (o.logy) { logyOn = true; $('logy').classList.add('on'); }
}
/* 直接开「重新训练」弹窗:?start=train / ?start=gen…(便于收藏和脚本化截图验证) */
{ const m = location.search.match(/[?&]start=([a-z0-9]+)/);
if (m && SM[m[1]]) setTimeout(() => openStart({ mode: 'from', from: m[1], title: '重新训练' }), 1200); }
/* ===================== 控件 ===================== */
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); render(); };
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); render(); };
prefLoad();
$('smooth').oninput = () => { $('smoothv').textContent = smoothv().toFixed(2); prefSave(); render(); };
$('logy').onclick = () => { logyOn = !logyOn; $('logy').classList.toggle('on', logyOn); prefSave(); render(); };
for (const id of PREF_IDS) { $(id).onchange = prefSave; $(id).oninput = prefSave; }
$('run').onchange = () => { userPicked = true; cur = null; poll(); };
$('scenario').onchange = () => { api.scenario($('scenario').value); cur = null; userPicked = false; conOff = 0; $('run').innerHTML = ''; poll(); };
$('auto').onclick = () => { const on = !$('auto').classList.contains('on'); $('auto').classList.toggle('on', on); clearInterval(timer); if (on) timer = setInterval(poll, 5000); };
timer = setInterval(poll, 5000); poll();
api('/api/explore').then(d => { const el = $('explore'); el.textContent = (d && d.md) ? d.md : '(demo 模式:无探索日志)'; }).catch(() => { const el = $('explore'); el.textContent = '(无法读取 docs/探索日志.md)'; });
$('auto').onclick = () => { const on = !$('auto').classList.contains('on'); $('auto').classList.toggle('on', on); clearInterval(timer); if (on) timer = setInterval(poll, 1000); };
timer = setInterval(poll, 1000); poll();
/* =====================================================================
示例数据:两种场景,每次刷新让"运行中"的那条日志向前推进。
@@ -696,7 +859,8 @@ function demoApi() {
function trainEval(x, t0, sps) {
const q = 1 - Math.exp(-x / 14000);
return { eval: { step: x, t: t0 + x * 1024 / sps, n: 512, n_ref: 512, success: Math.min(0.975, 0.08 + 0.87 * q + 0.012 * gauss()), collision_rate: Math.max(0.004, 0.31 * (1 - q) + 0.012 + 0.005 * gauss()),
time_ok_1p3: Math.min(0.95, 0.05 + 0.8 * q + 0.012 * gauss()), start_fail: Math.max(0, 0.05 * (1 - q) + 0.003 * gauss()) } };
time_ok_1p3: Math.min(0.95, 0.05 + 0.8 * q + 0.012 * gauss()), start_fail: Math.max(0, 0.05 * (1 - q) + 0.003 * gauss()),
ol_fk_mm: 5.5 * Math.exp(-x / 12000) + 2.0 + 0.25 * gauss(), ol_best_mm: 3.8 * Math.exp(-x / 12000) + 1.7 + 0.16 * gauss(), ol_div: Math.min(0.9, 0.16 + 0.45 * (1 - Math.exp(-x / 9000)) + 0.02 * gauss()) } };
}
function trainLog(total, from, to, sps, t0, evalEvery = 2000, wta = false) {
const L = [];
@@ -715,10 +879,10 @@ function demoApi() {
}
let sc, runs, logs, pipe, jobS, con, genAt = 0;
// 示例流水线:stages 里的 [阶段, 开始距今小时, 用时小时]
const mkPipe = (done, cur, curStartH) => { const t = now(); const st = {}; let tt = t - curStartH * 3600;
for (let i = done.length - 1; i >= 0; i--) { const [k, dur] = done[i]; st[k] = { start: tt - dur * 3600, end: tt, rc: 0, info: {} }; tt -= dur * 3600; }
if (cur) st[cur] = { start: t - curStartH * 3600, end: null, rc: null, info: {} };
return { started: tt, plan: ['kin', 'r4', 'gate1', 'gen', 'prep', 'train', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, current: cur }; };
const mkPipe = (done, cur, curStartH) => { const t = now(); const st = {}, el = {}; let tt = t - curStartH * 3600;
for (let i = done.length - 1; i >= 0; i--) { const [k, dur] = done[i]; st[k] = { start: tt - dur * 3600, end: tt, rc: 0, info: {} }; el[k] = dur * 3600; tt -= dur * 3600; }
if (cur) { st[cur] = { start: t - curStartH * 3600, end: null, rc: null, info: {} }; el[cur] = curStartH * 3600; }
return { started: tt, plan: ['kin', 'r4', 'gate1', 'gen', 'prep', 'train', 'dagger', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, elapsed: el, current: cur }; };
const CON = {
train: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== node ml/eval/dump_parity.cjs 2000\n== python -m ml.eval.check_parity\n{"PASS": true, "fk_mm": 0.0004, "gap_mm": 0.0011}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n样本数 1702000 ≥ 目标,完成\n== bash ml/prep/run_prep.sh 28\n== python -m ml.eval.check_r2 --samples ml/data/samples --out ml/data/r2.json\n{"pairs": 250, "label_std_mean": 0.071, "pass": true}\n== python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval\nstep 2000 loss 1.912 6150 samples/s\n== python -m ml.train --samples ml/data/samples --out ml/runs/base\n',
gen: '$ FROM=kin TO=accept bash ml/pipeline.sh all\n计划:kin r4 gate1 gen prep train m5 apg mine export bench accept\n== node ml/prep/export_kin.cjs\n== python -m ml.eval.check_parity\n{"PASS": true}\n== node ml/gen/consistency.cjs 300 4242\n{"consistentByGateRule": 0.86}\n一致性门禁结果 → CONSENSUS=1\n== bash ml/gen/run_parallel.sh 1786 28 1 --consensus 4\n',
@@ -804,6 +968,7 @@ function demoApi() {
fn.scenario = setup;
return fn;
}
window.__boot.ok = true; // 正文脚本跑到了最后一行,看门狗不用再报"脚本没启动"
</script>
</body>
</html>
+243 -22
View File
@@ -17,7 +17,7 @@
N-11 Windows 子进程用 CREATE_NO_WINDOW 自带隐藏控制台:关掉控制台黑窗口训练照跑,重开控制台可接着看、可停止;
N-9 生成阶段的行数按文件增量统计(只读新增字节),其他阶段不扫 shard;
N-12 停止后的状态改写走 ml.pstate(单一写者、原子替换);N-14 阶段名/估时/权重由 ml.pstate.STAGES 下发。"""
import argparse, glob, json, os, re, shutil, signal, subprocess, sys, threading, time, webbrowser
import argparse, glob, json, os, platform, re, shutil, signal, subprocess, sys, threading, time, webbrowser
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml import pstate
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
@@ -68,7 +68,102 @@ def read_log(name, offset):
return {'records': recs, 'offset': offset + end, 'size': size, 'reset': offset == 0, 'mtime': os.path.getmtime(f)}
_CPU = {'v': {}}
def cpu_name():
"""CPU 型号:psutil 只有核数,型号得自己读——Windows 在注册表,Linux 在 /proc/cpuinfo。"""
try:
if os.name == 'nt':
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'HARDWARE\DESCRIPTION\System\CentralProcessor\0') as k:
return re.sub(r'\s+', ' ', winreg.QueryValueEx(k, 'ProcessorNameString')[0]).strip()
with open('/proc/cpuinfo') as f:
for ln in f:
if ln.startswith('model name'):
return ln.split(':', 1)[1].strip()
except Exception:
pass
return platform.processor().strip()
class CpuSampler(threading.Thread):
"""后台每秒阻塞采样一次 CPU。cpu_percent 是"距上次调用"的差值:放在请求里算,窗口取决于两次
HTTP 请求的间隔(网页 1 秒轮询 + 手动 curl 会把它压成几毫秒,读数恒为 0)——所以单独一个线程按固定
1 秒窗口取,status 只读最近一次。"""
def __init__(self): super().__init__(daemon=True); self.start()
def run(self):
try:
import psutil
name = cpu_name() # 型号只读一次,别每秒翻注册表
psutil.cpu_percent(interval=None, percpu=True) # 打桩:第一次恒为 0,丢掉
while True:
per = psutil.cpu_percent(interval=1.0, percpu=True)
_CPU['v'] = {'total': round(sum(per) / max(1, len(per)), 1), 'per': [round(x) for x in per], 'n': len(per), 'model': name}
except Exception:
_CPU['v'] = {}
def cpu():
return {**_CPU['v'], **mahm()}
_MAHM = {'t': 0.0, 'v': {}}
def mahm():
"""借 MSI Afterburner 的共享内存(MAHMSharedMemory)读 CPU 温度 / 封装功耗——Windows 上没有别的免驱动口子:
psutil 在 Windows 没有 sensors_temperatures;ACPI 热区只有个 27.9 °C 的板温;Power Meter 计数器没有实例。
条目布局实测:头 32 字节(签名 MAHM / 版本 / 头大小 / 条目数 / 条目大小 / 时间戳),每条 1324 字节,
名字在前 260 字节、单位在其后 260、数值是偏移 1300 的 float(用 GPU1 power 对 nvidia-smi 校准过)。
读数比 30 秒旧才当没读到:Afterburner 的硬件轮询周期实测 10~13 秒(不是 1 秒),阈值卡 10 秒会
正好落在两次刷新之间、把还在用的读数丢掉(表现为温度和功耗一闪一闪地消失)。"""
now = time.time()
if now - _MAHM['t'] < 1.0: return _MAHM['v']
_MAHM['t'] = now
out = {}
try:
import ctypes, struct
k = ctypes.windll.kernel32
k.OpenFileMappingW.restype = ctypes.c_void_p
k.OpenFileMappingW.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_wchar_p]
k.MapViewOfFile.restype = ctypes.c_void_p
k.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_size_t]
h = k.OpenFileMappingW(0x0004, 0, 'MAHMSharedMemory')
p = k.MapViewOfFile(h, 0x0004, 0, 0, 0) if h else 0
if p:
hd = ctypes.string_at(p, 32)
num, esize, hsize = (int.from_bytes(hd[i * 4:i * 4 + 4], 'little') for i in (3, 4, 2))
upd = int.from_bytes(hd[20:24], 'little')
if 0 < num < 5000 and 32 < esize < 65536 and now - upd <= 30:
buf = ctypes.string_at(p, hsize + num * esize)
core = {}
for i in range(num):
e = buf[hsize + i * esize: hsize + (i + 1) * esize]
nm = e[:260].split(b'\x00')[0].decode('latin1')
if not nm: continue
val = struct.unpack('<f', e[1300:1304])[0]
if nm == 'CPU temperature' and 0 < val < 130: out['temp'] = round(val)
elif nm == 'CPU power' and 0 < val < 1000: out['power'] = round(val, 1)
elif nm.startswith('CPU') and nm.endswith(' temperature'):
try:
j = int(nm[3:-12])
if 0 < val < 130: core[j] = round(val)
except ValueError: pass
if core: out['coreT'] = [core[k] for k in sorted(core)]
except Exception:
out = {}
_MAHM['v'] = out
return out
_GPU_CACHE = {'t': 0.0, 'v': []}
def gpu():
# 网页 1 秒轮询:nvidia-smi 每次要 150–600 ms,缓存 2 秒(GPU 数字不需要更快),避免每轮都起子进程
if time.time() - _GPU_CACHE['t'] < 2.0: return _GPU_CACHE['v']
try:
q = 'name,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw'
r = subprocess.run(['nvidia-smi', f'--query-gpu={q}', '--format=csv,noheader,nounits'], capture_output=True, text=True, timeout=3)
@@ -77,9 +172,10 @@ def gpu():
n, u, mu, mt, t, p = [x.strip() for x in line.split(',')]
num = lambda x: float(x) if re.fullmatch(r'[\d.]+', x) else None
out.append({'name': n, 'util': num(u), 'mem_used': num(mu), 'mem_total': num(mt), 'temp': num(t), 'power': num(p)})
return out
except Exception:
return []
out = []
_GPU_CACHE.update(t=time.time(), v=out)
return out
def last_rec(name):
@@ -126,47 +222,134 @@ def newer(pattern, t0):
def stage_progress(st, s):
"""当前阶段的进度:返回 {'frac': 0..1 或 None(无法计量), 'label': 文字}。"""
"""当前阶段的进度:返回 {'frac': 0..1 或 None(无法计量), 'label': 文字, 'eta_s': 按实测速率外推的剩余秒数或 None}。"""
t0, info = s['start'], s.get('info') or {}
if st == 'gen':
n = sum(LC.count(f) for f in newer(f'{DATA}/shard_*.jsonl', t0) + newer(f'{DATA}/shard_*.jsonl.part', t0))
# 分子算全部(含之前轮次已完成的分片),分母是累计目标:补量时显示 50,008/79,688 而不是 0/79,688。
# 2026-10-18 实测:只统计本轮开始后改过的文件会让"补量"看起来像从零开始。
allf = glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl')) + glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl.part'))
n = sum(LC.count(f) for f in allf)
tgt = info.get('target_tasks') or 0
return {'frac': min(1, n / tgt) if tgt else None, 'label': f'已生成 {n:,} / {tgt:,} 条任务'}
new = sum(LC.count(f) for f in newer(f'{DATA}/shard_*.jsonl', t0) + newer(f'{DATA}/shard_*.jsonl.part', t0))
# 剩余时间按"本轮已写条数 ÷ 本轮已用时"外推(条数是实时数出来的,速率就是实测的),不再用静态估时
el = max(1.0, time.time() - t0)
rate = new / el if new > 0 else 0
eta = ((tgt - n) / rate if rate > 0 else None) if tgt > n else (0.0 if rate > 0 else None)
lab = f'已生成 {n:,} / {tgt:,} 条任务' + (f'(本轮补 {new:,} 条 · {rate * 60:.0f} 条/分)' if 0 < new < n else '')
return {'frac': min(1, n / tgt) if tgt else None, 'label': lab, 'eta_s': eta}
if st == 'gate1':
# 一致性门禁每 10 个任务往 stderr 打一行 "k/300"(consistency.cjs:41)。进度只读控制台日志最后 64 KB,
# 不跑任何计算、不占 CPU —— 正在生成的节点不受影响。日志里可能还留着上一次门禁的进度行,
# 所以只数最后一个 "consistency.cjs" 命令行之后的部分。
txt = console(0).get('text') or ''
cut = txt.rfind('consistency.cjs')
m = re.findall(r'^(\d+)/(\d+)\s*$', txt[cut:] if cut >= 0 else '', re.M)
n, N = (int(m[-1][0]), int(m[-1][1])) if m else (0, 300)
el = time.time() - t0
rate = n / el if n > 0 and el > 2 else 0
return {'frac': n / N if N else None,
'label': f'门禁 {n} / {N} 个任务' + (f' · {rate * 60:.0f} 个/分' if rate else '') if n else '准备中(日志里还没有进度行)',
'eta_s': (N - n) / rate if rate > 0 else None}
if st == 'prep':
nsh = len(glob.glob(os.path.join(ROOT, DATA, 'shard_*.jsonl')))
done = len(newer(f'{DATA}/samples/*.X.npy', t0))
if os.path.exists(os.path.join(ROOT, DATA, 'r2.json')) and newer(f'{DATA}/r2.json', t0): return {'frac': 1, 'label': '打包完成,R2 检查完成'}
return {'frac': min(0.95, done / nsh) if nsh else None, 'label': f'已打包 {done} / {nsh} 个分片' + (',R2 检查中' if nsh and done >= nsh else '')}
fin = newer(f'{DATA}/samples/*.X.npy', t0)
done = len(fin)
if os.path.exists(os.path.join(ROOT, DATA, 'r2.json')) and newer(f'{DATA}/r2.json', t0): return {'frac': 1, 'label': '打包完成,R2 检查完成', 'eta_s': 0}
# 2026-10-19:build_dataset 每 10% 打一行 [prep] shard_xxxx a/b;用它把"整片完成"之间的进度和 ETA 算出来
fin_n = {os.path.basename(p).split('.')[0] for p in fin}
part = {}
for ln in console(0)['text'].splitlines()[-800:]:
m = re.search(r'\[prep\] (shard_\d+) (\d+)/(\d+)', ln)
if m: part[m.group(1)] = int(m.group(2)) / max(1, int(m.group(3)))
run = {k: v for k, v in part.items() if k not in fin_n}
frac = (done + sum(run.values())) / nsh if nsh else None
el = time.time() - t0
eta = el * (1 - frac) / frac if frac and 0.02 < frac < 1 and el > 5 else None
lab = f'已打包 {done} / {nsh} 个分片'
if run: lab += f' · 进行中 {len(run)} 个(平均 {100 * sum(run.values()) / len(run):.0f}%)'
elif done == 0: lab += ' · 首波处理中,第一批完成后显示预估'
elif nsh and done >= nsh: lab += ',R2 检查中'
return {'frac': min(0.95, frac) if frac is not None else None, 'label': lab, 'eta_s': eta}
if st == 'train':
tag, bs = info.get('tag', 'base'), info.get('bench_steps', 2000)
b, m = last_rec(f'bench_{tag}') if newer(f'{RUNS}/bench_{tag}/log.jsonl', t0) else None, last_rec(tag) if newer(f'{RUNS}/{tag}/log.jsonl', t0) else None
if m and m.get('total'):
return {'frac': (bs + m['step']) / (bs + m['total']), 'label': f'正式训练 {m["step"]:,} / {m["total"]:,} 步', 'run': tag}
if b: return {'frac': b['step'] / (bs + 60000), 'label': f'测速 {b["step"]:,} / {bs:,} 步', 'run': f'bench_{tag}'}
return {'frac': (bs + m['step']) / (bs + m['total']), 'label': f'正式训练 {m["step"]:,} / {m["total"]:,} 步', 'run': tag,
'eta_s': m['eta_h'] * 3600 if m.get('eta_h') else None} # 训练脚本自己按实测样本/s 算的
if b: return {'frac': b['step'] / (bs + 24000), # 2026-10-19:正式步数按 configs/base.json 的 24000 估
'label': f'测速 {b["step"]:,} / {bs:,} 步', 'run': f'bench_{tag}'}
return {'frac': 0, 'label': '加载数据中'}
if st == 'dagger': # 2026-10-18 新增:每轮 = 采起点 → 老师重规划 → 打包 → 微调(微调占大头,进度按 0.1/0.25/0.05/0.6 分)
tag, R = info.get('tag', 'base'), int(info.get('rounds', 2))
k = len(newer(f'{RUNS}/{tag}_dag*/done', t0)); k = min(k, R)
base = f'第 {min(k + 1, R)} / {R} 轮'
if k >= R: return {'frac': 1, 'label': 'DAgger 完成'}
D, run = f'{DATA}/dag_{tag}_r{k + 1}', f'{tag}_dag{k + 1}'
m = last_rec(run) if newer(f'{RUNS}/{run}/log.jsonl', t0) else None
if m and m.get('total'):
return {'frac': (k + 0.4 + 0.6 * m['step'] / m['total']) / R, 'label': f'{base}:微调 {m["step"]:,} / {m["total"]:,} 步', 'run': run,
'eta_s': m['eta_h'] * 3600 if m.get('eta_h') else None}
if os.path.exists(os.path.join(ROOT, D, 'collect.json')):
nt = sum(LC.count(f) for f in glob.glob(os.path.join(ROOT, D, 'tasks_*.jsonl')))
nd = sum(LC.count(f) for f in glob.glob(os.path.join(ROOT, D, 'shard_dag_*.jsonl')) + glob.glob(os.path.join(ROOT, D, 'shard_dag_*.jsonl.part')))
return {'frac': (k + 0.1 + 0.25 * (nd / nt if nt else 0)) / R, 'label': f'{base}:老师重新规划 {nd:,} / {nt:,} 个起点'}
return {'frac': k / R, 'label': f'{base}:闭环采起点'}
if st == 'apg':
r = last_rec('apg') if newer(f'{RUNS}/apg/log.jsonl', t0) else None
return {'frac': r['it'] / r['total'] if r and r.get('total') else 0, 'label': f'APG {r["it"]:,} / {r["total"]:,} 步' if r else '准备中', 'run': 'apg'}
eta = None
if r and r.get('total'):
it, mn = r.get('it') or 0, r.get('min') or 0
eta = (r['total'] - it) / (it / mn) * 60 if it > 0 and mn > 0.1 else None # 步/分实测外推
return {'frac': r['it'] / r['total'] if r and r.get('total') else 0, 'label': f'APG {r["it"]:,} / {r["total"]:,} 步' if r else '准备中', 'run': 'apg', 'eta_s': eta}
if st == 'mine':
R = info.get('rounds', 6)
k = len(newer(f'{RUNS}/mine/r*/gate.json', t0))
cur = sorted(newer(f'{RUNS}/apg_r*/log.jsonl', t0), key=os.path.getmtime)
frac_in, lab = 0, f'第 {k + 1} 轮:挖错出题'
if cur and len(cur) > k:
name = os.path.basename(os.path.dirname(cur[-1])); r = last_rec(name)
if r and r.get('total'): frac_in = 0.3 + 0.6 * r['it'] / r['total']; lab = f'第 {k + 1} 轮:APG {r["it"]:,} / {r["total"]:,} 步'
return {'frac': min(1, (k + frac_in) / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)', 'run': name}
lab = f'第 {k + 1} 轮:挖错出题'
if cur:
name = os.path.basename(os.path.dirname(cur[-1])) # ml/runs/apg_rN
rn = re.sub(r'\D', '', name) or '1'
gate = os.path.join(ROOT, RUNS, 'mine', f'r{rn}', 'gate.json')
r = last_rec(name)
if r and r.get('total') and not os.path.exists(gate): # 这一轮还在跑(还没出 gate)
frac_in = 0.3 + 0.6 * r['it'] / r['total']
lab = f'第 {k + 1} 轮:APG {r["it"]:,} / {r["total"]:,} 步'
it, mn = r.get('it') or 0, r.get('min') or 0
eta = (r['total'] - it) / (it / mn) * 60 if it > 0 and mn > 0.1 else None
if eta is not None and k >= 1: # 剩余轮次按已完成轮次的平均时长外推
eta += max(0, R - k - 1) * (time.time() - t0) / k
return {'frac': min(1, (k + frac_in) / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)', 'run': name, 'eta_s': eta}
return {'frac': min(1, k / R), 'label': f'{lab}(共最多 {R} 轮,提升不足会提前停)'}
if st == 'm5':
tag = info.get('tag', 'base')
n = len(newer(f'{RUNS}/m5_{tag}_policy.json', t0)) + len(newer(f'{RUNS}/m5_{tag}_safety.json', t0))
return {'frac': n / 2, 'label': ['纯策略评估中', '加安全层评估中', '汇总'][min(n, 2)]}
el = time.time() - t0
eta = (2 - n) * (el / n) if n == 1 else (0.0 if n >= 2 else None) # 两次评估(纯策略 / 加安全层)按已完成的平均耗时外推
return {'frac': n / 2, 'label': ['纯策略评估中', '加安全层评估中', '汇总'][min(n, 2)], 'eta_s': eta}
return {'frac': None, 'label': '进行中'}
def stage_elapsed(pl, now=None):
"""每个阶段的累计用时(秒):pipeline.json 的 time{阶段: 秒} 是不含当前段的累计,运行中的阶段
再把"这一段"实时加上。老记录(没有 time 字段)用 start/end 的差值兜底。
网页的「总用时」=各阶段之和,「本步用时」=当前阶段这条 —— 确定/续跑都不归零。"""
now = now or time.time()
acc = pl.get('time') or {}
cur = pl.get('current')
out = {}
for k, r in (pl.get('stages') or {}).items():
t = acc.get(k)
if t is None and r.get('start') and r.get('end'): t = r['end'] - r['start']
t = t or 0
if k == cur and r.get('start') and r.get('end') is None: t += max(0.0, now - r['start'])
out[k] = round(t, 1)
return out
def pipeline():
d = jload(os.path.join(ROOT, RUNS, 'pipeline.json'))
if not d: return None
d['elapsed'] = stage_elapsed(d)
cur = d.get('current')
if cur and cur in d['stages']:
try: d['progress'] = stage_progress(cur, d['stages'][cur])
@@ -192,8 +375,9 @@ def status():
pb = jload(P(RUNS, 'policy_bench.json'))
if pb: pb = {k: {kk: v.get(kk) for kk in ('n', 'ok', 'reached', 'collisionFrames', 'brakeRate', 'recoveryRate', 'livelock', 'inferMsP99', 'safetyMsP99')}
for k, v in pb.items() if isinstance(v, dict) and 'n' in v}
return {'now': time.time(), 'gpu': gpu(),
'vram_budget_gb': float(os.environ.get('VRAM_BUDGET_GB', 20)), # 本机桌面常驻占 4–5 GB,给训练的预算 19–20 GB
return {'now': time.time(), 'gpu': gpu(), 'cpu': cpu(),
'vram_reserve_gb': float(os.environ.get('VRAM_RESERVE_GIB', 1)), # 给系统留的余量(GB):训练进程上限 = 整卡 − 已用 − 这个余量(浮动,见 ml/common/vram.py)
'vram_budget_gb': float(os.environ['VRAM_BUDGET_GB']) if os.environ.get('VRAM_BUDGET_GB') else None, # 只有显式指定才覆盖上面的口径
'pipeline': pl, 'check': CHECK.get(), 'lock': lock_info(), # 2026-10-18 第八轮修:上一版这三项被行尾注释吞掉,网页步骤条/锁状态拿不到
'stage_meta': [{'id': k, 'name': n, 'est_h': h, 'timeout_h': pstate.TIMEOUT_H[k]} for k, n, h in pstate.STAGES],
'r4': jload(P(RUNS, 'r4.json')),
@@ -208,7 +392,7 @@ def status():
STAGES = pstate.ORDER
ENV_KEYS = {'N_TASKS': r'\d+', 'NPROC': r'\d+', 'TARGET_SAMPLES': r'\d+', 'CFG': r'ml/configs/(base|wta|small)\.json',
'CONSENSUS': r'[01]?', 'ROUNDS': r'\d+', 'APG_STEPS': r'\d+', 'MINE_N': r'\d+', 'MIN_GAP_MM': r'\d+(\.\d+)?',
'FRESH': r'[01]', 'R2_STRICT': r'[01]', 'AUTO_WTA': r'[01]'}
'FRESH': r'[01]', 'FRESH_FROM': r'(setup|kin|r4|gate1|gen|prep|train|dagger|m5|apg|mine|export|bench|accept)', 'R2_STRICT': r'[01]', 'AUTO_WTA': r'[01]'}
CONSOLE = os.path.join(ROOT, RUNS, 'console.log')
@@ -264,6 +448,29 @@ def lock_info():
return dict(lk, alive=pstate.lock_alive(lk), age_s=time.time() - lk.get('beat', 0))
NT_NAMES = "'node.exe','xargs.exe','bash.exe','sh.exe','timeout.exe'"
NT_SWEEP = (r"pipeline\.sh|run_prep\.sh|run_parallel\.sh|build_dataset\.cjs|"
r"gen_dataset\.cjs|teacher\.cjs|policy_bench\.cjs")
def sweep_nt(keep_pid):
"""taskkill /T 在 Git Bash 下只杀到 bash,xargs/node 孙进程会逃(10-18 实测:停止后还有 28 个
gen_dataset 在跑;当晚二次踩到:两批 build_dataset 抢同一批 shard)。按命令行补扫整条流水线,
keep_pid 的祖先链(= 控制台自己拉起的 monitor bash)不动。先杀包装再杀 node,否则 node 一死
xargs 会立刻补下一个 shard。返回仍存活个数,-1 = pwsh 没跑成。"""
q = ("$anc=@(); $c=%d; while ($c -gt 0 -and ($p=Get-CimInstance Win32_Process -Filter \"ProcessId=$c\" "
"-ErrorAction SilentlyContinue)) { $anc+=$p.ProcessId; $c=$p.ParentProcessId }; "
"$m={ $_.Name -in %s -and $_.CommandLine -match '%s' -and $anc -notcontains $_.ProcessId }; "
"foreach ($x in @(Get-CimInstance Win32_Process | Where-Object $m | "
"Sort-Object { if ($_.Name -eq 'node.exe') { 1 } else { 0 } })) "
"{ Stop-Process -Id $x.ProcessId -Force -ErrorAction SilentlyContinue }; "
"Start-Sleep -Seconds 2; "
"@(Get-CimInstance Win32_Process | Where-Object $m).Count" % (int(keep_pid), NT_NAMES, NT_SWEEP))
r = subprocess.run(['pwsh', '-NoProfile', '-Command', q], capture_output=True, text=True, timeout=90)
lines = [s for s in (r.stdout or '').split() if s.isdigit()]
return int(lines[-1]) if lines else -1
def kill_session(sid):
"""POSIX:结束会话 sid 里的所有进程(含 timeout 另起的进程组)。"""
pids = []
@@ -331,7 +538,7 @@ class Job:
pid = self.p.pid if ours else int(lk.get('pid') or 0)
if not pid: return {'error': '锁里没有 pid,无法停止'}
cur = (pstate.load().get('current') or '')
if cur in ('train', 'm5', 'apg', 'mine'):
if cur in ('train', 'dagger', 'm5', 'apg', 'mine'):
open(STOPF, 'w').write(time.strftime('%H:%M:%S'))
with open(CONSOLE, 'a', encoding='utf-8') as f: f.write(f'\n[停止请求已发出,等待 {cur} 存档 last.pt…]\n')
for _ in range(180):
@@ -341,6 +548,14 @@ class Job:
with self.lock:
if os.name == 'nt':
subprocess.run(['taskkill', '/T', '/F', '/PID', str(pid)], capture_output=True)
# 补扫漏网的子孙;有残留就写进网页日志,别让用户以为停干净了
try:
left = sweep_nt(os.getpid())
except Exception:
left = -1
if left != 0:
with open(CONSOLE, 'a', encoding='utf-8') as f:
f.write(f'\n[停止补扫:仍有 {left} 个流水线子进程存活(-1=补扫没跑成),把这条发我]\n')
else:
if ours:
kill_session(pid)
@@ -393,6 +608,11 @@ class H(BaseHTTPRequestHandler):
if u.path == '/api/status': return self.send(200, json.dumps(status()), 'application/json')
if u.path == '/api/job': return self.send(200, json.dumps(JOB.state()), 'application/json')
if u.path == '/api/console': return self.send(200, json.dumps(console(int(q.get('offset', ['0'])[0]))), 'application/json')
if u.path == '/api/explore': # 2026-10-20:闭环探索日志(docs/探索日志.md)回写控制台
try:
with open(os.path.join(ROOT, 'docs', '探索日志.md'), encoding='utf-8') as f: md = f.read()
except Exception: md = ''
return self.send(200, json.dumps({'md': md}), 'application/json')
self.send(404, 'not found', 'text/plain')
except Exception as e:
self.send(500, json.dumps({'error': str(e)}), 'application/json')
@@ -429,6 +649,7 @@ def main():
a = ap.parse_args()
global CHECK
CHECK = SelfCheck()
CPUS = CpuSampler() # 每秒采一次 CPU(线程常驻,status 只读最近一次)
srv = ThreadingHTTPServer((a.host, a.port), H); srv.remote_control = a.remote_control
url = f'http://localhost:{a.port}/'
print(f'训练控制台:{url} (关闭本窗口只关控制台,已开始的训练继续跑;重新打开控制台即可接着看、可停止)', flush=True)
+39 -11
View File
@@ -7,6 +7,7 @@
# gen 生成老师轨迹(NPROC 进程;CONSENSUS=1 时加共识过滤;样本不足 TARGET_SAMPLES 自动补生成)
# prep 打包训练样本 + R2 标签一致性检查
# train 预训练(先 2000 步摸吞吐,再正式训练);输出目录随头分开:ml/runs/<CFG 名>
# dagger DAgger 纠偏(2026-10-18 新增):策略闭环走到的状态请老师重新规划、并入数据微调 DAG_ROUNDS 轮(ml/dagger/run.sh)
# m5 头门禁:贴立柱集穿柱率 ≤ 2%(纯策略口径),同时报仿真安全层口径;通过则记为选定模型,不过自动训 WTA 头再判
# apg APG 首轮微调(从选定模型开始)
# mine 挖错循环(3–6 轮,含门禁回滚)
@@ -44,11 +45,12 @@ 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"
run() { echo "== $*" >&2; "$@"; } # 回显走 stderr:gate1 用 tee 把 stdout 存成 consistency.json,回显混进去会让后面读它时 JSONDecodeError
ALL_STAGES="setup kin r4 gate1 gen prep train dagger 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" ;;
dagger) echo "tag=$TAG rounds=${DAG_ROUNDS:-2}" ;;
m5) echo "tag=$TAG" ;;
mine) echo "rounds=$ROUNDS apg_steps=$APG_STEPS" ;;
esac; }
@@ -76,15 +78,37 @@ on_exit() {
trap on_exit EXIT
trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返回码、放锁
# ---- 训练与门禁的单元(m5 自动分支要对两种头各调一次)
replay_ok() { # 回放自检结果有效:pass 为真、版本含 oracle 滚动(ver ≥ 2)、指纹 = 当前 kin.json 且 = 数据 shard 的指纹
$PY - <<'PY'
import json, sys
try:
r = json.load(open('ml/runs/replay_teacher.json', encoding='utf-8')); k = json.load(open('ml/assets/kin.json', encoding='utf-8'))['fp']
except Exception: sys.exit(1)
sys.exit(0 if r.get('pass') is True and r.get('ver', 1) >= 2 and r.get('fp') == k and r.get('data_fp') in (None, k) else 1)
PY
}
train_one() { # $1 = 配置文件
local cfg=$1 tag; tag=$(basename "$1" .json)
# 2026-10-18:开训前先把老师轨迹放进 PyTorch 仿真口径里回放——口径对不上时闭环指标不可信,训多少步都白训(rc 5 停)
# 2026-10-19(审查 review_closedloop F6):原来只看文件在不在——replay 失败时也写了文件(pass=false)再 rc 5,用户直接重跑 train 门禁就被跳过。
# 现在读 pass 字段 + 指纹(kin.json、数据 shard),不过就重跑一次,仍不过 rc 5;回放同时走完整 rollout()(老师 oracle),覆盖滚动路径。
replay_ok || run $PY -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json || true
replay_ok || { echo "老师回放自检未通过(ml/runs/replay_teacher.json 的 pass / 指纹),闭环指标不可信,不开训"; exit 5; }
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 ${BENCH_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
dagger_one() { # $1 = 头标签:从该头的预训练结果起做 DAgger,最终模型路径写进 ml/runs/<tag>_dagger.ckpt
local tag=$1 ck
[ -f ml/runs/$tag/best.pt ] || [ -f ml/runs/$tag/last.pt ] || { echo "DAgger:ml/runs/$tag/ 下没有 checkpoint(预训练没完成)"; return 3; }
# 2026-10-19(审查 P0-2):闭环 success > 0 用 best.pt,否则 last.pt(都取 EMA 权重)
ck=$($PY -m ml.common.ckpt pick ml/runs/$tag)
run bash ml/dagger/run.sh $tag $ck
}
m5_one() { # $1 = 头标签;返回 0 通过 / 3 不通过。起点模型:CKPT > DAgger 结果 > 预训练 best.pt
local tag=$1 ck o
if [ -n "${CKPT:-}" ]; then ck=$CKPT; elif [ -f ml/runs/${1}_dagger.ckpt ]; then ck=$(cat ml/runs/${1}_dagger.ckpt); else ck=ml/runs/$1/best.pt; fi
[ -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 ${M5_N:-1000} --out $o
@@ -127,19 +151,21 @@ case $stage in
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 续跑时跳过。
# 最多 5 次(第五轮由 2 改 4;2026-10-18 应用户要求 4 改 5:共识 + 间隙两道过滤叠加时余量更足);已写完的补量 shard 续跑时跳过。
s0=$((1 + NPROC))
for k in 1 2 3 4; do
for k in 1 2 3 4 5; do
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))")
[ "$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 ))
# 目标 = 已写完的分片条数 + 本轮要补的;不能用 pstate 里上次的 target_tasks 累加,
# 那样每次续跑(点停止后再「继续训练」)分母都加一截,进度条会越续越少(2026-10-18 实测 67368→84728)。
done_n=$($PY -c "import json;print(json.load(open('ml/data/stats.json', encoding='utf-8'))['tasks'])")
$PY -m ml.pstate set gen target_tasks=$(( done_n + 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', encoding='utf-8'));import sys;sys.exit(0 if s['samplesEvery3']>=0.9*$TARGET_SAMPLES else 7)" || echo "警告:补 4 次后样本仍不足目标的 90%,按现有样本继续(stats.json 见 samplesEvery3)" ;;
$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 "警告:补 5 次后样本仍不足目标的 90%,按现有样本继续(stats.json 见 samplesEvery3)" ;;
prep) # 间隙过滤口径必须与生成时一致(审查 N-1)
$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)" \
|| { echo "MIN_GAP_MM=$MIN_GAP_MM 与生成阶段 stats.json 的 minGapFilterMm 不一致:补量口径与训练集会分叉,请用同一值重跑 gen 或 prep"; exit 8; }
@@ -152,6 +178,7 @@ case $stage in
echo "警告:R2 不通过(近邻标签不自洽,见 ml/data/r2.json)。按默认继续训练,验收汇总会列出"
fi ;;
train) train_one $CFG ;;
dagger) dagger_one $TAG ;;
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
if m5_one $TAG; then :
elif [ "$TAG" != wta ] && [ "${AUTO_WTA:-1}" = 1 ] && [ -z "${CKPT:-}" ]; then
@@ -159,11 +186,12 @@ case $stage in
echo "M5:$TAG 头不通过 → 自动训练 WTA 回退头(约 3 小时)"
$PY -m ml.pstate set m5 branch=wta
train_one ml/configs/wta.json
dagger_one wta
m5_one wta || { echo "M5:两种头都未通过,把 ml/runs/m5_gate_*.json 发我"; exit 3; }
elif [ "${M5_STRICT:-1}" = 0 ]; then echo "M5:不通过,M5_STRICT=0(冒烟)→ 照常继续"; echo ml/runs/$TAG/best.pt > $SEL
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 ${APG_FIRST_STEPS:+--steps $APG_FIRST_STEPS} $res; cp ml/runs/apg/best.pt ml/runs/current.pt ;;
run $PY -m ml.apg.apg_finetune --init $ck --samples ml/data/samples $(ls -d ml/data/samples_dag_* 2>/dev/null | tr '\n' ' ') --out ml/runs/apg ${APG_FIRST_STEPS:+--steps $APG_FIRST_STEPS} $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:-} ;;
selftest) run bash ml/selftest.sh ;;
@@ -184,7 +212,7 @@ case $stage in
# 启动前自检(审查 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"
fl=""; [ "${FRESH:-0}" = 1 ] && fl="--fresh"; [ -n "${FRESH_FROM:-}" ] && fl="--fresh-from=$FRESH_FROM"
$PY -m ml.pstate reset ${PLAN// /,} $fl # 续跑 / 参数变了作废受影响阶段(旧产物移到 ml/archive/<时间>/)
TODO=$($PY -m ml.pstate todo)
echo "计划:$PLAN"; echo "待跑:${TODO:-(全部已完成)}"
+49 -19
View File
@@ -2,16 +2,33 @@
* node ml/prep/build_dataset.cjs ml/data/shard_0001.jsonl [outDir=ml/data/samples]
* 每条 keep=true 的轨迹按 50 Hz 时间律重放,每 3 步取一个样本:
* X [N,83] 特征(webapp/js/ik-policy.js features(),与部署同一份代码)
* Y [N,16,6] 未来 16 步每步增量 / STEP(到终点后补 0)
* Y [N,16,6] 未来 16 步每步增量 / (ACT_SCALE·STEP),夹在 ±1(到终点后补 0)
* V [N] 剩余时间(s)/ 5
* SG [N,13] 子目标标签:剩余两个途经点相对当前 q 的差 / 180(不足用终点补)+ 是否需要
* TID[N] 任务编号(tasks.jsonl 行号所在的全局 id,用于划分与闭环验证)
* 增广:30% 样本对 q_t 加 σ=1° 噪声,标签改为 8 步内线性收回老师轨迹(噪声态须无干涉且间隙 > 1 mm);
* 已提交前缀长度 r ∈ {0..3} 均匀随机,前缀取老师接下来 r 步的增量。 */
* 增广(2026-10-18 首次正式预训练后加大):40% 样本对 q_t 加 σ=AUG_SIGMA(默认 3°)噪声,标签改为 AUG_BACK(默认 12)步内线性收回老师轨迹;
* 噪声态须无干涉且间隙 > MIN_GAP_MM,收回途中第 3/6/9/12 步也须无干涉(σ 加大后直接线性拉回可能穿过障碍)。
* 原为 30%、σ=1°、8 步:闭环一偏出 1° 就是没见过的状态,首次 21850 步闭环到达 0%。更大的偏离由 DAgger 阶段补(ml/dagger)。
* 已提交前缀长度 r ∈ {0..3} 均匀随机,前缀取老师接下来 r 步的增量。
* meta [<shard>.meta.json] 打包参数:data_ver、act_scale、增广参数、sig(run_prep.sh 据 sig 判断要不要重新打包)。
* 2026-10-19(审查 review_data_actions C2):
* - 标签按 ACT_SCALE·STEP 归一化(默认 1.25,DATA_VER 2)。原来按 STEP 归一化,巡航时主导轴标签恰好 = ±1、执行端又夹在 ±1,
* 模型在 ±1 附近的对称误差有一半被截掉,主导轴系统性偏慢;留 25% 余量后老师标签最大 0.8,执行端的 ±1 只防越界。
* 运行时 inc = clamp(act, ±1)·act_scale·STEP(closed_loop.py / apg / ik-policy.js 同口径,act_scale 经 checkpoint 与 ONNX meta 传递);
* - 增广收回标签的亏空结转:被 ±1 截掉的部分记在 prevP 里、下一步补,原来 prevP = 目标点,截掉就丢了(教的是「落后了别追」);
* - DAgger 任务带 qPrev / v0(gen_dataset.cjs 写入):t=0 样本用真实上一帧,时间律从当前速度起步。
* 特征(83 维)定义不变,FEAT_VER 不变;数据版本由 meta.data_ver 区分,旧打包没有 meta → 训练按 act_scale 1.0 读。 */
'use strict';
const fs = require('fs'), path = require('path');
/* 打包参数(环境变量)与签名:run_prep.sh 用 `node build_dataset.cjs --sig` 取当前签名,与已打包 shard 的 meta.sig 不同就重新打包 */
const DATA_VER = 2;
const MIN_GAP_MM = +(process.env.MIN_GAP_MM || 3);
const AUG_P = +(process.env.AUG_P || 0.4), AUG_SIGMA = +(process.env.AUG_SIGMA || 3), AUG_BACK = +(process.env.AUG_BACK || 12);
const ACT_SCALE = +(process.env.ACT_SCALE || 1.25);
const SIG = ['v' + DATA_VER, 's' + ACT_SCALE, 'p' + AUG_P, 'sg' + AUG_SIGMA, 'b' + AUG_BACK, 'g' + MIN_GAP_MM].join('|');
if (process.argv[2] === '--sig') { console.log(SIG); process.exit(0); }
const IN = process.argv[2], OUTDIR = process.argv[3] || path.join(__dirname, '../data/samples');
if (!IN) { console.error('usage: build_dataset.cjs <shard.jsonl> [outDir]'); process.exit(1); }
if (!IN) { console.error('usage: build_dataset.cjs <shard.jsonl> [outDir] | --sig'); process.exit(1); }
const { S } = require('../../tests/route_bench_setup.cjs')();
const POL = require('../../webapp/js/ik-policy.js');
const { timeParam } = require('../gen/timelaw.cjs');
@@ -26,35 +43,45 @@ const hash = s => { let h = 2166136261; for (const c of s) h = Math.imul(h ^ c.c
const X = [], Y = [], V = [], SG = [], TID = [], tasks = [];
const lines = fs.readFileSync(IN, 'utf8').split('\n').filter(Boolean);
let nAug = 0, nAugRej = 0, nGapDrop = 0;
/* 2026-10-18:认证最小间隙 < 3 mm 的老师轨迹不进训练集;噪声增广接受地板同步为 3 mm(原 1 mm)。 */
const MIN_GAP_MM = +(process.env.MIN_GAP_MM || 3);
for (const line of lines) {
/* 2026-10-18:认证最小间隙 < 3 mm 的老师轨迹不进训练集(MIN_GAP_MM);噪声增广接受地板同步为 3 mm(原 1 mm)。 */
const ASTEP = STEP.map(v => v * ACT_SCALE); // 每单位动作对应的度数
const NT = lines.length, PSTEP = Math.max(1, Math.floor(NT / 10));
for (let li = 0; li < NT; li++) {
if (li % PSTEP === 0) console.log(`[prep] ${base} ${li}/${NT}`); // 每 10% 一行,28 个分片并行也看得见进度
const line = lines[li];
const r = JSON.parse(line);
if (!r.keep) continue;
if (r.cert && r.cert.minGap != null && r.cert.minGap < MIN_GAP_MM) { nGapDrop++; continue; } // 与 stats.cjs 同一过滤
const tid = hash(r.id) & 0x7fffffff;
const split = (hash(r.id + '#split') % 100) < 5 ? 'val' : 'train';
const P = r.path;
const tp = timeParam(P);
const tp = timeParam(P, { v0: r.v0 || 0 }); // DAgger 起点带速度:时间律从 v0 起步(gen_dataset.cjs 写 v0)
const traj = tp.traj, taus = tp.taus, N = traj.length - 1;
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, T: r.T, R: r.R, exec_s: N * POL.DT, travel: r.travel_deg, reached: r.reached, err: r.err, split });
tasks.push({ tid, id: r.id, cat: r.cat, mode: r.mode, q0: r.q0, qp0: r.qPrev || null, T: r.T, R: r.R, exec_s: N * POL.DT, travel: r.travel_deg, reached: r.reached, err: r.err, split });
const vt = tp.cum; // 顶点 τ
const inter = P.slice(1, -1).map((p, i) => ({ p, tau: vt[i + 1] }));
for (let t = 0; t <= N; t += 3) {
let q = traj[t].slice(), qPrev = (t > 0 ? traj[t - 1] : traj[0]).slice();
let q = traj[t].slice(), qPrev = (t > 0 ? traj[t - 1] : (r.qPrev || traj[0])).slice();
let off = [0, 0, 0, 0, 0, 0], aug = false;
if (rnd() < 0.3) {
const qn = q.map((v, j) => { let x = v + gauss(); const t2 = L[O[j]]; if (!t2.cont) x = Math.max(t2.lo, Math.min(t2.hi, x)); return x; });
if (rnd() < AUG_P) {
const qn = q.map((v, j) => { let x = v + AUG_SIGMA * gauss(); const t2 = L[O[j]]; if (!t2.cont) x = Math.max(t2.lo, Math.min(t2.hi, x)); return x; });
nAug++;
if (S.collideFree(qn) && S.clearance(qn, 5) > MIN_GAP_MM) { off = qn.map((v, j) => v - q[j]); q = qn; qPrev = qPrev.map((v, j) => v + off[j]); aug = true; }
else nAugRej++;
const o2 = qn.map((v, j) => v - q[j]);
const back = k => traj[Math.min(N, t + k)].map((v, j) => v + o2[j] * Math.max(0, 1 - k / AUG_BACK));
if (S.collideFree(qn) && S.clearance(qn, 5) > MIN_GAP_MM && [3, 6, 9, 12].every(k => k >= AUG_BACK || S.collideFree(back(k)))) {
off = o2; q = qn; qPrev = qPrev.map((v, j) => v + off[j]); aug = true;
} else nAugRej++;
}
const pos = k => { const i = Math.min(N, t + k); const w = aug ? Math.max(0, 1 - k / 8) : 0; return traj[i].map((v, j) => v + off[j] * w); };
const pos = k => { const i = Math.min(N, t + k); const w = aug ? Math.max(0, 1 - k / AUG_BACK) : 0; return traj[i].map((v, j) => v + off[j] * w); };
const y = new Float32Array(H * 6);
let prevP = q;
for (let k = 1; k <= H; k++) { const pk = pos(k); for (let j = 0; j < 6; j++) y[(k - 1) * 6 + j] = Math.max(-1, Math.min(1, (pk[j] - prevP[j]) / STEP[j])); prevP = pk; }
/* prevP = 按标签实际会走到的位置:被 ±1 截掉的亏空留在 prevP 与目标之差里,下一步接着补(2026-10-19,C2) */
let prevP = q.slice();
for (let k = 1; k <= H; k++) {
const pk = pos(k);
for (let j = 0; j < 6; j++) { const v = Math.max(-1, Math.min(1, (pk[j] - prevP[j]) / ASTEP[j])); y[(k - 1) * 6 + j] = v; prevP[j] += v * ASTEP[j]; }
}
const rr = Math.floor(rnd() * 4), prefix = [];
for (let k = 0; k < rr; k++) { const row = []; for (let j = 0; j < 6; j++) row.push(y[k * 6 + j] * STEP[j]); prefix.push(row); }
for (let k = 0; k < rr; k++) { const row = []; for (let j = 0; j < 6; j++) row.push(y[k * 6 + j] * ASTEP[j]); prefix.push(row); }
const remain = inter.filter(v => v.tau > taus[t] + 0.02);
const sub = remain.length && rnd() < 0.8 ? remain[0].p : null;
const gm = S.gapMatrix(q, POL.GAP_CAP);
@@ -74,7 +101,10 @@ writeNpy(path.join(OUTDIR, base + '.Y.npy'), cat(Y, H * 6), [n, H, 6]);
writeNpy(path.join(OUTDIR, base + '.V.npy'), Float32Array.from(V), [n]);
writeNpy(path.join(OUTDIR, base + '.SG.npy'), cat(SG, 13), [n, 13]);
writeNpy(path.join(OUTDIR, base + '.TID.npy'), TID, [n], 'int32');
/* meta 在 tasks.jsonl 之前写:tasks.jsonl 仍是「本 shard 打包完成」的标记 */
fs.writeFileSync(path.join(OUTDIR, base + '.meta.json'), JSON.stringify({ data_ver: DATA_VER, act_scale: ACT_SCALE, aug_p: AUG_P, aug_sigma: AUG_SIGMA,
aug_back: AUG_BACK, min_gap_mm: MIN_GAP_MM, aug_carry: true, sig: SIG }));
// 2026-10-18 断点续跑:tasks.jsonl 最后写、先写 .tmp 再改名 —— 它存在就代表这个 shard 的 5 个 npy 都已写完,run_prep.sh 据此跳过
const tf = path.join(OUTDIR, base + '.tasks.jsonl');
fs.writeFileSync(tf + '.tmp', tasks.map(t => JSON.stringify(t)).join('\n') + '\n'); fs.renameSync(tf + '.tmp', tf);
console.log(JSON.stringify({ shard: base, tasks: tasks.length, samples: n, aug: nAug, augRejected: nAugRej, gapDrop: nGapDrop, featVer: POL.FEAT_VER }));
console.log(JSON.stringify({ shard: base, tasks: tasks.length, samples: n, aug: nAug, augRejected: nAugRej, gapDrop: nGapDrop, featVer: POL.FEAT_VER, dataVer: DATA_VER, actScale: ACT_SCALE }));
+6 -1
View File
@@ -2,6 +2,8 @@
# 并行打包样本:bash ml/prep/run_prep.sh [进程数=CPU 核数]
# 2026-10-18 断点续跑:samples/<shard>.tasks.jsonl 存在且比 shard 新 → 该 shard 已打包完,跳过;
# shard 已不存在的旧打包结果删掉(不让上一批数据混进来)。中断后重跑只补没打完的 shard。
# 2026-10-19(审查 review_closedloop F11):打包参数(数据版本、ACT_SCALE、AUG_P/AUG_SIGMA/AUG_BACK、MIN_GAP_MM)变了也要重打:
# build_dataset.cjs --sig 给出当前签名,已打包 shard 的 <shard>.meta.json 里 sig 不同(或没有 meta 的旧打包)→ 重新打包。
set -e
cd "$(dirname "$0")/../.."
P=${1:-$(nproc)}
@@ -11,10 +13,13 @@ for t in $D/*.tasks.jsonl; do
[ -e "$t" ] || continue; b=$(basename "$t" .tasks.jsonl)
[ -f ml/data/$b.jsonl ] || { echo "[prep] 删除旧打包 $b(对应 shard 已不存在)"; rm -f $D/$b.*; }
done
SIG=$(node ml/prep/build_dataset.cjs --sig)
echo "[prep] 打包签名 $SIG"
todo=()
for f in ml/data/shard_*.jsonl; do
b=$(basename "$f" .jsonl)
if [ -f $D/$b.tasks.jsonl ] && [ $D/$b.tasks.jsonl -nt "$f" ]; then continue; fi
if [ -f $D/$b.tasks.jsonl ] && [ $D/$b.tasks.jsonl -nt "$f" ] && grep -qF "\"sig\":\"$SIG\"" $D/$b.meta.json 2>/dev/null; then continue; fi
rm -f $D/$b.tasks.jsonl # 先撤完成标记:重打中途断了不会被当成已完成
todo+=("$f")
done
echo "[prep] 共 $(ls ml/data/shard_*.jsonl | wc -l) 个 shard,待打包 ${#todo[@]} 个"
+48 -15
View File
@@ -1,6 +1,6 @@
"""流水线状态与互斥锁(单一写者:所有对 ml/runs/pipeline.json / pipeline.lock 的写都经过本模块,原子替换)。
pipeline.sh 自动调用,一般不需要手动运行:
python -m ml.pstate reset <阶段1,...> [--fresh] # `all` 开始:保留 rc=0 的已完成阶段(续跑);参数变了只作废受影响的阶段;--fresh 全部作废
python -m ml.pstate reset <阶段1,...> [--fresh] # `all` 开始:保留 rc=0 的已完成阶段(续跑);参数变了只作废受影响的阶段;--fresh 全部作废;--fresh-from=<阶段> 只作废该阶段及之后
python -m ml.pstate todo # 打印计划中尚未完成的阶段(空格分隔)
python -m ml.pstate begin <阶段> [键=值 ...] # 阶段开始
python -m ml.pstate set <阶段> 键=值 ... # 阶段中途更新
@@ -13,7 +13,9 @@ pipeline.sh 自动调用,一般不需要手动运行:
心跳超过 LOCK_STALE_S 未刷新视为陈旧(进程被强杀),可接管。
- 续跑(缺口 1):reset 默认保留已完成阶段;参数变了只作废受它影响的阶段(AFFECTS),其产物移到 ml/archive/,避免新旧参数混用。
- 阶段表 STAGES 是估时 / 超时 / 网页进度权重的唯一来源(审查 N-14:网页权重与耗时表同源)。
- 停止也走本模块(审查 N-12:此前 monitor 直写 pipeline.json,与子进程竞态)。"""
- 停止也走本模块(审查 N-12:此前 monitor 直写 pipeline.json,与子进程竞态)。
- 累计用时(2026-10-18):每个阶段跑过的时间并进顶部 time{阶段: 秒},跨「停止 → 继续训练」累加,
网页的「总用时 / 本步用时」据此显示;产物被归档(要重做)的阶段清零,其余保留。"""
import glob, json, os, shutil, socket, sys, time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -24,24 +26,32 @@ LOCK_STALE_S = 300 # 心跳每 60 s 一次;5 分钟没刷新 = 持锁
# 阶段顺序、名称、估时(小时,按 README §耗时表的共识分支上界;网页进度权重即此值)
STAGES = [
('setup', '安装依赖', 0.2), ('kin', '对拍', 0.2), ('r4', '推理延迟', 0.2), ('gate1', '一致性门禁', 0.5),
('gen', '生成老师轨迹', 5.0), ('prep', '打包样本', 0.5), ('train', '预训练', 3.0), ('m5', '头门禁', 0.5),
('gen', '生成老师轨迹', 5.0), ('prep', '打包样本', 2.0), ('train', '预训练', 4.5), ('dagger', 'DAgger 纠偏', 3.5), ('m5', '头门禁', 0.5),
('apg', 'APG 微调', 4.0), ('mine', '挖错循环', 9.0), ('export', '导出', 0.1), ('bench', '部署评测', 1.0),
('accept', '验收汇总', 0.5)]
ORDER = [s[0] for s in STAGES]
EST_H = {s[0]: s[2] for s in STAGES}
# m5 不过时流程内自动训 WTA 头(训练 3 h + 门禁),超时按分支后的总量给
TIMEOUT_H = {k: max(1.0, 3 * v) for k, v in EST_H.items()}
TIMEOUT_H['m5'] = 3 * (EST_H['train'] + 2 * EST_H['m5'])
TIMEOUT_H['m5'] = 3 * (EST_H['train'] + EST_H['dagger'] + 2 * EST_H['m5'])
# 参数 → 受它影响的阶段(这些阶段即使上次完成也要重跑,其产物先挪进 ml/archive/)。只改挖错轮数不会让 5 小时的生成作废。
_from = lambda st: ORDER[ORDER.index(st):]
AFFECTS = {**{k: _from('gen') for k in ('N_TASKS', 'NPROC', 'TARGET_SAMPLES', 'CONSENSUS', 'MIN_GAP_MM')},
'CFG': ['r4'] + _from('train'), **{k: _from('mine') for k in ('ROUNDS', 'APG_STEPS', 'MINE_N')}}
AFFECTS = {**{k: _from('gen') for k in ('N_TASKS', 'NPROC', 'CONSENSUS', 'MIN_GAP_MM')},
# TARGET_SAMPLES 只决定"补量到多少":不作废 gen 的已有分片(重跑 gen 会跳过写完的 shard、按差额补量),
# 但样本集会变,所以 prep 及之后作废。2026-10-18 实测:把它当"从 gen 作废"会让 5 小时的 5 万条分片白跑。
'TARGET_SAMPLES': ['gen'] + _from('prep'),
# 2026-10-19:打包参数(标签尺度、增广)变了 → prep 起作废;run_prep.sh 另按 meta.sig 只重打参数不符的 shard
**{k: _from('prep') for k in ('ACT_SCALE', 'AUG_P', 'AUG_SIGMA', 'AUG_BACK')},
'CFG': ['r4'] + _from('train'), **{k: _from('dagger') for k in ('DAG_ROUNDS', 'DAG_N', 'DAG_STEPS')},
**{k: _from('mine') for k in ('ROUNDS', 'APG_STEPS', 'MINE_N')}}
PARAM_KEYS = list(AFFECTS)
# 各阶段产物(相对仓库根,可含通配);作废 / 从头重跑时整体移到 ml/archive/<时间>/,不删除
PRODUCTS = {
'r4': ['ml/runs/r4', 'ml/runs/r4.json'], 'gate1': ['ml/data/consistency.json'],
'gen': ['ml/data/shard_*', 'ml/data/log_*', 'ml/data/stats.json'], 'prep': ['ml/data/samples', 'ml/data/r2.json'],
'train': ['ml/runs/base', 'ml/runs/wta', 'ml/runs/small', 'ml/runs/bench_*'],
'train': ['ml/runs/base', 'ml/runs/wta', 'ml/runs/small', 'ml/runs/bench_*', 'ml/runs/replay_teacher.json',
'ml/runs/review_*'], # 评审快照包(_review_pack.py 从 base 打的,内含 log.jsonl 会被监控页当成一条"预训练"运行);重做 train 时一并归档
'dagger': ['ml/runs/*_dag[0-9]*', 'ml/runs/*_dagger.ckpt', 'ml/data/dag_*', 'ml/data/samples_dag_*'],
'm5': ['ml/runs/m5_*', 'ml/runs/selected_ckpt'], 'apg': ['ml/runs/apg', 'ml/runs/current.pt'],
'mine': ['ml/runs/mine', 'ml/runs/apg_r*', 'ml/data/e2', 'ml/data/samples_e2'],
'export': ['webapp/models/traj_policy.onnx', 'webapp/models/traj_policy.meta.json'],
@@ -66,7 +76,7 @@ def load():
try:
with open(P, encoding='utf-8') as f: return json.load(f)
except Exception:
return {'started': time.time(), 'plan': None, 'stages': {}, 'current': None, 'params': {}}
return {'started': time.time(), 'plan': None, 'stages': {}, 'current': None, 'params': {}, 'time': {}}
def save(d):
@@ -143,11 +153,21 @@ def todo(d=None):
return [s for s in (d.get('plan') or []) if (d['stages'].get(s) or {}).get('rc') != 0]
def _acc_time(d, st, now):
"""把"这一段"的用时并进累计 time{阶段: 秒}。幂等:stage.end 已写就不再加,停止(mark_stopped)
与流水线的 end 都会调,重复调用不会双记。"""
s = d['stages'].get(st)
if s and s.get('start') and s.get('end') is None:
t = d.setdefault('time', {})
t[st] = round(t.get(st, 0) + max(0.0, now - s['start']), 1)
def mark_stopped(rc=-15):
"""网页「停止」后调用:当前阶段记为被停止,释放锁。"""
d = load(); st = d.get('current')
"""网页「停止」后调用:当前阶段记为被停止、这段用时并进累计,释放锁。"""
d = load(); st = d.get('current'); now = time.time()
if st and st in d['stages'] and d['stages'][st].get('end') is None:
d['stages'][st].update(end=time.time(), rc=rc)
_acc_time(d, st, now)
d['stages'][st].update(end=now, rc=rc)
d['current'] = None
save(d); lock_release()
@@ -179,22 +199,34 @@ def main():
if cmd == 'reset':
plan = rest[0].split(',') if rest and rest[0] else []
pn = params_now(); prev = d.get('params') or {}
changed = [k for k in PARAM_KEYS if prev and prev.get(k) != pn[k]]
ff = next((r.split('=', 1)[1] for r in rest if r.startswith('--fresh-from=')), '')
if '--fresh' in rest: bad = set(ORDER)
else:
changed = [k for k in PARAM_KEYS if prev and prev.get(k) != pn[k]]
bad = {st for k in changed for st in AFFECTS[k]}
if ff in ORDER: bad |= set(ORDER[ORDER.index(ff):])
if changed: print('参数与上次不同:' + json.dumps({k: (prev.get(k), pn[k]) for k in changed}, ensure_ascii=False)
+ ';受影响的阶段重跑:' + ' '.join(st for st in ORDER if st in bad), file=sys.stderr)
keep = {k: v for k, v in d.get('stages', {}).items() if v.get('rc') == 0 and k not in bad}
# 受影响 / 未完成的阶段:旧产物挪走,免得续训、跳过 shard 时把旧参数的东西接上
arc = archive([st for st in ORDER if st in bad]) if bad else None
# 受影响 / 未完成的阶段:旧产物挪走,免得续训、跳过 shard 时把旧参数的东西接上。
# 例外:只改了目标样本量时,gen 要重跑补量,但已有分片必须留着(run_parallel 跳过写完的 shard、
# 按差额补量),否则 5 小时的生成白跑。
no_archive = {'gen'} if 'TARGET_SAMPLES' in changed else set()
arch = [st for st in ORDER if st in bad and st not in no_archive]
arc = archive(arch) if arch else None
if arc: print(f'旧产物已移到 {arc}/(未删除)', file=sys.stderr)
d = {'started': now, 'plan': plan, 'stages': keep, 'current': None, 'params': pn}
# 累计用时跨续跑保留;只有产物被挪走的阶段(要从头重做)才清零
tm = {k: v for k, v in (d.get('time') or {}).items() if k not in arch}
d = {'started': now, 'plan': plan, 'stages': keep, 'current': None, 'params': pn, 'time': tm}
done = [s for s in plan if s in keep]
if done: print('跳过已完成的 %d 个阶段:%s' % (len(done), ' '.join(done)), file=sys.stderr)
save(d); return
elif cmd == 'begin':
st = rest[0]
prev = d['stages'].get(st) or {}
# 老记录(本次改动之前完成的)没有累计字段:用上一次的整段补上,免得续跑时用时从 0 起
if prev.get('start') and prev.get('end') and st not in (d.get('time') or {}):
d.setdefault('time', {})[st] = round(prev['end'] - prev['start'], 1)
d['stages'][st] = {'start': now, 'end': None, 'rc': None, 'info': kv(rest[1:]), 'timeout_h': TIMEOUT_H.get(st)}
d['current'] = st
elif cmd == 'set':
@@ -203,6 +235,7 @@ def main():
elif cmd == 'end':
st, rc = rest[0], int(rest[1]) if len(rest) > 1 else 0
s = d['stages'].setdefault(st, {'start': now, 'info': {}})
_acc_time(d, st, now)
s['end'], s['rc'] = now, rc
if d.get('current') == st: d['current'] = None
else:
+370 -69
View File
@@ -1,19 +1,23 @@
"""闭环仿真(验证 / 挖错 / 门禁共用,PyTorch 批量,无梯度)——按部署节奏运行:
50 Hz 执行;队列 ≤ 3 步时推理一次(已提交前缀 r = 队列长度),提交接下来 3 步;
冷启动用价值头对候选末态打分(短前瞻),之后按与上一计划的一致性选候选;
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点(到达 < 5° 或 1.5 s 超时后切下一个)。
冷启动用价值头对候选末态打分(短前瞻),之后按与上一条**真正提交**的计划的一致性选候选;
接近目标 < 5 mm 时用局部 DLS 精修;子目标头在新目标时给出途经点,按「越过顶点」规则切下一个(与标签同口径,见 sub_advance)。
safety=False 时不做任何碰撞拦截(暴露真实错误,挖错用);safety=True 时近似部署安全层(候选提交步有干涉则换下一条,全不过则停一步)。
动作反归一化:inc = clamp(act, −1, 1) · act_scale · STEP;act_scale 来自 checkpoint(旧模型缺省 1.0,见 ml/common/ckpt.load_policy)。
碰撞与到达只在每题自己的时限内计(短题与长题同批时不把超时后的部分算进去)。
python -m ml.sim.closed_loop --ckpt ml/runs/base/best.pt --samples ml/data/samples --split val --n 1000 [--safety]"""
import argparse, json, math, os
import torch
from ml.common.kin_torch import Kin
from ml.common.features import features, STEP, H, GAP_CAP, D2R, wrap180
from ml.common.features import features, STEP, VMAX, H, GAP_CAP, D2R, wrap180
DT = 0.02
FIRST_DEG = 5.0 # R7 首次方向判定阈值(度):小于它的抖动不算选边
SUB_LEAD_S = 0.02 # 子目标「越过顶点」提前量(s):标签在老师 τ > τ_顶点 − 0.02 时就换到下一个顶点(build_dataset.cjs remain 过滤)
SUB_REACH_DEG, SUB_TIMEOUT_S = 5.0, 1.5 # 旧规则(sub_rule='timeout'):到达 < 5° 或 1.5 s 超时
def tasks_to_tensors(tasks, device):
q0 = torch.tensor([t['q0'] for t in tasks], dtype=torch.float32, device=device)
@@ -28,6 +32,12 @@ def tasks_to_tensors(tasks, device):
NOREF_SEC = 6.0 # 无老师参考题的定长时限(s)
def time_limit(ex, max_sec=None):
"""每题时限(s):有老师 = 老师时长 × 2(至少 3 s);无老师 = NOREF_SEC;max_sec 覆盖全部。"""
if max_sec is not None: return torch.full_like(ex, max_sec)
return torch.where(torch.isfinite(ex), torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC))
def goal_err(kin, q, T, R):
xf = kin.cum_xf(q)
p = kin.fk(q, xf); Rq = kin.orient(q, xf).reshape(-1, 9)
@@ -38,23 +48,42 @@ def goal_err(kin, q, T, R):
return e, oe
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5):
"""局部 DLS 精修:n 步增量 [B,n,6]"""
def execution_points(kin, q, increments):
points, midpoints = [], []
for delta in increments.unbind(1):
nxt = kin.clamp_box(q + delta)
midpoints.append(q + 0.5 * (nxt - q))
points.append(nxt)
q = nxt
return torch.stack(points, 1), torch.stack(midpoints, 1)
def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5, analytic=True):
"""局部 DLS 精修:n 步增量 [B,n,6](analytic:几何雅可比,1 次 FK;否则 7 次批量有限差分)"""
step = torch.tensor(STEP, device=q.device)
B = q.shape[0]
pose = (R.abs().sum(-1) > 0).float()
Rt = R.view(-1, 3, 3)
def err(x):
xf = kin.cum_xf(x); p = kin.fk(x, xf); Rq = kin.orient(x, xf)
M = Rt @ Rq.transpose(1, 2)
rv = 0.5 * torch.stack([M[:, 2, 1] - M[:, 1, 2], M[:, 0, 2] - M[:, 2, 0], M[:, 1, 0] - M[:, 0, 1]], -1) * 100 * pose[:, None]
return torch.cat([T - p, rv], -1)
def err(x, Tb, Rtb, poseb, xf=None):
xf = xf if xf is not None else kin.cum_xf(x)
p = kin.fk(x, xf); Rq = kin.orient(x, xf)
M = Rtb @ Rq.transpose(1, 2)
rv = 0.5 * torch.stack([M[:, 2, 1] - M[:, 1, 2], M[:, 0, 2] - M[:, 2, 0], M[:, 1, 0] - M[:, 0, 1]], -1) * 100 * poseb[:, None]
return torch.cat([Tb - p, rv], -1)
out = []; x = q.clone()
ar = torch.arange(6, device=q.device)
for _ in range(n):
e0 = err(x); J = []
for j in range(6):
dx = torch.zeros_like(x); dx[:, j] = 1e-3
J.append(-(err(x + dx) - e0) / 1e-3)
J = torch.stack(J, -1) # [B,6(err),6(q)]
xf = kin.cum_xf(x)
e0 = err(x, T, Rt, pose, xf)
if analytic:
# 几何角速度 Jw 仅在姿态误差接近零时等于该 skew 残差的负导数。
Jv, Jw = kin.jac(x, xf)
J = torch.cat([Jv, 100 * Jw * pose[:, None, None]], 1)
else:
dx = torch.zeros(B, 6, 6, device=q.device); dx[:, ar, ar] = 1e-3
xp = (x[:, None, :] + dx).reshape(B * 6, 6)
ep = err(xp, T.repeat_interleave(6, 0), Rt.repeat_interleave(6, 0), pose.repeat_interleave(6, 0))
J = -((ep - e0.repeat_interleave(6, 0)) / 1e-3).view(B, 6, 6).transpose(1, 2) # [B,6(err),6(q)]
A = J @ J.transpose(1, 2) + lam ** 2 * torch.eye(6, device=q.device)
y = torch.linalg.solve(A, e0.unsqueeze(-1))
dq = (J.transpose(1, 2) @ y).squeeze(-1)
@@ -64,53 +93,202 @@ def dls_steps(kin, q, T, R, n=3, scale=0.3, lam=0.5):
return torch.stack(out, 1)
def jdiff(kin, a, b):
"""a − b(度),连续轴取最短弧。"""
d = a - b
return torch.where(kin.cont, wrap180(d), d)
def sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, rule='cross'):
"""子目标推进(每次规划前调用,每次最多推进一个)。subs [B,M,6] 待走途经点(绝对角),nsub [B] 剩余个数,
sub_prev [B,6] 上一个已越过的点(冷启动时 = 起点),sub_t [B] 当前子目标开始时刻。返回 (subs, nsub, sub_prev, sub_t)。
rule='cross'(默认):qe 沿拐角平分面法向越过当前顶点(提前 SUB_LEAD_S 秒)即切到下一个。
2026-10-19(审查 review_data_actions C5 / review_model_train P0-1):标签里 sub = 老师「下一个尚未经过的顶点」,
老师 τ 一过顶点(−0.02 s)立刻换;旧规则要等最大关节差 < 5° 或 1.5 s 超时,策略在顶点附近 5–10° 进不去时
sub 一直指着身后的点,这种「sub 已在身后」的特征序列训练里从未出现。运行时没有老师 τ,用几何等价物:
进入方向 a = 顶点 − 上一点、离开方向 b = 下一顶点 − 顶点(最后一个顶点取 a),各轴按 VMAX 折成秒(与 τ 同量纲),
法向 n = â + b̂;(qe − 顶点)·n̂ ≥ −SUB_LEAD_S 判为越过。没有超时(标签也没有)。
rule='timeout':旧部署口径,到达 < 5° 或 1.5 s 超时(诊断对照用)。
ik-policy.js Runner(subRule)与 apg_finetune.py 用同一规则,三处同步改。"""
has = nsub > 0
if not bool(has.any()): return subs, nsub, sub_prev, sub_t
cur = subs[:, 0]
if rule == 'timeout':
dd = jdiff(kin, cur, qe).abs().max(-1).values
adv = has & ((dd < SUB_REACH_DEG) | (tnow - sub_t > SUB_TIMEOUT_S))
elif rule == 'cross':
vmax = torch.tensor(VMAX, device=qe.device, dtype=qe.dtype)
un = lambda v: v / v.norm(dim=-1, keepdim=True).clamp(min=1e-9)
a = jdiff(kin, cur, sub_prev) / vmax
nxt = subs[:, 1] if subs.shape[1] > 1 else cur
b = torch.where((nsub > 1)[:, None], jdiff(kin, nxt, cur) / vmax, a)
n = un(un(a) + un(b))
proj = (jdiff(kin, qe, cur) / vmax * n).sum(-1)
adv = has & (proj >= -SUB_LEAD_S)
else:
raise ValueError('sub_rule: ' + str(rule))
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, -1:]], 1), subs)
sub_prev = torch.where(adv[:, None], cur, sub_prev)
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
return subs, nsub, sub_prev, sub_t
class TeacherOracle:
"""老师当策略(2026-10-19,审查 review_closedloop D1 / review_data_actions §4.1):
规划时从当前时间下标 it 取老师 50 Hz 轨迹的 16 步增量,按与 build_dataset.cjs 相同的标签口径
act = clamp(Δ / (act_scale·STEP), −1, 1) 交给 rollout(),其余(队列、前缀、提交、qe、DLS、done、碰撞、时限)一行不改。
traj [B,Tmax,6]:每题老师轨迹,末帧重复补齐(按 id 从 ml/data/shard_*.jsonl 取)。
sigma > 0:每次规划给 act 加 N(0, σ²)(σ 以 STEP 为单位,各候选独立),测闭环对残差噪声的敏感度。
跟随时第 it 步的 q 应等于 traj[:, it](DLS 接管或到达冻结之后除外)。"""
head = 'oracle'
def __init__(self, traj, act_scale=1.0, sigma=0.0):
self.traj, self.act_scale, self.sigma = traj, float(act_scale), float(sigma)
def plan(self, it, K, g):
B, Tm, _ = self.traj.shape
dev = self.traj.device
idx = (it + torch.arange(H + 1, device=dev)).clamp(max=Tm - 1)
seg = self.traj[:, idx]
astep = torch.tensor(STEP, device=dev) * self.act_scale
act = ((seg[:, 1:] - seg[:, :-1]) / astep).clamp(-1, 1)
act = act[:, None].expand(B, K, H, 6).contiguous()
if self.sigma > 0:
act = act + torch.randn(B, K, H, 6, device=dev, generator=g) * (self.sigma / self.act_scale)
return act
@torch.no_grad()
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False):
def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, return_traj=False, use_sub=True, sub_override=None,
qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross',
fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None,
fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1, safety_try=0, sel_w=0.1, dls_safety=0, fb_clear=0.0, fb_cmin=0.3, sub_refresh=0, fb_block_gain=1.0,
inc_cap=0, gap_margin=0.0, cert_batch=0, progress=None):
"""use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头;
qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放;
select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。
oracle:TeacherOracle,代替 model 出动作(model 可为 None,须配 select='first'、use_sub=False)。
replan:'r3'(部署:队列 ≤ 3 步就规划,稳态 r=3)/ 'empty'(队列空了才规划,每次 r=0,前缀消融用)。
sub_rule:子目标推进规则,见 sub_advance。除默认值外均为诊断用。
inc_cap=1:把「策略动作 + 目标反馈」的合计增量再按每轴 VMAX·DT 限一次幅(默认关;关时反馈是在 ±1 限幅之后叠加的,第30轮实测最大 1.66×VMAX)。
gap_margin>0:安全层要求间隙大于该毫米余量而不是 >0;宽相位 cap 自动抬到 margin+0.5,否则 0.5 的截顶值会让余量门限恒假。
两者默认值下行为与第32轮逐位相同。"""
g = torch.Generator(device=device); g.manual_seed(seed)
q0, T, R, ex, tr = tasks_to_tensors(tasks, device)
B = q0.shape[0]
step = torch.tensor(STEP, device=device)
gcap = max(0.5, gap_margin + 0.5) # 余量门限必须落在截顶值之内才有意义
act_scale = oracle.act_scale if oracle is not None else float(getattr(model, 'act_scale', 1.0))
astep = step * act_scale # 动作反归一化:每单位 act 对应的度数
has_ref = torch.isfinite(ex)
limit = torch.where(has_ref, torch.clamp(2 * ex, min=3.0), torch.full_like(ex, NOREF_SEC)) if max_sec is None else torch.full_like(ex, max_sec)
limit = time_limit(ex, max_sec)
nmax = int(math.ceil(limit.max().item() / DT))
q, qp = q0.clone(), q0.clone()
q, qp = q0.clone(), (qp0.to(device) if qp0 is not None else q0.clone())
queue = torch.zeros(B, 0, 6, device=device)
last = None
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device); sub_t = torch.zeros(B, device=device)
last = None; last_ok = torch.zeros(B, dtype=torch.bool, device=device)
fb_i = torch.zeros(B, 6, device=device) # 积分项:累积持续同向的目标反馈(慢变偏差)
fb_cache = None; pit = 0 # fb_every>1:复用上一次规划的目标反馈(误差方向慢变)
last_brake = torch.zeros(B, dtype=torch.bool, device=device)
subs = torch.zeros(B, 2, 6, device=device); nsub = torch.zeros(B, dtype=torch.long, device=device)
sub_t = torch.zeros(B, device=device); sub_prev = q0.clone()
done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device)
col = torch.zeros(B, device=device); travel = torch.zeros(B, device=device); brakes = torch.zeros(B, device=device)
first_hit = torch.full((B,), float('nan'), device=device); dls_it = torch.full((B,), -1, dtype=torch.long, device=device)
# 2026-10-20(轮次33):速度/加速度按「实际执行位移」算,不看指令——第30轮证明反馈是在限幅之后叠加的
vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device)
traj = [q.clone()] if return_traj else None
plans = {k: [] for k in ('it', 'qe', 'commit', 'points', 'midpoints', 'gaps', 'certified')} if return_traj else None
n_start = int(round(0.5 / DT)); moved_start = None # R2 起步失败:0.5 s 内关节总行程 < 1°
k2s = int(round(2.0 / DT)); e2 = None
e0, _ = goal_err(kin, q0, T, R)
j1_acc = torch.zeros(B, device=device); j1_max = torch.zeros(B, device=device); j1_min = torch.zeros(B, device=device)
j1_first = torch.zeros(B, device=device) # 2026-10-18 第五轮(审查 N-3):J1 累计偏离首次超过 FIRST_DEG 时的方向(真正的"首次方向")
cold = True
ar = torch.arange(B, device=device)
for it in range(nmax):
tnow = it * DT
if queue.shape[1] <= 3:
if (queue.shape[1] <= 3) if replan == 'r3' else (queue.shape[1] == 0):
r = queue.shape[1]
pre = torch.zeros(B, 3, 6, device=device); pre[:, :r] = queue[:, :r]
qe = q + queue.sum(1)
pre = torch.zeros(B, 3, 6, device=device)
qe = q
if r:
prefix_points, _ = execution_points(kin, q, queue)
pre[:, :r] = prefix_points - torch.cat([q[:, None], prefix_points[:, :-1]], 1)
qe = prefix_points[:, -1]
e, oe = goal_err(kin, qe, T, R)
near = (e < 5) & ((R.abs().sum(-1) == 0) | (oe < 5))
# 子目标推进
if (nsub > 0).any():
cur = subs[:, 0]
dd = torch.where(kin.cont, wrap180(cur - qe), cur - qe).abs().max(-1).values
adv = (nsub > 0) & ((dd < 5) | (tnow - sub_t > 1.5))
subs = torch.where(adv[:, None, None], torch.cat([subs[:, 1:], subs[:, 1:]], 1), subs)
nsub = torch.where(adv, nsub - 1, nsub); sub_t = torch.where(adv, torch.full_like(sub_t, tnow), sub_t)
subs, nsub, sub_prev, sub_t = sub_advance(kin, subs, nsub, sub_prev, sub_t, qe, tnow, sub_rule)
sf = (nsub > 0).float()
feat = features(kin, q, qp, T, R, pre, torch.full((B,), r, device=device), subs[:, 0], sf)
z = torch.randn(B * K, H, 6, device=device, generator=g)
act, val, sub = model.sample_k(feat, K, z)
inc = act.clamp(-1, 1) * step
if oracle is not None:
act = oracle.plan(it, K, g); sub = torch.zeros(B, 13, device=device)
else:
z = torch.randn(B * K, H, 6, device=device, generator=g) * z_scale
act, val, sub = model.sample_k(feat, K, z, steps=steps)
if sub_refresh and not cold: # 子目标头作为反应式规划:没有待走途经点时,用当前状态的预测补一个(训练标签在每个状态都有 sub)
need = sub[:, 12] > 0.5
newsubs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
rf = need & (nsub <= 0)
subs = torch.where(rf[:, None, None], newsubs, subs)
nsub = torch.where(rf, torch.full_like(nsub, 2), nsub)
sub_prev = torch.where(rf[:, None], q, sub_prev)
sub_t = torch.where(rf, torch.full_like(sub_t, tnow), sub_t)
inc = act.float().clamp(-1, 1) * astep
if fb_gain > 0: # 实验(2026-10-20):目标反馈加在所有候选上(安全层也评估带反馈的候选)
if fb_cache is None or fb_every <= 1 or pit % fb_every == 0:
fb = dls_steps(kin, qe, T, R, n=1); fb_cache = fb
else:
fb = fb_cache
pit += 1
gm_ = None
if fb_clear > 0: # 近障碍衰减:反馈方向可能把候选推进障碍,贴近时让模型自己的绕行占主导
dgc = kin.gaps(qe, fb_clear)['gm'].min(-1).values
gm_ = torch.clamp(dgc / fb_clear, fb_cmin, 1.0)
if fb_ki > 0:
fb_i = fb_decay * fb_i + fb[:, 0] * (~done)[:, None]
lim = step * fb_imax
fb_i = fb_i.clamp(-lim, lim)
inc = inc + fb_gain * fb[:, :, None, :] + fb_ki * fb_i[:, None, None, :]
elif fb_scale > 0: # 距离衰减:远离目标全增益,贴近目标减弱(减少过冲/刹停)
dg, _ = goal_err(kin, qe, T, R)
gdec = fb_gain * torch.clamp(dg / fb_scale, fb_near, 1.0)
if gm_ is not None: gdec = gdec * gm_
inc = inc + gdec[:, None, None, None] * fb[:, :, None, :]
else:
gv = torch.full((B,), fb_gain, device=device, dtype=fb.dtype)
if gm_ is not None: gv = gv * gm_
if fb_block_gain != 1.0: # 上一次被安全层全拒:减弱目标反馈,让模型自己的绕行候选有机会通过认证
gv = torch.where(last_brake, gv * fb_block_gain, gv)
inc = inc + gv[:, None, None, None] * fb[:, :, None, :]
if inc_cap:
inc = torch.maximum(torch.minimum(inc, step), -step) # 反馈之后才限幅:限的是真正要执行的每轴增量
if cold:
sub0 = sub
need = sub0[:, 12] > 0.5
subs = q[:, None] + sub0[:, :12].view(B, 2, 6) * 180
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub)); sub_t.fill_(tnow)
need = sub[:, 12] > 0.5
subs = q[:, None] + sub[:, :12].float().view(B, 2, 6) * 180
if sub_override is not None:
subs = sub_override[0].to(device).float()
n_ov = sub_override[1].to(device)
nsub_ov = n_ov.long() if n_ov.dtype != torch.bool else n_ov.long() * subs.shape[1]
need = nsub_ov > 0
nsub = nsub_ov
else:
nsub = torch.where(need, torch.full_like(nsub, 2), torch.zeros_like(nsub))
if not use_sub: nsub = torch.zeros_like(nsub)
sub_t.fill_(tnow); sub_prev = q.clone()
fut = qe[:, None, None] + torch.cumsum(inc[:, :, r:], 2) # [B,K,H-r,6]
if last is None:
if select == 'first':
score = torch.zeros(B, K, device=device); score[:, 0] = 1
elif select in ('endpoint', 'adapt'):
qa = fut[:, :, -1].reshape(B * K, 6)
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
score = -e_end.view(B, K)
if select == 'adapt' and last is not None:
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
sc = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
score = torch.where(last_brake[:, None], sc, score)
elif cold or select == 'value':
# 短前瞻:候选末态的价值
qa = fut[:, :, -1].reshape(B * K, 6); qb = fut[:, :, -2].reshape(B * K, 6)
f2 = features(kin, qa, qb, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0), torch.zeros(B * K, 3, 6, device=device),
@@ -118,30 +296,134 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
v2, _ = model.heads(model.encode(f2))
score = -v2.view(B, K)
else:
n = min(fut.shape[2], last.shape[1] - 3)
# 2026-10-19 修错位:last[i] = 上一计划从 qe_old 走 i 步后的位置;提交 3 步后 qe_new = last[3],
# 新候选 fut[0] 是 qe_new 再走 1 步 = 对应 last[4]。JS 部署(ik-policy.js)同一处同样修正。
n = min(fut.shape[2], last.shape[1] - (commit_n + 1))
w = 0.85 ** torch.arange(n, device=device, dtype=torch.float32)
d2 = (((fut[:, :, :n] - last[:, None, 3:3 + n]) / step) ** 2).sum(-1)
score = -(d2 * w).sum(-1)
order = score.argsort(-1, descending=True)
d2 = (((fut[:, :, :n] - last[:, None, commit_n + 1:commit_n + 1 + n]) / step) ** 2).sum(-1)
# 2026-10-19(审查 review_closedloop F5):上一次是刹停或 DLS 接管的行没有「已提交计划」,
# 与 JS(lastPlan = null → 一致性项缺席、各候选同分 → 取第 0 条)一致:同分,稳定排序取第 0 条
score = torch.where(last_ok[:, None], -(d2 * w).sum(-1), torch.zeros_like(d2[..., 0]))
if select == 'mix': # 一致性 + 端点进度(w=sel_w/mm)
qa = fut[:, :, -1].reshape(B * K, 6)
e_end, _ = goal_err(kin, qa, T.repeat_interleave(K, 0), R.repeat_interleave(K, 0))
score = score - sel_w * e_end.view(B, K)
order = torch.sort(score, dim=-1, descending=True, stable=True).indices
pick = order[:, 0]
commit = inc[torch.arange(B), pick, r:r + 3]
if safety:
ok_any = torch.zeros(B, dtype=torch.bool, device=device)
chosen = torch.zeros(B, 3, 6, device=device)
for kk in range(K):
ck = inc[torch.arange(B), order[:, kk], r:r + 3]
pts = qe[:, None] + torch.cumsum(ck, 1)
mids = torch.cat([qe[:, None], pts[:, :-1]], 1) + 0.5 * ck
if select == 'average': # 2026-10-20:候选平均(平滑误差)——所有候选置为 K 条动作的均值
inc = inc.mean(1, keepdim=True).expand_as(inc).contiguous()
K_eff = 1
else:
K_eff = K
commit = inc[ar, pick, r:r + commit_n]
if collect is not None: # 反馈蒸馏:收集与部署完全一致的 (特征, 复合动作=模型输出+目标反馈)
collect.append((feat.detach().float().cpu(), inc[ar, pick].detach().float().cpu()))
committed = torch.ones(B, dtype=torch.bool, device=device)
braked = torch.zeros(B, dtype=torch.bool, device=device)
if return_traj:
cert_pts = torch.full((B, commit_n, 6), float('nan'), device=device)
cert_mids = cert_pts.clone()
cert_gaps = torch.full((B, 2 * commit_n), float('nan'), device=device)
certified = torch.zeros(B, commit_n, dtype=torch.bool, device=device)
if safety and safety_partial: # 2026-10-20:允许「部分安全前缀」——执行到第一个不安全步为止,而不是全拒刹停
maxlen = torch.zeros(B, dtype=torch.long, device=device)
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
if cert_batch: # 一次 gaps 覆盖全部候选:典型路径略贵,但把「前面候选被拒→串行调用 K 次」的最坏情况封成 1 次
cks = inc[ar[:, None], order[:, :ntry], r:r + commit_n].reshape(B * ntry, commit_n, 6)
pts, mids = execution_points(kin, qe.repeat_interleave(ntry, 0), cks)
gaps6 = kin.gaps(torch.cat([pts, mids], 1).reshape(-1, 6), gcap)['gm'].min(-1).values.view(B, ntry, 2 * commit_n)
plen = torch.cumprod(((gaps6 > gap_margin)[:, :, :commit_n] &
(gaps6 > gap_margin)[:, :, commit_n:]).long(), -1).sum(-1)
maxlen = plen.max(-1).values
best = plen.argmax(-1) # 与串行同口径:严格更大者胜 ⇒ 首个最大值
chosen = cks.view(B, ntry, commit_n, 6)[ar, best]
chosen_k = order[ar, best]
if return_traj:
cert_pts = pts.view(B, ntry, commit_n, 6)[ar, best]
cert_mids = mids.view(B, ntry, commit_n, 6)[ar, best]
cert_gaps = gaps6[ar, best]
else:
for kk in range(ntry):
ck = inc[ar, order[:, kk], r:r + commit_n]
pts, mids = execution_points(kin, qe, ck)
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
okk = (kin.gaps(allq, 0.5)['gm'].min(-1).values > 0).view(B, 6).all(-1)
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
ok6 = gaps6 > gap_margin
okstep = ok6[:, :commit_n] & ok6[:, commit_n:]
plen = torch.cumprod(okstep.long(), 1).sum(1)
take = plen > maxlen
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k)
if return_traj:
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
maxlen = torch.maximum(maxlen, plen)
if B == 1 and int(maxlen[0]) >= commit_n: break # 实时单题:首个全安全候选即停(安全层 16 点→2-4 点)
msk = (torch.arange(commit_n, device=device)[None] < maxlen[:, None])
commit = torch.where(msk[..., None], chosen, torch.zeros_like(chosen))
if return_traj: certified = msk.clone()
brakes += (maxlen == 0).float(); braked = maxlen == 0
pick = chosen_k; committed = maxlen > 0
elif safety:
ok_any = torch.zeros(B, dtype=torch.bool, device=device)
chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone()
ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff
for kk in range(ntry):
ck = inc[ar, order[:, kk], r:r + commit_n]
pts, mids = execution_points(kin, qe, ck)
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
okk = (gaps6 > gap_margin).all(-1)
take = okk & ~ok_any
chosen = torch.where(take[:, None, None], ck, chosen); ok_any |= okk
chosen = torch.where(take[:, None, None], ck, chosen); chosen_k = torch.where(take, order[:, kk], chosen_k); ok_any |= okk
if return_traj:
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
commit = torch.where(ok_any[:, None, None], chosen, torch.zeros_like(chosen))
brakes += (~ok_any).float()
if return_traj: certified = ok_any[:, None].expand(-1, commit_n).clone()
brakes += (~ok_any).float(); braked = ~ok_any
pick = chosen_k; committed = ok_any
if safety and fb_gain > 0 and fb_fallback > 0:
fbcom = torch.zeros(B, commit_n, 6, device=device)
fbcom[:, :1] = dls_steps(kin, qe, T, R, n=1) * fb_fallback
pts, mids = execution_points(kin, qe, fbcom)
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
takef = braked & (gaps6 > gap_margin).all(-1)
commit = torch.where(takef[:, None, None], fbcom, commit)
if return_traj:
cert_pts = torch.where(takef[:, None, None], pts, cert_pts)
cert_mids = torch.where(takef[:, None, None], mids, cert_mids)
cert_gaps = torch.where(takef[:, None], gaps6, cert_gaps)
certified = certified | takef[:, None]
if near.any():
ref = dls_steps(kin, qe, T, R)
commit = torch.where(near[:, None, None], ref, commit)
last = torch.cat([qe[:, None], fut[torch.arange(B), pick]], 1)
ref = dls_steps(kin, qe, T, R, n=commit_n)
take = near
if dls_safety and safety: # 2026-10-20:近目标 DLS 接管也要过安全层(部署端 _certCommit 本来就会验;不过则退回候选计划)
pts, mids = execution_points(kin, qe, ref)
allq = torch.cat([pts, mids], 1).reshape(-1, 6)
gaps6 = kin.gaps(allq, gcap)['gm'].min(-1).values.view(B, 2 * commit_n)
okd = (gaps6 > gap_margin).all(-1)
take = near & okd
if return_traj:
cert_pts = torch.where(take[:, None, None], pts, cert_pts)
cert_mids = torch.where(take[:, None, None], mids, cert_mids)
cert_gaps = torch.where(take[:, None], gaps6, cert_gaps)
if return_traj:
certified = torch.where(take[:, None], torch.full_like(certified, bool(dls_safety and safety)), certified)
commit = torch.where(take[:, None, None], ref, commit)
dls_it = torch.where(take & (dls_it < 0), torch.full_like(dls_it, it), dls_it)
committed = committed & ~take
# 2026-10-19(F5):last 记真正提交的那条候选(安全层换了候选时是换后的那条);刹停 / DLS 接管的行无效
if fb_ki > 0: # 刹停/精修接管时清积分(防止原地累积到限幅后突然冲出去)
fb_i = torch.where(committed[:, None], fb_i, torch.zeros_like(fb_i))
last = torch.cat([qe[:, None], fut[ar, pick]], 1); last_ok = committed; last_brake = braked
if return_traj:
plans['it'].append(it + r)
for key, value in (('qe', qe), ('commit', commit), ('points', cert_pts), ('midpoints', cert_mids), ('gaps', cert_gaps), ('certified', certified)):
plans[key].append(value)
queue = torch.cat([queue, commit], 1)
cold = False
d = queue[:, 0]; queue = queue[:, 1:]
@@ -154,15 +436,28 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
if return_traj: traj.append(q.clone())
mid = qp + 0.5 * (q - qp)
gm = kin.gaps(torch.cat([q, mid], 0), 0.5)['gm'].min(-1).values
hit = ((gm[:B] <= 0) | (gm[B:] <= 0)) & ~done
tn = (it + 1) * DT
# 2026-10-19(审查 review_closedloop F4):只计每题自己时限内的碰撞(同批长题会把短题多仿真几秒)
hit = ((gm[:B] <= 0) | (gm[B:] <= 0)) & ~done & (tn <= limit + 1e-6)
col += hit.float()
first_hit = torch.where(hit & torch.isnan(first_hit), torch.full_like(first_hit, tn), first_hit)
act_ = ((~done) & (tn <= limit + 1e-6)).float() # 只计自己时限内的移动(与 col 同口径)
exec_d = (q - qp).abs()
vratio = torch.maximum(vratio, (exec_d / step).max(-1).values * act_) # 实际速度 / VMAX
aratio = torch.maximum(aratio, ((exec_d - prev_exec) / step).max(-1).values * act_) # 每控制步的速度变化 / VMAX
prev_exec = exec_d
e, oe = goal_err(kin, q, T, R)
if it + 1 == k2s: e2 = e.clone()
newly = (~done) & (e < 1) & (oe < 0.5)
t_done = torch.where(newly, torch.full_like(t_done, (it + 1) * DT), t_done); done |= newly
t_done = torch.where(newly, torch.full_like(t_done, tn), t_done); done |= newly
if progress is not None and (it == 0 or (it + 1) % 50 == 0 or it + 1 == nmax):
progress(it + 1, nmax)
if done.all(): break
ok = done & (t_done <= limit)
ok = done & (t_done <= limit + 1e-6)
if moved_start is None: moved_start = travel.clone()
e_end, _ = goal_err(kin, q, T, R)
if e2 is None: e2 = e_end
ratio2 = e2 / e0.clamp(min=1e-3)
start_fail = (moved_start < 1.0) & ~(done & (t_done <= 0.5))
okr = ok & has_ref & torch.isfinite(tr)
res = {'n': B, 'n_ref': int(has_ref.sum()), 'success': ok.float().mean().item(),
@@ -170,20 +465,25 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None,
'collision_rate': (col > 0).float().mean().item(), 'collision_steps_mean': col.mean().item(),
'travel_ratio_median': (travel[okr] / tr[okr].clamp(min=1)).median().item() if okr.any() else None,
'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item(),
'start_fail': start_fail.float().mean().item(),
'start_fail': start_fail.float().mean().item(), 'goal_ratio_2s': ratio2.median().item(),
'speed_ratio_mean': vratio.mean().item(), 'over_vmax_rate': (vratio > 1.001).float().mean().item(),
'time_budget': 'teacher×2(≥3 s)' if bool(has_ref.all()) else ('fixed %.0f s' % (max_sec or NOREF_SEC) if not has_ref.any() else 'mixed')}
# 绕行侧:J1 累计位移的正向/负向极值哪个更大(+1 / −1),供 R7 左右比例门禁
# 首次方向;全程没偏离到 FIRST_DEG 的(基本不绕 J1)退回净方向
side = torch.where(j1_first != 0, j1_first, torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max)))
per = {'done': done.cpu(), 't_done': t_done.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu()}
if return_traj: per['traj'] = torch.stack(traj, 1).cpu()
per = {'done': done.cpu(), 'ok': ok.cpu(), 't_done': t_done.cpu(), 'limit': limit.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(),
'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(),
'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu()}
if return_traj:
per['traj'] = torch.stack(traj, 1).cpu()
per['plans'] = {k: torch.tensor(v) if k == 'it' else torch.stack(v, 1).cpu() for k, v in plans.items()}
return res, per
def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
if not tasks: # 小样本摸底时 val 可能为空
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan')}, []
return {'n': 0, 'success': float('nan'), 'collision_rate': float('nan'), 'start_fail': float('nan'), 'brakes_mean': float('nan'),
'goal_ratio_2s': float('nan')}, []
agg, pers = {}, []
for i in range(0, len(tasks), bs):
r, p = rollout(model, kin, tasks[i:i + bs], device, K=K, safety=safety, seed=seed + i)
@@ -194,12 +494,14 @@ def evaluate(model, kin, tasks, device, K=8, safety=False, bs=256, seed=0):
n = sum(r['n'] for r, _ in pers)
out = {k: v / n for k, v in agg.items()}; out['n'] = n
out['n_ref'] = sum(r['n_ref'] for r, _ in pers)
# 中位数不能按批加权平均:全体任务重算(2026-10-19,选模分数用它)
out['goal_ratio_2s'] = torch.cat([p['goal_ratio_2s'] for _, p in pers]).median().item()
out['time_budget'] = pers[0][0]['time_budget'] if len({r['time_budget'] for r, _ in pers}) == 1 else 'mixed'
return out, pers
def main():
from ml.model.policy import build
from ml.common.ckpt import load_policy
from ml.data import load_tasks
ap = argparse.ArgumentParser()
ap.add_argument('--ckpt', required=True); ap.add_argument('--samples', default='ml/data/samples')
@@ -208,8 +510,7 @@ def main():
ap.add_argument('--safety', action='store_true'); ap.add_argument('--K', type=int, default=8)
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--out', default=None)
a = ap.parse_args()
ck = torch.load(a.ckpt, map_location=a.device)
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
model, ck = load_policy(a.ckpt, a.device)
kin = Kin(device=a.device)
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
if a.tasks: tasks = [json.loads(l) for l in open(a.tasks, encoding='utf-8') if l.strip()]
@@ -217,8 +518,8 @@ def main():
tasks = tasks[:a.n]
res, pers = evaluate(model, kin, tasks, a.device, K=a.K, safety=a.safety)
by = {}
flat_done = torch.cat([p['done'] for _, p in pers]); flat_col = torch.cat([p['col'] for _, p in pers])
for t, d, c in zip(tasks, flat_done.tolist(), flat_col.tolist()):
flat_ok = torch.cat([p['ok'] for _, p in pers]); flat_col = torch.cat([p['col'] for _, p in pers])
for t, d, c in zip(tasks, flat_ok.tolist(), flat_col.tolist()):
k = t.get('cat', '?') + '/' + t.get('mode', '?'); b = by.setdefault(k, [0, 0, 0]); b[0] += 1; b[1] += int(d); b[2] += int(c > 0)
res['by_cat_mode'] = {k: {'n': v[0], 'success': v[1] / v[0], 'collision': v[2] / v[0]} for k, v in by.items()}
s = json.dumps(res, indent=1); print(s)
+123 -32
View File
@@ -1,9 +1,27 @@
"""预训练(方案 §6):MeanFlow 一步头 + FK 损失 + 平滑 + 价值头 + 子目标头。
python -m ml.train --samples ml/data/samples --out ml/runs/base [--config ml/configs/base.json]
吞吐摸底:python -m ml.train --samples ml/data/samples --out ml/runs/bench_base --steps 2000 --no-eval
通过线(方案 R10):全程 60000 步 ≤ 3.5 h,即 samples_per_s ≥ 5000(3090 实测 6.2 k/s ≈ 2.8 h);日志 eta_h 为剩余小时数。
每 eval_every 步做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按 success 存 best.pt;连续 patience_evals 次不升即停。"""
import argparse, json, math, os, sys, time
通过线(方案 R10):samples_per_s ≥ 5000;日志 eta_h 为剩余小时数。
每 eval_every 步用 EMA 权重做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按选模分数存 best.pt;min_step 之后连续 patience_evals 次不升即停。
2026-10-18 首次正式预训练(21850 步,闭环到达 0–0.2%、穿柱 77–97%)后改:
- FK / 平滑损失不再加在 one_step(随机噪声) 上(那样等于对任意噪声回归同一个 Y → 条件均值,多峰头塌成单峰),
改加在 MF 分支里 r==t 且 t ≤ fk_tmax 的样本的配对去噪估计 x̂ = z − t·u 上(见 ml/model/meanflow.py);
- p_neq 前 p_neq_warm 步为 0(纯 flow matching 先把瞬时速度场学稳,再开 r≠t 的自举目标);
- 每次评估加开环指标(val 样本上一步采样的 16 步末端 FK 误差 mm、K 个候选的多样性),
塌缩或学偏当场可见。
2026-10-19(审查 review_model_train P0-2/P0-3、review_data_actions C3):
- EMA(train.ema,默认 0.999,前期按 (1+s)/(10+s) 放宽):评估、best/last 里的 'ema' 都是 EMA 权重;下游一律经 ckpt.load_policy 取 ck.get('ema', ck['model']);
- 选模:p_neq 预热期内及预热后第一次评估之前不存 best.pt(那时一步映射 u(z,0,1) 还没训过);
分数 = success + 0.05·(1 − collision_rate) + 0.05·(1 − min(1, goal_ratio_2s)) − 1e-4·min(ol_best_mm, 500),success 全 0 时闭环指标仍能排序;
早停只在 min_step = max(预热 + 2·eval_every, steps/2) 之后计数;
- 余弦学习率按真实预算走完(configs 的 steps 改为 24000,原 60000 从来跑不到,14k 步早停时学习率还有 0.87×);
- MeanFlow 采样:p_neq 0.5、p_edge 0.1(固定 (r=0,t=1) 的部署点,见 meanflow.sample_rt);
- 子目标头只看 sub 特征(68:75)置零的输入:sub 存在时 SG[0:6] 与输入 sd 是同一个量,原来 80% 的样本可以直接抄(标签泄露);
运行时子目标头只在冷启动 sf=0 时用,这样训练与使用同口径。价值头仍用完整输入(稳态推理时带 sub);
- 条件输入增广 aug_pre_drop / aug_pre_sigma / aug_dq_sigma(见 ml/data.Samples.augment);
- 动作尺度 act_scale 取自数据(ml/data.read_meta),存进 checkpoint;续训 / 微调起点与数据不一致直接报错。
- DAgger 微调(--init):只有起点 checkpoint 已走过自己的 p_neq 预热时才把预热置 0;起点优先取 EMA 权重。"""
import argparse, copy, json, math, os, sys, time
import torch
from ml.common.kin_torch import Kin
@@ -12,7 +30,7 @@ from ml.common.ckpt import Saver, atomic_save, rng_state, set_rng_state
from ml.common import vram
from ml.data import Samples, load_tasks
from ml.model.policy import build, wta_loss
from ml.model.meanflow import meanflow_loss, one_step
from ml.model.meanflow import meanflow_loss
from ml.sim.closed_loop import evaluate
@@ -28,46 +46,106 @@ def q_from_feat(X):
return torch.atan2(s, c) / D2R
@torch.no_grad()
def open_loop(model, kin, vdata, stepv, fk_idx, n=1024, K=8):
"""stepv:每单位动作对应的度数(= act_scale·STEP)。"""
"""开环指标(val 样本,老师状态上):
ol_fk_mm 候选 0 的 16 步末端与老师 Y 末端的胶囊点平均距离(mm);
ol_best_mm K 个候选里最接近老师的那条的同一误差(多峰时它应明显小于 ol_fk_mm);
ol_div K 个候选间标准差 / 动作平均幅度(塌成单峰时接近 0;首次正式预训练 last.pt 实测 0.20)。"""
g = torch.Generator(device=vdata.X.device); g.manual_seed(123)
X, Y, _, _ = vdata.batch(min(n, vdata.n), gen=g)
act, _, _ = model.sample_k(X, K)
act = act.float().clamp(-1, 1)
q = q_from_feat(X)
qt = q + (Y * stepv).sum(1)
qh = q[:, None] + (act * stepv).sum(2) # [B,K,6]
pt = fk_points(kin, qt, fk_idx)[:, None]
ph = fk_points(kin, qh.reshape(-1, 6), fk_idx).view(X.shape[0], K, -1, 3)
err = (ph - pt).norm(dim=-1).mean(-1) # [B,K] mm
div = (act.std(1).mean() / act.abs().mean().clamp(min=1e-6)).item()
return {'ol_fk_mm': err[:, 0].mean().item(), 'ol_best_mm': err.min(1).values.mean().item(), 'ol_div': div}
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--samples', nargs='+', default=['ml/data/samples']); ap.add_argument('--out', default='ml/runs/base')
ap.add_argument('--config', default='ml/configs/base.json'); ap.add_argument('--steps', type=int, default=None)
ap.add_argument('--resume', default=None); ap.add_argument('--no-eval', action='store_true')
ap.add_argument('--init', default=None, help='只载入权重(优先 EMA)、优化器与步数从 0 开始(DAgger 微调用)')
ap.add_argument('--freeze-enc', action='store_true', help='冻结编码器(enc + enc_norm),只微调解码器/头')
ap.add_argument('--lr', type=float, default=None); ap.add_argument('--warmup', type=int, default=None)
ap.add_argument('--stop-at', type=int, default=0, help='自检用:跑到第 N 步按停止请求处理(存 last.pt、rc 143)')
ap.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu'); ap.add_argument('--seed', type=int, default=0)
a = ap.parse_args()
cfg = json.load(open(a.config)); tc = cfg['train']
if a.steps: tc['steps'] = a.steps
if a.lr: tc['lr'] = a.lr
if a.warmup is not None: tc['warmup'] = a.warmup
init_ck = None
if a.init:
init_ck = torch.load(a.init, map_location='cpu')
# 2026-10-19(审查 P0-2):原来 --init 一律置 p_neq_warm=0,前提「起点已训过 r≠t」;best.pt 落在预热期(第 4000 步)时前提不成立,
# DAgger 一上来就用没训过的场做 JVP 自举。现在只有起点已走过它自己的预热才置 0。
init_warm = (init_ck.get('cfg') or {}).get('train', {}).get('p_neq_warm', 5000)
if init_ck.get('step', 0) >= init_warm: tc['p_neq_warm'] = 0
print(f'微调起点第 {init_ck.get("step")} 步(其预热 {init_warm} 步)→ 本次 p_neq_warm = {tc.get("p_neq_warm", 5000)}', flush=True)
os.makedirs(a.out, exist_ok=True)
torch.manual_seed(a.seed)
dev = a.device
vram.limit()
if dev.startswith('cuda'):
torch.backends.cuda.matmul.allow_tf32 = True; torch.backends.cudnn.allow_tf32 = True # MF 分支改 fp32 后用 TF32 保吞吐
kin = Kin(device=dev)
data = Samples(a.samples, device=dev, split='train')
val_tasks = [t for t in load_tasks(a.samples) if t['split'] == 'val'][:tc['eval_tasks']]
print(f'train samples {data.n}, val tasks {len(val_tasks)}, device {dev}', flush=True)
model = build(cfg).to(dev)
# 2026-10-19(审查 F11):val 只取主数据目录(第一个 --samples);DAgger 目录的样本全部当 train
data = Samples(a.samples, device=dev, split='train', val_dirs=a.samples[:1])
act_scale = data.act_scale
val_tasks = [t for t in load_tasks(a.samples[:1]) if t['split'] == 'val'][:tc['eval_tasks']]
try: vdata = Samples(a.samples[:1], device=dev, split='val')
except SystemExit: vdata = None
if vdata is not None and vdata.n == 0: vdata = None
print(f'train samples {data.n}, val tasks {len(val_tasks)}, act_scale {act_scale}, data_ver {data.data_ver}, device {dev}', flush=True)
model = build(cfg).to(dev); model.act_scale = act_scale
print(f'params {sum(p.numel() for p in model.parameters()) / 1e6:.2f} M', flush=True)
opt = torch.optim.AdamW(model.parameters(), lr=tc['lr'], betas=tuple(tc['betas']), weight_decay=tc['wd'])
opt = torch.optim.AdamW([p_ for p_ in model.parameters() if p_.requires_grad], lr=tc['lr'], betas=tuple(tc['betas']), weight_decay=tc['wd'])
step0, best, bad_evals = 0, -1.0, 0
def check_scale(ck, src):
s0 = float(ck.get('act_scale', 1.0))
if abs(s0 - act_scale) > 1e-9:
raise SystemExit(f'{src} 的 act_scale={s0} 与样本 act_scale={act_scale} 不一致:标签尺度变了,须从头训练(FRESH_FROM=prep)')
if init_ck is not None and not a.resume:
check_scale(init_ck, a.init)
model.load_state_dict(init_ck.get('ema') if init_ck.get('ema') is not None else init_ck['model'])
print(f'微调:权重取自 {a.init}({"EMA" if init_ck.get("ema") is not None else "原始"}权重),优化器重置', flush=True)
ema = copy.deepcopy(model).eval(); ema.act_scale = act_scale
for p_ in ema.parameters(): p_.requires_grad_(False)
if a.resume:
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
ck = torch.load(a.resume, map_location=dev); check_scale(ck, a.resume)
model.load_state_dict(ck['model'])
ema.load_state_dict(ck['ema'] if ck.get('ema') is not None else ck['model'])
if 'opt' in ck: opt.load_state_dict(ck['opt'])
step0, best, bad_evals = ck.get('step', 0), ck.get('best', -1.0), ck.get('bad_evals', 0)
if set_rng_state(ck.get('rng')): print('续训:随机数状态已恢复(batch 顺序与不断点一致)', flush=True)
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好 success {best:.4f}', flush=True)
print(f'续训:从第 {step0} 步继续(共 {tc["steps"]} 步),当前最好分数 {best:.4f}', flush=True)
del init_ck
total = tc['steps']
lr_at = lambda s: tc['lr'] * (s / tc['warmup'] if s < tc['warmup'] else 0.5 * (1 + math.cos(math.pi * (s - tc['warmup']) / max(1, total - tc['warmup']))))
amp = dev.startswith('cuda') and tc.get('amp') == 'bf16'
stepv = torch.tensor(STEP, device=dev)
astep = stepv * act_scale # 动作 → 度
fk_idx = torch.linspace(0, kin.cap_a.shape[0] - 1, tc['fk_caps'], device=dev).long()
ema_d = float(tc.get('ema', 0.999))
warm = 0 if model.head == 'wta' else int(tc.get('p_neq_warm', 5000))
sel_from = warm + tc['eval_every'] # 第一次可以存 best.pt 的步数:预热结束后的第一次评估
min_step = max(warm + 2 * tc['eval_every'], total // 2) # 早停计数起点
log = open(os.path.join(a.out, 'log.jsonl'), 'a' if a.resume else 'w') # 新训练覆盖旧日志,续训才追加
if not val_tasks and not a.no_eval:
print('没有 val 任务(样本太少),本次跳过闭环验证', flush=True); a.no_eval = True
t0, seen, diverged = time.time(), 0, False
saver = Saver(tc.get('ckpt_min', 10)) # 2026-10-18:每 10 分钟 + 收到停止请求时存 last.pt(原子写)
def save(name, step, extra=None):
d = {'model': model.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best, 'bad_evals': bad_evals, 'fp': kin.fp}
d = {'model': model.state_dict(), 'ema': ema.state_dict(), 'opt': opt.state_dict(), 'cfg': cfg, 'step': step, 'best': best,
'bad_evals': bad_evals, 'fp': kin.fp, 'act_scale': act_scale, 'data_ver': data.data_ver}
if name == 'last.pt': d['rng'] = rng_state()
if extra: d.update(extra)
atomic_save(d, os.path.join(a.out, name))
@@ -75,26 +153,32 @@ def main():
step = step0 - 1 # 续训时已训满:循环一次不跑,下面照常收尾
for step in range(step0, total):
for gpar in opt.param_groups: gpar['lr'] = lr_at(step)
X, Y, V, SG = data.batch(tc['batch'])
X, Y, V, SG = data.batch(tc['batch'], aug=tc)
model.train()
with torch.autocast('cuda', dtype=torch.bfloat16, enabled=amp):
mem = model.encode(X)
if model.head == 'wta':
l_mf, xh = wta_loss(model, mem, Y); mf_raw = l_mf.detach(); xh = xh.float()
else:
l_mf, mf_raw = meanflow_loss(model, mem, Y, p_neq=tc['p_neq'])
e = torch.randn_like(Y)
xh = one_step(model, mem, e).float()
v, s = model.heads(mem)
p_neq = tc['p_neq'] if step >= warm else 0.0
l_mf, mf_raw, aux = meanflow_loss(model, mem, Y, p_neq=p_neq, return_aux=True, p_edge=tc.get('p_edge', 0.0))
sel = ((aux['r'] == aux['t']) & (aux['t'] <= tc.get('fk_tmax', 0.3))).nonzero(as_tuple=True)[0]
tt = aux['t'][sel][:, None, None]
xh = aux['z'][sel] - tt * aux['u'][sel] # 配对去噪估计(fp32)
v, _ = model.heads(mem)
Xs = X.clone(); Xs[:, 68:75] = 0 # 子目标头:sub 特征置零(切断抄输入的捷径,见文件头)
_, s = model.heads(model.encode(Xs))
l_val = (v.float() - V).abs().mean(); l_sub = (s.float() - SG).abs().mean()
Xf, Yf = (X, Y) if model.head == 'wta' else (X[sel], Y[sel])
l_sm = torch.zeros((), device=dev); l_fk = torch.zeros((), device=dev)
if xh.shape[0] > 0:
d2 = xh[:, 2:] - 2 * xh[:, 1:-1] + xh[:, :-2]
l_sm = d2.pow(2).mean()
l_fk = torch.zeros((), device=dev)
if tc['w_fk'] > 0:
q = q_from_feat(X)
if tc['w_fk'] > 0 and xh.shape[0] > 0:
q = q_from_feat(Xf)
ks = torch.tensor([3, 7, 11, 15], device=dev)
qh = q[:, None] + torch.cumsum(xh.clamp(-1, 1) * stepv, 1)[:, ks]
qt = q[:, None] + torch.cumsum(Y * stepv, 1)[:, ks]
qh = q[:, None] + torch.cumsum(xh.clamp(-1, 1) * astep, 1)[:, ks]
qt = q[:, None] + torch.cumsum(Yf * astep, 1)[:, ks]
B4 = qh.shape[0] * 4
ph = fk_points(kin, qh.reshape(B4, 6), fk_idx); pt = fk_points(kin, qt.reshape(B4, 6), fk_idx)
l_fk = ((ph - pt).norm(dim=-1) / 10).mean()
@@ -105,6 +189,9 @@ def main():
if not torch.isfinite(loss):
print('非有限损失 → 训练发散(M5 门禁:触发 WTA 回退条件)', flush=True); save('diverged.pt', step); diverged = True; break
opt.step(); seen += X.shape[0]
with torch.no_grad(): # EMA;前期按 (1+s)/(10+s) 放宽,避免随机初值占权重
dk = min(ema_d, (1 + step) / (10 + step))
for pe, pm in zip(ema.parameters(), model.parameters()): pe.lerp_(pm.detach(), 1 - dk)
if saver.stop() or (a.stop_at and step + 1 >= a.stop_at):
save('last.pt', step + 1); log.close()
print(f'收到停止请求:已存档 last.pt(第 {step + 1} 步),下次运行自动续训', flush=True); sys.exit(143)
@@ -116,21 +203,25 @@ def main():
'eta_h': (total - step - 1) * tc['batch'] / max(seen / max(el, 1e-6), 1e-6) / 3600, 'vram': vram.peak_gib()}
print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + '\n'); log.flush()
if not a.no_eval and (step + 1) % tc['eval_every'] == 0:
model.eval()
res, _ = evaluate(model, kin, val_tasks, dev)
res['step'] = step + 1; res['t'] = time.time()
# bs=64 + 归还缓存池:WDDM 卡满时 15.9 GiB 的评估峰值会被驱动换出到内存,eval 从 2 分钟变 1 小时
res, _ = evaluate(ema, kin, val_tasks, dev, bs=64)
if vdata is not None: res.update(open_loop(ema, kin, vdata, astep, fk_idx))
torch.cuda.empty_cache()
sc = res['success'] + 0.05 * (1 - res['collision_rate']) + 0.05 * (1 - min(1.0, res.get('goal_ratio_2s', 1.0))) \
- 1e-4 * min(res.get('ol_best_mm', 0.0), 500.0)
res['step'] = step + 1; res['t'] = time.time(); res['score'] = sc; res['weights'] = 'ema'
print('EVAL', json.dumps(res), flush=True); log.write(json.dumps({'eval': res}) + '\n'); log.flush()
save('last.pt', step + 1)
if res['success'] > best + 1e-4:
best = res['success']; bad_evals = 0; save('best.pt', step + 1, {'eval': res})
else:
if step + 1 >= sel_from and sc > best + 1e-5:
best = sc; bad_evals = 0; save('best.pt', step + 1, {'eval': res})
elif step + 1 >= min_step:
bad_evals += 1
if bad_evals >= tc['patience_evals']: print('闭环到达率不再上升,停止', flush=True); break
if bad_evals >= tc['patience_evals']: save('last.pt', step + 1); print('闭环分数不再上升,停止', flush=True); break
save('last.pt', step + 1)
if diverged: # 发散的权重不能覆盖 last.pt / best.pt:last.pt 保留发散前最后一次存档,M5 门禁会因此判不过 → 自动 WTA
print('训练发散,last.pt / best.pt 保留发散前的存档', flush=True); return
save('last.pt', step + 1)
if a.no_eval: save('best.pt', step + 1)
print(f'done, best success {best:.4f}, {(time.time() - t0) / 3600:.2f} h', flush=True)
if a.no_eval or best <= -1.0: save('best.pt', step + 1) # 没评估过 / 总步数不到 sel_from:best = last
print(f'done, best score {best:.4f}, {(time.time() - t0) / 3600:.2f} h', flush=True)
if __name__ == '__main__':
+5 -5
View File
@@ -14,7 +14,7 @@ const fs = require('fs'), path = require('path');
const argv = process.argv.slice(2);
const flag = k => argv.includes(k);
const opt = k => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : null; };
const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n']);
const VALOPT = new Set(['--out', '--nc-n', '--route-n', '--r8-n', '--fb', '--commit', '--seg', '--proj', '--select']);
const pos = argv.filter((a, i) => !a.startsWith('--') && !(i > 0 && VALOPT.has(argv[i - 1])));
const MODEL = pos[0] || path.join(__dirname, '../webapp/models/traj_policy.onnx');
const SETS = (pos[1] || 'nc,route').split(',').concat(flag('--nc1000') ? ['nc1000'] : []);
@@ -66,12 +66,12 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
const cert = POL.makeCert(S, {});
const out = { model: MODEL, meta: { fp: meta.fp, eval: meta.eval } };
const runSet = async (name, cases, mode) => {
let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0;
let ok = 0, reached = 0, colTasks = 0, colFrames = 0, uncert = 0, brakes = 0, locks = 0, plans = 0, recovered = 0, lockTasks = 0, livelock = 0, starve = 0;
const inf = [], saf = [], look = [], planT = [], planW = [], cq = [], times = [], fails = []; let hits = 0; const why = {};
for (let ci = 0; ci < cases.length; ci++) {
const c = cases[ci];
const R = mode === 'pose' ? c.R : null;
const r = new POL.Runner(S, infer, { seed: 1000 + ci });
const r = new POL.Runner(S, infer, { seed: 1000 + ci, actScale: meta.act_scale || 1, fbGain: +opt('--fb') || 0, commit: Math.max(1, +opt('--commit') || 3), segCert: opt('--seg') !== '0', projMax: opt('--proj') === '0' ? 0 : 0.5, select: opt('--select') || 'consist' }); // 2026-10-19:动作尺度随模型
r.reset(c.q0); r.setGoal(c.T, R);
let prev = c.q0.slice(), cf = 0, tLock = null;
for (let k = 0; k < 500 && !r.done; k++) {
@@ -86,14 +86,14 @@ const pct = (a, p) => { if (!a.length) return null; const s = a.slice().sort((x,
const { e, oe } = r.err(r.q);
const rc = r.done && e < 1 && oe < 0.5;
if (rc) reached++; if (cf) { colTasks++; colFrames += cf; } if (rc && !cf) { ok++; times.push(r.t); } else fails.push({ ci, err: +e.toFixed(1), oerr: +oe.toFixed(1), colFrames: cf, brakes: r.stats.brakes });
inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); planW.push(...r.stats.planWarmMs); cq.push(...r.stats.certQ); hits += r.stats.certHit;
inf.push(...r.stats.inferMs); saf.push(...r.stats.safetyMs); look.push(...r.stats.lookaheadMs); planT.push(...r.stats.planMs); planW.push(...r.stats.planWarmMs); starve += r.stats.starveTicks || 0; cq.push(...r.stats.certQ); hits += r.stats.certHit;
for (const k in r.stats.certWhy) why[k] = (why[k] || 0) + r.stats.certWhy[k]; brakes += r.stats.brakes; locks += r.stats.locks; plans += r.stats.plans;
if (tLock != null) { lockTasks++; if (rc && !cf && r.t - tLock <= 3.0) recovered++; } else if (rc && !cf) recovered++;
if (!rc && r.stats.locks >= 2) livelock++;
if ((ci + 1) % 50 === 0) process.stderr.write(`${name} ${ci + 1}/${cases.length}\n`);
}
out[name] = { n: cases.length, ok, reached, collisionTasks: colTasks, collisionFrames: colFrames, uncertifiedSegs: uncert, brakes, locks,
plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock,
plans, brakeRate: plans ? +(brakes / plans).toFixed(4) : null, starveTicks: starve, starvePerPlan: plans ? +(starve / plans).toFixed(3) : null, lockTasks, recovered3s: recovered, recoveryRate: cases.length ? +(recovered / cases.length).toFixed(4) : null, livelock,
timeP50: pct(times, 0.5), inferMsP50: pct(inf, 0.5), inferMsP99: pct(inf, 0.99), safetyMsP50: pct(saf, 0.5), safetyMsP99: pct(saf, 0.99),
// 2026-10-18 第六轮(审查 B-1/B-2):前瞻、整次规划耗时,每次规划的距离查询数与记忆命中率,认证失败原因——与查看器 autotest 同口径对照
lookaheadMsP99: pct(look, 0.99), planMsP50: pct(planT, 0.5), planMsP99: pct(planT, 0.99), planWarmMsP99: pct(planW, 0.99), certQueriesP50: pct(cq, 0.5), certQueriesP99: pct(cq, 0.99),
+717
View File
@@ -0,0 +1,717 @@
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import torch
from ml.eval.eval_cl import json_finite, run
from ml.eval.diag_collision import replay_certificates, replay_dense, replay_first_hits
from ml.sim.closed_loop import TeacherOracle, rollout
from ml.common.kin_torch import Kin, seg_seg_dist
class EvalMetricsTest(unittest.TestCase):
def setUp(self):
self.tasks = [
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 1, 'travel': 5},
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': None, 'travel': None},
{'q0': [0] * 6, 'T': [0] * 3, 'exec_s': 2, 'travel': 6},
]
self.per = {k: torch.tensor(v) for k, v in {
'ok': [True, False, True], 'done': [True, False, True],
't_done': [0.5, float('nan'), 2.0], 'col': [1.0, 0.0, 0.0],
'travel': [6.0, 0.0, 8.0], 'brakes': [2.0, 4.0, 6.0],
'start_fail': [False, True, False], 'goal_ratio_2s': [0.3, 0.9, 0.1],
'err': [0.2, 200.0, 0.8], 'dls_it': [4, -1, 20],
'speed_ratio': [1.66, 0.9, 1.2], 'accel_ratio': [0.3, 0.2, 0.5],
}.items()}
def evaluate(self, tasks, bs=2, **kw):
offset = 0
def fake_rollout(model, kin, batch, device, **kwargs):
nonlocal offset
p = {k: v[offset:offset + len(batch)] for k, v in self.per.items()}
offset += len(batch)
return {'time_budget': 'mixed'}, p
with patch('ml.eval.eval_cl.rollout', side_effect=fake_rollout), contextlib.redirect_stdout(io.StringIO()):
return run(None, None, tasks, 'cpu', bs=bs, **kw)[0]
def test_global_denominators_and_collision_free_arrival(self):
result = self.evaluate(self.tasks)
self.assertEqual(result['n_ref'], 2)
self.assertAlmostEqual(result['success'], 2 / 3, places=6)
self.assertAlmostEqual(result['collision_free_success'], 1 / 3, places=6)
self.assertAlmostEqual(result['collision_rate'], 1 / 3, places=6)
self.assertEqual(result['time_ok_1p3'], 1.0)
self.assertEqual(result['time_mean_s'], 1.25)
self.assertAlmostEqual(result['travel_ratio_median'], 1.2, places=6)
self.assertAlmostEqual(result['goal_ratio_2s'], 0.3, places=6)
self.assertEqual(result['err_fail_med'], 200.0)
self.assertEqual(result['brakes_mean'], 4.0)
# 极值/分位按逐题值跨批汇总,不能进按 n 加权求均值的批聚合
self.assertAlmostEqual(result['speed_ratio_p50'], 1.2, places=6)
self.assertAlmostEqual(result['speed_ratio_p95'], 1.614, places=6)
self.assertAlmostEqual(result['speed_ratio_max'], 1.66, places=5)
self.assertAlmostEqual(result['over_vmax_rate'], 2 / 3, places=6) # 1.66 与 1.2 都算超 VMAX,0.9 不算
self.assertAlmostEqual(result['accel_ratio_max'], 0.5, places=6)
def test_empty_conditional_metrics_and_explicit_limit(self):
task = {**self.tasks[0], 'exec_s': None, 'travel': None}
result = self.evaluate([task], max_sec=0.04)
self.assertIsNone(result['err_fail_med'])
self.assertIsNone(result['time_ok_1p3'])
self.assertIsNone(result['travel_ratio_median'])
self.assertEqual(result['time_budget'], 'fixed 0.040 s')
self.assertEqual(result['collision_free_success'], 0.0)
def test_json_replaces_nonfinite_without_changing_zero(self):
value = {'items': [float('nan'), float('inf'), -float('inf'), 0.0]}
text = json.dumps(json_finite(value), allow_nan=False)
self.assertEqual(json.loads(text), {'items': [None, None, None, 0.0]})
def test_first_hit_probe_detects_collision(self):
class PlaneKin:
def __init__(self, **kwargs):
self.cont = torch.zeros(6, dtype=torch.bool)
self.lo, self.hi = torch.full((6,), -10.0), torch.full((6,), 10.0)
def gaps(self, q, cap):
return {'gm': q[:, :1].expand(-1, 49).clamp(max=cap)}
traj = torch.zeros(1, 3, 6)
traj[0, :, 0] = torch.tensor([1.0, 1.0, -1.0])
per = {'col': torch.tensor([1.0]), 'first_hit': torch.tensor([0.04]),
'dls_it': torch.tensor([-1]), 'traj': traj}
with tempfile.TemporaryDirectory() as folder:
trace = Path(folder) / 'trace.pt'
torch.save({'tasks': [self.tasks[0]], 'batches': [({}, per, 1)]}, trace)
with patch('ml.eval.diag_collision.Kin', PlaneKin), contextlib.redirect_stdout(io.StringIO()):
result = replay_first_hits(trace)
self.assertEqual(result['reported_collision_tasks'], 1)
self.assertEqual(result['replayed_first_hits32'], 1)
self.assertEqual(result['replayed_first_hits64'], 1)
self.assertEqual(result['rows'][0]['gap64_mm'], [0.5, 0.5, 0.0, -1.0])
self.assertEqual(result['rows'][0]['dense_gap64_min_mm'], -1.0)
def test_dense_probe_detects_between_checkpoints_and_respects_deadline(self):
class ThinObstacle:
fp = 'probe'
def __init__(self, **kwargs):
pass
def gaps(self, q, cap):
return {'gm': ((q[:, :1] - 0.25).abs() - 0.05).clamp(max=cap)}
traj = torch.zeros(4, 3, 6)
traj[:, :, 0] = torch.tensor([[0, 1, 1], [0, -0.5, -1], [0, 0, 0.25], [0, 0, 0.25]])
per = {'traj': traj, 'col': torch.zeros(4), 'limit': torch.tensor([0.04, 0.04, 0.02, 0.04]),
't_done': torch.tensor([float('nan')] * 3 + [0.02])}
with tempfile.TemporaryDirectory() as folder:
trace = Path(folder) / 'trace.pt'
output = Path(folder) / 'audit.json'
torch.save({'kin_fp': 'probe', 'tasks': [{}] * 4, 'batches': [({}, per, 4)]}, trace)
with patch('ml.eval.diag_collision.Kin', ThinObstacle), contextlib.redirect_stdout(io.StringIO()):
result = replay_dense(trace, [0, 1, 2, 3], 0.01, out=output)
with self.assertRaisesRegex(ValueError, 'valid task indices'):
replay_dense(trace, [4])
with self.assertRaisesRegex(ValueError, 'positive max_step_deg'):
replay_dense(trace, [0], 0.0)
self.assertEqual(json.loads(output.read_text())['collision_tasks'], 1)
self.assertEqual(result['collision_tasks'], 1)
self.assertEqual(result['rows'][0]['collision_segments'], 1)
self.assertAlmostEqual(result['rows'][0]['min_gap64_mm'], -0.05)
self.assertGreater(result['rows'][1]['min_gap64_mm'], 0)
for row in result['rows'][2:]:
self.assertGreater(row['min_gap64_mm'], 0)
self.assertEqual(row['steps'], 1)
def test_certificate_replay_samples_actual_movement_before_deadlines(self):
kin = SegmentCertificateTest.ThinObstacle()
kin.fp = 'probe'
traj = torch.zeros(3, 4, 6)
traj[:, :, 0] = torch.tensor([[0, 1, 1, 1], [0, -0.5, -1, -1], [0, 0, 1, 1]])
per = {'traj': traj, 'limit': torch.tensor([0.06, 0.02, 0.02]),
't_done': torch.tensor([float('nan'), 0.02, float('nan')]),
'plans': {'it': torch.arange(3), 'commit': torch.zeros(3, 3, 1, 6),
'certified': torch.ones(3, 3, 1, dtype=torch.bool), 'gaps': torch.full((3, 3, 2), 0.5)}}
with tempfile.TemporaryDirectory() as folder:
trace, output = Path(folder) / 'trace.pt', Path(folder) / 'audit.json'
torch.save({'kin_fp': 'probe', 'tasks': [{}] * 3, 'batches': [({}, per, 3)]}, trace)
with patch('ml.eval.diag_collision.Kin', return_value=kin), contextlib.redirect_stdout(io.StringIO()):
result = replay_certificates(trace, [0, 1, 2], out=output, repeats=1, extra_segments=[(0, 0)])
with self.assertRaisesRegex(ValueError, 'extra segment'):
replay_certificates(trace, [1], repeats=1, extra_segments=[(1, 1)])
self.assertEqual(json.loads(output.read_text())['dense_collision_segments'], 1)
self.assertEqual([(r['i'], r['start_it']) for r in result['rows']], [(0, 0), (1, 0)])
self.assertEqual(result['budgets']['32']['safe'], 1)
self.assertEqual(result['budgets']['32']['collision'], 1)
self.assertEqual(result['accepted_dense_collision_segments'], 0)
self.assertTrue(result['batch_single_match'])
self.assertEqual(result['timing']['calls'], 2)
class DlsSafetyTest(unittest.TestCase):
def test_rejected_refinement_retains_safe_candidate(self):
class ObstacleKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
task = {'q0': [0] * 6, 'T': [2, 0, 0]}
traj = torch.zeros(1, 2, 6)
traj[0, 1, 0] = 0.1
ref = torch.zeros(1, 1, 6)
ref[0, 0, 0] = 1.0
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 2.0), torch.zeros(q.shape[0])),
'dls_steps': lambda *args, **kwargs: ref,
}
results = []
with patch.multiple('ml.sim.closed_loop', **patches):
for gate in (0, 1):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.02, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
safety_partial=1, dls_safety=gate)
results.append(per)
self.assertEqual(results[0]['col'].item(), 1.0)
self.assertEqual(results[0]['dls_it'].item(), 0)
self.assertEqual(results[1]['col'].item(), 0.0)
self.assertEqual(results[1]['dls_it'].item(), -1)
self.assertAlmostEqual(results[1]['traj'][0, 1, 0].item(), 0.1)
def test_feedback_is_reduced_only_after_full_rejection(self):
class ObstacleKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
return {'gm': torch.where(q[:, :1] > 0.3, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
traj = torch.zeros(1, 17, 6)
traj[0, :, 0] = -0.2 * torch.arange(17)
ref = torch.zeros(1, 1, 6)
ref[0, 0, 0] = 1.0
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
'dls_steps': lambda *args, **kwargs: ref,
}
results = []
with patch.multiple('ml.sim.closed_loop', **patches):
for gain in (1.0, 0.5):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.04, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=1,
safety_partial=1, fb_gain=0.6, fb_block_gain=gain)
results.append(per)
self.assertEqual(results[0]['brakes'].item(), 2.0)
self.assertEqual(results[1]['brakes'].item(), 1.0)
self.assertEqual(results[1]['traj'][0, 1, 0].item(), 0.0)
self.assertAlmostEqual(results[1]['traj'][0, 2, 0].item(), 0.1)
self.assertEqual(results[1]['col'].item(), 0.0)
class ClampedPathSafetyTest(unittest.TestCase):
def rollout_candidate(self, increments, safety_partial, goal_error=10.0, **kwargs):
class ObstacleKin:
def clamp_box(self, q):
clamped = q.clone()
clamped[..., 4] = clamped[..., 4].clamp(max=0.0)
return clamped
def gaps(self, q, cap):
collision = (q[:, :1] > 0.1) & (q[:, 4:5] < 0.2)
return {'gm': torch.where(collision, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
task = {'q0': [0] * 6, 'T': [goal_error, 0, 0]}
commit_n = len(increments)
traj = torch.zeros(1, commit_n + 1, 6)
traj[0, 1:] = torch.tensor(increments).cumsum(0)
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), goal_error), torch.zeros(q.shape[0])),
}
with patch.multiple('ml.sim.closed_loop', **patches):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.02 * commit_n, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=commit_n,
safety_partial=safety_partial, **kwargs)
return per
def test_full_safety_rejects_collision_after_joint_clamp(self):
# Raw endpoint/midpoint are safe, but the J5 upper limit puts the endpoint at (0.2, 0).
per = self.rollout_candidate([[0.2, 0, 0, 0, 0.6, 0]], safety_partial=0)
self.assertEqual(per['col'].item(), 0.0)
torch.testing.assert_close(per['traj'], torch.zeros(1, 2, 6), rtol=0, atol=0)
def test_partial_safety_rejects_collision_after_joint_clamp(self):
per = self.rollout_candidate([[0.2, 0, 0, 0, 0.6, 0]], safety_partial=1)
self.assertEqual(per['col'].item(), 0.0)
torch.testing.assert_close(per['traj'], torch.zeros(1, 2, 6), rtol=0, atol=0)
def test_partial_safety_keeps_only_clamped_safe_prefix(self):
per = self.rollout_candidate([
[0.05, 0, 0, 0, 0.6, 0],
[0.2, 0, 0, 0, 0.6, 0],
], safety_partial=1)
self.assertAlmostEqual(per['traj'][0, -1, 0].item(), 0.05)
self.assertEqual(per['traj'][0, -1, 4].item(), 0.0)
self.assertEqual(per['col'].item(), 0.0)
expected = torch.zeros(1, 3, 6)
expected[0, 1:, 0] = 0.05
torch.testing.assert_close(per['traj'], expected)
torch.testing.assert_close(per['plans']['commit'][0, 0, 1], torch.zeros(6), rtol=0, atol=0)
def test_near_dls_rejects_clamped_collision_and_keeps_candidate(self):
candidate = [[0.05, 0, 0, 0, 0.6, 0]]
ref = torch.tensor([[[0.2, 0, 0, 0, 0.6, 0]]])
for safety_partial in (0, 1):
with self.subTest(safety_partial=safety_partial):
with patch('ml.sim.closed_loop.dls_steps', return_value=ref) as dls:
per = self.rollout_candidate(candidate, safety_partial, goal_error=2.0, dls_safety=1)
dls.assert_called_once()
self.assertEqual(per['col'].item(), 0.0)
self.assertEqual(per['dls_it'].item(), -1)
self.assertEqual(per['brakes'].item(), 0.0)
expected = torch.zeros(1, 2, 6)
expected[0, 1, 0] = 0.05
torch.testing.assert_close(per['traj'], expected)
torch.testing.assert_close(per['plans']['commit'], torch.tensor(candidate)[None, None])
self.assertTrue(per['plans']['certified'].all().item())
def test_r3_clamped_queue_origin_prefix_and_certificates_match_execution(self):
class SafeKin:
def clamp_box(self, q):
clamped = q.clone()
clamped[..., 4] = clamped[..., 4].clamp(max=0.0)
return clamped
def gaps(self, q, cap):
return {'gm': torch.ones_like(q[:, :1])}
prefixes = []
def capture_features(kin, q, qp, T, R, pre, r, sub, sf):
prefixes.append((pre.clone(), int(r.item())))
return torch.zeros(1, 83)
teacher = torch.zeros(1, 25, 6)
teacher[0, :, 0] = 0.02 * torch.arange(25)
teacher[0, :, 4] = 0.6 * torch.arange(25)
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
patches = {
'features': capture_features,
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
}
with patch.multiple('ml.sim.closed_loop', **patches):
_, per = rollout(None, SafeKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.16, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(teacher), replan='r3', commit_n=3, safety_partial=1)
self.assertEqual([r for _, r in prefixes], [0, 2, 3, 3])
for pre, r in prefixes[1:]:
torch.testing.assert_close(pre[..., 4], torch.zeros(1, 3), rtol=0, atol=0)
torch.testing.assert_close(pre[:, :r, 0], torch.full((1, r), 0.02))
self.assertEqual(per['col'].item(), 0.0)
expected = torch.zeros(1, 9, 6)
expected[0, :, 0] = 0.02 * torch.arange(9)
torch.testing.assert_close(per['traj'], expected)
plans = per['plans']
self.assertEqual(plans['it'].tolist(), [0, 3, 6, 9])
torch.testing.assert_close(plans['commit'][..., 4], torch.full((1, 4, 3), 0.6))
self.assertTrue(plans['certified'].all().item())
self.assertTrue((plans['gaps'] > 0).all().item())
checked = 0
for i, start in enumerate(plans['it'].tolist()):
n = min(3, 8 - start)
if n <= 0:
continue
torch.testing.assert_close(plans['qe'][:, i], per['traj'][:, start], rtol=0, atol=0)
before = per['traj'][:, start:start + n]
after = per['traj'][:, start + 1:start + n + 1]
torch.testing.assert_close(plans['points'][:, i, :n], after, rtol=0, atol=0)
torch.testing.assert_close(plans['midpoints'][:, i, :n], before + 0.5 * (after - before),
rtol=0, atol=0)
checked += n
self.assertEqual(checked, 8)
def rollout_fallback(self, safety_partial, midpoint_collision=False):
class ObstacleKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
collision = q[:, :1] > 0.3
if midpoint_collision:
collision |= (q[:, :1] > 0.09) & (q[:, :1] < 0.11)
return {'gm': torch.where(collision, -torch.ones_like(q[:, :1]), torch.ones_like(q[:, :1]))}
teacher = torch.zeros(1, 4, 6)
teacher[0, :, 0] = torch.arange(4)
ref = torch.zeros(1, 1, 6)
ref[0, 0, 0] = 0.2
task = {'q0': [0] * 6, 'T': [10, 0, 0]}
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
'dls_steps': lambda *args, **kwargs: ref,
}
with patch.multiple('ml.sim.closed_loop', **patches):
_, per = rollout(None, ObstacleKin(), [task], 'cpu', K=1, safety=True,
max_sec=0.06, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(teacher), replan='empty', commit_n=3,
safety_partial=safety_partial, fb_gain=1.0, fb_fallback=1.0)
return per
def test_fallback_executes_one_safe_dls_step_then_certified_zeros(self):
for safety_partial in (0, 1):
with self.subTest(safety_partial=safety_partial):
per = self.rollout_fallback(safety_partial)
self.assertEqual(per['col'].item(), 0.0)
self.assertEqual(per['brakes'].item(), 1.0)
expected = torch.zeros(1, 4, 6)
expected[0, 1:, 0] = 0.2
torch.testing.assert_close(per['traj'], expected)
commit = torch.zeros(1, 1, 3, 6)
commit[0, 0, 0, 0] = 0.2
plans = per['plans']
self.assertEqual(plans['it'].tolist(), [0])
torch.testing.assert_close(plans['commit'], commit, rtol=0, atol=0)
self.assertTrue(plans['certified'].all().item())
self.assertTrue((plans['gaps'] > 0).all().item())
torch.testing.assert_close(plans['points'][:, 0], expected[:, 1:])
torch.testing.assert_close(plans['midpoints'][:, 0],
expected[:, :-1] + 0.5 * (expected[:, 1:] - expected[:, :-1]))
def test_fallback_rejects_safe_endpoint_with_colliding_midpoint(self):
for safety_partial in (0, 1):
with self.subTest(safety_partial=safety_partial):
per = self.rollout_fallback(safety_partial, midpoint_collision=True)
self.assertEqual(per['col'].item(), 0.0)
self.assertEqual(per['brakes'].item(), 1.0)
torch.testing.assert_close(per['traj'], torch.zeros(1, 4, 6), rtol=0, atol=0)
torch.testing.assert_close(per['plans']['commit'], torch.zeros(1, 1, 3, 6), rtol=0, atol=0)
self.assertFalse(per['plans']['certified'].any().item())
class SegmentCertificateTest(unittest.TestCase):
class ThinObstacle(Kin):
def __init__(self):
self.dtype = torch.float64
self.lo, self.hi = torch.full((6,), -5.0), torch.full((6,), 5.0)
self.cont = torch.zeros(6, dtype=torch.bool)
self.cert_groups = torch.tensor([0])
self.cert_lever = torch.tensor([[1.0, 0, 0, 0, 0, 0]], dtype=self.dtype)
def gaps(self, q, cap):
return {'gm': ((q[:, :1] - 0.25).abs() - 0.05).clamp(max=cap)}
def test_bound_detects_hidden_collision_and_accepts_clear_segment(self):
a, b = torch.zeros(3, 6, dtype=torch.float64), torch.zeros(3, 6, dtype=torch.float64)
a[:, 0] = torch.tensor([0.0, -1.0, 0.25])
b[:, 0] = torch.tensor([1.0, 0.0, 0.25])
result = self.ThinObstacle().certify_segments(a, b)
self.assertEqual(result['safe'].tolist(), [False, True, False])
self.assertEqual(result['collision'].tolist(), [True, False, True])
self.assertEqual(result['queries'].tolist(), [4, 2, 2])
def test_budget_and_depth_exhaustion_do_not_accept_unknown_segment(self):
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
b[:, 0] = 1.0
kin = self.ThinObstacle()
for kwargs in ({'max_queries': 2}, {'max_depth': 0}):
with self.subTest(**kwargs):
result = kin.certify_segments(a, b, **kwargs)
self.assertFalse(result['safe'].item())
self.assertTrue(result['unresolved'].item())
self.assertEqual(result['queries'].item(), 2)
def test_shape_mismatch_between_endpoints_is_rejected(self):
a = torch.zeros(1, 6, dtype=torch.float64)
b = torch.zeros(2, 6, dtype=torch.float64)
kin = self.ThinObstacle()
for method in (kin.certify_segments, kin.certify_uniform):
with self.subTest(method=method.__name__):
with self.assertRaisesRegex(ValueError, 'identical shape'):
method(a, b)
def test_budget_exhaustion_scales_with_gap_and_bound(self):
class ConstantGap(self.ThinObstacle):
def gaps(self, q, cap):
return {'gm': torch.full((q.shape[0], 1), 0.1, dtype=q.dtype, device=q.device)}
# 界取 1 mm/度:1° 段要证明无碰撞,必须细分到位移界 < 两端间隙之和(0.2 mm)。
kin = ConstantGap()
kin.cert_lever = torch.ones(1, 6, dtype=torch.float64)
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
b[:, 0] = 1.0
for budget, safe, queries in ((4, False, 3), (8, False, 5), (9, True, 9)):
with self.subTest(budget=budget):
result = kin.certify_segments(a, b, max_queries=budget)
self.assertEqual(bool(result['safe'].item()), safe)
self.assertEqual(int(result['queries'].item()), queries)
def test_tensor_budget_cannot_wrap_depth(self):
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
a[:, 0] = b[:, 0] = 0.25
for tensor in (torch.tensor(255, dtype=torch.uint8), torch.tensor(255)):
with self.subTest(dtype=tensor.dtype):
with self.assertRaisesRegex(ValueError, 'plain ints'):
self.ThinObstacle().certify_segments(a, b, max_depth=tensor)
def test_batch_matches_per_root_and_ignores_order(self):
kin = self.ThinObstacle()
a = torch.tensor([[0.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
b = torch.tensor([[1.0, 0, 0, 0, 0, 0], [0.25, 0, 0, 0, 0, 0], [0.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
batch = kin.certify_segments(a, b, max_queries=16)
self.assertEqual(batch['collision'].tolist(), [True, True, False])
self.assertEqual(batch['safe'].tolist(), [False, False, True])
for perm in ([0, 1, 2], [2, 1, 0], [1, 0, 2]):
index = torch.tensor(perm)
moved = kin.certify_segments(a[index], b[index], max_queries=16)
for key in ('safe', 'collision', 'unresolved', 'queries'):
torch.testing.assert_close(moved[key], batch[key][index], rtol=0, atol=0)
def test_empty_batch_and_all_rejected_frontier(self):
kin = self.ThinObstacle()
empty = kin.certify_segments(torch.zeros(0, 6, dtype=torch.float64), torch.zeros(0, 6, dtype=torch.float64))
self.assertEqual(empty['safe'].shape, (0,))
a = torch.tensor([[0.25, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
result = kin.certify_segments(a, a.clone(), max_queries=2)
self.assertEqual(result['safe'].tolist(), [False, False])
self.assertEqual(result['collision'].tolist(), [True, False])
self.assertEqual(result['outside_box'].tolist(), [False, True])
def test_uniform_matches_bound_formula_and_needs_no_round_trips(self):
class ConstantGap(self.ThinObstacle):
def gaps(self, q, cap):
return {'gm': torch.full((q.shape[0], 1), 0.1, dtype=q.dtype, device=q.device)}
# 界 1 mm/度、间隙 0.1 mm:子段判据 0.2 > 1/n 需要 n ≥ 6,n=5 必须落 unproven。
kin = ConstantGap()
kin.cert_lever = torch.ones(1, 6, dtype=torch.float64)
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
b[:, 0] = 1.0
for n, safe in ((5, False), (6, True)):
with self.subTest(n=n):
result = kin.certify_uniform(a, b, n=n)
self.assertEqual(bool(result['safe'].item()), safe)
self.assertEqual(result['points'], n + 1)
self.assertFalse(bool(result['collision'].item()))
self.assertEqual(bool(result['unproven'].item()), not safe)
self.assertAlmostEqual(result['cap_required_mm'], 1.0 / n + 2e-4, places=9)
def test_uniform_catches_hidden_collision_and_rejects_short_grids(self):
a = torch.tensor([[0.0, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
b = torch.tensor([[1.0, 0, 0, 0, 0, 0], [0.0, 0, 0, 0, 0, 0], [6.0, 0, 0, 0, 0, 0]], dtype=torch.float64)
kin = self.ThinObstacle()
short = kin.certify_uniform(a, b, n=2)
self.assertEqual(short['collision'].tolist(), [False, False, False])
self.assertEqual(short['unproven'].tolist(), [True, False, False])
self.assertEqual(short['safe'].tolist(), [False, True, False])
for n in (4, 8, 64):
with self.subTest(n=n):
result = kin.certify_uniform(a, b, n=n)
self.assertEqual(result['collision'].tolist(), [True, False, False])
self.assertEqual(result['safe'].tolist(), [False, True, False])
self.assertEqual(result['outside_box'].tolist(), [False, False, True])
def test_uniform_requires_plain_int_count(self):
a = torch.zeros(1, 6, dtype=torch.float64)
for tensor in (torch.tensor(8, dtype=torch.uint8), torch.tensor(8)):
with self.subTest(dtype=tensor.dtype):
with self.assertRaisesRegex(ValueError, 'subdivision count'):
self.ThinObstacle().certify_uniform(a, a.clone(), n=tensor)
def test_nonfinite_and_outside_box_are_rejected(self):
a, b = torch.zeros(1, 6, dtype=torch.float64), torch.zeros(1, 6, dtype=torch.float64)
kin = self.ThinObstacle()
with patch.object(kin, 'gaps', side_effect=lambda q, cap: {'gm': torch.full((q.shape[0], 1), float('nan'))}):
result = kin.certify_segments(a, b)
self.assertFalse(result['safe'].item())
self.assertTrue(result['unresolved'].item())
for value in (float('inf'), -float('inf')):
with self.subTest(value=value):
with patch.object(kin, 'gaps',
side_effect=lambda q, cap, v=value: {'gm': torch.full((q.shape[0], 1), v)}):
result = kin.certify_segments(a, b)
self.assertFalse(result['safe'].item())
a[:, 0] = b[:, 0] = 6.0
result = kin.certify_segments(a, b)
self.assertFalse(result['safe'].item())
self.assertTrue(result['outside_box'].item())
def test_point_segment_distance_is_symmetric_and_differentiable(self):
p = torch.tensor([[0.0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], dtype=torch.float64, requires_grad=True)
q = torch.tensor([[2.0, 0, 0]] * 4, dtype=torch.float64)
point = torch.tensor([[1.0, 0, 0], [1, 3, 0], [4, 0, 0], [-1, 0, 0]], dtype=torch.float64)
forward = seg_seg_dist(p, q, point, point)
reverse = seg_seg_dist(point, point, p, q)
torch.testing.assert_close(forward, torch.tensor([0.0, 3, 2, 1], dtype=torch.float64), atol=1e-8, rtol=0)
torch.testing.assert_close(forward, reverse, atol=1e-8, rtol=0)
forward.sum().backward()
self.assertTrue(torch.isfinite(p.grad).all())
torch.testing.assert_close(seg_seg_dist(p.detach(), p.detach(), point, point),
(p.detach() - point).norm(dim=-1), atol=1e-8, rtol=0)
class SpeedAndMarginTest(unittest.TestCase):
"""轮次33:反馈叠加后的每轴速度限幅,以及安全层的正余量门限(含 cap 截顶坑)。"""
STEP = [1.2, 1.2, 1.8, 1.8, 2.4, 3.6]
def rollout_one(self, teacher_step, gap_value, fb=None, **kwargs):
class FlatKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
return {'gm': torch.full((q.shape[0], 49), float(gap_value))}
commit_n = 2
traj = torch.zeros(1, commit_n + 1, 6)
traj[0, 1:] = torch.tensor(teacher_step)[None].repeat(commit_n, 1).cumsum(0) # teacher_step = 每步各轴的度数增量
patches = {
'features': lambda *args: torch.zeros(1, 83),
'goal_err': lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0])),
}
with patch.multiple('ml.sim.closed_loop', **patches):
with patch('ml.sim.closed_loop.dls_steps', return_value=torch.tensor([[[1.0, 0, 0, 0, 0, 0]]])):
_, per = rollout(None, FlatKin(), [{'q0': [0] * 6, 'T': [10, 0, 0]}], 'cpu', K=1, safety=True,
max_sec=0.02 * commit_n, return_traj=True, use_sub=False, select='first',
oracle=TeacherOracle(traj), replan='empty', commit_n=commit_n,
**kwargs)
return per
def test_feedback_over_speed_is_measured_not_assumed(self):
# 策略给满 STEP(J1 +1.2°)+ 2.2×1.0° 反馈 = 3.4° > VMAX·DT
per = self.rollout_one([1.2, 0, 0, 0, 0, 0], 5.0, fb_gain=2.2)
self.assertAlmostEqual(per['traj'][0, 1, 0].item(), 3.4, places=5)
self.assertAlmostEqual(per['speed_ratio'][0].item(), 3.4 / 1.2, places=5)
def test_inc_cap_limits_actual_step_to_vmax(self):
per = self.rollout_one([1.2, 0, 0, 0, 0, 0], 5.0, fb_gain=2.2, inc_cap=1)
self.assertAlmostEqual(per['traj'][0, 1, 0].item(), 1.2, places=5)
self.assertLessEqual(per['speed_ratio'][0].item(), 1.0 + 1e-6)
def test_margin_rejects_near_obstacle_step_that_passes_at_zero(self):
moved = self.rollout_one([0.5, 0, 0, 0, 0, 0], 0.3, safety_partial=1)
self.assertAlmostEqual(moved['traj'][0, -1, 0].item(), 1.0, places=5)
self.assertEqual(moved['brakes'].item(), 0.0)
braked = self.rollout_one([0.5, 0, 0, 0, 0, 0], 0.3, gap_margin=0.5, safety_partial=1)
self.assertEqual(braked['brakes'].item(), 1.0)
torch.testing.assert_close(braked['traj'], torch.zeros(1, 3, 6), rtol=0, atol=0)
def test_margin_below_cap_still_accepts_clear_step(self):
# 余量 0.5 必须仍能通过 5 mm 间隙:若宽相位 cap 没抬到 margin+0.5,截顶值 0.5 会让门限恒假
per = self.rollout_one([0.5, 0, 0, 0, 0, 0], 5.0, gap_margin=0.5, safety_partial=1)
self.assertEqual(per['brakes'].item(), 0.0)
self.assertAlmostEqual(per['traj'][0, -1, 0].item(), 1.0, places=5)
self.assertTrue((per['plans']['gaps'] > 0.5).all().item())
class FeatureGeometryReuseTest(unittest.TestCase):
"""#16 第①条:gm_groups 复用调用者已算好的 cum_xf,省一次 FK,但结果必须与原口径逐位一致。"""
def test_gm_groups_with_supplied_xf_is_bitwise_identical(self):
from ml.common.features import features, GP, GAP_CAP
kin = Kin()
q = torch.tensor([[-129.9, 46.68, 43.49, -97.46, -85.1, -139.7],
[10.0, -20.0, 30.0, -40.0, 50.0, -60.0]])
xf = kin.cum_xf(q)
torch.testing.assert_close(kin.gm_groups(q, GP, GAP_CAP, xf=xf), kin.gm_groups(q, GP, GAP_CAP),
rtol=0, atol=0)
idx = [a * 7 + b for a, b in GP]
table = kin.gaps(q, GAP_CAP, xf)['gm'][:, idx]
torch.testing.assert_close(kin.gm_groups(q, GP, GAP_CAP), table, rtol=0, atol=0)
z = torch.zeros(len(q), 6)
f = features(kin, q, z, q[:, :3], torch.zeros(len(q), 9), z[:, None, :].expand(len(q), 3, 6),
torch.zeros(len(q)), z, torch.zeros(len(q)))
cols = 12 + 3 + 6 + 3 + 6 + 3 + 6 + 3 + 3 + 2 + 18 + 3 + 6 + 1 # 间隙块之前的特征数 = 75
self.assertEqual(cols + 8, 83)
torch.testing.assert_close(f[:, cols:cols + 8], table / GAP_CAP, rtol=0, atol=0)
class CertBatchEquivalenceTest(unittest.TestCase):
"""#16 第②条:把 K 条候选的认证并成一次 gaps 调用,选择结果必须与串行循环逐位一致。"""
class StubModel:
act_scale = 1.0
def __init__(self, act):
self.act = act
def sample_k(self, feat, K, z, steps=1):
B = feat.shape[0]
return (self.act.expand(B, K, 16, 6).clone(), torch.zeros(B, device=feat.device),
torch.zeros(B, 13, device=feat.device))
def run_both(self, acts, reject_first):
class StepKin:
def clamp_box(self, q):
return q
def gaps(self, q, cap):
bad = (q[:, 0] > 0.5) if reject_first else torch.zeros(len(q), dtype=torch.bool, device=q.device)
return {'gm': torch.where(bad[:, None], -torch.ones(len(q), 49, device=q.device),
torch.full((len(q), 49), 1.0, device=q.device))}
acts = torch.tensor(acts, dtype=torch.float32) # [K,16,6] 动作(±1 归一化)
model = self.StubModel(acts)
tasks = [{'q0': [0] * 6, 'T': [10, 0, 0]}] * 2 # B=2:批量索引形状必须在多行下也成立
out = []
for batched in (0, 1):
with patch('ml.sim.closed_loop.features', return_value=torch.zeros(len(tasks), 83)), \
patch('ml.sim.closed_loop.goal_err',
side_effect=lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0]))):
_, per = rollout(model, StepKin(), tasks, 'cpu', K=acts.shape[0], safety=True, max_sec=0.04,
return_traj=True, use_sub=False, select='first', replan='empty', commit_n=2,
safety_partial=1, cert_batch=batched)
out.append(per)
return out
def test_batched_certification_selects_the_same_candidate(self):
# 候选 0 第 2 步撞上 q0>0.5 的障碍 ⇒ 只有前缀 1 步;候选 1 全程安全 ⇒ 应被选中
acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14,
[[0.1, 0, 0, 0, 0, 0], [0.1, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
seq, batched = self.run_both(acts, reject_first=True)
torch.testing.assert_close(batched['traj'], seq['traj'], rtol=0, atol=0)
torch.testing.assert_close(batched['plans']['commit'], seq['plans']['commit'], rtol=0, atol=0)
torch.testing.assert_close(batched['plans']['certified'], seq['plans']['certified'], rtol=0, atol=0)
self.assertEqual(batched['brakes'].tolist(), seq['brakes'].tolist())
self.assertAlmostEqual(seq['traj'][0, -1, 0].item(), 0.24, places=5) # 候选 1 的两步 0.1×STEP=0.12°
def test_batched_certification_keeps_partial_prefix_choice(self):
# 所有候选都只走得了第 1 步 ⇒ 两条路径都取第 1 条候选、前缀长度 1
acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14,
[[0.3, 0, 0, 0, 0, 0], [0.6, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14]
seq, batched = self.run_both(acts, reject_first=True)
torch.testing.assert_close(batched['traj'], seq['traj'], rtol=0, atol=0)
torch.testing.assert_close(batched['plans']['commit'], seq['plans']['commit'], rtol=0, atol=0)
self.assertAlmostEqual(seq['traj'][0, 1, 0].item(), 0.24, places=5)
self.assertAlmostEqual(seq['traj'][0, 2, 0].item(), 0.24, places=5) # 第 2 步被拒后停在原地
def test_mismatched_xf_row_count_is_rejected(self):
kin = Kin()
q1 = torch.zeros(1, 6); q5 = torch.zeros(5, 6)
xf5 = kin.cum_xf(q5)
for name, fn in (('gaps', lambda: kin.gaps(q1, 40.0, xf5)),
('gm_groups', lambda: kin.gm_groups(q1, [(0, 2)], 40.0, xf=xf5))):
with self.subTest(fn=name):
with self.assertRaisesRegex(ValueError, '行数'):
fn()
self.assertEqual(kin.gaps(q5, 40.0, xf5)['gm'].shape, (5, 49))
self.assertEqual(kin.gm_groups(q5, [(0, 2)], 40.0, xf=xf5).shape, (5, 1))
if __name__ == '__main__':
unittest.main()
+3 -1
View File
@@ -55,7 +55,9 @@ async function boot() {
}
if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' });
const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once, featDyn: !!meta.feat_dyn });
runner = new ArmPolicy.Runner(S, infer, {});
const FB = qs.get('fb') === null ? 1.8 : +qs.get('fb'); // 2026-10-20:全程目标反馈(fb=0 关闭)
const COMMIT = qs.get('commit') === null ? 3 : Math.max(1, +qs.get('commit')); // 部署节奏:commit=1 每步规划
runner = new ArmPolicy.Runner(S, infer, { actScale: meta.act_scale || 1, fbGain: FB, commit: COMMIT }); // 2026-10-19:动作尺度随模型(旧模型无此字段 = 1)
/* 预热:首次推理有编译开销 */
await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8);
const tick = URL.createObjectURL(new Blob(['let h=null;onmessage=e=>{clearInterval(h);if(e.data>0)h=setInterval(()=>postMessage(0),e.data)}'], { type: 'text/javascript' }));
+118 -36
View File
@@ -138,7 +138,19 @@ function refineStep(S, q, T, Rt, maxScale) {
return r.concat([0.5 * (M[7] - M[5]) * 100, 0.5 * (M[2] - M[6]) * 100, 0.5 * (M[3] - M[1]) * 100]);
};
const e0 = e(q), m = e0.length, h = 1e-3, J = [];
if (S.jac) {
/* 解析几何雅可比(与 ml/sim/closed_loop.py 的 dls_steps(analytic=True) 同口径):
有限差分在 float32/双精度下都会给反馈方向注入 ~1–4% 噪声,闭环里会被慢变误差放大。
J[j][i] = −∂err_i/∂q_j:位置 err=T−p → +Jv;姿态 err=100·rotvec → +100·Jw。 */
const jv = S.jac(q);
for (let j = 0; j < 6; j++) {
const col = [jv.Jv[0][j], jv.Jv[1][j], jv.Jv[2][j]];
if (m === 6) col.push(100 * jv.Jw[0][j], 100 * jv.Jw[1][j], 100 * jv.Jw[2][j]);
J.push(col);
}
} else {
for (let j = 0; j < 6; j++) { const qa = q.slice(); qa[j] += h; const e1 = e(qa); J.push(e1.map((v, i) => -(v - e0[i]) / h)); }
}
// Δq = Jᵀ (J Jᵀ + λ²I)⁻¹ e,m ≤ 6
const lam = 0.5, A = [];
for (let i = 0; i < m; i++) { A.push([]); for (let k = 0; k < m; k++) { let s = i === k ? lam * lam : 0; for (let j = 0; j < 6; j++) s += J[j][i] * J[j][k]; A[i].push(s); } }
@@ -178,19 +190,21 @@ class Runner {
this.S = S; this.infer = infer;
this.o = Object.assign({ K: 8, commit: 3, margin: 5, /* 2026-10-18:运行时投影余量保持 5 mm(真机护栏,不迁就数据分布);数据侧另行过滤认证间隙 < 3 mm 的老师轨迹,APG 训练余量 3 mm */
projMax: 0.5, refineMm: 5, arriveMm: 1, arriveDeg: 0.5,
stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, seed: 12345, lookahead: true,
certBudget: 40, safetyMs: 30, firstMs: 45 }, opts || {}); // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理)
stuckPlans: 5, lockSec: 1.0, subReachDeg: 5, subTimeout: 1.5, subRule: 'cross', subLeadSec: 0.02, seed: 12345, lookahead: true,
actScale: 1, certBudget: 40, safetyMs: 30, firstMs: 45, fbGain: 0, segCert: true, select: 'consist' }, opts || {});
/* 2026-10-19:actScale = 模型 meta.act_scale(动作标签按 act_scale·STEP 归一化,旧模型 1);增量 = clamp(act, ±1)·actScale·STEP,
与 ml/sim/closed_loop.py 同口径。subRule:'cross'(默认,越过顶点即切,与训练标签同口径;见 _subAdvance)/ 'timeout'(旧:< subReachDeg 或 subTimeout 超时)。 */ // 每条候选最多 40 次距离查询;安全层总预算 30 ms,第 1 条候选可用到 45 ms(超出按刹停处理)
this.cert = makeCert(S, { leverTable: this.o.leverTable, cap: 20 });
this.rng = makeRng(this.o.seed);
/* 2026-10-18 第六轮(审查 B-1/B-2):耗时拆开记——inferMs 主推理、lookaheadMs 前瞻(第二次推理 + 8 份特征)、safetyMs 只含认证、
planMs 整次规划(R9 口径:必须 < 60 ms 周期);certQ 每次规划新增的距离查询数,certHit 记忆命中数,certWhy 认证失败原因计数 */
this.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0 };
this.stats = { plans: 0, inferMs: [], lookaheadMs: [], safetyMs: [], planMs: [], planWarmMs: [], planColdMs: [], certQ: [], certHit: 0, certWhy: {}, brakes: 0, refine: 0, lookahead: 0, locks: 0, projected: 0, partial: 0, starveTicks: 0 };
this.reset(null);
}
reset(q0) {
this.q = q0 ? q0.slice() : null; this.qPrev = this.q ? this.q.slice() : null;
this.queue = []; this.brakeTail = []; this.lastPlan = null; this.goal = null;
this.subs = []; this.subT = 0; this.vals = []; this.brakeRun = 0; this.lock = null; this.t = 0; this.cold = true; this.done = false;
this.subs = []; this.subT = 0; this.subPrev = null; this.vals = []; this.brakeRun = 0; this.lock = null; this.t = 0; this.cold = true; this.done = false;
}
setGoal(T, R) {
const jump = !this.goal || Math.hypot(T[0] - this.goal.T[0], T[1] - this.goal.T[1], T[2] - this.goal.T[2]) > 50 || (!!R !== !!this.goal.R);
@@ -211,7 +225,10 @@ class Runner {
if (!this.q) return null;
const d = this.queue.shift();
this.qPrev = this.q.slice();
if (d) this.q = this.q.map((v, j) => v + d[j]);
if (d) this.q = this.q.map((v, j) => { // 与 ml/sim/closed_loop.py 的 clamp_box 同口径:限位轴执行时夹到 LIM
const id = this.S.ORDER[j], L = this.S.LIM[id], x = v + d[j];
return L.cont ? x : Math.max(L.lo, Math.min(L.hi, x));
});
this.t += DT;
if (this.goal && !this.queue.length) {
const { e, oe } = this.err(this.q);
@@ -220,29 +237,79 @@ class Runner {
return this.q;
}
_cand(act, k, r) { // 第 k 条候选:增量(°),丢掉模型的前 r 步(已提交前缀),取接下来的步
const inc = [];
for (let s = 0; s < H; s++) { const row = []; for (let j = 0; j < 6; j++) row.push(Math.max(-1, Math.min(1, act[(k * H + s) * 6 + j])) * STEP[j]); inc.push(row); }
const inc = [], sc = this.o.actScale || 1;
for (let s = 0; s < H; s++) { const row = []; for (let j = 0; j < 6; j++) row.push(Math.max(-1, Math.min(1, act[(k * H + s) * 6 + j])) * sc * STEP[j]); inc.push(row); }
return inc;
}
_certCommit(qe, steps) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
/* 子目标推进(与 ml/sim/closed_loop.py sub_advance 同一规则,三处同步改)。
2026-10-19(审查 C5 / P0-1):训练标签里 sub = 老师「下一个尚未经过的顶点」,老师 τ 一过顶点(−0.02 s)就换;
旧规则要 < 5° 或 1.5 s 超时,策略在顶点附近进不去时 sub 一直指着身后的点(训练里从未出现的特征序列)。
'cross':进入方向 a = 顶点 − 上一点,离开方向 b = 下一顶点 − 顶点(最后一个取 a),各轴 /VMAX 折成秒;
法向 n = â + b̂,(qe − 顶点)·n̂ ≥ −subLeadSec 即越过。没有超时(标签也没有)。 */
_subAdvance(qe) {
if (!this.subs.length) return;
const S = this.S, o = this.o, sg = this.subs[0];
const df = (a, b, j) => (S.LIM[S.ORDER[j]].cont ? wrap180(a - b) : a - b);
let adv = false;
if (o.subRule === 'timeout') {
let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs(df(sg[j], qe[j], j)));
adv = m < o.subReachDeg || this.t - this.subT > o.subTimeout;
} else {
const unit = v => { const n = Math.hypot.apply(null, v); return n > 1e-9 ? v.map(x => x / n) : v.map(() => 0); };
const prev = this.subPrev || this.q;
const a = [0, 1, 2, 3, 4, 5].map(j => df(sg[j], prev[j], j) / VMAX[j]);
const b = this.subs.length > 1 ? [0, 1, 2, 3, 4, 5].map(j => df(this.subs[1][j], sg[j], j) / VMAX[j]) : a;
const ua = unit(a), ub = unit(b), n = unit(ua.map((x, j) => x + ub[j]));
let proj = 0; for (let j = 0; j < 6; j++) proj += df(qe[j], sg[j], j) / VMAX[j] * n[j];
adv = proj >= -o.subLeadSec;
}
if (adv) { this.subPrev = this.subs.shift(); this.subT = this.t; }
}
_certCommit(qe, steps, partial) { // 认证:提交步 + 刹停段(先用离散判定快速剔除,再做距离下界认证)
/* partial:与 ml/sim/closed_loop.py 的 safety_partial 同口径——整段不过时依次缩短前缀,
执行到第一个不安全步为止(而不是整段拒绝 → 刹停)。2026-10-20:浏览器 bench 刹车率 47% 的主因是整段拒绝。 */
const tryPath = n => {
const use = steps.slice(0, n);
const pts = [qe.slice()]; let q = qe.slice();
for (const d of steps) { q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
for (let i = 1; i < pts.length; i++) if (!this.S.collideFree(pts[i])) return { ok: false, tail: [] };
const tail = brakeSteps(steps.length ? steps[steps.length - 1] : [0, 0, 0, 0, 0, 0]);
for (const d of use) { q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
if (!this.o.segCert) {
/* 离散判定(步点 + 中点,与 ml/sim/closed_loop.py 安全层同口径):capsule 模型余量已含在 collideFree。
连续区间认证(segCert)~40 ms/次,是 50 Hz 部署的主要瓶颈;关闭后 planP50 预计 <30 ms。 */
let prev = qe.slice();
for (const p of pts.slice(1)) {
const mid = prev.map((v, j) => 0.5 * (v + p[j]));
if (!this.S.collideFree(mid) || !this.S.collideFree(p)) return null;
prev = p;
}
const tail = brakeSteps(use.length ? use[use.length - 1] : [0, 0, 0, 0, 0, 0]);
let qq = q;
if (tail.length) { for (const d of tail) qq = qq.map((v, j) => v + d[j]); if (!this.S.collideFree(qq)) return null; }
return { ok: true, tail: tail, steps: use };
}
for (let i = 1; i < pts.length; i++) if (!this.S.collideFree(pts[i])) return null;
const tail = brakeSteps(use.length ? use[use.length - 1] : [0, 0, 0, 0, 0, 0]);
/* 刹停段各步同向等比减速 → 关节空间里是一条直线,认证终点一段即可 */
if (tail.length) { for (const d of tail) q = q.map((v, j) => v + d[j]); pts.push(q.slice()); }
let qq = q;
if (tail.length) { for (const d of tail) qq = qq.map((v, j) => v + d[j]); pts.push(qq.slice()); }
const c = this.cert.path(pts, this.o.certBudget, this._until);
if (!c.ok) this.stats.certWhy[c.why || 'collide'] = (this.stats.certWhy[c.why || 'collide'] || 0) + 1;
return { ok: c.ok, tail };
return { ok: c.ok, tail: tail, steps: use };
};
if (!partial) { const r = tryPath(steps.length); return r || { ok: false, tail: [] }; }
for (let n = steps.length; n >= 1; n--) { const r = tryPath(n); if (r && r.ok) return r; }
return { ok: false, tail: [] };
}
async plan() {
if (!this.needPlan()) return;
this.planning = true;
const qlen0 = this.queue.length; // 规划开始时队列里可执行步数
const t = now(), c0 = this.cert.counters(), wasCold = this.cold; // 第八轮:冷启动(含前瞻)与稳态分开记,R9 判稳态
try { await this._plan(); } finally {
this.planning = false; this._until = undefined;
const c1 = this.cert.counters();
const dt = now() - t; this.stats.planMs.push(dt); (wasCold ? this.stats.planColdMs : this.stats.planWarmMs).push(dt); this.stats.certQ.push(c1.queries - c0.queries); this.stats.certHit += c1.hits - c0.hits;
/* 50 Hz 饥饿估计:规划耗时 dt 期间每 20 ms 消耗一步;超过 qlen0 的步数即队列空转 tick */
this.stats.starveTicks += Math.max(0, Math.ceil(dt / (DT * 1000)) - qlen0);
}
}
async _plan() {
@@ -257,13 +324,11 @@ class Runner {
const steps = []; let q = qe.slice();
for (let k = 0; k < o.commit; k++) { const rs = refineStep(S, q, this.goal.T, this.goal.R, 0.3); steps.push(rs.dq); q = q.map((v, j) => v + rs.dq[j]); }
const c = this._certCommit(qe, steps);
if (c.ok) { this.queue.push(...steps); this.brakeTail = c.tail; this.stats.refine++; return; }
/* 2026-10-19(审查 F5):DLS 接管时没有「已提交的模型计划」,lastPlan 置空(原来保留上一条,下次一致性去对齐一条没执行的轨迹;仿真同步) */
if (c.ok) { this.queue.push(...steps); this.brakeTail = c.tail; this.stats.refine++; this.lastPlan = null; return; }
}
/* 子目标推进 */
if (this.subs.length) {
const sg = this.subs[0]; let m = 0; for (let j = 0; j < 6; j++) m = Math.max(m, Math.abs((S.LIM[S.ORDER[j]].cont ? wrap180(sg[j] - qe[j]) : sg[j] - qe[j])));
if (m < o.subReachDeg || this.t - this.subT > o.subTimeout) { this.subs.shift(); this.subT = this.t; }
}
this._subAdvance(qe);
const gm = S.gapMatrix(this.q, GAP_CAP);
const st = { q: this.q, qPrev: this.qPrev, T: this.goal.T, R: this.goal.R, prefix, sub: this.subs[0] || null };
const f = features(S, st, gm);
@@ -279,28 +344,28 @@ class Runner {
if (this.needSub) {
this.needSub = false; this.subT = this.t;
const sb = out.sub.subarray(best * 13, best * 13 + 13);
if (sb[12] > 0.5) this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180));
if (sb[12] > 0.5) { this.subs = [0, 1].map(v => this.q.map((x, j) => x + sb[v * 6 + j] * 180)); this.subPrev = this.q.slice(); }
}
/* 候选:去掉前缀后的提交步,投影,打分 */
const cands = [], near = S.criticalPairs(qe, o.margin + 10).slice(0, 64);
const cands = [], near = o.projMax > 0 ? S.criticalPairs(qe, o.margin + 10).slice(0, 64) : [];
// 2026-10-20:全程目标反馈(与 ml/sim/closed_loop.py 的 fb_gain 一致)——一次 DLS,加到所有候选所有步
const fbStep = (o.fbGain > 0 && this.goal) ? refineStep(S, qe, this.goal.T, this.goal.R, 0.3).dq : null;
for (let k = 0; k < K; k++) {
const inc = this._cand(out.act, k, r);
let steps = inc.slice(r, r + o.commit), q = qe.slice();
const adj = [];
for (const d of steps) {
let qn = q.map((v, j) => v + d[j]);
const qp = project(S, qn, o.margin, o.projMax, near);
if (qp !== qn) this.stats.projected++;
adj.push(qp.map((v, j) => v - q[j])); q = qp;
}
steps = adj;
if (fbStep) for (let s = 0; s < H; s++) for (let j = 0; j < 6; j++) inc[s][j] += o.fbGain * fbStep[j];
const steps = inc.slice(r, r + o.commit); // 先不投影;只对进入认证的候选投影(2026-10-20:投影 8 份候选占安全层 ~13 ms)
const fut = []; let qq = qe.slice(); for (let s = r; s < H; s++) { qq = qq.map((v, j) => v + inc[s][j]); fut.push(qq); }
let j1 = 0; for (let s = 0; s < H; s++) j1 += inc[s][0];
let score = -2.0 * out.value[k];
if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐(时间平移 3 步)
if (o.select === 'endpoint') {
/* 2026-10-20:端点进度选择(与 ml/sim/closed_loop.py select='endpoint' 同口径)——n=512 时间效率 29→35%、ratio2s 0.343→0.285 */
const qa = fut[fut.length - 1], pe = S.fk(qa, 0);
score = -Math.hypot(pe[0] - this.goal.T[0], pe[1] - this.goal.T[1], pe[2] - this.goal.T[2]);
} else if (this.lastPlan) { // 与上一条已选计划的未来重叠步对齐:lastPlan[i] = 旧 qe 走 i 步,新 qe = lastPlan[commit],fut[s] 对应 lastPlan[s+commit+1]
// 2026-10-19 修错位(原对齐 lastPlan[s+commit],奖励比上一计划慢一步的候选;Python closed_loop.py 同步修正)
let c = 0, w = 1;
for (let s = 0; s < fut.length && s + o.commit < this.lastPlan.length; s++, w *= 0.85) {
let d2 = 0; for (let j = 0; j < 6; j++) d2 += Math.pow((fut[s][j] - this.lastPlan[s + o.commit][j]) / STEP[j], 2);
for (let s = 0; s < fut.length && s + o.commit + 1 < this.lastPlan.length; s++, w *= 0.85) {
let d2 = 0; for (let j = 0; j < 6; j++) d2 += Math.pow((fut[s][j] - this.lastPlan[s + o.commit + 1][j]) / STEP[j], 2);
c += w * d2;
}
score -= 0.02 * c;
@@ -319,7 +384,7 @@ class Runner {
/* 短前瞻:冷启动(新目标 / 目标跳变)时,各候选块末态再推一块,用其价值打分。
2026-10-18 第六轮:去掉"卡死时也前瞻"——与仿真 ml/sim/closed_loop.py(只在首块前瞻)对齐;
前瞻要算 8 份特征(8 次 gapMatrix cap40,沙箱 Node 实测 P99 126 ms),卡死时再做会让整次规划连续超周期。卡死由换向锁处理。 */
if (o.lookahead && this.cold) {
if (o.lookahead && this.cold && o.select !== 'endpoint') {
this.stats.lookahead++;
const feat2 = new Float32Array(pool.length * F_DIM), z2 = new Float32Array(pool.length * H * 6);
pool.forEach((c, i) => {
@@ -338,16 +403,33 @@ class Runner {
collideFree 1.5 ms、criticalPairs 3.1 ms,推理本身 1.5 ms——耗时在几何查询,不在模型。此前截止只在候选之间检查,
单条候选最多 40 次查询(≈150 ms)不受限。现在墙钟截止下沉到认证内部:第 1 条候选截止 t1+firstMs,其余共享到 t1+safetyMs;
到点判不过 → 沿上次已认证的刹停段停,安全性不变,只是可能多刹停。 */
let picked = null;
let picked = null, pickedSteps = null;
for (const c of pool) {
if (c !== pool[0] && now() - t1 > o.safetyMs) break;
this._until = t1 + (c === pool[0] ? Math.max(o.firstMs, o.safetyMs) : o.safetyMs);
const v = this._certCommit(qe, c.steps); if (v.ok) { picked = c; this.brakeTail = v.tail; break; }
let steps = c.steps;
if (o.projMax > 0) { // 投影只做给要认证的候选(按需),不再对全部 K 条预算
const adj = []; let q = qe.slice();
for (const d of c.steps) {
const qn = q.map((v, j) => v + d[j]);
let qp = project(S, qn, o.margin, o.projMax, near);
qp = qp.map((v, j) => { const L = S.LIM[S.ORDER[j]]; return L.cont ? v : Math.max(L.lo, Math.min(L.hi, v)); });
if (qp !== qn) this.stats.projected++;
adj.push(qp.map((v, j) => v - q[j])); q = qp;
}
steps = adj;
}
const v = this._certCommit(qe, steps, true);
if (v.ok) { picked = c; pickedSteps = v.steps; this.brakeTail = v.tail; break; }
}
this._until = undefined;
this.stats.safetyMs.push(now() - t1);
if (picked) {
this.queue.push(...picked.steps); this.lastPlan = [qe.slice()].concat(picked.fut); this.brakeRun = 0;
if (pickedSteps.length < picked.steps.length) this.stats.partial++;
this.queue.push(...pickedSteps);
/* 部分前缀时实际执行步数 < commit,一致性对齐的偏移不再成立 → 置空(Python 侧同轮次 14 的部分前缀满分对齐留待后续统一) */
this.lastPlan = pickedSteps.length === picked.steps.length ? [qe.slice()].concat(picked.fut) : null;
this.brakeRun = 0;
} else {
this.stats.brakes++; this.brakeRun++;
if (this.brakeTail.length) { this.queue.push(...this.brakeTail); this.brakeTail = []; }
+18 -1
View File
@@ -731,6 +731,23 @@ function make(cfg) {
for (let i = 0; i < n; i++) M = mul3(M, rotAxis(D[ORDER[i]], q[i] * D2R));
return M;
}
/* 几何雅可比(对度):Jv[i][j] = d p_i/d q_j (mm/deg),Jw[i][j] = 世界角速度分量/deg。
J4/J5 同轴不影响正确性:两者各贡献同一世界轴。 */
function jac(q) {
const { R: Rs, T: Ts } = cumXf(q);
const p = fk(q, 0);
const Jv = [[], [], []], Jw = [[], [], []];
for (let j = 0; j < 6; j++) {
const id = ORDER[j], Rj = Rs[j], tj = Ts[j], pj = P[id];
const a = mv3(Rj, D[id]);
const lever = [p[0] - (Rj[0] * pj[0] + Rj[1] * pj[1] + Rj[2] * pj[2] + tj[0]),
p[1] - (Rj[3] * pj[0] + Rj[4] * pj[1] + Rj[5] * pj[2] + tj[1]),
p[2] - (Rj[6] * pj[0] + Rj[7] * pj[1] + Rj[8] * pj[2] + tj[2])];
const v = cross3(a, lever);
for (let i = 0; i < 3; i++) { Jv[i][j] = v[i] * D2R; Jw[i][j] = a[i] * D2R; }
}
return { Jv: Jv, Jw: Jw };
}
/* M5 第 3 列(工具轴)列向量传播:J4/J5 同轴 ⇒ R4·R5=R(θ4+θ5) 合并为一次旋转,
只乘单位向量、不构造 3×3、免 T3/mul3/rotErrDeg/acos。与 orient(q,5) 列 2 一致(≤1e-15)。 */
const SAMEAX = D.J4[0] === D.J5[0] && D.J4[1] === D.J5[1] && D.J4[2] === D.J5[2];
@@ -3233,7 +3250,7 @@ function make(cfg) {
return { poseHit: pHit, poseTried: pTried, poseErr: pErr, poseMs: pTried ? pMs / pTried : 0 };
}
return { fk: fk, fkChain: fkChain, solve: solve, solveTo: solveTo, solveWarm: solveWarm, solveDyn: solveDyn,
return { fk: fk, fkChain: fkChain, jac: jac, solve: solve, solveTo: solveTo, solveWarm: solveWarm, solveDyn: solveDyn,
solveRoute: solveRoute, routeFrom: routeFrom, transitPt: transitPt, planAround: planAround, edgeFree: edgeFree, PST: PST, unwrapTo: unwrapTo, rtWarm: rtWarm, rtReset: () => { RT = null; }, rmPlan: rmPlan, rmSmooth: rmSmooth, modelFingerprint: modelFingerprint, RM: RM, wd: wd, classGap: classGap, classFingerprint: classFingerprint, P2D: P2D,
mixSolve: mixSolve, closest: closest, closest6: closest6, closestOri: closestOri, orient: orient,
branch: branch, selfTest: selfTest, poseSelfTest: poseSelfTest,