Arm: how much of each LeRobot policy the Arm backend (VGF, Ethos-U85) takes, and what blocks the rest
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 43/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Ambito
- embedded-iot, machine-learning
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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
- Lingua principale
- Python
- Stelle
- 5.2k
- Fork
- 1.2k
- Merge medio
- 2g 37m
- PR unite (30g)
- 693
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di pytorch/executorch
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
pytorch/executorch#24434 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
pytorch/executorch#24410 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement triaged
Difficoltà 2/5 Mezza giornata Idoneità per principianti 68/100
pytorch/executorch#21640 ·
I maintainer di solito rispondono entro 1 giorno
-
Vulkan: how much of each LeRobot policy the Vulkan backend takes, and what blocks the restForse già presa @SS-JIA l’ha presa oggi. Apertamodule: vulkan
pytorch/executorch#24688 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
module: cuda
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
pytorch/executorch#24684 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di pytorch/executorch
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
didi/helixharness#18 ·
-
[MOVIE]: Airport '77 (1977)Apertarequest-theme
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
LizardByte/ThemerrDB#9883 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
NousResearch/hermes-agent#149885 ·
I maintainer di solito rispondono entro 1 giorno
-
Set cache_ok = True on GUID TypeDecorator to enable SQLAlchemy query cachingForse già presa @patelshivani2283-lab l’ha presa oggi. Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 91/100
fedora-infra/anitya#2157 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
Project-MONAI/MONAI#9258 ·
I maintainer di solito rispondono entro 3 giorni