Download code/README.md from cognaclee/Frank-Insert-Wipe: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/cognaclee/Frank-Insert-Wipe/resolve/main/code/README.md
- Command line
-
hf download hf://cognaclee/Frank-Insert-Wipe/code/README.md
-
curl -L -o README.md https://huggingface.co/cognaclee/Frank-Insert-Wipe/resolve/main/code/README.md
AdaSpeVLA 真机训练代码
整理日期:2026-11-10。 用途:在 insert-tube 和 wipe-whiteboard 两个任务上训练 AdaSpeVLA,并和各自的 π0.5 基线做对照。
不包含:检查点、数据集、模型权重、HF token / SSH key 等任何凭证。
目录
| 路径 | 内容 |
|---|---|
adaspe_train/adaspe/ |
plugin 本体:model.py(AdaSpePi05 模型)、bspline.py(B 样条、knot、timing 标签、位姿几何)、timing_data.py(把未来 13 帧实测 state 带进 batch) |
adaspe_train/configs.py |
训练 config(不改 OpenPI 源码,通过注册接入)。TASK 决定 config 名,如 bread_adaspe_action、bread_adaspe_timing_from20k |
adaspe_train/train.py、adaspe_train/compute_norm_stats.py |
OpenPI 脚本的包装,先注册 config 再运行 |
adaspe_train/calibrate_timing_anchor.py |
标定 knot / timing 标签用的 motion_scale 和 e_crit |
adaspe_train/run_train.sh |
全流程:预检 → Action 40k → 3 个 timing |
adaspe_train/run_timing_one.sh |
单独跑一个 timing,便于多卡并行 |
adaspe_train/strip_old_train_state.sh |
磁盘不够时用:删掉旧检查点里的优化器状态,只留参数 |
adaspe_train/wait_gpus_and_launch.sh |
等 GPU 空闲再启动 |
adaspe_train/tests/ |
数值等价测试、冒烟测试、数据检查、标签检查 |
adaspe_train/assets/ |
bread 的归一化统计(示例),以及标签检查用的参考文件 |
env_patches/ |
需要放进 venv site-packages 的补丁(见下文) |
reference_adaspevla_source/ |
原版 AdaSpeVLA PyTorch 代码,tests/test_bspline_parity.py 用它做对照 |
Recipe
- OpenPI JAX,commit
215abfb,不改上游源码。 - 骨干 PaliGemma 从官方
pi05_base加载,全量微调;不加载 π0.5 action expert。plugin 头随机初始化,seed 0。 - 数据配置:OpenPI
LeRobotLiberoDataConfig,prompt_from_task=True;state 按 π0.5 默认离散化写进 prompt;图像resize_with_pad到 224,使用默认增强。 - 归一化统计直接用基线模型仓库里的
norm_stats.json(分位数归一化),用前先重算一遍,确认一致。 action_horizon=13(1.2s @10Hz),三次 B 样条 K=8(端点固定,ridge λ=1e-4),查询间隔 0.2–0.8s。- flow 头:隐藏层 384,6 层,6 头,cond_dim 512,ODE 15 步。flow 的目标是归一化后的原始动作。
- knot 密度目标和 timing 标签都按原版 AdaSpeVLA 计算:
- 运动来自窗口内的实测位姿(相对窗口第一帧,位置差 + 旋转向量),按
motion_scale归一化; - 夹爪用实测开度,以 0.5 为界判断切换;
- timing 标签是第一次运动变化量超过
e_crit或夹爪切换的时间,裁剪到 [0.2, 0.8]s。
- 运动来自窗口内的实测位姿(相对窗口第一帧,位置差 + 旋转向量),按
- 损失:Action 阶段 flow MSE + 0.5 × knot KL;timing 阶段 log-MSE。
- global batch 32,AdamW(b1 0.9,b2 0.95,wd 1e-10,梯度裁剪 1.0),cosine(warmup 1k,peak 2.5e-5,decay 30k,end 2.5e-6),EMA 0.99,评测用 EMA 参数。
- Action 训练 40k 步,保留 10k/20k/30k/40k。然后基于 20k/30k/40k 各训练一个 timing 头:10k 步,只训练
freq_head,其余全部冻结。 - 每 1000 步存一次检查点,wandb 关闭,FSDP 设备数等于 GPU 数。
环境搭建
- 克隆 OpenPI 并切到
215abfb,uv sync --frozen --python 3.11。缺内核头文件时加--no-install-package evdev。 - 把
env_patches/里的datasets_list_shim.py和datasets_list_shim.pth放进external/openpi/.venv/lib/python3.11/site-packages/:让 datasets 3.6 认识List特征(训练、norm stats 和检查脚本都要用)。不要把 datasets 升级到 4.x。 仓库里的脚本自己会找到adaspe_train/,不需要别的路径补丁。只有在仓库外的代码里import configs(比如自己写的 policy server)时,才需要在仓库根目录运行一次:echo "$PWD/adaspe_train" > external/openpi/.venv/lib/python3.11/site-packages/adaspe_train_path.pth - PaliGemma tokenizer 要预先下载到 HF cache。脚本默认离线运行(
HF_HUB_OFFLINE=1)。
路径
代码里没有写死的路径:所有脚本都从自己所在的位置推出仓库根目录(Python 用 __file__,shell 用 BASH_SOURCE),仓库放在哪里都可以,不用改路径。
根目录(即本仓库)下应有的布局,每一项都可以是软链接(比如指到更大的磁盘):
external/openpi:OpenPI 和 venvadaspe_train:这份代码reference_adaspevla_source:原版 AdaSpeVLA 代码(数值等价测试用;也可以用环境变量ADASPEVLA_REF指定别的位置)data/lerobot/<repo_id>:LeRobot 数据集checkpoints/:训练输出test/franka/<实验名>/:日志.cache/:HF、JAX 等缓存
pi05_base 参数默认在 .cache/openpi/openpi-assets/checkpoints/pi05_base/params(configs.py 的 PI05_BASE)。
换成新任务要改的地方
两个任务的格式和 bread 一致:两路 128×128 图像,19 维 state,7 维动作,10Hz,各只有一条 task 文本。
| 任务 | 数据集(revision) | 基线模型(revision) | 基线 norm_stats 在仓库里的路径 |
|---|---|---|---|
| insert-tube | dddddsds31341/insert-tube-lerobot(70bcd1f86ed35fc93a0422a564472355242a51ab),102 条 / 16,403 帧 |
dddddsds31341/Franka-Insert-Tube-pi05-step-20000(e97d90f5bed133b24e8de4189eb25254641e69cf) |
assets/bofang/insert-tube-lerobot/norm_stats.json |
| wipe-whiteboard | dddddsds31341/wipe-whiteboard-lerobot(2c8cfa8ddef1f83cb09ca7881a0944cd637cd6d0),112 条 / 29,795 帧 |
dddddsds31341/Franka-Wipe-Whiteboard-pi05-step-20000(628df08d8cea3965d8a5ece6f57a8f60dd242c2e) |
assets/bofang/wipe-whiteboard-lerobot/norm_stats.json |
configs.py
REPO_ID改成新数据集。- 把基线的
norm_stats.json放到adaspe_train/assets/baseline/<REPO_ID>/norm_stats.json。注意基线仓库里的目录名是bofang/...,要按我们的REPO_ID放。 TASK改成新任务名(如insert_tube),config 名会跟着变。检查点放在checkpoints/adaspe/<TASK>_adaspe_*,不会和别的任务混在一起。
重新标定,不能沿用 bread 的数(只读数据,CPU 上运行):
- 改
calibrate_timing_anchor.py里的 parquet 路径,然后运行。 - 把输出的
motion_scale和e_crit填进configs.py的TIMING_MOTION_SCALE和TIMING_E_CRIT。 - 记录标签分布,即 0.2s、0.8s 各占多少。
- 改
核对 state 含义。代码假设:
state[0]是夹爪开度(开 1,合 0);state[4:7]是位置;state[7:10]是 Euler xyz(scipy "xyz")。
先用
tests/check_data.py和几条 episode 确认新数据也是这样。wipe-whiteboard 可能全程拿着板擦,夹爪不开合;这不影响训练,只是 timing 事件全部来自运动。run 脚本:运行时设置环境变量
TASK,要和configs.py里的TASK一致。日志默认写到test/franka/<TASK>_adaspe/。tests/check_timing_labels.py依赖assets/timing_labels/<TASK>_H<H>.npy。先用新任务的数据运行calibrate_timing_anchor.py重新生成这个文件,再跑检查。
运行顺序
- 重算 norm stats,和基线逐位比较:
python adaspe_train/compute_norm_stats.py --config-name pi05_<TASK>_franka_ref - 标定(见上文)。
- CPU 测试:
tests/test_bspline_parity.py:和原版的误差 ≤1e-12;tests/smoke_model.py;tests/check_data.py;tests/check_timing_labels.py:0 处不一致。
- 预检和正式训练:
CUDA_VISIBLE_DEVICES=<卡> TASK=<任务名> bash adaspe_train/run_train.sh。可以先设PREFLIGHT_ONLY=1,只跑预检:训练 30 步,再从断点续训,确认 loss 有限、续训正常。 - timing 想并行的话,Action 跑完后每个 timing 用一张卡:
CUDA_VISIBLE_DEVICES=4 TASK=<任务名> bash adaspe_train/run_timing_one.sh 20,30、40 同理。
注意:Action 开始后不要改卡数。检查点只能在保存时的同样卡数上续训;global batch 32 要能被卡数整除,所以用 1、2 或 4 张卡。
交付时请给我
- 最终的 Action(20k/30k/40k)和 timing EMA 参数,以及所有文件的 sha256;
- loss 曲线;
- 实际用的 config 导出、标定出的
motion_scale/e_crit、标签分布、git commit; - 和本文档不一致的地方,逐条列出。
Jean Zay 上的配置(2026-11-10)
只在 Jean Zay 上用。别的机器不需要 adaspe_train/slurm/,直接用 run_train.sh / run_timing_one.sh。
- WORK 的 inode 配额紧张,所以
external/(OpenPI215abfb+ uv venv)、data/、checkpoints/、.cache/都是指向$SCRATCH/adaspevla_train/的软链接。SCRATCH 上 30 天不访问的文件会被清掉,交付用的参数要拷走。 - 任务由环境变量
TASK选择(bread/insert_tube/wipe_whiteboard),各任务的REPO_ID、motion_scale、e_crit写在adaspe_train/tasks.py;configs.py、标定脚本和测试都从这里读,不用再手改configs.py。还没标定的任务只注册pi05_<TASK>_franka_ref。 - 标定结果:
assets/calibration/<TASK>_H<H>.json;标签文件:assets/timing_labels/<TASK>_H<H>.npy(原 bread 的timing_labels_measured.npy保留不动,是 H=13 旧 recipe 的)。标定命令:TASK=<任务> python adaspe_train/calibrate_timing_anchor.py,结果要抄进tasks.py(标定和 check 用的都是adaspe/labels.py里和训练相同的标签函数)。 pi05_base:.cache/openpi/openpi-assets/checkpoints/pi05_base/params(官方 GCS,与 bucket 列表逐文件核对过大小)。tokenizer 在.cache/openpi/big_vision/paligemma_tokenizer.model(OpenPI 从OPENPI_DATA_HOME读,不是 HF cache)。- 计算节点不联网,所有数据/权重都在本地;脚本里
unset PYTHONPATH LD_LIBRARY_PATH、PYTHONNOUSERSITE=1,避免模块和~/.local混进 venv。 - SLURM(
adaspe_train/slurm/):都要在仓库根目录提交(SLURM 运行的是脚本的副本,根目录和日志路径都取提交目录),账号用-A <账号>给,脚本里不写账号。prep_checks.slurm:CPU 上跑 norm stats 重算+比较、parity、smoke、check_data、check_timing_labels(两个任务)。preflight.slurm:H100 dev,4 卡单独跑 preflight,作业名必须是adaspe-pf-<TASK>,写preflight_passed(可选:dev QoS 全集群共用 128 卡,常排不上;train_task.slurm第一节自己会先跑 preflight,失败就停链)。train_task.slurm:H100 t3 20 h 一节的链式作业(singleton,自动排下一节,最后一节或真失败时取消)。4 卡 FSDP 4 跑 Action 40k,然后 3 个 timing 各占一张卡并行。sbatch -A <账号> --job-name=adaspe-insert_tube --export=ALL,TASK=insert_tube adaspe_train/slurm/train_task.slurm(后面各节自动用同一个账号)- 日志:
test/franka/<作业名>-<id>.out和test/franka/<TASK>_adaspe/。
Recipe 修订(2026-11-10,和上面 "Recipe" 不同的地方)
开关都在 adaspe_train/tasks.py,方括号里是原值;改回原值并重新标定即可复现原 recipe(adaspe/labels.py 在原设置下和旧标签逐个一致,0 处不一致)。
| 项 | 现在 | 原来 | 原因(两个任务的数据) |
|---|---|---|---|
ACTION_HORIZON |
16(1.5 s) | 13(1.2 s) | 13 没有必须的理由;16 拟合误差配 K=11 反而更小 |
NUM_CONTROL_POINTS |
11 | 8 | 归一化动作的位姿 RMSE 0.024/0.021(原 0.029/0.025) |
| query interval | 0.2–1.5 s(上限 = horizon) | 0.2–0.8 s | 原上限 72–76% 的标签被顶住,并且强制截掉 chunk 后 1/3 |
| 夹爪事件(timing 标签、knot 密度) | 夹爪命令的开/合状态 | 实测开度过 0.5 | 实测开度比命令晚中位 19 帧(~1.9 s,前 ~1.4 s 完全不动,机械臂静止),超过 chunk 长度 |
| knot 密度 | 1 + 10·tanh(mv / e_crit) |
1 + 10·tanh(mv) |
原公式 tanh 在 68–73% 的帧间隔饱和,62–63% 的窗口 knot 目标近似均匀;现在 3–4% |
推理 sample_spline |
返回 execute_steps 和 dense_actions:前缀至少执行到预测的夹爪切换 |
只有 query interval | 静止时下一帧观测不变,切换若落在前缀之后会被一直推后 |
标定(H=16):insert-tube e_crit 11.98,标签 0.2 s 12.0% / 1.5 s 54.6%;wipe-whiteboard e_crit 8.96,0.2 s 10.2% / 1.5 s 60.0%。
在开爪命令处,样条在命令那一帧的值中位数 +0.93 / +0.98(均匀 knot 时 +0.66)。
另外修了一个原代码里的 bug:configs.py 里 import openpi.models.model as _model 被后面的 def _model(stage) 覆盖,两个 weight loader 调 _model.restore_params 会在训练开始时报 AttributeError。现在模块别名是 _openpi_model,config 函数叫 model_config。
部署(不在本仓库):客户端调用 sample_spline,执行 dense_actions[:, :execute_steps](每步一个 0.1 s 的增量命令),然后重新观测、推理。
变体:pi0.5 action expert 生成控制点(2026-11-13,VARIANT=expert)
动机:真机上 π0.5 基线的精度明显好于 AdaSpeVLA。两者的最大差别在生成动作的部分。AdaSpeVLA 用 20.4 M 的 flow 头,从零训练,只看 backbone 最后一层投影后的 token;π0.5 的 Gemma-300M expert 有 311 M transformer 参数,另有 117 M adaRMS 时间调制层,在 pi05_base 里预训练过,每一层都 attend 到 backbone 的 KV。原插件加载时把它丢掉了。样条本身不是瓶颈:用本 recipe 的样条(H=16、K=11)拟合 demo,9 步前缀累计误差的 p95 ≤ 一个典型步长的 5%(慢速阶段也一样)。
做法(adaspe/model.py 的 generator="pi05_expert";默认的 "flow_head" 代码和参数初始化都不变,旧检查点照常用):
- 网络是完整的 π0.5(PaliGemma + action expert),
pi05_base的全部权重都加载。PluginWeightLoader会检查 expert 和action_in_proj/action_out_proj/time_mlp_*是否齐全、形状是否一致,缺一个就报错。 - expert 的输入是 K=11 个控制点 token,π0.5 原本是 action token。token =
action_in_proj(带噪控制点,按 OpenPI 补零到 32 维)+cp_knot_proj(该控制点 Greville 位置的 sin-cos 编码、knot 向量、log horizon)。cp_knot_proj初始化为 0,所以第 0 步 expert 收到的输入和 π0.5 完全一样。 - flow 按 π0.5 的约定:t=1 是噪声,时间采样 Beta(1.5, 1),ODE 10 步(timing 阶段取特征也用 10 步)。adaRMS 时间调制、attention mask、位置编码和 KV cache 的用法都照搬
Pi0。 - knot 要从 prefix 预测,所以先单独跑一遍 prefix 拿 KV cache,再跑 expert(和 π0.5 推理时一样)。梯度穿过 KV cache,所以训练等价于 π0.5 的 prefix+suffix 联合前向(
tests/test_expert_parity.py有对比)。 - 不变的部分:knot 头、timing 头、标签、样条拟合、损失(flow MSE + 0.5 × knot KL)、数据、norm stats、40k 步、保留 10k/20k/30k/40k、timing 阶段。
运行:和原变体一样,--export 里加 VARIANT=expert,作业名前缀用 adaspe-expert-。原变体是 adaspe-,两个变体的作业链互不干扰:
sbatch -A <账号> --qos=qos_gpu_h100-dev --time=02:00:00 --job-name=adaspe-expert-dev-insert_tube \
--export=ALL,TASK=insert_tube,VARIANT=expert adaspe_train/slurm/train_task.slurm
config:<TASK>_adaspe_expert_action、<TASK>_adaspe_expert_timing_from{20,30,40}k。日志在 test/franka/<TASK>_adaspe_expert/,检查点在 checkpoints/adaspe/<TASK>_adaspe_expert_*。名字统一由 adaspe_train/names.sh 生成。
测试(CPU):tests/test_expert_parity.py、tests/smoke_model.py(两种 generator 都测)、tests/check_config_names.py,都在 slurm/prep_checks.slurm 里。