feat(export): add modular policy export with ONNX and OpenVINO runtime#3493
Open
samet-akcay wants to merge 16 commits intohuggingface:mainfrom
Open
feat(export): add modular policy export with ONNX and OpenVINO runtime#3493samet-akcay wants to merge 16 commits intohuggingface:mainfrom
samet-akcay wants to merge 16 commits intohuggingface:mainfrom
Conversation
…ends Adds the lerobot.export package with the Exporter orchestrator, ONNX and OpenVINO backends, action_chunking and kv_cache runners, processor specs, and manifest schema. Includes fail-fast validation for missing normalization stats, tokenizer assets, and unknown OpenVINO devices. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Wires ACT through the action_chunking runner. Numerical parity verified at rtol=1e-5 / atol=1e-5 against PyTorch eager on both ONNX Runtime and OpenVINO. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…NX, 0.08 OpenVINO) Wires PI05 through the kv_cache runner. Includes a fp64→fp32 fix to create_sinusoidal_pos_embedding to eliminate Erf(double) drift in the chained Euler denoise loop. Numerical parity: rtol=0.02 / atol=0.006 on ONNX Runtime; rtol=0.08 / atol=0.08 on OpenVINO (cross-runtime IR optimization gap). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Adds docs/design/policy-export.md (design RFC) and examples/policy_export/walkthrough.ipynb (end-to-end reproducible walkthrough for ACT and PI05 across ONNX Runtime and OpenVINO). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Disambiguates the runner from generic KV-cache attention by encoding both load-bearing properties of PI05-style policies it serves: a prefix encoder with cached KV attention plus flow-matching Euler integration in the denoise loop. Leaves room for a future sibling runner (e.g. fused flow-matching for GR00T-style policies with no prefix encoder) without overloading the existing name. - src/lerobot/export/runners/kv_cache.py -> kv_cache_flow.py - KVCacheRunner -> KVCacheFlowRunner - KVCacheExportConfig -> KVCacheFlowExportConfig - manifest runner type 'kv_cache' -> 'kv_cache_flow' - PI05 get_inference_type() returns 'kv_cache_flow'
The processor-spec refactor moved export_assets, export_stats, and export_processor_specs onto PreTrainedPolicy, which the toy backend test's ToyPolicy(nn.Module) does not inherit. Add inline no-op stubs so the 'register a runner+backend without core edits' test exercises the public extension surface again.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a new modular “policy_package” export + torch-free runtime execution path for LeRobot policies, with ONNX serialization and OpenVINO runtime loading, and validates the contract via a comprehensive export test suite.
Changes:
- Introduces an export subsystem (manifest schema, backends, runners, policy runtime wrapper) and integrates it into
PreTrainedPolicy. - Implements executable runtime processors (normalize/denormalize, relative/absolute actions, PI05 state prep, tokenize) and runner patterns (
single_pass,kv_cache_flow) for ACT and PI05. - Adds extensive tests for manifest stability/schema, runner/backend registries, processors, and numerical parity (where deps are available).
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/export/test_runtime_processors.py | Tests that exported runtime processor specs execute (normalize/denormalize, relative/absolute, tokenize). |
| tests/export/test_runner_registry.py | Validates runner selection/registry behavior and runner export invariants. |
| tests/export/test_processor_specs.py | Checks processor spec construction + JSON roundtrips (incl. PI05 + normalization). |
| tests/export/test_manifest_stability.py | Ensures manifest output is stable across re-exports (excluding created_at). |
| tests/export/test_manifest.py | Adds schema + Normalizer behavior tests and fixture roundtrip validation. |
| tests/export/test_failfast_errors.py | Verifies fail-fast error messages for missing stats and manifest parse errors. |
| tests/export/test_export_pi05.py | End-to-end PI05 export/runtime parity and stage-wise accuracy tests (optional deps). |
| tests/export/test_export_act.py | End-to-end ACT export/runtime parity tests for ONNX/OpenVINO (optional deps). |
| tests/export/test_backend_registry.py | Verifies backend/runner registries, plugin guarantees, and toy backend/runner integration. |
| tests/export/fixtures/manifest_act_converged.json | Adds a converged ACT manifest fixture for stability/roundtrip checks. |
| tests/export/conftest.py | Shared fixtures/utilities for export tests (policy factories, parity helpers, tokenizer cache loader). |
| tests/export/init.py | Marks tests.export as a package for shared utilities. |
| src/lerobot/policies/pretrained.py | Adds export/to_onnx/to_openvino/from_exported APIs plus default stats/assets/processor-spec hooks. |
| src/lerobot/policies/pi05/modeling_pi05.py | Adds PI05 exportable modules, export protocol implementation, and tokenizer asset bundling. |
| src/lerobot/policies/act/modeling_act.py | Adds ACT export wrapper module and export protocol implementation for single-pass runner. |
| src/lerobot/export/runners/single_pass.py | New runner for feedforward chunk-emitting policies (ACT-style). |
| src/lerobot/export/runners/kv_cache_flow.py | New runner for KV-cache encode + iterative denoise flow (PI05-style). |
| src/lerobot/export/runners/base.py | Defines runner protocol, registry, ExportModule, and shared helpers. |
| src/lerobot/export/runners/init.py | Implements auto-discovery import of runner modules for registration. |
| src/lerobot/export/protocols.py | Defines Exportable protocol and ExportInputs contract used by policies/runners. |
| src/lerobot/export/processors/runtime.py | Implements torch-free runtime execution for exported processor specs. |
| src/lerobot/export/processors/pi05.py | Builds PI05-specific processor specs (relative/prepare_state/tokenize + absolute). |
| src/lerobot/export/processors/normalize.py | Builds normalize/denormalize processor specs from grouped modes/features. |
| src/lerobot/export/processors/init.py | Exposes processor-spec builders and runtime pipeline builder. |
| src/lerobot/export/policy.py | Adds ExportedPolicy runtime wrapper (pre/post processing + runner orchestration). |
| src/lerobot/export/normalize.py | Implements stats IO and normalization/denormalization for multiple modes. |
| src/lerobot/export/manifest.py | Introduces manifest dataclasses + (de)serialization and ProcessorSpec flattening. |
| src/lerobot/export/interfaces.py | Defines backend and runtime session protocols to decouple runners/backends. |
| src/lerobot/export/exporter.py | Implements export_policy pipeline: runner selection, backend serialization, manifest emission. |
| src/lerobot/export/configs.py | Adds export config dataclasses for runner families (single-pass, KV-cache flow). |
| src/lerobot/export/backends/openvino.py | Runtime-only OpenVINO backend that loads ONNX artifacts and runs compiled requests. |
| src/lerobot/export/backends/onnx.py | ONNX backend for serialization (torch.onnx.export) and runtime (onnxruntime). |
| src/lerobot/export/backends/base.py | Backend registry and artifact path resolution helper. |
| src/lerobot/export/backends/init.py | Implements auto-discovery import of backend modules for registration. |
| src/lerobot/export/_package_utils.py | Package utilities: example batch generation, hardware config, stats extraction, config snapshot. |
| src/lerobot/export/init.py | Public API surface for export/load of policy packages. |
| pyproject.toml | Adds export extra dependencies and updates spellchecker ignore patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Drop unreachable manifest-backend branch in _detect_backend_name(); ModelConfig has no backend field and Manifest.to_dict() drops unknown JSON, so the branch could never trigger. Backend selection is now documented as suffix-inferred or caller-passed. - Rewrite to_openvino() docstring to reflect actual behavior: manifest does not record the backend; users must pass backend='openvino' to load_exported_policy() (or rely on .onnx-suffix inference into ONNX). - Strip 'task' key from _TokenizeProcessor output so downstream runners (e.g. SinglePassRunner) that astype(np.float32) every value do not crash on the residual raw string. - Improve PI05 export_assets() error: identify the real failure (AutoTokenizer.from_pretrained(local_files_only=True) cache miss) and include the underlying exception instead of pointing at a directory that was just created.
build_processor_pipeline() requires Path; the relative_actions and unknown-processor tests previously passed package_path=None with # type: ignore[arg-type]. Use the existing tmp_path fixture so the type signature is honored without suppression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
This PR adds a modular export path for LeRobot policies. I limited the scope to two different policy families just to show the concept, but the design is scalable to cover all of the policies with different backends.
The main point is the shape:
User Examples
OpenVINO uses the same exported package contract:
PI05 uses the same runtime API, but the policy family is different:
PI05 example contract:
Package Shape
Example executable manifest processors:
{"type": "normalize", "features": ["observation.state"], "artifact": "stats.safetensors"} {"type": "tokenize", "artifact": "tokenizer"} {"type": "denormalize", "features": ["action"], "artifact": "stats.safetensors"}Internal Shape
No extra adapter stack is needed between LeRobot and runtime backends.
Adding A Backend
Pseudo-code for TensorRT:
Expected extension rule:
This should also fit future backends such as:
Adding A Policy
If the policy fits an existing runner:
If it needs a new runtime pattern:
Expected extension rule:
Why ACT + PI05
ACT proves the feedforward/action-chunk path:
PI05 proves the VLA/KV-cache/tokenized path:
Runtime processor types implemented in this PR:
Validation
Commands used locally:
Additional local ignored validation script:
Observed ACT real-checkpoint validation:
Observed real PI05 base validation:
The committed tests still use synthetic compact PI05 configs for CI-portable parity. The local script can validate the real checkpoint path, but it is intentionally ignored because it is multi-GB and slow.
Scope And Limits
Included:
Not included:
Known constraints:
Relationship to
lerobot-rolloutThe new
lerobot-rolloutCLI (#3413) standardizes the deployment loop and currently loads policies asPreTrainedPolicy(torchnn.Module) instances. Our PR is orthogonal such that it adds a torch-free runtime path so policies can be deployed on edge targets that cannot ship PyTorch.The two features compose well, but plugging an
ExportedPolicyintolerobot-rolloutrequires a small integration. I'd like maintainer input on the preferred shape before implementing it.A few options would be:
PolicyLikeprotocol inlerobot.rolloutinstead ofPretrainedPolicyPreTrainedPolicyin a torch-using bridge module.InferenceEnginefor exported policies — selectable via--inference.type=exported_*.Happy to land it as a separate small refactor PR first if that's preferred.
This is out of scope for this PR either way, so I'm flagging now so we can align on direction.