Skip to content

Commit 31819f6

Browse files
Support to add running stages for E2E test
Co-authored-by: chong-chen <[email protected]>
1 parent bde6b11 commit 31819f6

File tree

3 files changed

+223
-6
lines changed

3 files changed

+223
-6
lines changed

backends/samsung/test/tester/samsung_tester.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,80 @@
44
# Licensed under the BSD License (the "License"); you may not use this file
55
# except in compliance with the License. See the license file in the root
66
# directory of this source tree for more details.
7-
from typing import List, Optional, Tuple
7+
import copy
8+
from typing import Any, List, Optional, Sequence, Tuple, Union
89

910
import executorch.backends.test.harness.stages as BaseStages
1011
import torch
1112
from executorch.backends.samsung.partition.enn_partitioner import EnnPartitioner
12-
from executorch.backends.samsung.utils.export_utils import get_edge_compile_config
13-
13+
from executorch.backends.samsung.quantizer.quantizer import EnnQuantizer, Precision
14+
from executorch.backends.samsung.test.utils import RuntimeExecutor
15+
from executorch.backends.samsung.utils.export_utils import (
16+
get_edge_compile_config,
17+
get_enn_pass_list,
18+
)
1419
from executorch.backends.test.harness import Tester as TesterBase
20+
from executorch.backends.test.harness.stages import StageType
1521
from executorch.exir import EdgeCompileConfig, to_edge_transform_and_lower
1622
from executorch.exir.backend.backend_details import CompileSpec
1723

24+
from executorch.exir.pass_manager import PassType
1825
from torch.export import ExportedProgram
1926

27+
from torchao.quantization.pt2e.quantizer import Quantizer
28+
2029

2130
class Export(BaseStages.Export):
2231
pass
2332

2433

2534
class Quantize(BaseStages.Quantize):
26-
pass
35+
def __init__(
36+
self,
37+
quantizer: Optional[Quantizer] = None,
38+
quantization_config: Optional[Any] = None,
39+
calibrate: bool = True,
40+
calibration_samples: Optional[Sequence[Any]] = None,
41+
is_qat: Optional[bool] = False,
42+
):
43+
super().__init__(
44+
quantizer=quantizer,
45+
quantization_config=quantization_config,
46+
calibrate=calibrate,
47+
calibration_samples=calibration_samples,
48+
is_qat=is_qat,
49+
)
2750

2851

2952
class ToEdgeTransformAndLower(BaseStages.ToEdgeTransformAndLower):
3053
def __init__(
3154
self,
3255
compile_specs: Optional[List[CompileSpec]] = None,
3356
edge_compile_config: Optional[EdgeCompileConfig] = None,
57+
transform_passes: Optional[Union[Sequence[PassType]]] = None,
3458
):
3559
compile_specs = compile_specs or []
3660
self.partitioners = [EnnPartitioner(compile_specs=compile_specs)]
3761
self.edge_compile_config = edge_compile_config or get_edge_compile_config()
62+
self.transform_passes = transform_passes or get_enn_pass_list()
3863
self.edge_dialect_program = None
3964

4065
def run(
4166
self, artifact: ExportedProgram, inputs=None, generate_etrecord: bool = False
4267
) -> None:
68+
artifact_copy = copy.deepcopy(artifact)
4369
self.edge_dialect_program = to_edge_transform_and_lower(
44-
artifact,
70+
artifact_copy,
71+
transform_passes=self.transform_passes,
4572
partitioner=self.partitioners,
4673
compile_config=self.edge_compile_config,
4774
)
4875

4976

5077
class ToExecutorch(BaseStages.ToExecutorch):
51-
pass
78+
def run_artifact(self, inputs):
79+
runtime_executor = RuntimeExecutor(self.artifact, inputs)
80+
return runtime_executor.run_on_device()
5281

5382

5483
class SamsungTester(TesterBase):
@@ -60,8 +89,14 @@ def __init__(
6089
):
6190
module.eval()
6291

92+
stage_classes = TesterBase.default_stage_classes() | {
93+
StageType.EXPORT: Export,
94+
StageType.TO_EXECUTORCH: ToExecutorch,
95+
}
96+
6397
super().__init__(
6498
module=module,
99+
stage_classes=stage_classes,
65100
example_inputs=example_inputs,
66101
dynamic_shapes=None,
67102
)
@@ -71,6 +106,14 @@ def __init__(
71106
self.example_inputs = example_inputs
72107
self.compile_specs = compile_specs
73108

109+
def quantize(self, quantize_stage: Optional[Quantize] = None):
110+
if quantize_stage is None:
111+
quantizer = EnnQuantizer()
112+
quantizer.setup_quant_params(Precision.A8W8)
113+
quantize_stage = Quantize(quantizer)
114+
115+
return super().quantize(quantize_stage)
116+
74117
def to_edge_transform_and_lower(
75118
self,
76119
edge_compile_config: Optional[EdgeCompileConfig] = None,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Samsung Electronics Co. LTD
2+
# All rights reserved
3+
#
4+
# Licensed under the BSD License (the "License"); you may not use this file
5+
# except in compliance with the License. See the license file in the root
6+
# directory of this source tree for more details.
7+
8+
from .runtime_executor import RuntimeExecutor
9+
10+
11+
__all__ = ["RuntimeExecutor"]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import logging
2+
import os
3+
import subprocess
4+
import tempfile
5+
6+
from functools import cache
7+
from pathlib import Path
8+
from typing import List, Tuple
9+
10+
import numpy as np
11+
import torch
12+
13+
14+
@cache
15+
def get_runner_path() -> Path:
16+
git_root = subprocess.check_output(
17+
["git", "rev-parse", "--show-toplevel"],
18+
cwd=os.path.dirname(os.path.realpath(__file__)),
19+
text=True,
20+
).strip()
21+
return Path(git_root) / "build_samsung_android/backends/samsung/enn_executor_runner"
22+
23+
24+
class ADBTestManager:
25+
def __init__(
26+
self,
27+
pte_file,
28+
work_directory,
29+
input_files: List[str],
30+
):
31+
self.pte_file = pte_file
32+
self.work_directory = work_directory
33+
self.input_files = input_files
34+
self.artifacts_dir = Path(self.pte_file).parent.absolute()
35+
self.output_folder = f"{self.work_directory}/output"
36+
self.runner = str(get_runner_path())
37+
38+
def _adb(self, cmd):
39+
cmds = ["adb"]
40+
41+
assert self._is_adb_connected, "Fail to get available device to execute."
42+
43+
cmds.extend(cmd)
44+
command = " ".join(cmds)
45+
result = subprocess.run(
46+
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
47+
)
48+
49+
if result.returncode != 0:
50+
logging.info(result.stdout.decode("utf-8").strip())
51+
logging.error(result.stderr.decode("utf-8").strip())
52+
raise RuntimeError("adb command execute failed")
53+
54+
def push(self):
55+
self._adb(["shell", f"rm -rf {self.work_directory}"])
56+
self._adb(["shell", f"mkdir -p {self.work_directory}"])
57+
self._adb(["push", self.pte_file, self.work_directory])
58+
self._adb(["push", self.runner, self.work_directory])
59+
60+
for input_file in self.input_files:
61+
input_file_path = os.path.join(self.artifacts_dir, input_file)
62+
if Path(input_file).name == input_file and os.path.isfile(input_file_path):
63+
# default search the same level directory with pte
64+
self._adb(["push", input_file_path, self.work_directory])
65+
elif os.path.isfile(input_file):
66+
self._adb(["push", input_file, self.work_directory])
67+
else:
68+
raise FileNotFoundError(f"Invalid input file path: {input_file}")
69+
70+
def execute(self):
71+
self._adb(["shell", f"rm -rf {self.output_folder}"])
72+
self._adb(["shell", f"mkdir -p {self.output_folder}"])
73+
# run the delegation
74+
input_files_list = " ".join([os.path.basename(x) for x in self.input_files])
75+
enn_executor_runner_args = " ".join(
76+
[
77+
f"--model {os.path.basename(self.pte_file)}",
78+
f'--input "{input_files_list}"',
79+
f"--output_path {self.output_folder}",
80+
]
81+
)
82+
enn_executor_runner_cmd = " ".join(
83+
[
84+
f"'cd {self.work_directory} &&",
85+
f"./enn_executor_runner {enn_executor_runner_args}'",
86+
]
87+
)
88+
89+
self._adb(["shell", f"{enn_executor_runner_cmd}"])
90+
91+
def pull(self, output_path):
92+
self._adb(["pull", "-a", self.output_folder, output_path])
93+
94+
@staticmethod
95+
def _is_adb_connected():
96+
try:
97+
output = subprocess.check_output(["adb", "devices"])
98+
devices = output.decode("utf-8").splitlines()[1:]
99+
return [device.split()[0] for device in devices if device.strip() != ""]
100+
except subprocess.CAlledProcessError:
101+
return False
102+
103+
104+
class RuntimeExecutor:
105+
def __init__(self, executorch_program, inputs):
106+
self.executorch_program = executorch_program
107+
self.inputs = inputs
108+
109+
def run_on_device(self) -> Tuple[torch.Tensor]:
110+
with tempfile.TemporaryDirectory() as tmp_dir:
111+
pte_filename, input_files = self._save_model_and_inputs(tmp_dir)
112+
test_manager = ADBTestManager(
113+
pte_file=os.path.join(tmp_dir, pte_filename),
114+
work_directory="/data/local/tmp/enn-executorch-test",
115+
input_files=input_files,
116+
)
117+
test_manager.push()
118+
test_manager.execute()
119+
host_output_save_dir = os.path.join(tmp_dir, "output")
120+
test_manager.pull(host_output_save_dir)
121+
122+
model_outputs = self._get_model_outputs()
123+
num_of_output_files = len(os.listdir(host_output_save_dir))
124+
assert num_of_output_files == len(
125+
model_outputs
126+
), f"Number of outputs is invalid, expect {len(model_outputs)} while got {num_of_output_files}"
127+
128+
result = []
129+
for idx in range(num_of_output_files):
130+
output_array = np.fromfile(
131+
os.path.join(host_output_save_dir, f"output_{idx}.bin"),
132+
dtype=np.uint8,
133+
)
134+
output_tensor = (
135+
torch.from_numpy(output_array)
136+
.view(dtype=model_outputs[idx].dtype)
137+
.view(*model_outputs[idx].shape)
138+
)
139+
result.append(output_tensor)
140+
141+
return tuple(result)
142+
143+
def _get_model_outputs(self):
144+
output_node = self.executorch_program.exported_program().graph.output_node()
145+
output_fake_tensors = []
146+
for ori_output in output_node.args[0]:
147+
output_fake_tensors.append(ori_output.meta["val"])
148+
149+
return tuple(output_fake_tensors)
150+
151+
def _save_model_and_inputs(self, save_dir):
152+
pte_file_name = "program.pte"
153+
file_path = os.path.join(save_dir, f"{pte_file_name}")
154+
with open(file_path, "wb") as file:
155+
self.executorch_program.write_to_file(file)
156+
157+
inputs_files = []
158+
for idx, input in enumerate(self.inputs):
159+
input_file_name = f"input_{idx}.bin"
160+
input.detach().numpy().tofile(os.path.join(save_dir, input_file_name))
161+
inputs_files.append(input_file_name)
162+
163+
return pte_file_name, inputs_files

0 commit comments

Comments
 (0)