本文复现仓库:GitHub - D-Robotics/rdk_LeRobot_tools at s600 · GitHub
机器人操作策略过去大多围绕单一机器人和单一任务训练:模型根据相机图像与关节状态预测动作,任务或硬件发生变化后通常需要重新采集数据。视觉-语言-动作模型(Vision-Language-Action model,VLA)则尝试把视觉理解、语言指令和机器人控制统一起来,使一个模型能够根据自然语言任务处理不同场景,并复用从大规模图文数据和多机器人数据中获得的表示能力。
π0 是 Physical Intelligence 提出的一种 flow-matching VLA。它以 PaliGemma 视觉语言模型为基础,引入专门处理机器人状态和连续动作的 Action Expert,并通过跨本体预训练与高质量 post-training 获得通用能力和下游任务执行能力。与将动作离散为文本 token 的自回归 VLA 不同,π0 从随机噪声出发迭代生成整段连续动作,更适合高频和精细操作。
本文关注 π0 的下游适配与端侧部署,完整流程从 LeRobot 兼容的 π0 base checkpoint 开始,使用 SO100 双相机数据进行全参数 post-training,建立服务器 BF16 基线,再完成 SigLIP、PaliGemma 与 Action Expert 的分段量化、HBM 编译和 S600 真机推理。
机械臂接收自然语言指令“Place the RDK camera box on top of the black MCU box. ”并执行
一、π0论文解读
部署之前先把论文中的模型关系讲清楚。因为后面所有量化边界、Tensor shape 和 KV Cache,都是从这一结构推导出来。
VLA 的问题定义
传统模仿学习通常解决一个很具体的问题:给定相机图像和关节状态,预测下一步动作。
(image, state) → action
这种策略可以在固定场景里做得很好,但任务一变、物体一换,往往就需要重新定义数据和重新训练。VLM 则走向另一个极端:它能理解图像和语言,却通常只会输出文本,不能直接给舵机发送连续关节目标。
VLA,也就是 Vision-Language-Action model,把两者接在了一起:
多路图像 + 语言指令 + 机器人本体状态
↓
VLA
↓
一段连续机器人动作
可以把三类模型粗略对比成:
| 模型 | 主要输入 | 主要输出 | 擅长解决的问题 |
|---|---|---|---|
| 视觉策略 | 图像、state | 单步或短动作 | 固定任务的闭环控制 |
| VLM | 图像、语言 | 文本 token | 语义理解和视觉问答 |
| VLA | 图像、语言、state | 连续动作 chunk | 根据自然语言执行机器人任务 |
这里的关键不是在 VLM 后面简单接一个 6 维回归头。机器人动作具有连续、高频和多解的特点:同一个抓取任务可能存在多条合理轨迹,而且相邻控制步必须足够平滑。π0 因此选择一次生成一个 action chunk,并用 flow matching 表达连续动作分布。
用论文里的符号,一次观测是:
o_t = [I_t^1, ..., I_t^n, language, q_t]
I_t^1...I_t^n是多路 RGB 图像。language是任务指令。q_t是机器人当前 proprioceptive state,也就是关节位置等本体状态。
模型预测的不是一个动作,而是未来 H 步动作块:
A_t = [a_t, a_t+1, ..., a_t+H-1]
π0 论文使用 H=50。这意味着一次模型推理可以给出 50 个控制目标,控制器不必每发送一步都重新运行完整 VLM。
图:π0 把视觉、语言与本体状态汇入同一策略,再输出连续动作块。
从单步动作扩展到带语言条件的动作块之后,问题就不再只是训练一个回归网络。模型既要继承 VLM 的视觉语义,又要为连续控制增加新的状态与动作表示。π0 的设计正是围绕这两个目标展开。
预训练背景与模型架构
π0 的论文标题是 π0: A Vision-Language-Action Flow Model for General Robot Control(论文主页)。作者希望构建的是 generalist robot policy,也就是机器人基础模型,而不是某台机械臂上的单任务网络。
论文的整体路线有四个关键词:
- VLM initialization:从已经具备互联网图文知识的 PaliGemma 出发。
- Cross-embodiment data:混合不同机器人、不同动作空间的数据训练同一个模型。
- Flow matching action expert:不用文本式离散 action token,而是生成高精度连续动作。
- Pre-training + post-training:先用大而杂的数据获得通用能力,再用高质量任务数据把行为收敛到稳定策略。
论文报告的预训练数据超过 10,000 小时,包含自有的 7 种机器人配置、68 类任务,以及来自 22 种机器人的 OXE 数据。它想证明的是:来自其他任务和其他机器人的经验,也能成为下游任务的初始化。
图源:Physical Intelligence,π0: A Vision-Language-Action Flow Model for General Robot Control,Figure 1
这张图最值得注意的不是任务数量,而是中间的训练逻辑:大规模预训练负责给模型提供广泛的语义和操作先验,高质量 post-training 数据负责告诉模型“这个具体任务应该怎样稳定地做”。
我们采集的 SO100 数据属于后者。100 个 episode 不可能重新造出论文中的通用 π0,但可以把已有 base model 适配到固定相机、固定机械臂和固定摆放任务。
VLM Backbone 与 Action Expert
论文从逻辑上把 π0 分成两部分:
较大的 VLM backbone
+
较小的 action expert
VLM backbone 初始化自 PaliGemma。论文 Figure 3 将它标为约 3B 参数,其中包含 SigLIP 图像编码器和 Gemma 语言模型;action expert 约 300M 参数,总体约 3.3B。
图源:Physical Intelligence,π0 论文 Figure 3。
这一点很重要:论文中的 action expert 不是独立运行、完全看不到 VLM 内部表示的“动作头”。π0 被实现为一个具有两套权重的 transformer:
- 图像和语言 token 路由到较大的 PaliGemma/VLM 权重。
- robot state 和 noisy action token 路由到较小的 action expert 权重。
- 两套权重主要通过 transformer self-attention 发生信息交互。
为了让 action expert 的 10 次迭代更快,它比 VLM 小很多。论文附录给出的 VLM width 是 2048,action expert width 是 1024,action expert 的 MLP dimension 是 4096。
输入、Token 路由与注意力
把细节暂时压缩,一次 π0 推理可以理解成:
RGB images
↓ SigLIP / ViT
visual embeddings
┐
├─ language tokens ─→ PaliGemma VLM context
│ ↓ KV/context
└─ state q_t ─────────→ Action Expert
↑
noisy action chunk
↓ 10 次 flow matching
future actions [50,d]
论文使用 blockwise causal attention,把输入分为三个 block:
Block 1: [images, language]
Block 2: [robot state]
Block 3: [50 noisy action tokens]
每个 block 内部是双向 attention,但前面的 block 不能看未来 block:
- 图像和语言不去看 robot state/action,尽量保持 PaliGemma 预训练时的输入分布。
- state 不看 noisy actions,因此它对应的 KV 在去噪过程中可以缓存。
- noisy action 可以看前面的全部条件,也可以在 50 个 action token 之间互相注意。
这就是后面端侧部署里出现 attention mask、position IDs 和 36 组 KV 的根源。它们并非工具链附加的无关输入,而是在保持论文定义的 Token 连接关系。
到这里可以得到一个关键结论:论文中的 π0 是 VLM Backbone 与 Action Expert 共同参与注意力计算的模型。S600 上看到的三份 HBM 是为了编译和调度形成的工程切分,不是三个彼此独立的策略。
Flow Matching 与动作块生成
如果直接用 MSE 回归一条动作轨迹,模型容易把多条合理轨迹平均成一条缺乏可执行性的轨迹。π0 使用 conditional flow matching,把动作生成写成一个从随机噪声逐渐流向真实动作分布的过程。
训练时,先从真实动作块 A 和高斯噪声 ε 之间取一个中间点:
A^τ = τA + (1-τ)ε
然后让 action expert 预测此时应该沿哪个方向移动,目标向量是:
u = A - ε
模型优化的是预测向量场与目标向量场之间的平方误差。推理时没有真实动作 A,所以从纯随机噪声开始,用 Euler integration 连续更新:
A^0 ~ N(0,I)
A^(τ+δ) = A^τ + δ · vθ(A^τ, observation)
论文采用 10 个 integration steps,也就是 δ=0.1:
随机 50-step noise
↓ step 0
较粗的动作形状
↓ step 1...8
逐渐符合图像、语言和当前 state
↓ step 9
最终 50-step continuous action chunk
所以后文看到 Expert HBM 连续运行 10 次,不是错误地重复推理同一个模型,而是在数值积分 flow matching 学到的向量场。
图:Action Expert 从整段随机噪声出发,经过 10 次迭代得到 [50,6] 连续动作。
与自回归动作生成的差异
一些 VLA 会把动作离散化为 token,再像语言模型一样一个 token 接一个 token 地生成。π0 选择 flow matching,主要面向以下需求:
- 直接表示连续动作,不需要先把角度量化成离散词表。
- 一次联合生成整段 action chunk,50 个动作 token 可以互相 attention。
- 能表达同一观测下多种合理动作模式。
- 适合高频、精细操作,而不是等几十个 action token 逐个自回归解码。
这也解释了为什么“只在开头算一次语言,后面永远绕过 PaliGemma”并不完全成立。语言 embedding 可以固定,但 PaliGemma context 还融合了当前图像;图像变了,提供给 action expert 的视觉 KV 也应该更新。同一个动作块内部的 10 次去噪可以复用 KV,跨动作块则不能盲目复用旧视觉 KV。
Flow Matching 解释了为什么 Expert 必须连续运行 10 次,也解释了为什么 Expert 的数值误差会被反复带入下一步。这个性质将在下一章直接决定 Expert 的量化策略。
从论文配置映射到 SO100 与 S600
论文在 RTX 4090 上报告三相机完整推理约 73 ms,其中包括图像编码、observation forward 和 10 次 action forward。对于 50 Hz 运动的机械臂,论文不是等到 50 步全部执行完,而是执行 25 步后重新推理下一块;作者还提到 temporal ensembling 在早期实验中反而降低了效果,因此采用 open-loop action chunks。
我们当前更关注量化精度定位,选择了更保守的严格同步基线:
| 项目 | π0 论文示例 | 当前 SO100/S600 |
|---|---|---|
| 相机 | 每台机器人 2~3 路 | front + side + 1 masked empty |
| 控制频率 | 20 Hz 或 50 Hz | 30 Hz,与采集数据一致 |
| action horizon | 50 | 50 |
| flow steps | 10 | 10 |
| 重推理时机 | 执行 16 或 25 步后 | 严格同步执行完整 50 步后 |
| 推理设备 | RTX 4090 | S600 BPU |
| 单次整链耗时 | 论文约 73 ms | 当前约 1.24 s |
这不是说当前执行的策略已经最优。它的意义是先让每个 action chunk 对应一组明确的新观测,方便比较服务器 BF16 与板端 HBM。等精度稳定后,再讨论 receding horizon、提前重规划或更严谨的异步队列。
面向 S600 的三段式计算图
论文是“VLM backbone + action expert”的逻辑结构;为了在 S600 上编译和调度,我们把推理图工程化拆成三段:
SigLIP HBM
↓ 生成每路视觉 embedding
PaliGemma HBM
↓ 生成融合图像与 prompt 的 prefix KV
Expert HBM × 10
↓
[50,6] SO100 actions
这是端侧编译与 runtime 的切分,不应该反过来误解成论文原本就是三个互不相干的网络。后面的量化也必须按这条真实依赖链逐段校准:下游模型要看到上游 HBM 的真实输出,而不是只看 PyTorch 浮点中间量。
SO100 双相机任务配置
我们的任务是:
Place the RDK camera box on top of the black MCU box.
SO100 follower 的真实 state/action 都是 6 维绝对角度:
shoulder_pan
shoulder_lift
elbow_flex
wrist_flex
wrist_roll
gripper
模型内部 state/action tensor 宽度保持 32,真实 6 维写在前面,其余维度补零;最终只反归一化前 6 维。
真实相机只有两路,但编译图保留三个物理槽:
slot 0: front,真实图像
slot 1: side,真实图像
slot 2: empty,空图像并在 attention 中 mask
三个物理槽,不等于三路真实相机。
最终视觉布局是:
每路 SigLIP 输出:256 个视觉 token
物理视觉 token:3 × 256 = 768
有效视觉 token:2 × 256 = 512
语言容量:48 token
物理 prefix tensor:768 + 48 = 816
物理 HBM tensor 保持 816 长度,但语言的逻辑 position 必须接在 512 个有效视觉 token 后,第三槽和未使用 prompt capacity 通过 mask 失效。这个“物理 shape”和“逻辑有效位置”的区别,后面会直接影响 RoPE、attention mask 和 Expert KV。
最初的数据只有 front。增加 side 后,不能只在 deployment JSON 里把 real_camera_num 从 1 改成 2,因为有效视觉 token、position、mask 和 Expert 收到的 KV 分布都变了。因此最终重新采集双相机数据,并同时解冻 SigLIP、PaliGemma 和 Expert。
LeRobot 的 policy.type=pi0 是 OpenPI 实现的 PyTorch 移植。训练和服务器 BF16 基线使用 LeRobot policy,不是直接运行原始 OpenPI JAX 服务。本文假设已经准备好 LeRobot 兼容的 π0 base checkpoint,并放在本地 pi0_base。
论文结构与本项目接口现在已经对应起来:两路真实图像先经过 SigLIP,PaliGemma 生成视觉语言 KV,Expert 再结合 6 维 SO100 状态生成 50 步动作。下一步需要回答的是,为什么这三段不能使用同一套 INT8 配置。
二、量化原理
π0 的量化难点不在于模型数量,而在于它既是级联系统,又包含 10 步迭代。上游误差会成为下游输入,Expert 的输出还会回灌到下一次 Flow Matching。因此,量化策略必须从数据流和误差传播来设计。
图:越靠近 position、attention、KV 与最终动作输出,越需要保留高精度。
模型结构决定精度分配
π0 不能把所有算子统一设成 W8A8,再只用“是否成功编译”判断量化是否可用。
根本原因是这条链路同时具有两个特点:
SigLIP → PaliGemma → Expert
级联依赖
Expert step 0 → step 1 → ... → step 9
迭代依赖
上游产生的误差会成为下游的真实输入,Expert 自己的误差还会在 10 次 flow matching 中反复反馈。三个部分对数值误差的敏感位置也不同:
| 部分 | 在 π0 中负责什么 | 最危险的量化误差 | 后文采用的方向 |
|---|---|---|---|
| SigLIP | 把图像变成 256 个视觉 token | position embedding 偏移后,物体空间位置被扭曲 | patch embedding 可 quant8,position embedding 保留 FP16,其余关键线性路径用 dynamic |
| PaliGemma | 融合图像、prompt,并生成 36 组 prefix KV | RoPE、QK score、attention 累加误差会污染整段 KV | attention matmul 使用 fixed16、FP32 accumulation、FP16 output,线性层采用 dynamic |
| Action Expert | 用 state、KV 和 noise 迭代生成 50 步动作 | 单步小误差经过 10 次反馈,并在反归一化后放大成关节角偏移 | transformer、projection、attention 和 output linear 均采用 dynamic,输出 tensor 保持 FP16 |
这张图是本文根据最终 S600 推理链路绘制的工程示意图。上半部分是误差传播路径,下半部分是必须与之对应的真实 HBM 校准路径。
为什么 SigLIP 的位置误差特别致命
图像 patch 数值有一点误差,通常只是视觉 embedding 轻微变化;但 position embedding 告诉模型“这个 patch 在画面中的哪里”。抓取依赖的恰好是空间位置,因此位置向量受损后,常见表现不是模型崩溃,而是落点稳定地偏前、偏后或偏向一侧。
为什么 PaliGemma 的 attention 不能太激进
PaliGemma 要在长 prefix 上融合视觉和语言。本项目物理 prefix 长度为 816,最终还要输出 36 组 KV。Q/K、RoPE 或 attention score 的微小偏差,会同时改变很多 token 的加权关系;这些 KV 随后又被 Expert 的 50 个 action token 使用,所以误差覆盖面比普通分类头大得多。
为什么 Expert 比较小,反而更值得保精度
Expert 每次只输出一个新的向量场更新,但这个输出会立刻复制回输入,继续参加下一步去噪。它不是 10 次彼此独立的预测,而是一个数值迭代过程:
第 0 步误差
↓ 成为第 1 步输入的一部分
第 1 步继续计算
↓
...
↓
第 9 步输出再经过 action_std/action_mean 反归一化
归一化空间里几百分之一的偏差,换回 degree 后就可能成为肉眼可见的关节偏移。Expert 只有约 300M 参数,比 PaliGemma 小得多,因此这里用模型大小和速度换精度通常更划算。
为什么下一级必须用上一级真实 HBM 输出校准
如果 PaliGemma 在校准时看的是 float SigLIP,而部署时看的是 quantized SigLIP HBM,它学到的激活范围并不对应真实运行分布。Expert 也一样:用 float PaliGemma KV 校准,无法覆盖板端 PaliGemma HBM 已经带入的误差。
因此最终校准顺序必须与端侧推理顺序一致:
真实双相机图片
↓
SigLIP HBM 实际输出
↓ 用它校准 PaliGemma
PaliGemma HBM 实际 36 组 KV
↓ 用它校准 Expert
Expert HBM × 10
这就是后文采用混合精度和“链路感知校准”的结构原因。具体的 W8A8、W8A16、dynamic、fixed16 含义以及完整命令,会在量化章节展开。
从 PyTorch Checkpoint 到 S600 HBM
服务器上的 Pi0 checkpoint 大约包含 40 亿参数。S600 BPU 不是 CUDA GPU,不能直接加载 PyTorch BF16 权重。
模型需要经过 S600 工具链:
PyTorch checkpoint
↓ export
BC / MLIR
↓ convert + compile
HBO
↓ link
HBM
HBM 不只是“压缩后的权重文件”。它同时固定了:
- 输入输出 tensor shape。
- attention mask 和 position IDs 的计算图。
- 每层的量化策略。
- BPU march 和编译调度。
- 部分固定 prompt 输入形式。
因此不能把 HBM 当成普通 checkpoint,靠改 JSON 就改变相机数或 token 布局。
HBM 固定了图结构以后,下一步才是选择每条计算路径使用哪一种精度。这里的 W8A8、W8A16、Dynamic 和 Fixed16 不是四个模型版本,而是可以分配到不同算子上的执行模式。
前面的结构分析已经说明 π0 不能统一量化。本节先把后面会反复出现的几种工具链精度模式讲清楚,再进入三段模型的具体配置。
W8A8
Weight 8-bit
Activation 8-bit
它最省空间、速度通常也快,但对视觉位置、attention score 和动作回归都比较激进。
W8A16
Weight 8-bit
Activation 16-bit
激活保留更高精度,通常比 W8A8 更适合回归任务,但权重误差仍然存在。
Dynamic
本文中的 dynamic 是 S600 SDK/Leap 导出脚本中的精度模式,表示不强制把对应路径固定成统一的 W8A8 或 W8A16,而是保留工具链支持的动态量化执行路径。
它不能简单等同于 PyTorch 的 torch.quantization.quantize_dynamic()。
Fixed16 Attention
最终 PaliGemma attention matmul 使用:
Q/K:16×16 fixed
S/V:16×16 fixed
accumulation:FP32
output:FP16
这样做的代价是模型更大,但能明显降低 attention 和 RoPE 相关误差。
链路感知校准
前面说的“级联误差”在第一次量化中真实发生了。问题不只是某一层 INT8 误差偏大,而是校准时使用的中间分布与板端实际分布不一致。
最开始尝试过“每个模型单独用浮点校准数据量化”的思路:
真实图片 → float SigLIP → 校准 PaliGemma
float PaliGemma KV → 校准 Expert
但部署时实际链路是:
真实图片 → quantized SigLIP HBM → quantized PaliGemma HBM → quantized Expert HBM
两个分布不一样。
如果量化误差在前级产生,下游模型在校准时却从未见过这种误差,误差就会沿链路放大:
图像位置误差
↓
SigLIP token 偏移
↓
PaliGemma attention/KV 偏移
↓
Expert 动作偏移
↓
夹爪反复开合、关节试探或 chunk 边界跳变
最终采用的是链路感知校准:后一级模型直接使用前一级真实 HBM 的输出做校准。
因此,完整量化不能在服务器上把三个模型各自独立转换后直接拼接。正确顺序必须与端侧推理一致:先量化 SigLIP,用它的真实 HBM 输出校准 PaliGemma;再运行 PaliGemma HBM,使用真实 KV 校准 Expert。理解这一点以后,下面的实操步骤就不再是孤立的命令,而是一条有前后依赖的流水线。
三、量化实操
量化的起点不是工具链,而是一个已经被 BF16 真机验证过的任务模型。本章先得到可靠的 SO100 Checkpoint,再按上一章的依赖顺序生成三份 HBM。
实验平台与软件版本
训练服务器:NVIDIA RTX 4090
训练框架:Hugging Face LeRobot v0.5.2
训练精度:BF16
板端:RDK S600
板端工具链:D-Robotics LLM S600 SDK 1.0.2
BPU march:nash-p
机器人:LeRobot SO100 follower + SO100 leader
相机:两路 USB UVC camera
代码位于 rdk_LeRobot_tools 的 s600 分支,Pi0 的正式实现统一放在 models/pi0/。板端 LeRobot Python 必须使用已经安装依赖的虚拟环境:
/home/sunrise/lerobot/.venv/bin/python
直接使用系统 python3,常见结果是缺少 torch、draccus 或 LeRobot 模块。
数据采集与全参数 Post-training
本项目增加了第二路相机,因此不能只训练 Expert。视觉输入域、有效 Token 数和后续 KV 分布都发生了变化,数据采集、输入 Schema 与训练冻结策略必须一起调整。
硬件连接与设备映射
本次设备映射为:
SO100 follower:/dev/ttyACM0
SO100 leader: /dev/ttyACM1
front 相机: /dev/video0
side 相机: /dev/video2
设备号不是永久固定的。每次采集前都应该检查:
ls -l /dev/ttyACM* /dev/serial/by-id/*
v4l2-ctl --list-devices
机械臂标定文件(此处应该替换成你自己标定过的):
/root/rdk_LeRobot_tools/models/pi0/calibration/robots/so_follower/so100_follower.json
/root/rdk_LeRobot_tools/models/pi0/calibration/teleoperators/so_leader/so100_leader.json
标定文件与具体机械臂绑定,不应该复制给另一套硬件直接使用。
数据录制命令
本次训练集配置:
100 episodes
每个 episode 10 秒
reset 5 秒
30 FPS
双相机 640×480 MJPG
max_relative_target=20
完整命令:
cd /home/sunrise/lerobot
.venv/bin/lerobot-record \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=so100_follower \
--robot.calibration_dir=/root/rdk_LeRobot_tools/models/pi0/calibration/robots/so_follower \
--robot.max_relative_target=20 \
--robot.cameras='{front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30, warmup_s: 10, fourcc: MJPG}, side: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30, warmup_s: 10, fourcc: MJPG}}' \
--teleop.type=so100_leader \
--teleop.port=/dev/ttyACM1 \
--teleop.id=so100_leader \
--teleop.calibration_dir=/root/rdk_LeRobot_tools/models/pi0/calibration/teleoperators/so_leader \
--display_data=false \
--play_sounds=false \
--dataset.fps=30 \
--dataset.repo_id=local/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--dataset.root=/root/rdk_LeRobot_tools/models/pi0/datasets/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--dataset.num_episodes=100 \
--dataset.single_task='Place the RDK camera box on top of the black MCU box.' \
--dataset.episode_time_s=10 \
--dataset.reset_time_s=5 \
--dataset.video=true \
--dataset.push_to_hub=false \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2
数据质量检查
Pi0 学到的是动作分布,不只是“最后有没有抓成功”。同一任务的数据最好保持:
- 相机位置和焦距固定。
- RDK camera box 与黑色 MCU box 的初始分布合理。
- 动作尽量连贯,不要大量犹豫、反复开合夹爪。
- 失败 episode 要么删除,要么明确作为恢复行为保留,不能无差别混入。
- reset 后的初始状态要覆盖实际部署时可能出现的位置。
之前一版数据虽然数量不少,但动作风格混乱。服务器浮点模型也出现试探性下探、夹爪反复开合,最后证明这部分并不是量化能够修复的问题。
数据质量检查通过后,才进入全参数 Post-training。这里解冻 SigLIP、PaliGemma 和 Expert,是为了让新增 side 视角真正进入视觉表示,而不是把适配压力全部留给最后的动作网络。
可训练模块与冻结策略
只训练 Expert 的优点是显存占用低、训练快。但本次任务增加了第二路相机,并且相机视角与 Pi0 基座数据不同。
如果冻结 SigLIP 和 PaliGemma,Expert 只能被迫适应一套没有针对当前相机域优化的视觉表示。它可能在训练 loss 上下降,但对物体位置、深度和遮挡的使用仍然不稳定。
最终使用:
freeze_vision_encoder=false
freeze_language_model=false
train_expert_only=false
也就是三段全部训练。最终可训练参数约 40.28 亿。
三相机槽输入 Schema
训练时使用以下映射:
dataset front → observation.images.base_0_rgb
dataset side → observation.images.left_wrist_0_rgb
empty slot → observation.images.right_wrist_0_rgb
关键配置:
policy.input_features =
base_0_rgb
left_wrist_0_rgb
right_wrist_0_rgb
observation.state[32]
rename_map =
front → base_0_rgb
side → left_wrist_0_rgb
训练数据的真实 state/action 只有 6 维,Pi0 内部 tensor 保持 32 维;未使用部分补零。部署后只反归一化并取前 6 维发送给 SO100。
脚本中的 policy.empty_cameras=0 表示不再额外追加第四、第五个空槽。第三槽已经通过声明的 right_wrist_0_rgb feature 保留;数据集中没有对应真实相机,预处理阶段按缺失相机处理为空输入。
训练配置与启动命令
先把数据集同步到训练服务器,然后执行仓库中的训练脚本:
cd /path/to/lerobot/rdk_LeRobot_tools
git checkout s600
LEROBOT_ROOT=/path/to/lerobot \
DATASET_ROOT=/path/to/datasets/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
BASE_MODEL=/path/to/pi0_base \
OUTPUT_DIR=/path/to/outputs/pi0_so100_2cam_full_b1_15k \
STEPS=15000 \
BATCH_SIZE=1 \
WANDB_ENABLE=true \
WANDB_ENTITY=<your_wandb_entity> \
bash models/pi0/tools/train_pi0_full_v5_2cam_100ep.sh
这份脚本的核心参数是:
BF16
batch size 1
15,000 steps
gradient checkpointing
learning rate 1e-5
1,000 warmup steps
cosine decay
memory-efficient Adafactor
最终 checkpoint 结构示例:
outputs/train/<run_name>/checkpoints/015000/pretrained_model
训练监控与结果判断
训练时建议同时看:
- loss 曲线是否持续下降。
- learning rate 是否按 warmup 和 cosine schedule 变化。
- gradient norm 是否持续爆炸。
- GPU 显存和吞吐是否稳定。
- checkpoint 的真实推理视频,而不是只看 loss。
本次最终记录 loss 约为 0.077,但这个数字不能单独代表抓取成功率。
服务器 BF16 基线与真实校准集
浮点基线的验证作用
如果直接把模型量化到 S600,真机效果不好时会同时存在四类嫌疑:
数据问题
训练问题
量化问题
板端控制问题
先在 GPU 服务器运行 BF16 checkpoint,可以把问题拆开:
服务器 BF16 也差
→ 优先检查数据和训练
服务器 BF16 正常,S600 HBM 差
→ 优先检查量化、position/mask、前后处理
两者离线动作都正常,真机仍异常
→ 优先检查控制频率、标定、PID、相机位置和异步策略
BF16 推理服务与板端客户端
在 GPU 服务器:
HF_HUB_OFFLINE=1 \
TRANSFORMERS_OFFLINE=1 \
HF_DATASETS_OFFLINE=1 \
conda run --no-capture-output -n lerobot \
python -u models/pi0/pi0_torch_server.py \
--checkpoint /path/to/checkpoints/015000/pretrained_model \
--host 0.0.0.0 \
--port 31001 \
--device cuda \
--dtype bfloat16 \
--camera-keys front side \
--token-file /secure/path/pi0_token
板端先做只读请求,不发送动作:
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u pi0_remote_pipeline.py \
--server-url http://<GPU_SERVER_IP>:31001 \
--token-file /secure/path/pi0_token \
--robot-port /dev/ttyACM0 \
--camera /dev/video0 \
--side-camera /dev/video2 \
--max-chunks 1 \
--prefetch-remaining-steps 0
不加 --execute 时只读取机械臂状态和相机,不调用 send_action()。
浮点基线确认正常以后,量化结果才有明确的参考对象。
只有 BF16 版本在相同相机、相同标定和同步控制下表现正常,量化误差才有可比较的基准。确认这一点后,从训练数据中抽取覆盖完整任务阶段的真实校准样本。
真实任务校准集
从训练数据中按 episode 和阶段均匀抽取 50 条样本:
conda run --no-capture-output -n lerobot \
python models/pi0/tools/prepare_pi0_calibration_v5_2cam.py \
--dataset-root /path/to/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--checkpoint /path/to/checkpoints/015000/pretrained_model \
--output-dir /path/to/calibration/pi0_full_v5_2cam_100ep_015000_real50 \
--repo-id local/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--samples 50 \
--camera-keys front side \
--camera-slots 3 \
--task 'Place the RDK camera box on top of the black MCU box.'
校准样本不应该只取 episode 开头。抓取任务不同阶段的激活分布差别很大:
接近物体
夹爪闭合
抬起
移动
放下
如果校准集只覆盖静止初始帧,动作阶段的激活范围仍可能溢出或被压缩。
所有量化步骤都设置:
export PI0_VALID_CAMERA_SLOTS=2
它告诉 position/mask patch:三个物理槽中只有两路真实相机有效。
SigLIP 与 PaliGemma 的级联量化
SigLIP:保护空间位置并采集真实视觉特征
SigLIP 对应整条链路的第一个敏感点:视觉 patch 可以适当压缩,但负责空间定位的 position embedding 需要优先保护。
最终 SigLIP 策略:
patch embedding:quant8
position embedding:FP16
attention linear:dynamic
MLP linear:dynamic
attention matmul:dynamic
projector:dynamic
LayerNorm:standard
工具链容器中执行:
export PI0_VALID_CAMERA_SLOTS=2
python3 models/pi0/tools/quantize_siglip_real_calib.py \
--model-dir /host/gemma/input/pi0_full_v5_2cam_100ep_015000 \
--calibration-dir /host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--output-dir /host/gemma/output/siglip_positionfp16_dynamic \
--device cuda:0 \
--vision-tokens-num 256 \
--valid-camera-slots 2 \
--max-samples 50 \
--jobs 20 \
--march nash-p \
--max-l2m-size 0 \
--patch-embedding-mode quant8 \
--position-embedding-mode fp16 \
--attention-linear-mode dynamic \
--mlp-linear-mode dynamic \
--attention-matmul-mode dynamic \
--projector-linear-mode dynamic \
--layernorm-mode standard
Position Embedding 的空间敏感性
SigLIP 的图像 patch 本身不包含“我在图像哪个位置”。位置向量负责告诉模型每个视觉 token 对应的空间位置。
如果 position embedding 量化误差过大,模型看到的不是简单的颜色误差,而是空间关系被扰动。对于抓取任务,这会直接表现为落点偏前、偏后或左右不稳定。
我们最终保留 position embedding FP16,而 patch embedding 仍使用 quant8,在模型大小和空间精度之间折中。
SigLIP HBM 编译完成后,先不要继续量化 PaliGemma。应把它放到 S600 上运行真实双相机样本,导出 PaliGemma 实际会收到的 FP16 视觉特征。
SigLIP 编译完成后,不直接用服务器 float SigLIP 输出校准 PaliGemma。
正确流程是:
- 把 SigLIP HBM 上传到 S600。
- 把 50 条真实校准图片上传到 S600。
- 在 S600 上运行 SigLIP HBM。
- 保存实际
paligemma_vision_fp16.bin。 - 把这些 HBM 输出传回量化服务器。
板端终端 A:
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u dump_siglip_hbm_calibration.py \
--calibration-dir calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--engine-dump-dir /root/pi0_calibration/engine_siglip_real50 \
--output-dir calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_hbm_real50 \
--prompt 'Place the RDK camera box on top of the black MCU box.'
板端终端 B:
cd /root/rdk_LeRobot_tools/models/pi0
PI0_STANDALONE_DUMP_DIR=/root/pi0_calibration/engine_siglip_real50 \
./run_pi0_standalone_config.sh configs/deployments/<temporary_config>.json
此时 PaliGemma 校准看到的视觉 embedding,已经包含 SigLIP HBM 的真实量化误差。
PaliGemma:使用真实 SigLIP HBM 输出
PaliGemma 对应第二个敏感点。PaliGemma 会把 816 长度 prefix 融合成 36 组 KV,因此 attention 数值误差会扩散到整个 Expert 条件,而不是只影响一个分类结果。
最终 PaliGemma 策略:
attention linear:dynamic
MLP linear:dynamic
MLP down projection:dynamic
attention matmul:fixed16
attention accumulation:FP32
attention output:FP16
18 layers
量化命令:
export PI0_VALID_CAMERA_SLOTS=2
python3 models/pi0/tools/quantize_paligemma_real_calib.py \
--model-dir /host/gemma/input/pi0_full_v5_2cam_100ep_015000 \
--calibration-dir /host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--vision-embeddings-dir /host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_hbm_real50 \
--output-dir /host/gemma/output/paligemma_siglip_hbmcalib_fixed16/paligemma \
--device cuda:0 \
--vision-tokens-num 256 \
--max-samples 50 \
--num-hidden-layers 18 \
--jobs 20 \
--march nash-p \
--compile-opt 2 \
--compile-balance 2 \
--compile-max-l2m-size 0 \
--max-hbm-bytes 2140000000 \
--attention-linear-mode dynamic \
--mlp-linear-mode dynamic \
--mlp-down-linear-mode dynamic \
--attention-matmul-mode fixed16 \
--fixed-prompt 'Place the RDK camera box on top of the black MCU box.' \
--prompt-embedding-input
RoPE、Mask 与逻辑 Token 位置
RoPE 是 Rotary Position Embedding。它会根据 token 的 position ID 旋转 attention 中的 Q/K,让模型感知 token 的相对位置。
如果训练时两路真实相机后面接语言 token,而部署时却把一个空相机的 256 token 也当成有效位置,语言和状态 token 的 position ID 会整体后移。
模型仍然能输出数值,但它理解的 token 关系已经错了。这类错误通常不会产生明显的 NaN,而是产生“看起来像模型在思考,但动作总是不对”的输出。
最终布局保留:
physical prefix tensor = 816
valid vision tokens = 512
valid prompt tokens = 16
logical Expert state position = 528
logical Expert action positions = 529...578
也就是说,物理 tensor 保持兼容,逻辑 position 和 mask 按两路真实相机计算。
新的 PaliGemma HBM 已经吸收了 SigLIP 的真实量化分布。接下来再把它放回 S600,导出 36 组 KV;这些 KV 才是 Expert 在最终部署中真正面对的条件。
Expert:使用真实 PaliGemma KV
Expert 对应第三个敏感点:Expert 的输出会回灌到下一次 flow step。校准输入必须来自最终 PaliGemma HBM,Expert 自身也不再追求激进的统一 INT8。
PaliGemma 编译完成后,再次把新 HBM 上传 S600。这次保存的不只是 vision embedding,而是 36 个 Expert KV:
expert_kv_00_fp16.bin
...
expert_kv_35_fp16.bin
每个 KV shape:
[1, 816, 256]
50 条样本共:
50 × 36 = 1800 个 KV 文件
板端终端 A:
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u dump_siglip_hbm_calibration.py \
--calibration-dir calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--engine-dump-dir /root/pi0_calibration/engine_paligemma_kv_real50 \
--output-dir calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_fixed16_paligemma_hbm_kv_real50 \
--prompt 'Place the RDK camera box on top of the black MCU box.' \
--save-expert-kv
板端终端 B 使用包含最终 SigLIP 和 PaliGemma 的临时配置启动 standalone engine。
把 KV 传回服务器后量化 Expert:
export PI0_VALID_CAMERA_SLOTS=2
python3 models/pi0/tools/quantize_expert_real_calib.py \
--model-dir /host/gemma/input/pi0_full_v5_2cam_100ep_015000 \
--calibration-dir /host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--paligemma-kv-dir /host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_fixed16_paligemma_hbm_kv_real50 \
--output-dir /host/gemma/output/expert_fixed16_paligemma_hbmkv_dynamic_real50 \
--device cuda:0 \
--vision-tokens-num 256 \
--max-samples 50 \
--jobs 20 \
--march nash-p \
--transformer-linear-mode dynamic \
--projection-linear-mode dynamic \
--attention-matmul-mode dynamic \
--output-linear-mode dynamic
Expert 最终选择 dynamic,而不是继续强制 W8A8/W8A16。原因是 Expert 的输出会直接映射成 degree,几十分之一的归一化误差反归一化后就可能变成明显的关节偏移。
Expert 本身比 PaliGemma 小,因此这里更值得用模型大小换精度。
至此量化流水线完成,得到 SigLIP、PaliGemma、Expert 三份 HBM,以及与它们绑定的 Prompt Embedding、Normalization Stats 和 SHA256。模型文件本身还不能驱动机械臂,下一章需要用端侧 Runtime 把三段计算、相机输入和 SO100 控制循环连成闭环。
四、部署实操
部署阶段解决的不是“如何加载三个文件”,而是一次请求中每个 Tensor 由谁创建、在哪个进程处理、怎样送入 BPU、如何在 30 Hz 下执行,以及异常退出时怎样可靠关闭力矩。
推理架构
最初使用过 SDK 自带 XLM Pi0 示例,但它有几个限制:
- 相机槽位和输入协议假设较固定。
- SDK 旧版本存在硬编码延时。
- 中间 tensor、position IDs 和 KV 不方便导出与替换。
- 很难精确控制同步/异步动作队列。
- 调试时不容易把 SigLIP、PaliGemma 和 Expert 分段比较。
最终实现了自己的 Pi0 standalone runtime,但底层仍然使用相同的 S600 DNN SDK 和模型管理接口。
主要文件:
models/pi0/native/standalone/pi0_standalone.cc
models/pi0/pi0_full_pipeline.py
models/pi0/run_pi0_standalone_config.sh
图:Python 管相机、SO100 与控制生命周期,C++ 管协议和 Tensor,BPU Runtime 执行三段 HBM。
运行时职责与数据接口
Python controller
├─ LeRobot SO100 接口
├─ 读取 front/side/state
├─ 30 Hz 动作发送
├─ 日志、torque 清理和安全确认
└─ TCP Protobuf server
C++ standalone engine
├─ TCP client
├─ 图像前处理
├─ 第三空相机槽和 mask
├─ SigLIP/PaliGemma/Expert HBM
├─ 10 次 Expert denoise
└─ state/action 归一化与反归一化
Protobuf 请求
Python 每次发送:
2× UINT8 CHW image
1× task string
1× FLOAT64 state[6]
C++ 返回:
FLOAT64 actions[50,6]
前处理
每路图像:
640×480 RGB
↓ resize with pad
224×224
↓ CHW FP16
[-1,1]
第三空槽填充为归一化后的 -1 图像,并由 PaliGemma mask 屏蔽。
后处理
Expert FP16 output[50,32]
↓ action_mean/action_std 反归一化
只取前 6 维
↓
夹爪裁剪到 [0,100]
↓
SO100 absolute degree target
推理阶段没有额外的单步 1° 限幅,也没有 LeRobot max_relative_target=20。20 只用于数据采集时保护遥操作。
进程与 Runtime 分层
量化完成只代表得到了三个 HBM。真正把它们变成可控制 SO100 的 runtime,还要解决进程边界、模型加载、BPU 调度、张量内存、三段模型衔接、去噪循环、控制频率和异常退出。
LeRobot SO100 + cameras
│
▼
pi0_full_pipeline.py
Python 控制器 / TCP server
│ Protobuf over 127.0.0.1:30001
▼
pi0_standalone_sdk102
C++ TCP client / Pi0 engine
│
▼
ModelRunner
│
▼
XLM ModelManager + hbDNN/hbUCP
│
├─ SigLIP HBM × 3 runner
├─ PaliGemma HBM × 1 runner
└─ Expert HBM × 1 runner
这里没有重新实现 BPU 驱动。底层仍然使用 SDK 1.0.2 的 hbDNN、hbUCP 和 XLM ModelManager,只是把原本受限的高层流程替换成可调试的 Pi0 编排层。
只搬入 runtime所需的最小 XLM utility 源码:
models/pi0/native/vendor/openexplorer_llm/xlm/src/utils/model_manager.cc
models/pi0/native/vendor/openexplorer_llm/xlm/src/utils/xlm_utils.cc
Python Controller 与 C++ Engine 生命周期
真实启动顺序:
1. Python 校验模型、统计量和硬件标定 SHA256
2. Python 连接 SO100 bus 和两路相机,但不使能 torque
3. Python bind 127.0.0.1:30001 并 listen
4. Python subprocess 启动 run_pi0_standalone_config.sh
5. shell 设置 SDK 动态库和 L2M 环境变量
6. shell exec C++ standalone engine
7. C++ 加载 HBM 并分配 tensor 内存
8. C++ 主动连接 Python 的 30001 端口
9. Python 接受连接并开始发送观测
所以:
Python = controller + socket server
C++ = inference engine + socket client
这种进程隔离让 Python 保留 LeRobot 生态,C++ 独立链接 S600 SDK。engine 崩溃时,Python 可以读取 engine.log、关闭 torque,并且同一个 C++ engine 可以被离线 smoke、真实只读和真机控制复用。
Runtime 启动脚本与环境变量
run_pi0_standalone_config.sh 不包含神经网络逻辑,它只准备 runtime:
export LD_LIBRARY_PATH=/root/D-Robotics_LLM_S600_1.0.2_SDK/oellm_runtime/lib:$LD_LIBRARY_PATH
export HB_DNN_USER_DEFINED_L2M_SIZES=6:6:6:6
exec pi0_standalone_sdk102 --config <deployment.json>
LD_LIBRARY_PATH用于找到 SDK 1.0.2 的 DNN、UCP 和 protobuf 动态库。HB_DNN_USER_DEFINED_L2M_SIZES设置四个 BPU core 的 runtime L2M 预算。exec让 shell 被 C++ 替换,Python 可以可靠管理整个 engine 进程组。
旧 SDK 示例中的固定 sleep(5000 ms) 不属于模型计算,standalone 路径没有保留这类等待。
Python 与 C++ 的边界确定以后,下一步沿一次真实请求向下看。Deployment JSON 首先绑定三份 HBM 的 ABI,随后 C++ Engine 完成缓存同步、模型调用和 Tensor 搬运。
单次请求的数据流与 Tensor ABI
Deployment JSON 与模型 ABI
关键字段:
{
"runtime": "standalone_dnn",
"siglip_hbm_path": "/root/pi0_models/.../pi0_siglip_ptq.hbm",
"paligemma_hbm_path": "/root/pi0_models/.../pi0_gemma_llm_ptq.hbm",
"action_hbm_path": "/root/pi0_models/.../pi0_gemma_expert_ptq.hbm",
"prompt_embedding_path": "/root/pi0_models/.../fixed_prompt_embedding.bin",
"norm_stats_path": "/root/rdk_LeRobot_tools/models/pi0/configs/norm_stats_....json",
"siglip_bpu_core": [0, 1, 2],
"paligemma_bpu_core": [0],
"action_bpu_core": [0, 1, 2, 3],
"real_camera_num": 2,
"exec_size": 6,
"denoise_num": 10,
"parallel_siglip": true,
"action_mode": "absolute",
"server_ip": "127.0.0.1",
"server_port": 30001
}
C++ 会硬校验 real_camera_num=2、exec_size=6、denoise_num=10,并要求 siglip_bpu_core 正好有三项。Python 还会校验三份 HBM、prompt embedding、norm stats 和 follower calibration 的 SHA256。
因此 JSON 只能绑定一个已经编译好的模型组合,不能动态改变 HBM 的相机数、token 数或 tensor shape。
ModelRunner 的 BPU 执行流程
每个 HBM graph 被包装成统一的 ModelRunner:
ModelManager::Init()
↓ 加载 HBM pack
按名字找到 siglip / gemma / gemma_expert graph
↓
获取 hbDNNTensor 输入输出数组
↓
保存允许使用的 InferBackend/BPU core
一次 Run() 的内部顺序:
FlushInputMem(CLEAN)
InferTaskSync()
FlushOutputMem(INVALIDATE)
ReleaseTaskHandle()
CPU 通过 tensor.sysMem.virAddr 写 input。执行前必须 CLEAN cache,BPU 输出后必须 INVALIDATE cache,否则两边都可能读到旧数据。
三路 SigLIP Runner 并行调度
runner 0 → BPU core 0 → front
runner 1 → BPU core 1 → side
runner 2 → BPU core 2 → empty
三个 runner 共享同一个 packed SigLIP HBM handle,但各自拥有独立输入输出 tensor。parallel_siglip=true 时使用三个 std::async 并行提交。
empty 槽仍然运行 SigLIP,是因为 PaliGemma 的 vision input 物理 shape 固定为三槽。它的输出会进入物理 tensor,但对应 prefix token 会被 attention mask 屏蔽。
HBM Tensor ABI 校验
ValidateLayout() 在接收第一张图之前检查每个 input/output 的数量、shape 和 dtype。
SigLIP:
image input: [1,3,224,224] FP16
position IDs: [1,256] S64
vision output: [1,256,2048] FP16
PaliGemma:
prompt embedding:[1,48,2048] FP16
vision embedding:[1,768,2048] FP16
attention mask: [1,1,816,816] FP16
KV output × 36: [1,816,256] FP16
Expert:
state: [1,32] FP16
noise/action: [1,50,32] FP16
denoise index: [1] S32
attention mask: [1,1,51,867] FP16
position IDs: [1,51] S32
KV input × 36: [1,816,256] FP16
output: [1,50,32] FP16
任意一项不一致,engine 直接退出。这能阻止“每个 HBM 单独能加载,但三段接口实际对不上”的组合进入真机。
静态 Tensor 初始化
固定 prompt embedding:
[1,48,2048] FP16
48 × 2048 × 2 = 196,608 bytes
三路 SigLIP position IDs 都是:
0,1,2,...,255
PaliGemma 物理 prefix 为:
0...767:三路 vision tensor
768...815:48 个 prompt 容量
真正可见的 token:
0...511:front + side
768...783:16 个真实 prompt token
被 mask 的位置:
512...767:empty camera
784...815:未使用 prompt capacity
Expert suffix 为 1 state + 50 actions = 51。它的逻辑 position 从 512 + 16 = 528 开始:
state position = 528
action positions = 529...578
这解释了为什么物理 KV 长度保持 816,但 Expert position 不能从 816 开始。
Protobuf 请求与响应协议
Python 每次发送:
images[0]:front,UINT8 CHW [3,H,W]
images[1]:side,UINT8 CHW [3,H,W]
languages[0]:task STRING
states[0]:FLOAT64 [6]
C++ 会拒绝:
- 不是正好两张图。
- 图像不是 CHW UINT8 RGB。
- state 不是正好 6 个 FLOAT64。
- task 与固定 prompt 配置不一致。
- message 大于 64 MiB。
返回的 wire tensor 是 [1,50,6] FLOAT64,Python 去掉 batch 维后得到 [50,6]。
C++ 图像前处理
Python 只发送原始 RGB。C++ 直接把结果写入 SigLIP 的 BPU input tensor:
输入 H×W
↓ 按长边等比例 resize
最长边 224
↓ 四周补 0
224×224
↓ value = uint8 × 2/255 - 1
CHW FP16
↓
hbDNNTensor.sysMem.virAddr
第三槽不经过 Protobuf,C++ 直接填充 FP16 -1.0。
前处理只实现一份,可以避免 Python、XLM 示例和量化校准分别实现 resize/normalize 后出现细微差异。
SigLIP 到 PaliGemma 的 Tensor 传递
每路 SigLIP 输出 [1,256,2048] FP16。C++ 不转 NumPy、不经过 socket,而是直接 memcpy 到 PaliGemma vision input 的连续区域:
offset 0 × 256 × 2048 → front
offset 1 × 256 × 2048 → side
offset 2 × 256 × 2048 → empty
最终得到 [1,768,2048] FP16。这既减少了序列化,也保证 PaliGemma 看到的就是实际 SigLIP HBM 输出。
PaliGemma KV 到 Expert 的 Tensor 传递
PaliGemma 不是直接输出机械臂动作。它输出融合了语言与当前视觉的 prefix KV,Expert 再基于这些 KV、当前关节状态和扩散噪声生成动作。
当前 HBM ABI 中:
PaliGemma outputs[1...36]
每个都是 [1,816,256] FP16
↓ 逐块 memcpy
Expert inputs[5...40]
单个 KV tensor 为:
816 × 256 × 2 bytes = 417,792 bytes
36 个总计约 14.35 MiB。它们只在 C++ 进程内部从一个 HBM tensor buffer 搬到另一个 HBM tensor buffer,不经过 Python,也不会被序列化成 Protobuf。
这里有两个容易混淆的“复用”:
- 同一次请求内复用:PaliGemma 只运行一次,生成的 36 个 KV 被 Expert 的 10 次去噪共同使用。
- 跨请求不复用:下一动作块换了相机图像后,必须重新运行三路 SigLIP 和 PaliGemma,生成新的视觉条件 KV。
如果跨动作块盲目复用旧 KV,速度会更快,但 Expert 实际依据的是旧画面,抓取阶段很容易出现“物体已经移动,模型还按上一帧修正”的问题。
State 归一化与噪声初始化
SO100 只提供 6 维关节状态,而 Expert 的内部动作宽度是 32。因此 C++ engine 会先做:
raw state [6] FLOAT64
↓ (x - state_mean) / (state_std + 1e-6)
normalized state [6]
↓ 后 26 维补 0
Expert state [1,32] FP16
这里使用的 state_mean/state_std 必须来自训练数据集对应 checkpoint 的统计量。把另一个数据集的 stats 填进来,即使所有 HBM 都能正常运行,机械臂落点也会整体偏移。
Expert 的初始 action/noise 为 [1,50,32] FP16:
正常运行:每次请求生成 N(0,1) 随机噪声
精度对比:读取固定 3,200-byte noise 文件
固定噪声只用于 CPU/BF16/HBM 的逐层误差复现。真机长期运行时若永远固定噪声,会消除采样随机性,但它不是解决动作抖动的办法。
Expert 十步 Flow Matching
Expert HBM 不是调用一次就结束,而是连续运行 10 次:
初始 noise/action x0
↓ Expert(step=0, state, mask, position, 36 KV)
x1
↓ Expert(step=1, 同一 state/mask/position/KV)
x2
↓ ...
x10
↓
最终 [1,50,32] normalized actions
每一步的 HBM 输出都会直接复制回 Expert input[1],成为下一步的 action/noise 输入。Expert input[2] 则写入当前去噪步 0...9。
为了减少 CPU cache flush:
step 0:flush 全部 Expert inputs
step 1...9:只 flush input[1] 与 input[2]
原因是 state、attention mask、position IDs 和 36 个 KV 在同一次请求的 10 步里都不变。只刷新真正变化的 buffer,既保持语义正确,也避免反复清理十几 MiB 的 KV。
这也是端侧 runtime 与普通“按顺序调用三个模型”脚本最大的区别之一:它必须理解 Expert 的迭代状态,并显式管理每一步输入输出 buffer 的生命周期。
动作反归一化与夹爪范围处理
第 10 步输出仍是 [1,50,32] FP16 的归一化动作。C++ engine 做的后处理是:
FP16 → FP32
↓ 取前 6 维
action = normalized × (action_std + 1e-6) + action_mean
↓
[50,6] FLOAT64
仅对第 6 维夹爪目标做物理范围裁剪:
gripper = clamp(gripper, 0, 100)
这里没有默认做单步 1°、20° 的关节限幅,也没有把动作替换成 PID 插值轨迹。这样做是为了尽量保留模型原始的 50 步输出。
要区分三种东西:
- 反归一化:模型定义的一部分,必须做。
- 夹爪物理范围 clamp:防止写入无意义位置,必须做。
- 额外平滑、blend、相对角度裁剪:控制策略,不属于模型本身;默认应关闭,只有明确验证后才启用。
C++ 返回 [50,6] 后,神经网络计算已经结束;后续行为由 Python 控制器决定。它必须把动作块、真实关节状态、Prompt/KV 生命周期和同步策略放在同一个控制时序中管理。
SO100 控制、Prompt 与同步执行
SO100 控制生命周期
Python 层并不参与神经网络计算,但负责所有与真机安全和时序有关的工作:
LeRobot SO100Follower
├─ 读取 6 维当前位置
├─ 读取 front / side RGB
├─ 向本地 C++ engine 发请求
├─ 接收 [50,6] 绝对位置动作
└─ 以 30 Hz 调用 send_action()
启动时的顺序很重要:
- 先校验 deployment JSON、文件存在性和三个 HBM 的 SHA256。
- 连接串口和相机,但不立即开力矩。
- 读取所有电机
Torque_Enable;只要有一个已经开启就拒绝启动,避免继承未知进程的控制状态。 - 默认进入
dry-run,只推理、保存日志,不发送动作。 - 只有同时给出
--execute --confirm ENABLE_SO100_MOTORS才可能开启力矩;正常安全模式还要求首块视觉/动作检查通过。已经完成只读验证后,可显式加--force-model-actions跳过这两个启发式门,但它不会跳过 hash、串口、力矩所有权和确认字符串检查。 - 每个目标发送前重新读取真实关节状态,记录跟踪误差和指令跳变量。
控制周期固定为:
30 Hz = 每 33.33 ms 发送一个目标
50 步动作块 = 约 1.67 s
如果 --max-relative-target 不传,LeRobot 不再替模型做额外相对目标裁剪。SO100 底层舵机仍会进行自己的位置闭环,但它不应该被误解成“Pi0 runtime 在额外跑一个软件 PID”。
严格同步控制时序
本项目正式验证默认使用:
--prefetch-steps 0
其时序是:
读取当前 state + 两张当前图像
↓
完整运行 SigLIP ×3 → PaliGemma → Expert ×10
↓
按 30 Hz 执行这 50 个动作
↓
动作块执行完,再读取新的 state + 新图像
↓
推理下一块
优点是每个动作块严格对应动作块开始时的真实观测,不会混入提前采样的状态,也便于和服务器 BF16 同步推理做 A/B 对比。
代价是动作块边界会等待下一次约 1.2 秒的端侧推理,因此肉眼可能看到短暂停顿。异步 prefetch 可以隐藏这段延迟,但它会引入“用哪一个未来 state、哪一帧图像推下一块”的策略问题。为了先验证模型精度,本教程不把异步推理作为默认方案。
运行日志与控制遥测
每次运行都会创建独立输出目录,核心文件包括:
pipeline.log Python 生命周期、安全门和异常
engine.log C++ 中三个 HBM 的耗时与错误
result.json 本次运行配置和最终汇总
chunks.jsonl 每个 50-step 动作块的输入/输出摘要
control_steps.jsonl 每个 30 Hz 控制步的 current/target/lag
control_steps.jsonl 至少记录:
current 发送前的真实关节位置
target 模型目标位置
max_abs_delta target 与真实位置的最大差
max_abs_command_delta 当前指令与上一指令的最大跳变
control_lag_ms 相对 30 Hz deadline 的延迟
gripper telemetry 周期性读取力矩等寄存器
因此“模型动作怪”不能只凭视频判断。先看 engine.log 是否推理异常,再区分:模型输出跳变、舵机跟踪不上、夹爪力矩掉线,还是控制线程错过 deadline。
中间 Tensor Dump 与精度定位
设置环境变量:
export PI0_STANDALONE_DUMP_DIR=/root/pi0_debug_dump
C++ engine 会为每次请求建立:
request_000000/
request_000001/
...
并保存关键中间量:
三路 SigLIP 预处理图像与输出
PaliGemma prompt / vision / attention mask
36 组 Expert KV
Expert state / noise / mask / position IDs
Expert step_00...step_09 输出
最终 normalized 与反归一化动作
这使误差定位可以按链路逐层进行:
相机前处理是否一致?
↓
SigLIP HBM 是否先偏?
↓
PaliGemma KV 是否放大误差?
↓
Expert 从第几个去噪 step 开始偏?
↓
还是 stats / 后处理才出错?
连续真机运行时必须关闭 dump。单次请求会产生大量二进制文件,长期打开会迅速占满 S600 磁盘。
BPU Core 配置与调度
当前正式配置为:
{
"siglip_bpu_core": [0, 1, 2],
"paligemma_bpu_core": [0],
"action_bpu_core": [0, 1, 2, 3],
"parallel_siglip": true
}
- 三个独立 SigLIP runner 分别绑定一个 core,并由三个 C++ future 并发提交。
- PaliGemma 的 backend 候选为 core 0。
- Expert 把 0~3 都交给 SDK backend;具体算子如何落核仍由编译结果和 runtime 调度决定,并不等于“一次 Expert 同时平均占满四核”。
可用下面命令持续观察:
watch -n 1 hrut_somstatus
bpu0...bpu3 ratio 是采样窗口内利用率,不是模型是否成功的唯一判断。Pi0 是阶段式执行:SigLIP、PaliGemma、Expert 依次运行,所以各核利用率天然会随阶段波动。
进程退出与力矩清理
Python 捕获 SIGINT/SIGTERM 后按以下顺序退出:
写回 result.json
↓
若本进程开启过力矩,则 disable_torque
↓
断开两路相机
↓
断开 SO100 bus
↓
向 C++ engine 进程组发送 SIGTERM
C++ client 自身带断线重连循环,方便 Python controller 重启;但父级 Python 退出时会杀掉整个 engine 进程组,避免后台残留继续占用 HBM、端口或 BPU。
所以手动结束时应对前台主命令按一次 Ctrl+C,而不是只杀 C++ 子进程。若发生异常退出,也要通过 result.json 和电机 Torque_Enable 再确认一次最终状态。
Prompt Embedding 与 KV Cache
当前部署 artifact 使用固定任务 prompt embedding:
Place the RDK camera box on top of the black MCU box.
固定 embedding 可以省掉板端 tokenizer 和语言 embedding 查找,但不代表 PaliGemma 可以永远只跑一次。
原因是 PaliGemma 的 KV 同时依赖:
固定 prompt
+
当前 front 图像
+
当前 side 图像
图像改变后,融合后的 KV 也应该改变。
最终策略:
- 每个新动作 chunk 都重新运行 SigLIP 和 PaliGemma。
- 同一个 chunk 的 10 次 Expert denoise 复用本次 PaliGemma KV。
- 不跨 chunk 复用旧 KV。
如果只复用旧 PaliGemma KV,模型相当于一直根据旧画面生成动作。
同步执行与异步预取
异步预取的行为与风险
异步模式会在上一块 50 步动作还没执行完时,提前读取图像或预测下一块动作。
优点:
- chunk 之间停顿小。
- 视觉吞吐更高。
风险:
- 采图时的机械臂 state 与真正执行下一块时不同。
- 上一块动作尚未结束,下一块已经基于中间状态生成。
- 夹爪可能在两个 chunk 间反复开合。
- 机械臂可能表现为抬起、下压之间抽搐。
严格同步执行配置
采集最新 state/front/side
↓
完整推理约 1.24 秒
↓
执行 50 步,30 Hz,约 1.67 秒
↓
重新采集
配置:
prefetch_steps=0
visual_refresh_chunks=1
control_hz=30
这里的 30 Hz 是动作发送频率,不是模型每秒看 30 张新图。
严格同步时,一次视觉闭环周期约为:
1.24 + 1.67 ≈ 2.91 秒
它牺牲了视觉刷新率,但先保证动作 chunk 和观测对应关系正确。后续如果重新做异步,需要使用更严谨的 state prediction、chunk overlap 或 action replanning,而不是简单提前启动线程。
图:prefetch_steps=0 时,推理与动作执行串行;块结束后才采集下一组真实观测。
运行逻辑确定以后,再进行 SDK 安装、C++ 编译和模型部署。日常使用只启动 Python 顶层 Pipeline,由它创建本地 TCP Server 并拉起 C++ Engine,不需要手工维护两个前台进程。
编译、模型部署与启动
端侧源码目录
models/pi0/
├─ pi0_full_pipeline.py
│ LeRobot、相机、TCP server、30 Hz 控制和日志
├─ run_pi0_standalone_config.sh
│ 校验 JSON、配置动态库与 L2M、启动 C++ engine
├─ run_live_sync.sh
│ 固化最终 artifact hash 与同步真机参数
├─ configs/deployments/*.json
│ 绑定三份 HBM、prompt、stats、core 和协议参数
└─ native/
├─ build_standalone_pi0.sh
├─ standalone/pi0_standalone.cc
├─ standalone/dnn_sdk102_compat.h
├─ sdk_1.0.2/pi0_demo/msg.proto
└─ vendor/openexplorer_llm/xlm/
├─ include/utils/{model_manager,xlm_utils}.h
└─ src/utils/{model_manager,xlm_utils}.cc
这里特意把必要的 XLM utility 源码 vendoring 到 rdk_LeRobot_tools,避免运行时依赖服务器上的源码目录或某个临时 SDK demo 工作区。最终分支自身就包含可审查、可重新编译的端侧框架。
SDK 1.0.2 安装
cd /root
wget https://d-robotics-aitoolchain.oss-cn-beijing.aliyuncs.com/llm_s600/1.0.2/D-Robotics_LLM_S600_1.0.2_SDK.tar.gz
tar -xzf D-Robotics_LLM_S600_1.0.2_SDK.tar.gz
本项目默认 SDK runtime 位于:
/root/D-Robotics_LLM_S600_1.0.2_SDK/oellm_runtime
编译和运行必须使用同一套 1.0.2 headers/libraries。只替换 .so 或只替换头文件,可能产生能编译却在 tensor/cache API 上行为不一致的问题。
Standalone Engine 编译
cd /root/rdk_LeRobot_tools
git checkout s600
bash models/pi0/native/build_standalone_pi0.sh
构建脚本本质上执行一条 C++17 链接:
pi0_standalone.cc
+ msg.pb.cc
+ model_manager.cc
+ xlm_utils.cc
+ SDK 1.0.2 libprotobuf/libdnn/libhbucp/libhlog
+ OpenCV
关键编译参数是:
-std=c++17 -O3 -pthread
-Wl,-rpath,/root/D-Robotics_LLM_S600_1.0.2_SDK/oellm_runtime/lib
最终二进制:
/root/rdk_LeRobot_tools/models/pi0/native/install/bin/pi0_standalone_sdk102
构建先写入 .tmp,成功后再原子替换正式二进制,避免编译中断留下半个可执行文件。
HBM Artifact 与版本绑定
最终使用:
SigLIP: 445,286,384 bytes
PaliGemma:2,047,778,016 bytes
Expert: 344,902,928 bytes
正式 deployment JSON 绑定:
siglip_hbm_path
paligemma_hbm_path
action_hbm_path
prompt_embedding_path
norm_stats_path
task
real_camera_num / exec_size / denoise_num
BPU core 配置
Python controller 还在启动 C++ 前核对三份 HBM、prompt、stats 和机械臂 calibration 的 SHA256。不能根据目录名中的 latest 猜模型,也不能只看文件大小,因为不同量化策略可能生成相同 ABI、相近大小,但数值行为完全不同。
Runtime 环境变量
run_pi0_standalone_config.sh 在执行二进制前设置:
export LD_LIBRARY_PATH=/root/D-Robotics_LLM_S600_1.0.2_SDK/oellm_runtime/lib:${LD_LIBRARY_PATH:-}
export HB_DNN_USER_DEFINED_L2M_SIZES=6:6:6:6
LD_LIBRARY_PATH保证加载 SDK 1.0.2 的 runtime 库,而不是系统里另一版同名库。HB_DNN_USER_DEFINED_L2M_SIZES为四个 BPU core 配置本任务需要的 L2M 空间。缺失或配置不匹配时,可能在 HBM load/infer 阶段失败,而不是在 Python 层报错。
runner 还会先执行 validate_pi0_config.py,然后用 exec 启动 C++。这样 shell 不残留额外父进程,Python 可以可靠地管理整个 engine 进程组。
Controller 与 Engine 启动顺序
本项目的方向是:
Python controller = TCP server
C++ engine = TCP client
正式启动顺序由 pi0_full_pipeline.py 自动完成:
Python bind/listen 127.0.0.1:30001
↓
Python subprocess 启动 run_pi0_standalone_config.sh
↓
C++ load 三份 HBM
↓
C++ connect 127.0.0.1:30001
↓
Python accept 后进入请求循环
单独运行 C++ 时看到反复 reconnect 并不代表 HBM 坏了,通常只是 Python server 尚未监听。日常使用应启动 Python 顶层命令,而不是分别手工维护两个进程。
连续同步推理启动
分支已经提供固定最终模型和 hash 的启动器:
cd /root/rdk_LeRobot_tools/models/pi0
./run_live_sync.sh
它等价于以这些关键参数启动 Python controller:
max_chunks=0 无限动作块,直到 Ctrl+C
prefetch_steps=0 严格同步
fixed_noise=false 每个请求使用随机噪声
chunk_blend_steps=0 不修改动作块开头
max_relative_target=None 不做额外相对角度裁剪
save_artifact_every=0 连续运行不保存大图和 npy
execute=true
confirm=ENABLE_SO100_MOTORS
常见启动故障
Address already in use
通常是旧 Python controller 仍占用 30001:
ss -ltnp | grep ':30001'
确认 PID 属于旧 Pi0 进程后再正常发送 SIGTERM,不要无差别杀 Python。
C++ 日志一直显示 reconnect
检查 Python 是否已成功 bind、是否因 HBM hash 或 torque 状态在 accept 前退出,并先读 pipeline.log。
缺少 libdnn、libhbucp 或 protobuf .so
不要直接裸跑 binary,使用 run_pi0_standalone_config.sh,并确认 SDK 目录没有被移动。
启动时报 tensor count/shape/type mismatch
这不是 JSON 小问题,而是装载了错误 HBM 或使用了不兼容 SDK 编译产物。停止运行,按 manifest 和 SHA256 重新核对三段模型。
No module named torch 或缺少 LeRobot 包
说明误用了系统 python3。控制器应使用:
/home/sunrise/lerobot/.venv/bin/python
提示 torque 已开启而拒绝启动
先确认是否还有旧控制进程。如果没有,再单独检查电机状态;不要为了“能跑”直接删除这个所有权检查。
完成部署后不要立即无限运行机械臂。验证应从配置、离线 Tensor、真实只读观测逐层推进;某一层失败时,只检查该层新增的变量。
分层验证、故障定位与结果
真机验证不应从连续运动开始,而应按以下顺序逐层增加变量:
静态配置检查
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python validate_pi0_config.py \
configs/deployments/pi0_full_v5_2cam_positionfp16_siglip_fixed16_paligemma_hbmkv_expert_20260801.json
离线双图 Smoke Test
不访问机械臂串口:
/home/sunrise/lerobot/.venv/bin/python -u pi0_standalone_offline.py \
--front /path/to/front.jpg \
--side /path/to/side.jpg \
--state 0 0 0 0 0 0 \
--output-dir diagnostics/offline_smoke
预期输出:
[50,6]
真实观测只读推理
/home/sunrise/lerobot/.venv/bin/python -u pi0_full_pipeline.py \
--config configs/deployments/pi0_full_v5_2cam_positionfp16_siglip_fixed16_paligemma_hbmkv_expert_20260801.json \
--max-chunks 1 \
--prefetch-steps 0 \
--no-fixed-noise \
--output-dir diagnostics/readonly_smoke
不加 --execute,不会发送机械臂动作。
有限动作块真机验证
先运行 1~3 个 chunk,确认电机顺序、夹爪和 torque 正常。
连续同步真机推理
cd /root/rdk_LeRobot_tools/models/pi0
./run_live_sync.sh
附录:
1.开发阶段的关键故障与根因
JSON 配置无法改变已编译输入 ABI
HBM 的 tensor shape、mask 和 position layout 在编译时已经固定。JSON 只能选择模型和运行参数,不能把一相机 HBM 变成双相机 HBM。
真实相机数与物理槽位混淆
最终是两路真实相机、三个物理槽、一个 masked empty。把第三槽当真实 token,会让语言和 Expert position 整体错位。
训练与部署的相机 Schema 不一致
早期训练配置曾出现五槽 schema,而部署只有三槽。即使 Expert 输入 shape 最终能补齐,模型学习的 token 语义仍然不一致。最终重新固定为三槽训练和三槽部署。
Position IDs 与 RoPE 错位
这类错误不会轻易报 shape mismatch,也不一定产生 NaN。模型会正常输出一串动作,但图像位置理解错误,最典型表现是抓取点持续偏移。
三段模型统一激进量化
W8A8/W8A16 不能机械地应用到所有层。视觉位置、PaliGemma attention 和 Expert 输出对精度的敏感度不同。
最终采用分段策略:
SigLIP:position embedding FP16
PaliGemma:attention matmul fixed16
Expert:关键路径 dynamic
使用浮点上游输出校准下游
这是最关键的问题之一。部署时 Expert 接收的是量化 PaliGemma KV,校准时也必须使用同一 HBM 的真实 KV。
混用不同版本的 PaliGemma Artifact
三段模型可以分别成功加载,并不代表它们属于同一训练和 position layout。部署时必须通过路径、size、SHA256 和 manifest 绑定成一个组合。
正式推理误用固定噪声
固定 noise 适合做 BF16/HBM 可重复精度对比,但正式 Pi0 推理应该使用随机 noise。否则容易把某个固定轨迹误认为模型的完整分布。
异步预取使用过期观测
动作块边界不连续、夹爪反复开合,很多时候不是 PID,而是下一块动作对应的观测已经过期。最终先关闭预取,改成严格同步。
将跟踪误差误判为指令跳变
舵机存在 PID 跟踪滞后。若安全门用“当前实际位置 vs 下一目标”判断跳变,正常的跟踪误差也可能被误判成模型输出跳变。
更合理的是比较:
当前 command target
vs
上一条 command target
实际位置误差仍然记录用于诊断,但不应该直接等同于模型 jump。
额外动作限幅改变模型轨迹
这会改变训练得到的动作轨迹,尤其会破坏抓取阶段的快速闭合和抬升。最终推理不再添加这种人工限幅,只保留夹爪物理范围裁剪。
夹爪力矩异常与模型输出混淆
夹爪异常还可能来自:
- Torque_Enable 被保护机制关闭。
- 电流、负载或温度保护。
- 进程退出时自动 disable torque。
- USB 串口瞬断。
- PID/舵机参数与训练机械臂不同。
因此 pipeline 同时记录夹爪 telemetry,而不是只看模型 action。
相机外参变化导致视觉分布漂移
Pi0 能泛化,但不意味着对完全不同的相机外参不敏感。抓取点总偏前时,首先要比较训练图像和当前图像,而不是立即修改后处理偏置。
SDK 示例固定等待掩盖真实性能
旧 SDK 示例请求路径中存在硬编码 sleep(5000 ms)。它不是 SigLIP、PaliGemma 或 Expert 的真实计算时间,却会让机械臂表现为“动一下、停很久”。
最终升级到 SDK 1.0.2,并重新编译修改后的 standalone runtime,删除了这类固定等待。判断性能时应分别记录 SigLIP、PaliGemma、Expert 和整链耗时,而不是只看动作之间的墙钟时间。
最终精度、性能与真机结果
最终链路已经完成:
- 双相机真实输入。
- 输出
[50,6]。 - 严格同步连续运行。
- 每个 chunk 更新 front 和 side。
- 单次整链推理约 1.24 秒。
- 动作发送频率 30 Hz。
在一个固定真实输入上,与服务器 BF16 动作对比:
MAE:0.4405°
RMSE:0.5903°
最大绝对误差:1.6888°
relative L2:0.852%
cosine similarity:0.999981858
2.Quick Start 命令清单
下面只保留本文双相机方案的最短操作路径,方便复现实验时直接查命令。它对应 front + side + 1 masked empty、SO100 六维绝对关节动作、100 个 episode、全参数训练和 SDK 1.0.2。若相机数量、任务文本或 checkpoint 改变,需要重新生成校准集、三份 HBM、deployment manifest 和 hash,不能直接复用本文最终 artifact。
A.1 在 S600 上检查设备
cd /root/rdk_LeRobot_tools
git checkout s600
ls -l /dev/ttyACM* /dev/serial/by-id/*
v4l2-ctl --list-devices
本文设备映射为:
follower:/dev/ttyACM0
leader: /dev/ttyACM1
front: /dev/video0
side: /dev/video2
A.2 采集 100 组双相机数据
cd /home/sunrise/lerobot
.venv/bin/lerobot-record \
--robot.type=so100_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=so100_follower \
--robot.calibration_dir=/root/rdk_LeRobot_tools/models/pi0/calibration/robots/so_follower \
--robot.max_relative_target=20 \
--robot.cameras='{front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30, warmup_s: 10, fourcc: MJPG}, side: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30, warmup_s: 10, fourcc: MJPG}}' \
--teleop.type=so100_leader \
--teleop.port=/dev/ttyACM1 \
--teleop.id=so100_leader \
--teleop.calibration_dir=/root/rdk_LeRobot_tools/models/pi0/calibration/teleoperators/so_leader \
--display_data=false \
--play_sounds=false \
--dataset.fps=30 \
--dataset.repo_id=local/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--dataset.root=/root/rdk_LeRobot_tools/models/pi0/datasets/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s \
--dataset.num_episodes=100 \
--dataset.single_task='Place the RDK camera box on top of the black MCU box.' \
--dataset.episode_time_s=10 \
--dataset.reset_time_s=5 \
--dataset.video=true \
--dataset.push_to_hub=false \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2
采集完成后,把整个数据集目录同步到训练服务器;meta/、data/ 和 videos/ 必须一起传输:
rsync -aP \
/root/rdk_LeRobot_tools/models/pi0/datasets/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s/ \
<GPU_USER>@<GPU_SERVER_IP>:/home/<GPU_USER>/datasets/so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s/
A.3 在 GPU 服务器全参数训练
export LEROBOT_ROOT=/home/<GPU_USER>/lerobot
export RDK_TOOLS=$LEROBOT_ROOT/rdk_LeRobot_tools
export CONDA=/home/<GPU_USER>/miniforge3/bin/conda
export DATASET_NAME=so100_rdk_camera_box_on_black_mcu_pi0_v5_2cam_100ep_10s
export DATASET_ROOT=/home/<GPU_USER>/datasets/$DATASET_NAME
export BASE_MODEL=/home/<GPU_USER>/gemma/pi0_base
export OUTPUT_DIR=$LEROBOT_ROOT/outputs/train/pi0_so100_2cam_full_b1_15k
cd "$RDK_TOOLS"
git checkout s600
LEROBOT_ROOT="$LEROBOT_ROOT" \
DATASET_NAME="$DATASET_NAME" \
DATASET_ROOT="$DATASET_ROOT" \
BASE_MODEL="$BASE_MODEL" \
OUTPUT_DIR="$OUTPUT_DIR" \
STEPS=15000 \
BATCH_SIZE=1 \
WANDB_ENABLE=true \
WANDB_PROJECT=lerobot-pi0-so100 \
WANDB_ENTITY=<WANDB_ENTITY> \
bash models/pi0/tools/train_pi0_full_v5_2cam_100ep.sh
最终 checkpoint:
export CHECKPOINT=$OUTPUT_DIR/checkpoints/015000/pretrained_model
test -f "$CHECKPOINT/config.json"
同时从该训练集导出 standalone runtime 使用的六维 state/action 统计量:
export NORM_STATS=$OUTPUT_DIR/norm_stats.json
$CONDA run --no-capture-output -n lerobot python -c '
import json, os
from pathlib import Path
stats = json.loads((Path(os.environ["DATASET_ROOT"]) / "meta/stats.json").read_text())
keys = ("mean", "std", "q01", "q99")
result = {
"norm_stats": {
"state": {key: stats["observation.state"][key] for key in keys},
"actions": {key: stats["action"][key] for key in keys},
}
}
Path(os.environ["NORM_STATS"]).write_text(json.dumps(result, indent=2) + "\n")
'
A.4 生成 50 条真实校准样本
export CALIB_DIR=/home/<GPU_USER>/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_real50
cd "$RDK_TOOLS"
$CONDA run --no-capture-output -n lerobot \
python models/pi0/tools/prepare_pi0_calibration_v5_2cam.py \
--dataset-root "$DATASET_ROOT" \
--checkpoint "$CHECKPOINT" \
--output-dir "$CALIB_DIR" \
--repo-id "local/$DATASET_NAME" \
--samples 50 \
--camera-keys front side \
--camera-slots 3 \
--task 'Place the RDK camera box on top of the black MCU box.'
A.5 在 SDK 1.0.2 工具链中级联量化
先把 checkpoint 和真实校准集映射到工具链容器,并统一设置两路有效相机:
export PI0_VALID_CAMERA_SLOTS=2
export RDK_TOOLS=/host/lerobot/rdk_LeRobot_tools
export MODEL_DIR=/host/gemma/input/pi0_full_v5_2cam_100ep_015000
export CALIB_DIR=/host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_real50
export QUANT_OUT=/host/gemma/output/pi0_full_v5_2cam_100ep_015000_sdk102
mkdir -p "$QUANT_OUT"
cd "$RDK_TOOLS"
1. SigLIP
python3 models/pi0/tools/quantize_siglip_real_calib.py \
--model-dir "$MODEL_DIR" \
--calibration-dir "$CALIB_DIR" \
--output-dir "$QUANT_OUT/siglip" \
--device cuda:0 \
--vision-tokens-num 256 \
--valid-camera-slots 2 \
--max-samples 50 \
--jobs 20 \
--march nash-p \
--max-l2m-size 0 \
--patch-embedding-mode quant8 \
--position-embedding-mode fp16 \
--attention-linear-mode dynamic \
--mlp-linear-mode dynamic \
--attention-matmul-mode dynamic \
--projector-linear-mode dynamic \
--layernorm-mode standard
把新 SigLIP HBM 和 50 条校准样本放到 S600,并准备只绑定该阶段 artifact 的临时 deployment JSON。两个板端终端分别执行:
# 终端 A:等待 C++ engine,并保存真实 SigLIP HBM 输出
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u dump_siglip_hbm_calibration.py \
--calibration-dir calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--engine-dump-dir /root/pi0_calibration/engine_siglip_real50 \
--output-dir calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_hbm_real50 \
--prompt 'Place the RDK camera box on top of the black MCU box.'
# 终端 B:使用新 SigLIP HBM 的临时配置启动 engine
cd /root/rdk_LeRobot_tools/models/pi0
PI0_STANDALONE_DUMP_DIR=/root/pi0_calibration/engine_siglip_real50 \
./run_pi0_standalone_config.sh configs/deployments/<SIGLIP_STAGE_CONFIG>.json
把板端生成的 pi0_full_v5_2cam_100ep_015000_siglip_hbm_real50 目录传回量化服务器,供 PaliGemma 校准使用。
2. PaliGemma
export SIGLIP_HBM_CALIB=/host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_hbm_real50
python3 models/pi0/tools/quantize_paligemma_real_calib.py \
--model-dir "$MODEL_DIR" \
--calibration-dir "$CALIB_DIR" \
--vision-embeddings-dir "$SIGLIP_HBM_CALIB" \
--output-dir "$QUANT_OUT/paligemma" \
--device cuda:0 \
--vision-tokens-num 256 \
--max-samples 50 \
--num-hidden-layers 18 \
--jobs 20 \
--march nash-p \
--compile-opt 2 \
--compile-balance 2 \
--compile-max-l2m-size 0 \
--max-hbm-bytes 2140000000 \
--attention-linear-mode dynamic \
--mlp-linear-mode dynamic \
--mlp-down-linear-mode dynamic \
--attention-matmul-mode fixed16 \
--fixed-prompt 'Place the RDK camera box on top of the black MCU box.' \
--prompt-embedding-input
把新 PaliGemma HBM 和 SigLIP HBM 一起部署到 S600,再导出 Expert 实际收到的 36 组 KV:
# 终端 A
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u dump_siglip_hbm_calibration.py \
--calibration-dir calibration_data/pi0_full_v5_2cam_100ep_015000_real50 \
--engine-dump-dir /root/pi0_calibration/engine_paligemma_kv_real50 \
--output-dir calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_fixed16_paligemma_hbm_kv_real50 \
--prompt 'Place the RDK camera box on top of the black MCU box.' \
--save-expert-kv
# 终端 B
cd /root/rdk_LeRobot_tools/models/pi0
PI0_STANDALONE_DUMP_DIR=/root/pi0_calibration/engine_paligemma_kv_real50 \
./run_pi0_standalone_config.sh configs/deployments/<PALIGEMMA_STAGE_CONFIG>.json
把生成的 50×36 个 KV 文件传回量化服务器。
3. Expert
export PALIGEMMA_HBM_KV=/host/gemma/calibration_data/pi0_full_v5_2cam_100ep_015000_siglip_fixed16_paligemma_hbm_kv_real50
python3 models/pi0/tools/quantize_expert_real_calib.py \
--model-dir "$MODEL_DIR" \
--calibration-dir "$CALIB_DIR" \
--paligemma-kv-dir "$PALIGEMMA_HBM_KV" \
--output-dir "$QUANT_OUT/expert" \
--device cuda:0 \
--vision-tokens-num 256 \
--max-samples 50 \
--jobs 20 \
--march nash-p \
--transformer-linear-mode dynamic \
--projection-linear-mode dynamic \
--attention-matmul-mode dynamic \
--output-linear-mode dynamic
量化顺序不能交换:
SigLIP HBM
→ 板端真实 vision embedding
→ PaliGemma HBM
→ 板端真实 36 组 KV
→ Expert HBM
A.6 部署并验证新 HBM
将三份 HBM、PaliGemma 生成的 prompt embedding、checkpoint 对应的 normalization stats 放入新的版本目录,不覆盖旧版本:
ssh root@<S600_IP> 'mkdir -p /root/pi0_models/versions/<BUNDLE_NAME>/{siglip,paligemma,expert}'
scp "$QUANT_OUT/siglip/pi0_siglip_ptq.hbm" \
root@<S600_IP>:/root/pi0_models/versions/<BUNDLE_NAME>/siglip/
scp "$QUANT_OUT/paligemma/pi0_gemma_llm_ptq.hbm" \
root@<S600_IP>:/root/pi0_models/versions/<BUNDLE_NAME>/paligemma/
scp "$QUANT_OUT/paligemma/fixed_prompt_embedding.bin" \
root@<S600_IP>:/root/pi0_models/versions/<BUNDLE_NAME>/paligemma/
scp "$QUANT_OUT/expert/pi0_gemma_expert_ptq.hbm" \
root@<S600_IP>:/root/pi0_models/versions/<BUNDLE_NAME>/expert/
scp "$NORM_STATS" \
root@<S600_IP>:/root/rdk_LeRobot_tools/models/pi0/configs/norm_stats_<BUNDLE_NAME>.json
为新 bundle 新建 deployment JSON 与 manifest,写入实际路径、文件大小和 SHA256;若要复用 run_live_sync.sh,还必须同步更新其中的 expected hashes。先做静态检查:
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python validate_pi0_config.py \
configs/deployments/<NEW_DEPLOYMENT_CONFIG>.json
再做不访问机械臂串口的双图整链 smoke test:
cd /root/rdk_LeRobot_tools/models/pi0
/home/sunrise/lerobot/.venv/bin/python -u pi0_standalone_offline.py \
--config configs/deployments/<NEW_DEPLOYMENT_CONFIG>.json \
--front /path/to/front.jpg \
--side /path/to/side.jpg \
--state 0 0 0 0 0 0 \
--output-dir diagnostics/offline_smoke_$(date +%Y%m%d_%H%M%S)
预期动作输出 shape:
[50,6]
A.7 启动已验证的 S600 同步真机推理
cd /root/rdk_LeRobot_tools/models/pi0
./run_live_sync.sh








