litert-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. examples/litert_cli.ipynb +313 -0
  2. examples/models/presets/default.py +19 -0
  3. examples/run_cli_demo.sh +38 -0
  4. examples/run_cli_npu.sh +89 -0
  5. examples/run_commands.sh +67 -0
  6. examples/run_models.sh +63 -0
  7. examples/run_smoke_tests.sh +58 -0
  8. examples/utils.ps1 +163 -0
  9. examples/utils.sh +184 -0
  10. litert_cli/__init__.py +15 -0
  11. litert_cli/commands/benchmark/__init__.py +16 -0
  12. litert_cli/commands/benchmark/android.py +212 -0
  13. litert_cli/commands/benchmark/cli.py +294 -0
  14. litert_cli/commands/benchmark/desktop.py +228 -0
  15. litert_cli/commands/benchmark/gcp.py +336 -0
  16. litert_cli/commands/clean.py +73 -0
  17. litert_cli/commands/compile.py +211 -0
  18. litert_cli/commands/convert/__init__.py +20 -0
  19. litert_cli/commands/convert/cli.py +255 -0
  20. litert_cli/commands/convert/generic.py +211 -0
  21. litert_cli/commands/convert/huggingface.py +175 -0
  22. litert_cli/commands/delete.py +56 -0
  23. litert_cli/commands/download.py +274 -0
  24. litert_cli/commands/import.py +124 -0
  25. litert_cli/commands/list.py +132 -0
  26. litert_cli/commands/lm.py +74 -0
  27. litert_cli/commands/quantize.py +193 -0
  28. litert_cli/commands/run/__init__.py +16 -0
  29. litert_cli/commands/run/android.py +394 -0
  30. litert_cli/commands/run/cli.py +297 -0
  31. litert_cli/commands/run/desktop.py +340 -0
  32. litert_cli/commands/visualize.py +234 -0
  33. litert_cli/core/android_utils.py +304 -0
  34. litert_cli/core/android_utils_test.py +236 -0
  35. litert_cli/core/constants.py +131 -0
  36. litert_cli/core/deps.py +180 -0
  37. litert_cli/core/deps_test.py +101 -0
  38. litert_cli/core/inputs.py +203 -0
  39. litert_cli/core/inputs_test.py +176 -0
  40. litert_cli/core/log_filters.py +50 -0
  41. litert_cli/core/models.py +96 -0
  42. litert_cli/core/npu_utils.py +382 -0
  43. litert_cli/core/targets_manager.py +192 -0
  44. litert_cli/core/utils.py +58 -0
  45. litert_cli/litert.py +119 -0
  46. litert_cli/litert_help_test.py +51 -0
  47. litert_cli/litert_test.py +88 -0
  48. litert_cli/models/__init__.py +145 -0
  49. litert_cli/models/asr/__init__.py +15 -0
  50. litert_cli/models/asr/asr_model.py +108 -0
  51. litert_cli/models/asr/parakeet_ctc.py +165 -0
  52. litert_cli/models/asr/runner.py +482 -0
  53. litert_cli/models/base.py +57 -0
  54. litert_cli/test_data/dummy_calib_data.py +26 -0
  55. litert_cli/test_data/dummy_cv_model.py +52 -0
  56. litert_cli/test_data/dummy_cv_model.tflite +0 -0
  57. litert_cli/test_data/generate_test_inputs.py +51 -0
  58. litert_cli/test_data/mobilenet_v3_calib_data.py +25 -0
  59. litert_cli/test_data/quantize_recipe.json +16 -0
  60. litert_cli/test_data/resnet18.py +31 -0
  61. litert_cli-0.1.0.dist-info/METADATA +38 -0
  62. litert_cli-0.1.0.dist-info/RECORD +67 -0
  63. litert_cli-0.1.0.dist-info/WHEEL +5 -0
  64. litert_cli-0.1.0.dist-info/entry_points.txt +2 -0
  65. litert_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
  66. litert_cli-0.1.0.dist-info/top_level.txt +3 -0
  67. tools/build_wheels.py +122 -0
@@ -0,0 +1,193 @@
1
+ # Copyright 2026 The LiteRT CLI Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Quantize a LiteRT model using AI Edge Quantizer."""
17
+
18
+ from __future__ import annotations
19
+
20
+ import importlib.util
21
+ import pathlib
22
+ import textwrap
23
+
24
+ import click
25
+ from litert_cli.core import deps
26
+
27
+
28
+ @click.command(
29
+ "quantize",
30
+ help=textwrap.dedent("""\
31
+ Quantize a LiteRT model.
32
+
33
+ MODEL: Path to the input .tflite model.
34
+
35
+ Examples:
36
+
37
+ Dynamic INT8 Quantization (Default):
38
+
39
+ $ litert quantize raw_model.tflite --output quant_model.tflite
40
+
41
+ Weight-Only INT8 Quantization:
42
+
43
+ $ litert quantize raw_model.tflite --output quant_model.tflite \
44
+ --recipe weight_only_wi8_afp32
45
+
46
+ Static Quantization (Requires calibration data):
47
+
48
+ $ litert quantize raw_model.tflite --recipe static_wi8_ai8 \
49
+ --calibration-data calib_data.py --output quant_model.tflite
50
+
51
+ Custom Recipe:
52
+
53
+ $ litert quantize raw_model.tflite --custom-recipe recipe.json \
54
+ --output quant_model.tflite
55
+ """),
56
+ )
57
+ @click.argument("model", type=str)
58
+ @click.option(
59
+ "--output",
60
+ type=click.Path(dir_okay=False, writable=True, path_type=pathlib.Path),
61
+ required=False,
62
+ default=None,
63
+ help="Path to save the output quantized .tflite model.",
64
+ )
65
+ @click.option(
66
+ "--recipe",
67
+ "quant_recipe",
68
+ type=str,
69
+ default="dynamic_wi8_afp32",
70
+ help=(
71
+ "Built-in quantization recipe to apply (e.g., dynamic_wi8_afp32,"
72
+ " weight_only_wi8_afp32, static_wi8_ai8). See"
73
+ " 'ai_edge_quantizer.recipe' for full list."
74
+ ),
75
+ )
76
+ @click.option(
77
+ "--calibration-data",
78
+ type=click.Path(
79
+ exists=True, dir_okay=False, resolve_path=True, path_type=pathlib.Path
80
+ ),
81
+ help=(
82
+ "Path to Python script providing calibration data (required for"
83
+ " 'static_wi8_ai8')."
84
+ ),
85
+ )
86
+ @click.option(
87
+ "--custom-recipe",
88
+ type=click.Path(
89
+ exists=True, dir_okay=False, resolve_path=True, path_type=pathlib.Path
90
+ ),
91
+ help="Path to JSON recipe file for custom configurations.",
92
+ )
93
+ @deps.require_extra("quantize")
94
+ def quantize_cmd(
95
+ model: str,
96
+ output: pathlib.Path | None,
97
+ quant_recipe: str,
98
+ calibration_data: pathlib.Path | None,
99
+ custom_recipe: pathlib.Path | None,
100
+ ) -> None:
101
+ r"""Quantize a LiteRT model.
102
+
103
+ Args:
104
+ model: Path to the input .tflite model or Model Reference.
105
+ output: Path to save the output quantized .tflite model.
106
+ quant_recipe: Built-in quantization recipe to apply.
107
+ calibration_data: Path to Python script providing calibration data.
108
+ custom_recipe: Path to JSON recipe file for custom configurations.
109
+
110
+ Raises:
111
+ click.UsageError: If calibration data is missing when required.
112
+ click.ClickException: If calibration script loading fails or is invalid.
113
+ """
114
+ from ai_edge_quantizer import quantizer as aeq # pylint: disable=g-import-not-at-top
115
+ from ai_edge_quantizer import qtyping # pylint: disable=g-import-not-at-top
116
+
117
+ from litert_cli.core import models as core_models
118
+
119
+ resolved_model_path, _ = core_models.resolve_model_reference(model)
120
+
121
+ if str(resolved_model_path) != str(model):
122
+ click.echo(f"Resolved model '{model}' to '{resolved_model_path}'")
123
+
124
+ model_path = pathlib.Path(resolved_model_path)
125
+
126
+ if quant_recipe == "static_wi8_ai8" and calibration_data is None:
127
+ raise click.UsageError(
128
+ "--calibration-data is required when --recipe is 'static_wi8_ai8'."
129
+ )
130
+
131
+ resolved_output = output or model_path.with_name(
132
+ f"{model_path.stem}_quant.tflite"
133
+ )
134
+
135
+ click.echo(f"Quantizing '{model_path}' to '{resolved_output}'...")
136
+ quantizer = aeq.Quantizer(str(model_path))
137
+
138
+ if custom_recipe:
139
+ click.echo(f"Loading custom recipe from '{custom_recipe}'...")
140
+ import json
141
+
142
+ with open(custom_recipe, "r") as f:
143
+ recipe_content = json.load(f)
144
+ quantizer.load_quantization_recipe(recipe_content)
145
+ else:
146
+ from ai_edge_quantizer import recipe as aeq_recipe
147
+
148
+ if hasattr(aeq_recipe, quant_recipe):
149
+ click.echo(f"Loading built-in recipe '{quant_recipe}'...")
150
+ recipe_obj = getattr(aeq_recipe, quant_recipe)()
151
+ quantizer.load_quantization_recipe(recipe_obj)
152
+ else:
153
+ click.echo(
154
+ f"Fallback configuring dynamic quantization for '{quant_recipe}'..."
155
+ )
156
+ quantizer.add_dynamic_config(
157
+ regex=".*",
158
+ operation_name=qtyping.TFLOperationName.ALL_SUPPORTED,
159
+ num_bits=8,
160
+ )
161
+
162
+ calibration_result = None
163
+ if quantizer.need_calibration:
164
+ if calibration_data is None:
165
+ raise click.UsageError(
166
+ "Calibration data is required for the specified recipe. Use "
167
+ "--calibration-data <script.py>"
168
+ )
169
+ click.echo(f"Loading calibration data from '{calibration_data}'...")
170
+
171
+ spec = importlib.util.spec_from_file_location(
172
+ "calib_module", str(calibration_data)
173
+ )
174
+ if spec is None or spec.loader is None:
175
+ raise click.ClickException(
176
+ f"Failed to load calibration script from '{calibration_data}'"
177
+ )
178
+ module = importlib.util.module_from_spec(spec)
179
+ spec.loader.exec_module(module)
180
+ if not hasattr(module, "get_calibration_data"):
181
+ raise click.ClickException(
182
+ "Calibration script must define 'get_calibration_data()' function."
183
+ )
184
+ calib_data = module.get_calibration_data()
185
+ click.echo("Running calibration...")
186
+ calibration_result = quantizer.calibrate(calib_data)
187
+
188
+ click.echo("Applying quantization...")
189
+ result = quantizer.quantize(calibration_result)
190
+ resolved_output.parent.mkdir(parents=True, exist_ok=True)
191
+ result.export_model(str(resolved_output), overwrite=True)
192
+
193
+ click.secho(f"Quantization complete! Saved to {resolved_output}", fg="green")
@@ -0,0 +1,16 @@
1
+ # Copyright 2026 The LiteRT CLI Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Empty init for run subpackage."""
@@ -0,0 +1,394 @@
1
+ # Copyright 2026 The LiteRT CLI Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+
16
+ """Android execution engine for LiteRT models.
17
+
18
+ Use adb to push the model and the run_model binary to the device, then run the
19
+ model on the device.
20
+
21
+ Usage Examples:
22
+ 1. Run a model on an Android device:
23
+ $ litert run /path/to/model.tflite --android
24
+
25
+ 2. Run with NPU acceleration and CPU fallback:
26
+ $ litert run /path/to/model.tflite --android --npu --cpu
27
+ OR
28
+ $ litert run /path/to/model.tflite --android --accelerator npu,cpu
29
+
30
+ 3. Run with custom inputs:
31
+ $ litert run /path/to/model.tflite --android --input input_name=value
32
+
33
+ 4. Run with multiple inputs:
34
+ $ litert run /path/to/model.tflite --android --input input1=value1 --input
35
+ input2=value2
36
+
37
+ 5. Run with specific signature:
38
+ $ litert run /path/to/model.tflite --android --signature_index 0
39
+
40
+ 6. Run with multiple iterations:
41
+ $ litert run /path/to/model.tflite --android --iterations 10
42
+
43
+ 7. Print tensor details:
44
+ $ litert run /path/to/model.tflite --android --print-tensors
45
+
46
+ 8. Run with sample size:
47
+ $ litert run /path/to/model.tflite --android --sample-size 100
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ from collections.abc import Sequence
53
+ import pathlib
54
+ import shlex
55
+ import subprocess
56
+ import tempfile
57
+ import time
58
+
59
+ import click
60
+ from litert_cli.core import android_utils
61
+ from litert_cli.core import constants
62
+ from litert_cli.core import inputs as inputs_utils
63
+ from litert_cli.core import npu_utils as npu
64
+
65
+
66
+ def _prepare_inputs_on_device(
67
+ *,
68
+ model_path: pathlib.Path,
69
+ inputs: Sequence[str],
70
+ signature_index: int,
71
+ android_root: str,
72
+ ) -> str:
73
+ """Parses inputs locally and pushes files to device, returns remote dir.
74
+
75
+ Args:
76
+ model_path: Local path to the LiteRT model file.
77
+ inputs: Tuple of input assignments (e.g. 'name=value').
78
+ signature_index: Signature index to run on device.
79
+ android_root: Remote workspace root path on Android device.
80
+
81
+ Returns:
82
+ Remote directory path where inputs are uploaded, or empty if no inputs.
83
+
84
+ Raises:
85
+ click.ClickException: If parsing fails or device push fails.
86
+ """
87
+ if not inputs:
88
+ return ""
89
+
90
+ try:
91
+ click.echo("Parsing inputs locally before pushing to device...")
92
+ from ai_edge_litert.compiled_model import CompiledModel # pylint: disable=g-import-not-at-top
93
+
94
+ cm = CompiledModel.from_file(str(model_path))
95
+ signatures = cm.get_signature_list()
96
+ if not signatures:
97
+ click.secho("No signatures found in the model for inputs.", fg="yellow")
98
+ return ""
99
+
100
+ sig_info = cm.get_signature_by_index(signature_index)
101
+ sig_key = sig_info["key"]
102
+ input_details = cm.get_input_tensor_details(sig_key)
103
+
104
+ # 2. Process input strings mapping (e.g., name=value or literal value).
105
+ parsed_inputs = {}
106
+ for inp in inputs:
107
+ if "=" in inp:
108
+ k, v = inp.split("=", 1)
109
+ parsed_inputs[k] = v
110
+ else:
111
+ parsed_inputs["_default_"] = inp
112
+
113
+ has_inputs = False
114
+ # 3. Convert literals or files to raw binaries in a local temporary
115
+ # directory.
116
+ with tempfile.TemporaryDirectory() as temp_dir:
117
+ temp_path = pathlib.Path(temp_dir)
118
+ for name, details in input_details.items():
119
+ input_data_str = parsed_inputs.get(name) or parsed_inputs.get(
120
+ "_default_"
121
+ )
122
+ if input_data_str:
123
+ shape = details.get("shape", [1])
124
+ tensor_type = details.get("dtype", "?")
125
+ np_dtype = inputs_utils.get_np_dtype(tensor_type)
126
+
127
+ click.echo(
128
+ f" Preparing input {name!r} from {input_data_str!r} (shape:"
129
+ f" {shape}, dtype: {tensor_type})"
130
+ )
131
+ data = inputs_utils.parse_input(input_data_str, shape, np_dtype)
132
+
133
+ raw_file_path = temp_path / f"{name}.raw"
134
+ data.tofile(raw_file_path)
135
+ has_inputs = True
136
+
137
+ if has_inputs:
138
+ # 4. Create remote directory and push input data batch tree to device.
139
+ remote_input_dir = f"{android_root}/inputs_{int(time.time())}"
140
+ click.echo(f"Pushing processed inputs to {remote_input_dir}...")
141
+ subprocess.run(
142
+ ["adb", "shell", f"mkdir -p {shlex.quote(remote_input_dir)}"],
143
+ check=True,
144
+ )
145
+ for local_file in temp_path.iterdir():
146
+ subprocess.run(
147
+ [
148
+ "adb",
149
+ "push",
150
+ local_file,
151
+ f"{remote_input_dir}/{local_file.name}",
152
+ ],
153
+ check=True,
154
+ stdout=subprocess.DEVNULL,
155
+ stderr=subprocess.DEVNULL,
156
+ )
157
+ return remote_input_dir
158
+
159
+ return ""
160
+
161
+ except Exception as e: # pylint: disable=broad-exception-caught
162
+ raise click.ClickException(
163
+ f"Failed to prepare inputs for Android model {model_path}: {e!r}"
164
+ ) from e
165
+
166
+
167
+ def run_android(
168
+ *,
169
+ model_path: str,
170
+ inputs: Sequence[str],
171
+ accelerator: str,
172
+ signature_index: int,
173
+ iterations: int,
174
+ print_tensors: bool,
175
+ sample_size: int,
176
+ ) -> None:
177
+ """Runs the model on an attached Android device using adb and run_model.
178
+
179
+ Args:
180
+ model_path: Local path to the LiteRT model file (.tflite).
181
+ inputs: Tuple of input assignments (e.g. 'name=value').
182
+ accelerator: Hardware accelerator ('cpu', 'gpu', 'npu').
183
+ signature_index: Signature index to execute.
184
+ iterations: Number of execute loops for remote runner.
185
+ print_tensors: Whether to print remote stats after execution completes.
186
+ sample_size: Limit execution sample stream print length per tensor.
187
+
188
+ Raises:
189
+ click.ClickException: On device error setup or failed execution triggers.
190
+ """
191
+ accel_list = [a.strip().lower() for a in accelerator.split(",") if a.strip()]
192
+ click.echo("Preparing to run on Android device via adb...")
193
+ android_utils.check_adb()
194
+
195
+ # Set up Android working directory
196
+ android_root = constants.LITERT_CLI_ANDROID_ROOT
197
+ try:
198
+ subprocess.run(
199
+ ["adb", "shell", f"mkdir -p {shlex.quote(android_root)}"],
200
+ check=True,
201
+ stdout=subprocess.DEVNULL,
202
+ stderr=subprocess.DEVNULL,
203
+ )
204
+ except subprocess.CalledProcessError as e:
205
+ raise click.ClickException(
206
+ f"Failed to create directory {android_root} on device: {e!r}"
207
+ ) from e
208
+
209
+ # Create remote execution tracking paths
210
+ model_name = pathlib.Path(model_path).name
211
+ remote_model_path = f"{android_root}/{model_name}"
212
+
213
+ # Determine device ABI
214
+ abi = android_utils.get_android_abi()
215
+ click.echo(f"Detected Android device ABI: {abi}")
216
+
217
+ run_model_bin = android_utils.find_android_binary("run_model", abi)
218
+
219
+ # Download libraries
220
+ lib_litert = android_utils.find_android_lib("libLiteRt.so", abi)
221
+ lib_clgl = android_utils.find_android_lib("libLiteRtClGlAccelerator.so", abi)
222
+
223
+ # Push model file and runner binary to Android device
224
+ click.echo(f"Pushing model {model_name} to device...")
225
+ subprocess.run(["adb", "push", model_path, remote_model_path], check=True)
226
+
227
+ remote_run_model_path = f"{android_root}/run_model"
228
+ if (
229
+ subprocess.run(
230
+ ["adb", "shell", f"[ -f {shlex.quote(remote_run_model_path)} ]"],
231
+ check=False,
232
+ ).returncode
233
+ == 0
234
+ ):
235
+ click.echo(" Skipping run_model (already on device)")
236
+ else:
237
+ click.echo("Pushing run_model to device...")
238
+ subprocess.run(
239
+ ["adb", "push", run_model_bin, remote_run_model_path], check=True
240
+ )
241
+
242
+ # Push libraries to default path
243
+ remote_lib_litert = f"{android_root}/{lib_litert.name}"
244
+ if (
245
+ subprocess.run(
246
+ ["adb", "shell", f"[ -f {shlex.quote(remote_lib_litert)} ]"],
247
+ check=False,
248
+ ).returncode
249
+ == 0
250
+ ):
251
+ click.echo(f" Skipping {lib_litert.name} (already on device)")
252
+ else:
253
+ click.echo(f"Pushing {lib_litert.name} to device...")
254
+ subprocess.run(["adb", "push", lib_litert, remote_lib_litert], check=True)
255
+
256
+ remote_lib_clgl = f"{android_root}/{lib_clgl.name}"
257
+ if (
258
+ subprocess.run(
259
+ ["adb", "shell", f"[ -f {shlex.quote(remote_lib_clgl)} ]"],
260
+ check=False,
261
+ ).returncode
262
+ == 0
263
+ ):
264
+ click.echo(f" Skipping {lib_clgl.name} (already on device)")
265
+ else:
266
+ click.echo(f"Pushing {lib_clgl.name} to device...")
267
+ subprocess.run(["adb", "push", lib_clgl, remote_lib_clgl], check=True)
268
+
269
+ remote_input_dir = _prepare_inputs_on_device(
270
+ model_path=pathlib.Path(model_path),
271
+ inputs=inputs,
272
+ signature_index=signature_index,
273
+ android_root=android_root,
274
+ )
275
+
276
+ # Pass None as device_id to use the default connected device.
277
+ remote_dispatch_dir = (
278
+ npu.push_npu_runtime_libraries(None, android_root)
279
+ if "npu" in accel_list
280
+ else ""
281
+ )
282
+
283
+ if "npu" in accel_list:
284
+ # Download and push SOC-specific LiteRT dispatch and compiler plugin libraries
285
+ target_model = npu.get_soc_target_model(None)
286
+ soc_vendor = "mediatek" if "mt" in target_model else "qualcomm"
287
+ lib_dispatch = android_utils.find_npu_dispatch_lib(soc_vendor, abi)
288
+ lib_compiler = android_utils.find_npu_compiler_plugin_lib(soc_vendor, abi)
289
+
290
+ remote_lib_dispatch = f"{android_root}/{lib_dispatch.name}"
291
+ if (
292
+ subprocess.run(
293
+ ["adb", "shell", f"[ -f {shlex.quote(remote_lib_dispatch)} ]"],
294
+ check=False,
295
+ ).returncode
296
+ == 0
297
+ ):
298
+ click.echo(f" Skipping {lib_dispatch.name} (already on device)")
299
+ else:
300
+ click.echo(f"Pushing {lib_dispatch.name} to device...")
301
+ subprocess.run(
302
+ ["adb", "push", lib_dispatch, remote_lib_dispatch], check=True
303
+ )
304
+
305
+ remote_lib_compiler = f"{android_root}/{lib_compiler.name}"
306
+ if (
307
+ subprocess.run(
308
+ ["adb", "shell", f"[ -f {shlex.quote(remote_lib_compiler)} ]"],
309
+ check=False,
310
+ ).returncode
311
+ == 0
312
+ ):
313
+ click.echo(f" Skipping {lib_compiler.name} (already on device)")
314
+ else:
315
+ click.echo(f"Pushing {lib_compiler.name} to device...")
316
+ subprocess.run(
317
+ ["adb", "push", lib_compiler, remote_lib_compiler], check=True
318
+ )
319
+
320
+ click.echo("Executing on device...\n")
321
+
322
+ # Need to make the binary executable before running the model
323
+ subprocess.run(
324
+ ["adb", "shell", f"chmod +x {shlex.quote(remote_run_model_path)}"],
325
+ check=True,
326
+ )
327
+
328
+ # Forward valid flags
329
+ run_cmd_args = [remote_run_model_path, f"--graph={remote_model_path}"]
330
+ if accelerator != "cpu":
331
+ run_cmd_args.append(f"--accelerator={accelerator}")
332
+ if remote_dispatch_dir:
333
+ run_cmd_args.append(f"--dispatch_library_dir={remote_dispatch_dir}")
334
+ run_cmd_args.append(f"--compiler_plugin_library_dir={remote_dispatch_dir}")
335
+ if iterations > 1:
336
+ run_cmd_args.append(f"--iterations={iterations}")
337
+ if signature_index != 0:
338
+ run_cmd_args.append(f"--signature_index={signature_index}")
339
+ if print_tensors:
340
+ run_cmd_args.append("--print_tensors=true")
341
+ run_cmd_args.append(f"--sample_size={sample_size}")
342
+ if remote_input_dir:
343
+ run_cmd_args.append(f"--input_dir={remote_input_dir}")
344
+
345
+ try:
346
+ if remote_dispatch_dir:
347
+ env_vars = (
348
+ f"LD_LIBRARY_PATH={shlex.quote(f'{remote_dispatch_dir}:{android_root}')}"
349
+ f" ADSP_LIBRARY_PATH={shlex.quote(remote_dispatch_dir)}"
350
+ )
351
+ else:
352
+ env_vars = ""
353
+ cmd_str = f"{env_vars} " if env_vars else ""
354
+ cmd_str += " ".join(shlex.quote(arg) for arg in run_cmd_args)
355
+ process = subprocess.Popen(
356
+ ["adb", "shell", cmd_str],
357
+ stdout=subprocess.PIPE,
358
+ stderr=subprocess.STDOUT,
359
+ text=True,
360
+ )
361
+
362
+ from litert_cli.core.log_filters import RunLogFilter
363
+
364
+ output_lines = []
365
+ log_filter = RunLogFilter(constants.DEFAULT_QUIET, print_tensors)
366
+
367
+ for line in process.stdout:
368
+ output_lines.append(line)
369
+ if log_filter.should_show(line):
370
+ click.echo(line, nl=False)
371
+
372
+ process.wait()
373
+ if process.returncode != 0:
374
+ click.secho(
375
+ f"Execution failed on device with exit code {process.returncode}",
376
+ fg="red",
377
+ )
378
+ click.echo("Full output for debugging:")
379
+ for line in output_lines:
380
+ click.echo(line, nl=False)
381
+ raise click.ClickException("Execution failed on device.")
382
+ except Exception as e:
383
+ raise click.ClickException(f"Failed to execute on device: {e}")
384
+ finally:
385
+ # Cleanup remote paths
386
+ click.echo("Clearing remote files...")
387
+ cleanup_cmds = [
388
+ f"rm -f {shlex.quote(remote_model_path)}"
389
+ f" {shlex.quote(remote_run_model_path)}"
390
+ ]
391
+ if remote_input_dir:
392
+ cleanup_cmds.append(f"rm -rf {shlex.quote(remote_input_dir)}")
393
+ cleanup_cmd = " && ".join(cleanup_cmds)
394
+ subprocess.run(["adb", "shell", cleanup_cmd], check=False)