机器人
在 Qualcomm Dragonwing IQ-9075 EVK 上一步步搭建一个视觉-语言-动作(VLA)模型,直到它在开发板的 Hexagon NPU 上生成机器人动作。本文完全自包含——你需要的每条命令和每行代码都在这一页上,且每一步都以可核对的输出结尾。 我们使用 Pi0.5,一个 30 亿参数的机器人基础模型:给它相机图像和一条自然英语指令,它就输出关节运动。大约十分钟就能完成第一次 NPU 推理。本页最后三分之一才是有趣的部分——为什么这个模型的四个组成部分最终会以那样的方式分布在硬件上。
本页的每个数字都是在运行 Ubuntu 24.04 Server 的真实 IQ-9075 EVK 上实测的。取自 Qualcomm AI Hub 官方公开性能分析的数字会明确标注,绝不与我们的数据混用。
你最终会得到什么
开始之前
下面每条命令都在开发板上运行,通过 SSH 或串口控制台。不需要 Qualcomm AI Hub 账户。没有云端编译任务。不需要浏览器。
设置一个后文都会引用的工作目录:
1. 安装 ROS 2 Jazzy 和 Qualcomm AI 运行时
先从 ROS 2 开始。以下命令来自软件设置页面:ppa:ubuntu-qcom-iot/qcom-ppa 在 IQ-9075 出厂镜像中已存在,因此不需要添加。重复添加会产生致命错误 E: Conflicting values set for option Trusted。如果遇到这个问题,请列出 /etc/apt/sources.list.d/ 并删除重复项。2. 无需 AI Hub 账户获取 Pi0.5
只有当你要把自己的模型带上 NPU 时,才需要 AI Hub 中按用户分配的云端编译任务。对于已发布的模型则不需要:Qualcomm 为其众多芯片预编译了 QNN 上下文二进制文件(context binary),并通过 AI Hub Models 提供,任何人无需账户即可下载。qualcomm-qcs9075——即 IQ-9075 EVK——正是 Pi0.5 的 default_device,所以获取模型只是一次普通的 HTTPS 下载:
.bin 文件和一个元数据文件,总计 2.9 GB:
量化是混合的:backbone 为 w4a16,视觉编码器和动作专家为 w8a16。
该模型包记录了
SDK build=v2.45.0.260326154327。它可以在 apt 提供的 QAIRT 2.46.0 上原样运行,因此不需要精确匹配 SDK 版本。3. 在 NPU 上运行模型的第一个部分
在写任何代码之前,先用qairt-tools 附带的 qnn-net-run 证明硬件能正常工作。
视觉编码器接受一张 RGB 图像,224×224,通道优先,float32,归一化到 [-1, 1]。生成一张:
2097152 字节——正好是 1 × 256 × 2048 个 float32 值。
4. 弄清你实际需要连接什么
现在你有四个模型,却不知道它们如何连接。不要猜,也不要相信metadata.json 的键顺序——直接问二进制文件本身。qnn-context-binary-utility 会转储上下文二进制文件声明的输入和输出,且顺序正是编译后的图所期望的:
action_expert 有 41 个输入):
action_expert 的输入顺序。 它是 l0, l1, l10, l11 … l17, l2, l3 … l9——按字符串排序,而不是数字排序。产出这些缓存的 backbone 是按 l0, l1, l2 … l17 输出的。如果按生产者顺序连接,你就悄无声息地打乱了 18 层中的 12 层。没有任何报错。模型照常运行。输出的动作是垃圾。
这就是为什么要从二进制文件生成顺序,而不是手动敲出来。
流水线如何组合
一个动作块需要 15 次 NPU 调用:三次视觉推理(每个相机槽位一次)、一次 token 嵌入、一次 backbone prefill,然后是经过动作专家的十次去噪步骤。值得记住的常量:
除了顺序陷阱外还有两处重命名要注意:
token_emb 输出 prefix_emb,但 backbone 称之为 hidden_state;prefix_att_2d 变成了 prefix_att_2d_masks。suffix_sin/suffix_cos 到达动作专家时叫 rope_emb_sin/rope_emb_cos。
机器人的状态去了哪里
这四个图中任何地方都没有状态输入张量。Pi0.5 将本体感觉离散化为 256 个桶,并将其拼接进语言提示词。以下格式复现自上游 openpi 实现:google/paligemma-3b-pt-224 是受限的(未接受许可证时返回 HTTP 401),但完全相同的文件可以从 Google 的 big_vision 存储桶免认证获取——openpi 也是从那里获取的:
8986bb4f423f07f8c7f70d0dbe3526fb2316056c17bae71b1ea975e77a168fc6。
为某个任务生成 token id,并把状态向量折叠进去:
5. 从你自己的代码运行模型
qnn-net-run 是一个测试工具;它读文件、写文件。要串联四个模型,你需要把它们放进同一个进程。完成这件事的 QRB ROS 软件包是 qrb_inference_manager,它的整个 API 只有三个调用。
安装它以及封装它的 ROS 节点:
$WORK/minimal_npu.cpp。这就是完整的程序:
g++,不需要 CMake:
6. 串联四个模型
这部分需要你自己编写,而且大多是记账式的工作。它的形态如下:- 在十次去噪步骤中,除
x_t和time_step外一切都是常量。 把约 34 MB 的 KV 缓存一次性打包进专家的输入缓冲区,之后每步只重写 6404 字节。 - 专家直接返回
x_{t+dt}。 它在内部完成 Euler 更新,因此主机端不需要x += dt * v。
inference_execute() 强制执行的契约,如果总大小不对它会告诉你:
7. 撞上墙:一个 NPU 装不下 Pi0.5
当四个上下文都在一个进程中构建时,你会得到这个:
它也不是一个干净的容量上限:反复的 map/unmap 会使地址空间碎片化,因此相同的总量会因进程之前做过什么而时而成功、时而失败。
明显的临时方案是在每次使用前后创建和销毁上下文。它能用,但很慢——权重加载占大头:
你三分之二的时间不是花在计算上。
8. 解锁第二个 NPU
还记得第 1 步的两个设备节点吗。问题是 QNN 是否允许你指定它们。直接问它——编写$WORK/devices.cpp:
qrb_inference_manager 调用的是 deviceCreate(nullptr, nullptr, …),总是落在设备 0 上。选择设备意味着传给它一个单条目的 QnnDevice_PlatformInfo_t。从源码构建该库并加入这一功能:
token_emb)、多输入张量和 HTP burst 模式,这些都不在 apt 的 1.1.1 版本中。
在 qrb_ros_nn_inference/qrb_inference_manager/src/qnn_inference/qnn_inference.cpp 中找到 create_device(),将 deviceCreate 调用替换为:
QnnInference 和 QrbInferenceManager 上都添加一个 const uint32_t device_id_ = 0; 成员和一个 device_id 构造函数参数,然后构建:
我们为什么这样拆分
两个 NPU 都可用后,分配四个模型有多种方式。最直观的是按大小平衡——每个设备放约 1.4 GB。这并不是最优答案。按实测延迟绘制的 Pi0.5 四个组件在 IQ-9075 两个 Hexagon NPU 上的布局。单 NPU 需付出 2258 ms 的权重换页(总计 3277 ms)。双 NPU 让所有组件保持常驻(1323 ms)。把开销大的阶段重新放到更快的 NPU 上——每设备字节数不变——达到 1111 ms。
vision_encoder 喂给 token_emb,后者喂给 backbone,后者再喂给 action_expert。没有任何部分并发运行。把模型分布到两个 NPU 上并没有并行化任何东西——它只是让每个组件都能保持已映射状态,使整条链不再为权重换入换出付出代价。第一个也是最大的收益就来自这里。
从各组件的开销出发,按每个动作块实测:
这两个 NPU 的速度并不相同。 每个组件在设备 1 上都比设备 0 慢 25–30%。我们反复测量且结果一致;我们没有确认的原因,也不打算猜测。
这种不对称决定了布局。
backbone 和 action_expert 合计约占 1030 ms 计算量中的约 888 ms——所以它们应放在快的设备上,两个开销小的组件放到慢的那个。按开销平衡,而不是按大小:
注意第一行和最后一行放到每个设备上的字节数完全相同。两者之间 212 ms 的差异纯粹来自哪个设备承担了开销大的工作。
最终状态,3 次预热后 12 次迭代的测量:
上下文换页现在恰好为零,整条流水线比它最初的单 NPU 路径快 4 倍——从 4421 ms 降到 1111 ms。
与 AI Hub 公布数字的比较
AI Hub 对每个组件的性能分析是孤立进行的。我们运行的是完整链路,阶段之间有主机端数据搬运,所以我们的数字必然更高。这是一个”同类与非同类”的对比,目的是定位开销所在,而不是宣称胜利:
约 223 ms 的差距主要来自在 CPU 上于各阶段之间搬运张量。仅
backbone 就输出 36 个 KV 张量、共约 34 MB,必须读出并重新打包进专家的输入缓冲区。这是显而易见的下一个优化目标,而 qrb_ros_transport 的 DMA-buf fd 传递就是实现机制——qrb_inference_manager 2.x 已经为此暴露了 inference_execute_dmabuf() 入口。
9. 证明数值正确性
在这里构建”快但错”的系统很容易——被打乱的 KV 缓存会产生自信、看似合理却毫无用处的动作。所以要对照你在第 3 步已经用过的参考实现进行校验。 从你的串联流水线中转储每个阶段的精确扁平输入缓冲区及其输出,用第 4 步得到的图顺序把输入拆回逐张量文件,通过qnn-net-run 重放,然后做 diff:
ref/Result_0/<name>.raw 与流水线对每个张量的输出进行比较。我们的结果逐位一致——四个组件的全部 45 个输出张量,而且在组件被固定到不同 NPU 时依然一致:
libQnnHtp.so;输出与 HTP 后端上的 qnn-net-run 逐位一致;所有组件常驻后上下文创建开销降为零。
封装为 ROS 2 节点
链路跑通之后,ROS 这一层平平无奇,而这正是重点: 有两个值得照抄的决定。推理耗时约 1.1 秒,对 executor 回调来说太长,所以在专用工作线程上运行它。并且丢弃推理期间到达的帧而不是排队——基于陈旧观测行动的 VLA,比以更低频率行动的 VLA 更糟。 随每个动作块一起发布各阶段延迟明细。这没有任何成本,并且意味着你以后做出的任何性能声明都不会与实时测量脱节。输出到底好不好?
延迟和逐位一致性证明了流水线是正确的。它们没有说明这些动作是否有用。在没有机器人的情况下验证这一点,可以重放 LIBERO 数据集(Pi0.5 校准所用的数据)中的一个真实回合——把人类演示者看到的观测原样喂给 NPU,然后把预测的动作块与他们实际的操作进行比较。 状态和动作都使用均值/标准差归一化,统计量随数据集一起发布在meta/stats.json 中。在 20 步 horizon 上对 15 个重规划步骤计分:
MAE / 动作标准差是无量纲的数字:0.145 表示误差约为该数据集中动作自然离散程度的 15%。 预测数据集均值的得分按构造恰为 1.0,因此模型确实在跟踪演示者。夹爪——在 ±1 处实质上是二值的,也是唯一一个对错毫无歧义的维度——匹配到 0.022。
这是在单个回合上的开环 teacher-forced 比较,不是任务成功率。策略从未看到自己动作的后果。这正是下一节要解决的问题。
它真的能完成任务吗?
上一节有个漏洞:决定接下来发生什么的是人类的动作,所以策略从未为自己的动作负责。一个存在细微错误的策略与一个好策略得分相近,因为两者的错误都从不累积。 闭环把人类移出了回路。仿真器渲染机械臂能看到的画面,策略做决策,仿真器执行那个决策,任务自身的目标判定谓词说明它是否成功。 结果发现这一切完全可以在板上运行——这出乎我们的意料,因为我们刚刚确认 Gazebo 无法在这里渲染。差别在于需求中的一个词:
Mesa 的软件光栅化器提供 4.5 compatibility profile。Ogre2 要求 core 于是崩溃;MuJoCo 的经典渲染器则很满意。所以不需要笔记本,不需要跨主机桥接,不需要跨机器 DDS:
渲染是开销大的那一半,所以只在重规划时渲染
在那个软件光栅化器上,一次双相机 256×256 观测耗费 340 ms。一次物理步进耗费 31 ms。NPU 在 1126 ms 内产出五十个动作。 如果每个仿真步都渲染,数字就会彻底倒挂——仿真器的开销是 3B VLA 的 3.2 倍:
所以只在重规划时渲染。这不是取巧——这恰恰是 50 步动作块带给你的东西,因为策略只在规划时才需要观测。它把一个 220 步的回合从 84.8 s 缩短到 15.3 s,并把瓶颈移回你预期中的 NPU 上。
按实测开销绘制的 NPU 动作块生产与仿真器步进消耗对照。一个动作块以 1126 ms 换来 50 个动作;每个仿真步都渲染会让仿真器达到策略开销的 3.2 倍,而只在重规划边界渲染则把它压缩到 18.7 s,让 NPU 重新成为主导。重规划 horizon 是调节旋钮:H=1 每回合耗时 426 s,H=50 耗时 18 s。
--replan-horizon 10 是一个可用的选择,而非调优后的结果。
四个会悄悄毁掉结果的约定
这些都不会抛出异常。搞错任何一个,闭环照样运行、照样渲染、照样报告一个成功率——为零的成功率。每一个都是通过与录制数据集比对而不是读文档确定的:结果
全部十个 LIBERO-10 任务——长程任务套件——每个任务十个初始状态:
这十个初始状态是两组独立的五个——LIBERO 的索引 0–4 和 20–24——有意作为两次独立扫描运行。单独一组无法告诉你恰好选中的状态是不是简单的,而这个套件中难度差异很大:
两个区间重叠得很充分,说明两组一致,将它们合并是合理的。十个任务中有一半以 10/10 解决。真正困难的任务是*“把黄白相间的马克杯放进微波炉并关上门”,得分 4/10——它是两组中唯一都得分很低的任务。“把两个摩卡壶都放到炉子上”*先是 2/5 后是 5/5,这有效地提醒我们五回合样本的分辨能力有多低。
一个闭环回合:'拿起书并把它放进置物架的后隔层',在 252 个仿真步和 26 个动作块内解决。视频下方的条带是 50 动作块——阴影格是下次重规划前将执行的十个动作,琥珀色格是此刻正在执行的动作,带轮廓的其余部分会被丢弃。每一帧都烧录了各阶段 NPU 延迟和加载的后端。
backend libQnnHtp.so 出现在每一帧上,因此 NPU 运行的证据随影像本身传播,而不是在旁边被口头断言。动作条带的存在是为了让丢弃可见——每五十个预测动作中有四十个在下次重规划时被扔掉,这看起来很浪费,直到你意识到正是它让闭环得以运行。
录制这段视频并非免费:额外的帧每对耗费 340 ms,所以测试框架会打印警告,说明带 --save-frames 的运行不是计时测量。视频由对已保存帧的独立离线处理合成,因此合成过程绝不落在被测量的回合内。
有两个细节比标题数字更重要:
十四次失败每一次都恰好跑满 520 步。 没有一次发散、抖动或产生胡乱输出——它们只是在任务中途耗尽了步数预算。失败模式是”对上限来说太慢”,而不是”错误”,这使得步数上限成为结果的一部分而非无关设置。
空策略在同一测试框架上得分为 24 中的 0。 全零动作和均匀随机动作都在每个任务上失败,且目标判定谓词在重置时从不预先满足。没有这项检查,一个高成功率同样可能只是在测量一个坏掉的判定谓词——而且从外部看完全一样。
这是一个简化的评测协议。 LIBERO 的完整评测是 4 个套件 × 10 个任务 × 50 个初始状态。这里是一个套件、每任务十个初始状态,因此 86% 带有约 ±7 个百分点的区间,不是基准测试数字。两组结果一致提高了”采样状态并非病态”的置信度,但可用状态的百分之二十仍然只是一个样本。我们也不与已发表的 pi0 级结果做比较——那需要它们的重规划 horizon 和步数上限与我们一致,而这一点我们尚未核实。
诚实的局限性
- 任务成功率基于简化协议。 完整的 LIBERO 评测是 4 个套件 × 10 个任务 × 50 个回合,在单块板上负担不起。此处的任何成功率都注明其回合数和置信区间,绝不作为套件级基准数字呈现。
- Gazebo 无法在这块板上渲染,但 MuJoCo 可以。
gz-harmonic在 arm64 上安装正常,但 Ogre2 需要桌面 OpenGL 3.3 core,而这里的 Adreno 驱动只暴露 OpenGL ES;Ogre v1 会中止,强制使用 Mesa llvmpipe 则在Ogre2RenderEngine::LoadImpl内段错误。LIBERO 的 MuJoCo 渲染器可由同一 llvmpipe 提供的 OpenGL 4.5 compatibility profile 满足,这就是闭环演示能完全在板上运行、无需笔记本参与的原因。渲染仍然是软件的:没有硬件路径,因为/dev/dri/renderD128是显示控制器而不是 GPU,所以 Mesa 的freedreno无法绑定它,而zink被两个 Vulkan ICD 都拒绝。 - 动作语义与具体机器人形态绑定。 已发布的导出面向 LIBERO 的 7 自由度 Franka Panda。其输出对于另一种机械臂不是有效的关节指令。
- 归一化统计量是外部的。 原始关节值会产生无意义的提示词;你需要策略训练时使用的统计量。
- 没有功耗或热学测量,也没有持续负载的浸泡测试。所有数字来自热学上未受压的板上的短时运行。
- 从未用实时相机测量过。 输入是合成的或从磁盘重放的。
- NPU 是独占资源。 两个进程各自映射完整模型包会争用同一 CDSP 预算,双双变慢。测量时确保 NPU 上没有其他任务。
- 我们采用的每设备约
~1600 MiB上限是经验性且保守的,不是文档记载的限制。
这对平台意味着什么
100 TOPS 使 30 亿参数的视觉-语言-动作模型变得可能。这次实践表明它同样是实用的——达到 4.3–4.5 倍实时——但前提是把两个 NPU 当作需要调度的资源,而不是一个加速器。朴素路径与知情路径之间的差距是 4 倍,而且朴素路径还会悄无声息地吞掉一个致命的映射错误。 剩余的提升空间在 CPU 而不是 NPU 上:每块 223 ms 的主机端张量搬运,零拷贝 DMA-buf 应能大幅消除它。相关内容
- NPU 上的深度估计 — 这一模式的单模型版本,每根”线”都清晰可见。
qrb_ros_nn_inference— 对于普通的单输入单输出模型,这个通用节点连那三个 API 调用都省了。qrb_ros_transport— DMA-buf fd 传递,解决主机端搬运开销的方案。- 上下文二进制文件 —
.bin是什么以及它是如何产生的。 - 软件设置 — 本页起点的 ROS 2 Jazzy 安装。

