state6 那一版实机 0/2。今天把它拆开量了一遍,结论是两件互相独立的事:一个推理端的 bug(已修),和一个训练问题(必须重训)。这一页是重训的完整操作手册 —— 每一步该跑什么、看到什么算通过、什么情况下要停下来别继续烧租金。
state6 的截断技巧没有生效。
它的设计意图是「不传 --policy.input_features=null,让 smolvla_base 自带的 state[6] 盖住数据集的 17 维,截断只留前 6 维(左臂,右臂任务里恒 0)」,以此断开 state 捷径。
实测:模型在推理时确实在读右臂 state(固定图像、只换 state,输出 pan 摆动 11.08°),而且把 state 全喂 0 并不比现在好。捷径从来没断过。数据集 xlerobot-right-pick-cup-20260826-0042 的 16487 帧里左臂 6 维全是 0,于是 checkpoint 的归一化统计量是 mean=0, std=0。而 lerobot 的归一化没有对 std==0 做任何保护:
# lerobot/processor/normalize_processor.py:359
denom = std + self.eps # eps = 1e-8
return (tensor - mean) / denom
训练时 (0-0)/1e-8 = 0,干净。真机上左臂是连着的,喂进去的是真实角度 —— 那 6 维被放大 1e8 倍。实测同一张图、同一右臂状态下:
| 左臂喂什么 | 右臂输出(pan/lift/elbow/wflex/wroll/grip) | 与基准差 |
|---|---|---|
| 0(训练时的样子) | -27.26 44.88 -21.23 -31.96 -0.19 16.93 | — |
| ±0.1° | -2.68 -2.45 42.53 -57.49 0.40 15.97 | maxΔ=63.76 |
| ±5° | 0.21 2.68 41.63 -61.43 1.12 16.34 | maxΔ=62.86 |
| 真机典型(数十度) | -0.45 1.80 35.61 -55.24 1.05 13.15 | maxΔ=56.84 |
左臂偏 0.1 度,右臂输出变 64 度;而 0.1° / 5° / 几十度结果几乎一样 —— token 已经饱和,模型退化成输出一个跟本体姿态无关的固定姿势。
run_policy_trials.py:1015 起,install_state_guard()):从 checkpoint 自己的统计量推,任何 std==0 的维度在喂给策略前钉回训练均值。不硬编码「左臂」,换模型自动重算;--dry / preview / 实跑同步循环 / RTC 四处全覆盖;只改喂给策略的那一份,安全层继续看真实 17 维(它要算 FK 和钳位)。对没有恒定维的老 checkpoint 自动 no-op。修完用同一个 checkpoint、同样参数重跑 2 次:
| 修复前 T1 | 修复前 T2 | 修复后 T1 | 修复后 T2 | |
|---|---|---|---|---|
| 帧数 / 60 s | 94 | 241 | 95 | 253 |
| 夹爪开合次数 | 10 | 18 | 5 | 10 |
| 每帧开合率 | 0.106 | 0.075 | 0.053 | 0.040 |
| CLIPPED | 0 | 10 (4.1%) | 0 | 3 (1.2%) |
| 下探被安全层削 | 0% | 9% | 0% | 2% |
| 闭爪帧 | 9 / 94(约 6 s) | — | 30 / 95(约 19 s) | |
| 判定 | 失败 | 失败 | 失败 | 失败 |
抖动两次都对半砍,闭爪时机从「一伸手就夹」推到「先够一段再夹」—— 顺序对了。但仍 0/2。
控制变量(所有集共用同一个 state,只换图像),沿够取过程测三个点:
| phase | 杯位跨度 | 输出 pan 跨度 | 敏感度 | pan r | pan 斜率 |
|---|---|---|---|---|---|
| 0.00(起始位) | 45.5° | 4.4° | 9.7% | -0.310 | -0.041 |
| 0.35 | 45.5° | 2.5° | 5.6% | +0.449 | +0.029 |
| 0.70(快够到) | 45.5° | 2.8° | 6.2% | +0.614 | +0.042 |
方向感随靠近变好(r 从 -0.31 升到 +0.61),但幅度始终是需要量的约 4%(示范里斜率应接近 +1)。
再换四种 state 喂法,看能不能救回来:
| state 喂法 | pan 跨度 | 敏感度 | r | 斜率 |
|---|---|---|---|---|
| 全零(17 维都是 0) | 8.37° | 18.4% | +0.188 | +0.043 |
| 只钉左臂(当前修复) | 15.00° | 33.0% | +0.255 | +0.086 |
| 固定真实 state | 5.50° | 12.1% | +0.467 | +0.052 |
| 每集真实 state(修复前) | 11.98° | 26.3% | -0.010 | -0.003 |
还量了 state 和图像谁说了算(固定其一、只变另一个):
A 只换图像 (state 固定) 输出 pan 跨度 6.19° 与杯位 r = -0.405
B 只换 state (图像固定) 输出 pan 跨度 11.08° 与杯位 r = +0.090
state 摆动 / 图像摆动 = 1.8x
state 对输出的影响比图像大 1.8 倍,而且跟杯位不相关(r=0.09,是噪声)。这就是 causal confusion 的标准长相:模型在复读示范的平均轨迹,起始姿态抖一点复读轨迹就跟着抖,而杯子在哪它基本没看。
|r| ≤ 0.34。input_features 那个技巧state6 的做法 | state dropout | |
|---|---|---|
| 手段 | 忘记传 --policy.input_features=null,靠截断副作用断捷径 | 在策略 prepare_state 里按概率把 state token 置零 |
| 看得出来吗 | 看不出来 —— config 里只留下 state:[6],分不清是故意还是漏了 | 载入时打印启用状态和分段进度 |
| 推理端一致吗 | 不一致 —— 推理照喂 17 维,没人拦 | 只在 self.training 下生效,推理原样 |
| 实测效果 | 没生效(模型仍在读右臂 state) | 待验 |
示范轨迹光滑,「这一帧的关节角」几乎能直接推出「下一帧该转到哪」。于是模型走捷径:读上一帧关节角往同方向再挪一点,比看图找杯子算该往哪转省事得多。梯度下降当然选前者 —— 结果是训练 loss 很低、真机上抓不到东西。数据越少越严重(我们只有 40 集)。
做法:训练时以概率 p 把 state 那一路输入整条置零(归一化之后置零 = 喂训练均值 = 中性值),逐样本,同一批里有的看得到有的看不到:
| 阶段 | 区间 | p | 作用 |
|---|---|---|---|
| A 纯视觉 | 0 ~ 30% | 1.0 | 完全读不到关节,强迫建立视觉通路 |
| B 退火 | 30% ~ 70% | 1.0 → 0.2 | 慢慢放开,学会把关节信息用起来 |
| C 永久下限 | 70% ~ 100% | 0.2 | 捷径永远走不通 |
state → action 还是损失下降最快的方向,捷径会很快被重新学回来,第一阶段建立的视觉依赖被冲掉,最后还是一个不看图的模型。留 20% 之后任何时候都有五分之一的样本没有 state,模型不可能只靠 state 把 loss 压下去。这就是「混合 + 退火」,不是「两段式冻结」。SD_DEBUG=1 看分段 loss 再决定,别盲目加长阶段 A。--policy.input_features=null —— 和 train-right-b16.html 那一页正好相反。那次靠它缺席来断捷径(已证明没生效);这次断捷径由 state dropout 显式负责,state 必须是完整的 17 维,否则两个机制会互相干扰,跑出来没法归因。脚本里已经写好了,别手改。hf auth login # 粘贴 write token
cat ~/.cache/huggingface/lerobot/xlerobot-team/xlerobot-right-pick-cup-20260826-0042/meta/upload.json
hf upload-large-folder --repo-type=dataset xlerobot-team/xlerobot-right-pick-cup-20260826-0042 <本地目录>。数据集不会在录制后自动补传。| 项 | 选择 | 为什么 |
|---|---|---|
| 平台 | RunPod | 之前几次都在这上面跑,流程趟通 |
| 模板 | PyTorch 2.8 / CUDA 12.8 | torchcodec 要求 cu128 |
| 显卡 | RTX 4090 | 够用。batch 16 的显存占用从没实测过 —— 用第 4 步的 40 步基准去量 |
| 存储 | ≥ 50 GB,挂 /workspace | checkpoint 每 2000 步存一次 + 数据集缓存 |
pip install -U "lerobot[dataset,training,smolvla]" huggingface_hub
hf auth login && hf auth whoami # 确认能读数据集 + 能写模型仓库
tmux new -s train # ★ 断线不杀训练 ★
05-training/state_dropout.py # 机制 + 退火 + 自检
05-training/train_state_dropout.py # 装补丁再交棒给 lerobot-train
05-training/train_smolvla_state_dropout.sh # 配方,对齐 state6
python state_dropout.py → 应看到退火各分段点 ok + 逐样本置零比例 p=0.5 -> 实测 0.4991 ok + 自检通过python -c "import torch,torchcodec;print(torch.__version__, torchcodec.__version__)"
ffmpeg -decoders | grep -Ei "cuvid|nvdec" # 要看到 av1_cuvid
BENCH=1 SD_DEBUG=1 ./train_smolvla_state_dropout.sh
torchcodec 装不上会静默退回 pyav,训练照跑但慢几倍 —— 白烧租金。== state dropout【已启用】 三行阶段说明,以及每 200 次前向一行 [state-dropout] step .../... p=1.000 置零 16/16。p 不动就是补丁没挂上,停下来查。顺便 nvidia-smi 记下显存峰值。./train_smolvla_state_dropout.sh
# 想调档: SD_P_END=0.3 SD_VISION_FRAC=0.4 ./train_smolvla_state_dropout.sh
| 参数 | 默认 | 含义 |
|---|---|---|
SD_VISION_FRAC | 0.3 | 阶段 A(纯视觉)占总步数比例 |
SD_ANNEAL_FRAC | 0.4 | 阶段 B(退火)占比 |
SD_P_END | 0.2 | 永久下限 |
SD_ENABLE | 1 | 0 = 对照组,其余完全一样 |
state6:B_eff=16 / lr=1e-4 / 20000 更新 / warmup 1000 / 增强开。唯一变量就是 state dropout。-p0.2- / -nodropout-),两组不会互相覆盖。4090 上一小时出头。state6 的训练侧一直是干净的,它本身就是合格 baseline。# Jetson 上
python 03-software/scripts/vision_sweep_phase.py \
05-training/<新模型>/checkpoints/020000/pretrained_model \
~/.cache/huggingface/lerobot/xlerobot-team/xlerobot-right-pick-cup-20260826-0042 \
"Pick up the cup with the right arm." right 12
+0.086 比。--exec-horizon(run_policy_trials.py:1015):覆盖 n_action_steps,控制执行几步就重新看画面。这个 checkpoint 是 50 步 @30 Hz = 1.67 秒开环,今天两次实机的重规划间隔都是这个数。
chunk_k,管一个控制周期消费几步(让计划按真实时间播放),不改变重规划频率。结果文件记 exec_horizon + exec_horizon_default,不同视野单独分组统计(trials_io.py:330)—— 那是两个不同的闭环系统,成功率不能混在一起算。老记录不会被拆组。
05-training/STATE-CHANNEL-BUG-2026-09-08.md · 实机日志 trials-20260908-063932.json / trials-20260908-07*.json ·
探针 vision_sweep_phase.py / state_vs_vision.py / state_regime.py。