onnxruntime
739 строк · 30.9 Кб
1# Copyright (c) Microsoft Corporation. All rights reserved.
2# Licensed under the MIT License.
3from __future__ import annotations
4
5import argparse
6import logging
7import os
8import pathlib
9import tempfile
10from collections import deque
11from enum import IntEnum
12
13import onnx
14
15from ..onnx_model_utils import ModelProtoWithShapeInfo, get_producer_consumer_maps, is_fixed_size_tensor, optimize_model
16
17
18class _SupportedOpsChecker:
19"""
20Class to process the md file with list of supported ops and caveats for an execution provider.
21e.g. /tools/ci_build/github/android/nnapi_supported_ops.md
22/tools/ci_build/github/apple/coreml_supported_mlprogram_ops.md
23/tools/ci_build/github/apple/coreml_supported_neuralnetwork_ops.md
24"""
25
26def __init__(self, filename):
27self._filename = filename
28self._ops = {} # op to caveats
29self._ops_seen = set()
30
31with open(filename) as f:
32for line in f:
33# we're looking for a markdown table with 2 columns. first is op name. second is caveats
34# op name is domain:op
35if line.startswith("|"):
36pieces = line.strip().split("|")
37if len(pieces) == 4: # pre-first '|'. op, caveat, post-last '|'
38domain_op = pieces[1]
39caveat = pieces[2]
40caveat = caveat.replace("<br/>", " ") # remove some HTML tags
41# skip lines that don't have the ':' which separates the domain and op
42# e.g. the table header will fail this check
43if ":" in domain_op:
44self._ops[domain_op] = caveat
45
46def is_op_supported(self, node):
47domain = node.domain if node.domain else "ai.onnx"
48domain_op = domain + ":" + node.op_type
49
50is_supported = domain_op in self._ops
51if is_supported:
52self._ops_seen.add(domain_op)
53
54return is_supported
55
56def get_caveats(self):
57caveats = []
58for op in sorted(self._ops_seen):
59caveat = self._ops[op]
60if caveat:
61caveats.append(f"{op}:{caveat}")
62
63return caveats
64
65
66class PartitioningInfo:
67class TryWithEP(IntEnum):
68NO = (0,)
69MAYBE = (1,)
70YES = 2
71
72def __init__(
73self,
74num_nodes: int,
75num_supported_nodes: int,
76num_partitions: int,
77supported_ops_checker: _SupportedOpsChecker,
78supported_groups: list[onnx.NodeProto],
79unsupported_ops: set[str],
80nodes_unsupported_due_to_op: int,
81nodes_unsupported_due_to_dynamic_input: int,
82num_unsupported_nodes_due_to_rank: int,
83ops_with_unsupported_rank: set[str],
84):
85self.num_nodes = num_nodes
86self.num_supported_nodes = num_supported_nodes
87self.num_partitions = num_partitions
88self.supported_ops_checker = supported_ops_checker
89self.supported_groups = supported_groups
90self.unsupported_ops = unsupported_ops
91self.nodes_unsupported_due_to_op = nodes_unsupported_due_to_op
92self.nodes_unsupported_due_to_dynamic_input = nodes_unsupported_due_to_dynamic_input
93self.num_unsupported_nodes_due_to_rank = num_unsupported_nodes_due_to_rank
94self.ops_with_unsupported_rank = ops_with_unsupported_rank
95
96self.num_subgraphs = 0
97self.num_nodes_in_subgraphs = 0
98
99def merge(self, other: PartitioningInfo):
100"""
101Merge the information from another PartitioningInfo instance into this one.
102"""
103self.num_nodes += other.num_nodes
104self.num_supported_nodes += other.num_supported_nodes
105self.num_partitions += other.num_partitions
106self.supported_groups.extend(other.supported_groups)
107self.unsupported_ops.update(other.unsupported_ops)
108self.nodes_unsupported_due_to_op += other.nodes_unsupported_due_to_op
109self.nodes_unsupported_due_to_dynamic_input += other.nodes_unsupported_due_to_dynamic_input
110self.num_unsupported_nodes_due_to_rank += other.num_unsupported_nodes_due_to_rank
111self.ops_with_unsupported_rank.update(other.ops_with_unsupported_rank)
112
113# hard assumption that we merge into the main graph partitioning info
114self.num_subgraphs += 1
115self.num_nodes_in_subgraphs += other.num_nodes
116
117def suitability(self):
118# semi-arbitrary choices that err on the side of MAYBE.
119# having 1 partition is always preferred, but if that is small it may not be useful.
120# having 2 partitions may be okay if they cover most nodes
121# more than 2 partitions and the device copy cost is almost guaranteed to outweigh the benefit of using the NPU
122# NOTE: This assumes the EP is not CPU based and there is device copy overhead to consider
123pct_supported = self.num_supported_nodes / self.num_nodes * 100
124if self.num_partitions == 1:
125if pct_supported > 75:
126return PartitioningInfo.TryWithEP.YES
127elif pct_supported > 50:
128return PartitioningInfo.TryWithEP.MAYBE
129else:
130return PartitioningInfo.TryWithEP.NO
131
132if self.num_partitions == 2:
133if pct_supported > 75:
134return PartitioningInfo.TryWithEP.MAYBE
135else:
136return PartitioningInfo.TryWithEP.NO
137
138return PartitioningInfo.TryWithEP.NO
139
140def print_analysis(self, logger: logging.Logger, ep_name: str):
141"""
142Analyze the partitioning information and log the analysis
143:param logger: Logger to use
144:param ep_name: Execution provider name to use in the log messages
145"""
146
147logger.info(
148f"{self.num_partitions} partitions with a total of {self.num_supported_nodes}/{self.num_nodes} "
149f"nodes can be handled by the {ep_name} EP."
150)
151
152if self.supported_groups:
153logger.info(
154f'\tPartition sizes: [{", ".join([str(len(partition)) for partition in self.supported_groups])}]'
155)
156
157# dump full groups if debug output is enabled
158for group in self.supported_groups:
159logger.debug(f'Nodes in group: {",".join([f"{node.op_type}:{node.name}" for node in group])}')
160
161logger.info(f"Unsupported nodes due to operator={self.nodes_unsupported_due_to_op}")
162if self.unsupported_ops:
163logger.info(f'\tUnsupported ops: {",".join(sorted(self.unsupported_ops))}')
164
165caveats = self.supported_ops_checker.get_caveats()
166if caveats:
167indent = " " * 5
168logger.info(
169"\tCaveats that have not been checked and may result in a node not actually being supported: "
170f'{"".join([os.linesep + indent + caveat for caveat in caveats])}'
171)
172
173if self.nodes_unsupported_due_to_dynamic_input:
174logger.info(
175"Unsupported nodes due to input having a dynamic shape=%d",
176self.nodes_unsupported_due_to_dynamic_input,
177)
178
179if self.num_unsupported_nodes_due_to_rank:
180logger.info(f"Unsupported nodes due to rank of input data={self.num_unsupported_nodes_due_to_rank}")
181logger.info(f"\tOps with unsupported rank: {','.join(sorted(self.ops_with_unsupported_rank))}")
182
183if self.num_subgraphs > 0:
184# TODO: CoreML has a flag. NNAPI doesn't. Either should be able to support a subgraph when treated as a
185# separate graph (only extra detail would be making sure implicit inputs are handled).
186# Merging the subgraph into the parent graph would be more complex.
187# e.g. for CoreML we could potentially convert Loop to while_loop and If to cond if the subgraphs in the
188# control flow node are fully supported.
189# NNAPI also has While and If.
190
191# It most likely will be necessary to support merging in If nodes with fully supported subgraphs,
192# as the subgraphs in those are often very simple, so the performance cost of going to the CPU EP and back
193# is high.
194logger.info(
195f"{self.num_nodes_in_subgraphs} nodes are in {self.num_subgraphs} subgraphs. "
196"Check EP as to whether subgraphs are supported."
197)
198
199pct_nodes_using_ep = self.num_supported_nodes / self.num_nodes * 100
200if self.num_partitions == 0:
201logger.info(f"{ep_name} cannot run any nodes in this model.")
202elif self.num_partitions == 1:
203if pct_nodes_using_ep > 75:
204logger.info(
205f"{ep_name} should work well for this model as there is one partition "
206f"covering {pct_nodes_using_ep:.1f}% of the nodes in the model."
207)
208elif pct_nodes_using_ep > 50:
209logger.info(
210f"{ep_name} may work well for this model, however only {pct_nodes_using_ep:.1f}% of nodes "
211"will use it. Performance testing is required to validate."
212)
213else:
214logger.info(
215f"{ep_name} will probably not work will for this model as only {pct_nodes_using_ep:.2f}% "
216"of nodes will use it."
217)
218
219elif self.num_partitions == 2 and pct_nodes_using_ep > 75:
220logger.info(
221f"{ep_name} can be considered for this model as there are two partitions "
222f"covering {pct_nodes_using_ep:.1f}% of the nodes. "
223"Performance testing is required to validate."
224)
225else:
226logger.info(
227f"{ep_name} is not recommended with this model as there are {self.num_partitions} partitions "
228f"covering {pct_nodes_using_ep:.1f}% of the nodes in the model. "
229"This will most likely result in worse performance than just using the CPU EP."
230)
231
232
233def _check_partitioning_for_graph(
234graph: onnx.GraphProto,
235node_to_producers: dict[onnx.NodeProto, set[onnx.NodeProto]],
236node_to_consumers: dict[onnx.NodeProto, set[onnx.NodeProto]],
237supported_ops_checker: _SupportedOpsChecker,
238outer_scope_initializers: set[str],
239require_fixed_input_sizes: bool,
240value_info: dict[str, onnx.ValueInfoProto],
241max_rank: int = 999, # max rank if EP has a limitation
242):
243# initializers have fixed sizes.
244initializers = [i.name for i in graph.initializer]
245
246def _is_fixed_shape_value(value):
247if value in value_info:
248return is_fixed_size_tensor(value_info[value])
249
250if value in initializers or value in outer_scope_initializers:
251return True
252
253# if something has an unknown shape (e.g. something downstream of a Reshape with dynamic input for the shape)
254# it won't have an entry in value_info
255return False
256
257#
258# Replicate logic from /onnxruntime/core/providers/partitioning_utils.cc:CreateSupportedPartitionNodeGroups
259# to roughly estimate number of partitions for nodes that is_node_supported_fn returns true for.
260#
261# We keep the structure and variable names as close as possible to the C++ implementation to simplify keeping them
262# in sync if future updates are needed.
263#
264# NOTE: CreateSupportedPartitionNodeGroups was recently updated to be QDQ aware so that partitions did not split
265# QDQ node groups. This code does not need to be QDQ aware as splitting a QDQ node group does not affect the total
266# number of partitions or supported nodes.
267#
268
269# we don't currently support a callback for additional group closure checks in the python implementation
270on_group_closed_fn = None
271
272supported_groups = []
273# number of inputs from unprocessed nodes (in-degree) per node
274in_degree = {}
275# nodes that are ready to process
276nodes_to_process = deque() # deque of Node instances
277# nodes that will be processed when considering the next partition node group
278nodes_to_process_with_next_group = deque()
279
280# initialize in-degrees and find root nodes
281for node in graph.node:
282node_input_edge_count = len(node_to_producers[node]) if node in node_to_producers else 0
283in_degree[node] = node_input_edge_count
284if node_input_edge_count == 0:
285# node is only dependent on graph input or initializers
286nodes_to_process.append(node)
287
288supported_group = []
289# the partition node group's border is the aggregate of its nodes' output nodes
290supported_group_border = set()
291num_supported_nodes = 0
292num_unsupported_nodes_due_to_op = 0
293num_unsupported_nodes_due_to_dynamic_input = 0
294num_unsupported_nodes_due_to_rank = 0
295unsupported_ops = set()
296ops_with_unsupported_rank = set()
297
298def close_group():
299if supported_group:
300keep_partition = not on_group_closed_fn or on_group_closed_fn(supported_group)
301
302if keep_partition:
303supported_groups.append(supported_group.copy())
304
305supported_group.clear()
306supported_group_border.clear()
307
308while nodes_to_process or nodes_to_process_with_next_group:
309if not nodes_to_process:
310close_group()
311nodes_to_process = nodes_to_process_with_next_group
312nodes_to_process_with_next_group = deque()
313continue
314
315node = nodes_to_process.popleft()
316
317is_op_supported = supported_ops_checker.is_op_supported(node)
318is_input_shape_supported = not require_fixed_input_sizes or all(_is_fixed_shape_value(i) for i in node.input)
319
320is_rank_supported = True
321if value_info:
322for node_input in node.input:
323if node_input and node_input in value_info and value_info[node_input].type.HasField("tensor_type"):
324input_rank = len(value_info[node_input].type.tensor_type.shape.dim)
325if input_rank > max_rank:
326is_rank_supported = False
327break
328
329# special-case if we can infer the rank from the length of the 'perms' Transpose attribute
330# e.g. this works with SegmentAnything where dynamic Reshape operators result in no shape info.
331if node.op_type == "Transpose" and len(node.attribute[0].ints) > max_rank:
332is_rank_supported = False
333
334is_node_supported = is_op_supported and is_input_shape_supported and is_rank_supported
335
336if not is_node_supported:
337if node in supported_group_border:
338# an unsupported node on the border will be processed after the current partition node group
339# so skip any additional processing/counting here
340nodes_to_process_with_next_group.append(node)
341continue
342
343if not is_op_supported:
344unsupported_ops.add(f'{node.domain if node.domain else "ai.onnx"}:{node.op_type}')
345num_unsupported_nodes_due_to_op += 1
346
347if not is_input_shape_supported:
348num_unsupported_nodes_due_to_dynamic_input += 1
349
350if not is_rank_supported:
351num_unsupported_nodes_due_to_rank += 1
352ops_with_unsupported_rank.add(f'{node.domain if node.domain else "ai.onnx"}:{node.op_type}')
353
354if is_node_supported:
355num_supported_nodes += 1
356
357# add node to the partition node group
358supported_group.append(node)
359
360# remove node from the border and add its outputs to the border
361if node in supported_group_border:
362supported_group_border.remove(node)
363
364# for each consumer node add to supported_group_border
365if node in node_to_consumers:
366for consumer in node_to_consumers[node]:
367supported_group_border.add(consumer)
368
369# adjust in-degrees of the node outputs and add any new nodes to process
370if node in node_to_consumers:
371for consumer in node_to_consumers[node]:
372consumer_node_in_degree = in_degree[consumer]
373consumer_node_in_degree -= 1
374if consumer_node_in_degree == 0:
375nodes_to_process.append(consumer)
376
377in_degree[consumer] = consumer_node_in_degree
378
379close_group()
380
381num_nodes = len(graph.node)
382num_partitions = len(supported_groups)
383
384info = PartitioningInfo(
385num_nodes,
386num_supported_nodes,
387num_partitions,
388supported_ops_checker,
389supported_groups,
390unsupported_ops,
391num_unsupported_nodes_due_to_op,
392num_unsupported_nodes_due_to_dynamic_input,
393num_unsupported_nodes_due_to_rank,
394ops_with_unsupported_rank,
395)
396
397return info
398
399
400def check_partitioning(
401main_graph: onnx.GraphProto,
402supported_ops_checker: _SupportedOpsChecker,
403require_fixed_input_sizes: bool,
404max_rank: int = 999,
405) -> PartitioningInfo:
406"""
407Estimate the partitions the graph will be split into for nodes that is_node_supported_fn returns true for.
408
409The check on whether a node is supported is purely based on the operator type. Additional limitations
410(e.g. NNAPI EP only supports 2D Conv) are not checked, so partitions may not be 100% accurate. The limitations
411for operators in the partitions are printed so the user can manually check.
412:param main_graph: Graph to process
413:param supported_ops_checker: Checker with info on supported ops.
414:param require_fixed_input_sizes: If True, require that the inputs to a potentially supported node are fixed size
415tensors for it to be considered as supported. This requires
416onnx.shape_inference.infer_shapes to have been run on the model to populate the
417shape information.
418If False, shapes are ignored during the check.
419:param max_rank: Set if EP has a limitation on the rank of tensors it supports.
420:return PartitioningInfo instance with details
421"""
422
423if require_fixed_input_sizes and len(main_graph.value_info) == 0 and len(main_graph.node) > 1:
424raise ValueError("Run onnx.shape_inference.infer_shapes on the model to populate the shape information.")
425
426# create lookup map from ValueInfo for efficiency
427def _update_value_info(graph: onnx.GraphProto, value_to_shape: dict[str, onnx.ValueInfoProto]):
428for v in graph.input:
429value_to_shape[v.name] = v
430for v in graph.output:
431value_to_shape[v.name] = v
432for v in graph.value_info:
433value_to_shape[v.name] = v
434
435# the producer/consumer maps are for the entire model
436node_to_producers, node_to_consumers = get_producer_consumer_maps(main_graph)
437
438def _check_graph(
439graph: onnx.GraphProto,
440outer_scope_value_info: dict[str, onnx.ValueInfoProto] | None,
441outer_scope_initializers: set[str] | None = None,
442partitioning_info: PartitioningInfo | None = None,
443) -> PartitioningInfo:
444if outer_scope_value_info is not None:
445# extend value info if we're using it. we replace any value shadowed with a local one
446value_info = outer_scope_value_info.copy()
447_update_value_info(graph, value_info)
448else:
449value_info = {}
450
451if outer_scope_initializers is None:
452outer_scope_initializers = set()
453
454info = _check_partitioning_for_graph(
455graph,
456node_to_producers,
457node_to_consumers,
458supported_ops_checker,
459outer_scope_initializers,
460require_fixed_input_sizes,
461value_info,
462max_rank,
463)
464
465if partitioning_info:
466# merge in subgraph info
467partitioning_info.merge(info)
468else:
469# main graph info
470partitioning_info = info
471
472# setup outer scope initializers. we copy the input set as a model may have multiple subgraphs
473# on multiple levels, so we need to keep the set for each descent separate
474subgraph_outer_scope_initializers = set(outer_scope_initializers)
475for initializer in graph.initializer:
476subgraph_outer_scope_initializers.add(initializer.name)
477
478for node in graph.node:
479# recurse into nodes with subgraphs
480for attr in node.attribute:
481if attr.HasField("g"):
482subgraph = attr.g
483partitioning_info = _check_graph(
484subgraph, value_info, subgraph_outer_scope_initializers, partitioning_info
485)
486
487return partitioning_info
488
489aggregated_partitioning_info = _check_graph(main_graph, {} if require_fixed_input_sizes else None)
490
491return aggregated_partitioning_info
492
493
494def _check_ep_partitioning(
495model: onnx.ModelProto, supported_ops_config: pathlib.Path, require_fixed_input_sizes: bool, max_rank: int = 999
496):
497supported_ops = _SupportedOpsChecker(supported_ops_config)
498partition_info = check_partitioning(model.graph, supported_ops, require_fixed_input_sizes, max_rank)
499return partition_info
500
501
502def check_nnapi_partitions(model, require_fixed_input_sizes: bool):
503# if we're running in the ORT python package the file should be local. otherwise assume we're running from the
504# ORT repo
505script_dir = pathlib.Path(__file__).parent
506local_config = script_dir / "nnapi_supported_ops.md"
507if local_config.exists():
508config_path = local_config
509else:
510ort_root = script_dir.parents[3]
511config_path = ort_root / "tools" / "ci_build" / "github" / "android" / "nnapi_supported_ops.md"
512
513return _check_ep_partitioning(model, config_path, require_fixed_input_sizes)
514
515
516def check_coreml_partitions(model: onnx.ModelProto, require_fixed_input_sizes: bool, config_filename: str):
517# if we're running in the ORT python package the file should be local. otherwise assume we're running from the
518# ORT repo
519script_dir = pathlib.Path(__file__).parent
520local_config = script_dir / config_filename
521if local_config.exists():
522config_path = local_config
523else:
524ort_root = script_dir.parents[3]
525config_path = ort_root / "tools" / "ci_build" / "github" / "apple" / config_filename
526
527max_rank = 5
528return _check_ep_partitioning(model, config_path, require_fixed_input_sizes, max_rank)
529
530
531def check_shapes(graph: onnx.GraphProto, logger: logging.Logger | None = None):
532"""
533Check the shapes of graph inputs, values and graph outputs to determine if they have static or dynamic sizes.
534NNAPI does not support dynamically sized values. CoreML does, but it will most likely cost performance.
535:param graph: Graph to check. If shape inferencing has been run the checks on values will be meaningful.
536:param logger: Optional logger for diagnostic information.
537:return: Tuple of List of inputs with dynamic shapes, Number of dynamic values found
538"""
539
540# it's OK if the input is dynamically sized and we do a Resize early to a fixed size.
541# it's not good if lots of ops have dynamic inputs
542
543num_fixed_values = 0
544num_dynamic_values = 0
545
546dynamic_inputs = []
547for i in graph.input:
548if not is_fixed_size_tensor(i):
549dynamic_inputs.append(i)
550# split/join to remove repeated whitespace and newlines from str(i)
551if logger:
552logger.info(f"Input is not a fixed size tensor: {' '.join(str(i).split())}")
553num_dynamic_values += 1
554else:
555num_fixed_values += 1
556
557dynamic_outputs = []
558for o in graph.output:
559if not is_fixed_size_tensor(o):
560dynamic_outputs.append(o)
561if logger:
562logger.info(f"Output is not a fixed size tensor: {' '.join(str(o).split())}")
563num_dynamic_values += 1
564else:
565num_fixed_values += 1
566
567# check we have value info.
568# special case some test graphs with a single node which only have graph input and output values, and
569# a model where all inputs are dynamic (results in no value_info)
570if not graph.value_info and not (len(graph.node) == 1 or len(dynamic_inputs) == len(graph.input)):
571logger.warning(
572"Unable to check shapes within model. "
573"ONNX shape inferencing should be run on the model prior to checking."
574)
575
576for vi in graph.value_info:
577if is_fixed_size_tensor(vi):
578num_fixed_values += 1
579else:
580num_dynamic_values += 1
581
582if logger:
583logger.info(
584f"Num values with fixed shape={num_fixed_values}. Num values with dynamic shape={num_dynamic_values}"
585)
586
587if dynamic_inputs:
588if dynamic_outputs:
589logger.info(
590"Model has dynamic inputs and outputs. Consider re-exporting model with fixed sizes "
591"if NNAPI or CoreML can be used with this model."
592)
593else:
594logger.info(
595"""Model has dynamically sized inputs but fixed sized outputs.
596If the sizes become fixed early in the model (e.g. pre-processing of a dynamic input size
597results in a fixed input size for the majority of the model) performance with NNAPI and CoreML,
598if applicable, should not be significantly impacted."""
599)
600
601return dynamic_inputs, num_dynamic_values
602
603
604def checker(model_path: pathlib.Path, logger: logging.Logger):
605model_with_shape_info_wrapper = ModelProtoWithShapeInfo(model_path)
606model_with_shape_info = model_with_shape_info_wrapper.model_with_shape_info
607
608dynamic_inputs, num_dynamic_values = check_shapes(model_with_shape_info.graph)
609
610def check_ep(ep_name, checker_func):
611logger.info(f"Checking {ep_name}")
612
613# check with shape info first so supported nodes takes into account values with dynamic shapes
614require_fixed_input_sizes = True
615partition_info = checker_func(model_with_shape_info, require_fixed_input_sizes)
616if logger.getEffectiveLevel() <= logging.INFO:
617partition_info.print_analysis(logger, ep_name)
618
619suitability = partition_info.suitability()
620logger.info(f"Model should perform well with {ep_name} as is: {suitability.name}")
621
622if suitability != PartitioningInfo.TryWithEP.YES and dynamic_inputs:
623logger.info("--------")
624logger.info("Checking if model will perform better if the dynamic shapes are fixed...")
625require_fixed_input_sizes = False
626partition_info_with_fixed_shapes = checker_func(model_with_shape_info, require_fixed_input_sizes)
627
628if logger.getEffectiveLevel() <= logging.INFO:
629# analyze and log detailed info
630logger.info("Partition information if the model was updated to make the shapes fixed:")
631partition_info_with_fixed_shapes.print_analysis(logger, ep_name)
632
633fixed_shape_suitability = partition_info_with_fixed_shapes.suitability()
634logger.info(
635f"Model should perform well with {ep_name} if modified to have fixed input shapes: "
636f"{fixed_shape_suitability.name}"
637)
638
639if fixed_shape_suitability != PartitioningInfo.TryWithEP.NO:
640logger.info("Shapes can be altered using python -m onnxruntime.tools.make_dynamic_shape_fixed")
641
642if fixed_shape_suitability.value > suitability.value:
643suitability = fixed_shape_suitability
644
645logger.info("================")
646logger.info("")
647
648return suitability
649
650nnapi_suitability = check_ep("NNAPI", check_nnapi_partitions)
651
652# Check for NeuralNetwork CoreML model
653def check_nn_coreml(model: onnx.ModelProto, require_fixed_input_sizes):
654return check_coreml_partitions(model, require_fixed_input_sizes, "coreml_supported_neuralnetwork_ops.md")
655
656# Check for MLProgram CoreML model
657def check_mlprogram_coreml(model: onnx.ModelProto, require_fixed_input_sizes):
658return check_coreml_partitions(model, require_fixed_input_sizes, "coreml_supported_mlprogram_ops.md")
659
660coreml_nn_suitability = check_ep("CoreML NeuralNetwork", check_nn_coreml)
661coreml_mlprogram_suitability = check_ep("CoreML MLProgram", check_mlprogram_coreml)
662
663if (
664nnapi_suitability != PartitioningInfo.TryWithEP.YES
665or coreml_nn_suitability != PartitioningInfo.TryWithEP.YES
666or coreml_mlprogram_suitability != PartitioningInfo.TryWithEP.YES
667) and logger.getEffectiveLevel() > logging.INFO:
668logger.info("Re-run with log level of INFO for more details on the NNAPI/CoreML issues.")
669
670return (
671nnapi_suitability != PartitioningInfo.TryWithEP.NO
672or coreml_nn_suitability != PartitioningInfo.TryWithEP.NO
673or coreml_mlprogram_suitability != PartitioningInfo.TryWithEP.NO
674)
675
676
677def analyze_model(model_path: pathlib.Path, skip_optimize: bool = False, logger: logging.Logger | None = None):
678"""
679Analyze the provided model to determine if it's likely to work well with the NNAPI or CoreML Execution Providers
680:param model_path: Model to analyze.
681:param skip_optimize: Skip optimizing to BASIC level before checking. When exporting to ORT format we will do this
682optimization..
683:param logger: Logger for output
684:return: True if either the NNAPI or CoreML Execution Providers may work well with this model.
685"""
686if not logger:
687logger = logging.getLogger("usability_checker")
688logger.setLevel(logging.INFO)
689
690logger.info(f"Checking {model_path} for usability with ORT Mobile.")
691
692with tempfile.TemporaryDirectory() as tmp:
693if not skip_optimize:
694tmp_path = pathlib.Path(tmp) / model_path.name
695optimize_model(model_path, tmp_path, use_external_initializers=True)
696model_path = tmp_path
697
698try_eps = checker(model_path.resolve(strict=True), logger)
699
700return try_eps
701
702
703def parse_args():
704parser = argparse.ArgumentParser(
705os.path.basename(__file__), description="""Analyze an ONNX model for usage with the ORT mobile"""
706)
707
708parser.add_argument("--log_level", choices=["debug", "info"], default="info", help="Logging level")
709parser.add_argument(
710"--skip_optimize",
711action="store_true",
712help="Don't optimize the model to BASIC level prior to analyzing. "
713"Optimization will occur when exporting the model to ORT format, so in general "
714"should not be skipped unless you have a specific reason to do so.",
715)
716parser.add_argument("model_path", type=pathlib.Path, help="Provide path to ONNX model")
717
718return parser.parse_args()
719
720
721def run_analyze_model():
722args = parse_args()
723logger = logging.getLogger("default")
724
725if args.log_level == "debug":
726logger.setLevel(logging.DEBUG)
727elif args.log_level == "info":
728logger.setLevel(logging.INFO)
729elif args.log_level == "warning":
730logger.setLevel(logging.WARNING)
731else:
732logger.setLevel(logging.ERROR)
733
734model_path = args.model_path.resolve()
735analyze_model(model_path, args.skip_optimize, logger)
736
737
738if __name__ == "__main__":
739run_analyze_model()
740