|
20 | 20 | import os |
21 | 21 | import pathlib |
22 | 22 | from typing import Union |
| 23 | +import sys |
| 24 | +import tarfile |
| 25 | +import io |
23 | 26 | import numpy |
24 | 27 |
|
25 | 28 | import tvm |
|
43 | 46 |
|
44 | 47 | HEXAGON_TOOLCHAIN = os.environ.get("HEXAGON_TOOLCHAIN", default="") # pylint: disable=invalid-name |
45 | 48 | 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 |
46 | 52 | HEXAGON_LINK_MAIN = ( |
47 | 53 | pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-link" |
48 | 54 | ) # pylint: disable=invalid-name |
@@ -145,6 +151,74 @@ def to_str(s): |
145 | 151 | return 0 |
146 | 152 |
|
147 | 153 |
|
| 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 | + |
148 | 222 | def create_aot_shared(so_name: Union[str, pathlib.Path], files, hexagon_arch: str, options=None): |
149 | 223 | """Export Hexagon AOT module.""" |
150 | 224 | options = options or [] |
@@ -242,3 +316,127 @@ def allocate_hexagon_array( |
242 | 316 | arr.copyfrom(data.reshape(physical_shape)) |
243 | 317 |
|
244 | 318 | 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