Arm: how much of each LeRobot policy the Arm backend (VGF, Ethos-U85) takes, and what blocks the rest
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 43/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
调研方向
Start with the ExecuTorch table under “Known issues” and run the short repro for one row using the ExecuTorch 1.6.0 wheel with its Arm extras; no device is needed. The issue lists six problems, including quantizer failures, unsupported lowerings, and slow partitioning, but does not select one to fix. Done means addressing an unowned problem and confirming its repro succeeds or the documented behavior changes.
由索引模型根据 Issue 内容生成。
描述
This issue tracks LeRobot policies exported to ExecuTorch with its Arm backend: the VGF path (VgfPartitioner, Arm's ML extensions for Vulkan) and the Ethos-U85 NPU (EthosUPartitioner, int8). It shows how much of each policy the backend takes, whether the program exports, and what each project still needs to look at. Other backends have their own issues: TensorRT in #24353, OpenVINO in #24325, XNNPACK in #24667, Qualcomm in #24668, Core ML and MLX in #24669, the CUDA backend in #24684, and Vulkan in #24688.
Why this matters for LeRobot: many low-cost robot boards are built on Arm chips, for example the Raspberry Pi 5 that LeRobot's LeKiwi mobile robot carries. The Arm CPU itself is XNNPACK's job (#24667). The Arm backend covers Arm's accelerators: VGF targets Arm GPUs through Arm's ML extensions for Vulkan (on GPUs without native support they run through Arm's emulation layer), and Ethos-U is Arm's NPU for microcontrollers and small chips, so only the smallest policies can fit it. This issue checks export only so far; nothing has run on a device yet.
Coverage
VGF (fp32)
| Policy | Ops on VGF | Heavy math on VGF | VGF programs | Exports to .pte |
|---|---|---|---|---|
| act | 1,479 / 1,480 (100%) | 100% | 1 | $\color{#2da44e}{\checkmark}$ |
| diffusion | 596 / 603 (99%) | 96% | 5 | $\color{#2da44e}{\checkmark}$ |
| dm05 | ⏳ | ⏳ | ⏳ | ⏳ |
| eo1 | ⏳ | ⏳ | ⏳ | ⏳ |
| evo1 | 10,383 / 12,347 (84%) with +bf16: 12,334 / 12,347 (100%) |
26% with +bf16: 100% |
61 with +bf16: 4 |
$\color{#d4a017}{\textsf{slow}}$ with +bf16: $\color{#d4a017}{\textsf{slow}}$ |
| fastwam | ⏳ | ⏳ | ⏳ | ⏳ |
| fineart_vla | 16,817 / 16,904 (99%) | 100% | 4 | $\color{#d4a017}{\textsf{slow}}$ |
| flux3 | ⏳ | ⏳ | ⏳ | ⏳ |
| gaussian_actor | 104 / 121 (86%) | 100% | 4 | $\color{#2da44e}{\checkmark}$ |
| groot | ⏳ | ⏳ | ⏳ | ⏳ |
| lawam | 12,118 / 14,419 (84%) | 44% | 132 | $\color{#d4a017}{\textsf{slow}}$ |
| lingbot_va | ⏳ | ⏳ | ⏳ | ⏳ |
| lingbot_vla_v2 | ⏳ | ⏳ | ⏳ | ⏳ |
| molmoact2 | ⏳ | ⏳ | ⏳ | ⏳ |
| multi_task_dit | 1,109 / 1,113 (100%) | 100% | 3 | $\color{#2da44e}{\checkmark}$ |
| pi0 | ⏳ | ⏳ | ⏳ | ⏳ |
| pi05 | ⏳ | ⏳ | ⏳ | ⏳ |
| pi0_fast | 5,159 / 5,191 (99%) | 100% | 5 | $\color{#d4a017}{\textsf{slow}}$ |
| smolvla | 2,859 / 5,836 (49%) with +bf16: 5,636 / 5,836 (97%) |
16% with +bf16: 100% |
177 with +bf16: 5 |
$\color{#2da44e}{\checkmark}$ with +bf16: $\color{#2da44e}{\checkmark}$ |
| tdmpc | 3,532 / 3,617 (98%) | 100% | 14 | $\color{#2da44e}{\checkmark}$ |
| vla_jepa | 5,781 / 8,877 (65%) with +bf16: 8,843 / 8,877 (100%) |
6% with +bf16: 100% |
192 with +bf16: 11 |
$\color{#d4a017}{\textsf{slow}}$ with +bf16: $\color{#2da44e}{\checkmark}$ |
| vqbet | 469 / 483 (97%) | 100% | 3 | ❌ |
| wall_oss_05 | ⏳ | ⏳ | ⏳ | ⏳ |
| wall_x | ⏳ | ⏳ | ⏳ | ⏳ |
| xvla | 13,146 / 13,148 (100%) | 100% | 1 | ❌ |
Ethos-U85 (int8, small policies only)
| Policy | Ops on Ethos-U85 | Heavy math on Ethos-U85 | Ethos-U85 programs | Exports to .pte |
|---|---|---|---|---|
| act | 1,655 / 1,656 (100%) | 100% | 1 | $\color{#2da44e}{\checkmark}$ |
| diffusion | 765 / 1,001 (76%) | 96% | 32 | $\color{#2da44e}{\checkmark}$ |
| gaussian_actor | 120 / 130 (92%) | 100% | 3 | $\color{#2da44e}{\checkmark}$ |
| multi_task_dit | 1,618 / 1,623 (100%) | 100% | 4 | $\color{#2da44e}{\checkmark}$ |
| tdmpc | ⏳ | ⏳ | ⏳ | ⏳ |
| vqbet | 757 / 772 (98%) | 100% | 5 | $\color{#2da44e}{\checkmark}$ |
Legend:
- "Ops on ...": the graph ops the partitioner takes, out of all ops, over all programs of the policy. Ops are counted after
to_edge. - "Heavy math on ...": the share of multiply-adds in matmuls, linear layers, convolutions and attention that the backend takes. A program counts as many times as one action chunk runs it (for example, once per denoising step).
- "... programs": how many delegate partitions the policy is split into. Between 2 partitions, the data goes back to the CPU.
- "Exports to .pte": $\color{#2da44e}{\checkmark}$ means
to_edge_transform_and_lower(...).to_executorch()finished. $\color{#d4a017}{\textsf{slow}}$ means partitioning or lowering did not finish within 15 minutes; a longer run is queued. ❌ means it fails; the cause is in the tables below. - ⏳: this cell is being measured (running or queued).
Notes:
- So far this is export only, with
VgfCompileSpec()andEthosUCompileSpec("ethos-u85-256")at their defaults. - Ethos-U85 runs are int8, quantized with the Arm backend's
EthosUQuantizer(symmetric, per channel) on the one frame the export traces. They cover only the small policies; the larger ones cannot fit a microcontroller NPU. - LeRobot has no Arm backend for
lerobot-exportyet (LeRobot table). So each policy is traced with the trace step oflerobot-export, the same way as the Vulkan rows of #24688, and each traced program is lowered with the Arm partitioner. - diffusion, dm05, eo1, fineart_vla, gaussian_actor, lingbot_vla_v2, molmoact2, multi_task_dit, tdmpc, vla_jepa, vqbet and wall_x use random weights, made by the same script and seed as in #24353. The other policies use their real weights.
- evo1 and smolvla keep their bf16 weights, so with the default spec their bf16 parts stay on the CPU (row 5 below).
Built on
- ExecuTorch: the official 1.6.0 wheel (the final release) with its
vgfandethos-uextras from pip: tosa-tools 2026.5.0, ethos-u-vela 5.2.0, ai_ml_sdk_model_converter 0.11.0, ai_ml_emulation_layer_for_vulkan 0.11.0. No source build and no patches. - torch 2.15.0, transformers 5.22.1.
- LeRobot: the
lerobot-export-stackbranch of shoumikhin/lerobot as it was on Nov 3, pinned by theexport-stack-2026-11-03tag (see #24353).
Known issues
"Fix" is an open pull request unless it says merged. "Needs a decision" is a design question for the maintainers. "Needs an owner" means nobody is fixing it yet.
ExecuTorch
- Pick up the rows marked "needs an owner". The first 2 rows block whole policies.
- Every row has a short repro below the table. It needs only the ExecuTorch 1.6.0 wheel with its Arm extras, no device.
| Problem | Policies | Fix | Status |
|---|---|---|---|
The Arm quantizer fails on the module that ExportedProgram.module() returns by default in torch 2.15: the guard node it adds is a call_module, and prepare_pt2e raises call_module is not supported. module(check_guards=False) works. |
every Ethos-U85 export | needs an owner | Repro ready (repro 1) |
An int64 index that export lifts as a tensor constant (not a buffer) fails the whole VGF lowering: index.Tensor requires index dtype must be int32. The same index as a buffer is cast to int32 and works. |
vqbet, xvla | needs an owner | Repro ready (repro 2) |
| The Ethos-U quantizer does not annotate Mish, so each Mish stays in float on the CPU and splits the graph. | diffusion (38 Mish, 32 programs) | needs an owner | Repro ready (repro 3) |
No TOSA lowering for transposed 1D convolution, argmin, topk with K above 4, rows picked by such indices, or int64 index math. Each one cuts the graph. |
diffusion (transposed conv1d), tdmpc (its planner's argmin, topk and index math) | needs an owner | Repro ready (repro 4) |
bf16 is not in the default VGF TOSA spec, so bf16 ops stay on the CPU with no warning. Adding +bf16 to the spec makes them lower. |
evo1, smolvla | needs a decision: enable bf16 by default, or warn | Repro ready (repro 5) |
Partitioning big graphs is slow. The Arm partitioners use torch.fx's CapabilityBasedPartitioner, and its merge step grows much faster than the graph (same as Vulkan repro 4 in #24688). |
evo1, fineart_vla, and the larger policies | needs an owner | Repro ready (repro 6): 200 blocks take 19 s, 400 blocks take 134 s |
Repros (export only, ExecuTorch 1.6.0, torch 2.15.0)
import time
import torch
from executorch.backends.arm.ethosu import EthosUCompileSpec, EthosUPartitioner
from executorch.backends.arm.quantizer import EthosUQuantizer, get_symmetric_quantization_config
from executorch.backends.arm.vgf import VgfCompileSpec, VgfPartitioner
from executorch.exir import EdgeCompileConfig, to_edge, to_edge_transform_and_lower
from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e
CONFIG = EdgeCompileConfig(_check_ir_validity=False)
SKIP = ("executorch_call_delegate", "getitem", "quantize_per", "dequantize_per")
class Fn(torch.nn.Module):
def __init__(self, fn, *modules):
super().__init__()
self.fn = fn
self.layers = torch.nn.ModuleList(modules)
def forward(self, x):
return self.fn(self, x)
def summary(edge):
"""How many Arm programs the model lowers to, and which ops stay on the CPU."""
names = [str(n.target).split("::")[-1].split("(")[0]
for n in edge.exported_program().graph.nodes if n.op == "call_function"]
return names.count("executorch_call_delegate"), sorted({n for n in names if not any(s in n for s in SKIP)})
def root(error):
while error.__cause__ is not None:
error = error.__cause__
return f"{type(error).__name__}: {str(error).splitlines()[0][:140]}"
def vgf(model, x, spec=None):
try:
ep = torch.export.export(model.eval(), (x,))
edge = to_edge_transform_and_lower(ep, partitioner=[VgfPartitioner(VgfCompileSpec(spec))], compile_config=CONFIG)
return summary(edge)
except Exception as error:
return root(error)
def quantizer():
q = EthosUQuantizer(EthosUCompileSpec("ethos-u85-256"))
q.set_global(get_symmetric_quantization_config(is_per_channel=True))
return q
def u85(model, x):
m = prepare_pt2e(torch.export.export(model.eval(), (x,)).module(check_guards=False), quantizer())
m(x)
ep = torch.export.export(convert_pt2e(m), (x,))
edge = to_edge_transform_and_lower(ep, partitioner=[EthosUPartitioner(EthosUCompileSpec("ethos-u85-256"))],
compile_config=CONFIG)
return summary(edge)
def case(label, result):
print(f"{label:46s} -> {result}", flush=True)
# 1. The Arm quantizer fails on the module that ExportedProgram.module() returns by default.
conv_mean = Fn(lambda s, t: s.layers[0](t).mean(dim=(2, 3)), torch.nn.Conv2d(3, 8, 3))
ep = torch.export.export(conv_mean.eval(), (torch.randn(1, 3, 16, 16),))
for check_guards in (True, False):
try:
prepare_pt2e(ep.module(check_guards=check_guards), quantizer())
result = "OK"
except Exception as error:
result = root(error)
case(f"1. module(check_guards={check_guards}) -> prepare_pt2e", result)
# 2. An int64 index that is a tensor constant (not a buffer) fails the whole lowering.
class Buffer(torch.nn.Module):
def __init__(self):
super().__init__()
self.register_buffer("idx", torch.tensor([0, 2]))
def forward(self, x):
return x[:, self.idx]
class Constant(torch.nn.Module):
def __init__(self):
super().__init__()
self.idx = torch.tensor([0, 2]) # a plain tensor attribute, which export lifts as a constant
def forward(self, x):
return x[:, self.idx]
x = torch.randn(1, 4, 8)
case("2. VGF x[:, idx], idx an int64 buffer", vgf(Buffer(), x))
case("2. VGF x[:, idx], idx an int64 constant", vgf(Constant(), x))
# 3. The Ethos-U quantizer does not annotate Mish, so it stays in float on the CPU.
def block(act):
return torch.nn.Sequential(torch.nn.Conv1d(16, 16, 3, padding=1), act, torch.nn.Conv1d(16, 16, 3, padding=1))
x = torch.randn(1, 16, 32)
for label, act in (("SiLU", torch.nn.SiLU()), ("ELU", torch.nn.ELU()), ("Mish", torch.nn.Mish())):
case(f"3. U85 Conv1d -> {label} -> Conv1d", u85(block(act), x))
# 4. Ops with no TOSA lowering stay on the CPU and split the graph.
case("4. VGF ConvTranspose1d", vgf(torch.nn.ConvTranspose1d(16, 8, 4, stride=2, padding=1), x))
case("4. VGF ConvTranspose2d (for comparison)", vgf(torch.nn.ConvTranspose2d(16, 8, 4, stride=2, padding=1), x.unsqueeze(-1)))
s = torch.randn(64, 16)
case("4. VGF argmax (for comparison)", vgf(Fn(lambda _, t: t.argmax(-1).to(torch.int32)), s))
case("4. VGF argmin", vgf(Fn(lambda _, t: t.argmin(-1).to(torch.int32)), s))
case("4. VGF topk(4) of a [64, 16] score", vgf(Fn(lambda _, t: t.topk(4).values), s))
case("4. VGF topk(8) of a [64, 16] score", vgf(Fn(lambda _, t: t.topk(8).values), s))
case("4. VGF rows picked by topk(8) indices", vgf(Fn(lambda _, t: t[t.sum(-1).topk(8).indices]), s))
case("4. VGF index % 7 (int64 math)", vgf(Fn(lambda _, t: (t.argmax(-1) % 7).to(torch.float32)), s))
# 5. bf16 is not in the default VGF TOSA spec, so bf16 ops stay on the CPU without a warning.
lin16 = torch.nn.Linear(64, 64).to(torch.bfloat16)
x16 = torch.randn(4, 64, dtype=torch.bfloat16)
case("5. VGF bf16 Linear, default VgfCompileSpec()", vgf(lin16, x16))
case("5. VGF bf16 Linear, TOSA spec with +bf16", vgf(lin16, x16, "TOSA-1.0+FP+INT+int4+int16+bf16"))
# 6. Partitioning time grows fast with graph size when CPU ops sit on side branches.
class Blocks(torch.nn.Module):
def __init__(self, n):
super().__init__()
self.layers = torch.nn.ModuleList(torch.nn.Linear(32, 32) for _ in range(n))
def forward(self, x):
for layer in self.layers:
a = layer(x)
x = a * torch.sigmoid(a) + torch.atan2(a, a + 2) # atan2 stays on the CPU
return x
for n in (50, 100, 200, 400):
ep = to_edge(torch.export.export(Blocks(n).eval(), (torch.randn(1, 32),)), compile_config=CONFIG).exported_program()
start = time.time()
tags = VgfPartitioner(VgfCompileSpec()).partition(ep).partition_tags
print(f"6. {n:4d} blocks: {len(tags):4d} VGF partitions, partition() took {time.time() - start:6.1f} s", flush=True)
Output:
1. module(check_guards=True) -> prepare_pt2e -> ExportPassBaseError: call_module is not supported.
1. module(check_guards=False) -> prepare_pt2e -> OK
2. VGF x[:, idx], idx an int64 buffer -> (1, [])
2. VGF x[:, idx], idx an int64 constant -> AssertionError: index.Tensor requires index dtype must be int32
3. U85 Conv1d -> SiLU -> Conv1d -> (1, [])
3. U85 Conv1d -> ELU -> Conv1d -> (1, [])
3. U85 Conv1d -> Mish -> Conv1d -> (2, ['exp', 'gt.Scalar', 'log1p', 'mul.Tensor', 'tanh', 'where.self'])
4. VGF ConvTranspose1d -> (0, ['convolution'])
4. VGF ConvTranspose2d (for comparison) -> (1, [])
4. VGF argmax (for comparison) -> (1, [])
4. VGF argmin -> (0, ['_to_dim_order_copy', 'argmin'])
4. VGF topk(4) of a [64, 16] score -> (1, [])
4. VGF topk(8) of a [64, 16] score -> (0, ['topk'])
4. VGF rows picked by topk(8) indices -> (2, ['_to_dim_order_copy', 'topk'])
4. VGF index % 7 (int64 math) -> (0, ['_to_dim_order_copy', 'argmax', 'remainder.Scalar'])
5. VGF bf16 Linear, default VgfCompileSpec() -> (0, ['linear'])
5. VGF bf16 Linear, TOSA spec with +bf16 -> (1, [])
6. 50 blocks: 51 VGF partitions, partition() took 0.4 s
6. 100 blocks: 101 VGF partitions, partition() took 2.2 s
6. 200 blocks: 201 VGF partitions, partition() took 18.9 s
6. 400 blocks: 401 VGF partitions, partition() took 133.6 s
LeRobot
- Decide the rows marked "needs a decision".
| Problem | Policies | Fix | Status |
|---|---|---|---|
lerobot-export has no Arm backend, so there is no Arm bundle for lerobot-rollout --bundle.path to run. |
all | needs a decision: an executorch_arm backend in LeRobot, next to executorch_xnnpack and executorch_openvino |
Open |
| LeRobot's export folds constants after quantizing, which turns the dequantized weights back into float, so the Ethos-U partitioner rejects convolutions and linear layers as "not quantized". Folding before quantizing works. | all Ethos-U85 exports | needs an owner (LeRobot) | Measured |
lerobot-export and the torch-free lerobot-rollout --bundle.path exist only on the lerobot-export-stack branch. |
all | needs a decision: how to bring them upstream in reviewable pieces (same row as #24684) | Open |
cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani
- 主要语言
- Python
- 星标
- 5.2k
- 派生
- 1.2k
- 平均合并
- 2 天 37 分钟
- 30 天内合并 PR
- 693
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
pytorch/executorch 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 74/100
pytorch/executorch#24434 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
pytorch/executorch#24410 ·
维护者通常 1 天内回复
-
enhancement triaged
难度 2/5 半天 新手友好度 68/100
pytorch/executorch#21640 ·
维护者通常 1 天内回复
-
Vulkan: how much of each LeRobot policy the Vulkan backend takes, and what blocks the rest可能已有人在做 @SS-JIA 今天认领。 未关闭module: vulkan
pytorch/executorch#24688 · 已指派 1 人 ·
维护者通常 1 天内回复
-
module: cuda
难度 5/5 一周以上 新手友好度 35/100
pytorch/executorch#24684 · 1 条评论 ·
维护者通常 1 天内回复
查看 pytorch/executorch 的全部 Issue
相似的 Issue
-
request-theme
难度 1/5 1 小时以内 新手友好度 78/100
LizardByte/ThemerrDB#9883 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 84/100
Project-MONAI/MONAI#9258 ·
维护者通常 3 天内回复
-
catalog:deprecation
难度 1/5 1 小时以内 新手友好度 88/100
surendranb/free-inference#31 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
fritzprix/libr-agent#2612 ·
维护者通常 1 天内回复
-
needs-triage presubmit-gate
难度 2/5 1-3 小时 新手友好度 76/100
gke-labs/kube-agents#5001 ·
维护者通常 1 天内回复