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/早停按到达率+开环误差
This commit is contained in:
+8
-1
@@ -40,12 +40,19 @@
|
||||
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=<出错阶段>` 接着跑,前面的不重做。
|
||||
|
||||
+14
-1
@@ -25,6 +25,7 @@
|
||||
| 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′ 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` |
|
||||
@@ -127,10 +128,22 @@ bash ml/pipeline.sh prep
|
||||
bash ml/pipeline.sh train # MeanFlow 头 → ml/runs/bench_base/、ml/runs/base/
|
||||
```
|
||||
|
||||
开训前先跑老师轨迹回放自检(`ml/runs/replay_teacher.json`,约 1 分钟):老师轨迹放进 PyTorch 仿真口径,到达 ≥ 95%、碰撞 ≤ 1% 才开训,否则 rc 5 停——口径对不上时闭环指标不可信。
|
||||
|
||||
先跑 2000 步测吞吐(`ml/runs/bench_base/log.jsonl` 的 `samples_per_s`,`eta_h` 是剩余小时数),再正式训练到 60000 步或闭环到达率连续 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)
|
||||
- 训练中每 2000 步打印一行 `EVAL {...}`:val 任务闭环(部署节奏、不开安全层)`success`、`collision_rate`、`time_ok_1p3`、`start_fail`。时限 = 老师时长 × 2(至少 3 s);另有开环三项(val 样本、老师状态上):`ol_fk_mm` 候选 0 的 16 步末端误差、`ol_best_mm` 8 个候选里最好的一条、`ol_div` 候选多样性(候选间标准差 / 动作幅度,塌成单峰时接近 0)。best.pt 与早停按「到达率 − 0.001 × ol_fk_mm」
|
||||
- 损失(2026-10-18 首次正式预训练后改):FK / 平滑只加在 MF 分支 r = t 且 t ≤ 0.3 的配对去噪估计 x̂ = z − t·u 上(原来加在随机噪声的一步采样上,等于回归条件均值,多峰塌成单峰);时间嵌入最高角频率 50(原 1000);MF 分支与 JVP 走 fp32(TF32 矩阵乘);前 5000 步 p_neq = 0。fp32 会让吞吐低于原 6.2 k/s,R10 以新实测为准
|
||||
- 数据增广:40% 样本加 σ = 3° 噪声、12 步收回(原 30% / 1° / 8 步),收回途中须无碰
|
||||
|
||||
## 6′. DAgger 纠偏
|
||||
|
||||
```bash
|
||||
bash ml/pipeline.sh dagger
|
||||
```
|
||||
|
||||
用预训练 best.pt 在 train 任务上闭环跑(关安全层),每题取「首次碰撞前 0.2 s」和一个随机时刻的状态做起点(须无碰、间隙 > `MIN_GAP_MM`、离目标 > 5 mm),老师从这些起点重新规划(`gen_dataset.cjs --tasks`,与主数据同认证、同时间律),打包后与主数据一起微调。默认 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 门禁:选定动作头
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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.25, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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.25, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
+27
-6
@@ -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,自检不必吃这么多。"
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"batch": 1024, "lr": 1e-3, "warmup": 1000, "steps": 60000, "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.25, "p_neq_warm": 5000, "fk_tmax": 0.3, "fk_caps": 96,
|
||||
"ckpt_min": 10, "eval_every": 2000, "eval_tasks": 512, "log_every": 50, "patience_evals": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""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, T, R, cat, src}。
|
||||
为什么需要:纯行为克隆在闭环里会偏出老师分布(首次 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
|
||||
|
||||
|
||||
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')
|
||||
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)
|
||||
ck = torch.load(a.ckpt, map_location=a.device)
|
||||
assert ck.get('fp') in (None, kin.fp), '碰撞模型指纹与 checkpoint 不符'
|
||||
model = build(ck['cfg']).to(a.device); model.load_state_dict(ck['model']); model.eval()
|
||||
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=False, seed=a.seed * 100003 + i, return_traj=True)
|
||||
tr = per['traj'].to(a.device) # [B,T,6]
|
||||
B, Tn, _ = tr.shape
|
||||
_, T, R, _, _ = tasks_to_tensors(tb, a.device)
|
||||
with torch.no_grad():
|
||||
g = torch.cat([kin.gaps(tr[:, s:s + 64].reshape(-1, 6), 40.0)['gm'].min(-1).values.view(B, -1) for s in range(0, Tn, 64)], 1)
|
||||
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]
|
||||
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()], 'T': t['T'], 'R': t.get('R'), 'cat': t.get('cat', 'A'),
|
||||
'src': {'tid': t['tid'], 'k': k, 'col': bool(hit.numel())}})
|
||||
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()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/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。
|
||||
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=$OUT/best.pt; 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; 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
|
||||
todo=(); for sh in $D/shard_dag_*.jsonl; do [ -e "$sh" ] || continue; b=$(basename $sh .jsonl); [ -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=$OUT/best.pt
|
||||
done
|
||||
echo "$CK" > ml/runs/${TAG}_dagger.ckpt
|
||||
echo "[dagger] 完成:后续阶段使用 $CK"
|
||||
@@ -0,0 +1,49 @@
|
||||
"""老师轨迹回放自检(2026-10-18 首次正式预训练后新增;train 阶段开训前自动跑,约 1 分钟):
|
||||
把 gen 写出的老师 50 Hz 轨迹(keep=true)直接放进闭环仿真用的 PyTorch 运动学 / 碰撞 / 到达判据里检查。
|
||||
老师轨迹在 JS 侧已认证无碰、已到达;如果这里判出碰撞或未到达,说明仿真口径(kin.json、间隙、到达门槛)和数据对不上,
|
||||
那么闭环指标本身不可信,训练多少步都没用 —— 直接停下(rc 5)。
|
||||
python -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json
|
||||
判据:到达率(cat≠D)≥ 0.95,碰撞率 ≤ 0.01。"""
|
||||
import argparse, glob, json, os, sys
|
||||
import torch
|
||||
|
||||
from ml.common.kin_torch import Kin
|
||||
from ml.sim.closed_loop import goal_err
|
||||
|
||||
|
||||
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('--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)')
|
||||
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)
|
||||
res = {'n': n, 'reach': reach / n, 'collision': col / n, 'min_gap_mm_p10': sorted(worst_gap)[n // 10],
|
||||
'pass': reach / n >= 0.95 and col / n <= 0.01, 'fp': 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)
|
||||
print('老师轨迹回放:到达 %.1f%%、碰撞 %.1f%%(%d 条)' % (100 * res['reach'], 100 * res['collision'], n), flush=True)
|
||||
sys.exit(0 if res['pass'] else 5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+4
-3
@@ -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
|
||||
|
||||
+24
-13
@@ -1,6 +1,12 @@
|
||||
"""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
|
||||
|
||||
@@ -14,18 +20,23 @@ def sample_rt(B, device, dtype, p_neq=0.25, mu=-0.4, sigma=1.0):
|
||||
return r, t
|
||||
|
||||
|
||||
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3):
|
||||
B = x.shape[0]
|
||||
r, t = sample_rt(B, x.device, x.dtype, p_neq)
|
||||
e = torch.randn_like(x)
|
||||
z = (1 - t)[:, None, None] * x + t[:, None, None] * e
|
||||
v = e - x
|
||||
fn = lambda z_, r_, t_: model.decode(mem, z_, r_, t_)
|
||||
u, dudt = jvp(fn, (z, r, t), (v, torch.zeros_like(r), torch.ones_like(t)))
|
||||
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()
|
||||
def meanflow_loss(model, mem, x, p_neq=0.25, ada_p=1.0, ada_c=1e-3, return_aux=False):
|
||||
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)
|
||||
e = torch.randn_like(x)
|
||||
z = (1 - t)[:, None, None] * x + t[:, None, None] * e
|
||||
v = e - x
|
||||
fn = lambda z_, r_, t_: model.decode(mem, z_, r_, t_)
|
||||
u, dudt = jvp(fn, (z, r, t), (v, torch.zeros_like(r), torch.ones_like(t)))
|
||||
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)
|
||||
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):
|
||||
|
||||
+9
-2
@@ -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)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -208,7 +208,7 @@
|
||||
<div class="d">后台执行 <code>bash ml/pipeline.sh all</code>,从对拍一路跑到验收。已完成的步骤自动跳过;训练每 10 分钟存档、生成按条续写,断了再点一次就从断点接着跑。网页和控制台窗口都可以关,训练不受影响。</div>
|
||||
<div class="form">
|
||||
<label>运行方式<select id="fMode"><option value="resume">续跑:跳过已完成的步骤</option><option value="from">从指定阶段重训(该阶段及之后全部重做)</option><option value="fresh">全部重跑(数据也重新生成)</option></select></label>
|
||||
<label>起始阶段<select id="fFrom"><option value="train">预训练 train(保留数据、样本、门禁)</option><option value="m5">头门禁 m5</option><option value="apg">后训练 apg</option><option value="mine">挖错循环 mine</option><option value="export">导出 export</option><option value="bench">部署评测 bench</option><option value="accept">验收 accept</option><option value="gen">生成数据 gen(重跑约 7 小时)</option><option value="prep">打包样本 prep</option><option value="gate1">一致性门禁 gate1</option><option value="r4">推理延迟 r4</option><option value="kin">对拍 kin</option></select></label>
|
||||
<label>起始阶段<select id="fFrom"><option value="train">预训练 train(保留数据、样本、门禁)</option><option value="dagger">DAgger 纠偏 dagger</option><option value="m5">头门禁 m5</option><option value="apg">后训练 apg</option><option value="mine">挖错循环 mine</option><option value="export">导出 export</option><option value="bench">部署评测 bench</option><option value="accept">验收 accept</option><option value="gen">生成数据 gen(重跑约 7 小时)</option><option value="prep">打包样本 prep</option><option value="gate1">一致性门禁 gate1</option><option value="r4">推理延迟 r4</option><option value="kin">对拍 kin</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>
|
||||
@@ -378,7 +378,7 @@ 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', '打包样本', 0.5], ['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) }]));
|
||||
function applyStageMeta(m) {
|
||||
@@ -788,7 +788,7 @@ function demoApi() {
|
||||
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', 'm5', 'apg', 'mine', 'export', 'bench', 'accept'], stages: st, elapsed: el, current: cur }; };
|
||||
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',
|
||||
|
||||
+16
-1
@@ -265,6 +265,21 @@ def stage_progress(st, s):
|
||||
'eta_s': m['eta_h'] * 3600 if m.get('eta_h') else None} # 训练脚本自己按实测样本/s 算的
|
||||
if b: return {'frac': b['step'] / (bs + 60000), '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
|
||||
eta = None
|
||||
@@ -509,7 +524,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):
|
||||
|
||||
+16
-4
@@ -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 轮,含门禁回滚)
|
||||
@@ -45,10 +46,11 @@ 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 "== $*" >&2; "$@"; } # 回显走 stderr:gate1 用 tee 把 stdout 存成 consistency.json,回显混进去会让后面读它时 JSONDecodeError
|
||||
ALL_STAGES="setup kin r4 gate1 gen prep train m5 apg mine export bench accept"
|
||||
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; }
|
||||
@@ -78,13 +80,21 @@ trap 'exit 143' TERM INT HUP # 被停止 / 超时:先走 on_exit 记返
|
||||
# ---- 训练与门禁的单元(m5 自动分支要对两种头各调一次)
|
||||
train_one() { # $1 = 配置文件
|
||||
local cfg=$1 tag; tag=$(basename "$1" .json)
|
||||
# 2026-10-18:开训前先把老师轨迹放进 PyTorch 仿真口径里回放——口径对不上时闭环指标不可信,训多少步都白训(rc 5 停)
|
||||
[ -f ml/runs/replay_teacher.json ] || run $PY -m ml.eval.replay_teacher --n 512 --out ml/runs/replay_teacher.json
|
||||
if [ -f ml/runs/bench_$tag/last.pt ]; then echo "吞吐摸底 ml/runs/bench_$tag 已有,跳过"
|
||||
else run $PY -m ml.train --samples ml/data/samples --out ml/runs/bench_$tag --steps ${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 = 头标签:从该头的预训练 best.pt 起做 DAgger,最终模型路径写进 ml/runs/<tag>_dagger.ckpt
|
||||
local tag=$1
|
||||
[ -f ml/runs/$tag/best.pt ] || { echo "DAgger:ml/runs/$tag/best.pt 不存在(预训练没完成)"; return 3; }
|
||||
run bash ml/dagger/run.sh $tag ml/runs/$tag/best.pt
|
||||
}
|
||||
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
|
||||
@@ -154,6 +164,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
|
||||
@@ -161,11 +172,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 ;;
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
* V [N] 剩余时间(s)/ 5
|
||||
* SG [N,13] 子目标标签:剩余两个途经点相对当前 q 的差 / 180(不足用终点补)+ 是否需要
|
||||
* TID[N] 任务编号(tasks.jsonl 行号所在的全局 id,用于划分与闭环验证)
|
||||
* 增广:30% 样本对 q_t 加 σ=1° 噪声,标签改为 8 步内线性收回老师轨迹(噪声态须无干涉且间隙 > 1 mm);
|
||||
* 增广(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 步的增量。 */
|
||||
'use strict';
|
||||
const fs = require('fs'), path = require('path');
|
||||
@@ -28,6 +30,7 @@ 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);
|
||||
const AUG_P = +(process.env.AUG_P || 0.4), AUG_SIGMA = +(process.env.AUG_SIGMA || 3), AUG_BACK = +(process.env.AUG_BACK || 12);
|
||||
for (const line of lines) {
|
||||
const r = JSON.parse(line);
|
||||
if (!r.keep) continue;
|
||||
@@ -43,13 +46,16 @@ for (const line of lines) {
|
||||
for (let t = 0; t <= N; t += 3) {
|
||||
let q = traj[t].slice(), qPrev = (t > 0 ? traj[t - 1] : 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; }
|
||||
|
||||
+6
-4
@@ -26,27 +26,29 @@ 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', '打包样本', 0.5), ('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', 'CONSENSUS', 'MIN_GAP_MM')},
|
||||
# TARGET_SAMPLES 只决定"补量到多少":不作废 gen 的已有分片(重跑 gen 会跳过写完的 shard、按差额补量),
|
||||
# 但样本集会变,所以 prep 及之后作废。2026-10-18 实测:把它当"从 gen 作废"会让 5 小时的 5 万条分片白跑。
|
||||
'TARGET_SAMPLES': ['gen'] + _from('prep'),
|
||||
'CFG': ['r4'] + _from('train'), **{k: _from('mine') for k in ('ROUNDS', 'APG_STEPS', 'MINE_N')}}
|
||||
'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'],
|
||||
'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'],
|
||||
|
||||
+59
-14
@@ -2,7 +2,13 @@
|
||||
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 次不升即停。"""
|
||||
每 eval_every 步做一次闭环验证(ml/sim/closed_loop.py,部署节奏),按 success 存 best.pt;连续 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 个候选的多样性),
|
||||
塌缩或学偏当场可见;best / 早停按 success 为主、开环误差为次(success 同为 0 时也能分出好坏,不会 10000 步就误停)。"""
|
||||
import argparse, json, math, os, sys, time
|
||||
import torch
|
||||
|
||||
@@ -12,7 +18,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,28 +34,61 @@ 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):
|
||||
"""开环指标(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='只载入权重、优化器与步数从 0 开始(DAgger 微调用)')
|
||||
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
|
||||
if a.init: tc['p_neq_warm'] = 0 # 微调起点已训过 r≠t,不再预热
|
||||
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']]
|
||||
try: vdata = Samples(a.samples, 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)}, device {dev}', flush=True)
|
||||
model = build(cfg).to(dev)
|
||||
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'])
|
||||
step0, best, bad_evals = 0, -1.0, 0
|
||||
if a.init and not a.resume:
|
||||
ck = torch.load(a.init, map_location=dev); model.load_state_dict(ck['model'])
|
||||
print(f'微调:权重取自 {a.init}(第 {ck.get("step")} 步),优化器重置', flush=True)
|
||||
if a.resume:
|
||||
ck = torch.load(a.resume, map_location=dev); model.load_state_dict(ck['model'])
|
||||
if 'opt' in ck: opt.load_state_dict(ck['opt'])
|
||||
@@ -82,19 +121,23 @@ def main():
|
||||
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()
|
||||
p_neq = tc['p_neq'] if step >= tc.get('p_neq_warm', 5000) else 0.0
|
||||
l_mf, mf_raw, aux = meanflow_loss(model, mem, Y, p_neq=p_neq, return_aux=True)
|
||||
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, s = model.heads(mem)
|
||||
l_val = (v.float() - V).abs().mean(); l_sub = (s.float() - SG).abs().mean()
|
||||
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)
|
||||
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()
|
||||
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]
|
||||
qt = q[:, None] + torch.cumsum(Yf * stepv, 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()
|
||||
@@ -119,12 +162,14 @@ def main():
|
||||
model.eval()
|
||||
# bs=64 + 归还缓存池:WDDM 卡满时 15.9 GiB 的评估峰值会被驱动换出到内存,eval 从 2 分钟变 1 小时
|
||||
res, _ = evaluate(model, kin, val_tasks, dev, bs=64)
|
||||
if vdata is not None: res.update(open_loop(model, kin, vdata, stepv, fk_idx))
|
||||
torch.cuda.empty_cache()
|
||||
res['step'] = step + 1; res['t'] = time.time()
|
||||
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})
|
||||
sc = res['success'] - 1e-3 * min(res.get('ol_fk_mm', 0.0), 500.0) # 到达率为主,开环误差(mm)只在到达率打平时起作用
|
||||
if sc > best + 1e-5:
|
||||
best = sc; bad_evals = 0; save('best.pt', step + 1, {'eval': res})
|
||||
else:
|
||||
bad_evals += 1
|
||||
if bad_evals >= tc['patience_evals']: print('闭环到达率不再上升,停止', flush=True); break
|
||||
@@ -132,7 +177,7 @@ def main():
|
||||
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)
|
||||
print(f'done, best score(到达率 − 0.001×开环 mm){best:.4f}, {(time.time() - t0) / 3600:.2f} h', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user