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
+30
View File
@@ -0,0 +1,30 @@
# isort: skip_file
# 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=redefined-builtin, wildcard-import
"""Utility Python functions for TVM testing"""
from ._ffi_api import (
echo,
object_use_count,
test_raise_error,
)
from .runner import local_run, rpc_run
from .locking import run_with_gpu_lock
from .utils import *
from . import env
+24
View File
@@ -0,0 +1,24 @@
# 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.
"""FFI APIs for tvm.testing"""
import tvm_ffi
# must import testing before init_ffi_api
import tvm_ffi.testing
tvm_ffi.init_ffi_api("testing", __name__)
+420
View File
@@ -0,0 +1,420 @@
# 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.
"""Thin capability probes for test gating.
This module exposes small ``has_*`` predicates that report whether the
current environment can run a given feature. They are meant to be used
with plain pytest markers and ``skipif``::
import pytest
import tvm.testing
@pytest.mark.gpu
@pytest.mark.skipif(not tvm.testing.env.has_cuda(), reason="need cuda")
def test_my_cuda_kernel():
...
Each probe's expensive query (device lookup, ``nvcc`` subprocess, libinfo)
is memoized with :func:`functools.cache`, so it runs at most once per
process even though ``skipif`` evaluates the predicate at import time for
every decorated test. Probes never raise: when support is absent they
return ``False`` (or a zero version tuple) rather than propagating an
error out of collection.
Three kinds of probe live here:
* **runtime device** probes (``has_cuda``, ``has_gpu`` …) ask whether a
usable device of a given kind is present;
* **build-support** probes (``has_cudnn`` …, ``build_flag_enabled`` …) ask whether
an optional library was compiled into the runtime;
* **version / capability** probes (``has_cuda_compute``,
``has_nvcc_version`` …) ask about a finer capability of a present device
or toolchain.
"""
import functools
import os
import tvm
__all__ = [
"build_flag_enabled",
"has_adreno_opencl",
# cpu features
"has_cpu_feature",
"has_cublas",
# runtime device
"has_cuda",
# version / capability
"has_cuda_compute",
"has_cudagraph",
# build support
"has_cudnn",
"has_gpu",
# toolchain / environment
"has_hexagon",
"has_hexagon_toolchain",
"has_hipblas",
"has_llvm",
"has_llvm_min_version",
"has_matrixcore",
"has_metal",
"has_multi_gpu",
"has_nccl",
"has_nvcc_version",
"has_nvptx",
"has_nvshmem",
"has_opencl",
"has_rocm",
"has_vulkan",
"has_x86_avx512",
"has_x86_vnni",
]
@functools.cache
def _device_exists(kind: str, index: int = 0) -> bool:
"""Return whether ``tvm.device(kind, index)`` is present and usable."""
try:
return bool(tvm.device(kind, index).exist)
except Exception: # pylint: disable=broad-except
# A missing backend / driver must skip the test, not crash collection.
return False
@functools.cache
def build_flag_enabled(flag: str) -> bool:
"""Return whether an optional build flag (e.g. ``USE_CUTLASS``) is on.
A flag counts as enabled unless it is explicitly disabled, so library
flags carrying a path (rather than a boolean) still register as present.
Callers gate via ``@pytest.mark.skipif(not env.build_flag_enabled("USE_X"), ...)``.
"""
try:
value = tvm.support.libinfo().get(flag, "OFF")
return str(value).lower() not in ("off", "false", "0")
except Exception: # pylint: disable=broad-except
return False
@functools.cache
def _target_enabled(kind: str) -> bool:
"""True if ``kind`` is selected by ``TVM_TEST_TARGETS`` (or the default set).
Honors the ``TVM_TEST_TARGETS`` opt-out, so CI can exclude a flaky
backend (e.g. opencl) via ``TVM_TEST_TARGETS`` and have its tests skip
even when a device is physically present.
"""
try:
from tvm.testing.utils import _tvm_test_targets # pylint: disable=import-outside-toplevel
for target in _tvm_test_targets():
k = target["kind"] if isinstance(target, dict) else str(target).split()[0]
if k == kind:
return True
return False
except Exception: # pylint: disable=broad-except
return True # fail open: the device check still gates
@functools.cache
def _runtime_enabled(kind: str) -> bool:
"""True if the runtime was built with support for target ``kind``.
Used for kinds whose device existence does not imply the backend was
compiled in -- notably ``llvm``, which maps to the always-present CPU
device, so ``tvm.device("llvm").exist`` is True even on ``USE_LLVM=OFF``.
"""
try:
return bool(tvm.runtime.enabled(kind))
except Exception: # pylint: disable=broad-except
return False
def _device_usable(kind: str) -> bool:
"""True if ``kind`` is enabled for this run and a ``kind`` device exists.
The TVM_TEST_TARGETS opt-out is checked first so that an excluded backend
never probes a (possibly crashy) device.
"""
return _target_enabled(kind) and _device_exists(kind)
# --- runtime device probes -------------------------------------------------
def has_cuda() -> bool:
"""True if a CUDA device is present and enabled in TVM_TEST_TARGETS."""
return _device_usable("cuda")
def has_rocm() -> bool:
"""True if a ROCm device is present and enabled in TVM_TEST_TARGETS."""
return _device_usable("rocm")
def has_vulkan() -> bool:
"""True if a Vulkan device is present and enabled in TVM_TEST_TARGETS."""
return _device_usable("vulkan")
def has_metal() -> bool:
"""True if a Metal device is present and enabled in TVM_TEST_TARGETS."""
return _device_usable("metal")
def has_opencl() -> bool:
"""True if an OpenCL device is present and enabled in TVM_TEST_TARGETS."""
return _device_usable("opencl")
def has_nvptx() -> bool:
"""True if NVPTX is usable: a (CUDA) device, plus the LLVM backend it needs."""
return _device_usable("nvptx") and has_llvm()
def has_llvm() -> bool:
"""True if the LLVM backend was built in and enabled in TVM_TEST_TARGETS.
Uses ``tvm.runtime.enabled`` rather than device existence: ``llvm`` maps to
the CPU device, which exists even on a ``USE_LLVM=OFF`` build.
"""
return _target_enabled("llvm") and _runtime_enabled("llvm")
def has_gpu() -> bool:
"""True if any GPU backend (cuda/rocm/opencl/metal/vulkan) is present."""
return (
_device_exists("cuda")
or _device_exists("rocm")
or _device_exists("opencl")
or _device_exists("metal")
or _device_exists("vulkan")
)
@functools.cache
def has_multi_gpu(count: int = 2) -> bool:
"""True if at least ``count`` devices of a single GPU backend exist."""
for kind in ("cuda", "rocm", "opencl", "metal", "vulkan"):
if all(_device_exists(kind, index) for index in range(count)):
return True
return False
# --- build-support probes --------------------------------------------------
#
# These wrap the optional-library build flags. Features that extend CUDA /
# ROCm additionally require the parent device to be present.
def has_cudnn() -> bool:
"""True if cuDNN was built in and a CUDA device is present."""
return has_cuda() and build_flag_enabled("USE_CUDNN")
def has_cublas() -> bool:
"""True if cuBLAS was built in and a CUDA device is present."""
return has_cuda() and build_flag_enabled("USE_CUBLAS")
def has_nccl() -> bool:
"""True if NCCL was built in and a CUDA device is present."""
return has_cuda() and build_flag_enabled("USE_NCCL")
def has_hipblas() -> bool:
"""True if hipBLAS was built in and a ROCm device is present."""
return has_rocm() and build_flag_enabled("USE_HIPBLAS")
@functools.cache
def has_nvshmem() -> bool:
"""True if the disco NVSHMEM runtime is available (requires CUDA).
Probes the runtime global function rather than the ``USE_NVSHMEM`` build
flag, since the flag can be set in builds that do not ship the runtime.
"""
try:
return has_cuda() and (
tvm.get_global_func("runtime.disco.nvshmem.init_nvshmem_uid", allow_missing=True)
is not None
)
except Exception: # pylint: disable=broad-except
return False
# --- version / capability probes -------------------------------------------
@functools.cache
def _cuda_compute_version() -> tuple:
"""Return the (major, minor) CUDA compute version, or (0, 0) if unknown."""
try:
from tvm.support import nvcc # pylint: disable=import-outside-toplevel
arch = nvcc.get_target_compute_version()
return nvcc.parse_compute_version(arch)
except Exception: # pylint: disable=broad-except
return (0, 0)
def has_cuda_compute(major: int, minor: int = 0, exact: bool = False) -> bool:
"""True if the CUDA compute capability satisfies ``(major, minor)``.
When ``exact`` is False (default) the check is ``compute >= (major,
minor)``; when True it requires an exact match. Returns False when no
CUDA device is present, so it implies :func:`has_cuda`.
"""
if not has_cuda():
return False
compute = _cuda_compute_version()
want = (major, minor)
if exact:
return compute == want
return compute >= want
@functools.cache
def _nvcc_version() -> tuple:
"""Return the (major, minor, release) nvcc version, or (0, 0, 0)."""
try:
from tvm.support import nvcc # pylint: disable=import-outside-toplevel
return nvcc.get_cuda_version()
except Exception: # pylint: disable=broad-except
return (0, 0, 0)
def has_nvcc_version(major: int, minor: int = 0, release: int = 0) -> bool:
"""True if a CUDA device is present and nvcc is at least ``(major, minor, release)``.
Returns False when no CUDA device is present, so it implies :func:`has_cuda`.
Gate a test with ``@pytest.mark.skipif(not tvm.testing.env.has_nvcc_version(11, 4),
reason="need nvcc >= 11.4")`` (add ``@pytest.mark.gpu`` for GPU selection).
"""
return has_cuda() and _nvcc_version() >= (major, minor, release)
@functools.cache
def _llvm_version_major() -> int:
"""Return the major LLVM version, or 0 if LLVM is unavailable."""
try:
return int(tvm.target.codegen.llvm_version_major())
except Exception: # pylint: disable=broad-except
return 0
def has_llvm_min_version(major: int) -> bool:
"""True if LLVM is available and its major version is at least ``major``."""
return has_llvm() and _llvm_version_major() >= major
@functools.cache
def has_matrixcore() -> bool:
"""True if a ROCm device with Matrix Core support (compute >= 8) exists."""
try:
from tvm.support import rocm # pylint: disable=import-outside-toplevel
return has_rocm() and bool(rocm.have_matrixcore(tvm.rocm().compute_version))
except Exception: # pylint: disable=broad-except
return False
@functools.cache
def has_cudagraph() -> bool:
"""True if a CUDA device is present and the toolkit supports CUDA Graphs.
Implies :func:`has_cuda`: ``nvcc.have_cudagraph()`` only checks the
toolkit version, so the device guard must be explicit. Gate a test with
``@pytest.mark.skipif(not tvm.testing.env.has_cudagraph(), reason=...)``
(add ``@pytest.mark.gpu`` for CI selection).
"""
try:
from tvm.support import nvcc # pylint: disable=import-outside-toplevel
return has_cuda() and bool(nvcc.have_cudagraph())
except Exception: # pylint: disable=broad-except
return False
# --- toolchain / environment probes ----------------------------------------
@functools.cache
def has_hexagon_toolchain() -> bool:
"""True if the Hexagon toolchain is available for compilation."""
try:
from tvm.contrib.hexagon import ( # pylint: disable=import-outside-toplevel
_ci_env_check,
)
return build_flag_enabled("USE_HEXAGON") and _ci_env_check._compile_time_check() is True
except Exception: # pylint: disable=broad-except
return False
@functools.cache
def has_hexagon() -> bool:
"""True if Hexagon can both compile and run (toolchain + attached device)."""
try:
from tvm.contrib.hexagon import ( # pylint: disable=import-outside-toplevel
_ci_env_check,
)
return has_hexagon_toolchain() and _ci_env_check._run_time_check() is True
except Exception: # pylint: disable=broad-except
return False
@functools.cache
def has_adreno_opencl() -> bool:
"""True if remote Adreno OpenCL testing is configured (RPC_TARGET set)."""
return build_flag_enabled("USE_OPENCL") and os.environ.get("RPC_TARGET") is not None
# --- cpu feature probes ----------------------------------------------------
@functools.cache
def _has_cpu_feature(features) -> bool:
"""True if the host CPU advertises the given LLVM target ``features``."""
try:
codegen = tvm.target.codegen
cpu = codegen.llvm_get_system_cpu()
triple = codegen.llvm_get_system_triple()
target = tvm.target.Target({"kind": "llvm", "mtriple": triple, "mcpu": cpu})
return bool(codegen.target_has_features(features, target))
except Exception: # pylint: disable=broad-except
return False
def has_cpu_feature(features) -> bool:
"""True if the host CPU supports ``features`` (a name or list of names)."""
if isinstance(features, list):
features = tuple(features)
return _has_cpu_feature(features)
def has_x86_vnni() -> bool:
"""True if the host CPU supports x86 VNNI (AVX512-VNNI or AVX-VNNI)."""
return has_cpu_feature("avx512vnni") or has_cpu_feature("avxvnni")
def has_x86_avx512() -> bool:
"""True if the host CPU supports the x86 AVX512 extensions."""
return has_cpu_feature(["avx512bw", "avx512cd", "avx512dq", "avx512vl", "avx512f"])
+65
View File
@@ -0,0 +1,65 @@
# 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.
"""Helpers for tests that use exclusive machine resources."""
import os
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeVar
from tvm_ffi.utils import FileLock
_LOCK_DIR_ENV_VAR = "TVM_TEST_LOCK_DIR"
_LOCK_DIR_NAME = "tvm-testing-locks"
_R = TypeVar("_R")
def _ensure_test_lock_path(filename: str) -> Path:
lock_dir_override = os.environ.get(_LOCK_DIR_ENV_VAR)
if lock_dir_override:
lock_dir = Path(lock_dir_override).expanduser()
else:
lock_dir = Path(tempfile.gettempdir()) / _LOCK_DIR_NAME
lock_dir.mkdir(parents=True, exist_ok=True)
return lock_dir / filename
def run_with_gpu_lock(func: Callable[..., _R], /, *args: Any, **kwargs: Any) -> _R:
"""Run a callable while holding the machine-local GPU lock.
The lock avoids contentious GPU access that may break GPU-related tests.
Parameters
----------
func : Callable
Callable containing the complete live local-GPU lifetime.
*args : Any
Positional arguments forwarded to ``func``.
**kwargs : Any
Keyword arguments forwarded to ``func``.
Returns
-------
result : Any
The return value of ``func``.
"""
lock_path = _ensure_test_lock_path("gpu.lock")
with FileLock(str(lock_path)):
return func(*args, **kwargs)
+81
View File
@@ -0,0 +1,81 @@
# 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, missing-function-docstring
"""Helper functions for popen_pool test cases.
These functions run inside PopenWorker subprocesses and must live in an
importable module (cloudpickle resolves them by module + qualname). The
previous version used FFI helpers (testing.sleep_in_ffi, testing.identity_cpp,
etc.) that were removed with ffi_testing.cc. This version is pure-Python and
uses time.sleep for any blocking needed by the timeout test.
"""
import time
import tvm_ffi
TEST_GLOBAL_STATE_1 = 0
TEST_GLOBAL_STATE_2 = 0
TEST_GLOBAL_STATE_3 = 0
def initializer(test_global_state_1, test_global_state_2, test_global_state_3):
global TEST_GLOBAL_STATE_1, TEST_GLOBAL_STATE_2, TEST_GLOBAL_STATE_3
TEST_GLOBAL_STATE_1 = test_global_state_1
TEST_GLOBAL_STATE_2 = test_global_state_2
TEST_GLOBAL_STATE_3 = test_global_state_3
def after_initializer():
global TEST_GLOBAL_STATE_1, TEST_GLOBAL_STATE_2, TEST_GLOBAL_STATE_3
return TEST_GLOBAL_STATE_1, TEST_GLOBAL_STATE_2, TEST_GLOBAL_STATE_3
@tvm_ffi.register_global_func("testing.identity_py", override=True)
def identity_py(arg):
return arg
def register_ffi():
@tvm_ffi.register_global_func("testing.nested_identity_py", override=True)
def _identity_py(arg): # pylint: disable=unused-variable
return arg
def call_py_ffi(arg):
_identity_py = tvm_ffi.get_global_func("testing.nested_identity_py")
return _identity_py(arg)
def call_cpp_ffi(arg):
import tvm # pylint: disable=import-outside-toplevel
return tvm.testing.echo(arg)
def call_cpp_py_ffi(arg):
# Call the Python-registered identity function through the FFI registry,
# exercising the same cross-language dispatch path that identity_cpp covered.
_identity = tvm_ffi.get_global_func("testing.identity_py")
return _identity(arg)
def timeout_job(seconds):
# Previously called testing.sleep_in_ffi (C++ FFI helper, now removed).
# Plain time.sleep is sufficient — the PopenPoolExecutor timeout mechanism
# watches wall-clock time and terminates the process just the same.
time.sleep(seconds)
+232
View File
@@ -0,0 +1,232 @@
# 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, missing-function-docstring
"""A utility method to run a TVM module on a remote device."""
from collections.abc import Callable
from typing import TYPE_CHECKING, Literal, Optional, Union
if TYPE_CHECKING:
import numpy as np
from tvm.runtime import Device, Module, Tensor
from tvm.s_tir.meta_schedule.runner import EvaluatorConfig, RPCConfig
# pylint: disable=import-outside-toplevel,protected-access
def _args_to_device(args, device):
import numpy as np
from tvm.runtime import Tensor, empty
uploaded_args = []
for arg in args:
if isinstance(arg, np.ndarray | Tensor):
uploaded_args.append(empty(arg.shape, dtype=arg.dtype, device=device).copyfrom(arg))
elif isinstance(arg, int | float):
uploaded_args.append(arg)
else:
raise ValueError(f"Unsupported input type: {type(arg)}")
return uploaded_args
def _args_to_numpy(args):
from tvm.runtime import Tensor
downloaded_args = []
for arg in args:
if isinstance(arg, Tensor):
downloaded_args.append(arg.numpy())
else:
downloaded_args.append(arg)
return downloaded_args
def _normalize_export_func(export_func, output_format) -> tuple[Callable, str]:
from tvm.support import ndk, tar
def export_with(func):
return lambda mod, path: mod.export_library(path, fcompile=func)
if export_func == "tar":
export_func = export_with(tar.tar)
output_format = "tar"
elif export_func == "ndk":
export_func = export_with(ndk.create_shared)
output_format = "so"
elif callable(export_func):
if output_format is None:
raise ValueError("output_format must be specified if `export_func` is callable")
else:
raise ValueError(f"Unsupported export_func: {export_func}")
return export_func, output_format
def local_run( # pylint: disable=too-many-arguments,too-many-locals
mod: "Module",
device_type: str,
args: list[Union["np.ndarray", "Tensor", int, float]],
evaluator_config: Optional["EvaluatorConfig"] = None,
export_func: Callable[["Module", str], None] | Literal["tar", "ndk"] = "tar",
output_format: str | None = None,
):
"""Run a TVM module on a local device.
Parameters
----------
mod : Module
The TVM module to run.
device_type : str
The device type to run the module on.
args : List[Union[np.ndarray, Tensor, int, float]]
The arguments to be fed to the module.
evaluator_config : Optional[EvaluatorConfig]
The evaluator configuration to use.
export_func : Union[Callable[Module, str], Literal["tar", "ndk"]]
The function to export the module to a file.
If callable, it must be a function that takes two arguments: the module to export and the
path to export to.
If "tar", the module will be exported to a tar file.
If "ndk", the module will be exported to a shared library.
output_format : Optional[str]
The format of the exported module.
If not specified, it will be inferred from the `export_func` argument.
Returns
-------
args : List[Union[np.ndarray, Tensor, int, float]]
The results of running the module.
profile_result : tvm.runtime.BenchmarkResult
The profiling result of running the module.
"""
import os.path as osp
import tempfile
from tvm.runtime import device, load_module
from tvm.s_tir.meta_schedule.runner import EvaluatorConfig
evaluator_config = EvaluatorConfig._normalized(evaluator_config)
export_func, output_format = _normalize_export_func(export_func, output_format)
with tempfile.TemporaryDirectory() as tmp_dir:
artifact_path = osp.join(tmp_dir, "tvm_tmp_mod." + output_format)
export_func(mod, artifact_path)
device: Device = device(device_type, 0)
args = _args_to_device(args, device)
remote_mod = load_module(artifact_path)
profile_result = remote_mod.time_evaluator(
func_name=remote_mod.entry_name,
dev=device,
number=evaluator_config.number,
repeat=evaluator_config.repeat,
min_repeat_ms=evaluator_config.min_repeat_ms,
f_preproc="cache_flush_cpu_non_first_arg"
if evaluator_config.enable_cpu_cache_flush
else "",
)(*args)
remote_mod(*args)
args = _args_to_numpy(args)
return args, profile_result
def rpc_run( # pylint: disable=too-many-arguments,too-many-locals
mod: "Module",
device_type: str,
args: list[Union["np.ndarray", "Tensor", int, float]],
evaluator_config: Optional["EvaluatorConfig"] = None,
rpc_config: Optional["RPCConfig"] = None,
export_func: Callable[["Module", str], None] | Literal["tar", "ndk"] = "tar",
output_format: str | None = None,
):
"""Run a TVM module on a remote device.
Parameters
----------
mod : Module
The TVM module to run.
device_type : str
The device type to run the module on.
args : List[Union[np.ndarray, Tensor, int, float]]
The arguments to be fed to the module.
evaluator_config : Optional[EvaluatorConfig]
The evaluator configuration to use.
rpc_config : Optional[RPCConfig]
The RPC configuration to connect to the remote device.
If not specified, the default RPC configuration will be used, which reads the following
environment variables:
- TVM_TRACKER_HOST
- TVM_TRACKER_PORT
- TVM_TRACKER_KEY
export_func : Union[Callable[Module, str], Literal["tar", "ndk"]]
The function to export the module to a file.
If callable, it must be a function that takes two arguments: the module to export and the
path to export to.
If "tar", the module will be exported to a tar file.
If "ndk", the module will be exported to a shared library.
output_format : Optional[str]
The format of the exported module.
If not specified, it will be inferred from the `export_func` argument.
Returns
-------
args : List[Union[np.ndarray, Tensor, int, float]]
The results of running the module.
profile_result : tvm.runtime.BenchmarkResult
The profiling result of running the module.
"""
import os.path as osp
import tempfile
from tvm.s_tir.meta_schedule.runner import EvaluatorConfig, RPCConfig
evaluator_config = EvaluatorConfig._normalized(evaluator_config)
rpc_config = RPCConfig._normalized(rpc_config)
export_func, output_format = _normalize_export_func(export_func, output_format)
with tempfile.TemporaryDirectory() as tmp_dir:
artifact_path = osp.join(tmp_dir, "tvm_tmp_mod." + output_format)
_, remote_path = osp.split(artifact_path)
session = rpc_config.connect_server()
device: Device = session.device(device_type, 0)
export_func(mod, artifact_path)
try:
session.upload(artifact_path, remote_path)
args = _args_to_device(args, device)
remote_mod = session.load_module(remote_path)
profile_result = remote_mod.time_evaluator(
func_name=remote_mod.entry_name,
dev=device,
number=evaluator_config.number,
repeat=evaluator_config.repeat,
min_repeat_ms=evaluator_config.min_repeat_ms,
f_preproc="cache_flush_cpu_non_first_arg"
if evaluator_config.enable_cpu_cache_flush
else "",
)(*args)
remote_mod(*args)
args = _args_to_numpy(args)
finally:
session.remove(remote_path)
session.remove(remote_path + "." + output_format)
session.remove("")
return args, profile_result
+241
View File
@@ -0,0 +1,241 @@
# 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, import-outside-toplevel, unused-variable
"""Common utility functions in TVM tirx"""
def mma_schedule(
workload,
k_inner,
in_dtype,
b_transposed,
i_factors,
j_factors,
k_factors,
index_map_A,
index_map_B,
index_map_C,
ldmatrix_a_intrin,
ldmatrix_b_intrin,
mma_intrin,
mma_fill_intrin,
mma_store_intrin,
shared_scope="shared",
):
"""Create a tensorized schedule for GEMM with MMA intrinsics."""
import tvm # pylint: disable=import-outside-toplevel
ir_module = tvm.IRModule({"main": workload})
sch = tvm.s_tir.Schedule(ir_module)
block = sch.get_sblock("C")
i, j, k = sch.get_loops(block)
i, i_tc = sch.split(i, factors=[None, 16])
j, j_tc = sch.split(j, factors=[None, 16])
k, k_tc = sch.split(k, factors=[None, k_inner])
sch.reorder(i, j, k, i_tc, j_tc, k_tc)
block_inner = sch.blockize(i_tc)
block_outer, block_inner = block_inner, block
num_ty = i_factors[2] * j_factors[2]
i0, i1, i2, i3, i4 = sch.split(i, factors=i_factors)
j0, j1, j2, j3, j4 = sch.split(j, factors=j_factors)
k0, k1, k2 = sch.split(k, k_factors)
sch.reorder(i0, j0, i1, j1, j2, i2, k0, k1, i3, j3, k2, i4, j4)
block_idx = sch.fuse(i0, j0)
block_idy = sch.fuse(i1, j1)
thread_idy = sch.fuse(j2, i2)
sch.bind(block_idx, "blockIdx.x")
sch.bind(block_idy, "blockIdx.y")
sch.bind(thread_idy, "threadIdx.y")
def fetch_to_shared(block, idx, ndim):
block_read = sch.cache_read(block, idx, shared_scope)
sch.compute_at(block_read, k0)
vector_size = 16 if in_dtype == "int8" else 8
warp_size = 32
fused = sch.fuse(*sch.get_loops(block_read)[-ndim:])
_, f_1, f_2, f_3 = sch.split(fused, factors=[None, num_ty, warp_size, vector_size])
sch.bind(f_2, "threadIdx.x")
sch.bind(f_1, "threadIdx.y")
sch.vectorize(f_3)
offset = 8 if in_dtype == "float16" else 16
sch.storage_align(block_read, 0, axis=-2, factor=32, offset=offset)
return block_read
fetch_to_shared(block_outer, 0, 2)
fetch_to_shared(block_outer, 1, 2)
A_warp = sch.cache_read(block_outer, 0, "warp")
B_warp = sch.cache_read(block_outer, 1, "warp")
sch.compute_at(A_warp, k1)
sch.compute_at(B_warp, k1)
C_warp = sch.cache_write(block_outer, 0, "warp")
sch.reverse_compute_at(C_warp, thread_idy)
ii, jj = sch.get_loops(C_warp)[-2:]
io, ii = sch.split(ii, factors=[None, 16])
jo, ji = sch.split(jj, factors=[None, 16])
sch.reorder(io, jo, ii, ji)
sch.decompose_reduction(block_outer, sch.get_loops(block_outer)[3])
block_init_c = sch.get_sblock("C_init")
def tile_wmma_fragment(block_read, height, width):
i, j = sch.get_loops(block_read)[-2:]
i0, i1 = sch.split(i, factors=[None, height])
j0, j1 = sch.split(j, factors=[None, width])
sch.reorder(i0, j0, i1, j1)
return i1
loop_a = tile_wmma_fragment(A_warp, 16, k_inner)
if b_transposed:
loop_b = tile_wmma_fragment(B_warp, 16, k_inner)
else:
loop_b = tile_wmma_fragment(B_warp, k_inner, 16)
sch.transform_layout(A_warp, ("write", 0), index_map_A)
sch.transform_layout(B_warp, ("write", 0), index_map_B)
sch.transform_layout(C_warp, ("read", 0), index_map_C)
sch.tensorize(loop_a, ldmatrix_a_intrin)
sch.tensorize(loop_b, ldmatrix_b_intrin)
sch.tensorize(sch.get_loops(block_inner)[-3], mma_intrin)
sch.tensorize(sch.get_loops(block_init_c)[-2], mma_fill_intrin)
sch.tensorize(sch.get_loops(C_warp)[-2], mma_store_intrin)
return sch
def mfma_schedule(
workload,
k_inner,
in_dtype,
b_transposed,
i_factors,
j_factors,
k_factors,
index_map_A,
index_map_B,
index_map_C,
ldmatrix_a_intrin,
ldmatrix_b_intrin,
mfma_intrin,
mfma_fill_intrin,
mfma_store_intrin,
shared_scope="shared",
):
"""Create a tensorized schedule for GEMM with MFMA intrinsics."""
import tvm
ir_module = tvm.IRModule({"main": workload})
sch = tvm.s_tir.Schedule(ir_module)
wmma_m = 16
wmma_n = 16
wmma_k = k_inner
warp_size = 64
block = sch.get_sblock("C")
i, j, k = sch.get_loops(block)
i, i_tc = sch.split(i, factors=[None, wmma_m])
j, j_tc = sch.split(j, factors=[None, wmma_n])
k, k_tc = sch.split(k, factors=[None, wmma_k])
sch.reorder(i, j, k, i_tc, j_tc, k_tc)
block_inner = sch.blockize(i_tc)
block_outer, block_inner = block_inner, block
num_ty = i_factors[2] * j_factors[2]
i0, i1, i2, i3, i4 = sch.split(i, factors=i_factors)
j0, j1, j2, j3, j4 = sch.split(j, factors=j_factors)
k0, k1, k2 = sch.split(k, k_factors)
sch.reorder(i0, j0, i1, j1, j2, i2, k0, k1, i3, j3, k2, i4, j4)
block_idx = sch.fuse(i0, j0)
block_idy = sch.fuse(i1, j1)
thread_idy = sch.fuse(j2, i2)
sch.bind(block_idx, "blockIdx.x")
sch.bind(block_idy, "blockIdx.y")
sch.bind(thread_idy, "threadIdx.y")
def fetch_to_shared(block, idx, ndim):
block_read = sch.cache_read(block, idx, shared_scope)
sch.compute_at(block_read, k0)
vector_size = 16 if in_dtype == "int8" else 8
fused = sch.fuse(*sch.get_loops(block_read)[-ndim:])
_, f_1, f_2, f_3 = sch.split(fused, factors=[None, num_ty, warp_size, vector_size])
sch.bind(f_2, "threadIdx.x")
sch.bind(f_1, "threadIdx.y")
sch.vectorize(f_3)
return block_read
fetch_to_shared(block_outer, 0, 2)
fetch_to_shared(block_outer, 1, 2)
A_warp = sch.cache_read(block_outer, 0, "warp")
B_warp = sch.cache_read(block_outer, 1, "warp")
sch.compute_at(A_warp, k1)
sch.compute_at(B_warp, k1)
C_warp = sch.cache_write(block_outer, 0, "warp")
sch.reverse_compute_at(C_warp, thread_idy)
ii, jj = sch.get_loops(C_warp)[-2:]
io, ii = sch.split(ii, factors=[None, 16])
jo, ji = sch.split(jj, factors=[None, 16])
sch.reorder(io, jo, ii, ji)
sch.decompose_reduction(block_outer, sch.get_loops(block_outer)[3])
block_init_c = sch.get_sblock("C_init")
def tile_wmma_fragment(block_read, height, width):
i, j = sch.get_loops(block_read)[-2:]
i0, i1 = sch.split(i, factors=[None, height])
j0, j1 = sch.split(j, factors=[None, width])
sch.reorder(i0, j0, i1, j1)
return i1
loop_a = tile_wmma_fragment(A_warp, 16, k_inner)
if b_transposed:
loop_b = tile_wmma_fragment(B_warp, 16, k_inner)
else:
loop_b = tile_wmma_fragment(B_warp, k_inner, 16)
sch.transform_layout(A_warp, ("write", 0), index_map_A)
sch.transform_layout(B_warp, ("write", 0), index_map_B)
sch.transform_layout(C_warp, ("read", 0), index_map_C)
sch.tensorize(loop_a, ldmatrix_a_intrin)
sch.tensorize(loop_b, ldmatrix_b_intrin)
sch.tensorize(sch.get_loops(block_inner)[-3], mfma_intrin)
sch.tensorize(sch.get_loops(block_init_c)[-2], mfma_fill_intrin)
sch.tensorize(sch.get_loops(C_warp)[-2], mfma_store_intrin)
return sch
+960
View File
@@ -0,0 +1,960 @@
# 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: E501
# pylint: disable=invalid-name,unnecessary-comprehension,redefined-outer-name
"""TVM testing utilities
Organization
************
This file contains functions expected to be called directly by a user
while writing unit tests. Integrations with the pytest framework
for TVM's own test suite are in ``tests/python/conftest.py``.
Testing Markers
***************
We use pytest markers to specify the requirements of test functions.
Currently there is a single distinction that matters for our testing
environment: does the test require a gpu. Tests that require a gpu are
tagged with the ``gpu`` pytest marker -- the only registered marker (see
the ``markers`` entry in ``pyproject.toml``). This lets us select the
gpu subset of tests with ``pytest -m gpu`` (and exclude them on cpu-only
nodes with ``pytest -m "not gpu"``).
The ``gpu`` marker only controls which testing node a test runs on; it
does not check whether the required hardware or libraries are actually
present. To gate a test on a specific capability, combine the marker
with a ``skipif`` that consults the memoized environment probes in
:py:mod:`tvm.testing.env`:
.. code-block:: python
@pytest.mark.gpu
@pytest.mark.skipif(not tvm.testing.env.has_cuda(), reason="need cuda")
def test_cuda_vectorize_add():
...
There is one ``has_*`` (or ``is_*``) probe per capability -- for example
:py:func:`tvm.testing.env.has_gpu`, :py:func:`tvm.testing.env.has_cuda`,
and :py:func:`tvm.testing.env.has_vulkan`. For optional Python packages,
prefer ``pytest.importorskip("pkg_name")`` instead of a ``skipif``.
To run a test against a variety of targets, parametrize over ``target`` with
``@pytest.mark.parametrize("target", [...])`` -- tag GPU targets with
``pytest.mark.gpu`` so the CI routes them to GPU nodes, and skip an unavailable
target with ``pytest.mark.skipif(not tvm.testing.device_enabled(target))``. The
set of enabled targets is controlled by the ``TVM_TEST_TARGETS`` environment
variable, so the CI can run different targets on different testing nodes.
"""
import copy
import copyreg
import ctypes
import functools
import inspect
import logging
import os
import pickle
import platform
import runpy
import sys
import time
from pathlib import Path
import ml_dtypes
import numpy as np
import pytest
import tvm
import tvm.arith
import tvm.support.utils
import tvm.te
import tvm.tirx
from tvm.contrib import cudnn
from tvm.support import nvcc
SKIP_SLOW_TESTS = os.getenv("SKIP_SLOW_TESTS", "").lower() in {"true", "1", "yes"}
IS_IN_CI = os.getenv("CI", "") == "true"
_REQUEST_HOOK_INITIALIZERS = {}
skip_if_wheel_test = pytest.mark.skipif(
os.getenv("WHEEL_TEST", "").lower() in {"true", "1", "yes"},
reason="Test not supported in wheel.",
)
def assert_allclose(actual, desired, rtol=1e-7, atol=1e-7, verbose=True):
"""Version of np.testing.assert_allclose with `atol` and `rtol` fields set
in reasonable defaults.
Arguments `actual` and `desired` are not interchangeable, since the function
compares the `abs(actual-desired)` with `atol+rtol*abs(desired)`. Since we
often allow `desired` to be close to zero, we generally want non-zero `atol`.
"""
actual = np.asanyarray(actual)
desired = np.asanyarray(desired)
np.testing.assert_allclose(actual.shape, desired.shape)
np.testing.assert_allclose(actual, desired, rtol=rtol, atol=atol, verbose=verbose)
def check_numerical_grads(
function, input_values, grad_values, function_value=None, delta=1e-3, atol=1e-2, rtol=0.1
):
"""A helper function that checks that numerical gradients of a function are
equal to gradients computed in some different way (analytical gradients).
Numerical gradients are computed using finite difference approximation. To
reduce the number of function evaluations, the number of points used is
gradually increased if the error value is too high (up to 5 points).
Parameters
----------
function
A function that takes inputs either as positional or as keyword
arguments (either `function(*input_values)` or `function(**input_values)`
should be correct) and returns a scalar result. Should accept numpy
ndarrays.
input_values : Dict[str, numpy.ndarray] or List[numpy.ndarray]
A list of values or a dict assigning values to variables. Represents the
point at which gradients should be computed.
grad_values : Dict[str, numpy.ndarray] or List[numpy.ndarray]
Gradients computed using a different method.
function_value : float, optional
Should be equal to `function(**input_values)`.
delta : float, optional
A small number used for numerical computation of partial derivatives.
The default 1e-3 is a good choice for float32.
atol : float, optional
Absolute tolerance. Gets multiplied by `sqrt(n)` where n is the size of a
gradient.
rtol : float, optional
Relative tolerance.
"""
# If input_values is a list then function accepts positional arguments
# In this case transform it to a function taking kwargs of the form {"0": ..., "1": ...}
if not isinstance(input_values, dict):
input_len = len(input_values)
input_values = {str(idx): val for idx, val in enumerate(input_values)}
def _function(_input_len=input_len, _orig_function=function, **kwargs):
return _orig_function(*(kwargs[str(i)] for i in range(input_len)))
function = _function
grad_values = {str(idx): val for idx, val in enumerate(grad_values)}
if function_value is None:
function_value = function(**input_values)
# a helper to modify j-th element of val by a_delta
def modify(val, j, a_delta):
val = val.copy()
val.reshape(-1)[j] = val.reshape(-1)[j] + a_delta
return val
# numerically compute a partial derivative with respect to j-th element of the var `name`
def derivative(x_name, j, a_delta):
modified_values = {
n: modify(val, j, a_delta) if n == x_name else val for n, val in input_values.items()
}
return (function(**modified_values) - function_value) / a_delta
def compare_derivative(j, n_der, grad):
der = grad.reshape(-1)[j]
return np.abs(n_der - der) < atol + rtol * np.abs(n_der)
for x_name, grad in grad_values.items():
if grad.shape != input_values[x_name].shape:
raise AssertionError(
f"Gradient wrt '{x_name}' has unexpected shape {grad.shape}, expected {input_values[x_name].shape} "
)
ngrad = np.zeros_like(grad)
wrong_positions = []
# compute partial derivatives for each position in this variable
for j in range(np.prod(grad.shape)):
# forward difference approximation
nder = derivative(x_name, j, delta)
# if the derivative is not equal to the analytical one, try to use more
# precise and expensive methods
if not compare_derivative(j, nder, grad):
# central difference approximation
nder = (derivative(x_name, j, -delta) + nder) / 2
if not compare_derivative(j, nder, grad):
# central difference approximation using h = delta/2
cnder2 = (
derivative(x_name, j, delta / 2) + derivative(x_name, j, -delta / 2)
) / 2
# five-point derivative
nder = (4 * cnder2 - nder) / 3
# if the derivatives still don't match, add this position to the
# list of wrong positions
if not compare_derivative(j, nder, grad):
wrong_positions.append(np.unravel_index(j, grad.shape))
ngrad.reshape(-1)[j] = nder
wrong_percentage = int(100 * len(wrong_positions) / np.prod(grad.shape))
dist = np.sqrt(np.sum((ngrad - grad) ** 2))
grad_norm = np.sqrt(np.sum(ngrad**2))
if not (np.isfinite(dist) and np.isfinite(grad_norm)):
raise ValueError(
f"NaN or infinity detected during numerical gradient checking wrt '{x_name}'\n"
f"analytical grad = {grad}\n numerical grad = {ngrad}\n"
)
# we multiply atol by this number to make it more universal for different sizes
sqrt_n = np.sqrt(float(np.prod(grad.shape)))
if dist > atol * sqrt_n + rtol * grad_norm:
raise AssertionError(
f"Analytical and numerical grads wrt '{x_name}' differ too much\n"
f"analytical grad = {grad}\n numerical grad = {ngrad}\n"
f"{wrong_percentage}% of elements differ, first 10 of wrong positions: {wrong_positions[:10]}\n"
"distance > atol*sqrt(n) + rtol*grad_norm\n"
f"distance {dist} > {atol}*{sqrt_n} + {rtol}*{grad_norm}"
)
max_diff = np.max(np.abs(ngrad - grad))
avg_diff = np.mean(np.abs(ngrad - grad))
logging.info(
"Numerical grad test wrt '%s' of shape %s passes, "
"dist = %f, max_diff = %f, avg_diff = %f",
x_name,
grad.shape,
dist,
max_diff,
avg_diff,
)
def assert_prim_expr_equal(lhs, rhs):
"""Assert lhs and rhs equals to each iother.
Parameters
----------
lhs : tvm.tirx.Expr
The left operand.
rhs : tvm.tirx.Expr
The left operand.
"""
ana = tvm.arith.Analyzer()
if not ana.can_prove_equal(lhs, rhs):
raise ValueError(f"{lhs} and {rhs} are not equal")
def check_bool_expr_is_true(bool_expr, vranges, cond=None):
"""Check that bool_expr holds given the condition cond
for every value of free variables from vranges.
For example, ``2x > 4y`` solves to ``x > 2y`` given ``x in (0, 10)``
and ``y in (0, 10)``. Here bool_expr is ``x > 2y``,
vranges is ``{x: (0, 10), y: (0, 10)}``, cond is ``2x > 4y``.
We create iterations to check::
for x in range(10):
for y in range(10):
assert !(2x > 4y) || (x > 2y)
Parameters
----------
bool_expr : tvm.ir.Expr
Boolean expression to check
vranges: Dict[tvm.tirx.expr.Var, tvm.ir.Range]
Free variables and their ranges
cond: tvm.ir.Expr
extra conditions needs to be satisfied.
"""
if cond is not None:
bool_expr = tvm.te.any(tvm.tirx.Not(cond), bool_expr)
def _run_expr(expr, vranges):
"""Evaluate expr for every value of free variables
given by vranges and return the tensor of results.
"""
def _compute_body(*us):
vmap = {v: u + r.min for (v, r), u in zip(vranges.items(), us)}
return tvm.tirx.stmt_functor.substitute(expr, vmap)
A = tvm.te.compute([r.extent.value for v, r in vranges.items()], _compute_body)
args = [tvm.runtime.empty(A.shape, A.dtype)]
mod = tvm.compile(tvm.IRModule.from_expr(tvm.te.create_prim_func([A])))
mod(*args)
return args[0].numpy()
res = _run_expr(bool_expr, vranges)
if not np.all(res):
indices = list(np.argwhere(res == 0)[0])
counterex = [(str(v), i + r.min) for (v, r), i in zip(vranges.items(), indices)]
counterex = sorted(counterex, key=lambda x: x[0])
counterex = ", ".join([v + " = " + str(i) for v, i in counterex])
ana = tvm.arith.Analyzer()
raise AssertionError(
f"Expression {ana.simplify(bool_expr)}\nis not true on {vranges}\n"
f"Counterexample: {counterex}"
)
def check_int_constraints_trans_consistency(constraints_trans, vranges=None):
"""Check IntConstraintsTransform is a bijective transformation.
Parameters
----------
constraints_trans : arith.IntConstraintsTransform
Integer constraints transformation
vranges: Dict[tvm.tirx.Var, tvm.ir.Range]
Free variables and their ranges
"""
if vranges is None:
vranges = {}
def _check_forward(constraints1, constraints2, varmap, backvarmap):
ana = tvm.arith.Analyzer()
all_vranges = vranges.copy()
all_vranges.update({v: r for v, r in constraints1.ranges.items()})
# Check that the transformation is injective
cond_on_vars = tvm.tirx.const(1, "bool")
for v in constraints1.variables:
if v in varmap:
# variable mapping is consistent
v_back = ana.simplify(tvm.tirx.stmt_functor.substitute(varmap[v], backvarmap))
cond_on_vars = tvm.te.all(cond_on_vars, v == v_back)
# Also we have to check that the new relations are true when old relations are true
cond_subst = tvm.tirx.stmt_functor.substitute(
tvm.te.all(tvm.tirx.const(1, "bool"), *constraints2.relations), backvarmap
)
# We have to include relations from vranges too
for v in constraints2.variables:
if v in constraints2.ranges:
r = constraints2.ranges[v]
range_cond = tvm.te.all(v >= r.min, v < r.min + r.extent)
range_cond = tvm.tirx.stmt_functor.substitute(range_cond, backvarmap)
cond_subst = tvm.te.all(cond_subst, range_cond)
cond_subst = ana.simplify(cond_subst)
check_bool_expr_is_true(
tvm.te.all(cond_subst, cond_on_vars),
all_vranges,
cond=tvm.te.all(tvm.tirx.const(1, "bool"), *constraints1.relations),
)
_check_forward(
constraints_trans.src,
constraints_trans.dst,
constraints_trans.src_to_dst,
constraints_trans.dst_to_src,
)
_check_forward(
constraints_trans.dst,
constraints_trans.src,
constraints_trans.dst_to_src,
constraints_trans.src_to_dst,
)
def _get_targets(target_names=None):
if target_names is None:
target_names = _tvm_test_targets()
if not target_names:
target_names = DEFAULT_TEST_TARGETS
targets = []
for target in target_names:
if isinstance(target, dict):
target_kind = target["kind"]
else:
target_kind = target.split()[0]
if target_kind == "cuda" and "cudnn" in tvm.target.Target(target).attrs.get("libs", []):
is_enabled = tvm.support.libinfo().get("USE_CUDNN", "OFF").lower() in [
"on",
"true",
"1",
]
is_runnable = is_enabled and cudnn.exists()
elif target_kind == "hexagon":
is_enabled = tvm.support.libinfo().get("USE_HEXAGON", "OFF").lower() in [
"on",
"true",
"1",
]
# If Hexagon has compile-time support, we can always fall back
is_runnable = is_enabled and "ANDROID_SERIAL_NUMBER" in os.environ
else:
is_enabled = tvm.runtime.enabled(target_kind)
is_runnable = is_enabled and tvm.device(target_kind).exist
targets.append(
{
"target": target,
"target_kind": target_kind,
"is_enabled": is_enabled,
"is_runnable": is_runnable,
}
)
if all(not t["is_runnable"] for t in targets):
if tvm.runtime.enabled("llvm"):
logging.warning(
"None of the following targets are supported by this build of TVM: %s."
" Try setting TVM_TEST_TARGETS to a supported target. Defaulting to llvm.",
target_names,
)
return _get_targets(["llvm"])
raise RuntimeError(
"None of the following targets are supported by this build of TVM: %s."
" Try setting TVM_TEST_TARGETS to a supported target."
" Cannot default to llvm, as it is not enabled." % target_names
)
return targets
DEFAULT_TEST_TARGETS = [
"llvm",
"cuda",
"nvptx",
{"kind": "vulkan", "from_device": 0},
"opencl",
{"kind": "opencl", "device": "mali"},
{"kind": "opencl", "device": "intel_graphics"},
"metal",
"rocm",
"hexagon",
]
def device_enabled(target):
"""Check if a target should be used when testing.
Gate a device-specific test on this with
``@pytest.mark.skipif(not tvm.testing.device_enabled(target))``.
This allows the user to control which devices they are testing against. In
tests, this should be used to check if a device should be used when said
device is an optional part of the test.
Parameters
----------
target : str or Dict[str, Any] or tvm.target.Target
Target string to check against
Returns
-------
bool
Whether or not the device associated with this target is enabled.
Example
-------
>>> @pytest.mark.gpu
>>> def test_mytest():
>>> for target in ["cuda", "llvm"]:
>>> if device_enabled(target):
>>> test_body...
Here, `test_body` will only be reached by with `target="cuda"` on gpu test
nodes and `target="llvm"` on cpu test nodes.
"""
if isinstance(target, dict):
target_kind = target["kind"]
elif hasattr(target, "kind"):
target_kind = target.kind.name
else:
assert isinstance(target, str), "device_enabled requires a target as a string"
# Target strings may include extra flags; only compare the kind.
target_kind = target.split(" ")[0]
return any(target_kind == t["target_kind"] for t in _get_targets() if t["is_runnable"])
def enabled_targets():
"""Get all enabled targets with associated devices.
In most cases, parametrize over the specific targets you need with
``@pytest.mark.parametrize`` instead of iterating this function.
In this context, enabled means that TVM was built with support for
this target, the target name appears in the TVM_TEST_TARGETS
environment variable, and a suitable device for running this
target exists. If TVM_TEST_TARGETS is not set, it defaults to
variable DEFAULT_TEST_TARGETS in this module.
If you use this function in a test, you **must** mark the test with
``@pytest.mark.gpu`` (otherwise it will never be run on the gpu).
Returns
-------
targets: list
A list of pairs of all enabled devices and the associated context
"""
return [(t["target"], tvm.device(t["target_kind"])) for t in _get_targets() if t["is_runnable"]]
def _parse_target_entry(entry):
"""Parse a target entry from TVM_TEST_TARGETS env var.
Entries can be plain kind names (e.g. "llvm") or JSON dicts
(e.g. '{"kind": "opencl", "device": "mali"}').
"""
entry = entry.strip()
if entry.startswith("{"):
import json # pylint: disable=import-outside-toplevel
return json.loads(entry)
return entry
def _tvm_test_targets():
target_str = os.environ.get("TVM_TEST_TARGETS", "").strip()
if target_str:
# De-duplicate while preserving order. dict items can't be hashed
# directly, so use their str() form as the dedup key.
targets = []
seen = set()
for t in target_str.split(";"):
t = t.strip()
if not t:
continue
parsed = _parse_target_entry(t)
key = str(parsed)
if key in seen:
continue
seen.add(key)
targets.append(parsed)
return targets
return DEFAULT_TEST_TARGETS
def _compose(args, decs):
"""Helper to apply multiple markers"""
if len(args) > 0:
f = args[0]
for d in reversed(decs):
f = d(f)
return f
return decs
slow = pytest.mark.skipif(
SKIP_SLOW_TESTS,
reason="Skipping slow test since the SKIP_SLOW_TESTS environment variable is 'true'",
)
def skip_if_32bit(reason):
def decorator(*args):
if "32bit" in platform.architecture()[0]:
return _compose(args, [pytest.mark.skip(reason=reason)])
return _compose(args, [])
return decorator
def parameter(*values, ids=None, by_dict=None):
"""Convenience function to define pytest parametrized fixtures.
Declaring a variable using ``tvm.testing.parameter`` will define a
parametrized pytest fixture that can be used by test
functions. This is intended for cases that have no setup cost,
such as strings, integers, tuples, etc. For cases that have a
significant setup cost, please use :py:func:`tvm.testing.fixture`
instead.
If a test function accepts multiple parameters defined using
``tvm.testing.parameter``, then the test will be run using every
combination of those parameters.
The parameter definition applies to all tests in a module. If a
specific test should have different values for the parameter, that
test should be marked with ``@pytest.mark.parametrize``.
Parameters
----------
values : Any
A list of parameter values. A unit test that accepts this
parameter as an argument will be run once for each parameter
given.
ids : List[str], optional
A list of names for the parameters. If None, pytest will
generate a name from the value. These generated names may not
be readable/useful for composite types such as tuples.
by_dict : Dict[str, Any]
A mapping from parameter name to parameter value, to set both the
values and ids.
Returns
-------
function
A function output from pytest.fixture.
Example
-------
>>> size = tvm.testing.parameter(1, 10, 100)
>>> def test_using_size(size):
>>> ... # Test code here
Or
>>> shape = tvm.testing.parameter((5,10), (512,1024), ids=['small','large'])
>>> def test_using_size(shape):
>>> ... # Test code here
Or
>>> shape = tvm.testing.parameter(by_dict={'small': (5,10), 'large': (512,1024)})
>>> def test_using_size(shape):
>>> ... # Test code here
"""
if by_dict is not None:
if values or ids:
raise RuntimeError(
"Use of the by_dict parameter cannot be used alongside positional arguments"
)
ids, values = zip(*by_dict.items())
# Optional cls parameter in case a parameter is defined inside a
# class scope.
@pytest.fixture(params=values, ids=ids, scope="session")
def as_fixture(*_cls, request):
return request.param
return as_fixture
def fixture(func=None, *, cache_return_value=False):
"""Convenience function to define pytest fixtures.
This should be used as a decorator to mark functions that set up
state before a function. The return value of that fixture
function is then accessible by test functions as that accept it as
a parameter.
Fixture functions can accept parameters defined with
:py:func:`tvm.testing.parameter`.
By default, the setup will be performed once for each unit test
that uses a fixture, to ensure that unit tests are independent.
If the setup is expensive to perform, then the
cache_return_value=True argument can be passed to cache the setup.
The fixture function will be run only once (or once per parameter,
if used with tvm.testing.parameter). The cached setup value is
retained for the lifetime of the test process, and each test receives
an independent copy. If the environment variable TVM_TEST_DISABLE_CACHE
is set to a non-zero value, it will disable this feature and no caching
will be performed.
Example
-------
>>> @tvm.testing.fixture
>>> def cheap_setup():
>>> return 5 # Setup code here.
>>>
>>> def test_feature_x(target, dev, cheap_setup)
>>> assert(cheap_setup == 5) # Run test here
Or
>>> size = tvm.testing.parameter(1, 10, 100)
>>>
>>> @tvm.testing.fixture
>>> def cheap_setup(size):
>>> return 5*size # Setup code here, based on size.
>>>
>>> def test_feature_x(cheap_setup):
>>> assert(cheap_setup in [5, 50, 500])
Or
>>> @tvm.testing.fixture(cache_return_value=True)
>>> def expensive_setup():
>>> time.sleep(10) # Setup code here
>>> return 5
>>>
>>> def test_feature_x(target, dev, expensive_setup):
>>> assert(expensive_setup == 5)
"""
force_disable_cache = bool(int(os.environ.get("TVM_TEST_DISABLE_CACHE", "0")))
cache_return_value = cache_return_value and not force_disable_cache
def wraps(func):
if cache_return_value:
func = _fixture_cache(func)
func = pytest.fixture(func, scope="function")
return func
if func is None:
return wraps
return wraps(func)
class _DeepCopyAllowedClasses(dict):
def __init__(self, allowed_class_list):
self.allowed_class_list = allowed_class_list
super().__init__()
def get(self, key, *args, **kwargs):
"""Overrides behavior of copy.deepcopy to avoid implicit copy.
By default, copy.deepcopy uses a dict of id->object to track
all objects that it has seen, which is passed as the second
argument to all recursive calls. This class is intended to be
passed in instead, and inspects the type of all objects being
copied.
Where copy.deepcopy does a best-effort attempt at copying an
object, for unit tests we would rather have all objects either
be copied correctly, or to throw an error. Classes that
define an explicit method to perform a copy are allowed, as
are any explicitly listed classes. Classes that would fall
back to using object.__reduce__, and are not explicitly listed
as safe, will throw an exception.
"""
obj = ctypes.cast(key, ctypes.py_object).value
cls = type(obj)
if (
cls in copy._deepcopy_dispatch
or issubclass(cls, type)
or getattr(obj, "__deepcopy__", None)
or copyreg.dispatch_table.get(cls)
or cls.__reduce__ is not object.__reduce__
or cls.__reduce_ex__ is not object.__reduce_ex__
or cls in self.allowed_class_list
):
return super().get(key, *args, **kwargs)
rfc_url = (
"https://github.com/apache/tvm-rfcs/blob/main/rfcs/0007-parametrized-unit-tests.md"
)
raise TypeError(
f"Cannot copy fixture of type {cls.__name__}. TVM fixture caching "
"is limited to objects that explicitly provide the ability "
"to be copied (e.g. through __deepcopy__, __getstate__, or __setstate__),"
"and forbids the use of the default `object.__reduce__` and "
"`object.__reduce_ex__`. For third-party classes that are "
"safe to use with copy.deepcopy, please add the class to "
"the arguments of _DeepCopyAllowedClasses in tvm.testing._fixture_cache.\n"
"\n"
f"For discussion on this restriction, please see {rfc_url}."
)
def _fixture_cache(func):
cache = {}
# Using functools.lru_cache would require the function arguments
# to be hashable, which wouldn't allow caching fixtures that
# depend on numpy arrays. For example, a fixture that takes a
# numpy array as input, then calculates uses a slow method to
# compute a known correct output for that input. Therefore,
# including a fallback for serializable types.
def get_cache_key(*args, **kwargs):
try:
hash((args, kwargs))
return (args, kwargs)
except TypeError:
pass
try:
return pickle.dumps((args, kwargs))
except TypeError as e:
raise TypeError(
"TVM caching of fixtures requires arguments to the fixture "
"to be either hashable or serializable"
) from e
@functools.wraps(func)
def wrapper(*args, **kwargs):
cache_key = get_cache_key(*args, **kwargs)
try:
cached_value = cache[cache_key]
except KeyError:
cached_value = cache[cache_key] = func(*args, **kwargs)
return copy.deepcopy(
cached_value,
# allowed_class_list should be a list of classes that
# are safe to copy using copy.deepcopy, but do not
# implement __deepcopy__, __reduce__, or
# __reduce_ex__.
_DeepCopyAllowedClasses(allowed_class_list=[]),
)
return wrapper
def identity_after(x, sleep):
"""Testing function to return identity after sleep
Parameters
----------
x : int
The input value.
sleep : float
The amount of time to sleep
Returns
-------
x : object
The original value
"""
if sleep:
time.sleep(sleep)
return x
def terminate_self():
"""Testing function to terminate the process."""
sys.exit(-1)
def is_ampere_or_newer():
"""Check if the target environment has an NVIDIA Ampere GPU or newer."""
arch = nvcc.get_target_compute_version()
major, minor = nvcc.parse_compute_version(arch)
return major >= 8 and minor != 9
def install_request_hook(hook_script: Path) -> None:
"""Add a wrapper around urllib.request for CI tests."""
if not IS_IN_CI:
return
hook_script = Path(hook_script).resolve()
if not hook_script.is_file():
raise RuntimeError(f"Request hook {hook_script} does not exist")
# Load the exact hook file without exposing the test root as an import path.
# Cache its initializer because Sphinx invokes this once per gallery example.
try:
init = _REQUEST_HOOK_INITIALIZERS[hook_script]
except KeyError:
init = _REQUEST_HOOK_INITIALIZERS[hook_script] = runpy.run_path(str(hook_script))["init"]
init()
def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
'val' is anything else.
"""
val = val.lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return 1
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
raise ValueError(f"invalid truth value {val!r}")
def main():
test_file = inspect.getsourcefile(sys._getframe(1))
sys.exit(pytest.main([test_file, *sys.argv[1:]]))
ml_dtypes_dict = {
"float8_e4m3fn": ml_dtypes.float8_e4m3fn,
"float8_e5m2": ml_dtypes.float8_e5m2,
"bfloat16": ml_dtypes.bfloat16,
"int4": ml_dtypes.int4,
}
def np_dtype_from_str(dtype: str) -> np.dtype:
"""Convert a string dtype to a numpy dtype."""
return np.dtype(ml_dtypes_dict[dtype]) if dtype in ml_dtypes_dict else np.dtype(dtype)
def generate_random_array(dtype: str, shape: tuple) -> np.ndarray:
"""
Generate a random array by generating random bits and casting to the target dtype.
Supported dtypes:
- "int8", "uint8", "float16", "float32", "bfloat16", "float8_e4m3fn", "float8_e5m2"
"""
try:
np_dtype = np_dtype_from_str(dtype)
except TypeError:
raise ValueError("Provided dtype is not a valid numpy dtype.")
# Determine the bit length for this dtype.
bit_length = np_dtype.itemsize * 8
# Choose an appropriate unsigned container type.
if bit_length <= 8:
container = np.uint8
elif bit_length <= 16:
container = np.uint16
elif bit_length <= 32:
container = np.uint32
elif bit_length <= 64:
container = np.uint64
else:
raise ValueError(f"Unsupported dtype bit length: {bit_length}")
# Generate random integers in the full range of the bit length.
random_ints = np.random.randint(0, 2**bit_length, size=shape, dtype=container)
# Reinterpret the bit pattern as the desired dtype.
res = random_ints.view(np_dtype)
with np.errstate(invalid="ignore"):
invalid_indices = np.where(~np.isfinite(res))
for idx in zip(*invalid_indices):
while True:
with np.errstate(invalid="ignore"):
if np.isfinite(res[idx]):
break
# Generate a new random value for this specific position
new_random_int = np.random.randint(0, 2**bit_length, size=1, dtype=container)
res[idx] = new_random_int.view(np_dtype)[0]
return res