Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

555 lines
18 KiB
Python

"""Helper functions for target auto-detection."""
import os
from pathlib import Path
from typing import TYPE_CHECKING, Callable, List, Optional, Tuple # noqa: UP035
from tvm import IRModule, relax
from tvm.ir.transform import Pass
from tvm.support import ndk, tar, xcode
from tvm.target import Target
from tvm_ffi import get_global_func, register_global_func
from . import logging
from .auto_device import AUTO_DETECT_DEVICES, detect_device, device2str
from .constants import MLC_MULTI_ARCH
from .style import bold, green, red
if TYPE_CHECKING:
from mlc_llm.compiler.compile import CompileArgs
logger = logging.getLogger(__name__)
# TODO: add help message on how to specify the target manually
HELP_MSG = """TBD"""
FOUND = green("Found")
NOT_FOUND = red("Not found")
BuildFunc = Callable[[IRModule, "CompileArgs", Pass], None]
def detect_target_and_host(
target_hint: str,
host_hint: str = "auto",
enable_subgroups: Optional[bool] = None,
) -> Tuple[Target, BuildFunc]: # noqa: UP006
"""Detect the configuration for the target device and its host, for example, target GPU and
the host CPU.
Parameters
----------
target_hint : str
The hint for the target device.
host_hint : str
The hint for the host CPU, default is "auto".
"""
target, build_func = _detect_target_gpu(target_hint)
if target.host is None:
target = Target(target, host=_detect_target_host(host_hint))
target = _apply_webgpu_subgroups(target, enable_subgroups)
if target.kind.name == "cuda":
# Enable thrust for CUDA
target_dict = dict(target.export())
target_dict["libs"] = (
(target_dict["libs"] + ["thrust"]) if "libs" in target_dict else ["thrust"]
)
target = Target(target_dict)
_register_cuda_hook(target)
elif target.kind.name == "rocm":
target_dict = dict(target.export())
extra_libs = ["thrust", "rocblas", "miopen", "hipblas"]
target_dict["libs"] = (
(target_dict["libs"] + extra_libs) if "libs" in target_dict else extra_libs
)
target = Target(target_dict)
return target, build_func
def _apply_webgpu_subgroups(target: Target, enable_subgroups: Optional[bool]) -> Target:
if not enable_subgroups:
return target
if target.kind.name != "webgpu":
logger.warning(
"--enable-subgroups is only supported for WebGPU targets; ignoring for %s",
target.kind.name,
)
return target
target_dict = dict(target.export())
target_dict["supports_subgroups"] = True
return Target(target_dict)
def _detect_target_gpu(hint: str) -> Tuple[Target, BuildFunc]: # noqa: UP006
if hint in ["iphone", "macabi", "android", "webgpu", "mali", "opencl"]:
hint += ":generic"
if hint == "auto" or hint in AUTO_DETECT_DEVICES:
target: Optional[Target] = None
device = detect_device(hint)
if device is not None:
device_str = device2str(device)
try:
target = Target.from_device(device)
except ValueError:
logger.info("%s: Cannot detect target from device: %s", NOT_FOUND, device_str)
if target is None:
raise ValueError(f"No target detected from device: {hint}. Please specify explicitly")
logger.info(
'%s configuration of target device "%s": %s',
FOUND,
bold(device_str),
target.export(),
)
return target, _build_default()
if hint in PRESET:
preset = PRESET[hint]
target = Target(preset["target"])
build = preset.get("build", _build_default)
return target, build()
if _is_device(hint):
logger.info("Detecting target device: %s", hint)
target = Target.from_device(hint)
logger.info("%s target: %s", FOUND, target.export())
return target, _build_default()
try:
logger.info("Try creating device target from string: %s", hint)
target = Target(hint)
logger.info("%s target: %s", FOUND, target.export())
return target, _build_default()
except Exception as err:
logger.info("%s: Failed to create target", NOT_FOUND)
raise ValueError(f"Invalid target: {hint}") from err
def _detect_target_host(hint: str) -> Target:
"""Detect the host CPU architecture."""
if hint == "auto":
target_triple = get_global_func("tvm.codegen.llvm.GetDefaultTargetTriple")()
target = Target.from_device("cpu")
logger.info("%s host LLVM triple: %s", FOUND, bold(target.attrs["mtriple"]))
logger.info("%s host LLVM CPU: %s", FOUND, bold(target.attrs["mcpu"]))
return target
target_triple = hint
logger.info("Using LLVM triple specified by --host: %s", bold(target_triple))
return Target({"kind": "llvm", "mtriple": target_triple})
def _is_device(device: str):
if " " in device:
return False
if device.count(":") != 1:
return False
return True
def _add_system_lib_prefix(mod: IRModule, prefix: str, is_system_lib: bool) -> IRModule:
if is_system_lib and prefix:
mod = mod.with_attrs({"system_lib_prefix": prefix})
elif is_system_lib:
logger.warning(
"%s is not specified when building a static library",
bold("--system-lib-prefix"),
)
elif prefix:
logger.warning(
"--system-lib-prefix is specified, but it will not take any effect "
"when building the shared library"
)
return mod
def _build_metal_x86_64():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=False)
assert output.suffix == ".dylib"
relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
).export_library(
str(output),
fcompile=xcode.create_dylib,
sdk="macosx",
arch="x86_64",
)
return build
def _build_iphone():
@register_global_func("tvm_callback_metal_compile", override=True)
def compile_metal(src, target):
libs = target.attrs.get("libs", None)
if libs:
return xcode.compile_metal(src, sdk=libs[0])
return xcode.compile_metal(src)
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=True)
assert output.suffix == ".tar"
relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=True,
).export_library(
str(output),
fcompile=tar.tar,
)
return build
def _build_android():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=True)
assert output.suffix == ".tar"
ex = relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=True,
)
ex.export_library(
str(output),
fcompile=tar.tar,
)
if args.debug_dump is not None:
source = ex.mod.imports[0].imports[0].inspect_source()
with open(args.debug_dump / "kernel.cl", "w", encoding="utf-8") as f:
f.write(source)
return build
def _build_android_so():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=False)
assert output.suffix == ".so"
ex = relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=False,
)
ex.export_library(
str(output),
fcompile=ndk.create_shared,
)
if args.debug_dump is not None:
source = ex.mod.imports[0].imports[0].inspect_source()
with open(args.debug_dump / "kernel.cl", "w", encoding="utf-8") as f:
f.write(source)
return build
def _build_webgpu():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=True)
assert output.suffix == ".wasm"
# Try to locate `mlc_wasm_runtime.bc`
bc_path = None
bc_candidates = ["web/dist/wasm/mlc_wasm_runtime.bc"]
if os.environ.get("MLC_LLM_SOURCE_DIR", None):
mlc_source_home_dir = os.environ["MLC_LLM_SOURCE_DIR"]
bc_candidates.append(
os.path.join(mlc_source_home_dir, "web", "dist", "wasm", "mlc_wasm_runtime.bc")
)
error_info = (
"Cannot find library: mlc_wasm_runtime.bc\n"
+ "Make sure you have run `./web/prep_emcc_deps.sh` and "
+ "`export MLC_LLM_SOURCE_DIR=/path/to/mlc-llm` so that we can locate the file. "
+ "We tried to look at candidate paths:\n"
)
for candidate in bc_candidates:
error_info += candidate + "\n"
if Path(candidate).exists():
bc_path = candidate
if not bc_path:
raise RuntimeError(error_info)
relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=True,
).export_library(
str(output),
libs=[bc_path],
)
return build
def _build_mali():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=True)
assert output.suffix == ".so"
mod = relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=True,
)
if "TVM_NDK_CC" in os.environ:
mod.export_library(str(output), fcompile=ndk.create_shared)
else:
mod.export_library(str(output))
return build
def _build_default():
def build(mod: IRModule, args: "CompileArgs", pipeline=None):
output = args.output
if output.suffix in [".tar", ".lib"]:
system_lib = True
elif output.suffix in [".so", ".dylib", ".dll"]:
system_lib = False
else:
logger.warning("Unknown output suffix: %s. Assuming shared library.", output.suffix)
system_lib = False
mod = _add_system_lib_prefix(mod, args.system_lib_prefix, is_system_lib=system_lib)
relax.build(
mod,
target=args.target,
relax_pipeline=pipeline,
system_lib=system_lib,
).export_library(
str(output),
)
return build
def detect_cuda_arch_list(target: Target) -> List[int]: # noqa: UP006
"""Detect the CUDA architecture list from the target."""
def convert_to_num(arch_str):
arch_num_str = "".join(filter(str.isdigit, arch_str))
assert arch_num_str, f"'{arch_str}' does not contain any digits"
return int(arch_num_str)
assert target.kind.name == "cuda", f"Expect target to be CUDA, but got {target}"
if MLC_MULTI_ARCH is not None:
multi_arch = [convert_to_num(x) for x in MLC_MULTI_ARCH.split(",")]
else:
assert target.attrs.get("arch", "").startswith("sm_")
multi_arch = [convert_to_num(target.attrs.get("arch")[3:])]
multi_arch = list(set(multi_arch))
return multi_arch
def _register_cuda_hook(target: Target):
if MLC_MULTI_ARCH is None:
default_arch = target.attrs.get("arch", None)
logger.info("Generating code for CUDA architecture: %s", bold(default_arch))
logger.info(
"To produce multi-arch fatbin, set environment variable %s. "
"Example: MLC_MULTI_ARCH=70,72,75,80,86,87,89,90a",
bold("MLC_MULTI_ARCH"),
)
multi_arch = None
else:
logger.info("%s %s: %s", FOUND, bold("MLC_MULTI_ARCH"), MLC_MULTI_ARCH)
multi_arch = [x.strip() for x in MLC_MULTI_ARCH.split(",")]
logger.info("Generating code for CUDA architecture: %s", multi_arch)
@register_global_func("tvm_callback_cuda_compile", override=True)
def tvm_callback_cuda_compile(code):
"""use nvcc to generate fatbin code for better optimization"""
from tvm.support import nvcc
if multi_arch is None:
ptx = nvcc.compile_cuda(code, target_format="fatbin", compiler="nvcc")
else:
arch = []
for compute_version in multi_arch:
arch += [
"-gencode",
f"arch=compute_{compute_version},code=sm_{compute_version}",
]
ptx = nvcc.compile_cuda(code, target_format="fatbin", arch=arch, compiler="nvcc")
return ptx
def detect_system_lib_prefix(
target_hint: str, prefix_hint: str, model_name: str, quantization: str
) -> str:
"""Detect the iOS / Android system lib prefix to identify the library needed to load the app.
Parameters
----------
target_hint : str
The hint for the target device.
prefix_hint : str
The hint for the system lib prefix.
"""
if prefix_hint == "auto" and (
target_hint.startswith("iphone")
or target_hint.startswith("macabi")
or target_hint.startswith("android")
):
prefix = f"{model_name}_{quantization}_".replace("-", "_")
logger.warning(
"%s is automatically picked from the filename, %s, this allows us to use the filename "
"as the model_lib in android/iOS builds. Please avoid renaming the .tar file when "
"uploading the prebuilt.",
bold("--system-lib-prefix"),
bold(prefix),
)
return prefix
if target_hint not in ["iphone", "macabi", "android"]:
return ""
return prefix_hint
_MACABI_ARCH = os.environ.get("MLC_MACABI_ARCH", "").strip() or "arm64"
if _MACABI_ARCH not in ["arm64", "x86_64"]:
_MACABI_ARCH = "arm64"
_MACABI_MTRIPLE = (
"x86_64-apple-ios18.0-macabi" if _MACABI_ARCH == "x86_64" else "arm64-apple-ios18.0-macabi"
)
PRESET = {
"iphone:generic": {
"target": {
"kind": "metal",
"max_threads_per_block": 256,
"max_shared_memory_per_block": 32768,
"thread_warp_size": 1,
"libs": ["iphoneos"],
"host": {
"kind": "llvm",
"mtriple": "arm64-apple-darwin",
},
},
"build": _build_iphone,
},
"macabi:generic": {
"target": {
"kind": "metal",
"max_threads_per_block": 256,
"max_shared_memory_per_block": 32768,
"thread_warp_size": 1,
"libs": ["macosx"],
"host": {
"kind": "llvm",
"mtriple": _MACABI_MTRIPLE,
},
},
"build": _build_iphone,
},
"android:generic": {
"target": {
"kind": "opencl",
"host": {
"kind": "llvm",
"mtriple": "aarch64-linux-android",
},
},
"build": _build_android,
},
"android:adreno": {
"target": {
"kind": "opencl",
"device": "adreno",
"max_threads_per_block": 512,
"host": {
"kind": "llvm",
"mtriple": "aarch64-linux-android",
},
},
"build": _build_android,
},
"android:adreno-so": {
"target": {
"kind": "opencl",
"device": "adreno",
"max_threads_per_block": 512,
"host": {
"kind": "llvm",
"mtriple": "aarch64-linux-android",
},
},
"build": _build_android_so,
},
"windows:adreno_x86": {
"target": {
"kind": "opencl",
"device": "adreno",
"max_threads_per_block": 512,
"host": {
"kind": "llvm",
"mtriple": "x86_64-pc-windows-msvc",
},
},
},
"metal:x86-64": {
"target": {
"kind": "metal",
"max_threads_per_block": 256,
"max_shared_memory_per_block": 32768,
"thread_warp_size": 1,
},
"build": _build_metal_x86_64,
},
"webgpu:generic": {
"target": {
"kind": "webgpu",
"host": {
"kind": "llvm",
"mtriple": "wasm32-unknown-unknown-wasm",
},
},
"build": _build_webgpu,
},
"opencl:generic": {
"target": {
"kind": "opencl",
},
},
"mali:generic": {
"target": {
"kind": "opencl",
"host": {
"kind": "llvm",
"mtriple": "aarch64-linux-gnu",
},
},
"build": _build_mali,
},
"metal:generic": {
"target": {
"kind": "metal",
"max_threads_per_block": 256,
"max_shared_memory_per_block": 32768,
"thread_warp_size": 1,
},
},
"vulkan:generic": {
"target": {
"kind": "vulkan",
"max_threads_per_block": 256,
"max_shared_memory_per_block": 32768,
"thread_warp_size": 1,
"supports_float16": 1,
"supports_int64": 1,
"supports_int16": 1,
"supports_int8": 1,
"supports_8bit_buffer": 1,
"supports_16bit_buffer": 1,
"supports_storage_buffer_storage_class": 1,
},
},
}