← 返回文章库

一个 VLA 开源实现长什么样?unifolm-vla 代码结构解读

最后更新 2026-08-23
⏱ 约 14 分钟 🟡 涉接线/强电
你将学到
  • 看清一个真实 VLA 项目由哪几个可辨认的代码块拼成,各自负责什么
  • 搞懂动作分块、动作归一化这两个配置项为什么会出现在训练入口
  • 理解推理端与控制端分离的部署架构,以及它带来的频率问题

VLA 这三个字母现在的处境有点尴尬:到处都能看到,但大部分文章讲完「视觉—语言—动作」这个展开就没有下文了。你知道它输入图像和指令、输出动作,可一旦想动手,问题立刻变得很具体——这东西的代码到底长什么样?哪一部分是模型,哪一部分是数据,权重从哪来,训练脚本要配什么,最后怎么接到机器人上?

概念那一层之前那篇讲 VLA 是什么已经铺过了,这篇不重复。这里只做一件事:把一个真实的开源 VLA 项目——宇树的 unifolm-vla——从代码结构层面拆开,一项一项对着看。

先把边界说清楚:本文完全基于该仓库的公开 README 与文件树,我没有真机、也没有跑过训练或推理,因此不会出现任何效果评测、成功率或性能数字。 涉及模型表现的地方,我只会转述 README 自己的说法并标明这是仓库的表述。这是一篇代码结构解读,不是评测。

仓库到底给了什么:逐项核对

评价一个「开源模型」项目,最实在的办法是拿一张清单去勾。README 里有个 Open-Source Plan 小节,三项全打了勾:训练、推理、权重。这三项凑齐,项目才算真的能用——只放推理代码不放权重,或者只放权重不放训练脚本,都是很常见的半开源。

顺着文件树核对一遍,能勾上的东西是这些:

unifolm-vla/
    ├── assets              # 演示用的 GIF
    ├── experiments         # LIBERO 仿真评测相关
    ├── deployment          # 部署服务端代码
    ├── prepare_data        # 数据集预处理与格式转换脚本
    ├── scripts             # 训练 / 评测 / 部署的入口脚本
    └── src
         └── unifolm_vla
                ├── config          # 训练配置
                ├── model           # 模型结构与骨干定义
                ├── rlds_dataloader # 数据加载、变换与 dataloader
                └── training        # 训练逻辑

这是 README 里那张架构图的原样。模型定义、训练、数据接口、推理、部署,五块都在。 权重不在仓库里(模型权重本来也不该进 git),README 用一张表给出三个检查点的下载入口,托管在 HuggingFace 上:

  • UnifoLM-VLM-Base——按 README 的说法,是在通用图文问答数据和开源机器人数据上微调得到的视觉语言底座
  • UnifoLM-VLA-Base——在宇树开源数据集上微调
  • UnifoLM-VLA-LIBERO——在 LIBERO 数据集上微调

三个权重的关系值得停一秒。它们不是三个平行的选项,而是一条链: 底座那个是「视觉语言」阶段的产物,后两个是在它之上接着往「动作」方向练出来的。这一点在训练脚本的配置里被直接印证了——训练时要把 base_vlm 指向 UnifoLM-VLM-Base 的路径或权重地址,它的作用是初始化视觉语言骨干。

也就是说,你自己训一个 VLA,起点不是随机初始化,是一个已经会看图会读字的模型。 这基本是当前这类项目的通行做法,代码里体现得很直白。

拆开模型:三块可辨认的东西

src/unifolm_vla/model/ 下的文件排布,几乎是把「VLA 在工程上是哪几块」这个问题的答案摆在了台面上:

model/
  ├── framework/
  │     ├── base_framework.py
  │     ├── share_tools.py
  │     └── unifolm_vla.py
  ├── modules/
  │     ├── vlm/
  │     │     └── QWen2_5.py
  │     └── action_model/
  │           ├── DiT_ActionHeader.py
  │           └── flow_matching_modules/
  │                 ├── action_encoder.py
  │                 └── cross_attention_dit.py
  ├── tools.py
  └── utils/pooling_utils.py

modules/ 下正好分成 vlmaction_model 两个目录,framework/ 里的 unifolm_vla.py 把它们组装起来。这个划分不是这个项目特有的,而是 VLA 工程实现的通用骨架,你可以按三块来理解:

第一块是视觉编码器。 负责把摄像头图像变成一串特征向量。在这类实现里它通常不是独立目录,而是长在视觉语言模型内部——QWen2_5.py 这个文件名对应的就是这一层,仓库致谢里也明确写了大量代码继承自 Qwen2.5-VL。

第二块是语言/多模态骨干。 它同时接收图像特征和文字指令,输出一个融合了两者的表示。VLA 相对于纯视觉策略的全部优势都压在这一块上:你说「把桌子擦干净」和「把积木叠起来」,靠的是它把语义落到画面里的具体位置上。

第三块是动作解码头。 拿骨干输出的表示,生成机器人能执行的动作。这个仓库里它叫 DiT_ActionHeader,配一组 flow matching 模块,其中 cross_attention_dit.py 这个文件名说明动作生成是通过交叉注意力去读取骨干侧信息的——动作头不是简单接在骨干输出后面的一个 MLP,而是一个自己有结构的生成模块。

顺带一个观察:rlds_dataloader/ 下还有个 action_tokenizer.py。把连续动作离散化成 token、再让语言模型像吐词一样吐动作,是 VLA 的另一条实现路线,跟基于扩散/flow matching 的连续动作头是两种思路。两条路线的痕迹在这个仓库里同时存在。 我不去猜作者在哪一步用了哪一个,只提醒你读这类仓库时留意这种共存——它往往对应着项目的演进历史。

输入吃什么,输出吐什么

这是最该问清楚、也最容易被含糊过去的问题。我只写 README 明确写了的部分。

输入侧,README 在描述真机推理时写得很直接:机器人客户端从真实机器人上采集观测(observations),发给服务端做动作推理。观测具体包含什么,README 没有逐项列举,但训练配置那一步给出了硬证据——constants.py 里要配的四个量是 NUM_ACTIONS_CHUNKACTION_DIMPROPRIO_DIMACTION_PROPRIO_NORMALIZATION_TYPEPROPRIO_DIM 就是本体感知(proprioception)的维度,这条配置项的存在本身就说明模型除了图像还吃本体状态。 语言指令这一侧不用多说,模型定位就是视觉语言动作模型,README 里也写了它通过持续预训练把文本指令与二维/三维空间细节做了深度结合。

输出侧NUM_ACTIONS_CHUNK 这个名字给出了答案。README 对它的描述是「模型预测的动作块大小」——模型一次吐的不是一个动作,是一段动作序列。 维度由 ACTION_DIM 决定,取值来自你数据集里动作的自由度。

还有一个细节,第一次读容易滑过去:无论是 LIBERO 仿真评测脚本还是真机服务端脚本,要改的字段里都有一个 unnorm_keyun-norm,反归一化。 这说明模型内部吐出来的动作是归一化后的数值,要还原成真实的关节量,得知道当初是按哪个数据集的统计量归一化的。训练侧那个 ACTION_PROPRIO_NORMALIZATION_TYPE 与它正好是一对。

这一对配置项是所有模仿学习部署最经典的坑之一:训练时用 A 数据集的均值方差归一化,推理时填了 B 的 key,模型输出不会报错,机器人会以一个看起来「差不多但就是不对」的姿态动起来。它不崩溃,它只是错。 这类沉默的错误比异常难查得多,所以看到脚本里显式要求你填 unnorm_key,应该理解成设计者在逼你把这件事想清楚。

动作表示:仓库没替你决定的那一半

ACTION_DIM 只告诉你动作是几维的,没告诉你每一维是什么意思。这恰恰是你把自己的机器人接进来时,第一个必须自己定的东西。工程上常见的三种选择,各有各的代价:

绝对关节角。 每一维直接是某个关节的目标角度。好处是没有歧义、不累积误差——第 100 步预测错了,第 101 步只要预测对,机器人立刻回到正确位置。代价是模型必须学会「这台机器人此刻处于什么姿态」,泛化到另一台构型不同的机器人上基本无望。

相对增量。 每一维是相对当前状态的变化量。好处是数值范围小、分布集中,模型好学,而且跨构型迁移的可能性略大一些。代价是误差会累积:每一步偏一点点,几十步之后手就飘到别处去了,而模型自己看不见这件事。

末端位姿。 动作定义在末端执行器的空间位姿上(位置加姿态,再加一个夹爪开合)。好处是它跟机器人具体有几个关节解耦,换一台机械臂只要重新解逆运动学就行。代价是把逆运动学的负担推给了下游,遇到奇异位形或超出工作空间时,模型给出的目标可能根本解不出来。

没有哪个是标准答案。真正要紧的是:采集数据时用的是哪一种,训练和部署就必须严格用同一种。 这件事上的不一致,同样属于「不报错但全错」的那一类。

动作分块:为什么几乎所有 VLA 都这么做

回到 NUM_ACTIONS_CHUNK。一次预测一段动作(action chunking)在这个方向上已经近乎标配,原因有两个,都很实在。

第一个是误差累积。 如果模型每步只预测一个动作,那么每一步的预测误差都会改变机器人的状态,下一步的输入就带上了这个偏差,再预测再偏——这是模仿学习里被讨论了很多年的分布漂移问题。一次预测一整段,段内的动作是模型在同一个观测下一起规划出来的,彼此协调,中途不会因为单步抖动而跑偏。

第二个是推理频率。 这一点更工程。一个带大模型骨干的 VLA,单次前向要花的时间远不是控制回路能接受的。如果每个控制周期都要跑一次模型,机器人就得等着它。一次出一段动作,模型的调用频率立刻被摊薄若干倍。

代价当然也有:段内是「盲」的。模型在段的开头看了一眼世界,然后闭着眼把这一段执行完。段越长,对环境突变的响应越迟钝。段长本质上是「稳定性」和「反应速度」之间的一个旋钮,所以它被放在配置文件里让你自己调,而不是写死在代码里。

推理跑不过控制回路,怎么办

顺着上面那条往下,是这个仓库在部署上最能说明问题的设计。README 写得很明确:推理在服务端执行,机器人客户端采集观测发给服务端,服务端算出动作再回传。

具体链路是这样的:

# 服务端:改好 ckpt_path / port / unnorm_key / vlm_pretrained_path 后启动
bash scripts/eval_scripts/run_real_eval_server.sh

# 客户端:先建一条到服务端的 SSH 隧道
ssh user_name@remote_server_IP -CNg -L port:127.0.0.1:port

客户端那一侧,README 指向了 unitree_deploy,说明要先按 unifolm-world-model-action 仓库里的说明装好环境、在真机上把控制器跑起来,再参考 robot_client.py 改自己的脚本。

这个架构的动机不难理解——大模型需要显卡,机器人身上未必背得动那块卡。但它同时把一个问题摆到了明面上:模型的输出频率和机器人的控制频率根本不是一个量级,中间还隔着一段网络。

机器人的关节控制回路通常要求稳定的高频输入,一拍都不能少;而模型这边,一次前向要几十上百毫秒,还得加上一个来回的网络延迟。这中间的落差不会自己消失,工程上必须有人来填。常见的填法有两种:

一是动作队列。 模型吐出的一整段动作进队列,控制侧按自己的节奏一帧一帧取。队列快见底时再触发下一次推理,让新一段动作在旧的用完之前到位。这也解释了为什么动作分块和这种部署架构总是成对出现——分块的另一半意义,就是给这条链路提供缓冲。

二是插值。 相邻两个动作点之间按控制周期做平滑过渡,避免关节收到一串阶跃指令。少了这一步,机器人动作会明显发顿,机械上也不友好。

还有一件必须提前想好的事:队列空了、或者网络断了,控制侧该干什么? 保持最后一个动作、缓慢回到安全姿态、还是直接切断输出——这属于真机部署的安全底线,跟模型好不好没有关系。README 在这一层没有展开,我也不去替它推断实现细节,只是提醒你:自己接的时候,这个分支必须写。

数据怎么喂进去:两次格式转换

prepare_data/ 这个目录对应的流程,README 讲得比模型部分还细,因为这是自己训练时绕不开的一段。

起点是 HuggingFace LeRobot 的数据集格式。宇树自家采数据到转 LeRobot 格式那一段,走的是另一个仓库的工具链——unitree_lerobot 那套数据转换与验证流程负责把遥操作采到的原始 JSON 转成 LeRobot 数据集,unifolm-vla 从这里接手。

接手之后是两跳:

# 第一跳:LeRobot → HDF5
cd prepare_data
python convert_lerobot_to_hdf5.py \
    --data_path /path/to/your/source_dir/dataset1_name \
    --target_path /path/to/save/the/converted/data/directory

# 第二跳:HDF5 → RLDS
cd prepare_data/hdf5_to_rlds/rlds_dataset
tfds build --data_dir /path/to/save/the/converted/data/directory

最终产物是 source_dir/1.0.0 这样一个目录,README 特别强调这个 1.0.0 版本目录才是能拿去训练的东西。

为什么要绕这么一圈?答案在 dataloader 的目录名里:rlds_dataloader/datasets/rlds/oxe/RLDS 和 OXE 是机器人数据在学术侧的通行格式与数据集集合,致谢里也列了 Open-X。这个项目的数据侧是照着那套生态搭的,而不是照着 LeRobot 搭的,所以你得把数据翻译过去。

这一段绕路本身就是个信号:机器人数据格式目前还没有统一。 你在这个领域干活,写格式转换脚本会是常态,不是意外。

数据准备好之后,要让 dataloader 认得它,README 列了四个必须动的文件:

src/unifolm_vla/rlds_dataloader/datasets/rlds/oxe/configs.py     # 数据集基本配置
src/unifolm_vla/rlds_dataloader/datasets/rlds/oxe/transforms.py  # 该数据集的字段变换
src/unifolm_vla/rlds_dataloader/datasets/rlds/oxe/mixtures.py    # 数据混合配比
src/unifolm_vla/rlds_dataloader/datasets/datasets.py             # 数据集注册

四个文件里都有宇树自己那批 G1 数据集的样例条目,照着抄改就行。mixtures.py 的存在尤其能说明这类训练的实际形态:你很少只用一个数据集训练,更多是把若干个按比例混起来。 训练脚本里的 data_mix 参数就是选这个混合配方的。

README 的数据集表格里列了一批开源数据集,任务名直接写在名字里——G1_Stack_BlockG1_Fold_TowelG1_Wipe_TableG1_Pour_MedicineG1_DualRobot_Clean_Table 等等,都是操作类任务。至于单个策略在这些任务上表现如何,README 有它自己的表述,我不做转述之外的任何评价——仓库里没有评测报告,我也没有可复现的条件。

训练与评测的入口

训练侧要动的配置,README 列了明确的顺序:

  1. base_vlm:指向视觉语言底座权重,用来初始化骨干
  2. oxe_data_root:RLDS 数据的根目录
  3. data_mix:用哪个数据集或哪个混合配方
  4. 检查点与日志的保存路径
  5. num_processes:按可用 GPU 数量调,对应分布式训练规模

配置项本身平平无奇,但把它们连起来看,训练一个 VLA 需要什么就清楚了:一个预训练底座 + 一批 RLDS 格式的演示数据 + 多卡。 src/unifolm_vla/config/deepseeds/ 下放着几份 DeepSpeed 配置(ZeRO-2、ZeRO-3 都有),也印证了这不是单卡能轻松跑起来的规模。我不去猜具体要几张卡——README 没写,仓库里也没有这个数字。

环境这边,README 要求 CUDA 12.4、Python 3.10.18,并明确说强烈建议用同版本以保证兼容性,另外要装 FlashAttention2,以及一个固定 commit 的 lerobot。这些版本以官方仓库当前文档为准,我抄在这里只是想说明一件事:这类项目对环境的敏感度很高,README 把版本钉死不是啰嗦,是经验。

评测有两条路。仿真那条走 LIBERO,需要另外装 LIBERO 环境,然后改 run_eval_libero.sh 里的检查点路径、任务套件名、unnorm_key 和几个路径变量。真机那条就是前面讲的服务端加客户端。先在仿真里跑通、再上真机,这个顺序和强化学习那条链路里 Sim2Sim 的思路是同一个道理:在进入高成本验证之前,先用一个便宜的环境把明显的问题筛掉。

站在谁的肩膀上

README 的致谢部分把上游列得很清楚,说大量代码继承自这几个项目:Qwen2.5-VL、Isaac-GR00T、Open-X、openvla-oft、InternVLA-M1。

这份名单本身就是一张地图。视觉语言骨干来自通用多模态模型,数据格式与生态来自 Open-X 那一套,VLA 的训练与微调范式则参考了几个更早的开源实现。一个厂商的 VLA 项目,真正自己写的那部分,占比可能远比你想象的小。 这不是贬义——在这个变化很快的方向上,能把成熟组件接对、把自家机器人的数据和部署链路补齐,本身就是主要工作量所在。

基于它做二次开发时,记得沿着这条链把各个上游项目的许可证逐个看一遍,并保留应有的署名。README 里也给了引用格式,写论文或做展示时用得上。

读完这个仓库,你应该记住三件事

第一,VLA 在代码层面是可以拆的,而且拆法很固定。 视觉编码器、语言多模态骨干、动作解码头,加上一条把演示数据变成训练样本的管线。你以后再打开任何一个 VLA 项目,先去找这四样东西在哪,比读十页介绍都快。

第二,配置项比架构图更能告诉你真相。 NUM_ACTIONS_CHUNK 意味着输出是一段动作,PROPRIO_DIM 意味着输入含本体状态,unnorm_key 意味着输出是归一化的、需要还原。这些字段是设计者不得不暴露给你的东西,藏不住。读一个陌生项目时,训练脚本的参数列表往往比 README 正文信息密度更高。

第三,模型只是链路的一段。 数据格式转换、归一化统计量对齐、推理与控制的频率匹配、断连时的安全行为——这些没有一样属于「AI」,但它们决定了模型能不能真的驱动一台机器人。真正的难点很少在模型里。

想把这条链路的上游补齐,数据是怎么采出来又怎么变成训练集的那篇接着往前看;想把概念底子夯实一点,具身智能到底在解决什么问题可以回头再读一遍。这一卷的其他仓库解读都归在宇树与足式机器人专题下面。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明