Skip to content

Commit b74fd34

Browse files
committed
[HEX] Add docker wrapper for linker call
1 parent 1dde062 commit b74fd34

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

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)