Skip to content

Commit 2805a81

Browse files
support etdump generation in executorch.runtime (#14205)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #14172 by @Gasoonjia ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/gasoonjia/41/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/41/head Merge bot PR base: https://github.com/pytorch/executorch/tree/main Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/gasoonjia/41/orig @diff-train-skip-merge Co-authored-by: gasoonjia <[email protected]>
1 parent 9e37d87 commit 2805a81

File tree

6 files changed

+226
-20
lines changed

6 files changed

+226
-20
lines changed

devtools/etdump/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ runtime.python_library(
2929
},
3030
visibility = [
3131
"//executorch/devtools/...",
32+
"//executorch/runtime/test/...",
3233
],
3334
deps = [
3435
"fbsource//third-party/pypi/setuptools:setuptools",

extension/pybindings/pybindings.cpp

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,7 @@ struct PyProgram final {
12961296

12971297
std::unique_ptr<PyMethod> load_method(const std::string& method_name) {
12981298
Result<Method> res = state_->program_->load_method(
1299-
method_name.c_str(), memory_->mem_manager());
1299+
method_name.c_str(), memory_->mem_manager(), event_tracer_.get());
13001300
THROW_IF_ERROR(
13011301
res.error(),
13021302
"Failed to load method %s, error: 0x:%" PRIx32,
@@ -1321,6 +1321,39 @@ struct PyProgram final {
13211321
return std::make_unique<PyMethodMeta>(state_, std::move(res.get()));
13221322
}
13231323

1324+
bool has_etdump() {
1325+
return static_cast<bool>(event_tracer_);
1326+
}
1327+
1328+
void write_etdump_result_to_file(
1329+
const std::string& path,
1330+
const py::object& debug_buffer_path) {
1331+
if (!has_etdump()) {
1332+
throw std::runtime_error("No etdump found");
1333+
}
1334+
auto& etdump = *event_tracer_;
1335+
etdump_result result = etdump.get_etdump_data();
1336+
if (result.buf != nullptr && result.size > 0) {
1337+
write_data_to_file(path, result.buf, result.size);
1338+
free(result.buf);
1339+
if (debug_buffer_size_ > 0 &&
1340+
py::isinstance<py::str>(debug_buffer_path)) {
1341+
// Also write out the debug buffer to a separate file if requested.
1342+
std::string debug_buffer_path_str =
1343+
py::cast<std::string>(debug_buffer_path);
1344+
const auto debug_buffer = get_etdump_debug_buffer();
1345+
write_data_to_file(
1346+
debug_buffer_path_str, debug_buffer.data(), debug_buffer.size());
1347+
}
1348+
} else {
1349+
ET_LOG(
1350+
Info,
1351+
"No etdump data found, try rebuilding with "
1352+
"the CMake option EXECUTORCH_ENABLE_EVENT_TRACER set to ON or with "
1353+
"buck run --config executorch.event_tracer_enabled=true");
1354+
}
1355+
}
1356+
13241357
private:
13251358
std::shared_ptr<ProgramMemory> memory_;
13261359
std::shared_ptr<ProgramState> state_;
@@ -1554,6 +1587,13 @@ PYBIND11_MODULE(EXECUTORCH_PYTHON_MODULE_NAME, m) {
15541587
"method_meta",
15551588
&PyProgram::method_meta,
15561589
py::arg("method_name"),
1590+
call_guard)
1591+
.def("has_etdump", &PyProgram::has_etdump, call_guard)
1592+
.def(
1593+
"write_etdump_result_to_file",
1594+
&PyProgram::write_etdump_result_to_file,
1595+
py::arg("path"),
1596+
py::arg("debug_buffer_path") = py::none(),
15571597
call_guard);
15581598
py::class_<PyMethod>(m, "ExecuTorchMethod")
15591599
.def("set_inputs", &PyMethod::set_inputs, py::arg("inputs"), call_guard)

pytest.ini

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ addopts =
88
--capture=sys
99
# don't suppress warnings, but don't shove them all to the end either
1010
-p no:warnings
11-
11+
1212
# === TEST DIRECTORIES TO RUN ===
13-
13+
1414
# ci/scripts
1515
.ci/scripts/tests
16-
16+
1717
# backends
1818
backends/apple/coreml/test
1919
backends/test/harness/tests
@@ -28,15 +28,15 @@ addopts =
2828
--ignore=backends/xnnpack/test/quantizer/test_xnnpack_quantizer.py
2929
# Ignore backends/test root - WIP testing infra, see https://github.com/pytorch/executorch/discussions/11140
3030
--ignore=backends/test
31-
31+
3232
# codegen
3333
codegen/test
34-
34+
3535
# devtools
3636
devtools/
3737
# Ignore test with missing dependencies
3838
--ignore=devtools/visualization/visualization_utils_test.py
39-
39+
4040
# examples
4141
examples/models/test
4242
examples/models/llama/tests
@@ -53,7 +53,7 @@ addopts =
5353
--ignore=examples/models/llava/test/test_pte.py
5454
# Ignore failing llava tests (missing accelerate dependency)
5555
--ignore=examples/models/llava/test/test_llava.py
56-
56+
5757
# exir
5858
exir/
5959
# Ignore tests with missing custom_ops_generated_lib dependencies
@@ -73,11 +73,11 @@ addopts =
7373
--ignore=exir/operator/test/test_operator.py
7474
--ignore=exir/tests/test_common.py
7575
--ignore=exir/tests/test_op_convert.py
76-
76+
7777
# export
7878
export/tests
7979
--ignore=export/tests/test_export_stages.py
80-
80+
8181
# extension
8282
extension/
8383
# Ignore tests with missing dependencies or build issues
@@ -87,28 +87,29 @@ addopts =
8787
--ignore=extension/llm/tokenizers/third-party/sentencepiece/python/test/sentencepiece_test.py
8888
# Ignore failing tokenizer tests
8989
--ignore=extension/llm/tokenizers/test/test_tekken_python.py
90-
90+
9191
# kernels
9292
kernels/prim_ops/test
9393
kernels/quantized
9494
kernels/test/test_case_gen.py
9595
# Ignore test depending on test-only cpp ops lib
9696
--ignore=kernels/quantized/test/test_quant_dequant_per_token.py
97-
97+
9898
# profiler
9999
profiler/
100100
# Ignore test with missing dependencies
101101
--ignore=profiler/test/test_profiler_e2e.py
102-
102+
103103
# runtime
104104
runtime
105-
105+
# Ignore tests with missing compiler dependencies
106+
--ignore=runtime/test/test_runtime_etdump_gen.py
106107
# test
107108
test/
108109
# Ignore tests with missing dependencies
109110
--ignore=test/end2end/test_end2end.py
110111
--ignore=test/end2end/test_temp_allocator_fix.py
111-
112+
112113
# tools
113114
tools/cmake
114115

runtime/__init__.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,55 @@
3131
3232
.. code-block:: text
3333
34-
Program methods: ('forward', 'forward2')
34+
Program methods: {'forward'}
3535
Ran forward((tensor([[1., 1.],
3636
[1., 1.]]), tensor([[1., 1.],
3737
[1., 1.]])))
38-
outputs: [tensor([[1., 1.],
39-
[1., 1.]])]
38+
outputs: [tensor([[2., 2.],
39+
[2., 2.]])]
40+
41+
Example usage with ETDump generation:
42+
43+
.. code-block:: python
44+
45+
from pathlib import Path
46+
import os
47+
48+
import torch
49+
from executorch.runtime import Verification, Runtime, Program, Method
50+
51+
# Create program with etdump generation enabled
52+
et_runtime: Runtime = Runtime.get()
53+
program: Program = et_runtime.load_program(
54+
Path("/tmp/program.pte"),
55+
verification=Verification.Minimal,
56+
enable_etdump=True,
57+
debug_buffer_size=1e7, # A large buffer size to ensure that all debug info is captured
58+
)
59+
60+
# Load method and execute
61+
forward: Method = program.load_method("forward")
62+
inputs = (torch.ones(2, 2), torch.ones(2, 2))
63+
outputs = forward.execute(inputs)
64+
65+
# Write etdump result to file
66+
etdump_file = "/tmp/etdump_output.etdp"
67+
debug_file = "/tmp/debug_output.bin"
68+
program.write_etdump_result_to_file(etdump_file, debug_file)
69+
70+
# Check that files were created
71+
print(f"ETDump file created: {os.path.exists(etdump_file)}")
72+
print(f"Debug file created: {os.path.exists(debug_file)}")
73+
print("Directory contents:", os.listdir("/tmp"))
74+
75+
Example output:
76+
77+
.. code-block:: text
78+
79+
Program methods: {'forward'}
80+
ETDump file created: True
81+
Debug file created: True
82+
Directory contents: ['program.pte', 'etdump_output.etdp', 'debug_output.bin']
4083
"""
4184

4285
import functools
@@ -137,6 +180,17 @@ def metadata(self, method_name: str) -> MethodMeta:
137180
"""
138181
return self._program.method_meta(method_name)
139182

183+
def write_etdump_result_to_file(
184+
self, etdump_path: str, debug_buffer_path: str
185+
) -> None:
186+
"""Writes the etdump and debug result to a file.
187+
188+
Args:
189+
etdump_path: The path to the etdump file.
190+
debug_buffer_path: The path to the debug buffer file.
191+
"""
192+
self._program.write_etdump_result_to_file(etdump_path, debug_buffer_path)
193+
140194

141195
class BackendRegistry:
142196
"""The registry of backends that are available to the runtime."""
@@ -201,6 +255,8 @@ def load_program(
201255
data: Union[bytes, bytearray, BinaryIO, Path, str],
202256
*,
203257
verification: Verification = Verification.InternalConsistency,
258+
enable_etdump: bool = False,
259+
debug_buffer_size: int = 0,
204260
) -> Program:
205261
"""Loads an ExecuTorch program from a PTE binary.
206262
@@ -214,8 +270,8 @@ def load_program(
214270
if isinstance(data, (Path, str)):
215271
p = self._legacy_module._load_program(
216272
str(data),
217-
enable_etdump=False,
218-
debug_buffer_size=0,
273+
enable_etdump=enable_etdump,
274+
debug_buffer_size=debug_buffer_size,
219275
program_verification=verification,
220276
)
221277
return Program(p, data=None)

runtime/test/TARGETS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,16 @@ runtime.python_test(
88
deps = [
99
"//executorch/extension/pybindings/test:make_test",
1010
"//executorch/runtime:runtime",
11+
"//executorch/devtools/etdump:serialize",
12+
],
13+
)
14+
15+
runtime.python_test(
16+
name = "test_runtime_etdump_gen",
17+
srcs = ["test_runtime_etdump_gen.py"],
18+
deps = [
19+
"//executorch/extension/pybindings/test:make_test",
20+
"//executorch/runtime:runtime",
21+
"//executorch/devtools/etdump:serialize",
1122
],
1223
)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import tempfile
9+
import unittest
10+
11+
import torch
12+
from executorch.devtools.etdump.serialize import deserialize_from_etdump_flatcc
13+
14+
from executorch.extension.pybindings.test.make_test import create_program, ModuleAdd
15+
from executorch.runtime import Runtime, Verification
16+
17+
18+
class RuntimeETDumpGenTest(unittest.TestCase):
19+
def test_etdump_generation(self):
20+
"""Test etdump generation by creating a program with etdump enabled and verifying the output."""
21+
22+
ep, inputs = create_program(ModuleAdd())
23+
runtime = Runtime.get()
24+
25+
with tempfile.TemporaryDirectory() as temp_dir:
26+
# Save the program to a file
27+
program_path = os.path.join(temp_dir, "test_program.pte")
28+
with open(program_path, "wb") as f:
29+
f.write(ep.buffer)
30+
31+
# Load program with etdump generation enabled
32+
program = runtime.load_program(
33+
program_path,
34+
verification=Verification.Minimal,
35+
enable_etdump=True,
36+
debug_buffer_size=int(
37+
1e7
38+
), # Large buffer size to ensure all debug info is captured
39+
)
40+
41+
# Execute the method
42+
method = program.load_method("forward")
43+
outputs = method.execute(inputs)
44+
45+
# Verify the computation is correct
46+
self.assertTrue(torch.allclose(outputs[0], inputs[0] + inputs[1]))
47+
48+
# Write etdump result to files
49+
etdump_path = os.path.join(temp_dir, "etdump_output.etdp")
50+
debug_path = os.path.join(temp_dir, "debug_output.bin")
51+
program.write_etdump_result_to_file(etdump_path, debug_path)
52+
53+
# Check that files were created
54+
self.assertTrue(
55+
os.path.exists(etdump_path), f"ETDump file not created at {etdump_path}"
56+
)
57+
self.assertTrue(
58+
os.path.exists(debug_path), f"Debug file not created at {debug_path}"
59+
)
60+
61+
# Verify the etdump file is not empty
62+
etdump_size = os.path.getsize(etdump_path)
63+
self.assertGreater(etdump_size, 0, "ETDump file is empty")
64+
65+
# Read and deserialize the etdump file to verify its structure
66+
with open(etdump_path, "rb") as f:
67+
etdump_data = f.read()
68+
69+
# Deserialize the etdump and check its header/structure
70+
etdump = deserialize_from_etdump_flatcc(etdump_data)
71+
72+
# Verify ETDump header properties
73+
self.assertIsInstance(
74+
etdump.version, int, "ETDump version should be an integer"
75+
)
76+
self.assertGreaterEqual(
77+
etdump.version, 0, "ETDump version should be non-negative"
78+
)
79+
80+
# Verify run_data structure
81+
self.assertIsInstance(
82+
etdump.run_data, list, "ETDump run_data should be a list"
83+
)
84+
self.assertGreater(
85+
len(etdump.run_data),
86+
0,
87+
"ETDump should contain at least one run data entry",
88+
)
89+
90+
# Check the first run_data entry
91+
run_data = etdump.run_data[0]
92+
self.assertIsInstance(
93+
run_data.events, list, "Run data should contain events list"
94+
)
95+
self.assertGreater(
96+
len(run_data.events), 0, "Run data should contain at least one events"
97+
)

0 commit comments

Comments
 (0)