代码指南和提示

本文档用于记录 TVM 代码库中供审查者和贡献者参考的提示。其中大多数是通过贡献和流程中的经验教训总结出来的。

C++ 代码风格

  • 使用 Google C/C++ 风格。

  • 公共接口函数以 doxygen 格式记录。

  • 如果类型简短,则倾向于使用具体类型声明而不是 auto

  • 倾向于通过常量引用传递(例如 const Expr&)而不是按值传递。除非函数通过复制构造函数或移动来消耗该值,在这种情况下,按值传递比按常量引用传递更好。

  • 尽可能倾向于使用 const 成员函数。

我们使用 clang-format 来强制执行代码风格。由于不同版本的 clang-format 可能会因其版本而更改,建议使用与主版本相同的 clang-format 版本。您也可以通过 docker 使用以下命令。

# Run a specific file through clang-format
docker/bash.sh ci_lint clang-format-10 [path-to-file]

# Run all linters, including clang-format
python tests/scripts/ci.py lint

clang-format 也不是完美的,必要时,您可以在某些代码区域禁用 clang-format。

// clang-format off
void Test() {
   // clang-format will be disabled in this region.
}
// clang-format on

由于 clang-format 可能无法识别宏,建议像普通函数风格一样使用宏。

#define MACRO_IMPL { custom impl; }
#define MACRO_FUNC(x)

// not preferred, because clang-format might recognize it as types.
virtual void Func1() MACRO_IMPL

// preferred
virtual void Func2() MACRO_IMPL;

void Func3() {
  // preferred
  MACRO_FUNC(xyz);
}

Python 代码风格

  • 函数和类以 numpydoc 格式记录。

  • 使用 python tests/scripts/ci.py lint 检查您的代码风格

  • 坚持使用 python 3.7 中的语言特性

  • 对于具有提前返回的函数,对于条件体并行且简短的函数,例如对参数应用简单映射的函数,优先使用 if/elif/else 链。对于更过程化的函数,特别是当最终的 else 块比 ifelif 块长得多时,优先选择不缩进最终的 else 情况。

    禁用 pylint 检查 no-else-return 以允许这种区分。请参阅 此处 <https://github.com/apache/tvm/pull/11327> 的进一步讨论。

    # All cases have bodies with similar flow control.  While this could
    # be expressed as a sequence of if conditions, a reader would need to
    # inspect the body of each condition to know that only one conditional
    # body may be reached.
    def sign(x):
        if x > 0:
            return "+"
        elif x < 0:
            return "-"
        else:
            return ""
    
    # The initial special case is an early return for a special case,
    # followed by a more general method.  Using an else block for the
    # condition would add unnecessary indentation for the remainder of the
    # function.
    def num_unique_subsets(values):
        if len(values)==0:
            return 1
    
        # Longer, more general solution here
        ...
    

编写 Python 测试

我们使用 pytest 进行所有 python 测试。tests/python 包含所有测试。

如果您希望您的测试在各种目标上运行,请使用 tvm.testing.parametrize_targets() 装饰器。例如

@tvm.testing.parametrize_targets
def test_mytest(target, dev):
  ...

将使用 target="llvm"target="cuda" 和其他一些目标运行 test_mytest。 这也确保您的测试由 CI 在正确的硬件上运行。如果您只想针对几个目标进行测试,请使用 @tvm.testing.parametrize_targets("target_1", "target_2")。 如果您想在单个目标上进行测试,请使用 tvm.testing() 中的关联装饰器。 例如,CUDA 测试使用 @tvm.testing.requires_cuda 装饰器。

网络资源

在 CI 中,从互联网下载文件是导致测试失败不稳定的一个重要来源(例如,远程服务器可能会宕机或速度缓慢),因此请尽量避免在测试期间使用网络。在某些情况下,这并非合理的建议(例如,需要下载模型的文档教程)。

在这些情况下,您可以将文件重新托管在 S3 中,以便在 CI 中快速访问。committer 可以使用 upload_ci_resource.yml GitHub Actions 工作流程 上的 workflow_dispatch 事件上传由名称、哈希和 S3 中的路径指定的文件。sha256 必须与文件匹配,否则将不会上传。上传路径是用户定义的,因此它可以是任何路径(不允许尾部或前导斜杠),但请注意不要意外地与现有资源冲突。上传后,您应该发送 PR 以使用新 URL 更新 request_hook.py 中的 URL_MAP

处理整数常量表达式

我们经常需要在 TVM 中处理常量整数表达式。在此之前,我们要问的第一个问题是,是否有必要获取常量整数。如果符号表达式也有效并让逻辑流动,我们应该尽可能多地使用符号表达式。这样生成的代码适用于事先未知的形状。

请注意,在某些情况下,我们无法知道某些信息,例如符号变量的符号,在某些情况下做出假设是可以的。同时为变量是常量的情况添加精确的支持。

如果我们必须获取常量整数表达式,我们应该使用 int64_t 类型而不是 int 获取常量值,以避免潜在的整数溢出。我们始终可以通过 make_const 使用相应的表达式类型重建整数。以下代码给出了一个示例。

Expr CalculateExpr(Expr value) {
  int64_t int_value = GetConstInt<int64_t>(value);
  int_value = CalculateExprInInt64(int_value);
  return make_const(value.type(), int_value);
}