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.
- examples/litert_cli.ipynb +313 -0
- examples/models/presets/default.py +19 -0
- examples/run_cli_demo.sh +38 -0
- examples/run_cli_npu.sh +89 -0
- examples/run_commands.sh +67 -0
- examples/run_models.sh +63 -0
- examples/run_smoke_tests.sh +58 -0
- examples/utils.ps1 +163 -0
- examples/utils.sh +184 -0
- litert_cli/__init__.py +15 -0
- litert_cli/commands/benchmark/__init__.py +16 -0
- litert_cli/commands/benchmark/android.py +212 -0
- litert_cli/commands/benchmark/cli.py +294 -0
- litert_cli/commands/benchmark/desktop.py +228 -0
- litert_cli/commands/benchmark/gcp.py +336 -0
- litert_cli/commands/clean.py +73 -0
- litert_cli/commands/compile.py +211 -0
- litert_cli/commands/convert/__init__.py +20 -0
- litert_cli/commands/convert/cli.py +255 -0
- litert_cli/commands/convert/generic.py +211 -0
- litert_cli/commands/convert/huggingface.py +175 -0
- litert_cli/commands/delete.py +56 -0
- litert_cli/commands/download.py +274 -0
- litert_cli/commands/import.py +124 -0
- litert_cli/commands/list.py +132 -0
- litert_cli/commands/lm.py +74 -0
- litert_cli/commands/quantize.py +193 -0
- litert_cli/commands/run/__init__.py +16 -0
- litert_cli/commands/run/android.py +394 -0
- litert_cli/commands/run/cli.py +297 -0
- litert_cli/commands/run/desktop.py +340 -0
- litert_cli/commands/visualize.py +234 -0
- litert_cli/core/android_utils.py +304 -0
- litert_cli/core/android_utils_test.py +236 -0
- litert_cli/core/constants.py +131 -0
- litert_cli/core/deps.py +180 -0
- litert_cli/core/deps_test.py +101 -0
- litert_cli/core/inputs.py +203 -0
- litert_cli/core/inputs_test.py +176 -0
- litert_cli/core/log_filters.py +50 -0
- litert_cli/core/models.py +96 -0
- litert_cli/core/npu_utils.py +382 -0
- litert_cli/core/targets_manager.py +192 -0
- litert_cli/core/utils.py +58 -0
- litert_cli/litert.py +119 -0
- litert_cli/litert_help_test.py +51 -0
- litert_cli/litert_test.py +88 -0
- litert_cli/models/__init__.py +145 -0
- litert_cli/models/asr/__init__.py +15 -0
- litert_cli/models/asr/asr_model.py +108 -0
- litert_cli/models/asr/parakeet_ctc.py +165 -0
- litert_cli/models/asr/runner.py +482 -0
- litert_cli/models/base.py +57 -0
- litert_cli/test_data/dummy_calib_data.py +26 -0
- litert_cli/test_data/dummy_cv_model.py +52 -0
- litert_cli/test_data/dummy_cv_model.tflite +0 -0
- litert_cli/test_data/generate_test_inputs.py +51 -0
- litert_cli/test_data/mobilenet_v3_calib_data.py +25 -0
- litert_cli/test_data/quantize_recipe.json +16 -0
- litert_cli/test_data/resnet18.py +31 -0
- litert_cli-0.1.0.dist-info/METADATA +38 -0
- litert_cli-0.1.0.dist-info/RECORD +67 -0
- litert_cli-0.1.0.dist-info/WHEEL +5 -0
- litert_cli-0.1.0.dist-info/entry_points.txt +2 -0
- litert_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
- litert_cli-0.1.0.dist-info/top_level.txt +3 -0
- 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)
|