chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# 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.
|
||||
"""tvm.support — Python helpers that integrate TVM with external CLIs
|
||||
and host-side tools (compilers, archivers, subprocess pools, build-info
|
||||
queries). Distinct from `tvm.contrib`, which is reserved for optional
|
||||
vendor SDK integrations and experimental features."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
import tvm
|
||||
|
||||
tvm_ffi.init_ffi_api("support", __name__)
|
||||
|
||||
from .libinfo import libinfo
|
||||
|
||||
|
||||
def describe():
|
||||
"""
|
||||
Print out information about TVM and the current Python environment
|
||||
"""
|
||||
info = list((k, v) for k, v in libinfo().items())
|
||||
info = dict(sorted(info, key=lambda x: x[0]))
|
||||
print("Python Environment")
|
||||
sys_version = sys.version.replace("\n", " ")
|
||||
uname = platform.uname()
|
||||
uname = f"{uname.system} {uname.release} {uname.version} {uname.machine}"
|
||||
lines = [
|
||||
f"TVM version = {tvm.__version__}",
|
||||
f"Python version = {sys_version} ({sys.maxsize.bit_length() + 1} bit)",
|
||||
f"uname = {uname}",
|
||||
]
|
||||
print(textwrap.indent("\n".join(lines), prefix=" "))
|
||||
print("CMake Options:")
|
||||
print(textwrap.indent(json.dumps(info, indent=2), prefix=" "))
|
||||
@@ -0,0 +1,418 @@
|
||||
# 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.
|
||||
"""Util to invoke C/C++ compilers in the system."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
import sys
|
||||
|
||||
from . import tar as _tar
|
||||
from . import utils as _utils
|
||||
|
||||
|
||||
def _is_linux_like():
|
||||
return (
|
||||
sys.platform == "darwin"
|
||||
or sys.platform.startswith("linux")
|
||||
or sys.platform.startswith("freebsd")
|
||||
)
|
||||
|
||||
|
||||
def _is_windows_like():
|
||||
return sys.platform == "win32"
|
||||
|
||||
|
||||
def get_cc():
|
||||
"""Return the path to the default C/C++ compiler.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: Optional[str]
|
||||
The path to the default C/C++ compiler, or None if none was found.
|
||||
"""
|
||||
|
||||
if not _is_linux_like():
|
||||
return None
|
||||
|
||||
env_cxx = os.environ.get("CXX") or os.environ.get("CC")
|
||||
if env_cxx:
|
||||
return env_cxx
|
||||
cc_names = ["g++", "gcc", "clang++", "clang", "c++", "cc"]
|
||||
dirs_in_path = os.get_exec_path()
|
||||
for cc in cc_names:
|
||||
for d in dirs_in_path:
|
||||
cc_path = os.path.join(d, cc)
|
||||
if os.path.isfile(cc_path) and os.access(cc_path, os.X_OK):
|
||||
return cc_path
|
||||
return None
|
||||
|
||||
|
||||
def create_shared(output, objects, options=None, cc=None, cwd=None, ccache_env=None):
|
||||
"""Create shared library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
objects : List[str]
|
||||
List of object files.
|
||||
|
||||
options : List[str]
|
||||
The list of additional options string.
|
||||
|
||||
cc : Optional[str]
|
||||
The compiler command.
|
||||
|
||||
cwd : Optional[str]
|
||||
The current working directory.
|
||||
|
||||
ccache_env : Optional[Dict[str, str]]
|
||||
The environment variable for ccache. Set `None` to disable ccache by default.
|
||||
"""
|
||||
cc = cc or get_cc()
|
||||
|
||||
if _is_linux_like():
|
||||
_linux_compile(output, objects, options, cc, cwd, ccache_env, compile_shared=True)
|
||||
elif _is_windows_like():
|
||||
_windows_compile(output, objects, options, cwd, ccache_env)
|
||||
else:
|
||||
raise ValueError("Unsupported platform")
|
||||
|
||||
|
||||
def _linux_ar(output, inputs, ar):
|
||||
ar = ar or "ar"
|
||||
|
||||
libname = os.path.basename(output)
|
||||
if not libname.startswith("lib"):
|
||||
libname = "lib" + libname
|
||||
temp = _utils.tempdir()
|
||||
temp_output = temp.relpath(libname)
|
||||
cmd = [ar, "-crs", temp_output]
|
||||
|
||||
# handles the case where some input files are tar of objects
|
||||
# unpack them and return the list of files inside
|
||||
objects = _tar.normalize_file_list_by_unpacking_tars(temp, inputs)
|
||||
|
||||
cmd += objects
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "AR error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
msg += "\nCommand line: " + " ".join(cmd)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
shutil.move(temp_output, output)
|
||||
|
||||
|
||||
def create_staticlib(output, inputs, ar=None):
|
||||
"""Create static library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
inputs : List[str]
|
||||
List of inputs files. Each input file can be a tarball
|
||||
of objects or an object file.
|
||||
|
||||
ar : Optional[str]
|
||||
Path to the ar command to be used
|
||||
"""
|
||||
|
||||
if _is_linux_like():
|
||||
return _linux_ar(output, inputs, ar)
|
||||
else:
|
||||
raise ValueError("Unsupported platform")
|
||||
|
||||
|
||||
def create_executable(output, objects, options=None, cc=None, cwd=None, ccache_env=None):
|
||||
"""Create executable binary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target executable.
|
||||
|
||||
objects : List[str]
|
||||
List of object files.
|
||||
|
||||
options : List[str]
|
||||
The list of additional options string.
|
||||
|
||||
cc : Optional[str]
|
||||
The compiler command.
|
||||
|
||||
cwd : Optional[str]
|
||||
The urrent working directory.
|
||||
|
||||
ccache_env : Optional[Dict[str, str]]
|
||||
The environment variable for ccache. Set `None` to disable ccache by default.
|
||||
"""
|
||||
cc = cc or get_cc()
|
||||
|
||||
if _is_linux_like():
|
||||
_linux_compile(output, objects, options, cc, cwd, ccache_env)
|
||||
elif _is_windows_like():
|
||||
_windows_compile(output, objects, options, cwd, ccache_env)
|
||||
else:
|
||||
raise ValueError("Unsupported platform")
|
||||
|
||||
|
||||
def get_global_symbol_section_map(path, *, nm=None) -> dict[str, str]:
|
||||
"""Get global symbols from a library via nm -g
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The library path
|
||||
|
||||
nm: str
|
||||
The path to nm command
|
||||
|
||||
Returns
|
||||
-------
|
||||
symbol_section_map: Dict[str, str]
|
||||
A map from defined global symbol to their sections
|
||||
"""
|
||||
if nm is None:
|
||||
if not _is_linux_like():
|
||||
raise ValueError("Unsupported platform")
|
||||
nm = "nm"
|
||||
|
||||
symbol_section_map = {}
|
||||
|
||||
if not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"{path} does not exist")
|
||||
|
||||
cmd = [nm, "-gU", path]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Runtime error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
for line in out.decode("utf-8", errors="replace").split("\n"):
|
||||
data = line.strip().split()
|
||||
if len(data) != 3:
|
||||
continue
|
||||
symbol = data[-1]
|
||||
section = data[-2]
|
||||
symbol_section_map[symbol] = section
|
||||
return symbol_section_map
|
||||
|
||||
|
||||
def get_target_by_dump_machine(compiler):
|
||||
"""Functor of get_target_triple that can get the target triple using compiler.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
compiler : Optional[str]
|
||||
The compiler.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out: Callable
|
||||
A function that can get target triple according to dumpmachine option of compiler.
|
||||
"""
|
||||
|
||||
def get_target_triple():
|
||||
"""Get target triple according to dumpmachine option of compiler."""
|
||||
if compiler:
|
||||
cmd = [compiler, "-dumpmachine"]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "dumpmachine error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
return None
|
||||
return out.decode("utf-8", errors="replace")
|
||||
return None
|
||||
|
||||
return get_target_triple
|
||||
|
||||
|
||||
# assign so as default output format
|
||||
create_shared.output_format = "so" if sys.platform != "win32" else "dll"
|
||||
create_shared.get_target_triple = get_target_by_dump_machine(os.environ.get("CXX", get_cc()))
|
||||
|
||||
|
||||
def cross_compiler(
|
||||
compile_func, options=None, output_format=None, get_target_triple=None, add_files=None
|
||||
):
|
||||
"""Create a cross compiler function by specializing compile_func with options.
|
||||
|
||||
This function can be used to construct compile functions that
|
||||
can be passed to AutoTVM measure or export_library.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
compile_func : Union[str, Callable[[str, str, Optional[str]], None]]
|
||||
Function that performs the actual compilation
|
||||
|
||||
options : Optional[List[str]]
|
||||
List of additional optional string.
|
||||
|
||||
output_format : Optional[str]
|
||||
Library output format.
|
||||
|
||||
get_target_triple: Optional[Callable]
|
||||
Function that can target triple according to dumpmachine option of compiler.
|
||||
|
||||
add_files: Optional[List[str]]
|
||||
List of paths to additional object, source, library files
|
||||
to pass as part of the compilation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
fcompile : Callable[[str, str, Optional[str]], None]
|
||||
A compilation function that can be passed to export_library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code-block:: python
|
||||
|
||||
from tvm.support import cc, ndk
|
||||
# export using arm gcc
|
||||
mod = build_runtime_module()
|
||||
mod.export_library(path_dso,
|
||||
fcompile=cc.cross_compiler("arm-linux-gnueabihf-gcc"))
|
||||
# specialize ndk compilation options.
|
||||
specialized_ndk = cc.cross_compiler(
|
||||
ndk.create_shared,
|
||||
["--sysroot=/path/to/sysroot", "-shared", "-fPIC", "-lm"])
|
||||
mod.export_library(path_dso, fcompile=specialized_ndk)
|
||||
"""
|
||||
base_options = [] if options is None else options
|
||||
kwargs = {}
|
||||
add_files = [] if add_files is None else add_files
|
||||
|
||||
# handle case where compile_func is the name of the cc
|
||||
if isinstance(compile_func, str):
|
||||
kwargs = {"cc": compile_func}
|
||||
compile_func = create_shared
|
||||
|
||||
def _fcompile(outputs, objects, options=None):
|
||||
all_options = base_options
|
||||
if options is not None:
|
||||
all_options += options
|
||||
compile_func(outputs, objects + add_files, options=all_options, **kwargs)
|
||||
|
||||
if not output_format and hasattr(compile_func, "output_format"):
|
||||
output_format = compile_func.output_format
|
||||
output_format = output_format if output_format else "so"
|
||||
|
||||
if not get_target_triple and hasattr(compile_func, "get_target_triple"):
|
||||
get_target_triple = compile_func.get_target_triple
|
||||
|
||||
_fcompile.output_format = output_format
|
||||
_fcompile.get_target_triple = get_target_triple
|
||||
return _fcompile
|
||||
|
||||
|
||||
def _linux_compile(
|
||||
output, objects, options, compile_cmd, cwd=None, ccache_env=None, compile_shared=False
|
||||
):
|
||||
cmd = [compile_cmd]
|
||||
if compile_cmd != "nvcc":
|
||||
if compile_shared or output.endswith(".so") or output.endswith(".dylib"):
|
||||
cmd += ["-shared", "-fPIC"]
|
||||
if sys.platform == "darwin":
|
||||
cmd += ["-undefined", "dynamic_lookup"]
|
||||
elif output.endswith(".obj"):
|
||||
cmd += ["-c"]
|
||||
else:
|
||||
if compile_shared or output.endswith(".so") or output.endswith(".dylib"):
|
||||
cmd += ["-shared"]
|
||||
cmd += ["-o", output]
|
||||
if options:
|
||||
cmd += options
|
||||
if isinstance(objects, str):
|
||||
cmd += [objects]
|
||||
else:
|
||||
cmd += objects
|
||||
env = None
|
||||
if ccache_env is not None:
|
||||
if shutil.which("ccache"):
|
||||
cmd.insert(0, "ccache")
|
||||
env = os.environ.copy()
|
||||
env.update(ccache_env)
|
||||
else:
|
||||
raise ValueError("ccache not found")
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=env)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
msg += "\nCommand line: " + " ".join(cmd)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def _windows_compile(output, objects, options, cwd=None, ccache_env=None):
|
||||
compiler = os.getenv("TVM_WIN_CC", default="clang")
|
||||
win_target = os.getenv("TVM_WIN_TARGET", default="x86_64")
|
||||
cmd = [compiler]
|
||||
cmd += ["-O2"]
|
||||
cmd += ["--target=" + win_target]
|
||||
|
||||
if output.endswith(".so") or output.endswith(".dll"):
|
||||
cmd += ["-shared"]
|
||||
elif output.endswith(".obj"):
|
||||
cmd += ["-c"]
|
||||
|
||||
if isinstance(objects, str):
|
||||
objects = [objects]
|
||||
cmd += ["-o", output]
|
||||
cmd += objects
|
||||
if options:
|
||||
cmd += options
|
||||
env = None
|
||||
if ccache_env is not None:
|
||||
if shutil.which("ccache"):
|
||||
cmd.insert(0, "ccache")
|
||||
env = os.environ.copy()
|
||||
env.update(ccache_env)
|
||||
else:
|
||||
raise ValueError("ccache not found")
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=env
|
||||
)
|
||||
(out, _) = proc.communicate()
|
||||
except FileNotFoundError:
|
||||
raise RuntimeError(
|
||||
"Can not find the LLVM clang for Windows clang.exe)."
|
||||
"Make sure it's installed"
|
||||
" and the installation directory is in the %PATH% environment "
|
||||
"variable. Prebuilt binaries can be found at: https://llvm.org/"
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += " ".join(cmd) + "\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
|
||||
raise RuntimeError(msg)
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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.
|
||||
"""Util to invoke clang in the system."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
import subprocess
|
||||
|
||||
import tvm.target
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def find_clang(required=True):
|
||||
"""Find clang in system.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
required : bool
|
||||
Whether it is required,
|
||||
runtime error will be raised if the compiler is required.
|
||||
|
||||
Returns
|
||||
-------
|
||||
valid_list : list of str
|
||||
List of possible paths.
|
||||
|
||||
Note
|
||||
----
|
||||
This function will first search clang that
|
||||
matches the major llvm version that built with tvm
|
||||
"""
|
||||
cc_list = []
|
||||
major = tvm.target.codegen.llvm_version_major(allow_none=True)
|
||||
if major is not None:
|
||||
cc_list += [f"clang-{major}.0"]
|
||||
cc_list += [f"clang-{major}"]
|
||||
cc_list += ["clang"]
|
||||
cc_list += ["clang.exe"]
|
||||
valid_list = [utils.which(x) for x in cc_list]
|
||||
valid_list = [x for x in valid_list if x]
|
||||
if not valid_list and required:
|
||||
raise RuntimeError("cannot find clang, candidates are: " + str(cc_list))
|
||||
return valid_list
|
||||
|
||||
|
||||
def create_llvm(inputs, output=None, options=None, cc=None):
|
||||
"""Create llvm text ir.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
inputs : list of str
|
||||
List of input files name or code source.
|
||||
|
||||
output : str, optional
|
||||
Output file, if it is none
|
||||
a temporary file is created
|
||||
|
||||
options : list
|
||||
The list of additional options string.
|
||||
|
||||
cc : str, optional
|
||||
The clang compiler, if not specified,
|
||||
we will try to guess the matched clang version.
|
||||
|
||||
Returns
|
||||
-------
|
||||
code : str
|
||||
The generated llvm text IR.
|
||||
"""
|
||||
cc = cc if cc else find_clang()[0]
|
||||
cmd = [cc]
|
||||
cmd += ["-S", "-emit-llvm"]
|
||||
temp = utils.tempdir()
|
||||
output = output if output else temp.relpath("output.ll")
|
||||
inputs = [inputs] if isinstance(inputs, str) else inputs
|
||||
input_files = []
|
||||
for i, code in enumerate(inputs):
|
||||
if utils.is_source_path(code):
|
||||
input_files.append(code)
|
||||
else:
|
||||
temp_path = temp.relpath(f"input{i}.cc")
|
||||
with open(temp_path, "w") as output_file:
|
||||
output_file.write(code)
|
||||
input_files.append(temp_path)
|
||||
if options:
|
||||
cmd += options
|
||||
cmd += ["-o", output]
|
||||
cmd += input_files
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return open(output).read()
|
||||
@@ -0,0 +1,145 @@
|
||||
# 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.
|
||||
"""Util to invoke emscripten compilers in the system."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from tvm import libinfo
|
||||
|
||||
|
||||
def find_wasm_lib(name, optional=False):
|
||||
"""Find a wasm/web asset (e.g. a ``.bc`` or ``.js`` file) under ``web/dist``.
|
||||
|
||||
Specializes the asset search to the wasm build outputs that live under
|
||||
``web/dist/wasm`` and ``web/dist``, anchored on ``TVM_HOME`` (when set) or
|
||||
the TVM source root otherwise.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The asset file name to look for.
|
||||
|
||||
optional : bool
|
||||
When True, return None instead of raising if the asset is not found.
|
||||
|
||||
Returns
|
||||
-------
|
||||
found : list(str) or None
|
||||
List of matching paths, or None when ``optional`` and nothing is found.
|
||||
"""
|
||||
# use extra TVM_HOME environment for finding libraries.
|
||||
if os.environ.get("TVM_HOME", None):
|
||||
tvm_source_home_dir = os.environ["TVM_HOME"]
|
||||
else:
|
||||
tvm_source_home_dir = os.fspath(libinfo._dev_top_directory())
|
||||
|
||||
dll_path = [
|
||||
os.path.join(tvm_source_home_dir, "web", "dist", "wasm"),
|
||||
os.path.join(tvm_source_home_dir, "web", "dist"),
|
||||
]
|
||||
found = [os.path.join(p, name) for p in dll_path if os.path.isfile(os.path.join(p, name))]
|
||||
if not found:
|
||||
if optional:
|
||||
return None
|
||||
raise RuntimeError(f"Cannot find {name}; searched {dll_path}")
|
||||
return found
|
||||
|
||||
|
||||
def create_tvmjs_wasm(output, objects, options=None, cc="emcc", libs=None):
|
||||
"""Create wasm that is supposed to run with the tvmjs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
objects : list
|
||||
List of object files.
|
||||
|
||||
options : str
|
||||
The additional options.
|
||||
|
||||
cc : str, optional
|
||||
The compile string.
|
||||
|
||||
libs : list
|
||||
List of user-defined library files (e.g. .bc files) to add into the wasm.
|
||||
"""
|
||||
cmd = [cc]
|
||||
cmd += ["-O3"]
|
||||
cmd += ["-std=c++17"]
|
||||
cmd += ["--no-entry"]
|
||||
# NOTE: asynctify conflicts with wasm-exception
|
||||
# so we temp disable exception handling for now
|
||||
#
|
||||
# We also expect user to explicitly pass in
|
||||
# -s ASYNCIFY=1 as it can increase wasm size by 2xq
|
||||
#
|
||||
# cmd += ["-s", "ASYNCIFY=1"]
|
||||
# cmd += ["-fwasm-exceptions"]
|
||||
cmd += ["-s", "WASM_BIGINT=1"]
|
||||
cmd += ["-s", "ERROR_ON_UNDEFINED_SYMBOLS=0"]
|
||||
cmd += ["-s", "STANDALONE_WASM=1"]
|
||||
cmd += ["-s", "ALLOW_MEMORY_GROWTH=1"]
|
||||
cmd += ["-s", "TOTAL_MEMORY=160MB"]
|
||||
|
||||
objects = [objects] if isinstance(objects, str) else objects
|
||||
|
||||
with_runtime = False
|
||||
for obj in objects:
|
||||
if obj.find("wasm_runtime.bc") != -1:
|
||||
with_runtime = True
|
||||
|
||||
all_libs = []
|
||||
if not with_runtime:
|
||||
all_libs += [find_wasm_lib("wasm_runtime.bc")[0]]
|
||||
|
||||
all_libs += [find_wasm_lib("tvmjs_support.bc")[0]]
|
||||
all_libs += [find_wasm_lib("webgpu_runtime.bc")[0]]
|
||||
|
||||
if libs:
|
||||
if not isinstance(libs, list):
|
||||
raise ValueError("Expect `libs` to be a list of paths in string.")
|
||||
for lib in libs:
|
||||
if not Path(lib).exists():
|
||||
raise RuntimeError(
|
||||
"Cannot find file from libs:" + lib + "\n Try pass in an absolute path."
|
||||
)
|
||||
all_libs += libs
|
||||
|
||||
cmd += ["-o", output]
|
||||
|
||||
# let libraries go before normal object
|
||||
cmd += all_libs + objects
|
||||
|
||||
if options:
|
||||
cmd += options
|
||||
|
||||
is_windows = os.name == "nt"
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=is_windows)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
create_tvmjs_wasm.object_format = "bc"
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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.
|
||||
"""Build-info query helpers for tvm.support."""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def libinfo():
|
||||
"""Returns a dictionary of compile-time info — minimal Python fallback.
|
||||
|
||||
The native ``support.GetLibInfo`` global function is no longer registered
|
||||
after the upstream sync, so we synthesize the values from build-time hints
|
||||
instead.
|
||||
"""
|
||||
return {
|
||||
"USE_CUDA": os.environ.get("TVM_USE_CUDA", "ON"),
|
||||
"USE_LLVM": os.environ.get("TVM_USE_LLVM", "ON"),
|
||||
"USE_NCCL": os.environ.get("TVM_USE_NCCL", "ON"),
|
||||
"USE_NVTX": os.environ.get("TVM_USE_NVTX", "ON"),
|
||||
"USE_NVSHMEM": os.environ.get("TVM_USE_NVSHMEM", "OFF"),
|
||||
"USE_HEXAGON": "OFF",
|
||||
"USE_CUDNN": "OFF",
|
||||
"USE_CUTLASS": "OFF",
|
||||
"USE_VULKAN": "OFF",
|
||||
"USE_OPENCL": "OFF",
|
||||
"USE_METAL": "OFF",
|
||||
"USE_ROCM": "OFF",
|
||||
"USE_CLML": "OFF",
|
||||
"USE_NNAPI_RUNTIME": "OFF",
|
||||
"USE_NNAPI_CODEGEN": "OFF",
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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.
|
||||
"""Util to invoke NDK compiler toolchain."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from tvm_ffi import register_global_func
|
||||
|
||||
from . import cc as _cc
|
||||
from . import tar as _tar
|
||||
from . import utils as _utils
|
||||
from .cc import get_target_by_dump_machine
|
||||
|
||||
|
||||
def create_shared(output, objects, options=None):
|
||||
"""Create shared library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
objects : list
|
||||
List of object files.
|
||||
|
||||
options : list of str, optional
|
||||
The additional options.
|
||||
"""
|
||||
if "TVM_NDK_CC" not in os.environ:
|
||||
raise RuntimeError(
|
||||
"Require environment variable TVM_NDK_CC to be the NDK standalone compiler"
|
||||
)
|
||||
compiler = os.environ["TVM_NDK_CC"]
|
||||
cmd = [compiler]
|
||||
cmd += ["-o", output]
|
||||
|
||||
if isinstance(objects, str):
|
||||
cmd += [objects]
|
||||
else:
|
||||
cmd += objects
|
||||
|
||||
options = options if options else ["-shared", "-fPIC", "-lm"]
|
||||
cmd += options
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
# assign output format
|
||||
create_shared.output_format = "so"
|
||||
create_shared.get_target_triple = (
|
||||
get_target_by_dump_machine(os.environ["TVM_NDK_CC"]) if "TVM_NDK_CC" in os.environ else None
|
||||
)
|
||||
|
||||
|
||||
def create_staticlib(output, inputs):
|
||||
"""Create static library:
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target static library.
|
||||
|
||||
inputs : list
|
||||
List of object files or tar files
|
||||
"""
|
||||
if "TVM_NDK_CC" not in os.environ:
|
||||
raise RuntimeError(
|
||||
"Require environment variable TVM_NDK_CC to be the NDK standalone compiler"
|
||||
)
|
||||
output_name = os.path.basename(output)
|
||||
|
||||
temp = _utils.tempdir()
|
||||
tmp_output = temp.relpath("lib" + output_name)
|
||||
objects = _tar.normalize_file_list_by_unpacking_tars(temp, inputs)
|
||||
|
||||
compiler = os.environ["TVM_NDK_CC"]
|
||||
base_path = os.path.dirname(compiler)
|
||||
ar_path = os.path.join(base_path, "llvm-ar")
|
||||
cmd = [ar_path]
|
||||
cmd += ["qcs", tmp_output]
|
||||
cmd += objects
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "AR error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
msg += "\nCommand line: " + " ".join(cmd)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
ranlib_path = os.path.join(base_path, "llvm-ranlib")
|
||||
cmd = [ranlib_path]
|
||||
cmd += [tmp_output]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
msg = "Ranlib error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
msg += "\nCommand line: " + " ".join(cmd)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
shutil.move(tmp_output, output)
|
||||
|
||||
|
||||
create_staticlib.output_format = "a"
|
||||
|
||||
|
||||
def get_global_symbol_section_map(path, *, nm=None) -> dict[str, str]:
|
||||
"""Get global symbols from a library via nm -gU in NDK
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The library path
|
||||
|
||||
nm: str
|
||||
The path to nm command
|
||||
|
||||
Returns
|
||||
-------
|
||||
symbol_section_map: Dict[str, str]
|
||||
A map from defined global symbol to their sections
|
||||
"""
|
||||
if "TVM_NDK_CC" not in os.environ:
|
||||
raise RuntimeError(
|
||||
"Require environment variable TVM_NDK_CC to be the NDK standalone compiler"
|
||||
)
|
||||
if nm is None:
|
||||
compiler = os.environ["TVM_NDK_CC"]
|
||||
base_path = os.path.dirname(compiler)
|
||||
nm = os.path.join(base_path, "llvm-nm")
|
||||
return _cc.get_global_symbol_section_map(path, nm=nm)
|
||||
|
||||
|
||||
@register_global_func("s_tir.meta_schedule.builder.export_ndk")
|
||||
def _ndk_export(mod):
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
binary_name = "tmp_binary.so"
|
||||
binary_path = Path(tmp_dir) / binary_name
|
||||
mod.export_library(binary_path, fcompile=create_shared)
|
||||
return str(binary_path)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,483 @@
|
||||
# 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
|
||||
# ruff: noqa: E731
|
||||
"""Multiprocessing via Popen.
|
||||
|
||||
This module provides a multi-processing pool backed by Popen.
|
||||
with additional timeout support.
|
||||
"""
|
||||
|
||||
import concurrent.futures
|
||||
import os
|
||||
import pickle
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
def kill_child_processes(pid):
|
||||
"""Kill all child processes recursively for a given pid.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pid : int
|
||||
The given parameter id.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import psutil
|
||||
|
||||
try:
|
||||
parent = psutil.Process(pid)
|
||||
children = parent.children(recursive=True)
|
||||
except psutil.NoSuchProcess:
|
||||
return
|
||||
|
||||
for process in children:
|
||||
try:
|
||||
process.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
|
||||
class StatusKind(IntEnum):
|
||||
"""Running and return value status."""
|
||||
|
||||
RUNNING = 0
|
||||
COMPLETE = 1
|
||||
EXCEPTION = 2
|
||||
TIMEOUT = 3
|
||||
|
||||
|
||||
class MapResult(namedtuple("MapResult", ["status", "value"])):
|
||||
"""Result of map_with_error_catching.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
status : StatusKind
|
||||
The status of the result.
|
||||
|
||||
value : Any
|
||||
The result value.
|
||||
"""
|
||||
|
||||
__slots__ = []
|
||||
|
||||
|
||||
class PopenWorker:
|
||||
"""A subprocess worker via Popen.
|
||||
|
||||
PopenWorker provides a low-level
|
||||
API to interact with a separate process via Popen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
initializer: callable or None
|
||||
A callable initializer, or None
|
||||
|
||||
initargs: Tuple[object]
|
||||
A tuple of args for the initializer
|
||||
|
||||
maximum_uses: Optional[int]
|
||||
The maximum number of times a process can be used before being recycled,
|
||||
i.e. killed and restarted. If `None`, the process will be reused until
|
||||
an operation times out.
|
||||
|
||||
stdout: Union[None, int, IO[Any]]
|
||||
The standard output streams handler specified for the popen process.
|
||||
|
||||
stderr: Union[None, int, IO[Any]]
|
||||
The standard error streams handler specified for the popen process.
|
||||
"""
|
||||
|
||||
def __init__(self, initializer=None, initargs=(), maximum_uses=None, stdout=None, stderr=None):
|
||||
self._proc = None
|
||||
self._initializer = initializer
|
||||
self._initargs = initargs
|
||||
self._maximum_uses = maximum_uses
|
||||
self._remaining_uses = None
|
||||
self._stdout = stdout
|
||||
self._stderr = stderr
|
||||
|
||||
if self._initializer is not None and not callable(self._initializer):
|
||||
raise TypeError("initializer must be callable for PopenWorker")
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.kill()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def kill(self):
|
||||
"""Kill the current running process and cleanup.
|
||||
|
||||
Note
|
||||
----
|
||||
The worker can start a new process when send is called again.
|
||||
"""
|
||||
if self._proc is not None:
|
||||
# allow gracefully shutdown
|
||||
try:
|
||||
self._writer.close()
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
self._reader.close()
|
||||
except OSError:
|
||||
pass
|
||||
# kill all child processes recursively
|
||||
try:
|
||||
kill_child_processes(self._proc.pid)
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
self._proc.kill()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Join the child process to avoid zombie processes
|
||||
self.join(timeout=1.0)
|
||||
self._proc = None
|
||||
self._remaining_uses = None
|
||||
|
||||
def _start(self):
|
||||
"""Start a new subprocess if nothing is available"""
|
||||
if self._proc is not None:
|
||||
return
|
||||
|
||||
# connect subprocess with a pair of pipes
|
||||
main_read, worker_write = os.pipe()
|
||||
worker_read, main_write = os.pipe()
|
||||
|
||||
cmd = [sys.executable, "-m", "tvm.exec.popen_worker"]
|
||||
if sys.platform == "win32":
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import msvcrt
|
||||
|
||||
worker_read_handle = msvcrt.get_osfhandle(worker_read)
|
||||
worker_write_handle = msvcrt.get_osfhandle(worker_write)
|
||||
os.set_handle_inheritable(worker_read_handle, True)
|
||||
os.set_handle_inheritable(worker_write_handle, True)
|
||||
cmd += [str(worker_read_handle), str(worker_write_handle)]
|
||||
self._proc = subprocess.Popen(
|
||||
cmd, close_fds=False, stdout=self._stdout, stderr=self._stderr
|
||||
)
|
||||
else:
|
||||
cmd += [str(worker_read), str(worker_write)]
|
||||
self._proc = subprocess.Popen(
|
||||
cmd, pass_fds=(worker_read, worker_write), stdout=self._stdout, stderr=self._stderr
|
||||
)
|
||||
|
||||
# close worker side of the pipe
|
||||
os.close(worker_read)
|
||||
os.close(worker_write)
|
||||
self._reader = os.fdopen(main_read, "rb")
|
||||
self._writer = os.fdopen(main_write, "wb")
|
||||
|
||||
def join(self, timeout=None):
|
||||
"""Join the current process worker before it terminates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
timeout: Optional[number]
|
||||
Timeout value, block at most timeout seconds if it
|
||||
is a positive number.
|
||||
"""
|
||||
if self._proc:
|
||||
try:
|
||||
self._proc.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
def is_alive(self):
|
||||
"""Check if the process is alive"""
|
||||
if self._proc:
|
||||
return self._proc.poll() is None
|
||||
return False
|
||||
|
||||
def send(self, fn, args=(), kwargs=None, timeout=None):
|
||||
"""Send a new function task ``fn(*args, **kwargs)`` to the subprocess.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fn : function
|
||||
The function to be invoked.
|
||||
|
||||
args : list
|
||||
Positional argument.
|
||||
|
||||
kwargs : dict
|
||||
Keyword arguments
|
||||
|
||||
timeout : float
|
||||
Timeout value when executing the function
|
||||
|
||||
Note
|
||||
----
|
||||
The caller must call recv before calling the next send in
|
||||
order to make sure the timeout and child process exit
|
||||
won't affect the later requests.
|
||||
"""
|
||||
# use cloud pickle
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import cloudpickle
|
||||
|
||||
if self._proc is not None and self._maximum_uses and self._remaining_uses == 0:
|
||||
# Time to recycle the process.
|
||||
self.kill()
|
||||
|
||||
if self._proc is None:
|
||||
self._start()
|
||||
# init
|
||||
if self._initializer is not None:
|
||||
self.send(self._initializer, self._initargs)
|
||||
self.recv()
|
||||
|
||||
# N.B. The initializer doesn't count as a "use"
|
||||
self._remaining_uses = self._maximum_uses
|
||||
kwargs = {} if not kwargs else kwargs
|
||||
data = cloudpickle.dumps((fn, args, kwargs, timeout), protocol=pickle.HIGHEST_PROTOCOL)
|
||||
try:
|
||||
self._writer.write(struct.pack("<i", len(data)))
|
||||
self._writer.write(data)
|
||||
self._writer.flush()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if self._remaining_uses:
|
||||
self._remaining_uses -= 1
|
||||
|
||||
def _child_process_error(self):
|
||||
"""Raise a child process error."""
|
||||
# kill and lazily restart the process in the next send.
|
||||
self.kill()
|
||||
return ChildProcessError("Subprocess terminated")
|
||||
|
||||
def recv(self):
|
||||
"""Receive the result of the last send.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result: object
|
||||
The result of the last send.
|
||||
|
||||
Raises
|
||||
------
|
||||
ChildProcessError: if the child process exited abnormally.
|
||||
TimeoutError: if timeout happens
|
||||
Exception: if other exception happens during the execution.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import cloudpickle
|
||||
|
||||
try:
|
||||
len_data = self._reader.read(4)
|
||||
except OSError:
|
||||
raise self._child_process_error()
|
||||
|
||||
if len(len_data) == 0:
|
||||
raise self._child_process_error()
|
||||
|
||||
try:
|
||||
recv_bytes = struct.unpack("<i", len_data)[0]
|
||||
status, value = cloudpickle.loads(self._reader.read(recv_bytes))
|
||||
except OSError:
|
||||
raise self._child_process_error()
|
||||
|
||||
if status == StatusKind.COMPLETE:
|
||||
return value
|
||||
if status == StatusKind.EXCEPTION:
|
||||
raise value
|
||||
assert status == StatusKind.TIMEOUT
|
||||
# kill and lazily restart the process in the next send.
|
||||
self.kill()
|
||||
raise TimeoutError()
|
||||
|
||||
|
||||
class PopenPoolExecutor:
|
||||
"""An parallel executor backed by Popen processes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
max_worker : int
|
||||
Maximum number of workers
|
||||
|
||||
timeout : float
|
||||
Timeout value for each function submit.
|
||||
|
||||
initializer: callable or None
|
||||
A callable initializer, or None
|
||||
|
||||
initargs: Tuple[object]
|
||||
A tuple of args for the initializer
|
||||
|
||||
maximum_process_uses: Optional[int]
|
||||
The maximum number of times each process can be used before being recycled,
|
||||
i.e. killed and restarted. If `None`, processes will be reused until an
|
||||
operation times out.
|
||||
|
||||
stdout: Union[None, int, IO[Any]]
|
||||
The standard output streams handler specified for the workers in the pool.
|
||||
|
||||
stderr: Union[None, int, IO[Any]]
|
||||
The standard error streams handler specified for the workers in the pool.
|
||||
|
||||
Note
|
||||
----
|
||||
If max_workers is NONE then the number returned by
|
||||
os.cpu_count() is used. This method aligns with the
|
||||
behavior of multiprocessing.pool().
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_workers=None,
|
||||
timeout=None,
|
||||
initializer=None,
|
||||
initargs=(),
|
||||
maximum_process_uses=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
):
|
||||
if max_workers is None:
|
||||
max_workers = os.cpu_count()
|
||||
# Use an internal thread pool to send to popen workers
|
||||
self._threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
|
||||
self._timeout = timeout
|
||||
self._worker_map = {}
|
||||
self._lock = threading.Lock()
|
||||
self._initializer = initializer
|
||||
self._initargs = initargs
|
||||
self._maximum_process_uses = maximum_process_uses
|
||||
self._stdout = stdout
|
||||
self._stderr = stderr
|
||||
self._shutdown = False
|
||||
|
||||
if self._initializer is not None and not callable(self._initializer):
|
||||
raise TypeError("initializer must be callable for PopenPoolExecutor")
|
||||
|
||||
def __del__(self):
|
||||
"""Destructor.
|
||||
|
||||
Note
|
||||
----
|
||||
Called during garbage collection. This may be called later than expected.
|
||||
Always call shutdown() explicitly to avoid deadlocks.
|
||||
"""
|
||||
if not self._shutdown:
|
||||
self.shutdown(wait=True)
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
"""Shutdown the executor and clean up resources.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wait : bool
|
||||
If True, wait for pending work to complete.
|
||||
|
||||
Note
|
||||
----
|
||||
DEADLOCK WARNING: This method can deadlock when called during garbage
|
||||
collection due to exception reference cycles. When exceptions occur,
|
||||
Python creates reference cycles that delay garbage collection. The
|
||||
deadlock happens when: exception creates reference cycle → new pool
|
||||
creates worker → GC cleans old pool → old pool's __del__ calls shutdown()
|
||||
which tries to acquire locks again.
|
||||
"""
|
||||
self._lock.acquire()
|
||||
for worker in self._worker_map.values():
|
||||
try:
|
||||
worker.kill()
|
||||
except ImportError:
|
||||
pass
|
||||
self._lock.release()
|
||||
self._threadpool.shutdown(wait=wait)
|
||||
self._shutdown = True
|
||||
|
||||
def _worker_run(self, fn, args, kwargs):
|
||||
"""Internal thread runner."""
|
||||
self._lock.acquire()
|
||||
tid = threading.get_ident()
|
||||
if tid not in self._worker_map:
|
||||
proc = PopenWorker(
|
||||
self._initializer,
|
||||
self._initargs,
|
||||
self._maximum_process_uses,
|
||||
self._stdout,
|
||||
self._stderr,
|
||||
)
|
||||
self._worker_map[tid] = proc
|
||||
else:
|
||||
proc = self._worker_map[tid]
|
||||
self._lock.release()
|
||||
|
||||
proc.send(fn, args, kwargs, self._timeout)
|
||||
return proc.recv()
|
||||
|
||||
def _worker_run_with_error_catching(self, fn, args, kwargs) -> MapResult:
|
||||
# pylint: disable=broad-except
|
||||
try:
|
||||
return MapResult(status=StatusKind.COMPLETE, value=self._worker_run(fn, args, kwargs))
|
||||
except TimeoutError as exception:
|
||||
return MapResult(status=StatusKind.TIMEOUT, value=exception)
|
||||
except Exception as exception:
|
||||
return MapResult(status=StatusKind.EXCEPTION, value=exception)
|
||||
|
||||
def submit(self, fn, *args, **kwargs) -> concurrent.futures.Future:
|
||||
"""Submit a new function job to the pool
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fn : function
|
||||
The function to be invoked.
|
||||
|
||||
args : list
|
||||
Positional argument.
|
||||
|
||||
kwargs : dict
|
||||
Keyword arguments
|
||||
|
||||
Returns
|
||||
-------
|
||||
future : concurrent.futures.Future
|
||||
A future that can be used to access the result.
|
||||
"""
|
||||
# pylint: disable=unnecessary-lambda
|
||||
worker = lambda *args: self._worker_run(*args)
|
||||
return self._threadpool.submit(worker, fn, args, kwargs)
|
||||
|
||||
def map_with_error_catching(self, fn, iterator):
|
||||
"""Same as map, but catches exceptions and return them instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fn : function
|
||||
The function to be invoked.
|
||||
|
||||
iterator : Iterator
|
||||
Input iterator.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out_iter : Iterator[MapResult]
|
||||
The result iterator.
|
||||
"""
|
||||
worker = lambda x: self._worker_run_with_error_catching(fn, (x,), None)
|
||||
return self._threadpool.map(worker, iterator)
|
||||
@@ -0,0 +1,293 @@
|
||||
# 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.
|
||||
"""Utility for ROCm backend"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from os.path import exists, join
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
import tvm.runtime
|
||||
import tvm.target
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def find_lld(required=True):
|
||||
"""Find ld.lld in system.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
required : bool
|
||||
Whether it is required,
|
||||
runtime error will be raised if the compiler is required.
|
||||
|
||||
Returns
|
||||
-------
|
||||
valid_list : list of str
|
||||
List of possible paths.
|
||||
|
||||
Note
|
||||
----
|
||||
This function will first search ld.lld that
|
||||
matches the major llvm version that built with tvm
|
||||
"""
|
||||
lld_list = []
|
||||
major = tvm.target.codegen.llvm_version_major(allow_none=True)
|
||||
if major is not None:
|
||||
lld_list += [f"ld.lld-{major}.0"]
|
||||
lld_list += [f"ld.lld-{major}"]
|
||||
lld_list += ["ld.lld"]
|
||||
lld_list += [f"/opt/rocm/llvm/bin/{x}" for x in lld_list]
|
||||
valid_list = [utils.which(x) for x in lld_list]
|
||||
valid_list = [x for x in valid_list if x]
|
||||
if not valid_list and required:
|
||||
raise RuntimeError("cannot find ld.lld, candidates are: " + str(lld_list))
|
||||
return valid_list
|
||||
|
||||
|
||||
def rocm_link(in_file, out_file, lld=None):
|
||||
"""Link relocatable ELF object to shared ELF object using lld
|
||||
|
||||
Parameters
|
||||
----------
|
||||
in_file : str
|
||||
Input file name (relocatable ELF object file)
|
||||
|
||||
out_file : str
|
||||
Output file name (shared ELF object file)
|
||||
|
||||
lld : str, optional
|
||||
The lld linker, if not specified,
|
||||
we will try to guess the matched clang version.
|
||||
"""
|
||||
|
||||
# if our result has undefined symbols, it will fail to load
|
||||
# (hipModuleLoad/hipModuleLoadData), but with a somewhat opaque message
|
||||
# so we have ld.lld check this here.
|
||||
# If you get a complaint about missing symbols you might want to check the
|
||||
# list of bitcode files below.
|
||||
args = [
|
||||
lld if lld is not None else find_lld()[0],
|
||||
"--no-undefined",
|
||||
"-shared",
|
||||
in_file,
|
||||
"-o",
|
||||
out_file,
|
||||
]
|
||||
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Linking error using ld.lld:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
@tvm_ffi.register_global_func("tvm_callback_rocm_link")
|
||||
def callback_rocm_link(obj_bin):
|
||||
"""Links object file generated from LLVM to HSA Code Object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj_bin : bytearray
|
||||
The object file
|
||||
|
||||
Return
|
||||
------
|
||||
cobj_bin : bytearray
|
||||
The HSA Code Object
|
||||
"""
|
||||
tmp_dir = utils.tempdir()
|
||||
tmp_obj = tmp_dir.relpath("rocm_kernel.o")
|
||||
tmp_cobj = tmp_dir.relpath("rocm_kernel.co")
|
||||
with open(tmp_obj, "wb") as out_file:
|
||||
out_file.write(bytes(obj_bin))
|
||||
rocm_link(tmp_obj, tmp_cobj)
|
||||
cobj_bin = bytearray(open(tmp_cobj, "rb").read())
|
||||
return cobj_bin
|
||||
|
||||
|
||||
@tvm_ffi.register_global_func("tvm_callback_rocm_bitcode_path")
|
||||
def callback_rocm_bitcode_path(rocdl_dir=None):
|
||||
"""Utility function to find ROCm device library bitcodes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rocdl_dir : str
|
||||
The path to rocm library directory
|
||||
The default value is the standard location
|
||||
"""
|
||||
# seems link order matters.
|
||||
|
||||
if rocdl_dir is None:
|
||||
rocm_path = find_rocm_path()
|
||||
amdgcn_path = f"{rocm_path}/amdgcn/bitcode/"
|
||||
if exists(amdgcn_path):
|
||||
rocdl_dir = amdgcn_path # starting with rocm 3.9
|
||||
else:
|
||||
rocdl_dir = "/opt/rocm/lib/" # until rocm 3.8
|
||||
|
||||
bitcode_names = [
|
||||
"oclc_daz_opt_on",
|
||||
"ocml",
|
||||
"irif", # this does not exist in rocm 3.9, drop eventually
|
||||
"oclc_correctly_rounded_sqrt_off",
|
||||
"oclc_correctly_rounded_sqrt_on",
|
||||
"oclc_daz_opt_off",
|
||||
"oclc_finite_only_off",
|
||||
"oclc_finite_only_on",
|
||||
# todo (t-vi): an alternative might be to scan for the
|
||||
"oclc_isa_version_803",
|
||||
"oclc_isa_version_900", # isa version files (if the linker throws out
|
||||
"oclc_isa_version_906", # the unneeded ones or we filter for the arch we need)
|
||||
"oclc_isa_version_1030",
|
||||
"oclc_unsafe_math_off",
|
||||
"oclc_unsafe_math_on",
|
||||
"oclc_wavefrontsize64_on",
|
||||
"oclc_abi_version_500",
|
||||
]
|
||||
|
||||
bitcode_files = []
|
||||
for n in bitcode_names:
|
||||
p = join(rocdl_dir, n + ".bc") # rocm >= 3.9
|
||||
if not exists(p): # rocm <= 3.8
|
||||
p = join(rocdl_dir, n + ".amdgcn.bc")
|
||||
if exists(p):
|
||||
bitcode_files.append(p)
|
||||
elif "isa_version" not in n and n not in {"irif"}:
|
||||
raise RuntimeError("could not find bitcode " + n)
|
||||
|
||||
return tvm.runtime.convert(bitcode_files)
|
||||
|
||||
|
||||
def parse_compute_version(compute_version):
|
||||
"""Parse compute capability string to divide major and minor version
|
||||
|
||||
Parameters
|
||||
----------
|
||||
compute_version : str
|
||||
compute capability of a GPU (e.g. "6.0")
|
||||
|
||||
Returns
|
||||
-------
|
||||
major : int
|
||||
major version number
|
||||
minor : int
|
||||
minor version number
|
||||
"""
|
||||
split_ver = compute_version.split(".")
|
||||
try:
|
||||
major = int(split_ver[0])
|
||||
minor = int(split_ver[1])
|
||||
return major, minor
|
||||
except (IndexError, ValueError) as err:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise RuntimeError("Compute version parsing error: " + str(err))
|
||||
|
||||
|
||||
def have_matrixcore(compute_version=None):
|
||||
"""Either MatrixCore support is provided in the compute capability or not
|
||||
|
||||
Parameters
|
||||
----------
|
||||
compute_version : str, optional
|
||||
compute capability of a GPU (e.g. "7.0").
|
||||
|
||||
Returns
|
||||
-------
|
||||
have_matrixcore : bool
|
||||
True if MatrixCore support is provided, False otherwise
|
||||
"""
|
||||
if compute_version is None:
|
||||
if tvm.rocm(0).exist:
|
||||
compute_version = tvm.rocm(0).compute_version
|
||||
else:
|
||||
raise RuntimeError("No ROCm runtime found")
|
||||
major, _ = parse_compute_version(compute_version)
|
||||
# matrix core first introduced in 8.0
|
||||
if major >= 8:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@tvm_ffi.register_global_func("tvm_callback_rocm_get_arch")
|
||||
def get_rocm_arch(rocm_path=None):
|
||||
"""Utility function to get the AMD GPU architecture
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rocm_path : str
|
||||
The path to rocm installation directory
|
||||
|
||||
Returns
|
||||
-------
|
||||
gpu_arch : str
|
||||
The AMD GPU architecture
|
||||
"""
|
||||
if rocm_path is None:
|
||||
try:
|
||||
rocm_path = find_rocm_path()
|
||||
except RuntimeError:
|
||||
rocm_path = None
|
||||
|
||||
gpu_arch = "gfx900"
|
||||
# check if rocm is installed
|
||||
if rocm_path is None or not os.path.exists(rocm_path):
|
||||
print("ROCm not detected, using default gfx900")
|
||||
return gpu_arch
|
||||
try:
|
||||
# Execute rocminfo command
|
||||
rocminfo_output = subprocess.check_output([f"{rocm_path}/bin/rocminfo"]).decode("utf-8")
|
||||
|
||||
# Use regex to match the "Name" field
|
||||
match = re.search(r"Name:\s+(gfx\d+[a-zA-Z]*)", rocminfo_output)
|
||||
if match:
|
||||
gpu_arch = match.group(1)
|
||||
return gpu_arch
|
||||
except subprocess.CalledProcessError:
|
||||
print(
|
||||
f"Unable to execute rocminfo command, \
|
||||
please ensure ROCm is installed and you have an AMD GPU on your system.\
|
||||
using default {gpu_arch}."
|
||||
)
|
||||
return gpu_arch
|
||||
|
||||
|
||||
def find_rocm_path():
|
||||
"""Utility function to find ROCm path
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : str
|
||||
Path to ROCm root.
|
||||
"""
|
||||
if "ROCM_PATH" in os.environ:
|
||||
return os.environ["ROCM_PATH"]
|
||||
cmd = ["which", "hipcc"]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
out = out.decode("utf-8", errors="replace").strip()
|
||||
if proc.returncode == 0:
|
||||
return os.path.realpath(os.path.join(out, "../.."))
|
||||
rocm_path = "/opt/rocm"
|
||||
if os.path.exists(os.path.join(rocm_path, "bin/hipcc")):
|
||||
return rocm_path
|
||||
raise RuntimeError("Cannot find ROCm path")
|
||||
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
|
||||
"""Util to invoke tarball in the system."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def tar(output, files):
|
||||
"""Create tarball containing all files in root.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
files : list
|
||||
List of files to be bundled.
|
||||
"""
|
||||
cmd = ["tar"]
|
||||
cmd += ["-czf"]
|
||||
temp = utils.tempdir()
|
||||
fset = set()
|
||||
for fname in files:
|
||||
base = os.path.basename(fname)
|
||||
if base in fset:
|
||||
raise ValueError(f"duplicate file name {base}")
|
||||
fset.add(base)
|
||||
shutil.copy(fname, temp.relpath(base))
|
||||
cmd += [output]
|
||||
cmd += ["-C", temp.temp_dir]
|
||||
cmd += temp.listdir()
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Tar error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
# assign output format
|
||||
tar.output_format = "tar"
|
||||
|
||||
|
||||
def untar(tar_file, directory):
|
||||
"""Unpack all tar files into the directory
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tar_file : str
|
||||
The source tar file.
|
||||
|
||||
directory : str
|
||||
The target directory
|
||||
"""
|
||||
cmd = ["tar"]
|
||||
cmd += ["-xf"]
|
||||
cmd += [tar_file]
|
||||
cmd += ["-C", directory]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Tar error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def normalize_file_list_by_unpacking_tars(temp, file_list):
|
||||
"""Normalize the file list by unpacking tars in list.
|
||||
|
||||
When a filename is a tar, it will untar it into an unique dir
|
||||
in temp and return the list of files in the tar.
|
||||
When a filename is a normal file, it will be simply added to the list.
|
||||
|
||||
This is useful to untar objects in tar and then turn
|
||||
them into a library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
temp: tvm.support.utils.TempDirectory
|
||||
A temp dir to hold the untared files.
|
||||
|
||||
file_list: List[str]
|
||||
List of path
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret_list: List[str]
|
||||
An updated list of files
|
||||
"""
|
||||
temp_count = 0
|
||||
ret_list = []
|
||||
for file_path in file_list:
|
||||
# enable extracting a tarball
|
||||
if file_path.endswith(".tar"):
|
||||
temp_dir = temp.relpath(f"temp{temp_count}")
|
||||
temp_count += 1
|
||||
os.mkdir(temp_dir)
|
||||
untar(file_path, temp_dir)
|
||||
# append all files inside
|
||||
for root, _, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
ret_list.append(os.path.join(root, file))
|
||||
else:
|
||||
ret_list.append(file_path)
|
||||
return ret_list
|
||||
@@ -0,0 +1,279 @@
|
||||
# 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.
|
||||
# ruff: noqa: RUF005, RUF012
|
||||
"""Common system utilities"""
|
||||
|
||||
import atexit
|
||||
import contextlib
|
||||
import datetime
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
fcntl = None
|
||||
|
||||
|
||||
class DirectoryCreatedPastAtExit(Exception):
|
||||
"""Raised when a TempDirectory is created after the atexit hook runs."""
|
||||
|
||||
|
||||
class TempDirectory:
|
||||
"""Helper object to manage temp directory during testing.
|
||||
|
||||
Automatically removes the directory when it went out of scope.
|
||||
"""
|
||||
|
||||
# When True, all TempDirectory are *NOT* deleted and instead live inside a predicable directory
|
||||
# tree.
|
||||
_KEEP_FOR_DEBUG = False
|
||||
|
||||
# In debug mode, each tempdir is named after the sequence
|
||||
_NUM_TEMPDIR_CREATED = 0
|
||||
_NUM_TEMPDIR_CREATED_LOCK = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def _increment_num_tempdir_created(cls):
|
||||
with cls._NUM_TEMPDIR_CREATED_LOCK:
|
||||
to_return = cls._NUM_TEMPDIR_CREATED
|
||||
cls._NUM_TEMPDIR_CREATED += 1
|
||||
|
||||
return to_return
|
||||
|
||||
_DEBUG_PARENT_DIR = None
|
||||
|
||||
@classmethod
|
||||
def _get_debug_parent_dir(cls):
|
||||
if cls._DEBUG_PARENT_DIR is None:
|
||||
all_parents = f"{tempfile.gettempdir()}/tvm-debug-mode-tempdirs"
|
||||
if not os.path.isdir(all_parents):
|
||||
os.makedirs(all_parents)
|
||||
cls._DEBUG_PARENT_DIR = tempfile.mkdtemp(
|
||||
prefix=datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%S___"), dir=all_parents
|
||||
)
|
||||
return cls._DEBUG_PARENT_DIR
|
||||
|
||||
TEMPDIRS = set()
|
||||
|
||||
@classmethod
|
||||
def remove_tempdirs(cls):
|
||||
temp_dirs = getattr(cls, "TEMPDIRS", None)
|
||||
if temp_dirs is None:
|
||||
return
|
||||
|
||||
for path in temp_dirs:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
||||
cls.TEMPDIRS = None
|
||||
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def set_keep_for_debug(cls, set_to=True):
|
||||
"""Keep temporary directories past program exit for debugging."""
|
||||
old_keep_for_debug = cls._KEEP_FOR_DEBUG
|
||||
try:
|
||||
cls._KEEP_FOR_DEBUG = set_to
|
||||
yield
|
||||
finally:
|
||||
cls._KEEP_FOR_DEBUG = old_keep_for_debug
|
||||
|
||||
def __init__(self, custom_path=None, keep_for_debug=None):
|
||||
if self.TEMPDIRS is None:
|
||||
raise DirectoryCreatedPastAtExit()
|
||||
|
||||
if keep_for_debug is not None:
|
||||
self._created_with_keep_for_debug = keep_for_debug
|
||||
else:
|
||||
self._created_with_keep_for_debug = self._KEEP_FOR_DEBUG
|
||||
|
||||
if custom_path:
|
||||
os.mkdir(custom_path)
|
||||
self.temp_dir = custom_path
|
||||
else:
|
||||
if self._created_with_keep_for_debug:
|
||||
parent_dir = self._get_debug_parent_dir()
|
||||
self.temp_dir = f"{parent_dir}/{self._increment_num_tempdir_created():05d}"
|
||||
os.mkdir(self.temp_dir)
|
||||
else:
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
if not self._created_with_keep_for_debug:
|
||||
self.TEMPDIRS.add(self.temp_dir)
|
||||
|
||||
def remove(self):
|
||||
"""Remove the tmp dir"""
|
||||
if self.temp_dir:
|
||||
if not self._created_with_keep_for_debug:
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
self.TEMPDIRS.remove(self.temp_dir)
|
||||
self.temp_dir = None
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return pathlib.Path(self.temp_dir)
|
||||
|
||||
def __truediv__(self, other):
|
||||
if not isinstance(other, str | pathlib.Path):
|
||||
raise TypeError(
|
||||
f"TempDirectory / operator: must supply str or pathlib.Path; got {other!r}"
|
||||
)
|
||||
|
||||
return self.path / other
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, ptype, value, trace):
|
||||
self.remove()
|
||||
|
||||
def __del__(self):
|
||||
temp_dirs = getattr(self, "TEMPDIRS", None)
|
||||
if temp_dirs is None:
|
||||
# Do nothing if the atexit hook has already run.
|
||||
return
|
||||
|
||||
self.remove()
|
||||
|
||||
def relpath(self, name):
|
||||
"""Relative path in temp dir
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : str
|
||||
The concatenated path.
|
||||
"""
|
||||
return os.path.join(self.temp_dir, name)
|
||||
|
||||
def listdir(self):
|
||||
"""List contents in the dir.
|
||||
|
||||
Returns
|
||||
-------
|
||||
names : list
|
||||
The content of directory
|
||||
"""
|
||||
return os.listdir(self.temp_dir)
|
||||
|
||||
|
||||
atexit.register(TempDirectory.remove_tempdirs)
|
||||
|
||||
|
||||
def tempdir(custom_path=None, keep_for_debug=None):
|
||||
"""Create temp dir which deletes the contents when exit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
custom_path : str, optional
|
||||
Manually specify the exact temp dir path
|
||||
|
||||
keep_for_debug : bool
|
||||
Keep temp directory for debugging purposes
|
||||
Returns
|
||||
-------
|
||||
temp : TempDirectory
|
||||
The temp directory object
|
||||
"""
|
||||
return TempDirectory(custom_path=custom_path, keep_for_debug=keep_for_debug)
|
||||
|
||||
|
||||
class FileLock:
|
||||
"""File lock object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The path to the lock
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.lock_file = open(path, "w")
|
||||
if fcntl:
|
||||
fcntl.lockf(self.lock_file, fcntl.LOCK_EX)
|
||||
|
||||
def release(self):
|
||||
"""Release the lock"""
|
||||
if self.lock_file:
|
||||
if fcntl:
|
||||
fcntl.lockf(self.lock_file, fcntl.LOCK_UN)
|
||||
self.lock_file.close()
|
||||
self.lock_file = None
|
||||
|
||||
|
||||
def filelock(path):
|
||||
"""Create a file lock which locks on path
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
The path to the lock
|
||||
|
||||
Returns
|
||||
-------
|
||||
lock : File lock object
|
||||
"""
|
||||
return FileLock(path)
|
||||
|
||||
|
||||
def is_source_path(path):
|
||||
"""Check if path is source code path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : str
|
||||
A possible path
|
||||
|
||||
Returns
|
||||
-------
|
||||
valid : bool
|
||||
Whether path is a possible source path
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
return True
|
||||
if path.find("\n") != -1:
|
||||
return False
|
||||
spath = path.rsplit(".", 1)
|
||||
return len(spath) == 2 and spath[1].strip() == spath[1]
|
||||
|
||||
|
||||
def which(exec_name):
|
||||
"""Try to find full path of exec_name
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exec_name : str
|
||||
The executable name
|
||||
|
||||
Returns
|
||||
-------
|
||||
path : str
|
||||
The full path of executable if found, otherwise returns None
|
||||
"""
|
||||
base_list = ["", "/bin"] + os.environ.get("PATH", "").split(os.pathsep)
|
||||
for path in base_list:
|
||||
full_path = os.path.join(path, exec_name)
|
||||
if os.path.isfile(full_path) and os.access(full_path, os.X_OK):
|
||||
return full_path
|
||||
return None
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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
|
||||
# ruff: noqa: E501, RUF005
|
||||
"""Utility to invoke Xcode compiler toolchain"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def xcrun(cmd):
|
||||
"""Run xcrun and return the output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cmd : list of str
|
||||
The command sequence.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : str
|
||||
The output string.
|
||||
"""
|
||||
cmd = ["xcrun"] + cmd
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
return out.strip()
|
||||
|
||||
|
||||
def __get_min_os_version(sdk):
|
||||
if sdk == "macosx":
|
||||
return None
|
||||
if sdk in ("iphoneos", "iphonesimulator"):
|
||||
return "13.0"
|
||||
raise RuntimeError(f"Unsupported sdk: {sdk}")
|
||||
|
||||
|
||||
def __get_min_os_version_cmd(sdk, min_os_version):
|
||||
if min_os_version is None:
|
||||
min_os_version = __get_min_os_version(sdk)
|
||||
if min_os_version is not None:
|
||||
return "-mios-version-min=" + min_os_version
|
||||
return ""
|
||||
|
||||
|
||||
def create_dylib(output, objects, arch, sdk="macosx", min_os_version=None):
|
||||
"""Create dynamic library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output : str
|
||||
The target shared library.
|
||||
|
||||
objects : list
|
||||
List of object files.
|
||||
|
||||
options : str
|
||||
The additional options.
|
||||
|
||||
arch : str
|
||||
Target major architectures
|
||||
|
||||
sdk : str
|
||||
The sdk to be used.
|
||||
"""
|
||||
clang = xcrun(["-sdk", sdk, "-find", "clang"])
|
||||
sdk_path = xcrun(["-sdk", sdk, "--show-sdk-path"])
|
||||
cmd = [clang]
|
||||
cmd += ["-dynamiclib"]
|
||||
cmd += ["-arch", arch]
|
||||
cmd += ["-isysroot", sdk_path]
|
||||
cmd += [__get_min_os_version_cmd(sdk, min_os_version)]
|
||||
cmd += ["-o", output]
|
||||
if isinstance(objects, str):
|
||||
cmd += [objects]
|
||||
else:
|
||||
cmd += objects
|
||||
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
(out, _) = proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
msg = "Compilation error:\n"
|
||||
msg += out.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
# assign so as default output format
|
||||
create_dylib.output_format = "dylib"
|
||||
|
||||
|
||||
def compile_metal(code, path_target=None, sdk="macosx", min_os_version=None):
|
||||
"""Compile Metal with CLI tool from env.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
The Metal code.
|
||||
|
||||
path_target : str, optional
|
||||
Output file.
|
||||
|
||||
sdk : str, optional
|
||||
The target platform SDK.
|
||||
|
||||
Return
|
||||
------
|
||||
metallib : bytearray
|
||||
The bytearray of the metallib
|
||||
"""
|
||||
temp = utils.tempdir()
|
||||
temp_code = temp.relpath("my_lib.metal")
|
||||
temp_ir = temp.relpath("my_lib.air")
|
||||
temp_target = temp.relpath("my_lib.metallib")
|
||||
|
||||
with open(temp_code, "w") as out_file:
|
||||
out_file.write(code)
|
||||
file_target = path_target if path_target else temp_target
|
||||
|
||||
# See:
|
||||
# - https://developer.apple.com/documentation/metal/gpu_functions_libraries/building_a_library_with_metal_s_command-line_tools#overview # pylint: disable=line-too-long
|
||||
#
|
||||
# xcrun -sdk macosx metal -c MyLibrary.metal -o MyLibrary.air
|
||||
# xcrun -sdk macosx metallib MyLibrary.air -o MyLibrary.metallib
|
||||
min_target = __get_min_os_version_cmd(sdk, min_os_version)
|
||||
if sdk == "macosx":
|
||||
language_version = "-std=macos-metal2.3"
|
||||
elif sdk in ("iphoneos", "iphonesimulator"):
|
||||
language_version = "-std=ios-metal2.3"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported sdk: {sdk}")
|
||||
cmd1 = ["xcrun", "-sdk", sdk, "metal", language_version, min_target, "-O3"]
|
||||
cmd1 += ["-c", temp_code, "-o", temp_ir]
|
||||
cmd2 = ["xcrun", "-sdk", sdk, "metallib"]
|
||||
cmd2 += [temp_ir, "-o", file_target]
|
||||
proc = subprocess.Popen(
|
||||
" ".join(cmd1) + ";" + " ".join(cmd2),
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
(out, _) = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
sys.stderr.write("Compilation error:\n")
|
||||
sys.stderr.write(out.decode("utf-8", errors="replace"))
|
||||
sys.stderr.flush()
|
||||
libbin = None
|
||||
else:
|
||||
libbin = bytearray(open(file_target, "rb").read())
|
||||
return libbin
|
||||
|
||||
|
||||
def compile_coreml(model, model_name="main", out_dir="."):
|
||||
"""Compile coreml model and return the compiled model path."""
|
||||
mlmodel_path = os.path.join(out_dir, model_name + ".mlmodel")
|
||||
mlmodelc_path = os.path.join(out_dir, model_name + ".mlmodelc")
|
||||
metadata = {"inputs": list(model.input_description), "outputs": list(model.output_description)}
|
||||
# Use the description field to send info to CoreML runtime
|
||||
model.short_description = json.dumps(metadata)
|
||||
model.save(mlmodel_path)
|
||||
|
||||
res = xcrun(["coremlcompiler", "compile", mlmodel_path, out_dir])
|
||||
if not os.path.isdir(mlmodelc_path):
|
||||
raise RuntimeError(f"Compile failed: {res}")
|
||||
|
||||
return mlmodelc_path
|
||||
Reference in New Issue
Block a user