Python 目标参数化

总结

对于任何支持的运行时,TVM 都应产生数值上正确的结果。因此,在编写验证数值输出的单元测试时,这些单元测试应在所有支持的运行时上运行。由于这是一个非常常见的用例,TVM 具有辅助函数来参数化单元测试,以便它们将在所有已启用且具有兼容设备的 targets 上运行。

测试套件中的单个 Python 函数可以扩展为多个参数化的单元测试,每个测试都测试单个目标设备。为了运行测试,必须满足以下所有条件。

  • 测试存在于已传递给 pytest 的文件或目录中。

  • 应用于函数的 pytest 标记(显式或通过目标参数化)必须与传递给 pytest 的 -m 参数的表达式兼容。

  • 对于使用 target fixture 的参数化测试,目标必须出现在环境变量 TVM_TEST_TARGETS 中。

  • 对于使用 target fixture 的参数化测试,config.cmake 中的构建配置必须启用相应的运行时。

单元测试文件内容

在多个目标上运行测试的推荐方法是通过参数化测试。这可以通过使用 @tvm.testing.parametrize_targets('target_1', 'target_2', ...) 进行装饰,并接受 targetdev 作为函数参数来显式完成。该函数将为列出的每个目标运行一次,并且每个目标的成功/失败将单独报告。如果由于目标在 config.cmake 中被禁用,或者由于没有合适的硬件而无法运行目标,则该目标将被报告为跳过。

# Explicit listing of targets to use.
@tvm.testing.parametrize_target('llvm', 'cuda')
def test_function(target, dev):
    # Test code goes here

对于应在所有目标上正确运行的测试,可以省略装饰器。任何接受 targetdev 参数的测试都将自动在 TVM_TEST_TARGETS 中指定的所有目标上进行参数化。参数化为每个目标提供相同的通过/失败/跳过报告,同时允许测试套件轻松扩展以覆盖其他目标。

# Implicitly parametrized to run on all targets
# in environment variable TVM_TEST_TARGETS
def test_function(target, dev):
    # Test code goes here

@tvm.testing.parametrize_targets 也可以用作裸装饰器,以显式引起对参数化的注意,但没有其他效果。

# Explicitly parametrized to run on all targets
# in environment variable TVM_TEST_TARGETS
@tvm.testing.parametrize_targets
def test_function(target, dev):
    # Test code goes here

可以使用 @tvm.testing.exclude_targets@tvm.testing.known_failing_targets 装饰器排除或标记特定目标为预期失败。有关其预期用例的更多信息,请参阅其文档字符串。

在某些情况下,可能有必要跨多个参数进行参数化。例如,可能存在应测试的特定于目标的实现,其中某些目标具有多个实现。这些可以通过显式地对参数元组进行参数化来完成,如下所示。在这些情况下,仅会运行显式列出的目标,但它们仍将应用适当的 @tvm.testing.requires_RUNTIME 标记。

@pytest.mark.parametrize('target,impl', [
     ('llvm', cpu_implementation),
     ('cuda', gpu_implementation_small_batch),
     ('cuda', gpu_implementation_large_batch),
 ])
 def test_function(target, dev, impl):
     # Test code goes here

参数化功能是在 pytest 标记之上实现的。可以使用 pytest 标记装饰每个测试函数以包含元数据。最常应用的标记如下。

  • @pytest.mark.gpu - 将函数标记为使用 GPU 功能。这本身没有效果,但可以与命令行参数 -m gpu-m 'not gpu' 配对,以限制 pytest 将执行哪些测试。不应单独调用此项,但它是单元测试中使用的其他标记的一部分。

  • @tvm.testing.uses_gpu - 应用 @pytest.mark.gpu。如果存在 GPU,则应使用此项来标记可能使用 GPU 的单元测试。此装饰器仅对于显式循环遍历 tvm.testing.enabled_targets() 的测试是必需的,但这不再是编写单元测试的首选样式(请参阅下文)。当使用 tvm.testing.parametrize_targets() 时,对于 GPU 目标,此装饰器是隐式的,不需要显式应用。

  • @tvm.testing.requires_gpu - 应用 @tvm.testing.uses_gpu,并且如果不存在 GPU,则另外标记测试应完全跳过 (@pytest.mark.skipif)。

  • @tvfm.testing.requires_RUNTIME - 多个装饰器(例如 @tvm.testing.requires_cuda),每个装饰器都会在无法使用指定的运行时时跳过测试。如果运行时在 config.cmake 中被禁用,或者如果不存在兼容的设备,则无法使用该运行时。对于使用 GPU 的运行时,这包括 @tvm.testing.requires_gpu

当使用参数化目标时,每个测试运行都使用与正在使用的目标相对应的 @tvm.testing.requires_RUNTIME 进行装饰。因此,如果目标在 config.cmake 中被禁用或没有合适的硬件来运行,它将被显式列为跳过。

还存在一个 tvm.testing.enabled_targets(),它基于环境变量 TVM_TEST_TARGETS、构建配置和物理硬件,返回当前机器上已启用且可运行的所有目标。当前大多数测试都显式循环遍历从 enabled_targets() 返回的目标,但不应将其用于新测试。此样式的 pytest 输出会静默跳过在 config.cmake 中禁用的运行时,或者没有可以运行它们的设备的运行时。此外,测试会在第一个失败的目标上停止,这对于错误是发生在特定目标上还是每个目标上是模棱两可的。

# Old style, do not use.
def test_function():
    for target,dev in tvm.testing.enabled_targets():
        # Test code goes here

本地运行

要在本地运行 Python 单元测试,请在 ${TVM_HOME} 目录中使用命令 pytest

  • 环境变量
    • TVM_TEST_TARGETS 应该是要运行的目标的分号分隔列表。如果未设置,则默认为 tvm.testing.DEFAULT_TEST_TARGETS 中定义的目标。

      注意:如果 TVM_TEST_TARGETS 不包含任何已启用且具有该类型可访问设备的目标,则测试将回退到仅在 llvm 目标上运行。

    • TVM_LIBRARY_PATH 应该是 libtvm.so 库的路径。例如,这可以用于使用调试版本运行测试。如果未设置,则将相对于 TVM 源代码目录搜索 libtvm.so

  • 命令行参数

    • 传递文件夹或文件的路径将仅运行该文件夹或文件中的单元测试。例如,这对于避免在没有安装特定前端的系统上运行 tests/python/frontend 中的测试很有用。

    • -m 参数仅运行使用特定 pytest 标记标记的单元测试。最常见的用法是使用 m gpu 仅运行标记为 @pytest.mark.gpu 并使用 GPU 运行的测试。它也可以用于仅运行不使用 GPU 的测试,方法是传递 m 'not gpu'

      注意:此筛选发生在基于 TVM_TEST_TARGETS 环境变量选择目标之后。即使指定了 -m gpu,如果 TVM_TEST_TARGETS 不包含 GPU 目标,则不会运行任何 GPU 测试。

在本地 Docker 容器中运行

docker/bash.sh 脚本可用于在与 CI 使用的 Docker 镜像相同的镜像内运行单元测试。第一个参数应指定要运行的 Docker 镜像(例如 docker/bash.sh ci_gpu)。允许的镜像名称在 TVM 源代码目录中的 Jenkinsfile 顶部定义,并映射到 tlcpack 中的镜像。

如果未提供其他参数,则将加载带有交互式 bash 会话的 Docker 镜像。如果脚本作为可选参数传递(例如 docker/bash.sh ci_gpu tests/scripts/task_python_unittest.sh),则该脚本将在 Docker 镜像内执行。

注意:Docker 镜像包含所有系统依赖项,但不包含这些系统的 build/config.cmake 配置文件。TVM 源代码目录用作 Docker 镜像的主目录,因此这将默认为使用与本地配置相同的 config/build 目录。一种解决方案是维护单独的 build_localbuild_docker 目录,并在进入/退出 Docker 时从 build 建立到相应文件夹的符号链接。

在 CI 中运行

CI 中的所有内容都从 Jenkinsfile 中存在的任务定义开始。这包括定义使用哪个 Docker 镜像、编译时配置是什么以及哪些测试包含在哪些阶段中。

  • Docker 镜像

    Jenkinsfile 的每个任务(例如“BUILD: CPU”)都调用 docker/bash.sh。docker/bash.sh 调用后面的参数定义 CI 中的 Docker 镜像,就像在本地一样。

  • 编译时配置

    Docker 镜像没有内置 config.cmake 文件,因此这是每个 BUILD 任务中的第一步。这是使用 tests/scripts/task_config_build_*.sh 脚本完成的。使用哪个脚本取决于正在测试的构建,并在 Jenkinsfile 中指定。

    每个 BUILD 任务都通过打包库以供以后测试使用来结束。

  • 运行哪些测试

    Jenkinsfile 的 Unit TestIntegration Test 阶段确定如何调用 pytest。每个任务首先解压缩先前在 BUILD 阶段中编译的已编译库,然后运行测试脚本(例如 tests/script/task_python_unittest.sh)。这些脚本设置传递给 pytest 的文件/文件夹和命令行选项。

    其中几个脚本包含 -m gpu 选项,该选项将测试限制为仅运行包含 @pytest.mark.gpu 标记的测试。