wall-oss-flow 权重(7.9 GB)在本机 3090 上跑通三档可视化后,用 freeze_vlm 方案
完成 LIBERO spatial 微调,闭环仿真成功率从 0 提升到 14%(7/50)——期间排掉了 4 个独立 bug:
训推不一致、捷径学习"失明"、仿真渲染错绑碰撞体、执行 chunk 过长。成功视频见首节。
📓 想逐行读源码?→ 代码导读
Notebook(按一次前向的数据流组织,含真实文件行号)
WALL-OSS 是自变量机器人(X Square Robot)2025 年 9 月开源的视觉-语言-动作
(Vision-Language-Action,VLA)模型:在 Qwen2.5-VL 视觉语言模型上外挂一个"动作专家",
让同一个模型既能「看图 + 读指令 + 用语言推理」,又能直接输出机械臂的连续控制信号。
官方提供两个动作头变体——wall-oss-fast(动作离散成 token,像生成文字一样生成动作)和
wall-oss-flow(flow matching 连续去噪生成动作);本页全部实验用的是后者(4.22B 参数,7.9 GB)。
两个对读懂本页实验很关键的设计:① 动作头实际输出 20 维跨本体布局——同一套权重 兼容单臂、双臂等不同机器人,LIBERO 的 7 维动作只占用前 7 个槽位,其余用 dof_mask 屏蔽; ② 动作是整段 chunk 一次生成的——chunk 越长执行越顺滑、但两次"看图"之间盲走越久, 这正是后文"32 步改 10 步才首次成功"那个问题的根源。
下表直接解析 model.safetensors 的 header 统计得出(不是论文数字)。
WALL-OSS 的"外挂动作专家"是一种 MoE(专家混合)改造:把 Qwen2.5-VL 的 36 层 transformer 里
每层的 FFN 复制出一个瘦版本,按 token 类型硬路由——图文 token 走原来的语言 FFN,
动作/状态 token 走新的动作 FFN;而注意力完全共享(attention_moe=false,
mlp_moe=true),动作 token 在全部 36 层里都能直接"看到"图文 token 的表征,
这是它区别于"冻结 VLM 后面接个动作头"方案的关键——融合发生在每一层,不是只在最后一层。
| 组件 | 参数量 | 职责 |
|---|---|---|
| ViT 视觉编码器 | 0.67B | 把两路 256×256 相机图切成 patch 编码为视觉 token(32 层,hidden 1280) |
| 共享注意力 + 词嵌入 + 输出头 | 0.66B | 36 层 GQA 注意力(16 个查询头 / 2 个 KV 头,hidden 2048)——图文与动作 token 在这里互相读取 |
| 语言 FFN(experts.0) | 2.44B | 每层中间维 11008 的 SwiGLU,处理图文 token(参数大头) |
| 动作专家 FFN(experts.1) | 0.45B | 每层中间维 2048 的"瘦" SwiGLU,只处理动作/状态 token —— 本次微调唯一训练的主体 |
| ActionProcessor 进出投影 | 0.013B | 动作/状态在 20 维物理空间与 2048 维隐空间之间的进出翻译(明细见下) |
| 合计 | 4.22B | bf16 存储 = 7.9 GB |
动作专家的完整组成(层名和形状抄自权重文件): 除了上面 36 层瘦 FFN 这个"主体",还有一圈小而关键的进出投影——
| 模块 | 形状 | 干什么 |
|---|---|---|
| propri_proj | 40 → 2048 | 本体状态 20 维 + dof_mask 20 维拼接后投成 1 个状态 token 注入序列(训练时 50% 概率整体置 NaN,见后文"防失明") |
| w1 | 40 → 2048 | 带噪动作 20 维 + dof_mask 20 维 → 每个时间步 1 个动作 token(chunk 10 步 = 10 个 token) |
| time_embed + w2 + w3 | 正弦编码 → 4096 → 2048 → 2048 | 把去噪时间步 t 编码后与动作 token 融合(模型要知道"现在噪声还剩多少") |
| action_proj_back | 2048 → 20 | 最后一层隐状态投回 20 维物理空间,输出的是"流场"(见下节) |
为什么不直接回归动作?因为动作分布是多峰的——同一个场景,从左绕、从右绕都能 完成任务,直接回归 MSE 会把两条可行轨迹平均成一条"两边都不像"的废轨迹。Flow matching 的做法是学一个把随机噪声"流"成真实动作的速度场,推理时从噪声出发沿场积分, 每次采样出的是一条完整、自洽的轨迹,而不是逐维平均值。
| 阶段 | 做法(数字抄自源码/config) |
|---|---|
| 训练时 | 采样噪声程度 t~Beta(1.5,1) 再翻转缩放(偏向高噪声端,多练"从零起步");
构造插值 noisy = (1−t)·noise + t·action;模型预测速度场
flow = action − noise,MSE 损失 × dof_mask(20 维里只有前 7 维计损失,
其余 13 个空槽不产生梯度) |
| 推理时 | 从纯高斯噪声出发,5 步 Euler 积分(config 里
num_inference_timesteps: 5):每步把当前带噪动作 + 时间步喂进模型求速度,
沿速度走 1/5 —— 5 次前向就把 10 步 × 20 维的动作 chunk 从噪声里"长"出来 |
| 条件从哪来 | 不是把图像特征拼进输入,而是靠共享注意力: 动作 token 在 36 层里逐层 attend 到视觉/语言/状态 token——场景一变,速度场就变 |
官方另一个变体 wall-oss-fast 走的是相反路线:用 FAST tokenizer 把连续动作离散成 token,像生成文字一样自回归生成动作。flow 版本推理步数固定(5 步),fast 版本兼容性更接近纯 LLM 生态。
预训练(官方,我们没有参与):WALL-OSS 用大规模多模态 + 具身数据做了 多阶段课程训练,官方称为 Unified Cross-Level CoT——把「指令推理 → 子目标分解 → 细粒度动作生成」 统一进同一个可微框架,所以它既能像 VLM 一样回答问题(上面 VQA 章节),又能出动作。 20 维跨本体布局(双臂 6+6+1+1、头部 2、升降 1、底盘 3)对应自变量自家的双臂轮式机器人。
微调数据:我们用的是 physical-intelligence/libero
(LeRobot v2.0 格式)。它的来源链条是:LIBERO 学术基准(原始任务定义)→ OpenVLA 团队在仿真里
重新采集演示 → Physical Intelligence 转成 LeRobot 格式发布。
| 维度 | 内容 |
|---|---|
| 规模 | 1693 集 / 27.3 万帧 / 40 任务(spatial、object、goal、long 四套件 × 各 10 任务),10 fps |
| 每帧内容 | image 256×256(第三人称)+ wrist_image 256×256(腕部)+ state 8 维(末端位姿 6 + 夹爪两指 2)+ actions 7 维(OSC 末端增量 6 + 夹爪开合 1) |
| 本次用量 | libero_spatial 套件全部 432 集 ≈ 5 万帧(10 个任务全是"把黑碗放到盘子上",只是黑碗初始位置不同——专门考空间理解) |
| 进模型前的加工 | 7 维动作 pad 进 20 维布局的前 7 槽 + dof_mask 标记有效位;per-维 min/max 归一化到 [-1,1](statistics 从 parquet 自算,权重里不带 LIBERO 的 normalizer——这是官方要求"先微调再评估"的原因);按 action_horizon=10 滑窗切 chunk |
官方配置是 8×A100 全量微调;24G 单卡放不下 4.22B 的全量 AdamW(优化器状态就要
50GB+),所以用 freeze_vlm: true:ViT、语言 FFN、共享注意力全部冻结,
只训动作专家 FFN + ActionProcessor ≈ 0.47B(占总参数 11%)。
| 项 | 值 |
|---|---|
| 可训参数 / 显存 | 0.47B / 实测 14 GB(24.6 GB 卡,bf16 混合精度) |
| 优化器与学习率 | AdamW · lr 5e-5 cosine 衰减至 1e-5 · warmup 50 步 |
| batch | 1 × 梯度累积 16 = 等效 batch 16(显存还有富余,batch 2-4 是后续提速项) |
| 单 epoch | 50322 个滑窗 × 0.18s/步 ≈ 2.5 小时 |
| 防捷径学习 | PROPRI_DROPOUT=0.5:每个样本 50% 概率把本体状态整体置 NaN(mask=0), 逼模型从图像里找答案——没有这一条,模型会走"只看本体状态外推"的捷径,闭环必败(详见下文破案记录 ②) |
| 训练历程 | ① train_full:chunk 32 × 3 epoch(治好"失明")→ ② train_h10:chunk 10 × 3 epoch(首次闭环成功 5/50)→ ③ h10b:续训 6 epoch(7/50,当前最佳)→ ④ h10c:调度修正对照 +3 epoch(5/50,说明此配方下训练量比 lr 调度细节更重要),loss 0.30 → 0.005 量级 |
这就是最终目标:模型靠视觉闭环、真实完成任务。MuJoCo 仿真中 Franka Panda 机械臂由微调模型驱动(每 10 步重新看图规划),下面 7 段视频全部被 LIBERO 环境判定为 SUCCESS——找到黑碗 → 下探抓取 → 提起 → 移到盘子上方 → 放下。覆盖 5 种不同空间布局(含最难的"木柜顶层抽屉里"), 说明模型确实在用视觉区分"碗在哪",而不是背某一条轨迹。
7 段 SUCCESS 视频 · LIBERO 环境自动判定 · 每段约 3-4 秒(上限 220 步)。 源文件 ~/learn/02_wall-x/artifacts/wallx-output/rollouts_h10b5_fix/libero_spatial/。
| libero_spatial 任务(黑碗位置) | 成功率 |
|---|---|
| 盘子与小烤碗之间 | 2/5 |
| 小烤碗上方 | 2/5 |
| 饼干盒旁 | 1/5 |
| 饼干盒上方 | 1/5 |
| 木柜顶层抽屉里 | 1/5 |
| 其余 5 种布局 | 0/5 |
| 合计 | 7/50 = 14%(官方 8×A100 全量微调 100 epoch 可达 ~96%; 本结果为 24G 单卡、只训 0.45B 动作专家、12 epoch 的下限验证) |
成功不是一步到位的。下面三段失败视频对应排 bug 过程的三个阶段, 和上面的成功视频放在一起看,能看清模型是怎么一步步变好的。
①→②→③ 的进化:从"乱动"到"够得着抓不准"到"偶尔成功"——每一步对应一个被排掉的 bug(失明 → 渲染 → chunk 过长)。要到官方 96% 还需要更多训练量和解冻视觉编码器。
给模型看一帧机器人视角的桌面图,问它"要把红方块放进同色盘子,下一步该做什么"。 模型先定位物体、判断当前状态,再给出操作步骤——这是 VLA 模型「看懂场景 → 规划动作」的语言侧证据。
模型正确完成了三步推理:定位红方块 → 判断它还在桌面上而不在盘中 → 规划"抓取后放入红色盘子"。
24G 的卡到底能不能 finetune?——能。用官方 freeze_vlm: true 方案冻结
3.8B VLM、只训 0.45B 动作专家:实测显存 14 GB / 24.6 GB,LIBERO spatial 前 120 集训 3 个 epoch
耗时 2 小时 12 分,loss 从 0.30 收敛到 0.023。下面两张图是同一集训练时从未见过的留出演示
(episode 1400)上的开环预测:微调前各维度系统性漂移、夹爪时机完全错乱;微调后趋势、峰谷、
夹爪开合时机全部对上——模型确实学会了这个本体。
从"开环贴合"到"闭环成功"隔着 4 个独立 bug,依次排掉: ① 官方评估脚本 4 处训推不一致(腕部相机命名、预测步数 10 vs 32、prompt 布局、缺失模块), 用逐字节 diff 对齐;② 捷径学习"失明"——微调后模型忽略图像只回归本体状态(噪声图测试实锤: 喂随机噪声预测照样"准"),用 50% state dropout 重训治愈(治愈后噪声图测试预测崩溃 = 真在看图); ③ 仿真渲染 bug——robosuite 离屏渲染把绿色碰撞网格叠在机械臂上,模型从没见过"绿迷彩机械臂", 关闭 geomgroup 0 修复;④ 执行 chunk 过长——32 步盲走 1.6 秒误差积累,改为官方配方 action_horizon=10 后首次出现闭环成功。
| 项 | 内容 |
|---|---|
| 镜像 | wallx:latest(pytorch 2.6 + flash-attn 2.7.4)→ wallx-libero:latest(+ lerobot 0.3.4 + robosuite 1.4.0 + mujoco 2.3.7 + LIBERO),Dockerfile / Dockerfile.libero 两级构建 |
| 权重 | ~/app/models/wall-x/wall-oss-flow(7.9 GB)+ wall-oss-flow-libero/(symlink 目录:config.yml + 自算 normalizer_*.pth,不动原权重) |
| 数据 | ~/learn/02_wall-x/artifacts/lerobot/physical-intelligence/libero(33 GB,1693 集 / 27.3 万帧,hf-mirror 拉取) |
| 硬件 | RTX 3090 24 GB —— 三档推理全部够用;freeze_vlm finetune 实测 14 GB,单卡可行(全参才需 60 GB+) |
docker run --rm --gpus all \
-v ~/app/models/wall-x/wall-oss-flow:/models/wall-oss-flow:ro \
-v $PWD/vqa_demo.py:/opt/vqa_demo.py:ro \
wallx:latest python /opt/vqa_demo.py
| 坑 | 解法 |
|---|---|
| LIBERO 首次 import 交互式问路径,Docker build 直接 EOFError | build 时预写 ~/.libero/config.yaml |
| mujoco 3.10 与 robosuite 1.4.0 API 不兼容(mj_fullM TypeError) | pin mujoco==2.3.7 |
| LIBERO requirements 锁 numpy 1.22/transformers 4.21,会毁掉 wallx 环境 | LIBERO 用 --no-deps 装,运行时依赖手动补 |
| 预训练权重是 20 维跨本体动作布局,官方 libero 配置(7 维)加载直接维度不匹配 | 数据 pad 进前 7 槽 + 20 维 normalizer,dof_mask 标记有效位 |
| 权重不含 libero 归一化统计,官方流程要求先 finetune | 从 parquet 直接算 min/max 生成 normalizer_*.pth(双 key:repo_id + libero_all) |
| legacy 推理栈缺 wall_x/infer/data_utils.py(上游漏提交,1.1.0 已废弃该栈) | stub 模块挂载,LIBERO 路径不触发这些函数 |
| prepare_batch 的 state pad 被注释 + normalize 返回值未接收 + fixed_action_dim 实为 7 | 挂载 3 个 patch 文件修复(不改镜像) |
| PyTorch 2.6 torch.load weights_only 拒绝 LIBERO init states | TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 |