This is an automated email from the ASF dual-hosted git repository. kparzysz pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tvm.git
The following commit(s) were added to refs/heads/main by this push: new dc522a6ff6 [Hexagon] Run single RPC server on Android in each testing session (#11547) dc522a6ff6 is described below commit dc522a6ff65b68532cd1bba43827cd981114df2c Author: Mehrdad Hessar <mhes...@octoml.ai> AuthorDate: Fri Jun 10 14:33:24 2022 -0700 [Hexagon] Run single RPC server on Android in each testing session (#11547) * Reuse hexagon launcher in test session * separate random name generation * revert get_aot_executor * Fix launcher for simulator case * add stop server for simulator --- python/tvm/contrib/hexagon/build.py | 158 +++++++++++---------- python/tvm/contrib/hexagon/pytest_plugin.py | 66 +++++++-- python/tvm/contrib/hexagon/session.py | 90 +++++++----- tests/python/contrib/test_hexagon/test_launcher.py | 2 - 4 files changed, 195 insertions(+), 121 deletions(-) diff --git a/python/tvm/contrib/hexagon/build.py b/python/tvm/contrib/hexagon/build.py index c659d66bec..7e29f645ce 100644 --- a/python/tvm/contrib/hexagon/build.py +++ b/python/tvm/contrib/hexagon/build.py @@ -28,6 +28,7 @@ import stat import random import string import subprocess +import tempfile from typing import Union import tvm @@ -36,6 +37,7 @@ from .session import Session HEXAGON_RPC_LIB_DIR = os.environ.get("HEXAGON_RPC_LIB_DIR") +ANDROID_BASH_FILE_NAME = "android_bash.sh" def _get_hexagon_rpc_lib_dir() -> pathlib.Path: @@ -116,7 +118,6 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): self._rpc_info.update(rpc_info) self._workspace = self._create_workspace(workspace) self._device_key = self.HEXAGON_REMOTE_DEVICE_KEY - self._serial_number = None @abc.abstractmethod def start_server(self): @@ -128,6 +129,11 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): """Stop the RPC server""" ... + @abc.abstractmethod + def cleanup_directory(self): + """Cleanup working directory""" + ... + @abc.abstractmethod def _copy_to_remote( self, local_path: Union[str, pathlib.Path], remote_path: Union[str, pathlib.Path] @@ -144,13 +150,18 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): ... @abc.abstractmethod - def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]): + def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]) -> pathlib.Path: """Create a directory in the remote location. Parameters ---------- remote_path : str or pathlib.Path Name of the directory to be created. + + Returns + ------- + pathlib.Path : + Absolute path of the remote workspace. """ ... @@ -171,10 +182,9 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): if not workspace: base_dir = self._rpc_info["workspace_base"] workspace = os.path.join(base_dir, _get_test_directory_name()) - self._create_remote_directory(workspace) - return pathlib.Path(workspace) + return self._create_remote_directory(workspace) - def upload(self, local_path: Union[str, pathlib.Path], remote_filename: str): + def upload(self, local_path: Union[str, pathlib.Path], remote_filename: str) -> pathlib.Path: """Upload a local file to the remote workspace. Parameters @@ -183,9 +193,16 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): Path to the local file to be copied. remote_filename : str Name of the file in the remote workspace. + + Returns + ------- + pathlib.Path : + Uploaded file remote path. """ assert self._workspace - self._copy_to_remote(local_path, os.path.join(str(self._workspace), remote_filename)) + remote_file_path = self._workspace / remote_filename + self._copy_to_remote(local_path, str(remote_file_path)) + return remote_file_path def start_session(self, session_name: str = "hexagon-rpc") -> Session: """Connect to the RPC server. @@ -221,10 +238,7 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): session and loaded. If the object passed is a string or pathlib.Path, it must - be either a bare file name (without any path components), - or a full path in the remote system. If it is a file name, - the file must already have been uploaded to the remote, - and be placed in the remote workspace. + be a full path in the remote system. session : Session @@ -240,7 +254,10 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): return session.load_module(module) def get_graph_executor( - self, graph_json: str, module_name: Union[str, pathlib.Path], session: Session + self, + graph_json: str, + module: Union[str, pathlib.Path, tvm.runtime.Module], + session: Session, ): """Create a local GraphModule which consumes a remote libmod. @@ -248,8 +265,14 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): ---------- graph_json : str The string with the graph JSON. - module_name : str or pathlib.Path - Remote module filename. Same restrictions apply as in load_module(). + module : Union[str, pathlib.Path, tvm.runtime.Module] + + The module to load. If `module` is a + `tvm.runtime.Module`, it will be uploaded to the remote + session and loaded. + + If the object passed is a string or pathlib.Path, it must + be a full path in the remote system. session : Session Remote session. The session must be established (via __enter__) prior to calling this function. @@ -259,13 +282,12 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): GraphModule : Runtime graph module that can be used to execute the graph. """ - graph_mod = self.load_module(module_name, session) - return tvm.contrib.graph_executor.create(graph_json, graph_mod, session.device) + return session.get_graph_executor(graph_json, module) def get_graph_debug_executor( self, graph_json: str, - module_name: Union[str, pathlib.Path], + module: Union[str, pathlib.Path, tvm.runtime.Module], session: Session, dump_root: Union[str, pathlib.Path] = None, ): @@ -275,39 +297,24 @@ class HexagonLauncherRPC(metaclass=abc.ABCMeta): ---------- graph_json : str The string with the graph JSON. - module_name : str or pathlib.Path - Remote module filename. Same restrictions apply as in load_module(). - session : Session - Remote session. The session must be established (via __enter__) - prior to calling this function. - - Returns - ------- - GraphModuleDebug : - Runtime debug graph module that can be used to debug the graph. - """ - graph_mod = self.load_module(module_name, session) - return tvm.contrib.debugger.debug_executor.create( - graph_json, graph_mod, session.device, dump_root=str(dump_root) - ) + module : Union[str, pathlib.Path, tvm.runtime.Module] - def get_aot_executor(self, module_name: Union[str, pathlib.Path], session: Session): - """Create a local AoTModule which consumes a remote libmod. + The module to load. If `module` is a + `tvm.runtime.Module`, it will be uploaded to the remote + session and loaded. - Parameters - ---------- - module_name : str or pathlib.Path - Remote module filename. Same restrictions apply as in load_module(). + If the object passed is a string or pathlib.Path, it must + be a full path in the remote system. session : Session Remote session. The session must be established (via __enter__) prior to calling this function. Returns ------- - aot_module : AotModule - Runtime AOT module that can be used to execute. + GraphModuleDebug : + Runtime debug graph module that can be used to debug the graph. """ - return session.get_aot_executor(module_name) + return session.get_graph_debug_executor(graph_json, module, dump_root=dump_root) class HexagonLauncherAndroid(HexagonLauncherRPC): @@ -315,7 +322,6 @@ class HexagonLauncherAndroid(HexagonLauncherRPC): ANDROID_HEXAGON_TEST_BASE_DIR = pathlib.Path("/data/local/tmp/hexagon_test") ANDROID_HEXAGON_RPC_FILES = [ - "android_bash.sh", "libhexagon_rpc_skel.so", "libtvm_runtime.so", "tvm_rpc_android", @@ -354,39 +360,42 @@ class HexagonLauncherAndroid(HexagonLauncherRPC): self._adb_device_sub_cmd + ["push", str(local_path), str(remote_path)] ) - def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]): + def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]) -> pathlib.Path: """Abstract method implementation. See description in HexagonLauncherRPC.""" subprocess.check_call(self._adb_device_sub_cmd + ["shell", "mkdir", "-p", str(remote_path)]) + return pathlib.Path(remote_path) def _copy_binaries(self): """Upload Android server binaries.""" # Create bash script - android_bash_script_path = _get_hexagon_rpc_lib_dir() / "android_bash.sh" - with open(_get_hexagon_rpc_lib_dir() / "android_bash.sh.template", "r") as src_f: - if os.path.exists(android_bash_script_path): - os.remove(android_bash_script_path) - with open(android_bash_script_path, "w") as dest_f: - for line in src_f.readlines(): - if "<RPC_TRACKER_HOST>" in line: - line = line.replace( - "<RPC_TRACKER_HOST>", str(self._rpc_info["rpc_tracker_host"]) - ) - if "<RPC_TRACKER_PORT>" in line: - line = line.replace( - "<RPC_TRACKER_PORT>", str(self._rpc_info["rpc_tracker_port"]) - ) - if "<HEXAGON_REMOTE_DEVICE_KEY>" in line: - line = line.replace("<HEXAGON_REMOTE_DEVICE_KEY>", self._device_key) - if "<RPC_SERVER_PORT>" in line: - line = line.replace( - "<RPC_SERVER_PORT>", str(self._rpc_info["rpc_server_port"]) - ) - dest_f.write(line) - - # Make shell script executable - android_bash_stat = os.stat(android_bash_script_path) - os.chmod(android_bash_script_path, android_bash_stat.st_mode | stat.S_IEXEC) + with open(_get_hexagon_rpc_lib_dir() / f"{ANDROID_BASH_FILE_NAME}.template", "r") as src_f: + with tempfile.TemporaryDirectory() as temp_dir: + android_bash_script_path = pathlib.Path(temp_dir) / ANDROID_BASH_FILE_NAME + with open(android_bash_script_path, "w") as dest_f: + for line in src_f.readlines(): + if "<RPC_TRACKER_HOST>" in line: + line = line.replace( + "<RPC_TRACKER_HOST>", str(self._rpc_info["rpc_tracker_host"]) + ) + if "<RPC_TRACKER_PORT>" in line: + line = line.replace( + "<RPC_TRACKER_PORT>", str(self._rpc_info["rpc_tracker_port"]) + ) + if "<HEXAGON_REMOTE_DEVICE_KEY>" in line: + line = line.replace("<HEXAGON_REMOTE_DEVICE_KEY>", self._device_key) + if "<RPC_SERVER_PORT>" in line: + line = line.replace( + "<RPC_SERVER_PORT>", str(self._rpc_info["rpc_server_port"]) + ) + dest_f.write(line) + + # Make shell script executable + android_bash_stat = os.stat(android_bash_script_path) + os.chmod(android_bash_script_path, android_bash_stat.st_mode | stat.S_IEXEC) + self._copy_to_remote( + android_bash_script_path, self._workspace / android_bash_script_path.name + ) # Push files lib_dir = _get_hexagon_rpc_lib_dir() @@ -436,7 +445,8 @@ class HexagonLauncherAndroid(HexagonLauncherRPC): # Run server and connect to tracker subprocess.Popen( - self._adb_device_sub_cmd + ["shell", f"cd {self._workspace} && ./android_bash.sh"], + self._adb_device_sub_cmd + + ["shell", f"cd {self._workspace} && ./{ANDROID_BASH_FILE_NAME}"], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, @@ -472,8 +482,8 @@ class HexagonLauncherAndroid(HexagonLauncherRPC): self._adb_device_sub_cmd + ["shell", f"kill `cat {self._workspace}/rpc_pid.txt`"] ) - def _cleanup_directory(self): - # Remove workspace directory on remote target + def cleanup_directory(self): + """Abstract method implementation. See description in HexagonLauncherRPC.""" subprocess.Popen(self._adb_device_sub_cmd + ["shell", f"rm -rf {self._workspace}"]) def start_server(self): @@ -485,7 +495,7 @@ class HexagonLauncherAndroid(HexagonLauncherRPC): """Abstract method implementation. See description in HexagonLauncherRPC.""" self._cleanup_port_forwarding() self._terminate_remote() - self._cleanup_directory() + self.cleanup_directory() class HexagonLauncherSimulator(HexagonLauncherRPC): @@ -511,9 +521,10 @@ class HexagonLauncherSimulator(HexagonLauncherRPC): """Abstract method implementation. See description in HexagonLauncherRPC.""" subprocess.check_call(["cp", str(local_path), str(remote_path)]) - def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]): + def _create_remote_directory(self, remote_path: Union[str, pathlib.Path]) -> pathlib.Path: """Abstract method implementation. See description in HexagonLauncherRPC.""" subprocess.check_call(["mkdir", "-p", str(remote_path)]) + return pathlib.Path(os.path.abspath(remote_path)) def _copy_libcxx(self, dest_dir: Union[str, pathlib.Path]): """Copy libc++ libraries to the remote workspace.""" @@ -585,6 +596,9 @@ class HexagonLauncherSimulator(HexagonLauncherRPC): self._server_process = mp.Process(target=lambda *a: _start(self, *a)) self._server_process.start() + def cleanup_directory(self): + """Abstract method implementation. See description in HexagonLauncherRPC.""" + def stop_server(self): """Abstract method implementation. See description in HexagonLauncherRPC.""" self._server_process.terminate() diff --git a/python/tvm/contrib/hexagon/pytest_plugin.py b/python/tvm/contrib/hexagon/pytest_plugin.py index 278bd833da..1841c654b9 100644 --- a/python/tvm/contrib/hexagon/pytest_plugin.py +++ b/python/tvm/contrib/hexagon/pytest_plugin.py @@ -56,7 +56,7 @@ def _compose(args, decs): requires_hexagon_toolchain = tvm.testing.requires_hexagon(support_required="compile-only") -@tvm.testing.fixture +@pytest.fixture(scope="session") def android_serial_number() -> Optional[str]: serial = os.getenv(ANDROID_SERIAL_NUMBER, default="") # Setting ANDROID_SERIAL_NUMBER to an empty string should be @@ -138,22 +138,29 @@ def tvm_tracker_port(_tracker_info) -> int: return port -@tvm.testing.fixture +@pytest.fixture(scope="session") +def rpc_server_port_for_session() -> int: + return get_free_port() + + +@pytest.fixture() def rpc_server_port() -> int: return get_free_port() -@tvm.testing.fixture +@pytest.fixture(scope="session") def adb_server_socket() -> str: return os.getenv(ADB_SERVER_SOCKET, default="tcp:5037") -@tvm.testing.fixture -def hexagon_launcher( - request, android_serial_number, rpc_server_port, adb_server_socket +@pytest.fixture(scope="session") +def hexagon_server_process( + request, android_serial_number, rpc_server_port_for_session, adb_server_socket ) -> HexagonLauncherRPC: - """Initials and returns hexagon launcher if ANDROID_SERIAL_NUMBER is defined""" - if android_serial_number is None: + """Initials and returns hexagon launcher if ANDROID_SERIAL_NUMBER is defined. + This launcher is started only once per test session. + """ + if android_serial_number is None or android_serial_number == "simulator": yield None else: # Requesting these fixtures sets up a local tracker, if one @@ -165,19 +172,54 @@ def hexagon_launcher( rpc_info = { "rpc_tracker_host": tvm_tracker_host, "rpc_tracker_port": tvm_tracker_port, - "rpc_server_port": rpc_server_port, + "rpc_server_port": rpc_server_port_for_session, "adb_server_socket": adb_server_socket, } launcher = HexagonLauncher(serial_number=android_serial_number, rpc_info=rpc_info) - launcher.start_server() + try: + launcher.start_server() yield launcher finally: launcher.stop_server() -@tvm.testing.fixture -def hexagon_session(hexagon_launcher) -> Session: +@pytest.fixture +def hexagon_launcher( + hexagon_server_process, + rpc_server_port, + tvm_tracker_host, + tvm_tracker_port, + adb_server_socket, + android_serial_number, +) -> HexagonLauncherRPC: + """Initials and returns hexagon launcher which reuses RPC info and Android serial number.""" + if android_serial_number is None: + yield None + + if android_serial_number != "simulator": + rpc_info = hexagon_server_process._rpc_info + else: + rpc_info = { + "rpc_tracker_host": tvm_tracker_host, + "rpc_tracker_port": tvm_tracker_port, + "rpc_server_port": rpc_server_port, + "adb_server_socket": adb_server_socket, + } + + launcher = HexagonLauncher(serial_number=android_serial_number, rpc_info=rpc_info) + try: + if android_serial_number == "simulator": + launcher.start_server() + yield launcher + finally: + if android_serial_number == "simulator": + launcher.stop_server() + launcher.cleanup_directory() + + +@pytest.fixture +def hexagon_session(hexagon_launcher: HexagonLauncherRPC) -> Session: if hexagon_launcher is None: yield None else: diff --git a/python/tvm/contrib/hexagon/session.py b/python/tvm/contrib/hexagon/session.py index f30fe6e470..0c0bf296df 100644 --- a/python/tvm/contrib/hexagon/session.py +++ b/python/tvm/contrib/hexagon/session.py @@ -93,7 +93,8 @@ class Session: raise exception def __exit__(self, exc_type, exc_value, exc_traceback): - pass + # close session to the tracker + del self._rpc @property def device(self): @@ -109,7 +110,7 @@ class Session: return self._device - def upload(self, local_path: Union[str, pathlib.Path], remote_filename: str): + def upload(self, local_path: Union[str, pathlib.Path], remote_filename: str) -> pathlib.Path: """Upload a local file to the remote workspace. Parameters @@ -118,8 +119,13 @@ class Session: Path to the local file to be copied. remote_filename : str Name of the file in the remote workspace. + + Returns + ------- + pathlib.Path : + Uploaded file remote path. """ - self._launcher.upload(local_path, remote_filename) + return self._launcher.upload(local_path, remote_filename) def load_module(self, module: Union[str, pathlib.Path, tvm.runtime.Module]): """Load TVM module. @@ -136,10 +142,7 @@ class Session: session and loaded. If the object passed is a string or pathlib.Path, it must - be either a bare file name (without any path components), - or a full path in the remote system. If it is a file name, - the file must already have been uploaded to the remote, - and be placed in the remote workspace. + be a full path in the remote system. Returns ------- @@ -155,16 +158,19 @@ class Session: binary_name = "test_binary.so" binary_path = temp_dir / binary_name module.save(str(binary_path)) - self.upload(binary_path, binary_name) - module = binary_name + remote_file_path = self.upload(binary_path, binary_name) + else: + remote_file_path = module - assert isinstance(module, (str, pathlib.Path)), "Invalid path type:" + str(type(module)) - return self._rpc.get_function("tvm.hexagon.load_module")(str(module)) + assert isinstance(remote_file_path, (str, pathlib.Path)), "Invalid path type:" + str( + type(remote_file_path) + ) + return self._rpc.get_function("tvm.hexagon.load_module")(str(remote_file_path)) def get_graph_executor( self, graph_json: str, - module_name: Union[str, pathlib.Path], + module_name: Union[str, pathlib.Path, tvm.runtime.Module], ): """Create a local GraphModule which consumes a remote libmod. @@ -173,14 +179,10 @@ class Session: Parameters ---------- - - module_name : Union[str, pathlib.Path] - + module_name : Union[str, pathlib.Path, tvm.runtime.Module] The remote module filename, following the same restrictions as `load_module`. - graph_json : str - The string with the graph JSON. Returns @@ -196,31 +198,54 @@ class Session: def get_aot_executor( self, - module_name: Union[str, pathlib.Path], + module_file: Union[str, pathlib.Path], ): """Create a local GraphModule which consumes a remote libmod. - The session must be established (via __enter__) prior to calling this function. - Parameters ---------- + module_file : Union[str, pathlib.Path] + The remote module filename, following the same restrictions + as `load_module`. The filename should be an absolute path. + Returns + ------- + GraphModule : + Runtime graph module that can be used to execute the graph. + """ + aot_mod = self.load_module(module_file) + return tvm.runtime.executor.AotModule(aot_mod["default"](self.device)) - module_name : Union[str, pathlib.Path] + def get_graph_debug_executor( + self, + graph_json: str, + module_name: Union[str, pathlib.Path, tvm.runtime.Module], + dump_root: Union[str, pathlib.Path] = None, + ): + """Create a local GraphModuleDebug which consumes a remote libmod. + Parameters + ---------- + graph_json : str + The string with the graph JSON. + module_name : Union[str, pathlib.Path, tvm.runtime.Module] The remote module filename, following the same restrictions as `load_module`. + session : Session + Remote session. The session must be established (via __enter__) + prior to calling this function. Returns ------- - GraphModule : - Runtime graph module that can be used to execute the graph. - + GraphModuleDebug : + Runtime debug graph module that can be used to debug the graph. """ - aot_mod = self.load_module(module_name) - self._set_device_type(aot_mod) - return tvm.runtime.executor.AotModule(aot_mod["default"](self.device)) + graph_debug_mod = self.load_module(module_name) + self._set_device_type(graph_debug_mod) + return tvm.contrib.debugger.debug_executor.create( + graph_json, graph_debug_mod, self.device, dump_root=str(dump_root) + ) def get_executor_from_factory(self, module: ExecutorFactoryModule): """Create a local GraphModule which consumes a remote libmod. @@ -286,11 +311,7 @@ class Session: Runtime graph module that can be used to execute the graph. """ - - graph_json = module.get_graph_json() - graph_mod = self.load_module(module.get_lib()) - - return tvm.contrib.graph_executor.create(graph_json, graph_mod, self.device) + return self.get_graph_executor(module.get_graph_json(), module.get_lib()) def _aot_executor_from_factory( self, @@ -354,7 +375,6 @@ class Session: f"Target kind should be from these options: [hexagon, llvm]." ) - self.upload(binary_path, binary_name) + remote_file_path = self.upload(binary_path, binary_name) - aot_mod = self.load_module(binary_name) - return tvm.runtime.executor.AotModule(aot_mod["default"](self.device)) + return self.get_aot_executor(remote_file_path) diff --git a/tests/python/contrib/test_hexagon/test_launcher.py b/tests/python/contrib/test_hexagon/test_launcher.py index ad798925ee..aae2e598f6 100644 --- a/tests/python/contrib/test_hexagon/test_launcher.py +++ b/tests/python/contrib/test_hexagon/test_launcher.py @@ -15,8 +15,6 @@ # specific language governing permissions and limitations # under the License. -import sys -import pytest import numpy as np import tvm.testing