Skip to content

Commit 77b7250

Browse files
authored
Merge pull request #338 from open-edge-platform/update-branch
bug: robotic training tool convert.py is not functioning correctly (#884)
2 parents 868859a + e54948c commit 77b7250

File tree

3 files changed

+246
-23
lines changed

3 files changed

+246
-23
lines changed

usecases/robotic/training-ui/server/convert.py

Lines changed: 236 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,235 @@
1-
# Copyright (C) 2025 Intel Corporation
1+
# Copyright (C) 2025 Intel Corporation
22
# SPDX-License-Identifier: Apache-2.0
33

4+
import os
5+
import sys
6+
import time
7+
import logging
8+
import argparse
9+
import numpy as np
10+
from collections import deque, OrderedDict
11+
12+
import torch
13+
import openvino as ov
14+
import openvino.properties as props
15+
from openvino.runtime import serialize
16+
17+
from lerobot.policies.act.configuration_act import ACTConfig
18+
from lerobot.policies.act.modeling_act import ACTPolicy
19+
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
20+
from lerobot.datasets.utils import dataset_to_policy_features
21+
from lerobot.configs.types import FeatureType
22+
from lerobot.datasets.lerobot_dataset import LeRobotDataset
23+
24+
25+
DEBUG = os.environ.get("DEBUG", "0") == "1"
26+
logger = logging.getLogger(__name__)
27+
logging.basicConfig(level=logging.DEBUG if DEBUG else logging.INFO)
28+
29+
30+
def create_placeholder_observation(input_features, batch_size=1, dtype=torch.float32):
31+
"""Create placeholder tensors that mirror the model's expected inputs."""
32+
if not input_features:
33+
raise ValueError(
34+
"No input features provided for placeholder creation.")
35+
36+
data = OrderedDict()
37+
for feature_key, feature in input_features.items():
38+
if feature.shape is None:
39+
raise ValueError(
40+
f"Feature '{feature_key}' does not define a shape.")
41+
42+
full_shape = (batch_size, *feature.shape)
43+
# Use uniform random tensors to avoid introducing bias while keeping dtype consistent.
44+
data[feature_key] = torch.rand(full_shape, dtype=dtype)
45+
46+
return data
47+
48+
49+
class ACTPolicyWrapper(torch.nn.Module):
50+
"""
51+
Wrapper for ACTPolicy to make it traceable for OpenVINO conversion.
52+
This wrapper is stateless and returns the entire action chunk.
53+
The calling code is responsible for managing the action queue.
54+
"""
55+
56+
def __init__(self, policy):
57+
super().__init__()
58+
self.policy = policy
59+
self.input_keys = list(self.policy.config.input_features.keys())
60+
if self.policy.config.temporal_ensemble_coeff is not None:
61+
logger.warning(
62+
"The provided wrapper does not support temporal ensembling for tracing. "
63+
"Please use a model without temporal ensembling or implement external state management for it."
64+
"Setting temporal_ensemble_coeff to None."
65+
)
66+
self.policy.config.temporal_ensemble_coeff = None
67+
68+
@torch.no_grad()
69+
def forward(self, *inputs):
70+
"""
71+
This method wraps `predict_action_chunk` and is JIT-traceable.
72+
It returns a full chunk of predicted actions.
73+
"""
74+
if len(inputs) != len(self.input_keys):
75+
raise ValueError(
76+
f"Expected {len(self.input_keys)} inputs, received {len(inputs)}."
77+
)
78+
79+
batch = {
80+
feature_key: tensor for feature_key, tensor in zip(self.input_keys, inputs)
81+
}
82+
print(batch)
83+
84+
self.policy.eval()
85+
# predict_action_chunk is coming from ACTPolicy
86+
actions = self.policy.predict_action_chunk(batch)
87+
return actions
88+
89+
90+
def load_model(model_weight_path, dataset_dir):
91+
if not os.path.exists(model_weight_path):
92+
raise FileNotFoundError("Model weight path does not exist.")
93+
94+
if not os.path.exists(dataset_dir):
95+
raise FileNotFoundError("Dataset directory does not exist.")
96+
97+
dataset_metadata = LeRobotDatasetMetadata(
98+
"sample_lerobot_dataset",
99+
root=dataset_dir
100+
)
101+
features = dataset_to_policy_features(dataset_metadata.features)
102+
output_features = {
103+
key: ft for key, ft in features.items() if ft.type is FeatureType.ACTION
104+
}
105+
input_features = {
106+
key: ft for key, ft in features.items() if key not in output_features
107+
}
108+
logger.info(f"Input features: {input_features}")
109+
logger.info(f"Output features: {output_features}")
110+
111+
cfg = ACTConfig(
112+
input_features=input_features,
113+
output_features=output_features,
114+
)
115+
from lerobot.configs.policies import PreTrainedConfig
116+
cfg = PreTrainedConfig.from_pretrained(
117+
model_weight_path
118+
)
119+
120+
policy = ACTPolicy.from_pretrained(
121+
model_weight_path,
122+
config=cfg,
123+
)
124+
policy.to("cpu")
125+
logger.debug(f"Policy model info:\n{policy}")
126+
127+
return policy
128+
129+
130+
def convert_to_openvino(policy, output_dir):
131+
example_input = create_placeholder_observation(
132+
policy.config.input_features)
133+
134+
logger.info("Creating wrapper for JIT tracing...")
135+
wrapper = ACTPolicyWrapper(policy)
136+
wrapper.eval()
137+
138+
# Prepare individual tensor inputs for tracing
139+
example_inputs = tuple(example_input[key] for key in wrapper.input_keys)
140+
141+
logger.info("Tracing the model ...")
142+
try:
143+
with torch.no_grad():
144+
traced_policy = torch.jit.trace(
145+
wrapper,
146+
example_inputs,
147+
strict=False
148+
)
149+
except Exception as e:
150+
raise RuntimeError(
151+
f"Error during tracing: {e}. "
152+
"Ensure the model is compatible with JIT tracing and all inputs are tensors."
153+
)
154+
155+
logger.info("Testing traced model ...")
156+
with torch.no_grad():
157+
traced_output = traced_policy(*example_inputs)
158+
logger.debug(f"Traced output shape: {traced_output.shape}")
159+
logger.debug(f"Traced output: {traced_output}")
160+
161+
logger.info("Saving the traced model ...")
162+
traced_model_path = os.path.join(output_dir, "traced_model.pt")
163+
traced_policy.save(traced_model_path)
164+
logger.info(f"Traced model saved to: {traced_model_path}")
165+
166+
logger.info("Converting to OpenVINO ...")
167+
ov_model_path = os.path.join(output_dir, "model.xml")
168+
ov_input = []
169+
for tensor in example_inputs:
170+
np_dtype = tensor.numpy().dtype
171+
ov_dtype = ov.Type(np_dtype)
172+
ov_input.append((list(tensor.shape), ov_dtype))
173+
174+
ov_model = ov.convert_model(traced_policy, input=ov_input)
175+
ov.save_model(ov_model, ov_model_path)
176+
177+
core = ov.Core()
178+
saved_model = core.read_model(ov_model_path)
179+
for i, inp in enumerate(saved_model.inputs):
180+
# Set the name according to the original feature keys
181+
inp.get_tensor().set_names({wrapper.input_keys[i]})
182+
for i, out in enumerate(saved_model.outputs):
183+
out.get_tensor().set_names({f"outputs_{i}"})
184+
185+
logger.info(f"{saved_model}")
186+
saved_xml_path = os.path.join(output_dir, "ov_model.xml")
187+
saved_bin_path = os.path.join(output_dir, "ov_model.bin")
188+
serialize(saved_model, saved_xml_path, saved_bin_path)
189+
logger.info(f"OpenVINO model saved to: {saved_xml_path}")
190+
191+
192+
def evaluate_ov_model(ov_model_path, example_input, device="CPU", enable_npu_high_precision=False):
193+
"""Evaluate the OpenVINO model with example input."""
194+
core = ov.Core()
195+
if not os.path.exists(ov_model_path):
196+
raise FileNotFoundError(
197+
f"OpenVINO model path does not exist: {ov_model_path}")
198+
199+
ov_model = core.read_model(ov_model_path)
200+
201+
# Priotize latency for real-time applications
202+
compile_properties = {
203+
props.hint.performance_mode(): props.hint.PerformanceMode.LATENCY
204+
}
205+
206+
if device == "NPU" and enable_npu_high_precision:
207+
logger.info("Enabling NPU high precision mode for specific layers...")
208+
compile_properties["NPU_COMPILATION_MODE_PARAMS"] = "compute-layers-with-higher-precision=Sqrt,Power,ReduceMean,Add"
209+
210+
compiled_model = core.compile_model(ov_model, device, compile_properties)
211+
inputs = {
212+
input_tensor.any_name: example_input[key].numpy()
213+
for input_tensor, key in zip(compiled_model.inputs, example_input.keys())
214+
}
215+
216+
execution_time = []
217+
for i in range(5):
218+
logger.info(f"Warm-up run {i + 1} ...")
219+
results = compiled_model(inputs)
220+
221+
for i in range(100):
222+
logger.info(f"Inferencing run {i + 1} ...")
223+
start_time = time.time()
224+
results = compiled_model(inputs)
225+
execution_time.append(time.time() - start_time)
226+
227+
logger.info(
228+
f"[{device}] - Average execution time over 100 runs: {np.mean(execution_time):.6f} seconds")
229+
# The output is a dictionary, get the result tensor
230+
return list(results.values())[0]
231+
232+
4233
def main():
5234
parser = argparse.ArgumentParser(
6235
description="Convert Lerobot ACT model to OpenVINO format."
@@ -17,12 +246,6 @@ def main():
17246
required=True,
18247
help="Path to the dataset directory."
19248
)
20-
parser.add_argument(
21-
"--output-dir",
22-
type=str,
23-
default="./data/ov_models",
24-
help="Directory to save the converted OpenVINO model."
25-
)
26249
parser.add_argument(
27250
"--run-eval",
28251
action="store_true",
@@ -43,24 +266,16 @@ def main():
43266

44267
args = parser.parse_args()
45268

46-
# ---- NEVER STORE TAINTED PATHS ----
269+
output_dir = "./data/ov_models"
47270
model_weight_dir = args.model_weight_dir
48271
dataset_dir = args.dataset_dir
49272
run_eval = args.run_eval
50273
eval_device = args.eval_device
51274
enable_npu_high_precision = args.enable_npu_high_precision
52275

53-
# ---- SANITIZE OUTPUT DIR (NEW VARIABLE) ----
54-
try:
55-
safe_output_dir = validate_output_dir(args.output_dir)
56-
except ValueError as e:
57-
print(f"Invalid output_dir: {e}", file=sys.stderr)
58-
sys.exit(1)
59-
60-
# ---- SAFE SINK ----
276+
safe_output_dir = os.path.abspath(output_dir)
61277
os.makedirs(safe_output_dir, exist_ok=True)
62278

63-
# ---- DATASET SETUP ----
64279
dataset_repo_id = os.path.basename(os.path.normpath(dataset_dir))
65280
dataset_root = os.path.abspath(dataset_dir)
66281
dataset = LeRobotDataset(
@@ -78,7 +293,6 @@ def main():
78293
)
79294
policy.config.temporal_ensemble_coeff = None
80295

81-
# ---- USE SAFE PATH ONLY ----
82296
convert_to_openvino(policy, safe_output_dir)
83297

84298
if run_eval:
@@ -117,4 +331,7 @@ def main():
117331

118332
logger.info("### Conversion Summary ###")
119333
logger.info(f"- Max difference: {max_diff:.4f}")
120-
logger.info(f"- Mean difference: {mean_diff:.4f}")
334+
logger.info(f"- Mean difference: {mean_diff:.4f}")
335+
336+
if __name__ == "__main__":
337+
main()

usecases/robotic/training-ui/server/inference.sh

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ set -e
77

88
rm -rf /home/user/.cache/huggingface/lerobot/lerobot/eval_pick888
99

10-
CAMERA_CONFIG='{ hand: {type: opencv, index_or_path: /dev/video0, width: 640, height: 480, fps: 25}, front: {type: opencv, index_or_path: /dev/video2, width: 640, height: 480, fps: 15}}'
11-
POLICY_PATH="./output/a6a9bee0-627b-428e-b049-dad237d880cc/pick888/checkpoints/last/pretrained_model"
10+
POLICY_PATH=""
11+
CAMERA_CONFIG='{ hand: {type: opencv, index_or_path: /dev/video0, width: 640, height: 480, fps: 30}, front: {type: opencv, index_or_path: /dev/video2, width: 640, height: 480, fps: 25}}'
1212
OPENVINO_MODEL_PATH="./data/ov_models/ov_model.xml"
1313
OPENVINO_DEVICE="${OPENVINO_DEVICE:-CPU}"
1414

@@ -34,6 +34,12 @@ run_openvino() {
3434
python3 inference.py "${COMMON_ARGS[@]}" --openvino_model_path="$OPENVINO_MODEL_PATH" --openvino_device="$OPENVINO_DEVICE"
3535
}
3636

37+
if [ -z "$POLICY_PATH" ]; then
38+
echo "Please put the pretrained model path in POLICY_PATH first before running the script. Rerun the script after making the changes."
39+
exit 1
40+
fi
41+
echo "Make sure you have the correct configurations of camera as your training setup. Edit the CAMERA_CONFIG if required."
42+
3743
read -rp "Select inference backend (openvino/pytorch) [openvino]: " backend
3844
backend=${backend:-openvino}
3945
backend=${backend,,}
@@ -49,4 +55,4 @@ case "$backend" in
4955
echo "Unsupported backend: $backend" >&2
5056
exit 1
5157
;;
52-
esac
58+
esac

usecases/robotic/training-ui/server/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ gym-hil
66
opencv-python
77
pyrealsense2
88
feetech-servo-sdk
9-
9+
openvino==2025.4.0

0 commit comments

Comments
 (0)