chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
+54
View File
@@ -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=" "))
+418
View File
@@ -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)
+110
View File
@@ -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()
+145
View File
@@ -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"
+45
View File
@@ -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",
}
+166
View File
@@ -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
+483
View File
@@ -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)
+293
View File
@@ -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")
+127
View File
@@ -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
+279
View File
@@ -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
+184
View File
@@ -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