codekingpro/portable-devtools
114k
1#!/usr/bin/env python3
2# Copyright (c) Microsoft Corporation. All rights reserved.
3# Licensed under the MIT License.
4
5from __future__ import annotations
6
7import argparse
8import contextlib
9import enum
10import os
11import pathlib
12import tempfile
13
14import onnxruntime as ort
15
16from .file_utils import files_from_file_or_dir, path_match_suffix_ignore_case
17from .onnx_model_utils import get_optimization_level
18from .ort_format_model import create_config_from_models
19
20
21class OptimizationStyle(enum.Enum):
22 Fixed = 0
23 Runtime = 1
24
25
26def _optimization_suffix(optimization_level_str: str, optimization_style: OptimizationStyle, suffix: str):
27 return "{}{}{}".format(
28 f".{optimization_level_str}" if optimization_level_str != "all" else "",
29 ".with_runtime_opt" if optimization_style == OptimizationStyle.Runtime else "",
30 suffix,
31 )
32
33
34def _create_config_file_path(
35 model_path_or_dir: pathlib.Path,
36 output_dir: pathlib.Path | None,
37 optimization_level_str: str,
38 optimization_style: OptimizationStyle,
39 enable_type_reduction: bool,
40):
41 config_name = "{}{}".format(
42 "required_operators_and_types" if enable_type_reduction else "required_operators",
43 _optimization_suffix(optimization_level_str, optimization_style, ".config"),
44 )
45
46 if model_path_or_dir.is_dir():
47 return (output_dir or model_path_or_dir) / config_name
48
49 model_config_path = model_path_or_dir.with_suffix(f".{config_name}")
50
51 if output_dir is not None:
52 return output_dir / model_config_path.name
53
54 return model_config_path
55
56
57def _create_session_options(
58 optimization_level: ort.GraphOptimizationLevel,
59 output_model_path: pathlib.Path,
60 custom_op_library: pathlib.Path,
61 session_options_config_entries: dict[str, str],
62):
63 so = ort.SessionOptions()
64 so.optimized_model_filepath = str(output_model_path)
65 so.graph_optimization_level = optimization_level
66
67 if custom_op_library:
68 so.register_custom_ops_library(str(custom_op_library))
69
70 for key, value in session_options_config_entries.items():
71 so.add_session_config_entry(key, value)
72
73 return so
74
75
76def _convert(
77 model_path_or_dir: pathlib.Path,
78 output_dir: pathlib.Path | None,
79 optimization_level_str: str,
80 optimization_style: OptimizationStyle,
81 custom_op_library: pathlib.Path,
82 create_optimized_onnx_model: bool,
83 allow_conversion_failures: bool,
84 target_platform: str,
85 session_options_config_entries: dict[str, str],
86) -> list[pathlib.Path]:
87 model_dir = model_path_or_dir if model_path_or_dir.is_dir() else model_path_or_dir.parent
88 output_dir = output_dir or model_dir
89
90 optimization_level = get_optimization_level(optimization_level_str)
91
92 def is_model_file_to_convert(file_path: pathlib.Path):
93 if not path_match_suffix_ignore_case(file_path, ".onnx"):
94 return False
95 # ignore any files with an extension of .optimized.onnx which are presumably from previous executions
96 # of this script
97 if path_match_suffix_ignore_case(file_path, ".optimized.onnx"):
98 print(f"Ignoring '{file_path}'")
99 return False
100 return True
101
102 models = files_from_file_or_dir(model_path_or_dir, is_model_file_to_convert)
103
104 if len(models) == 0:
105 raise ValueError(f"No model files were found in '{model_path_or_dir}'")
106
107 providers = ["CPUExecutionProvider"]
108
109 # if the optimization level is greater than or equal to 'layout' we manually exclude the NCHWc transformer.
110 # It's not applicable to ARM devices, and creates a device specific model which won't run on all hardware.
111 # If someone really really really wants to run it they could manually create an optimized onnx model first,
112 # or they could comment out this code.
113 optimizer_filter = None
114 if (
115 (optimization_level == ort.GraphOptimizationLevel.ORT_ENABLE_ALL)
116 or (optimization_level == ort.GraphOptimizationLevel.ORT_ENABLE_LAYOUT)
117 ) and target_platform != "amd64":
118 optimizer_filter = ["NchwcTransformer"]
119
120 converted_models = []
121
122 for model in models:
123 try:
124 relative_model_path = model.relative_to(model_dir)
125
126 (output_dir / relative_model_path).parent.mkdir(parents=True, exist_ok=True)
127
128 ort_target_path = (output_dir / relative_model_path).with_suffix(
129 _optimization_suffix(optimization_level_str, optimization_style, ".ort")
130 )
131
132 if create_optimized_onnx_model:
133 # Create an ONNX file with the same optimization level that will be used for the ORT format file.
134 # This allows the ONNX equivalent of the ORT format model to be easily viewed in Netron.
135 # If runtime optimizations are saved in the ORT format model, there may be some difference in the
136 # graphs at runtime between the ORT format model and this saved ONNX model.
137 optimized_target_path = (output_dir / relative_model_path).with_suffix(
138 _optimization_suffix(optimization_level_str, optimization_style, ".optimized.onnx")
139 )
140 so = _create_session_options(
141 optimization_level, optimized_target_path, custom_op_library, session_options_config_entries
142 )
143 if optimization_style == OptimizationStyle.Runtime:
144 # Limit the optimizations to those that can run in a model with runtime optimizations.
145 so.add_session_config_entry("optimization.minimal_build_optimizations", "apply")
146
147 print(f"Saving optimized ONNX model {model} to {optimized_target_path}")
148 _ = ort.InferenceSession(
149 str(model), sess_options=so, providers=providers, disabled_optimizers=optimizer_filter
150 )
151
152 # Load ONNX model, optimize, and save to ORT format
153 so = _create_session_options(
154 optimization_level, ort_target_path, custom_op_library, session_options_config_entries
155 )
156 so.add_session_config_entry("session.save_model_format", "ORT")
157 if optimization_style == OptimizationStyle.Runtime:
158 so.add_session_config_entry("optimization.minimal_build_optimizations", "save")
159
160 print(f"Converting optimized ONNX model {model} to ORT format model {ort_target_path}")
161 _ = ort.InferenceSession(
162 str(model), sess_options=so, providers=providers, disabled_optimizers=optimizer_filter
163 )
164
165 converted_models.append(ort_target_path)
166
167 # orig_size = os.path.getsize(onnx_target_path)
168 # new_size = os.path.getsize(ort_target_path)
169 # print("Serialized {} to {}. Sizes: orig={} new={} diff={} new:old={:.4f}:1.0".format(
170 # onnx_target_path, ort_target_path, orig_size, new_size, new_size - orig_size, new_size / orig_size))
171 except Exception as e:
172 print(f"Error converting {model}: {e}")
173 if not allow_conversion_failures:
174 raise
175
176 print(f"Converted {len(converted_models)}/{len(models)} models successfully.")
177
178 return converted_models
179
180
181def parse_args():
182 parser = argparse.ArgumentParser(
183 os.path.basename(__file__),
184 description="""Convert the ONNX format model/s in the provided directory to ORT format models.
185 All files with a `.onnx` extension will be processed. For each one, an ORT format model will be created in the
186 given output directory, if specified, or the same directory.
187 A configuration file will also be created containing the list of required operators for all
188 converted models. This configuration file should be used as input to the minimal build via the
189 `--include_ops_by_config` parameter.
190 """,
191 )
192
193 parser.add_argument(
194 "--output_dir",
195 type=pathlib.Path,
196 help="Provide an output directory for the converted model/s and configuration file. "
197 "If unspecified, the converted ORT format model/s will be in the same directory as the ONNX model/s.",
198 )
199
200 parser.add_argument(
201 "--optimization_style",
202 nargs="+",
203 default=[OptimizationStyle.Fixed.name, OptimizationStyle.Runtime.name],
204 choices=[e.name for e in OptimizationStyle],
205 help="Style of optimization to perform on the ORT format model. "
206 "Multiple values may be provided. The conversion will run once for each value. "
207 "The general guidance is to use models optimized with "
208 f"'{OptimizationStyle.Runtime.name}' style when using NNAPI or CoreML and "
209 f"'{OptimizationStyle.Fixed.name}' style otherwise. "
210 f"'{OptimizationStyle.Fixed.name}': Run optimizations directly before saving the ORT "
211 "format model. This bakes in any platform-specific optimizations. "
212 f"'{OptimizationStyle.Runtime.name}': Run basic optimizations directly and save certain "
213 "other optimizations to be applied at runtime if possible. This is useful when using a "
214 "compiling EP like NNAPI or CoreML that may run an unknown (at model conversion time) "
215 "number of nodes. The saved optimizations can further optimize nodes not assigned to the "
216 "compiling EP at runtime.",
217 )
218
219 parser.add_argument(
220 "--enable_type_reduction",
221 action="store_true",
222 help="Add operator specific type information to the configuration file to potentially reduce "
223 "the types supported by individual operator implementations.",
224 )
225
226 parser.add_argument(
227 "--custom_op_library",
228 type=pathlib.Path,
229 default=None,
230 help="Provide path to shared library containing custom operator kernels to register.",
231 )
232
233 parser.add_argument(
234 "--save_optimized_onnx_model",
235 action="store_true",
236 help="Save the optimized version of each ONNX model. "
237 "This will have the same level of optimizations applied as the ORT format model.",
238 )
239
240 parser.add_argument(
241 "--allow_conversion_failures",
242 action="store_true",
243 help="Whether to proceed after encountering model conversion failures.",
244 )
245
246 parser.add_argument(
247 "--target_platform",
248 type=str,
249 default=None,
250 choices=["arm", "amd64"],
251 help="Specify the target platform where the exported model will be used. "
252 "This parameter can be used to choose between platform-specific options, "
253 "such as QDQIsInt8Allowed(arm), NCHWc (amd64) and NHWC (arm/amd64) format, different "
254 "optimizer level options, etc.",
255 )
256
257 parser.add_argument(
258 "model_path_or_dir",
259 type=pathlib.Path,
260 help="Provide path to ONNX model or directory containing ONNX model/s to convert. "
261 "All files with a .onnx extension, including those in subdirectories, will be "
262 "processed.",
263 )
264
265 parsed_args = parser.parse_args()
266 parsed_args.optimization_style = [OptimizationStyle[style_str] for style_str in parsed_args.optimization_style]
267 return parsed_args
268
269
270def convert_onnx_models_to_ort(
271 model_path_or_dir: pathlib.Path,
272 output_dir: pathlib.Path | None = None,
273 optimization_styles: list[OptimizationStyle] | None = None,
274 custom_op_library_path: pathlib.Path | None = None,
275 target_platform: str | None = None,
276 save_optimized_onnx_model: bool = False,
277 allow_conversion_failures: bool = False,
278 enable_type_reduction: bool = False,
279):
280 if output_dir is not None:
281 if not output_dir.is_dir():
282 output_dir.mkdir(parents=True)
283 output_dir = output_dir.resolve(strict=True)
284
285 optimization_styles = optimization_styles or []
286
287 # setting optimization level is not expected to be needed by typical users, but it can be set with this
288 # environment variable
289 optimization_level_str = os.getenv("ORT_CONVERT_ONNX_MODELS_TO_ORT_OPTIMIZATION_LEVEL", "all")
290 model_path_or_dir = model_path_or_dir.resolve()
291 custom_op_library = custom_op_library_path.resolve() if custom_op_library_path else None
292
293 if not model_path_or_dir.is_dir() and not model_path_or_dir.is_file():
294 raise FileNotFoundError(f"Model path '{model_path_or_dir}' is not a file or directory.")
295
296 if custom_op_library and not custom_op_library.is_file():
297 raise FileNotFoundError(f"Unable to find custom operator library '{custom_op_library}'")
298
299 session_options_config_entries = {}
300
301 if target_platform is not None and target_platform == "arm":
302 session_options_config_entries["session.qdqisint8allowed"] = "1"
303 else:
304 session_options_config_entries["session.qdqisint8allowed"] = "0"
305
306 for optimization_style in optimization_styles:
307 print(
308 f"Converting models with optimization style '{optimization_style.name}' and level '{optimization_level_str}'"
309 )
310
311 converted_models = _convert(
312 model_path_or_dir=model_path_or_dir,
313 output_dir=output_dir,
314 optimization_level_str=optimization_level_str,
315 optimization_style=optimization_style,
316 custom_op_library=custom_op_library,
317 create_optimized_onnx_model=save_optimized_onnx_model,
318 allow_conversion_failures=allow_conversion_failures,
319 target_platform=target_platform,
320 session_options_config_entries=session_options_config_entries,
321 )
322
323 with contextlib.ExitStack() as context_stack:
324 if optimization_style == OptimizationStyle.Runtime:
325 # Convert models again without runtime optimizations.
326 # Runtime optimizations may not end up being applied, so we need to use both converted models with and
327 # without runtime optimizations to get a complete set of ops that may be needed for the config file.
328 model_dir = model_path_or_dir if model_path_or_dir.is_dir() else model_path_or_dir.parent
329 temp_output_dir = context_stack.enter_context(
330 tempfile.TemporaryDirectory(dir=model_dir, suffix=".without_runtime_opt")
331 )
332 session_options_config_entries_for_second_conversion = session_options_config_entries.copy()
333 # Limit the optimizations to those that can run in a model with runtime optimizations.
334 session_options_config_entries_for_second_conversion["optimization.minimal_build_optimizations"] = (
335 "apply"
336 )
337
338 print(
339 "Converting models again without runtime optimizations to generate a complete config file. "
340 "These converted models are temporary and will be deleted."
341 )
342 converted_models += _convert(
343 model_path_or_dir=model_path_or_dir,
344 output_dir=temp_output_dir,
345 optimization_level_str=optimization_level_str,
346 optimization_style=OptimizationStyle.Fixed,
347 custom_op_library=custom_op_library,
348 create_optimized_onnx_model=False, # not useful as they would be created in a temp directory
349 allow_conversion_failures=allow_conversion_failures,
350 target_platform=target_platform,
351 session_options_config_entries=session_options_config_entries_for_second_conversion,
352 )
353
354 print(
355 f"Generating config file from ORT format models with optimization style '{optimization_style.name}' and level '{optimization_level_str}'"
356 )
357
358 config_file = _create_config_file_path(
359 model_path_or_dir,
360 output_dir,
361 optimization_level_str,
362 optimization_style,
363 enable_type_reduction,
364 )
365
366 create_config_from_models(converted_models, config_file, enable_type_reduction)
367
368
369if __name__ == "__main__":
370 args = parse_args()
371 convert_onnx_models_to_ort(
372 args.model_path_or_dir,
373 output_dir=args.output_dir,
374 optimization_styles=args.optimization_style,
375 custom_op_library_path=args.custom_op_library,
376 target_platform=args.target_platform,
377 save_optimized_onnx_model=args.save_optimized_onnx_model,
378 allow_conversion_failures=args.allow_conversion_failures,
379 enable_type_reduction=args.enable_type_reduction,
380 )
381 