WALL-OSS · X Square Robot · RTX 3090 实机运行

3090 单卡微调的具身模型,在 LIBERO 仿真里真实完成了抓取放置

wall-oss-flow 权重(7.9 GB)在本机 3090 上跑通三档可视化后,用 freeze_vlm 方案 完成 LIBERO spatial 微调,闭环仿真成功率从 0 提升到 14%(7/50)——期间排掉了 4 个独立 bug: 训推不一致、捷径学习"失明"、仿真渲染错绑碰撞体、执行 chunk 过长。成功视频见首节。
📓 想逐行读源码?→ 代码导读 Notebook(按一次前向的数据流组织,含真实文件行号)

WALL-OSS 是什么 4.22B 参数 · VLA 模型

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)。

主视角相机256×256 RGB
腕部相机256×256 RGB
语言指令"把黑碗放到盘子上"
本体状态末端位姿+夹爪 8 维
Qwen2.5-VL-3B 骨干(ViT 0.67B + LLM 3.08B) 把图像和文字编码成统一的场景表征,也能直接输出语言回答(见下方 VQA 章节)· ❄ 本次微调整个冻结(freeze_vlm)
↓ 场景表征(两个专家共享注意力)
动作专家 0.45B · Flow Matching 从随机噪声出发,在场景表征的引导下迭代"去噪",一次生成一整段连续动作 · 🔥 本次唯一放开训练的部分
动作 chunk:10 步 × 7 维 每步 = 末端位姿增量 6 维 + 夹爪开合 1 维;仿真执行完这 10 步(约 0.5 秒)后重新看图、 重新生成下一段——这就是"闭环"

两个对读懂本页实验很关键的设计:① 动作头实际输出 20 维跨本体布局——同一套权重 兼容单臂、双臂等不同机器人,LIBERO 的 7 维动作只占用前 7 个槽位,其余用 dof_mask 屏蔽; ② 动作是整段 chunk 一次生成的——chunk 越长执行越顺滑、但两次"看图"之间盲走越久, 这正是后文"32 步改 10 步才首次成功"那个问题的根源。

拆开看这 4.22B 参数 safetensors 逐张实测

下表直接解析 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.22Bbf16 存储 = 7.9 GB

动作专家的完整组成(层名和形状抄自权重文件): 除了上面 36 层瘦 FFN 这个"主体",还有一圈小而关键的进出投影——

模块形状干什么
propri_proj40 → 2048 本体状态 20 维 + dof_mask 20 维拼接后投成 1 个状态 token 注入序列(训练时 50% 概率整体置 NaN,见后文"防失明")
w140 → 2048 带噪动作 20 维 + dof_mask 20 维 → 每个时间步 1 个动作 token(chunk 10 步 = 10 个 token)
time_embed + w2 + w3正弦编码 → 4096 → 2048 → 2048 把去噪时间步 t 编码后与动作 token 融合(模型要知道"现在噪声还剩多少")
action_proj_back2048 → 20 最后一层隐状态投回 20 维物理空间,输出的是"流场"(见下节)

Flow Matching:动作是怎么被"去噪"出来的

为什么不直接回归动作?因为动作分布是多峰的——同一个场景,从左绕、从右绕都能 完成任务,直接回归 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 生态。

数据集:预训练数据与 LIBERO 微调数据

预训练(官方,我们没有参与):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

训练细节:3090 上的完整配方 实测

官方配置是 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 步
batch1 × 梯度累积 16 = 等效 batch 16(显存还有富余,batch 2-4 是后续提速项)
单 epoch50322 个滑窗 × 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 量级

闭环仿真成功案例 本机实测 · 7/50

这就是最终目标:模型靠视觉闭环、真实完成任务。MuJoCo 仿真中 Franka Panda 机械臂由微调模型驱动(每 10 步重新看图规划),下面 7 段视频全部被 LIBERO 环境判定为 SUCCESS——找到黑碗 → 下探抓取 → 提起 → 移到盘子上方 → 放下。覆盖 5 种不同空间布局(含最难的"木柜顶层抽屉里"), 说明模型确实在用视觉区分"碗在哪",而不是背某一条轨迹。

SUCCESS · pick up the black bowl between the plate and the ramekin and place it on the plate
SUCCESS · pick up the black bowl next to the cookie box and place it on the plate
SUCCESS · pick up the black bowl on the ramekin and place it on the plate
SUCCESS · pick up the black bowl on the cookie box and place it on the plate
SUCCESS · pick up the black bowl on the ramekin and place it on the plate
SUCCESS · pick up the black bowl between the plate and the ramekin and place it on the plate
SUCCESS · pick up the black bowl in the top drawer of the wooden cabinet and place it on the plate

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 过程的三个阶段, 和上面的成功视频放在一起看,能看清模型是怎么一步步变好的。

① 微调前 zero-shot · 机械臂大幅乱动,完全够不到目标 (画面里机械臂是绿色的——这正是后来才发现的渲染 bug:碰撞网格错绑在机身上)
② 治好"失明"+修复渲染后(32 步长 chunk)· 能直奔黑碗、下探、 移向盘子——轨迹形状全对,但抓取差最后一厘米,"空手"完成了放置动作
③ 最终模型(10 步短 chunk,与成功视频同一权重)的失败样本 · 接近成功的近失误——同一个模型 5/50 成功、45/50 是这种差一点的失败

①→②→③ 的进化:从"乱动"到"够得着抓不准"到"偶尔成功"——每一步对应一个被排掉的 bug(失明 → 渲染 → chunk 过长)。要到官方 96% 还需要更多训练量和解冻视觉编码器。

VQA 思维链推理 实机跑通

给模型看一帧机器人视角的桌面图,问它"要把红方块放进同色盘子,下一步该做什么"。 模型先定位物体、判断当前状态,再给出操作步骤——这是 VLA 模型「看懂场景 → 规划动作」的语言侧证据。

输入图:桌面上有红/黄/蓝三个盘子与红/蓝两个方块
输入图像 · assets/cot_example_frame.png(红/黄/蓝盘子 + 红/蓝方块)
问题
To move the red block in the plate with same color, what should you do next? Think step by step.
WALL-OSS-Flow 输出
To move the red block in the plate with the same color, you should first locate the red block. It's currently positioned on the blue surface, not in the plate. Then, you need to pick up the red block. Once you have the red block, you can then place it in the plate that is also red in color. This will ensure that the red block is in the correct plate.

模型正确完成了三步推理:定位红方块 → 判断它还在桌面上而不在盘中 → 规划"抓取后放入红色盘子"。

3090 单卡 finetune:微调前后开环对比 本机实测 · freeze_vlm

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)上的开环预测:微调前各维度系统性漂移、夹爪时机完全错乱;微调后趋势、峰谷、 夹爪开合时机全部对上——模型确实学会了这个本体。

微调前 zero-shot 开环预测(episode 1400)
微调前(zero-shot)· episode 1400 · 预测漂移、夹爪错乱
微调后开环预测(episode 1400)
微调后(3 epoch)· 同一集 · 全维度贴合、夹爪时机对上

从"开环贴合"到"闭环成功"隔着 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 直接 EOFErrorbuild 时预写 ~/.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 statesTORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1