惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
A
Arctic Wolf
S
Securelist
O
OpenAI News
T
Threatpost
Forbes - Security
Forbes - Security
N
News and Events Feed by Topic
S
Secure Thoughts
H
Heimdal Security Blog
S
Security Affairs
P
Privacy International News Feed
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
T
Tailwind CSS Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hacker News: Ask HN
Hacker News: Ask HN
T
Troy Hunt's Blog
S
SegmentFault 最新的问题
腾讯CDC
V
Visual Studio Blog
Last Week in AI
Last Week in AI
H
Hacker News: Front Page
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Project Zero
Project Zero
WordPress大学
WordPress大学
NISL@THU
NISL@THU
博客园 - 【当耐特】
博客园 - Franky
Webroot Blog
Webroot Blog
博客园_首页
T
Tenable Blog
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
阮一峰的网络日志
阮一峰的网络日志
V2EX - 技术
V2EX - 技术
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
The Hacker News
The Hacker News

Пусть этот камень будет более крепким, чем человек

【琐记】烟火与尘埃 【Triton】Triton实现矩阵乘 【LLM推理加速】FlashAttention 【LLM推理加速】PagedAttention 【LLM推理加速】Online Softmax LLM基础知识【1】 Transformer模型 【AI编译】LayerGroup Tiling Tile的疑惑和思考 【AI编译】深度优先的Tile调度,万事大吉? 【AI编译】多级流水线Tile调度策略 【CUDA C++】GPU内存使用【3】 【AI编译】Cache缓存地址映射 【CUDA C++】GPU存储【2】 【CUDA C++】GPU基本介绍【1】 【00】0序章-不受欢迎的来客 【转载】我来了——持续低熵 【Halide】调度优化【2】 【感想】写作进度报告5 【Halide】调度优化【1】 【转载】北大中文男足战报2 【BYOC】TVM切分子图 【转载】北大中文男足战报1 【AI编译】张量生命周期管理 SystemC 用寄存器同步建模方法 【脉动阵列】脉动阵列类型 【im2col】AScend conv accelerate 【感想】写作进度报告4 【BYOC】TVM添加自定义编译器 ccompiler 【感想】写作进度报告3 【Tengine】推理流程脑图【2】 【Tengine】推理流程脑图【1】 【NCNN】学习ncnn模型转换 【编译器】使用llvm编译自定义语言【3】编译 object 【编译器】使用llvm编译自定义语言【2】转llvm IR 【编译器】使用llvm编译自定义语言【1】构建AST 【AI编译】如何进行内存分配 【感想】写作进度报告2 【AI编译】layer-group之后如何tiling 【AI编译】如何进行layer-group 【量化】连续卷积层首尾量化的可行性 【Gemm】内存对齐 【gemm】Gemm计算加速 【TVM】通过代码学习编译流程【5】FuseOps 【TVM】通过代码学习编译流程【6】CodeGen 【TVM】通过代码学习类【3.5】Pass 【TVM】通过代码学习编译流程【4】BuildRelay 【AI编译】Tiling操作能优化什么时间 【TVM】通过代码学习编译流程【3】模型编译 【TVM】通过代码学习编译流程【2】模型转换 【TVM】通过代码学习编译流程【1】必要知识 【感想】写作进度报告1 【Winograd】卷积加速算法原理及实现 SystemC 等待异步事件解决方案 【TVM】Python脚本实现模型编译和保存 【推理引擎】常见AI推理框架 【3D建模】T110E3卡迪夫蓝调皮肤模型 【TVM】C++部署运行TVM 【推理引擎】NCNN和Tengine量化推理逻辑对比 【3D建模】IS-7攻城锤流纹岩皮肤展示 博客汇总目录 【Im2Col】卷积加速算法【2】NHWC 【Im2Col】卷积加速算法【1】 NCHW openBlas库的安装与简单使用 C语言工程调用Cpp库解决方案 foo Hello World
【TVM】根据例子走通代码库
Post author: XianMu@Пусть этот камень будет более крепким, чем ч · 2024-05-24 · via Пусть этот камень будет более крепким, чем человек

# 前言

最近开始学习 TVM。感觉 TVM 英文文档中 TVM Codebase Walkthrough by Example 一节对于理解 TVM 工程非常有用。本篇文章只是翻译,可以直接跳转查看英文全文。

这个时代有这么多愿意开源并将技术介绍给我们的行业大牛,真是我们的荣幸,膜拜!
------ 大家好啊 我是 暮冬 Z 羡慕

# Codebase Structure Overview

在 TVM 存储库的根目录中,我们有以下子目录,它们共同构成了大部分代码库。

  • src
    C++ code for operator compilation and deployment runtimes.
    算子编译 、 runtime 部署 的 C++ 代码
  • src/relay
    Implementation of Relay, a new functional IR for deep learning framework.
    Relay IR 的实现 算子的映射关系在 src/relay/op
  • python
    Python frontend that wraps C++ functions and objects implemented in src.
    python 前端
  • src/topi
    Compute definitions and backend schedules for standard neural network operators.
    计算标准神经网络算子的定义和后端调度

TVM 中 Python 和 C++ 的互操作性不是单向的。尽管在 TVM 中 C++ 完成繁重的内部执行工作,Python 完成用户接口, TVM 中也存在 C++ 调用 Python 的情况:For example, the convolution operator is implemented in Python, and its implementation is invoked from C++ code in Relay.(Relay 中的 C++ 调用 Python 实现的卷积算子)

# Vector Add Example

使用 vector add 的例子来查看底层 TVM API.

n = 1024
A = tvm.te.placeholder((n,), name='A')
B = tvm.te.placeholder((n,), name='B')
C = tvm.te.compute(A.shape, lambda i: A[i] + B[i], name="C")

这里面 A、B、C 都是 tvm.tensor.Tensor 其 Python 定义位于 python/tvm/te/tensor.py . 支撑的 C++ 定义位于 include/tvm/te/tensor.hsrc/te/tensor.cc 所有的 Python 类型定义都能找到对应的相同名字的 C++ 定义。

Python 对 C++ 的包装位于 python/tvm/_ffi/

一个 Tensor 包含一个 Operation 类,定义于 python/tvm/te/tensor.py,对应的 C++ 实现位于 include/tvm/te/operation.hsrc/tvm/te/operationTensorOperation 类的输出。

我们将输出张量 C 对应的操作传递给 tvm.te.create_schedule() 函数 (来自于 python/tvm/te/schedule.py 。)

s = tvm.te.create_schedule(C.op)

这个函数映射到 C++ 函数 include/tvm/schedule.h

inline Schedule create_schedule(Array<Operation> ops) {
  return Schedule(ops);
}

Schedule 包含 Stage 输出 Operation 的集合。

Stage 对应于一个操作 Operation 。上面的 vector add 操作中有两个 placeholder ops 和一个 compute op. 所以 Schedule s 有三个状态 Stage ,每个 Stage 持有以下信息: 循环嵌套结构、每个循环的类型( Parallel,Vectorized,Unrolled )、以及在下一个循环嵌套 Stage 中在哪里执行它自己的计算。

ScheduleStage 本身定义在 tvm/python/te/schedule.pyinclude/tvm/te/schedule.h , 和 src/te/schedule/schedule_ops.cc

为简单起见,我们使用 tvm.build(...) 处理上方 create_schedule() 函数创建的默认 Schedule s 和 <em>。我们必须添加必要的线程绑定,来使得其能在 GPU 上运行:

target = "cuda"
bx, tx = s[C].split(C.op.axis[0], factor=64)
s[C].bind(bx, tvm.te.thread_axis("blockIdx.x"))
s[C].bind(tx, tvm.te.thread_axis("threadIdx.x"))
fadd = tvm.build(s, [A, B, C], target)

tvm.build(...) ,定义在 python/tvm/driver/build_module.py , 需要输入一个 Schedule ; input , output Tensor ; 以及一个 target 。返回一个 tvm.runtime.Module

整个 tvm.build(...) 过程可以分成两步:

  • i. 降级 高级的、初始的循环嵌套结构被转换为 最终的、低级的 IR

  • ii. 代码生成 low level IR 生成目标机器码

降级是通过 tvm.lower() 函数完成的,它定义在 python/tvm/build\_module.py 。第一,指定绑定推理,一个最初的循环嵌套结构就创建好了。

def lower(sch,
          args,
          name="default_function",
          binds=None,
          simple_mode=False):
   ...
   bounds = schedule.InferBound(sch)
   stmt = schedule.ScheduleOps(sch, bounds)
   ...

边界推断是推断所有循环边界和中间缓冲区大小的过程。如果你的目标是 CUDA,且你用了 share memory,它需要的最小 size 在此处确定。绑定推理时在 src/te/schedule/bound.cc,src/te/schedule/graph.cc src/te/schedule/message\_passing.cc 中实现的。

stmtScheduleOps() 的输出,表示一个初识的循环嵌套结构。如果在 schedule 中已经应用了 reordersplit 原语,那么初始的循环嵌套结构已经反映了这些变化。 ScheduleOps() 定义在 rc/te/schedule/schedule_ops.cc

接下来应用一些 lowering passes to stmt . 这些 passes 在 src/tir/pass 子文件夹下实现。举个例子,如果在你的 schedule 中应用了 vectorize 或者 unroll 原语,他们会被应用到循环 vectorization 和 unrolling passes。

...
stmt = ir_pass.VectorizeLoop(stmt)
...
stmt = ir_pass.UnrollLoop(
    stmt,
    cfg.auto_unroll_max_step,
    cfg.auto_unroll_max_depth,
    cfg.auto_unroll_max_extent,
    cfg.unroll_explicit)
...

在降级 lowering 结束后, build() 函数生成目标机器代码。如果你的设备是 X86, 这个代码可能包含 SSE 或者 AVX 指令;如果是 CUDA 设备,将包含 PTX 指令。 此外,除了目标特定的机器代码之外,TVM 还生成负责内存管理、内核启动等的主机端代码。

build\_module() 函数完成代码生成,定义在 python/tvm/target/codegen.py 。在 C++ 端代码生成定义在 src/target/codegenbuild\_module() Python 函数会搜索在 src/target/codegen/codegen.cc 中的 build() 函数。

build() 函数 PackedFunc 注册表中为目标设备查找代码生成器,并调用找到的函数。例如, codegen.build\_cuda 函数注册在 src/codegen/build_cuda_on.cc ,就像这样:

TVM_REGISTER_GLOBAL("codegen.build_cuda")
.set_body([](TVMArgs args, TVMRetValue* rv) {
    *rv = BuildCUDA(args[0]);
  });

上方的 BuildCUDA() 函数使用定义在 src/codegen/codegen_cuda.ccCodeGenCUDA 类,从 lowered IR 生成 CUDA kernel source,并使用 NVRTC 编译 kernel。如果你的目标设备使用 LLVM,包括 X86、ARM、NVPTX 和 AMDGPU,代码可由定义在 src/codegen/llvm/codegen_llvm.ccCodeGenLLVM 来生成。 CodeGenLLVM 将 TVM IR 转换成 LLVM IR,运行一些 LLVM 优化 passes,以及生成目标机器码。

src/codegen/codegen.cc 中的 Build() 函数会返回一个 runtime::Module 类,它定义在 include/tvm/runtime/module.hsrc/runtime/module.cc 。一个 Module 类是一个潜在目标 设备的特定 ModuleNode 的容器。

每个后端都实现一个 ModuleNode 的子类,来添加目标特定的 runtime API 调用。 例如,CUDA 后端在 src/runtime/cuda/cuda_module.cc 实现 CUDAModuleNode 类,来管理 CUDA 驱动 API。上方的 BuildCUDA() 函数用 runtime::Module 包装了 CUDAModuleNode ,并包装到 Python 端。LLVM 后端在 src/codegen/llvm/llvm_module.cc 实现了 LLVMModuleNode ,处理了 JIT 执行和编译代码。其他对应各个后端的 ModuleNode 子类可以在 src/runtime 子文件夹找到。
返回的 module ,可以被认作编译函数和设备 API 的组合,可以被 TVM 的 NDArray objects 调用。

dev = tvm.device(target, 0)
a = tvm.nd.array(np.random.uniform(size=n).astype(A.dtype), dev)
b = tvm.nd.array(np.random.uniform(size=n).astype(B.dtype), dev)
c = tvm.nd.array(np.zeros(n, dtype=C.dtype), dev)
fadd(a, b, c)
output = c.numpy()

在幕后,TVM 会自动分配设备内存并管理内存传输。为了实现这个目标,每个后端都需要继承在 include/tvm/runtime/device_api.h 定义的 DeviceAPI 类,使用设备特定的 API 重写里面的内存管理方法。例如,CUDA 后端在 src/runtime/cuda/cuda_device_api.cc 使用 cudaMalloccudaMemcpy 实现了 CUDADeviceAPI .

第一次使用 fadd(a, b, c) 调用编译后的模块时,会调用 ModuleNodeGetFunction() 方法来获取可用于内核调用的 PackedFunc 。例如,在 src/runtime/cuda/cuda_module.cc CUDA 后端实现了 CUDAModuleNode::GetFunction() 函数如下:

PackedFunc CUDAModuleNode::GetFunction(
      const std::string& name,
      const std::shared_ptr<ModuleNode>& sptr_to_self) {
  auto it = fmap_.find(name);
  const FunctionInfo& info = it->second;
  CUDAWrappedFunc f;
  f.Init(this, sptr_to_self, name, info.arg_types.size(), info.launch_param_tags);
  return PackFuncVoidAddr(f, info.arg_types);
}

PackedFunc 的重载函数 operator() 会被调用。从而会调用定义在 src/runtime/cuda/cuda_module.ccCUDAWrappedFuncoperator() 函数,最终我们会看到 cuLaunchKernel 驱动会调用:

p
class CUDAWrappedFunc {
 public:
  void Init(...)
  ...
  void operator()(TVMArgs args,
                  TVMRetValue* rv,
                  void** void_args) const {
    int device_id;
    CUDA_CALL(cudaGetDevice(&device_id));
    if (fcache_[device_id] == nullptr) {
      fcache_[device_id] = m_->GetFunc(device_id, func_name_);
    }
    CUstream strm = static_cast<CUstream>(CUDAThreadEntry::ThreadLocal()->stream);
    ThreadWorkLoad wl = launch_param_config_.Extract(args);
    CUresult result = cuLaunchKernel(
        fcache_[device_id],
        wl.grid_dim(0),
        wl.grid_dim(1),
        wl.grid_dim(2),
        wl.block_dim(0),
        wl.block_dim(1),
        wl.block_dim(2),
        0, strm, void_args, 0);
  }
};

本文概括了 TVM 如何编译和执行函数。 虽然本文没有详细说明 TOPI 或 Relay,但最终所有神经网络算子都会经历与上述相同的编译过程。

# 后记

本博客目前以及可预期的将来都不会支持评论功能。各位大侠如若有指教和问题,可以在我的 github 项目 或随便一个项目下提出 issue,并指明哪一篇博客,我看到一定及时回复!