——以及两小时之内怎么训一个更好的。
一句话:那个 6000 步的模型不是训坏了,是训到 30% 就停了。这件事跟用不用 A100 基本没关系,跟 batch 也没有想象中那么大关系。真正被改掉的,是跑了多少步。
更新(2026-08-24 深夜,查完 LeRobot 源码和 Hub 仓库之后):两件事变了。
① 不用从头重训。那个 6000 步 checkpoint 的 training_state/(优化器状态 412 MB、调度器状态、随机数状态)还完整地留在 Hub 仓库里——你本地只下载了 pretrained_model/,所以看起来像没有。它可以直接 --resume 接着跑,那 6000 步的钱不用再付一次。
② 学习率那个数字从「复算」变成「实测」。仓库里 scheduler_state.json 存的是
"_last_lr": [7.990453104925807e-05],跟我复算的 7.99e-05 一模一样。这条现在是硬证据。
下面第七节的命令已经按这个改过了。完整的严谨版(含最小对照实验设计、梯度累积的坑、引用行号)写在仓库里的 05-training/SMOLVLA-TRAINING-RECIPE.md。
之前的判断是「batch 从 2 提到 64 是主要问题」。数字都对,但结论要改。
LeRobot 官方文档里给的 SmolVLA 微调命令(docs/source/smolvla.mdx 第 57–66 行)就是:
--batch_size=64 \
--steps=20000
同一份文档第 49 行还写着:「20000 步在单张 A100 上大约要 4 小时」。
更巧的是,文档第 35–40 行描述他们论文用的参考数据集:「50 集,5 个不同的方块位置,每个位置 10 集」——跟你录的结构一模一样。他们还特意提了一句:试过 25 集,不够,效果差。
所以 batch=64 不是错误,那就是官方配方。错的是这个配方要配 20000 步,而你只跑了 6000。
先用大白话说清楚「学习率」是什么。训练就像拧一个旋钮找最佳位置。学习率是每次拧多少。标准做法是一开始拧得大(快速找到大概方向),越往后拧得越小(精修)。最后那段小幅度的精修,正是把「大概对」变成「真的对」的部分。
| 老模型(8-20 那个) | 今天这个(A100) | |
|---|---|---|
| batch | 2 | 64 |
| 命令里写的步数 | 20000 | 20000 |
| 实际跑到 | 16000 (18000 步时磁盘满崩了) | 6000 |
| 走完了降温曲线的 | 80% | 30% |
| 停下时的学习率 | 1.18e-05 | 7.99e-05 |
两个都是从 1.0e-04 出发的。老模型停下时已经降到了出发时的八分之一——它正在做最后的精修。今天这个停下时还有 8.0e-05,几乎就是出发时的大小,还在大刀阔斧地改参数。
它不是「训坏了」,是没降完温就出锅了。
一个很容易踩的坑:lerobot 的降温曲线会按你写的 --steps 自动伸缩(src/lerobot/optim/schedulers.py 第 149–153 行)。你写 --steps=20000,它就把整条降温曲线摊到 20000 步上。
所以中途停在 6000 步,不等于「提前练完了 6000 步的量」,而等于「一条为 20000 步设计的曲线,只走了前面 30%」。这两件事差别很大。
一个模型学东西是有顺序的。它先学会所有示范的共同点——动作的大形状、伸手的大概高度、什么时候闭爪。最后才学会那些必须看图才知道的细节:这个杯子在左边,所以腰要多转 20 度。
半路停下,它就停在一个「谁都不得罪」的中间姿势上。这不是抽象的说法,是能量出来的:
今天两轮各 10 次实机试验(都是这个 6000 步模型,都是 0/10),我从 trace 里取出每一次闭爪那一瞬间的姿态,一共 27 次:
| 你的示范 | 模型 | |
|---|---|---|
| 抓取时腰部转角的范围 | −37° 到 +27° 跨度 64° | −25.8° 到 +1.7° |
| 这个角度的散布(标准差) | 19.8 | 6.4 压到三成 |
| 抓取时手腕转角 | 平均 −13.2° | −3.7° ≈ 全程平均值 −5.2° |
你示范里有 19 集(占 38%)抓取时腰要转到 +6° 以上,模型这 27 次一次都没到过。
这三行数字,正好对上你亲眼看到的三件事:
另外「正前方的杯子它能跨在两侧」也说得通:正前方那组需要 −7.4°,它给 −9.5°,碰巧对准了。五个位置里只有这一个能对上。
本来担心「云端和 Jetson 用的 LeRobot 版本不一样」会把结论搞乱。查完了:两次训练的
183 个配置字段里只有 6 项不同,而两棵源码树里 schedulers.py、
factory.py、lerobot_train.py、configs/train.py、
SmolVLA 的配置文件 全部 0 行差异,模型代码只差一行 import 写法(Python 3.10 的兼容写法)。
不是版本问题。
老模型那次用了 eval_split=0.2,新模型用的是 0.0。我原以为这只是「有没有留验证集」的区别,
其实它决定了训练集里有哪些 episode。
LeRobot 的切法是「每个 task 的最后 ceil(集数×比例) 集留出」(datasets/factory.py 第 156 行)。
这个数据集只有一个 task,50 集、留 20% → 训练集是 ep0–39,ep40–49 被整个留出。
训练日志原话也印着:Train/eval split: 40 train, 10 eval。
而 ep40–49 正好是我量出来的最右边那一组杯位(杯子在画面里 0.84,抓取时腰要转到 +11.7°)。
也就是说:那个表现更好的老模型,从来没有训练过第五个杯位。
这有两层意思。第一,你拿老模型去试最右边的杯子时,它是真的没见过,抓不到很正常。 第二,新模型数据更多、位置更全却更差 —— 这个方向说明「数据不够」解释不了新模型为什么差, 反而让「没训完」这个解释更站得住。
你担心是不是自己动作不够统一。数据不支持这个担心,反而相反。
我把杯子在头部相机画面里的位置用颜色检出来(50 集全部检出),和抓取时的关节角度对照:
| 对照 | 相关系数 | 意思 |
|---|---|---|
| 杯子在画面里左右的位置 ↔ 抓取时腰部转角 | 0.953 | 几乎是一条直线 |
| 杯子在画面里远近的位置 ↔ 抓取时肩部角度 | −0.846 | 同样很干净 |
还有一条:闭爪的那 9 帧里,肩、肘、腰的运动幅度中位数分别是 0.42° / 0.44° / 0.43°,90% 的情况下不超过 1°。也就是说你示范时是停稳、夹住、再抬,纪律非常好。
而模型每个控制周期肩部的变化,中位数 3.98°,但有 10% 超过 23.8°、1% 超过 64.2°。这就是「一边往上跑一边夹」的来源。
先说三条规则,比表本身重要:
--steps 必须设成你真的会跑完的数字它同时决定两件事:更新多少次参数,以及降温曲线摊多长。设 20000 就得跑满 20000。中途停 = 白花钱,今天就是这么丢的。
它不改变降温曲线,也不改变更新次数。所以「用不用 A100」「batch 是 16 还是 64」,对最终效果的影响,远小于「跑没跑完」。
1e-4,不要动官方就是拿 1e-4 配 batch 64 调好的。你把 batch 往小调,继续用 1e-4 是安全的(梯度噪声大一点,对小数据集反而略有好处)。不要往上调——那是往大 batch 走时才需要的补偿,你这次是往小走。
| batch | 学习率 | --steps | 更新次数 | 看几遍数据 | 预计 A100 时长 | 评价 |
|---|---|---|---|---|---|---|
| 64 resume | 不动 | 20000 从 6000 接着跑 | 16000 | 53 遍 | +2.0 h 估 | 最推荐。跑到 16000 步时,更新次数/曲线完成度/学习率累积三项与老模型完全相同,唯一变量只剩 batch —— 这本身就是一次干净的对照实验 |
| 64 resume | 不动 | 20000 跑满 | 20000 | 66 遍 | +2.8 h 估 | 再多 0.8 小时就能把整条曲线走完,等于补齐官方配方 |
| 2 | 1e-4 | 20000 | 20000 | 2.1 遍 | ~0.6–1 h 估 | 老模型的路子。A100 上极度浪费,读数据会先成为瓶颈,实际未必比 16 快 |
| 16 | 1e-4 | 20000 | 20000 | 16 遍 | ~1.3 h 估 | 推荐。两小时内能跑完整条降温曲线,更新次数比老模型还多 25% |
| 32 | 1e-4 | 20000 | 20000 | 33 遍 | ~2.2 h 估 | 最接近官方,可能稍微超两小时。如果 16 的时候发现读数据是瓶颈,就换这个 |
| 64 | 1e-4 | 20000 | 20000 | 66 遍 | ~4 h 官方实测 | 官方原版配方。效果大概率最好,但时间不够 |
| 64 | 1e-4 | 10000 | 10000 | 33 遍 | ~2 h 估 | 不推荐。时间是够了,但更新次数只有老模型的 62%,等于换个方式重蹈今天的覆辙 |
时长是怎么来的,得说清楚:官方只给了一个实测点——batch 64、20000 步、约 4 小时,折合每步 0.72 秒。其余几行是按「计算量大致跟 batch 成正比、外加一点固定开销」推的,是估算,不是实测。
校准方法很简单:开跑三分钟后,日志里会打 step_s:0.xxx。拿这个数 × 步数,就是真实总时长。如果超预算,前 200 步不值钱,当场停掉换个 batch 重开。
--save_freq=4000,把中间存档都留下这样你会拿到 8000 / 12000 / 16000 / 20000 四个存档,回来一个一个上机器测。不要默认最后一个最好——GOAL-NEXT.md 第二节第 2 条早就写了这一条。一个存档 869 MB,四个约 3.5 GB,租机器时留够盘。(上一轮 Jetson 就是跑到 18000 步磁盘满了崩的,别再来一次。)
这条不是常识,是我量出来的,所以详细说一下。
杯子在头部相机画面里的横向位置,五个位置分别落在画面宽度的 0.56 / 0.62 / 0.70 / 0.82 / 0.84。相邻两个之间只差 0.06。
而 lerobot 默认的 affine 增强,会把图像随机平移 ±0.05——差不多正好是一个位置的间距。
平移增强教给模型的是「画面挪了,动作不用变」。可这个任务要它学的偏偏是「画面挪了,动作就得跟着变」。两者方向正好相反。开了反而把唯一的空间线索抹掉。
--dataset.image_transforms.enable=true \
--dataset.image_transforms.tfs.affine.weight=0
亮度、对比度、饱和度、色调、锐度这几个可以开,房间光线本来就不太稳。如果你想严格只改一个变量,这一条也可以先不开——它不是关键项。
--dataset.eval_split=0.0,50 集全部拿去训之前做过一次「留出一个位置」的离线评测:没见过的位置 loss 0.2300,训练过的位置 0.2348,比值 0.98——看起来完美。然后实机 0/10。
这个指标在这个任务上没有预测力。为了它切掉 10 集训练数据不划算,尤其官方文档还专门说过 25 集就不够用了。真正的验收是上机器跑。
lerobot-train \
--resume=true \
--config_path=Suyang99/xlerobot-smolvla-cup-grasp-right-a100-exp \
--steps=20000 \
--save_freq=2000 \
--output_dir=outputs/train/a100_resume_20k
--config_path 可以直接写 Hub 仓库名,它会自己把最新的 checkpoint 连同 training_state/ 下载下来再续跑(configs/train.py 第 222–258 行)。
resume 的时候不要改 --steps 的长度,也不要改 batch。原来那次配置的就是 20000,存下来的调度器位置 last_epoch=6000 正好是这条曲线上的点。你要是把长度改了,恢复出来的位置就落在另一条曲线上了。
那就别 resume(resume 要求同样的 batch 64),改成一次干净的新训练:
lerobot-train \
--policy.path=lerobot/smolvla_base \
--dataset.repo_id=Suyang99/xlerobot-cup-grasp-20260820-0230 \
--rename_map='{"observation.images.head":"observation.images.camera1","observation.images.right_arm_wrist":"observation.images.camera2","observation.images.left_arm_wrist":"observation.images.camera3"}' \
--batch_size=8 --num_workers=8 \
--steps=20000 --save_freq=2000 \
--dataset.eval_split=0.2 \
--policy.device=cuda --policy.push_to_hub=false \
--output_dir=outputs/train/b8_eval02_20k \
--job_name=b8_eval02_20k
--dataset.eval_split=0.2 不是写错。老模型用的就是 0.2、训练集是 ep0–39。
把这一项对齐,这次训练才能和老模型构成「只差 batch」的干净对照(见上面三·五)。
20000 步跑满,估计 1.5 小时左右。
两小时到点还没跑完?不要紧,这次不一样。
LeRobot 每存一个 checkpoint,都会把 pretrained_model/ 和 training_state/
一起写进去(common/train_utils.py 第 243、314 行)。所以到点直接停,
把最后一个存档连 training_state/ 一起拉下来,以后随时能再 resume 续到 20000。
⚠️ 下载时千万记得带上 training_state/。上次就是只下了 pretrained_model/,
才让人以为不能续跑。
按官方那个实测点(20000 步 / batch 64 / 约 4 小时)估:+1 小时到约 11000 步, +2 小时到约 16000 步(正好等于老模型的更新次数),+2.8 小时跑满 20000。
step_s。× 步数 = 真实总时长。超预算就现在停掉重开。中间存档不要钱,所以一次 resume 就能同时做三组对照:
| 拿谁比谁 | 唯一的差别 | 回答什么 |
|---|---|---|
| 现有的 6000 vs 新的 20000 | 只有步数 | 是不是「只是没训完」 |
| 新的 16000 vs 老模型 16000 | 只有 batch(2 vs 64) | 大 batch 本身有没有害 |
| 新的 16000 vs 新的 20000 | 只有曲线走没走完 | 把温降完值不值那 0.8 小时 |
每个存档的测法一样:杯子摆训练用过的 5 个位置,每位 2 次共 10 次,每次记「成功/失败、闭爪时腰部转到了多少度、有没有降到杯沿以下、失败类型」。腰部转角是重点 —— 它是目前唯一被量化证明坍缩的量。
如果所有存档都一样失败,那说明训练超参根本不是瓶颈,就别再往训练上花钱了,去补位置数据(第十节)和修换块跳变。
| 结论 | 依据 | 性质 |
|---|---|---|
| 两次训练除了 batch 和 eval_split,其余逐项相同 | 两个 train_config.json 并排读 | 实测 |
| 官方配方是 batch 64 / 20000 步 / 4 小时 | docs/source/smolvla.mdx 第 49、57–66 行 | 实测(文档原文) |
降温曲线按 --steps 自动伸缩 | schedulers.py 第 149–153 行 | 实测(代码) |
| 停下时学习率 7.99e-05 vs 1.18e-05 | 按上述代码复算。老模型日志在 step:18K 处打的是 lr:5.5e-06,复算在 17,700 步处为 5.65e-06 —— 日志的步数是取整显示的,两者一致 | 实测 |
| 模型闭爪时腰角散布压到三成、从未超过 +1.7° | 今天 20 次试验的 trace,27 个闭爪时刻 | 实测 |
| 示范里图像位置与关节角相关系数 0.953 / −0.846 | 50 集头部相机首帧 + 抓取帧关节角 | 实测 |
| 「没训完 → 先学共同点、后学细节」 | 训练过程的一般规律,与上面的数字一致 | 推断(但和实测吻合) |
| batch 16 约 1.3 小时 | 从官方唯一一个实测点按比例推的 | 估算,开跑三分钟即可校准 |
| 平移增强会抹掉空间线索 | 位置间距 0.06 vs 增强幅度 ±0.05,都是实测;「因此有害」是推理 | 推断(依据是实测的) |
五个杯子位置在画面里落在 0.56 / 0.62 / 0.70 / 0.82 / 0.84。注意 0.70 和 0.82 之间是空的——这一段没有任何训练数据。
你今天试的「右边偏内」那个位置,很可能正好落在这个洞里。所以:先把训练配方修对(这一页),再去补位置数据。顺序反了的话,你会分不清是配方的问题还是数据的问题。