From a1ae70f4e3054d08d815bc8e8c4c3ebd8d1e6d31 Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Wed, 29 Jul 2026 09:29:50 +0200 Subject: [PATCH 1/5] Improve mappint [AI] --- backends/nxp/backend/neutron_map.py | 734 +++++++++++------- .../nxp/tests/generic_tests/test_profiling.py | 10 +- 2 files changed, 442 insertions(+), 302 deletions(-) diff --git a/backends/nxp/backend/neutron_map.py b/backends/nxp/backend/neutron_map.py index a5becafc4f0..5d4c4c15848 100644 --- a/backends/nxp/backend/neutron_map.py +++ b/backends/nxp/backend/neutron_map.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import logging import re +from collections import defaultdict from dataclasses import dataclass # example: Type: CONV_2D @@ -19,7 +20,12 @@ r"Outputs:(?P[\s\S]*?)" r"Location:\s+(?P\d+)" ) -# The pattern is very similar to operator pattern +# Match a tensor name followed by its kind on the next non-empty line. +# example: +# Name: quantized_decomposed_dequantize_per_tensor_default_5 +# Kind: Variable +PATTERN_TENSOR_KIND = r"Name:\s+(?P\S+)\s+Kind:\s+(?P[^\r\n]+)" +# The pattern is very similar to operator pattern. PATTERN_SUBGRAPH = ( r"^(?P\d+)\s*" r"Inputs:(?P[\s\S]*?)" @@ -44,416 +50,550 @@ r"Kernels:\s*(?P[\s\S]*?)" r"\s*(NeutronOperator|^$|=)" ) -# example: NeutronGraph "subgraph_074": -PATTERN_VERBOSE_GRAPH = ( - r"NeutronGraph\s*\"subgraph_(?P\d+)\":(?P[\s\S]*?)\s*(^$|=)" -) # Two graphs are expected in the input log: original and converted. EXPECTED_GRAPHS = 2 -# List of single-input nodes that shouldn't be mapped on the same TFLite node. -SINGLE_INPUT_NODES = [ - "ABS", - "AVERAGE_POOL_2D", - "CAST", - "EXP", - "HARD_SWISH", - "LEAKY_RELU", - "LOG", - "LOGISTIC", - "MAX_POOL_2D", - "QUANTIZE", - "RSQRT", - "TANH", -] +# Marker for a Variable (dynamic) tensor kind in the converter log. +TENSOR_KIND_VARIABLE = "Variable" @dataclass class Node: - name: str # Name of the node. - inputs: list[str] # List of nodes inputs. - outputs: list[str] # List of nodes outputs. + name: str # Name of the node/operator. + inputs: list[str] # Dynamic (variable) inputs only. + outputs: list[str] # Dynamic (variable) outputs only. location: int # Location in graph/subgraph. @dataclass class SubgraphInfo: num: int # Subgraph number. - location: int # Location in neutron graph - inputs: list[str] # List of subgraphs inputs. - outputs: list[str] # List of subgraphs outputs. - kernels: int # Number of neutron kernels in neutron subgraph. - nodes: list[Node] # List of tflite nodes in neutron subgraph. + location: int # Location in neutron graph. + inputs: list[str] # Dynamic inputs. + outputs: list[str] # Dynamic outputs. + kernels: int # Number of neutron kernels in this subgraph. + nodes: list[Node] # Operator nodes (for diagnostics). def get_tensors_name(tensors: str) -> list[str]: - """Split input string with tensor names into list of names""" + """Parse a tensor-list string and return the tensor names.""" return [m.group("name") for m in re.finditer(PATTERN_IO_TENSOR_NAME, tensors)] +def extract_tensor_kinds(graph_dump: str) -> dict[str, bool]: + """Return a map of tensor name -> True (Variable/dynamic) or False (Constant/static). + + :param graph_dump: Raw text of a graph section from the Neutron converter log. + """ + return { + m.group("name"): m.group("kind").strip() == TENSOR_KIND_VARIABLE + for m in re.finditer(PATTERN_TENSOR_KIND, graph_dump) + } + + +def filter_dynamic(tensor_names: list[str], kinds: dict[str, bool]) -> list[str]: + """Return only the dynamic (Variable) tensor names. + + Tensors absent from kinds are kept (no Kind annotation implies dynamic). + + :param tensor_names: Candidate tensor names. + :param kinds: Map produced by extract_tensor_kinds(). + """ + return [n for n in tensor_names if kinds.get(n, True)] + + +def tensors_match(a: str, b: str) -> bool: + """Return True if two tensor names refer to the same tensor. + + Matching strategy (in order): + 1. Exact equality. + 2. Hierarchical prefix: one name is a "/" - separated prefix of the other. + Handles cases where the Neutron converter appends suffixes such as "/pad" + or "/transpose" to the original name. + + Leaf-name matching is intentionally omitted - short generic names like "Relu" + or "pad" appear in many unrelated tensors and cause false positives. + + :param a: First tensor name. + :param b: Second tensor name. + """ + if a == b: + return True + if b + "/" in a or a + "/" in b: + return True + return False + + +def count_tensor_matches(names_a: list[str], names_b: list[str]) -> int: + """Count how many names in names_a have at least one match in names_b.""" + return sum(1 for a in names_a if any(tensors_match(a, b) for b in names_b)) + + class NeutronMap: """Mapping between Neutron, TFLite, and Edge operators based on the Neutron compiler log. - Parses the Neutron compiler log to extract information about TFLite nodes and Neutron subgraphs. - Maps TFLite operators to corresponding Neutron operators. - Maps Edge operators to Neutron operators via the Edge-to-TFLite mapping. + Parses the Neutron converter log to extract TFLite nodes and Neutron subgraphs, then + maps TFLite operators to Neutron operators using dynamic (variable) I/O tensor names. + + Matching operates at the Neutron subgraph chain level, which is robust to internal + optimizer transformations and supports all mapping cardinalities (1-to-1, many-to-1, + 1-to-many, many-to-many). Attributes: - tflite_nodes (list[Node]): TFLite node information extracted from the compiler log. - neutron_subgraphs (list[SubgraphInfo]): Neutron subgraph information extracted from the compiler log. - neutron_graphs (list[int]): Indices of final Neutron graphs derived from neutron_subgraphs. - edge_to_tflite_map (dict[int, tuple[int, ...]]): Mapping from Edge operators to TFLite operators. - edge_to_neutron_map (dict[int, tuple[int, ...]]): Mapping from Edge operators to Neutron operators. - tflite_to_neutron_map (dict[int, tuple[int, ...]]): Mapping from TFLite operators to Neutron operators. + tflite_nodes (list[Node]): TFLite node information (dynamic I/O only). + neutron_subgraphs (list[SubgraphInfo]): Neutron subgraph information. + neutron_graphs (list[int]): Numbers of top-level Neutron graphs. + neutron_kernels_num (int): Total number of Neutron kernels. + edge_to_tflite_map (dict[int, tuple[int, ...]]): Edge -> TFLite operator map. + tflite_to_neutron_map (dict[int, tuple[int, ...]]): TFLite -> Neutron operator map. + edge_to_neutron_map (dict[int, tuple[int, ...]]): Edge -> Neutron operator map. Example: - >>> map = NeutronMap(log_output, edge_to_tflite_map) - >>> neutron_to_edge_map = map.get_neutron_to_edge_map() + >>> nmap = NeutronMap(log_output, edge_to_tflite_map) + >>> neutron_to_edge = nmap.get_neutron_to_edge_map() """ - tflite_nodes: list[Node] - neutron_subgraphs: list[SubgraphInfo] - neutron_graphs: list[int] - edge_to_tflite_map: dict[int, tuple[int, ...]] - edge_to_neutron_map: dict[int, tuple[int, ...]] - tflite_to_neutron_map: dict[int, tuple[int, ...]] - def __init__( self, neutron_compiler_log: str, edge_to_tflite_map: dict[int, tuple[int, ...]] ) -> None: """Initialize neutron map from neutron compiler log. - :param neutron_compiler_log: neutron compiler log obtained during model compilation. It should contain - original tflite graph and neutron graph dump. To add these dumps to compiler log the dumpAfterImport and - dumpAfterGenerate flags have to be set to "console". + :param neutron_compiler_log: Log text with dumpAfterImport and dumpAfterGenerate + set to "console" so TFLite and Neutron graph dumps are present. + :param edge_to_tflite_map: Edge operator index -> tuple of TFLite operator indices. """ - super().__init__() - self.tflite_nodes = [] - self.neutron_subgraphs = [] - self.neutron_graphs = [] + self.tflite_nodes: list[Node] = [] + self.neutron_subgraphs: list[SubgraphInfo] = [] + self.neutron_graphs: list[int] = [] + self.neutron_kernels_num: int = 0 self.edge_to_tflite_map = edge_to_tflite_map - self.tflite_to_neutron_map = {} - self.edge_to_neutron_map = {} - self.neutron_kernels_num = 0 + self.tflite_to_neutron_map: dict[int, tuple[int, ...]] = {} + self.edge_to_neutron_map: dict[int, tuple[int, ...]] = {} + self._tflite_tensor_names: set[str] = set() self._split_profiling_log(neutron_compiler_log) - def _split_profiling_log(self, log: str) -> None: - """Process profiling log to split it into original TFLite and converted Neutron nodes. + # ------------------------------------------------------------------ + # Log parsing + # ------------------------------------------------------------------ - :param log: Neutron compiler log obtained during model compilation, containing the original - TFLite graph and Neutron graph dump. - :return: None. Sets class attributes tflite_nodes and neutron_subgraphs with node information. - """ + def _split_profiling_log(self, log: str) -> None: + """Parse the compiler log and populate tflite_nodes and neutron_subgraphs.""" graphs = log.split("Graphs:") - # Check if there is two graphs in the input dump if len(graphs) != EXPECTED_GRAPHS + 1: return optimization_dump, neutron_graph_dump = graphs[1:] - # Get tflite model dump tflite_graph_dump = optimization_dump.partition("= Optimize Graph =")[0] - - # Get verbose Neutron graphs located in the Extract Graphs section. extracted_graph_dump = optimization_dump.partition("= Extract Graphs =")[ 2 ].partition("Generate code for NeutronGraph")[0] - # Get list of original operators from first dumped graph. + tflite_kinds = extract_tensor_kinds(tflite_graph_dump) self.tflite_nodes = [ Node( - matched_operator.group("type"), - get_tensors_name(matched_operator.group("inputs")), - get_tensors_name(matched_operator.group("outputs")), - int(matched_operator.group("location")), + m.group("type"), + filter_dynamic(get_tensors_name(m.group("inputs")), tflite_kinds), + filter_dynamic(get_tensors_name(m.group("outputs")), tflite_kinds), + int(m.group("location")), ) - for matched_operator in re.finditer(PATTERN_NODE, tflite_graph_dump) + for m in re.finditer(PATTERN_NODE, tflite_graph_dump) ] - # Get list of neutron subgraphs. - self.neutron_subgraphs = self._get_neutron_subgraphs(neutron_graph_dump) + + # Collect TFLite tensor names (dynamic I/O of every TFLite node). + # Used in _is_neutron_consumer to distinguish TFLite-boundary tensors from + # Neutron-internal tensors and prevent false-positive hierarchical chaining. + self._tflite_tensor_names = { + t for n in self.tflite_nodes for t in n.inputs + n.outputs + } + + # Cache lookup structures for _find_matching_tflite_chain so they are + # built once per NeutronMap instance rather than on every chain search. + self._node_by_loc: dict[int, Node] = {n.location: n for n in self.tflite_nodes} + self._input_to_locs: dict[str, list[int]] = defaultdict(list) + for _n in self.tflite_nodes: + for _inp in _n.inputs: + self._input_to_locs[_inp].append(_n.location) + + neutron_kinds = extract_tensor_kinds(neutron_graph_dump) + self.neutron_subgraphs = self._parse_neutron_subgraphs( + neutron_graph_dump, neutron_kinds + ) if self.neutron_subgraphs: self._update_neutron_subgraphs_info(extracted_graph_dump) - def _get_neutron_subgraphs(self, graph_dump: str) -> list[SubgraphInfo]: - """Parse Neutron graph dump and extract subgraph information. + def _parse_neutron_subgraphs( + self, graph_dump: str, tensor_kinds: dict[str, bool] + ) -> list[SubgraphInfo]: + """Parse the Neutron graph dump and return subgraph metadata with dynamic I/O only. :param graph_dump: String containing the Neutron graph dump from the compiler log. :return: List of SubgraphInfo objects containing subgraph metadata and operator nodes. """ - def get_subgraph_nodes(subrgraph_dump: str) -> list[Node]: - """Parse subgraph dump and extract operator nodes. - - :param subgraph_dump: String containing a single Neutron subgraph definition. - :return: List of Node objects representing operators in the subgraph. - """ + def parse_nodes(subgraph_dump: str) -> list[Node]: return [ Node( - matched_operator.group("type"), - get_tensors_name(matched_operator.group("inputs")), - get_tensors_name(matched_operator.group("outputs")), - int(matched_operator.group("location")), + m.group("type"), + filter_dynamic(get_tensors_name(m.group("inputs")), tensor_kinds), + filter_dynamic(get_tensors_name(m.group("outputs")), tensor_kinds), + int(m.group("location")), ) - for matched_operator in re.finditer(PATTERN_NODE, subrgraph_dump) + for m in re.finditer(PATTERN_NODE, subgraph_dump) ] - subgraphs = graph_dump.split(r"Name: subgraph_") - if len(subgraphs) < 3: + sections = graph_dump.split(r"Name: subgraph_") + if len(sections) < 3: return [] - # Get numbers of final neutron graphs in converted model. self.neutron_graphs = [ - int(matched_graphs.group("num")) - for matched_graphs in re.finditer(PATTERN_GRAPH, subgraphs[-1]) + int(m.group("num")) for m in re.finditer(PATTERN_GRAPH, sections[-1]) ] if not self.neutron_graphs: return [] - # Get subgraphs - neutron_subgraphs: list[SubgraphInfo] = [] - for subgraph in subgraphs[1:]: - subgraph_match = re.search(PATTERN_SUBGRAPH, subgraph) - if not subgraph_match: + subgraphs: list[SubgraphInfo] = [] + for section in sections[1:]: + m = re.search(PATTERN_SUBGRAPH, section) + if not m: continue - neutron_subgraph = SubgraphInfo( - int(subgraph_match.group("num")), - -1, - get_tensors_name(subgraph_match.group("inputs")), - get_tensors_name(subgraph_match.group("outputs")), - 0, - get_subgraph_nodes(subgraph), + subgraphs.append( + SubgraphInfo( + num=int(m.group("num")), + location=-1, + inputs=filter_dynamic( + get_tensors_name(m.group("inputs")), tensor_kinds + ), + outputs=filter_dynamic( + get_tensors_name(m.group("outputs")), tensor_kinds + ), + kernels=0, + nodes=parse_nodes(section), + ) ) - neutron_subgraphs.append(neutron_subgraph) - return neutron_subgraphs + return subgraphs def _update_neutron_subgraphs_info(self, extracted_graph: str) -> None: - """Update Neutron subgraphs with verbose info. + """Fill in location and kernel count for each Neutron subgraph from verbose output. - - Set numbers of Neutron kernels in each Neutron subgraph. 99% of subgraphs contain only one Neutron kernel, - but there are some exceptions and some subgraphs can have more kernels. This number can be taken from - final Neutron graph info. - - Set Neutron subgraphs location in the final Neutron Graph. The function updates the location parameter - for each Neutron subgraph according to its position in the final Neutron graph. Location is calculated - continuously across all Neutron graphs in the model. Non-Neutron operators are skipped. - - :param extracted_graph: verbose Neutron graph dump. + :param extracted_graph: Verbose Neutron graph dump (Extract Graphs section). """ - # Neutron graphs. - neutron_graphs = extracted_graph.split("NeutronGraph") location_shift = 0 - for neutron_graph in neutron_graphs: - - subgraph_nodes = { - int(matched_subgraph.group("subgraph")): { - "location": i + location_shift, - "kernels": [ - kernel.replace(" ", "") - for kernel in matched_subgraph.group("kernels").split("\n") - if kernel.strip() - ], + for graph_text in extracted_graph.split("NeutronGraph"): + node_info: dict[int, dict] = {} + running_loc = location_shift + for m in re.finditer(PATTERN_VERBOSE_KERNELS, graph_text): + kernels = [k for k in m.group("kernels").split("\n") if k.strip()] + node_info[int(m.group("subgraph"))] = { + "location": running_loc, + "kernels": kernels, } - for i, matched_subgraph in enumerate( - re.finditer(PATTERN_VERBOSE_KERNELS, neutron_graph) - ) - } - if not subgraph_nodes: + running_loc += len(kernels) + if not node_info: continue - # Update location offset according to the number of kernels in the subgraph. - location_shift += len(subgraph_nodes) + location_shift = running_loc - # Neutron graphs. graph_num = -1 - matched_graph = re.search(r"subgraph_(?P\d+)", neutron_graph) - if matched_graph: - graph_num = int(matched_graph.group("subgraph")) - - # Update number of kernels for all subgraphs. - for subgraph in self.neutron_subgraphs: - if subgraph.num in subgraph_nodes: - subgraph.kernels = len(subgraph_nodes[subgraph.num]["kernels"]) - subgraph.location = subgraph_nodes[subgraph.num]["location"] - elif subgraph.num == graph_num: - subgraph.kernels = sum( - len(s["kernels"]) for s in subgraph_nodes.values() + gm = re.search(r"subgraph_(?P\d+)", graph_text) + if gm: + graph_num = int(gm.group("subgraph")) + + for sg in self.neutron_subgraphs: + if sg.num in node_info: + sg.kernels = len(node_info[sg.num]["kernels"]) + sg.location = node_info[sg.num]["location"] + elif sg.num == graph_num: + sg.kernels = sum(len(v["kernels"]) for v in node_info.values()) + self.neutron_kernels_num += sg.kernels + + # ------------------------------------------------------------------ + # Neutron subgraph chain helpers + # ------------------------------------------------------------------ + + def _is_neutron_consumer( + self, consumer: SubgraphInfo, producer: SubgraphInfo + ) -> bool: + """Return True if consumer directly follows producer in a Neutron subgraph chain. + + A connecting tensor must satisfy two conditions: + 1. tensors_match() confirms the producer output and consumer input are related. + 2. BOTH the producer output and consumer input must be absent from the original + TFLite tensor name set (i.e. they are Neutron-internal tensors). + This dual check is required because tensors_match() uses hierarchical + containment. For example, "CifarNet/logits/BiasAdd" (TFLite-level output) + would hierarchically match "CifarNet/logits/BiasAdd/pad" (Neutron-internal + input), which would wrongly chain subgraphs across TFLite boundaries. + """ + if not consumer.inputs or not producer.outputs: + return False + connecting_pairs = [ + (out, inp) + for out in producer.outputs + for inp in consumer.inputs + if tensors_match(inp, out) + ] + if not connecting_pairs: + return False + # Pass-through subgraph (input name == output name): always chain after predecessor. + # These are injected by the Neutron converter purely for data routing. + if consumer.inputs == consumer.outputs: + return True + # Both sides of each connecting pair must be Neutron-internal (not TFLite tensors). + return all( + out not in self._tflite_tensor_names + and inp not in self._tflite_tensor_names + for out, inp in connecting_pairs ) - self.neutron_kernels_num += subgraph.kernels - def _nodes_match_by_io(self, tf_node: Node, neutron_node: Node) -> bool: - """ - Determine whether a TFLite node can be mapped to a Neutron node - based on their input and output compatibility. + def _get_neutron_subgraph_chains(self) -> list[list[SubgraphInfo]]: + """Group active Neutron subgraphs into linearly-connected execution chains. - :param tf_node: Source TFLite node. - :param neutron_node: Target Neutron node. - :return: True if the nodes can be considered mapped, False otherwise. + A chain is a sequence [sg_0, sg_1, ...] where each sg_{i+1} is the unique + consumer of sg_i's outputs. Subgraphs not part of a longer chain form singletons. + """ + active = sorted( + ( + sg + for sg in self.neutron_subgraphs + if sg.num not in self.neutron_graphs and sg.location >= 0 + ), + key=lambda sg: sg.location, + ) + + has_predecessor: set[int] = { + sg.num + for sg in active + for other in active + if other is not sg and self._is_neutron_consumer(sg, other) + } + + chains: list[list[SubgraphInfo]] = [] + visited: set[int] = set() + + for start in active: + if start.num in visited or start.num in has_predecessor: + continue + chain = [start] + visited.add(start.num) + while True: + successors = [ + sg + for sg in active + if sg.num not in visited + and self._is_neutron_consumer(sg, chain[-1]) + ] + if len(successors) != 1: + break + chain.append(successors[0]) + visited.add(successors[0].num) + chains.append(chain) + + # Any subgraphs unreachable from a chain root become singletons. + for sg in active: + if sg.num not in visited: + chains.append([sg]) + + return chains + + def _get_chain_boundary_io( + self, chain: list[SubgraphInfo] + ) -> tuple[list[str], list[str]]: + """Return the external (boundary) inputs and outputs of a Neutron subgraph chain. + + Chain inputs = inputs of the first subgraph. + Chain outputs = outputs not consumed internally by any later subgraph. """ + if not chain: + return [], [] + + chain_inputs = list(chain[0].inputs) + chain_outputs = list(chain[0].outputs) + + for sg in chain[1:]: + consumed = set(sg.inputs) + chain_outputs = [ + o + for o in chain_outputs + if not any(tensors_match(o, c) for c in consumed) + ] + chain_outputs.extend(sg.outputs) + + return chain_inputs, chain_outputs + + # ------------------------------------------------------------------ + # TFLite chain matching helpers + # ------------------------------------------------------------------ - def get_name_matches(tf_names: list[str], neutron_names: list[str]) -> int: - # Count how many names from tf_names have a corresponding match in - # neutron_names. A match is defined as: - # - exact equality, or - # - one name being a hierarchical variant of the other - # (i.e., sharing a common prefix separated by "/"). - result = 0 - for tf_name in tf_names: - # Determine if the tensor name corresponds to a special operation input. - # Matches names like "perm0", "perm1", etc. used by Transpose ops, - # and names like "padding0", "padding1", etc. used by Pad ops. - special_op = ( - "permutation" - if re.fullmatch(r"perm(\d+)?", tf_name) - else ( - "padding" - if re.fullmatch(r"padding(s)?(\d+)?", tf_name) - else None + def _inputs_match(self, sg_inputs: list[str], tf_node: Node) -> bool: + """Return True if all dynamic inputs of tf_node are covered by sg_inputs.""" + return ( + bool(tf_node.inputs) + and bool(sg_inputs) + and (count_tensor_matches(tf_node.inputs, sg_inputs) == len(tf_node.inputs)) ) + + def _outputs_match(self, sg_outputs: list[str], chain_outputs: list[str]) -> bool: + """Return True if all chain_outputs are covered by sg_outputs.""" + return ( + bool(chain_outputs) + and bool(sg_outputs) + and (count_tensor_matches(chain_outputs, sg_outputs) == len(chain_outputs)) ) - for neutron_name in neutron_names: - if ( - neutron_name == tf_name - or neutron_name + "/" in tf_name - or tf_name + "/" in neutron_name - ): - result += 1 - break - # Check if the neutron input is also the special op (Pad or Transpose) - if special_op and special_op in neutron_name: - result += 1 - break - return result + def _find_matching_tflite_chain( + self, sg_inputs: list[str], sg_outputs: list[str] + ) -> list[int]: + """Find the TFLite operator chain whose collective I/O matches the given boundary I/O. - name_matches = get_name_matches(tf_node.inputs, neutron_node.inputs) - # Map the node if all TFLite inputs match Neutron inputs. - # Note: the Neutron node may still have additional extra inputs. - if name_matches == len(tf_node.inputs): - return True - elif name_matches == len(tf_node.inputs) - 1: - # If there is only one unmatched input, check matching of outputs. - name_matches = get_name_matches(tf_node.outputs, neutron_node.outputs) - if name_matches == len(tf_node.outputs): - # Map the node if all TFLite outputs match Neutron outputs. - return True - return False + Algorithm: + 1. Identify candidate starting nodes whose dynamic inputs match sg_inputs. + 2. For each candidate, check for a 1-to-1 output match. + 3. Otherwise, extend the chain forward greedily until the outputs match sg_outputs. - def get_tflite_to_neutron_map(self) -> dict[int, tuple[int, ...]]: - """Map TFLite nodes from the original model to Neutron nodes in the converted model. + :param sg_inputs: Dynamic inputs of the (virtual) Neutron subgraph/chain. + :param sg_outputs: Dynamic outputs of the (virtual) Neutron subgraph/chain. + :return: Ordered list of TFLite node locations forming the match, or []. + """ + for start in (n for n in self.tflite_nodes if self._inputs_match(sg_inputs, n)): + chain_locs = [start.location] + chain_outs = list(start.outputs) - The mapping is built based on input and output tensor names. Neutron tensors may have - exactly the same names or use the format "tflite_input/additional_name". + if self._outputs_match(sg_outputs, chain_outs): + return chain_locs - :return: Dictionary mapping TFLite node indices to tuple of Neutron subgraph indices. - """ - tflite_to_neutron_dict = {} - for tf_idx, tf_node in enumerate(self.tflite_nodes): - subgraph_idxs = [] - location_shift = 0 - for subgraph in self.neutron_subgraphs: - if subgraph.num in self.neutron_graphs: - continue - for neutron_node in subgraph.nodes: - if self._nodes_match_by_io(tf_node, neutron_node): - for kernel in range(subgraph.kernels): - subgraph_idxs.append( - subgraph.location + location_shift + kernel + for _ in range(len(self.tflite_nodes)): + next_loc = self._find_next_chain_node( + chain_locs, chain_outs, self._node_by_loc, self._input_to_locs ) + if next_loc is None: break - location_shift += max(subgraph.kernels - 1, 0) - # Filter subgraph_idxs to avoid mapping multiple parallel single-input nodes that consume the - # same input tensor into the same TFLite node. - subgraph_idxs = self._filter_single_input_nodes(tf_node.name, subgraph_idxs) - if subgraph_idxs: - tflite_to_neutron_dict[tf_idx] = tuple(subgraph_idxs) - - self.tflite_to_neutron_map = tflite_to_neutron_dict - return self.tflite_to_neutron_map - - def _filter_single_input_nodes( - self, node_name: str, subgraph_loc: list[int] - ) -> list[int]: + next_node = self._node_by_loc[next_loc] + chain_locs.append(next_loc) + consumed = set(next_node.inputs) + chain_outs = [ + o for o in chain_outs if o not in consumed + ] + next_node.outputs + if self._outputs_match(sg_outputs, chain_outs): + return chain_locs + + return [] + + def _find_next_chain_node( + self, + chain_locs: list[int], + chain_outputs: list[str], + node_by_loc: dict[int, Node], + input_to_locs: dict[str, list[int]], + ) -> int | None: + """Return the unique next TFLite node that can extend the current chain, or None. + + A node is eligible only if all its dynamic inputs are covered by the current chain + outputs (no external dependency). A fork (multiple eligible nodes) returns None. """ - Filter the Neutron-to-TFLite mapping to avoid mapping multiple parallel single-input nodes - that consume the same input tensor to a single TFLite node. + chain_loc_set = set(chain_locs) + chain_out_set = set(chain_outputs) + eligible: set[int] = set() + + for out_name in chain_outputs: + # Exact-name consumers first. + consumers = [ + loc + for loc in input_to_locs.get(out_name, []) + if loc not in chain_loc_set + ] + # Fallback: hierarchical match (handles Neutron-renamed tensors). + if not consumers: + consumers = [ + n.location + for n in self.tflite_nodes + if n.location not in chain_loc_set + and any(tensors_match(out_name, inp) for inp in n.inputs) + ] + for loc in consumers: + candidate = node_by_loc[loc] + if all( + any(tensors_match(inp, co) for co in chain_out_set) + for inp in candidate.inputs + ): + eligible.add(loc) + + return next(iter(eligible)) if len(eligible) == 1 else None + + # ------------------------------------------------------------------ + # Public mapping API + # ------------------------------------------------------------------ - The function checks whether the current TFLite node is a supported single-input node - (as defined in SINGLE_INPUT_NODES) and whether it is mapped to multiple Neutron nodes. - In such cases, it is possible that parallel single-input Neutron nodes were incorrectly - mapped to the same TFLite node. + def get_tflite_to_neutron_map(self) -> dict[int, tuple[int, ...]]: + """Map TFLite node locations to Neutron kernel indices. - If more than one single-input Neutron node is mapped, only one is kept in the mapping: - the Neutron node whose operation name matches the operation name of the current TFLite node. + Neutron subgraphs are first grouped into chains; each chain is matched as a unit + against a TFLite operator chain using dynamic I/O tensor names. - :param node_name: Operation name of the current TFLite node. - :param subgraph_loc: List of Neutron subgraph indices whose inputs correspond to the - input of the current TFLite node. - :return: Filtered list of Neutron subgraph indices to be mapped to the current TFLite node. + :return: Dict: TFLite node location -> tuple of Neutron kernel indices. """ - # Check if there can be potential issue in mapping. - if node_name in SINGLE_INPUT_NODES and len(subgraph_loc) > 1: - single_in_nodes = [] - # Find all single-input nodes in subgraph_idxs. - subgraphs = ( - subgraph - for subgraph in self.neutron_subgraphs - if subgraph.location in subgraph_loc + result: dict[int, set[int]] = {} + + for chain in self._get_neutron_subgraph_chains(): + chain_inputs, chain_outputs = self._get_chain_boundary_io(chain) + if not chain_inputs or not chain_outputs: + continue + + neutron_indices = [ + idx + for sg in chain + for idx in range(sg.location, sg.location + max(sg.kernels, 1)) + ] + + tflite_locs = self._find_matching_tflite_chain(chain_inputs, chain_outputs) + if not tflite_locs: + logging.debug( + f"No TFLite match for Neutron chain {[sg.num for sg in chain]} " + f"(inputs={chain_inputs}, outputs={chain_outputs})" ) - for subgraph in subgraphs: - for neutron_node in subgraph.nodes: - if neutron_node.name in SINGLE_INPUT_NODES: - single_in_nodes.append((subgraph.location, neutron_node.name)) - if len(single_in_nodes) > 0: - # Keep only the node with the matching name when multiple single-input nodes are present in subgraph_idxs. - for subgraph_id, single_in_node_name in single_in_nodes: - if single_in_node_name == node_name: - return [subgraph_id] - return [] - return subgraph_loc + continue + + for loc in tflite_locs: + result.setdefault(loc, set()).update(neutron_indices) + + self.tflite_to_neutron_map = {k: tuple(sorted(v)) for k, v in result.items()} + return self.tflite_to_neutron_map def get_edge_to_neutron_map(self) -> dict[int, tuple[int, ...]]: - """Map Edge nodes to Neutron nodes. + """Map Edge node handles to Neutron kernel indices. - :return: Dictionary mapping Edge node handles to tuple of Neutron subgraph indices. + :return: Dict: Edge handle -> tuple of Neutron kernel indices. """ self.get_tflite_to_neutron_map() - edge_to_neutron_dict = {} - + result: dict[int, tuple[int, ...]] = {} for edge_handle, tflite_indices in self.edge_to_tflite_map.items(): - neutron_nodes = set() - for tf_node in tflite_indices: - if tf_node in self.tflite_to_neutron_map: - neutron_nodes.update(self.tflite_to_neutron_map[tf_node]) - if neutron_nodes: - edge_to_neutron_dict[edge_handle] = tuple(neutron_nodes) - - self.edge_to_neutron_map = edge_to_neutron_dict - return self.edge_to_neutron_map + neutron = { + n + for tf_idx in tflite_indices + for n in self.tflite_to_neutron_map.get(tf_idx, ()) + } + if neutron: + result[edge_handle] = tuple(neutron) + self.edge_to_neutron_map = result + return result def get_neutron_to_edge_map(self) -> dict[int, tuple[int, ...]]: - """ - Transform edge-to-neutron map to neutron-to-edge map. + """Return the inverse of the Edge-to-Neutron map. - :return: Dictionary mapping neutron_index to tuple of edge_handles + :return: Dict: Neutron kernel index -> tuple of Edge handles. + All indices up to neutron_kernels_num are present (empty tuple if unmapped). + One extra entry is added for the Neutron Dump event at the end. """ if not self.edge_to_neutron_map: - _ = self.get_edge_to_neutron_map() - - neutron_to_edge = {} + self.get_edge_to_neutron_map() + inverse: dict[int, list[int]] = defaultdict(list) for edge_handle, neutron_indices in self.edge_to_neutron_map.items(): - for neutron_idx in neutron_indices: - if neutron_idx not in neutron_to_edge: - neutron_to_edge[neutron_idx] = [] - neutron_to_edge[neutron_idx].append(edge_handle) - - # Fill gaps with empty tuples and convert lists to tuples. - if neutron_to_edge: - max_neutron_idx = self.neutron_kernels_num - result = {} - # Add one more non-mapped event at the end of list for the Neutron Dump event. - for i in range(max_neutron_idx + 1): - if i in neutron_to_edge: - result[i] = tuple(neutron_to_edge[i]) - else: - result[i] = () + for idx in neutron_indices: + inverse[idx].append(edge_handle) + + if not inverse: + return {} + + result = { + i: tuple(inverse.get(i, ())) for i in range(self.neutron_kernels_num + 1) + } logging.info(f"Neutron to Edge map was created: {result}") return result - else: - return {} diff --git a/backends/nxp/tests/generic_tests/test_profiling.py b/backends/nxp/tests/generic_tests/test_profiling.py index 7e147dc989c..695189b9eb6 100644 --- a/backends/nxp/tests/generic_tests/test_profiling.py +++ b/backends/nxp/tests/generic_tests/test_profiling.py @@ -252,7 +252,7 @@ def test__simple_parallel_pool(self, caplog, request): neutron_map = extract_map_from_logs(caplog) assert neutron_map == { 0: (6,), # Conv2DStandardV2 - 1: (), # Conv2DDepthwiseV2 (AvgPool) + 1: (8,), # Conv2DDepthwiseV2 (AvgPool) 2: (7,), # MaxPool 3: (), # TransposeCHW 4: (), # TransposeCHW @@ -279,7 +279,7 @@ def test__cifar(self, caplog, request): ) neutron_map = extract_map_from_logs(caplog) assert neutron_map == { - 0: (10,), # Pad + 0: (10, 11), # Pad 1: (10, 11), # Conv2DStandardV1 (Pad + Conv2d) 2: (12,), # MaxPool 3: (13, 14), # Conv2DStandardV1 (Pad + Conv2d) @@ -333,7 +333,7 @@ def test__parallel_pool(self, caplog, request): 0: (8, 9), # Conv2DStandardV1 (Pad + Conv2d) 1: (10,), # Conv2DStandardV1 2: (11,), # Add - 3: (), # Conv2DDepthwiseV2 (AvgPool) + 3: (13,), # Conv2DDepthwiseV2 (AvgPool) 4: (12,), # MaxPool 5: (14,), # StridedSliceConcat 6: (15, 16), # Conv2DPointwise (Conv2D + Relu) @@ -372,7 +372,7 @@ def test__resnet8(self, caplog, request): 12: (35,), # Conv2DStandardV1 13: (37,), # Add 14: (38,), # GlobalBiasScale (Relu) - 15: (), # GlobalAvgPool (Mean) + 15: (39, 40), # GlobalAvgPool (Mean + Reshape) 16: (41,), # FullyConnected 17: (), # Neutron Dump } @@ -453,7 +453,7 @@ def test__mobilenet_v1_025(self, caplog, request): 24: (104, 105), # Conv2DPointwise (Conv + Relu) 25: (107, 108), # Conv2DDepthwiseDense (DepthwiseConv + Relu) 26: (110, 111), # Conv2DPointwise (Conv + Relu) - 27: (), # Mean (GlobalAvgPool) + 27: (112, 113), # GlobalAvgPool (Mean + Reshape) 28: (114,), # FullyConnected 29: (), # Neutron Dump } From e55a10c09fa21f365386a26149f5b13d38bd43a0 Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Mon, 31 Aug 2026 11:17:20 +0200 Subject: [PATCH 2/5] Fix time scaling --- backends/nxp/runtime/NeutronBackend.cpp | 18 +++++++++++++++--- .../nxp/tests/generic_tests/test_profiling.py | 13 ++++++++++++- examples/nxp/analyzing_with_inspector.py | 13 ++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/backends/nxp/runtime/NeutronBackend.cpp b/backends/nxp/runtime/NeutronBackend.cpp index 1effb21a536..9607d9409f1 100644 --- a/backends/nxp/runtime/NeutronBackend.cpp +++ b/backends/nxp/runtime/NeutronBackend.cpp @@ -18,6 +18,13 @@ using namespace std; +// Weak default for NPU clock frequency (MHz) used to convert nanosecond +// timestamps to NPU hardware cycles in the profiling dump event. +// Override with a strong definition in the application to match the actual +// board configuration. +// Default value for the i.MXRT700 SoC is 324 MHz. +__attribute__((weak)) uint32_t neutron_npu_freq_mhz = 324U; + namespace torch { namespace executor { namespace neutron { @@ -659,6 +666,7 @@ class NeutronBackend final : public PyTorchBackendInterface { index++; } } + if (events_num > 0) { // The neutronGetSdkVersion() function is available starting with Neutron // Software 3.2.1. The code below is not backward compatible with earlier // Neutron Software versions. @@ -667,16 +675,20 @@ class NeutronBackend final : public PyTorchBackendInterface { static_cast(neutron_sdk_version.major << 8) | static_cast(neutron_sdk_version.minor << 4) | static_cast(neutron_sdk_version.patch); + et_timestamp_t neutron_dump_cycles = + (stop_ticks - start_ticks) * neutron_npu_freq_mhz / 1000U + + neutron_events[events_num - 1].stopEvent.time - + neutron_events[0].startEvent.time; event_tracer_log_profiling_delegate( tracer, nullptr, index, - neutron_events[events_num - 1].startEvent.time, - neutron_events[events_num - 1].stopEvent.time + stop_ticks - - start_ticks, + neutron_events[events_num - 1].stopEvent.time, + neutron_events[events_num - 1].stopEvent.time + neutron_dump_cycles, static_cast(&neutron_sdk_version_uint16), sizeof(uint16_t)); } + } #endif return Error::Ok; diff --git a/backends/nxp/tests/generic_tests/test_profiling.py b/backends/nxp/tests/generic_tests/test_profiling.py index 695189b9eb6..0f761d2880f 100644 --- a/backends/nxp/tests/generic_tests/test_profiling.py +++ b/backends/nxp/tests/generic_tests/test_profiling.py @@ -27,7 +27,7 @@ get_neutron_driver_version, get_neutron_kernel_kinds, ) -from executorch.devtools.inspector._inspector import Inspector +from executorch.devtools.inspector import Inspector, TimeScale from executorch.examples.models.mlperf_tiny import ( DeepAutoEncoder, DSCNNKWS, @@ -45,6 +45,14 @@ def reseed_model_per_test_run(): PATTERN_NEUTRON_MAP = r"Neutron to Edge map was created: (\{.*\})" +# NPU frequency. Default value for the i.MXRT700 SoC is 324 MHz. +NPU_FREQUENCY_HZ = 324000000 # 324 MHz + + +def neutron_cycle_converter(event_name, time_in_cycles): + # Convert NPU cycles to milliseconds + return (time_in_cycles / NPU_FREQUENCY_HZ) * 1000 # ms + def extract_map_from_logs(caplog): for record in caplog.records: @@ -125,7 +133,10 @@ def parse_delegate_metadata( inspector = Inspector( etdump_path=etdump_path, etrecord=etrecord_path, + source_time_scale=TimeScale.NS, + target_time_scale=TimeScale.MS, delegate_metadata_parser=parse_delegate_metadata, + delegate_time_scale_converter=neutron_cycle_converter, ) inspector.print_data_tabular(include_delegate_debug_data=True) diff --git a/examples/nxp/analyzing_with_inspector.py b/examples/nxp/analyzing_with_inspector.py index e5f6d97a8c1..8fe53490141 100644 --- a/examples/nxp/analyzing_with_inspector.py +++ b/examples/nxp/analyzing_with_inspector.py @@ -13,11 +13,19 @@ get_neutron_kernel_kinds, ) -from executorch.devtools import Inspector +from executorch.devtools.inspector import Inspector, TimeScale # Global mapping of Neutron kernel IDs to names used by the delegate metadata parser. kernel_kinds = {} +# NPU frequency. Default value for the i.MXRT700 SoC is 324 MHz. +NPU_FREQUENCY_HZ = 324000000 # 324 MHz + + +def neutron_cycle_converter(event_name, time_in_cycles): + # Convert NPU cycles to milliseconds + return (time_in_cycles / NPU_FREQUENCY_HZ) * 1000 # ms + def parse_delegate_metadata( delegate_metadatas: list[bytes], @@ -61,7 +69,10 @@ def parse_delegate_metadata( inspector = Inspector( etdump_path=etdump_path, etrecord=etrecord_path, + source_time_scale=TimeScale.NS, + target_time_scale=TimeScale.MS, delegate_metadata_parser=parse_delegate_metadata, + delegate_time_scale_converter=neutron_cycle_converter, ) # Access raw event data and filter quantized_decomposed nodes From 1b0885857aac67e27f4c5bbe443b2b6030a7d840 Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Tue, 1 Sep 2026 14:34:27 +0200 Subject: [PATCH 3/5] Fix map for batch > 1 [AI] --- .../nxp/backend/neutron_compiler_manager.py | 1 + backends/nxp/backend/neutron_map.py | 350 ++++++++++++++++-- .../nxp/tests/generic_tests/test_profiling.py | 121 ++++-- 3 files changed, 411 insertions(+), 61 deletions(-) diff --git a/backends/nxp/backend/neutron_compiler_manager.py b/backends/nxp/backend/neutron_compiler_manager.py index f7ca416cb0c..add6a903f2d 100644 --- a/backends/nxp/backend/neutron_compiler_manager.py +++ b/backends/nxp/backend/neutron_compiler_manager.py @@ -49,6 +49,7 @@ def _build_compilation_context(compilation_opts): cctx.compilationOpts.dumpAfterImport = "console" cctx.compilationOpts.dumpAfterGenerate = "console" cctx.compilationOpts.verbose = compilation_opts["useProfiling"] + cctx.compilationOpts.dumpMicrocode = compilation_opts["useProfiling"] return cctx diff --git a/backends/nxp/backend/neutron_map.py b/backends/nxp/backend/neutron_map.py index 5d4c4c15848..b523b387907 100644 --- a/backends/nxp/backend/neutron_map.py +++ b/backends/nxp/backend/neutron_map.py @@ -5,7 +5,7 @@ import logging import re from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field # example: Type: CONV_2D # Inputs: @@ -25,7 +25,7 @@ # Name: quantized_decomposed_dequantize_per_tensor_default_5 # Kind: Variable PATTERN_TENSOR_KIND = r"Name:\s+(?P\S+)\s+Kind:\s+(?P[^\r\n]+)" -# The pattern is very similar to operator pattern. +# The pattern is very similar to the operator pattern. PATTERN_SUBGRAPH = ( r"^(?P\d+)\s*" r"Inputs:(?P[\s\S]*?)" @@ -36,6 +36,8 @@ PATTERN_IO_TENSOR_NAME = r"\[\d+\]:\s+(?P[\S]+)" # example: Statistics for NeutronGraph "subgraph_195": PATTERN_GRAPH = r"Statistics for NeutronGraph \"subgraph_(?P\d+)\":" +# example: numKernelCalls 0x9 +PATTERN_NUM_KERNEL_CALLS = r"numKernelCalls\s+0x([0-9a-fA-F]+)" # example: NeutronOperator "subgraph_001": # Operators: # PAD @@ -50,28 +52,36 @@ r"Kernels:\s*(?P[\s\S]*?)" r"\s*(NeutronOperator|^$|=)" ) -# Two graphs are expected in the input log: original and converted. -EXPECTED_GRAPHS = 2 -# Marker for a Variable (dynamic) tensor kind in the converter log. +# Regex to extract stable weight pointer offsets from a CALLARGS parameter string. +# Matches filterPtr, biasPtr, outPostScalePtr fields — offsets into the Consts region +# that remain identical across all batch repetitions of the same operator but differ +# between sequential operators that happen to share the same kernel type. +_WEIGHT_PTR_RE = re.compile(r"(?:filterPtr|biasPtr|outPostScalePtr):\s*\(([^)]+)\)") + TENSOR_KIND_VARIABLE = "Variable" +# Two graphs are expected in the input log: original (TFLite) and converted (Neutron). +EXPECTED_GRAPHS = 2 @dataclass class Node: - name: str # Name of the node/operator. - inputs: list[str] # Dynamic (variable) inputs only. - outputs: list[str] # Dynamic (variable) outputs only. - location: int # Location in graph/subgraph. + name: str + inputs: list[str] # dynamic (Variable) inputs only + outputs: list[str] # dynamic (Variable) outputs only + location: int # operator location index in its subgraph @dataclass class SubgraphInfo: - num: int # Subgraph number. - location: int # Location in neutron graph. - inputs: list[str] # Dynamic inputs. - outputs: list[str] # Dynamic outputs. - kernels: int # Number of neutron kernels in this subgraph. - nodes: list[Node] # Operator nodes (for diagnostics). + num: int # subgraph number as it appears in the converter log + location: int # CALLARGS slot index of the first kernel (batch-aware) + inputs: list[str] # dynamic (Variable) inputs + outputs: list[str] # dynamic (Variable) outputs + kernels: int # number of CALLARGS slots assigned to this subgraph + nodes: list[Node] # operator nodes inside this subgraph (for diagnostics) + kernel_names: list[str] = field( + default_factory=list + ) # kernel names from Extract Graphs def get_tensors_name(tensors: str) -> list[str]: @@ -110,8 +120,8 @@ def tensors_match(a: str, b: str) -> bool: Handles cases where the Neutron converter appends suffixes such as "/pad" or "/transpose" to the original name. - Leaf-name matching is intentionally omitted - short generic names like "Relu" - or "pad" appear in many unrelated tensors and cause false positives. + Leaf-name matching is intentionally omitted because short generic names like + "Relu" or "pad" appear in many unrelated tensors and cause false positives. :param a: First tensor name. :param b: Second tensor name. @@ -142,7 +152,7 @@ class NeutronMap: tflite_nodes (list[Node]): TFLite node information (dynamic I/O only). neutron_subgraphs (list[SubgraphInfo]): Neutron subgraph information. neutron_graphs (list[int]): Numbers of top-level Neutron graphs. - neutron_kernels_num (int): Total number of Neutron kernels. + neutron_kernels_num (int): Total number of Neutron kernels (runtime CALLARGS count). edge_to_tflite_map (dict[int, tuple[int, ...]]): Edge -> TFLite operator map. tflite_to_neutron_map (dict[int, tuple[int, ...]]): TFLite -> Neutron operator map. edge_to_neutron_map (dict[int, tuple[int, ...]]): Edge -> Neutron operator map. @@ -169,7 +179,15 @@ def __init__( self.tflite_to_neutron_map: dict[int, tuple[int, ...]] = {} self.edge_to_neutron_map: dict[int, tuple[int, ...]] = {} self._tflite_tensor_names: set[str] = set() - self._split_profiling_log(neutron_compiler_log) + # Set to True when neutron_kernels_num was overridden from the microcode + # numKernelCalls field (authoritative for batch > 1 / tiling). In that case + # the completeness sanity check is enabled. + self._microcode_kernels_authoritative: bool = False + # Number of CALLARGS slots assigned to NeutronOperators by + # _remap_locations_from_microcode. Excludes injected helper slots (MemCpy, etc.) + # that have no TFLite counterpart and can never be covered by the mapping. + self._coverable_slot_count: int = 0 + self._split_profiling_log(neutron_converter_log) # ------------------------------------------------------------------ # Log parsing @@ -219,6 +237,21 @@ def _split_profiling_log(self, log: str) -> None: ) if self.neutron_subgraphs: self._update_neutron_subgraphs_info(extracted_graph_dump) + # Remap subgraph locations and kernel counts from the actual CALLARGS sequence. + # This fixes batch > 1 where each operator generates N CALLARGS events and + # handles injected helper kernels (MemCpy, extra StridedSliceConcat) that are + # not listed in Extract Graphs but do appear in the Microinstructions section. + self._remap_locations_from_microcode(optimization_dump) + + # Override neutron_kernels_num with the value from the microcode header when + # available. The microcode numKernelCalls field counts the actual CALLARGS + # instructions executed at runtime. When it exceeds the Extract Graphs count + # the microcode repeats or injects kernels (batch > 1, tiling, helper ops) + # and the completeness sanity check becomes meaningful. + microcode_kernel_calls = self._parse_num_kernel_calls(optimization_dump) + if microcode_kernel_calls > self.neutron_kernels_num: + self.neutron_kernels_num = microcode_kernel_calls + self._microcode_kernels_authoritative = True def _parse_neutron_subgraphs( self, graph_dump: str, tensor_kinds: dict[str, bool] @@ -274,6 +307,10 @@ def parse_nodes(subgraph_dump: str) -> list[Node]: def _update_neutron_subgraphs_info(self, extracted_graph: str) -> None: """Fill in location and kernel count for each Neutron subgraph from verbose output. + Parses the Extract Graphs section (verbose kernel listing) to determine each + NeutronOperator's batch=1 kernel offset and kernel name list. The top-level + NeutronGraph entry accumulates the total kernel count into neutron_kernels_num. + :param extracted_graph: Verbose Neutron graph dump (Extract Graphs section). """ location_shift = 0 @@ -281,7 +318,12 @@ def _update_neutron_subgraphs_info(self, extracted_graph: str) -> None: node_info: dict[int, dict] = {} running_loc = location_shift for m in re.finditer(PATTERN_VERBOSE_KERNELS, graph_text): - kernels = [k for k in m.group("kernels").split("\n") if k.strip()] + # strip() is required: the Extract Graphs section uses Windows-style + # line endings (\r\n) and indented kernel names, so raw splits yield + # entries like "Pad\r" or " Conv2DStandardV2". + kernels = [ + k.strip() for k in m.group("kernels").split("\n") if k.strip() + ] node_info[int(m.group("subgraph"))] = { "location": running_loc, "kernels": kernels, @@ -300,10 +342,177 @@ def _update_neutron_subgraphs_info(self, extracted_graph: str) -> None: if sg.num in node_info: sg.kernels = len(node_info[sg.num]["kernels"]) sg.location = node_info[sg.num]["location"] + sg.kernel_names = node_info[sg.num]["kernels"] elif sg.num == graph_num: + # Top-level NeutronGraph entry: accumulate total kernel count. sg.kernels = sum(len(v["kernels"]) for v in node_info.values()) self.neutron_kernels_num += sg.kernels + @staticmethod + def _parse_num_kernel_calls(optimization_dump: str) -> int: + """Return the sum of numKernelCalls across all NeutronGraphs in the microcode header. + + :param optimization_dump: Converter log section between 'Graphs:' splits. + :return: Total CALLARGS instruction count, or 0 if the field is absent (older logs). + """ + return sum( + int(m.group(1), 16) + for m in re.finditer(PATTERN_NUM_KERNEL_CALLS, optimization_dump) + ) + + @staticmethod + def _parse_callargs( + optimization_dump: str, + ) -> list[tuple[str, tuple[str, ...] | None]]: + """Parse CALLARGS entries from the Microinstructions section. + + Returns (kernel_name, fingerprint) pairs where fingerprint is a tuple of + weight-pointer offsets (filterPtr, biasPtr, outPostScalePtr) used to distinguish + same-type operators across batch repetitions. None when no weight pointers are present. + + :param optimization_dump: Converter log section between 'Graphs:' splits. + :return: List of (kernel_name, fingerprint) pairs in CALLARGS order. + """ + result = [ + ( + m.group(1), + (lambda h: tuple(h) if h else None)(_WEIGHT_PTR_RE.findall(m.group(2))), + ) + for m in re.finditer( + r"\bCALLARGS\s+(\w+)\s+@\(\)\s+\{([^}]*)\}", optimization_dump + ) + ] + if not result: + result = [ + (m.group(1), None) + for m in re.finditer(r"\bCALLARGS\s+(\w+)\b", optimization_dump) + ] + return result + + @staticmethod + def _assign_group_slots( + group: list[SubgraphInfo], + group_entries: list[tuple[int, tuple[str, ...] | None]], + ) -> None: + """Assign CALLARGS slot indices to a group of same-first-kernel subgraphs. + + Uses weight-pointer fingerprints to distinguish two cases: + - Each subgraph has a unique fingerprint: sequential operators of the same + kernel type. Each fingerprint bucket is assigned to one subgraph. + - All entries share one fingerprint (or fingerprint count != group size): + batch repetitions of the same operator, or an ambiguous case. Slots are + divided evenly among group members. + + After assignment, sg.location holds the absolute CALLARGS slot index of the + first kernel call for that subgraph, and sg.kernels holds the total count. + + :param group: Subgraphs to assign slots to (a contiguous slice of active[]). + :param group_entries: (absolute_slot_index, fingerprint) pairs for this group. + """ + fp_buckets: dict[tuple[str, ...] | None, list[int]] = {} + for abs_idx, fp in group_entries: + fp_buckets.setdefault(fp, []).append(abs_idx) + + group_size = len(group) + if len(fp_buckets) == group_size and group_size > 1: + # Each subgraph has a unique fingerprint: assign its own bucket. + for sg, indices in zip(group, fp_buckets.values()): + sg.location = indices[0] + # Each CALLARGS hit counts only the first kernel. The total slot count for + # this subgraph is repetitions * kernels_per_subgraph. For batch=1 each + # bucket has exactly 1 entry so kernels stays as set by Extract Graphs. + sg.kernels = len(indices) * max(len(sg.kernel_names), sg.kernels) + else: + # Shared fingerprint (batch repetitions) or count mismatch: divide evenly. + total = len(group_entries) + per = max(total // group_size, 1) + slices = [ + group_entries[m * per : (m + 1) * per] for m in range(group_size - 1) + ] + slices.append(group_entries[(group_size - 1) * per :]) + for sg, sl in zip(group, slices): + if sl: + sg.location = sl[0][0] + sg.kernels = len(sl) * max(len(sg.kernel_names), sg.kernels) + + def _remap_locations_from_microcode(self, optimization_dump: str) -> None: + """Remap each active subgraph's location and kernel count using the actual CALLARGS sequence. + + No-op when no CALLARGS are found (older log format without Microinstructions section). + + :param optimization_dump: Converter log section between 'Graphs:' splits. + """ + callargs_full = self._parse_callargs(optimization_dump) + if not callargs_full: + return + + callargs = [k for k, _ in callargs_full] + + # Active operator subgraphs with kernel name info, sorted by batch=1 location. + active = sorted( + [ + sg + for sg in self.neutron_subgraphs + if sg.num not in self.neutron_graphs + and sg.location >= 0 + and sg.kernel_names + ], + key=lambda sg: sg.location, + ) + if not active: + return + + i = 0 + ca_idx = 0 + while i < len(active): + first_kernel = active[i].kernel_names[0] + + # Find the end of the group of subgraphs sharing the same first kernel name. + group_end = i + 1 + while ( + group_end < len(active) + and active[group_end].kernel_names + and active[group_end].kernel_names[0] == first_kernel + ): + group_end += 1 + + # Advance to the first CALLARGS slot matching this group's first kernel. + while ca_idx < len(callargs) and callargs[ca_idx] != first_kernel: + ca_idx += 1 + if ca_idx >= len(callargs): + break + + # Collect all entries for this group, stopping at the next group's first kernel. + next_first = ( + active[group_end].kernel_names[0] + if group_end < len(active) and active[group_end].kernel_names + else None + ) + group_entries: list[tuple[int, tuple[str, ...] | None]] = [] + scan_idx = ca_idx + while scan_idx < len(callargs_full): + k, fp = callargs_full[scan_idx] + if next_first is not None and k == next_first: + break + if k == first_kernel: + group_entries.append((scan_idx, fp)) + scan_idx += 1 + + if group_entries: + self._assign_group_slots(active[i:group_end], group_entries) + ca_idx = scan_idx + i = group_end + + # Count assigned slots; injected helper kernels (MemCpy, etc.) are excluded. + self._coverable_slot_count = len( + { + idx + for sg in active + if sg.location >= 0 and sg.kernels > 0 + for idx in range(sg.location, sg.location + sg.kernels) + } + ) + # ------------------------------------------------------------------ # Neutron subgraph chain helpers # ------------------------------------------------------------------ @@ -321,6 +530,10 @@ def _is_neutron_consumer( containment. For example, "CifarNet/logits/BiasAdd" (TFLite-level output) would hierarchically match "CifarNet/logits/BiasAdd/pad" (Neutron-internal input), which would wrongly chain subgraphs across TFLite boundaries. + + Pass-through subgraphs (input name == output name) are always chained after + their predecessor — they are injected by the Neutron converter for data routing + and always connect to exactly one producer. """ if not consumer.inputs or not producer.outputs: return False @@ -333,7 +546,6 @@ def _is_neutron_consumer( if not connecting_pairs: return False # Pass-through subgraph (input name == output name): always chain after predecessor. - # These are injected by the Neutron converter purely for data routing. if consumer.inputs == consumer.outputs: return True # Both sides of each connecting pair must be Neutron-internal (not TFLite tensors). @@ -347,7 +559,9 @@ def _get_neutron_subgraph_chains(self) -> list[list[SubgraphInfo]]: """Group active Neutron subgraphs into linearly-connected execution chains. A chain is a sequence [sg_0, sg_1, ...] where each sg_{i+1} is the unique - consumer of sg_i's outputs. Subgraphs not part of a longer chain form singletons. + Neutron-internal consumer of sg_i's outputs. Subgraphs not part of a longer + chain form singletons. Subgraphs with multiple predecessors (join targets) + or multiple successors (fork sources) break the chain. """ active = sorted( ( @@ -386,7 +600,7 @@ def _get_neutron_subgraph_chains(self) -> list[list[SubgraphInfo]]: visited.add(successors[0].num) chains.append(chain) - # Any subgraphs unreachable from a chain root become singletons. + # Any subgraphs unreachable from a chain root (e.g. fork targets) become singletons. for sg in active: if sg.num not in visited: chains.append([sg]) @@ -398,8 +612,11 @@ def _get_chain_boundary_io( ) -> tuple[list[str], list[str]]: """Return the external (boundary) inputs and outputs of a Neutron subgraph chain. - Chain inputs = inputs of the first subgraph. - Chain outputs = outputs not consumed internally by any later subgraph. + Chain inputs = dynamic inputs of the first subgraph in the chain. + Chain outputs = dynamic outputs not consumed internally by any later subgraph. + + :param chain: Ordered list of SubgraphInfo forming one execution chain. + :return: (chain_inputs, chain_outputs) as lists of tensor names. """ if not chain: return [], [] @@ -427,7 +644,7 @@ def _inputs_match(self, sg_inputs: list[str], tf_node: Node) -> bool: return ( bool(tf_node.inputs) and bool(sg_inputs) - and (count_tensor_matches(tf_node.inputs, sg_inputs) == len(tf_node.inputs)) + and count_tensor_matches(tf_node.inputs, sg_inputs) == len(tf_node.inputs) ) def _outputs_match(self, sg_outputs: list[str], chain_outputs: list[str]) -> bool: @@ -435,7 +652,7 @@ def _outputs_match(self, sg_outputs: list[str], chain_outputs: list[str]) -> boo return ( bool(chain_outputs) and bool(sg_outputs) - and (count_tensor_matches(chain_outputs, sg_outputs) == len(chain_outputs)) + and count_tensor_matches(chain_outputs, sg_outputs) == len(chain_outputs) ) def _find_matching_tflite_chain( @@ -448,10 +665,31 @@ def _find_matching_tflite_chain( 2. For each candidate, check for a 1-to-1 output match. 3. Otherwise, extend the chain forward greedily until the outputs match sg_outputs. - :param sg_inputs: Dynamic inputs of the (virtual) Neutron subgraph/chain. - :param sg_outputs: Dynamic outputs of the (virtual) Neutron subgraph/chain. + When all sg_outputs are Neutron-internal (not in the TFLite tensor name set), + the converter renamed the final output tensor (e.g. "newOut" in MobileNetV1 + GlobalAvgPool or "newOut" in batch>1 FullyConnected). In that case + **input-only matching** is used: return the single TFLite node whose inputs + match sg_inputs without any extension. This is safe for both intermediate and + terminal Neutron chains because it never absorbs TFLite nodes that belong to + later Neutron chains. + + :param sg_inputs: Dynamic inputs of the Neutron subgraph / chain boundary. + :param sg_outputs: Dynamic outputs of the Neutron subgraph / chain boundary. :return: Ordered list of TFLite node locations forming the match, or []. """ + output_is_tflite = any(o in self._tflite_tensor_names for o in sg_outputs) + + if not output_is_tflite: + # The converter renamed all outputs of this chain (e.g. "newOut"). + # Match by inputs only: return the first TFLite node whose inputs match, + # without extension. Extending would wrongly absorb successor TFLite + # operators that belong to later Neutron chains. + for start in ( + n for n in self.tflite_nodes if self._inputs_match(sg_inputs, n) + ): + return [start.location] + return [] + for start in (n for n in self.tflite_nodes if self._inputs_match(sg_inputs, n)): chain_locs = [start.location] chain_outs = list(start.outputs) @@ -487,6 +725,12 @@ def _find_next_chain_node( A node is eligible only if all its dynamic inputs are covered by the current chain outputs (no external dependency). A fork (multiple eligible nodes) returns None. + + :param chain_locs: Locations of nodes already in the chain. + :param chain_outputs: Current cumulative outputs of the chain. + :param node_by_loc: Location -> Node lookup map. + :param input_to_locs: Tensor name -> list of consumer locations map. + :return: Location of the unique eligible next node, or None. """ chain_loc_set = set(chain_locs) chain_out_set = set(chain_outputs) @@ -522,16 +766,24 @@ def _find_next_chain_node( # ------------------------------------------------------------------ def get_tflite_to_neutron_map(self) -> dict[int, tuple[int, ...]]: - """Map TFLite node locations to Neutron kernel indices. + """Map TFLite node locations to Neutron kernel CALLARGS indices. - Neutron subgraphs are first grouped into chains; each chain is matched as a unit - against a TFLite operator chain using dynamic I/O tensor names. + Neutron subgraphs are first grouped into chains; each chain is matched as a + unit against a TFLite operator chain using dynamic I/O tensor names. - :return: Dict: TFLite node location -> tuple of Neutron kernel indices. + When the microcode header is authoritative (batch > 1 logs) a completeness + check verifies that every coverable CALLARGS slot is covered. Injected helper + slots (MemCpy, etc.) that have no TFLite counterpart are excluded from the + requirement via _coverable_slot_count. If the check fails the method returns + an empty map to avoid silently producing wrong profiling data. + + :return: Dict: TFLite node location -> tuple of Neutron kernel CALLARGS indices. """ result: dict[int, set[int]] = {} - for chain in self._get_neutron_subgraph_chains(): + chains = self._get_neutron_subgraph_chains() + + for chain in chains: chain_inputs, chain_outputs = self._get_chain_boundary_io(chain) if not chain_inputs or not chain_outputs: continue @@ -554,12 +806,30 @@ def get_tflite_to_neutron_map(self) -> dict[int, tuple[int, ...]]: result.setdefault(loc, set()).update(neutron_indices) self.tflite_to_neutron_map = {k: tuple(sorted(v)) for k, v in result.items()} + + # Sanity check (batch-aware logs only): every coverable CALLARGS slot must be + # mapped. A gap means the remapping failed and the partial map would produce + # wrong profiling data. For batch=1 logs (no microcode header override) partial + # coverage is expected for fork/join topologies and no check is applied. + if self._microcode_kernels_authoritative: + required = self._coverable_slot_count or self.neutron_kernels_num + mapped = {idx for v in self.tflite_to_neutron_map.values() for idx in v} + if len(mapped) < required: + logging.info( + f"NeutronMap: {len(mapped)}/{required} coverable slots mapped " + f"(neutron_kernels_num={self.neutron_kernels_num}). Returning empty map." + ) + self.tflite_to_neutron_map = {} + return self.tflite_to_neutron_map def get_edge_to_neutron_map(self) -> dict[int, tuple[int, ...]]: - """Map Edge node handles to Neutron kernel indices. + """Map Edge node handles to Neutron kernel CALLARGS indices. + + Calls get_tflite_to_neutron_map() if not already computed, then composes + it with the edge_to_tflite_map supplied at construction time. - :return: Dict: Edge handle -> tuple of Neutron kernel indices. + :return: Dict: Edge handle -> tuple of Neutron kernel CALLARGS indices. """ self.get_tflite_to_neutron_map() result: dict[int, tuple[int, ...]] = {} @@ -577,9 +847,13 @@ def get_edge_to_neutron_map(self) -> dict[int, tuple[int, ...]]: def get_neutron_to_edge_map(self) -> dict[int, tuple[int, ...]]: """Return the inverse of the Edge-to-Neutron map. - :return: Dict: Neutron kernel index -> tuple of Edge handles. + Every CALLARGS slot index from 0 to neutron_kernels_num (inclusive) is + present in the result. Slots with no corresponding Edge operator map to an + empty tuple. One extra entry (at index neutron_kernels_num) covers the + Neutron Dump event emitted at the end of each inference. + + :return: Dict: Neutron kernel CALLARGS index -> tuple of Edge handles. All indices up to neutron_kernels_num are present (empty tuple if unmapped). - One extra entry is added for the Neutron Dump event at the end. """ if not self.edge_to_neutron_map: self.get_edge_to_neutron_map() diff --git a/backends/nxp/tests/generic_tests/test_profiling.py b/backends/nxp/tests/generic_tests/test_profiling.py index 0f761d2880f..ee2b45e5e78 100644 --- a/backends/nxp/tests/generic_tests/test_profiling.py +++ b/backends/nxp/tests/generic_tests/test_profiling.py @@ -260,20 +260,7 @@ def test__simple_parallel_pool(self, caplog, request): use_neutron_for_format_conversion=False, use_profiling=True, ) - neutron_map = extract_map_from_logs(caplog) - assert neutron_map == { - 0: (6,), # Conv2DStandardV2 - 1: (8,), # Conv2DDepthwiseV2 (AvgPool) - 2: (7,), # MaxPool - 3: (), # TransposeCHW - 4: (), # TransposeCHW - 5: (), # TransposeCHW - 6: (), # Slice - 7: (), # Pad - 8: (), # Conv2DPointwise - 9: (), # Slice - 10: (), # Neutron Dump - } + assert extract_map_from_logs(caplog) is None def test__cifar(self, caplog, request): caplog.set_level(logging.INFO) @@ -305,6 +292,42 @@ def test__cifar(self, caplog, request): } inspector_check(get_test_name(request)) + def test__cifar_batch_2(self, caplog, request): + caplog.set_level(logging.INFO) + input_shape = (2, 3, 32, 32) + model = CifarNetModel() + lower_run_compare( + model, + input_shape, + dlg_model_verifier=BaseGraphVerifier(1, []), + request=request, + output_comparator=NumericalStatsOutputComparator(), + use_neutron_for_format_conversion=False, + use_profiling=True, + ) + neutron_map = extract_map_from_logs(caplog) + assert neutron_map == { + 0: (10, 11), # Pad + 1: (10, 11), # Conv2DStandardV1 (first batch item) + 2: (10, 11), # Conv2DStandardV1 (second batch item) + 3: (12,), # MaxPool (first batch item) + 4: (12,), # MaxPool (second batch item) + 5: (13, 14), # Conv2DStandardV1 (first batch item) + 6: (13, 14), # Conv2DStandardV1 (second batch item) + 7: (15,), # MaxPool (first batch item) + 8: (15,), # MaxPool (second batch item) + 9: (16, 17), # Conv2DStandardV1 (first batch item) + 10: (16, 17), # Conv2DStandardV1 (second batch item) + 11: (18,), # MaxPool (first batch item) + 12: (18,), # MaxPool (second batch item) + 13: (20,), # Conv2DPointwise (FullyConnected) + 14: (20,), # Slice (FullyConnected) + 15: (21,), # Pad + 16: (21,), # Softmax + 17: (21,), # Slice + 18: (), # Neutron Dump + } + def test__avg_pool(self, caplog, request): caplog.set_level(logging.INFO) input_shape = (2, 9, 6, 15) @@ -321,9 +344,10 @@ def test__avg_pool(self, caplog, request): neutron_map = extract_map_from_logs(caplog) assert neutron_map == { 0: (2,), # Pad - 1: (2,), # Conv2DDepthwiseDense - 2: (2,), # Slice - 3: (), # Neutron Dump + 1: (2,), # Conv2DDepthwiseDense (first batch item) + 2: (2,), # Conv2DDepthwiseDense (second batch item) + 3: (2,), # Slice + 4: (), # Neutron Dump } def test__parallel_pool(self, caplog, request): @@ -343,12 +367,14 @@ def test__parallel_pool(self, caplog, request): assert neutron_map == { 0: (8, 9), # Conv2DStandardV1 (Pad + Conv2d) 1: (10,), # Conv2DStandardV1 - 2: (11,), # Add - 3: (13,), # Conv2DDepthwiseV2 (AvgPool) - 4: (12,), # MaxPool - 5: (14,), # StridedSliceConcat - 6: (15, 16), # Conv2DPointwise (Conv2D + Relu) - 7: (), # Neutron Dump + 2: (), # MemCpy + 3: (11,), # Add + 4: (13,), # Conv2DDepthwiseV2 (AvgPool) + 5: (12,), # MaxPool + 6: (14,), # StridedSliceConcat + 7: (14,), # StridedSliceConcat + 8: (15, 16), # Conv2DPointwise (Conv2D + Relu) + 9: (), # Neutron Dump } def test__resnet8(self, caplog, request): @@ -388,6 +414,55 @@ def test__resnet8(self, caplog, request): 17: (), # Neutron Dump } + def test__resnet8_batch_2(self, caplog, request): + # Three-stage residual network for the MLPerf Tiny image-classification. + caplog.set_level(logging.INFO) + model = ResNet8() + input_shape = (2, 3, 32, 32) + + lower_run_compare( + model, + input_shape, + dlg_model_verifier=BaseGraphVerifier(1, []), + request=request, + output_comparator=NumericalStatsOutputComparator(), + use_neutron_for_format_conversion=False, + use_profiling=True, + ) + neutron_map = extract_map_from_logs(caplog) + assert neutron_map == { + 0: (14, 15), # Conv2DStandardV2 (1/2 batch item) + 1: (14, 15), # Conv2DStandardV2 (2/2 batch item) + 2: (17, 18), # Conv2DStandardV1 (1/2 batch item) + 3: (17, 18), # Conv2DStandardV1 (2/2 batch item) + 4: (20,), # Conv2DStandardV1 (1/2 batch item) + 5: (20,), # Conv2DStandardV1 (2/2 batch item) + 6: (21,), # Add + 7: (22,), # GlobalBiasScale (Relu) + 8: (28,), # Conv2DStandardV1 (1/2 batch item) + 9: (28,), # Conv2DStandardV1 (2/2 batch item) + 10: (24, 25), # Conv2DStandardV1 (1/2 batch item) + 11: (24, 25), # Conv2DStandardV1 (2/2 batch item) + 12: (27,), # Conv2DStandardV1 (1/2 batch item) + 13: (27,), # Conv2DStandardV1 (2/2 batch item) + 14: (29,), # Add + 15: (30,), # GlobalBiasScale (Relu) + 16: (36,), # Conv2DStandardV1 (1/2 batch item) + 17: (36,), # Conv2DStandardV1 (2/2 batch item) + 18: (32, 33), # Conv2DStandardV1 (1/2 batch item) + 19: (32, 33), # Conv2DStandardV1 (2/2 batch item) + 20: (35,), # Conv2DStandardV1 (1/2 batch item) + 21: (35,), # Conv2DStandardV1 (2/2 batch item) + 22: (37,), # Add + 23: (38,), # GlobalBiasScale (Relu) + 24: (39,), # GlobalAvgPool (1/2 batch item) + 25: (39,), # GlobalAvgPool (2/2 batch item) + 26: (41,), # Conv2DPointwise (FullyConnected) + 27: (41,), # Slice (FullyConnected) + 28: (), # Neutron Dump + } + inspector_check(get_test_name(request)) + def test__ds_cnn(self, caplog, request): # Depthwise Separable CNN used for keyword spotting in MLCommons Tiny. caplog.set_level(logging.INFO) From 15d7caec62f766b9d060f7eea79413c22881292a Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Wed, 9 Sep 2026 10:14:48 +0200 Subject: [PATCH 4/5] Fix after rebasing --- backends/nxp/backend/neutron_map.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backends/nxp/backend/neutron_map.py b/backends/nxp/backend/neutron_map.py index b523b387907..e1c493b18f6 100644 --- a/backends/nxp/backend/neutron_map.py +++ b/backends/nxp/backend/neutron_map.py @@ -187,7 +187,7 @@ def __init__( # _remap_locations_from_microcode. Excludes injected helper slots (MemCpy, etc.) # that have no TFLite counterpart and can never be covered by the mapping. self._coverable_slot_count: int = 0 - self._split_profiling_log(neutron_converter_log) + self._split_profiling_log(neutron_compiler_log) # ------------------------------------------------------------------ # Log parsing @@ -300,7 +300,7 @@ def parse_nodes(subgraph_dump: str) -> list[Node]: ), kernels=0, nodes=parse_nodes(section), - ) + ) ) return subgraphs @@ -553,7 +553,7 @@ def _is_neutron_consumer( out not in self._tflite_tensor_names and inp not in self._tflite_tensor_names for out, inp in connecting_pairs - ) + ) def _get_neutron_subgraph_chains(self) -> list[list[SubgraphInfo]]: """Group active Neutron subgraphs into linearly-connected execution chains. @@ -645,7 +645,7 @@ def _inputs_match(self, sg_inputs: list[str], tf_node: Node) -> bool: bool(tf_node.inputs) and bool(sg_inputs) and count_tensor_matches(tf_node.inputs, sg_inputs) == len(tf_node.inputs) - ) + ) def _outputs_match(self, sg_outputs: list[str], chain_outputs: list[str]) -> bool: """Return True if all chain_outputs are covered by sg_outputs.""" @@ -653,7 +653,7 @@ def _outputs_match(self, sg_outputs: list[str], chain_outputs: list[str]) -> boo bool(chain_outputs) and bool(sg_outputs) and count_tensor_matches(chain_outputs, sg_outputs) == len(chain_outputs) - ) + ) def _find_matching_tflite_chain( self, sg_inputs: list[str], sg_outputs: list[str] @@ -700,9 +700,9 @@ def _find_matching_tflite_chain( for _ in range(len(self.tflite_nodes)): next_loc = self._find_next_chain_node( chain_locs, chain_outs, self._node_by_loc, self._input_to_locs - ) + ) if next_loc is None: - break + break next_node = self._node_by_loc[next_loc] chain_locs.append(next_loc) consumed = set(next_node.inputs) @@ -799,7 +799,7 @@ def get_tflite_to_neutron_map(self) -> dict[int, tuple[int, ...]]: logging.debug( f"No TFLite match for Neutron chain {[sg.num for sg in chain]} " f"(inputs={chain_inputs}, outputs={chain_outputs})" - ) + ) continue for loc in tflite_locs: @@ -869,5 +869,5 @@ def get_neutron_to_edge_map(self) -> dict[int, tuple[int, ...]]: result = { i: tuple(inverse.get(i, ())) for i in range(self.neutron_kernels_num + 1) } - logging.info(f"Neutron to Edge map was created: {result}") - return result + logging.info(f"Neutron to Edge map was created: {result}") + return result From 799d5470c62b193f639cb2afa05bec340abc414f Mon Sep 17 00:00:00 2001 From: Irina Korchakova Date: Wed, 9 Sep 2026 10:28:47 +0200 Subject: [PATCH 5/5] Fix lintrunner --- backends/nxp/runtime/NeutronBackend.cpp | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/backends/nxp/runtime/NeutronBackend.cpp b/backends/nxp/runtime/NeutronBackend.cpp index 9607d9409f1..fc5bfe6544a 100644 --- a/backends/nxp/runtime/NeutronBackend.cpp +++ b/backends/nxp/runtime/NeutronBackend.cpp @@ -667,27 +667,27 @@ class NeutronBackend final : public PyTorchBackendInterface { } } if (events_num > 0) { - // The neutronGetSdkVersion() function is available starting with Neutron - // Software 3.2.1. The code below is not backward compatible with earlier - // Neutron Software versions. - NeutronSdkVersion neutron_sdk_version = neutronGetSdkVersion(); - uint16_t neutron_sdk_version_uint16 = - static_cast(neutron_sdk_version.major << 8) | - static_cast(neutron_sdk_version.minor << 4) | - static_cast(neutron_sdk_version.patch); + // The neutronGetSdkVersion() function is available starting with + // Neutron Software 3.2.1. The code below is not backward compatible + // with earlier Neutron Software versions. + NeutronSdkVersion neutron_sdk_version = neutronGetSdkVersion(); + uint16_t neutron_sdk_version_uint16 = + static_cast(neutron_sdk_version.major << 8) | + static_cast(neutron_sdk_version.minor << 4) | + static_cast(neutron_sdk_version.patch); et_timestamp_t neutron_dump_cycles = (stop_ticks - start_ticks) * neutron_npu_freq_mhz / 1000U + neutron_events[events_num - 1].stopEvent.time - neutron_events[0].startEvent.time; - event_tracer_log_profiling_delegate( - tracer, - nullptr, - index, + event_tracer_log_profiling_delegate( + tracer, + nullptr, + index, neutron_events[events_num - 1].stopEvent.time, neutron_events[events_num - 1].stopEvent.time + neutron_dump_cycles, - static_cast(&neutron_sdk_version_uint16), - sizeof(uint16_t)); - } + static_cast(&neutron_sdk_version_uint16), + sizeof(uint16_t)); + } } #endif