Skip to content

Commit 46fb2ff

Browse files
authored
Hexagon compilation on MacOS system (#14308)
Short desc This changes allow my to compile and tune models for hexagon directly from my macOS laptop without full switching to linux environment. List of changes Replace local linker call with call from docker container with Hexagon SDK. Yes, that is the only SDK tool used by TVM during compilation. Enhanced search of ADB. Not only in PATH, but also in ANDROID_HOME, ANDROID_SDK_ROOT and default sdk installation directory. Mac OS doesn't allow to easily change default PATH env var for UI application launched from dock bar. So adb is not available for IDE by default. Motivation Some engineers would like to continue work with comfortable macOS environment even if they have to play with hexagon devices. At this moment there is no official Hexagon SDK for macOS system. Alternatives are next: fully switch to remote linux, use local linux virtual machine or try to introduce required hexagon SDK functionality for macOS. The last option is more preferable to me. Signed-off-by: Alexander Peskov <[email protected]>
1 parent 5abcf72 commit 46fb2ff

File tree

3 files changed

+263
-1
lines changed

3 files changed

+263
-1
lines changed

python/tvm/contrib/hexagon/build.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import random
3030
import string
3131
import subprocess
32+
import sys
3233
import tempfile
3334
from typing import Union
3435

@@ -89,6 +90,67 @@ def _get_test_directory_name() -> str:
8990
return f"{date_str}-{random_str}"
9091

9192

93+
def _get_adb_path() -> str:
94+
"""Define path to adb
95+
96+
Order of search:
97+
1. From PATH
98+
2. From ANDROID_SDK_ROOT
99+
3. From ANDROID_HOME
100+
3. From default android sdk installation directory (platform specific)
101+
"""
102+
103+
def check_execution(exe_path):
104+
try:
105+
ret_code = subprocess.call(
106+
[exe_path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
107+
)
108+
except FileNotFoundError:
109+
ret_code = -1
110+
111+
return ret_code == 0
112+
113+
# Check if adb available via PATH
114+
if check_execution("adb"):
115+
return "adb"
116+
117+
# Check if adb available via env vars or default directories
118+
list_of_paths = [
119+
os.environ.get("ANDROID_SDK_ROOT", default=""),
120+
os.environ.get("ANDROID_HOME", default=""),
121+
]
122+
123+
if sys.platform == "darwin":
124+
list_of_paths += [
125+
os.path.join(pathlib.Path.home(), "Library", "Android", "sdk", "platform-tools")
126+
]
127+
if sys.platform == "win32":
128+
list_of_paths += [
129+
os.path.join(
130+
pathlib.Path.home(), "AppData", "Local", "Android", "sdk", "platform-tools"
131+
)
132+
]
133+
if sys.platform == "linux":
134+
list_of_paths += [os.path.join(pathlib.Path.home(), "Android", "Sdk", "platform-tools")]
135+
136+
list_of_paths = [path for path in list_of_paths if path != ""]
137+
138+
found_path = None
139+
for candidate_path in list_of_paths:
140+
adb_path = os.path.join(candidate_path, "adb")
141+
if os.path.isfile(adb_path) and check_execution(adb_path):
142+
found_path = adb_path
143+
break
144+
145+
if found_path is None:
146+
raise RuntimeError(
147+
"ADB was not found. It should be available via PATH, ANDROID_SDK_ROOT "
148+
"or ANDROID_HOME env var."
149+
)
150+
151+
return found_path
152+
153+
92154
class HexagonLauncherRPC(metaclass=abc.ABCMeta):
93155
"""Base class for RPC-based launchers.
94156
@@ -301,7 +363,8 @@ def __init__(
301363
assert self._serial_number != "", "Android serial number is not set."
302364

303365
adb_socket = rpc_info["adb_server_socket"] if rpc_info["adb_server_socket"] else "tcp:5037"
304-
self._adb_device_sub_cmd = ["adb", "-L", adb_socket, "-s", self._serial_number]
366+
adb_exe = _get_adb_path()
367+
self._adb_device_sub_cmd = [adb_exe, "-L", adb_socket, "-s", self._serial_number]
305368
self.forwarded_ports_ = []
306369
self._hexagon_debug = hexagon_debug
307370
self._clear_logcat = clear_logcat

python/tvm/contrib/hexagon/session.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ def _aot_executor_from_factory(
403403
elif target_type == "llvm":
404404
module.export_library(
405405
str(binary_path),
406+
fcompile=hexagon.create_shared,
406407
cc=hexagon.hexagon_clang_plus(),
407408
)
408409
else:

python/tvm/contrib/hexagon/tools.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import os
2121
import pathlib
2222
from typing import Union
23+
import sys
24+
import tarfile
25+
import io
2326
import numpy
2427

2528
import tvm
@@ -43,6 +46,9 @@
4346

4447
HEXAGON_TOOLCHAIN = os.environ.get("HEXAGON_TOOLCHAIN", default="") # pylint: disable=invalid-name
4548
HEXAGON_SDK_ROOT = os.environ.get("HEXAGON_SDK_ROOT", default="") # pylint: disable=invalid-name
49+
HEXAGON_SDK_DOCKER_IMAGE = os.environ.get(
50+
"HEXAGON_SDK_DOCKER_IMAGE", default=""
51+
) # pylint: disable=invalid-name
4652
HEXAGON_LINK_MAIN = (
4753
pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-link"
4854
) # pylint: disable=invalid-name
@@ -145,6 +151,74 @@ def to_str(s):
145151
return 0
146152

147153

154+
def link_shared_macos(so_name, objs, extra_args=None):
155+
"""Link Hexagon shared library using docker container with proper tooling.
156+
157+
Parameters
158+
----------
159+
so_name : str
160+
Name of the shared library file.
161+
objs : list[str,StringImm]
162+
extra_args : dict (str->str) or Map<String,String>
163+
Additional arguments:
164+
'hex_arch' - Hexagon architecture, e.g. v66
165+
166+
Returns
167+
-------
168+
ret_val : int
169+
This function returns 0 at the moment.
170+
"""
171+
# The list of object files can be passed as built-in Python strings,
172+
# or as tvm.tir.StringImm's.
173+
def to_str(s):
174+
if isinstance(s, tvm.tir.StringImm):
175+
return s.value
176+
assert isinstance(s, str), 'argument "' + str(s) + '" should be a string or StrImm'
177+
return s
178+
179+
objs = [to_str(s) for s in objs]
180+
181+
if not extra_args:
182+
extra_args = {}
183+
hex_arch = extra_args.get("hex_arch") or "v66"
184+
185+
ses = ContainerSession(HEXAGON_SDK_DOCKER_IMAGE)
186+
187+
hexagon_sdk_tools_path = ses.get_env("HEXAGON_TOOLCHAIN")
188+
libpath = os.path.join(hexagon_sdk_tools_path, "target", "hexagon", "lib", hex_arch, "G0")
189+
linker = os.path.join(hexagon_sdk_tools_path, "bin", "hexagon-link")
190+
191+
# Copy input data to docker container
192+
docker_objs = [ses.copy_to(obj) for obj in objs]
193+
docker_so_name = ses.tmp_dir + "/" + os.path.basename(so_name)
194+
195+
link_cmd = [linker, "-shared", "-fPIC", "-o", docker_so_name]
196+
link_cmd += docker_objs
197+
link_cmd += [
198+
"-Bdynamic",
199+
"-export-dynamic",
200+
"-L" + os.path.join(libpath, "pic"),
201+
"-lgcc",
202+
]
203+
ses.exec(link_cmd)
204+
205+
# Copy result back to host
206+
ses.copy_from(docker_so_name, so_name)
207+
return 0
208+
209+
210+
if sys.platform == "darwin":
211+
212+
def __create_shared_mac(so_name, objs, **kwargs):
213+
return link_shared_macos(so_name, objs, kwargs)
214+
215+
create_shared = __create_shared_mac
216+
register_func("tvm.contrib.hexagon.link_shared", f=link_shared_macos, override=True)
217+
else: # Linux and Win32
218+
create_shared = cc.create_shared
219+
register_func("tvm.contrib.hexagon.link_shared", f=link_shared, override=True)
220+
221+
148222
def create_aot_shared(so_name: Union[str, pathlib.Path], files, hexagon_arch: str, options=None):
149223
"""Export Hexagon AOT module."""
150224
options = options or []
@@ -242,3 +316,127 @@ def allocate_hexagon_array(
242316
arr.copyfrom(data.reshape(physical_shape))
243317

244318
return arr._create_view(tensor_shape)
319+
320+
321+
class ContainerSession:
322+
"""Docker container session
323+
324+
Parameters
325+
----------
326+
base_image_name : str
327+
Docker image name to use. Empty string means to use default "tlcpack/ci-hexagon"
328+
base image.
329+
"""
330+
331+
def __init__(self, base_image_name: str = ""):
332+
self._client = None
333+
self._container = None
334+
self.tmp_dir = None
335+
336+
self._client = ContainerSession._get_docker_client()
337+
338+
if base_image_name == "":
339+
base_image_name = ContainerSession._get_latest_ci_image(self._client)
340+
341+
self._container = ContainerSession._find_container_or_create(self._client, base_image_name)
342+
343+
exit_code, tmp_dir_b = self._container.exec_run("mktemp -d -t tvm-toolbox-XXXXXXXXXX")
344+
assert exit_code == 0
345+
346+
self.tmp_dir = tmp_dir_b.decode("utf-8").rstrip()
347+
348+
def __del__(self):
349+
self.close()
350+
351+
@staticmethod
352+
def _get_latest_ci_image(client) -> str:
353+
ci_images = client.images.list(name="tlcpack/ci-hexagon")
354+
ci_images.sort(reverse=True, key=lambda img: img.tags[0])
355+
return ci_images[0].tags[0]
356+
357+
@staticmethod
358+
def _get_docker_client():
359+
try:
360+
# pylint: disable=import-outside-toplevel
361+
from docker import from_env
362+
from docker.errors import DockerException
363+
except (ModuleNotFoundError, ImportError):
364+
raise Exception("Docker SDK module is not installed. Please install it.")
365+
366+
try:
367+
client = from_env()
368+
except DockerException:
369+
raise Exception(
370+
"Docker server is not available. Please verify the docker is installed, "
371+
"launched and available via command line ('dokcer ps' should works)."
372+
)
373+
374+
return client
375+
376+
@staticmethod
377+
def _find_container_or_create(client, image_name: str):
378+
all_containers = client.containers.list(all=True)
379+
380+
filtered_containers = []
381+
for container in all_containers:
382+
tags: list = container.image.tags
383+
img_name: str = tags[0]
384+
if img_name.startswith(image_name) and container.name.startswith("tvm-hex-toolbox"):
385+
filtered_containers.append(container)
386+
387+
if len(filtered_containers) == 0:
388+
container = client.containers.run(
389+
image=image_name, detach=True, tty=True, name="tvm-hex-toolbox"
390+
)
391+
else:
392+
container = filtered_containers[0]
393+
394+
if container.status != "running":
395+
container.start()
396+
397+
return container
398+
399+
def exec(self, cmd) -> str:
400+
"""Execute command inside docker container"""
401+
exit_code, res = self._container.exec_run(cmd)
402+
assert exit_code == 0
403+
return res.decode("utf-8")
404+
405+
def get_env(self, key: str) -> str:
406+
"""Return env var value from docker container"""
407+
res: str = self.exec(f"bash -c 'echo \"${key}\"'")
408+
return res.rstrip(" \n")
409+
410+
def copy_to(self, host_file_path: str) -> str:
411+
"""Upload file to docker container"""
412+
file_name = os.path.basename(host_file_path)
413+
414+
byte_stream = io.BytesIO()
415+
with tarfile.open(fileobj=byte_stream, mode="w:gz") as tar:
416+
tar.add(host_file_path, arcname=file_name)
417+
418+
self._container.put_archive(path=self.tmp_dir, data=byte_stream.getvalue())
419+
420+
return f"{self.tmp_dir}/{file_name}"
421+
422+
def copy_from(self, container_file_path: str, host_file_path: str):
423+
"""Download file from docker container"""
424+
tar_bytes_gen, _ = self._container.get_archive(container_file_path)
425+
426+
# convert to bytes
427+
tar_bytes = bytes()
428+
for chunk in tar_bytes_gen:
429+
tar_bytes += chunk
430+
431+
tar = tarfile.open(fileobj=io.BytesIO(initial_bytes=tar_bytes))
432+
assert len(tar.getmembers()) == 1
433+
tar_element_reader = tar.extractfile(tar.getmembers()[0])
434+
with open(host_file_path, "wb") as host_file:
435+
for chunk in tar_element_reader:
436+
host_file.write(chunk)
437+
438+
def close(self):
439+
"""Close docker container session"""
440+
if self.tmp_dir is not None:
441+
exit_code, _ = self._container.exec_run(f"rm -rf {self.tmp_dir}")
442+
assert exit_code == 0

0 commit comments

Comments
 (0)