Skip to content

Commit 27bd1bb

Browse files
authored
NXP backend: Add e2e test for profiling feature (pytorch#20785)
### Summary Add an end-to-end test for the profiling feature that generates ETDump and ETRecord artifacts and validates the resulting profiling data using the ExecuTorch Inspector. Changes: 1. Enable DevTools and Event Tracer support when building nxp_executor_runner. 2. Save profiling artifacts for all models converted with the profiling flag: - ETDump (trace.etdump) is generated and saved by nxp_executor_runner. - ETRecord (etrecord.bin) is generated and saved in nsys_testing.py. Add Inspector-based validation that: - Loads the generated ETDump and ETRecord. - Parses Neutron delegate metadata. - Verifies kernel profiling events and profiling dump events. - Confirms the profiling data can be successfully processed by the ExecuTorch Inspector. Note: This test is currently marked as xfail and should be enabled after the Neutron SW 3.2 release. ### Test plan Test included. cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent 5decfcd commit 27bd1bb

5 files changed

Lines changed: 191 additions & 9 deletions

File tree

backends/nxp/runtime/NeutronBackend.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ class NeutronBackend final : public PyTorchBackendInterface {
586586
char* profile_info =
587587
static_cast<char*>(cfg->dcfg.outputs[profiling_index]);
588588
NeutronFullProfilingEvent* neutron_events =
589-
reinterpreter_cast<NeutronFullProfilingEvent*>(profile_info);
589+
reinterpret_cast<NeutronFullProfilingEvent*>(profile_info);
590590
executorch::runtime::EventTracer* tracer = context.event_tracer();
591591
uint32_t start_time = 0;
592592
int index = 0;

backends/nxp/tests/generic_tests/test_profiling.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
# LICENSE file in the root directory of this source tree.
55
import ast
66
import logging
7+
import os
78
import re
9+
from typing import Any, Union
810

911
import numpy as np
1012
import pytest
@@ -13,17 +15,20 @@
1315
from executorch.backends.nxp.tests.model_output_comparator import (
1416
NumericalStatsOutputComparator,
1517
)
16-
1718
from 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
2026
from executorch.examples.models.mlperf_tiny import (
2127
DeepAutoEncoder,
2228
DSCNNKWS,
2329
MobileNetV1025,
2430
ResNet8,
2531
)
26-
2732
from 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+
49151
class SimpleParallelPoolModel(torch.nn.Module):
50152
def __init__(self, channels: int):
51153
super().__init__()
@@ -84,7 +186,10 @@ def forward(self, x):
84186

85187

86188
class 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)

backends/nxp/tests/nsys_testing.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ def wrapper(*args, **kwargs):
153153

154154
save_pte_program(delegated_program, test_name + "_delegated", test_dir)
155155

156+
# Generate ETRecord if profiling flag is set.
157+
if use_profiling:
158+
etrecord_path = os.path.join(npu_results_dir, "etrecord.bin")
159+
# Create directory if it doesn't exist
160+
os.makedirs(os.path.dirname(etrecord_path), exist_ok=True)
161+
# Save ETRecord
162+
delegated_program.get_etrecord().save(etrecord_path)
163+
logging.info(f"The ETRecord for the model was saved to {etrecord_path}.")
164+
156165
# Preparation of quantized dataset, requires quantization parameters from converted delegated model
157166
if remove_quant_io_ops:
158167
dataset_dir_quant = os.path.join(test_dir, "dataset_quant")

examples/nxp/executor_runner/CMakeLists.txt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
cmake_minimum_required(VERSION 3.16)
77
project(nxp_executor_runner C CXX)
88

9+
option(EVENT_TRACER "Build with event tracing support" ON)
10+
11+
set(NXP_RUNNER_DEFINES NEUTRON_CMODEL)
12+
set(NXP_RUNNER_LIBS)
13+
914
set(EIQ_NEUTRON_TARGET
1015
"imxrt700"
1116
CACHE STRING "Neutron Target to build the executor_runner"
@@ -74,14 +79,18 @@ else()
7479
message(STATUS "Neutron Driver -- found")
7580
endif()
7681

77-
# Check if the ExecuTorch Target is aleady defined, what indicate this
82+
# Check if the ExecuTorch Target is already defined, what indicate this
7883
# CMakeLists.txt is involved as subproject:
7984
message(STATUS "Looking for executorch target")
8085
if(NOT TARGET executorch)
8186
message(STATUS "Looking for executorch target -- not found, adding")
8287
set(EXECUTORCH_BUILD_NXP_NEUTRON ON)
8388
set(EXECUTORCH_BUILD_KERNELS_QUANTIZED ON)
8489
set(EXECUTORCH_BUILD_KERNELS_QUANTIZED_AOT ON)
90+
if(EVENT_TRACER)
91+
set(EXECUTORCH_BUILD_DEVTOOLS ON)
92+
set(EXECUTORCH_ENABLE_EVENT_TRACER ON)
93+
endif()
8594
add_subdirectory(
8695
"${EXECUTORCH_PATH}" "${CMAKE_CURRENT_BINARY_DIR}/executorch"
8796
EXCLUDE_FROM_ALL
@@ -90,6 +99,22 @@ else()
9099
message(STATUS "Looking for executorch target -- found")
91100
endif()
92101

102+
if(EVENT_TRACER)
103+
list(APPEND NXP_RUNNER_DEFINES ET_EVENT_TRACER_ENABLED)
104+
list(APPEND NXP_RUNNER_LIBS etdump flatccrt)
105+
106+
message(STATUS "Looking for flatccrt")
107+
if(NOT TARGET flatccrt)
108+
message(STATUS "Looking for flatccrt -- not found, adding")
109+
add_subdirectory(
110+
"${EXECUTORCH_PATH}/third-party/flatcc"
111+
"${CMAKE_CURRENT_BINARY_DIR}/flatcc" EXCLUDE_FROM_ALL
112+
)
113+
else()
114+
message(STATUS "Looking for flatcc -- found")
115+
endif()
116+
endif()
117+
93118
message(STATUS "Looking for gflags")
94119
if(NOT TARGET gflags::gflags)
95120
message(STATUS "Looking for gflags -- not found, adding")
@@ -108,7 +133,7 @@ add_executable(
108133
${EXECUTORCH_PATH}/extension/data_loader/file_data_loader.cpp
109134
)
110135

111-
target_compile_options(nxp_executor_runner PRIVATE -DNEUTRON_CMODEL)
136+
target_compile_definitions(nxp_executor_runner PRIVATE ${NXP_RUNNER_DEFINES})
112137

113138
target_link_libraries(
114139
nxp_executor_runner
@@ -119,4 +144,5 @@ target_link_libraries(
119144
"-Wl,--whole-archive"
120145
executorch_delegate_neutron
121146
"-Wl,--no-whole-archive"
147+
${NXP_RUNNER_LIBS}
122148
)

examples/nxp/executor_runner/nxp_executor_runner.cpp

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Neutron Backend.
1414
*/
1515

16+
#include <executorch/devtools/etdump/etdump_flatcc.h>
1617
#include <executorch/extension/data_loader/file_data_loader.h>
1718
#include <executorch/runtime/executor/method.h>
1819
#include <executorch/runtime/executor/program.h>
@@ -204,6 +205,28 @@ Error saveOutputs(
204205
return Error::Ok;
205206
}
206207

208+
#ifdef ET_EVENT_TRACER_ENABLED
209+
void saveETDump(
210+
executorch::etdump::ETDumpGen* etdump_gen,
211+
const std::string& outputPath) {
212+
struct stat st;
213+
const auto trace = etdump_gen->get_etdump_data();
214+
if (trace.buf && trace.size > 0) {
215+
if (stat(outputPath.c_str(), &st) == -1) {
216+
mkdir(outputPath.c_str(), 0700);
217+
}
218+
std::string fileName = outputPath + "/trace.etdump";
219+
printf("Saving file %s\n", fileName.c_str());
220+
FILE* f = fopen(fileName.c_str(), "w+");
221+
if (f != NULL) {
222+
fwrite(static_cast<uint8_t*>(trace.buf), 1, trace.size, f);
223+
fclose(f);
224+
}
225+
free(trace.buf);
226+
}
227+
}
228+
#endif
229+
207230
template <typename T>
208231
Error printClassificationOutput(
209232
const torch::executor::EValue& value,
@@ -438,8 +461,20 @@ int main(int argc, char* argv[]) {
438461
&method_allocator, &planned_memory, &tmp_allocator);
439462

440463
{
441-
Result<torch::executor::Method> method =
442-
program->load_method(method_name, &memory_manager);
464+
#ifdef ET_EVENT_TRACER_ENABLED
465+
// Create ETDumpGen before inference
466+
auto etdump_gen_ptr = std::make_unique<executorch::etdump::ETDumpGen>();
467+
executorch::etdump::ETDumpGen* etdump_gen = etdump_gen_ptr.get();
468+
#endif
469+
Result<torch::executor::Method> method = program->load_method(
470+
method_name,
471+
&memory_manager,
472+
#ifdef ET_EVENT_TRACER_ENABLED
473+
etdump_gen
474+
#else
475+
nullptr
476+
#endif
477+
);
443478
if (!method.ok()) {
444479
fprintf(
445480
stderr,
@@ -572,6 +607,12 @@ int main(int argc, char* argv[]) {
572607
}
573608
}
574609
}
610+
611+
#ifdef ET_EVENT_TRACER_ENABLED
612+
// Save ETDump.
613+
saveETDump(etdump_gen, FLAGS_output);
614+
#endif
615+
575616
} // Destruct the method object before destroying the Neutron Device.
576617

577618
printf("Finished...\n");

0 commit comments

Comments
 (0)