Skip to content

Commit 31196dd

Browse files
committed
Adding SDK Example runner python script that runs through e2e test with output verification
1 parent adae047 commit 31196dd

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import argparse
2+
import contextlib
3+
import os
4+
import subprocess
5+
import tempfile
6+
from typing import List, Tuple, Union
7+
8+
import torch
9+
from executorch.exir import ExecutorchProgramManager, to_edge
10+
from executorch.exir.tracer import Value
11+
from executorch.sdk.bundled_program.config import MethodTestCase, MethodTestSuite
12+
13+
from executorch.sdk.bundled_program.core import create_bundled_program
14+
from executorch.sdk.bundled_program.serialize import (
15+
serialize_from_bundled_program_to_flatbuffer,
16+
)
17+
from executorch.sdk.inspector import Inspector
18+
from executorch.sdk.inspector._inspector_utils import compare_results
19+
from torch.export import export
20+
21+
@contextlib.contextmanager
22+
def change_directory(path: str):
23+
# record cwd (current working directory)
24+
cwd = os.getcwd()
25+
try:
26+
os.chdir(path)
27+
yield os.getcwd()
28+
finally:
29+
# restore cwd
30+
os.chdir(cwd)
31+
32+
33+
def run_command(command):
34+
command = command.split()
35+
try:
36+
result = subprocess.run(
37+
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
38+
)
39+
except subprocess.CalledProcessError as e:
40+
print("Command failed with return code", e.returncode)
41+
print("Output:")
42+
print(e.output.decode())
43+
else:
44+
for line in result.stdout.decode().split("\n"):
45+
print(line)
46+
47+
48+
# A simple model for a test case.
49+
class TestModel(torch.nn.Module):
50+
def __init__(self):
51+
super().__init__()
52+
self.linear = torch.nn.Linear(3, 3)
53+
54+
def forward(self, arg):
55+
return self.linear(arg)
56+
57+
def get_eager_model(self) -> torch.nn.Module:
58+
return self
59+
60+
def get_example_inputs(self):
61+
return (torch.randn(3, 3),)
62+
63+
64+
# Builds the sdk_example_runner and returns the path to it.
65+
def build_executor_runner(executorch_root_dir):
66+
with change_directory(executorch_root_dir):
67+
# Clean any existing cmake caches and configure cmake to get ready for a build.
68+
# run_command("rm -rf cmake-out")
69+
run_command("mkdir cmake-out")
70+
run_command("cd cmake-out")
71+
run_command(
72+
"cmake -DBUCK2=buck2 -DEXECUTORCH_BUILD_SDK=1 -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=1 -B cmake-out ."
73+
)
74+
75+
# Build the sdk_example_runner
76+
run_command("cmake --build cmake-out -j8 -t sdk_example_runner")
77+
78+
# Return the path to the sdk_example_runner binary.
79+
return "cmake-out/examples/sdk/sdk_example_runner"
80+
81+
82+
# Take in an eager mode model and convert it to an ExecuTorch program.
83+
def export_to_exec_prog(
84+
model: Union[torch.fx.GraphModule, torch.nn.Module],
85+
example_inputs: Tuple[Value, ...],
86+
) -> ExecutorchProgramManager:
87+
model.eval()
88+
core_aten_ep = export(model, example_inputs)
89+
edge_manager = to_edge(core_aten_ep)
90+
executorch_manager = edge_manager.to_executorch()
91+
return executorch_manager
92+
93+
94+
# Take in an ExecuTorch program and bundle along some input test cases with it
95+
# to produce a bundled program. This bundled program can be consumed by the
96+
# sdk_example_runner to run the model along with the bundled input test cases.
97+
def generate_bundled_program(executorch_program, model, example_inputs):
98+
method_test_suites: List[MethodTestSuite] = []
99+
method_test_cases: List[MethodTestCase] = []
100+
101+
method_test_cases = [MethodTestCase(inputs=example_inputs)]
102+
103+
method_test_suites.append(
104+
MethodTestSuite(
105+
method_name="forward",
106+
test_cases=method_test_cases,
107+
)
108+
)
109+
110+
bundled_program = create_bundled_program(executorch_program, method_test_suites)
111+
bundled_program_buffer = serialize_from_bundled_program_to_flatbuffer(
112+
bundled_program
113+
)
114+
115+
return bundled_program_buffer
116+
117+
118+
# Runs the sdk_example_runner on the given bundled program and returns the paths
119+
# to the etdump and debug_output files generated by the sdk_example_runner. These
120+
# will be used later for comparison against the outputs of the eager mode model.
121+
def run_and_generate_etdump(executorch_root_dir, working_dir_name, binary_path, bundled_program_buffer):
122+
with change_directory(executorch_root_dir):
123+
bundled_program_path = f"{working_dir_name}/bundled_program.pt"
124+
f = open(bundled_program_path, "wb")
125+
f.write(bundled_program_buffer)
126+
f.close()
127+
128+
etdump_path = f"{working_dir_name}/etdump.etdp"
129+
debug_output_path = f"{working_dir_name}/debug_output.bin"
130+
131+
cmd = f"{binary_path} --bundled_program_path {bundled_program_path} --etdump_path {etdump_path} --debug_output_path {debug_output_path} --dump_outputs"
132+
print(f"Running executor runner: {cmd}")
133+
run_command(cmd)
134+
135+
return etdump_path, debug_output_path
136+
137+
138+
# Takes in the etdump file and the debug_output file generated by the sdk_example_runner
139+
# and compares the outputs of the eager mode model and the executorch program using the
140+
# Inspector API's that are explained here in more detail.
141+
# https://pytorch.org/executorch/main/sdk-inspector.html
142+
def verify_outputs(etdump_path, debug_output_path, model, example_inputs):
143+
inspector = Inspector(etdump_path=etdump_path, debug_buffer_path=debug_output_path)
144+
for event_block in inspector.event_blocks:
145+
if event_block.name == "Execute":
146+
# Disable gradient computation since we are only interested in verifying outputs.
147+
with torch.no_grad():
148+
model.eval()
149+
ref_output = model(*example_inputs)
150+
151+
# If the output is a single tensor then convert it into a list for convenience.
152+
if isinstance(ref_output, torch.Tensor):
153+
ref_output = [ref_output]
154+
155+
# Compare the outputs of the eager mode and the executorch program.
156+
# This function will return three stats SNR, MSE, and cosine similarity.
157+
# For a model that is performing weel SNR should be as high as possible,
158+
# MSE should be as close to zero as possible, and cosine similarity should
159+
# be as close to one as possible.
160+
compare_results(
161+
reference_output=ref_output,
162+
run_output=event_block.run_output,
163+
)
164+
165+
166+
if __name__ == "__main__":
167+
parser = argparse.ArgumentParser()
168+
parser.add_argument(
169+
"--executorch_root_dir",
170+
required=True,
171+
help="Set the path to the root of the executorch repo directory.",
172+
)
173+
args = parser.parse_args()
174+
175+
model = TestModel()
176+
example_inputs = model.get_example_inputs()
177+
178+
exec_prog = export_to_exec_prog(model, example_inputs)
179+
bundled_program_buffer = generate_bundled_program(exec_prog, model, example_inputs)
180+
binary_path = build_executor_runner(args.executorch_root_dir)
181+
with tempfile.TemporaryDirectory() as tmpdirname:
182+
etdump_path, debug_output_path = run_and_generate_etdump(
183+
args.executorch_root_dir, tmpdirname, binary_path, bundled_program_buffer
184+
)
185+
verify_outputs(etdump_path, debug_output_path, model, example_inputs)

0 commit comments

Comments
 (0)