44# LICENSE file in the root directory of this source tree.
55import ast
66import logging
7+ import os
78import re
9+ from typing import Any , Union
810
911import numpy as np
1012import pytest
1315from executorch .backends .nxp .tests .model_output_comparator import (
1416 NumericalStatsOutputComparator ,
1517)
16-
1718from executorch .backends .nxp .tests .models import AvgPool2dModule , SoftmaxModule
18- from executorch .backends .nxp .tests .nsys_testing import lower_run_compare
19+ from executorch .backends .nxp .tests .nsys_testing import (
20+ get_test_name ,
21+ lower_run_compare ,
22+ OUTPUTS_DIR ,
23+ )
1924
25+ from executorch .devtools .inspector ._inspector import Inspector
2026from executorch .examples .models .mlperf_tiny import (
2127 DeepAutoEncoder ,
2228 DSCNNKWS ,
2329 MobileNetV1025 ,
2430 ResNet8 ,
2531)
26-
2732from executorch .examples .nxp .experimental .cifar_net .cifar_net import CifarNetModel
2833
2934
@@ -46,6 +51,103 @@ def extract_map_from_logs(caplog):
4651 return None
4752
4853
54+ def inspector_check (test_name : str ) -> None :
55+ """
56+ Validate ExecuTorch Inspector profiling output.
57+
58+ Checks:
59+ 1. Required profiling artifacts (etrecord.bin, trace.etdump) exist.
60+ 2. Inspector can be created and profiling data can be parsed.
61+ 3. All numeric delegate events except the last contain
62+ "Neutron kernel" metadata.
63+ 4. The last numeric delegate event contains
64+ "Profiling dump" metadata.
65+ 5. The profiling dump event does not have associated op types.
66+ """
67+
68+ def parse_delegate_metadata (
69+ delegate_metadatas : list [bytes ],
70+ ) -> Union [list [str ], dict [str , Any ]]:
71+ """Metadata parser for Neutron Backend metadata.
72+
73+ The parser is a callable that deserializes the data and returns neutron kernel number.
74+ The deserialized data is then added back to the corresponding event in the event block for user consumption.
75+ """
76+
77+ metadata_list = []
78+ for metadata_bytes in delegate_metadatas :
79+ if len (metadata_bytes ) == 1 :
80+ function_code = metadata_bytes [0 ]
81+ if function_code == 0 :
82+ metadata_list .append ("Profiling dump" )
83+ else :
84+ metadata_list .append ("Neutron kernel " + str (function_code ))
85+ else :
86+ metadata_list .append ("Invalid metadata size" )
87+ return metadata_list
88+
89+ npu_results_path = os .path .join (OUTPUTS_DIR , test_name , "results_npu" )
90+ etrecord_path = os .path .join (npu_results_path , "etrecord.bin" )
91+ etdump_path = os .path .join (npu_results_path , "trace.etdump" )
92+
93+ # Verify profiling artifacts were generated.
94+ for file_path in (etrecord_path , etdump_path ):
95+ assert os .path .isfile (
96+ file_path
97+ ), f"Required profiling file does not exist: { file_path } "
98+
99+ # Create Inspector and parse profiling data.
100+ try :
101+ inspector = Inspector (
102+ etdump_path = etdump_path ,
103+ etrecord = etrecord_path ,
104+ delegate_metadata_parser = parse_delegate_metadata ,
105+ )
106+ inspector .print_data_tabular (include_delegate_debug_data = True )
107+
108+ except Exception as e :
109+ raise RuntimeError (
110+ "Failed to create or run Inspector for "
111+ f"etdump='{ etdump_path } ', "
112+ f"etrecord='{ etrecord_path } '"
113+ ) from e
114+
115+ # Collect delegated profiling events whose names are numeric
116+ # (0, 1, 2, ..., N). These events are emitted by the Neutron backend.
117+ numeric_events = [
118+ event
119+ for event_block in inspector .event_blocks
120+ for event in event_block .events
121+ if str (event .name ).isdigit ()
122+ ]
123+
124+ assert numeric_events , "No numeric delegate profiling events found"
125+
126+ # All delegate events except the last one should describe
127+ # individual Neutron kernels.
128+ for event in numeric_events [:- 1 ]:
129+ metadata = str (event .delegate_debug_metadatas )
130+
131+ assert "Neutron kernel" in metadata , (
132+ f"Event { event .name } : expected 'Neutron kernel', " f"got { metadata } "
133+ )
134+
135+ # The final numeric event should represent the profiling dump.
136+ profiling_dump_event = numeric_events [- 1 ]
137+ profiling_metadata = str (profiling_dump_event .delegate_debug_metadatas )
138+
139+ assert "Profiling dump" in profiling_metadata , (
140+ f"Event { profiling_dump_event .name } : "
141+ f"expected 'Profiling dump', got { profiling_metadata } "
142+ )
143+
144+ # Profiling dump event is expected to have no associated operators.
145+ assert profiling_dump_event .op_types == [], (
146+ f"Event { profiling_dump_event .name } : expected empty op_types, "
147+ f"got { profiling_dump_event .op_types } "
148+ )
149+
150+
49151class SimpleParallelPoolModel (torch .nn .Module ):
50152 def __init__ (self , channels : int ):
51153 super ().__init__ ()
@@ -84,7 +186,10 @@ def forward(self, x):
84186
85187
86188class TestProfiling :
87- @pytest .mark .xfail (reason = "SoftMax support PR is not merged so far." , strict = True )
189+ @pytest .mark .xfail (
190+ reason = "Profiling support for cmodel and SoftMax fix will be available in Neutron SW 3.2." ,
191+ strict = True ,
192+ )
88193 def test__softmax (self , caplog , request ):
89194 caplog .set_level (logging .INFO )
90195 model = SoftmaxModule (- 1 )
@@ -164,6 +269,7 @@ def test__cifar(self, caplog, request):
164269 10 : (21 ,), # Slice
165270 11 : (), # Neutron Dump
166271 }
272+ inspector_check (get_test_name (request ))
167273
168274 def test__avg_pool (self , caplog , request ):
169275 caplog .set_level (logging .INFO )
0 commit comments