Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Hexagon] Run single RPC server on Android in each testing session #11547

Merged
merged 5 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 86 additions & 72 deletions python/tvm/contrib/hexagon/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import socket
import stat
import subprocess
import tempfile
from typing import Union

import tvm
Expand All @@ -34,6 +35,7 @@


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:
Expand Down Expand Up @@ -112,7 +114,6 @@ def __init__(self, rpc_info: dict, workspace: Union[str, pathlib.Path] = None):
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):
Expand All @@ -124,6 +125,11 @@ def stop_server(self):
"""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]
Expand All @@ -140,13 +146,18 @@ def _copy_to_remote(
...

@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.
"""
...

Expand All @@ -167,10 +178,9 @@ def _create_workspace(self, workspace: Union[str, pathlib.Path]) -> pathlib.Path
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
Expand All @@ -179,9 +189,16 @@ def upload(self, local_path: Union[str, pathlib.Path], remote_filename: str):
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.
Expand Down Expand Up @@ -217,10 +234,7 @@ def load_module(self, module: Union[str, pathlib.Path, tvm.runtime.Module], sess
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the symmetry of loading a file using the same path as returned from upload, but are there cases where we'd want to work within the existing workspace, without requiring the full path? The use cases coming to mind would be uploading a model once, then running several independent tests on it. Within a single process, the model to run should be passed to each independent target, so this change wouldn't affect it. If the upload happens in one process, then the independent tests happen in subsequent processes, it might cause an issue, but then the workspace itself would need to be passed through.

So I don't think I see any major concerns, but reducing previously functional behavior does give me pause.


session : Session

Expand All @@ -236,16 +250,25 @@ def load_module(self, module: Union[str, pathlib.Path, tvm.runtime.Module], sess
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.

Parameters
----------
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.
Expand All @@ -255,13 +278,12 @@ def get_graph_executor(
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,
):
Expand All @@ -271,47 +293,31 @@ def get_graph_debug_executor(
----------
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):
"""Hexagon Launcher for Android."""

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",
Expand Down Expand Up @@ -350,39 +356,42 @@ def _copy_to_remote(
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()
Expand Down Expand Up @@ -432,7 +441,8 @@ def _run_server_script(self):

# 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,
Expand Down Expand Up @@ -468,8 +478,8 @@ def _terminate_remote(self):
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):
Expand All @@ -481,7 +491,7 @@ def stop_server(self):
"""Abstract method implementation. See description in HexagonLauncherRPC."""
self._cleanup_port_forwarding()
self._terminate_remote()
self._cleanup_directory()
self.cleanup_directory()


class HexagonLauncherSimulator(HexagonLauncherRPC):
Expand All @@ -507,9 +517,10 @@ def _copy_to_remote(
"""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."""
Expand Down Expand Up @@ -581,6 +592,9 @@ def _start(self):
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()
Expand Down
Loading