562 lines
19 KiB
Python
562 lines
19 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# pylint: disable=invalid-name, f-string-without-interpolation, consider-using-from-import
|
|
# ruff: noqa: E501
|
|
"""Tools/compilers/linkers for Hexagon"""
|
|
|
|
import io
|
|
import itertools
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
|
|
import numpy
|
|
from tvm_ffi import register_global_func
|
|
|
|
import tvm
|
|
import tvm.support.cc as cc
|
|
|
|
# Linking Hexagon shared libraries.
|
|
#
|
|
# link_shared(name-of-shared-library, list-of-objects, kw-args)
|
|
#
|
|
# To use a custom linker, define a function that returns the path to the
|
|
# linker, and pass it to 'register_linker':
|
|
#
|
|
# def custom_linker_path():
|
|
# return '/path/to/hexagon/linker'
|
|
#
|
|
# register_linker(custom_linker_path)
|
|
#
|
|
# Subsequent calls to 'link_shared' will use the newly registered linker.
|
|
|
|
HEXAGON_TOOLCHAIN = os.environ.get("HEXAGON_TOOLCHAIN", default="") # pylint: disable=invalid-name
|
|
HEXAGON_SDK_ROOT = os.environ.get("HEXAGON_SDK_ROOT", default="") # pylint: disable=invalid-name
|
|
HEXAGON_SDK_DOCKER_IMAGE = os.environ.get("HEXAGON_SDK_DOCKER_IMAGE", default="") # pylint: disable=invalid-name
|
|
HEXAGON_LINK_MAIN = pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-link" # pylint: disable=invalid-name
|
|
HEXAGON_CLANG_PLUS = pathlib.Path(HEXAGON_TOOLCHAIN) / "bin" / "hexagon-clang++" # pylint: disable=invalid-name
|
|
HEXAGON_SDK_INCLUDE_DIRS = [ # pylint: disable=invalid-name
|
|
pathlib.Path(HEXAGON_SDK_ROOT) / "incs",
|
|
pathlib.Path(HEXAGON_SDK_ROOT) / "incs" / "stddef",
|
|
]
|
|
|
|
HEXAGON_SIMULATOR_NAME = "simulator"
|
|
|
|
|
|
def register_linker(f):
|
|
"""Register a function that will return the path to the Hexagon linker."""
|
|
return register_global_func("tvm.contrib.hexagon.hexagon_link", f, True)
|
|
|
|
|
|
@register_global_func("tvm.contrib.hexagon.hexagon_link")
|
|
def hexagon_link() -> str:
|
|
"""Return path to the Hexagon linker."""
|
|
return str(HEXAGON_LINK_MAIN)
|
|
|
|
|
|
def hexagon_clang_plus() -> str:
|
|
"""Return path to the Hexagon clang++."""
|
|
return str(HEXAGON_CLANG_PLUS)
|
|
|
|
|
|
def toolchain_version(toolchain=None) -> list[int]:
|
|
"""Return the version of the Hexagon toolchain.
|
|
|
|
Parameters
|
|
----------
|
|
toolchain: str, optional
|
|
Path to the Hexagon toolchain. If not provided, the environment
|
|
variable HEXAGON_TOOLCHAIN is used.
|
|
|
|
Returns
|
|
-------
|
|
version: List[int]
|
|
List of numerical components of the version number. E.g. for version
|
|
"8.5.06" it will be [8, 5, 6].
|
|
"""
|
|
|
|
if toolchain is None:
|
|
toolchain = HEXAGON_TOOLCHAIN
|
|
assert toolchain is not None, "Please specify toolchain, or set HEXAGON_TOOLCHAIN variable"
|
|
result = subprocess.run(
|
|
[f"{toolchain}/bin/hexagon-clang", "-v"], capture_output=True, check=True
|
|
)
|
|
output = result.stderr.decode()
|
|
for line in output.splitlines():
|
|
m = re.match(r".* [Cc]lang version ([0-9\.]+)", line)
|
|
if m:
|
|
assert len(m.groups()) == 1
|
|
return [int(v) for v in m.group(1).split(".")]
|
|
raise RuntimeError("Cannot establish toolchain version")
|
|
|
|
|
|
@register_global_func("tvm.contrib.hexagon.link_shared")
|
|
def link_shared(so_name, objs, extra_args=None):
|
|
"""Link shared library on Hexagon using the registered Hexagon linker.
|
|
|
|
Parameters
|
|
----------
|
|
so_name : str
|
|
Name of the shared library file.
|
|
objs : list[str, tvm.tirx.StringImm]
|
|
extra_args : dict (str->str) or Map<String,String>
|
|
Additional arguments:
|
|
'hex_arch' - Hexagon architecture, e.g. v68
|
|
'verbose' - Print additional information if the key is present
|
|
|
|
Returns
|
|
-------
|
|
ret_val : int
|
|
This function returns 0 at the moment.
|
|
"""
|
|
|
|
# The list of object files can be passed as built-in Python strings,
|
|
# or as tvm.tirx.StringImm's.
|
|
def to_str(s):
|
|
if isinstance(s, tvm.tirx.StringImm):
|
|
return s.value
|
|
assert isinstance(s, str), 'argument "' + str(s) + '" should be a string or StrImm'
|
|
return s
|
|
|
|
objs = [to_str(s) for s in objs]
|
|
|
|
if not extra_args:
|
|
extra_args = {}
|
|
hex_arch = extra_args.get("hex_arch") or "v68"
|
|
linker = tvm.get_global_func("tvm.contrib.hexagon.hexagon_link")()
|
|
if extra_args.get("verbose"):
|
|
print("tvm.contrib.hexagon.link_shared:")
|
|
print(" Using linker:", linker)
|
|
print(" Library name:", so_name)
|
|
print(" Object files:", objs)
|
|
print(" Architecture:", hex_arch)
|
|
if not os.access(linker, os.X_OK):
|
|
message = 'The linker "' + linker + '" does not exist or is not executable.'
|
|
if not os.environ.get("HEXAGON_TOOLCHAIN"):
|
|
message += (
|
|
" The environment variable HEXAGON_TOOLCHAIN is unset. Please export "
|
|
+ "HEXAGON_TOOLCHAIN in your environment, so that ${HEXAGON_TOOLCHAIN}/bin/"
|
|
+ "hexagon-link exists."
|
|
)
|
|
else:
|
|
message += (
|
|
" Please verify the value of the HEXAGON_LINKER environment variable "
|
|
+ '(currently set to "'
|
|
+ HEXAGON_TOOLCHAIN
|
|
+ '").'
|
|
)
|
|
raise Exception(message)
|
|
|
|
libpath = os.path.join(HEXAGON_TOOLCHAIN, "target", "hexagon", "lib", hex_arch, "G0")
|
|
cc.create_shared(
|
|
so_name,
|
|
objs,
|
|
# pylint: disable=bad-whitespace
|
|
options=[
|
|
"-Bdynamic",
|
|
"-shared",
|
|
"-export-dynamic",
|
|
os.path.join(libpath, "pic", "libgcc.so"),
|
|
],
|
|
cc=linker,
|
|
)
|
|
return 0
|
|
|
|
|
|
def link_shared_macos(so_name, objs, extra_args=None):
|
|
"""Link Hexagon shared library using docker container with proper tooling.
|
|
|
|
Parameters
|
|
----------
|
|
so_name : str
|
|
Name of the shared library file.
|
|
objs : list[str, tvm.tirx.StringImm]
|
|
extra_args : dict (str->str) or Map<String,String>
|
|
Additional arguments:
|
|
'hex_arch' - Hexagon architecture, e.g. v68
|
|
|
|
Returns
|
|
-------
|
|
ret_val : int
|
|
This function returns 0 at the moment.
|
|
"""
|
|
|
|
# The list of object files can be passed as built-in Python strings,
|
|
# or as tvm.tirx.StringImm's.
|
|
def to_str(s):
|
|
if isinstance(s, tvm.tirx.StringImm):
|
|
return s.value
|
|
assert isinstance(s, str), 'argument "' + str(s) + '" should be a string or StrImm'
|
|
return s
|
|
|
|
objs = [to_str(s) for s in objs]
|
|
|
|
if not extra_args:
|
|
extra_args = {}
|
|
hex_arch = extra_args.get("hex_arch") or "v68"
|
|
|
|
ses = ContainerSession(HEXAGON_SDK_DOCKER_IMAGE)
|
|
|
|
hexagon_sdk_tools_path = ses.get_env("HEXAGON_TOOLCHAIN")
|
|
libpath = os.path.join(hexagon_sdk_tools_path, "target", "hexagon", "lib", hex_arch, "G0")
|
|
linker = os.path.join(hexagon_sdk_tools_path, "bin", "hexagon-link")
|
|
|
|
# Copy input data to docker container
|
|
docker_objs = [ses.copy_to(obj) for obj in objs]
|
|
docker_so_name = ses.tmp_dir + "/" + os.path.basename(so_name)
|
|
|
|
link_cmd = [linker, "-shared", "-fPIC", "-o", docker_so_name]
|
|
link_cmd += docker_objs
|
|
link_cmd += [
|
|
"-Bdynamic",
|
|
"-export-dynamic",
|
|
"-L" + os.path.join(libpath, "pic"),
|
|
"-lgcc",
|
|
]
|
|
ses.exec(link_cmd)
|
|
|
|
# Copy result back to host
|
|
ses.copy_from(docker_so_name, so_name)
|
|
return 0
|
|
|
|
|
|
if sys.platform == "darwin":
|
|
|
|
def __create_shared_mac(so_name, objs, **kwargs):
|
|
return link_shared_macos(so_name, objs, kwargs)
|
|
|
|
create_shared = __create_shared_mac
|
|
register_global_func("tvm.contrib.hexagon.link_shared", f=link_shared_macos, override=True)
|
|
else: # Linux and Win32
|
|
create_shared = cc.create_shared
|
|
register_global_func("tvm.contrib.hexagon.link_shared", f=link_shared, override=True)
|
|
|
|
|
|
def create_aot_shared(so_name: str | pathlib.Path, files, hexagon_arch: str, options=None):
|
|
"""Export Hexagon AOT module."""
|
|
options = options or []
|
|
if not os.access(str(HEXAGON_CLANG_PLUS), os.X_OK):
|
|
raise Exception(
|
|
'The Clang++ "' + str(HEXAGON_CLANG_PLUS) + '" does not exist or is not executable.'
|
|
)
|
|
if not HEXAGON_TOOLCHAIN:
|
|
raise Exception(
|
|
" The environment variable HEXAGON_TOOLCHAIN is unset. Please export "
|
|
+ "HEXAGON_TOOLCHAIN in your environment."
|
|
)
|
|
if not HEXAGON_SDK_ROOT:
|
|
raise Exception(
|
|
" The environment variable HEXAGON_SDK_ROOT is unset. Please export "
|
|
+ "HEXAGON_SDK_ROOT in your environment."
|
|
)
|
|
|
|
# The AOT C codegen uses TVM runtime functions
|
|
# (e.g. TVMBackendAllocWorkspace) directly. On Hexagon these calls
|
|
# should be made using functions pointers provided as __TVM*
|
|
# variables in the provided context. This workaround allows the
|
|
# the TVM runtime symbols to be visible to the compiled shared
|
|
# library.
|
|
#
|
|
# This workaround can be removed when AOT codegen can be done with
|
|
# LLVM codegen.
|
|
workaround_link_flags = os.environ.get("HEXAGON_SHARED_LINK_FLAGS")
|
|
if workaround_link_flags:
|
|
options.extend(workaround_link_flags.split())
|
|
|
|
tvm_dir = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) / ".." / ".." / ".." / ".."
|
|
compute_arch = f"compute{hexagon_arch}"
|
|
compile_options = [
|
|
"-O3",
|
|
f"-I{tvm_dir / 'include'}",
|
|
f"-I{tvm_dir / '3rdparty' / 'dlpack' / 'include'}",
|
|
f"-I{pathlib.Path(HEXAGON_SDK_ROOT) / 'rtos' / 'qurt' / compute_arch / 'include' / 'posix'}",
|
|
f"-I{pathlib.Path(HEXAGON_SDK_ROOT) / 'rtos' / 'qurt' / compute_arch / 'include' / 'qurt'}",
|
|
"-D_MACH_I32=int",
|
|
]
|
|
|
|
# For debugging
|
|
for path in HEXAGON_SDK_INCLUDE_DIRS:
|
|
compile_options.append(f"-I{path!s}")
|
|
|
|
cross_compile = cc.cross_compiler(compile_func=hexagon_clang_plus())
|
|
cross_compile.output_format = "o"
|
|
c_files = [str(file) for file in files]
|
|
cross_compile(str(so_name), c_files, options=compile_options + options)
|
|
|
|
|
|
def pack_imports(
|
|
module: tvm.runtime.Module,
|
|
is_system_lib: bool, # pylint: disable=unused-argument
|
|
c_symbol_prefix: str,
|
|
workspace_dir: str,
|
|
):
|
|
"""Create an ELF object file that contains the binary data for the modules
|
|
imported in `module`. This is a callback function for use as `fpack_imports`
|
|
in `export_library`.
|
|
|
|
Parameters
|
|
----------
|
|
module: tvm.runtime.Module
|
|
Module whose imported modules need to be serialized.
|
|
is_system_lib: bool
|
|
Flag whether the exported module will be used as a system library.
|
|
c_symbol_prefix: str
|
|
Prefix to prepend to the blob symbol.
|
|
workspace_dir: str
|
|
Location for created files.
|
|
|
|
Returns
|
|
-------
|
|
file_name: str
|
|
The name of the created object file.
|
|
"""
|
|
|
|
path_bin = os.path.join(workspace_dir, "imports.bin")
|
|
pack_to_bin_f_name = "runtime.ModulePackImportsToTensor"
|
|
fpack_to_bin = tvm.get_global_func(pack_to_bin_f_name)
|
|
assert fpack_to_bin, f"Expecting {pack_to_bin_f_name} in registry"
|
|
|
|
fpack_to_bin(module).numpy().tofile(path_bin)
|
|
|
|
mblob_symbol = c_symbol_prefix + tvm.get_global_func("runtime.ModuleImportsBlobName")()
|
|
|
|
binary_size = os.path.getsize(path_bin)
|
|
hexagon_toolchain = os.environ.get("HEXAGON_TOOLCHAIN")
|
|
assert hexagon_toolchain, "Please set HEXAGON_TOOLCHAIN variable"
|
|
version = toolchain_version(hexagon_toolchain)
|
|
assert version[0] == 8 and version[1] >= 5, (
|
|
"Please use Hexagon toolchain version 8.5.x or later"
|
|
)
|
|
if version[1] <= 6:
|
|
path_o = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.o")
|
|
subprocess.run(
|
|
[
|
|
f"{hexagon_toolchain}/bin/hexagon-clang",
|
|
"-x",
|
|
"c",
|
|
"-c",
|
|
"/dev/null",
|
|
"-o",
|
|
path_o,
|
|
],
|
|
check=True,
|
|
)
|
|
subprocess.run(
|
|
[
|
|
f"{hexagon_toolchain}/bin/hexagon-llvm-objcopy",
|
|
path_o,
|
|
"--add-section",
|
|
f".rodata={path_bin}",
|
|
"--add-symbol",
|
|
f"{mblob_symbol}=.rodata:0,object",
|
|
],
|
|
check=True,
|
|
)
|
|
return path_o
|
|
|
|
else: # 8.6.07+
|
|
path_c = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.c")
|
|
path_o = os.path.join(workspace_dir, f"{c_symbol_prefix}devc.o")
|
|
with open(path_c, "w") as f:
|
|
f.write(
|
|
f"const unsigned char {mblob_symbol}[{binary_size}] "
|
|
f'__attribute__((section(".rodata"))) = {{0x1}};'
|
|
)
|
|
subprocess.run(
|
|
[f"{hexagon_toolchain}/bin/hexagon-clang", "-c", path_c, "-o", path_o], check=True
|
|
)
|
|
subprocess.run(
|
|
[
|
|
f"{hexagon_toolchain}/bin/hexagon-llvm-objcopy",
|
|
path_o,
|
|
"--update-section",
|
|
f".rodata={path_bin}",
|
|
],
|
|
check=True,
|
|
)
|
|
return path_o
|
|
|
|
|
|
def export_module(module, out_dir, binary_name="test_binary.so"):
|
|
"""Export Hexagon shared object to a file."""
|
|
binary_path = pathlib.Path(out_dir) / binary_name
|
|
module.write_to_file(str(binary_path))
|
|
return binary_path
|
|
|
|
|
|
def allocate_hexagon_array(
|
|
dev, tensor_shape=None, dtype=None, data=None, axis_separators=None, mem_scope=None
|
|
):
|
|
"""
|
|
Allocate a hexagon array which could be a 2D array
|
|
on physical memory defined by axis_separators
|
|
"""
|
|
if tensor_shape is None:
|
|
assert data is not None, "Must provide either tensor shape or numpy data array"
|
|
tensor_shape = data.shape
|
|
elif data is not None:
|
|
assert tensor_shape == data.shape, (
|
|
"Mismatch between provided tensor shape and numpy data array shape"
|
|
)
|
|
|
|
if dtype is None:
|
|
assert data is not None, "Must provide either dtype or numpy data array"
|
|
dtype = data.dtype.name
|
|
elif data is not None:
|
|
assert dtype == data.dtype, "Mismatch between provided dtype and numpy data array dtype"
|
|
|
|
if axis_separators is None:
|
|
axis_separators = []
|
|
|
|
boundaries = [0, *axis_separators, len(tensor_shape)]
|
|
physical_shape = [
|
|
numpy.prod(tensor_shape[dim_i:dim_f]) for dim_i, dim_f in itertools.pairwise(boundaries)
|
|
]
|
|
|
|
arr = tvm.runtime.empty(physical_shape, dtype=dtype, device=dev, mem_scope=mem_scope)
|
|
|
|
if data is not None:
|
|
arr.copyfrom(data.reshape(physical_shape))
|
|
|
|
return arr._create_view(tensor_shape)
|
|
|
|
|
|
class ContainerSession:
|
|
"""Docker container session
|
|
|
|
Parameters
|
|
----------
|
|
base_image_name : str
|
|
Docker image name to use. Empty string means to use default "tlcpack/ci-hexagon"
|
|
base image.
|
|
"""
|
|
|
|
def __init__(self, base_image_name: str = ""):
|
|
self._client = None
|
|
self._container = None
|
|
self.tmp_dir = None
|
|
|
|
self._client = ContainerSession._get_docker_client()
|
|
|
|
if base_image_name == "":
|
|
base_image_name = ContainerSession._get_latest_ci_image(self._client)
|
|
|
|
self._container = ContainerSession._find_container_or_create(self._client, base_image_name)
|
|
|
|
exit_code, tmp_dir_b = self._container.exec_run("mktemp -d -t tvm-toolbox-XXXXXXXXXX")
|
|
assert exit_code == 0
|
|
|
|
self.tmp_dir = tmp_dir_b.decode("utf-8").rstrip()
|
|
|
|
def __del__(self):
|
|
self.close()
|
|
|
|
@staticmethod
|
|
def _get_latest_ci_image(client) -> str:
|
|
ci_images = client.images.list(name="tlcpack/ci-hexagon")
|
|
ci_images.sort(reverse=True, key=lambda img: img.tags[0])
|
|
return ci_images[0].tags[0]
|
|
|
|
@staticmethod
|
|
def _get_docker_client():
|
|
try:
|
|
from docker import from_env # pylint: disable=import-outside-toplevel
|
|
from docker.errors import DockerException
|
|
except (ModuleNotFoundError, ImportError):
|
|
raise Exception("Docker SDK module is not installed. Please install it.")
|
|
|
|
try:
|
|
client = from_env()
|
|
except DockerException:
|
|
raise Exception(
|
|
"Docker server is not available. Please verify the docker is installed, "
|
|
"launched and available via command line ('dokcer ps' should works)."
|
|
)
|
|
|
|
return client
|
|
|
|
@staticmethod
|
|
def _find_container_or_create(client, image_name: str):
|
|
all_containers = client.containers.list(all=True)
|
|
|
|
filtered_containers = []
|
|
for container in all_containers:
|
|
tags: list = container.image.tags
|
|
img_name: str = tags[0]
|
|
if img_name.startswith(image_name) and container.name.startswith("tvm-hex-toolbox"):
|
|
filtered_containers.append(container)
|
|
|
|
if len(filtered_containers) == 0:
|
|
container = client.containers.run(
|
|
image=image_name, detach=True, tty=True, name="tvm-hex-toolbox"
|
|
)
|
|
else:
|
|
container = filtered_containers[0]
|
|
|
|
if container.status != "running":
|
|
container.start()
|
|
|
|
return container
|
|
|
|
def exec(self, cmd) -> str:
|
|
"""Execute command inside docker container"""
|
|
exit_code, res = self._container.exec_run(cmd)
|
|
assert exit_code == 0
|
|
return res.decode("utf-8")
|
|
|
|
def get_env(self, key: str) -> str:
|
|
"""Return env var value from docker container"""
|
|
res: str = self.exec(f"bash -c 'echo \"${key}\"'")
|
|
return res.rstrip(" \n")
|
|
|
|
def copy_to(self, host_file_path: str) -> str:
|
|
"""Upload file to docker container"""
|
|
file_name = os.path.basename(host_file_path)
|
|
|
|
byte_stream = io.BytesIO()
|
|
with tarfile.open(fileobj=byte_stream, mode="w:gz") as tar:
|
|
tar.add(host_file_path, arcname=file_name)
|
|
|
|
self._container.put_archive(path=self.tmp_dir, data=byte_stream.getvalue())
|
|
|
|
return f"{self.tmp_dir}/{file_name}"
|
|
|
|
def copy_from(self, container_file_path: str, host_file_path: str):
|
|
"""Download file from docker container"""
|
|
tar_bytes_gen, _ = self._container.get_archive(container_file_path)
|
|
|
|
# convert to bytes
|
|
tar_bytes = b""
|
|
for chunk in tar_bytes_gen:
|
|
tar_bytes += chunk
|
|
|
|
tar = tarfile.open(fileobj=io.BytesIO(initial_bytes=tar_bytes))
|
|
assert len(tar.getmembers()) == 1
|
|
tar_element_reader = tar.extractfile(tar.getmembers()[0])
|
|
with open(host_file_path, "wb") as host_file:
|
|
for chunk in tar_element_reader:
|
|
host_file.write(chunk)
|
|
|
|
def close(self):
|
|
"""Close docker container session"""
|
|
if self.tmp_dir is not None:
|
|
exit_code, _ = self._container.exec_run(f"rm -rf {self.tmp_dir}")
|
|
assert exit_code == 0
|