代码指南与技巧

本文档用于记录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已被禁用以支持这种区分。更多讨论请参见这里

    # 所有情况都具有相似流程控制的主体。虽然可以用一系列if条件表达,但读者需要检查每个条件的函数体才能知道只会执行一个条件分支。
    def sign(x):
        if x > 0:
            return "+"
        elif x < 0:
            return "-"
        else:
            return ""
    
    # 初始特殊情况是特殊情况的提前返回,后接更通用的方法。对该条件使用else块会给函数剩余部分添加不必要的缩进。
    def num_unique_subsets(values):
        if len(values)==0:
            return 1
    
        # 此处是更长、更通用的解决方案
        ...
    

编写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中的快速访问。提交者可以通过upload_ci_resource.yml GitHub Actions工作流上的workflow_dispatch事件上传文件,该文件由名称、哈希值和S3中的路径指定。sha256必须与文件匹配,否则将不会上传。上传路径是用户自定义的,因此可以是任何路径(不允许包含前导或尾随斜杠),但请注意不要意外与现有资源冲突。上传后,您应该提交一个PR来更新request_hook.py中的URL_MAP,以包含新的URL。

处理整数常量表达式

在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);
}