深度学习编译中间件之NNVM(四)TVM设计理念与开发者指南

2023-10-29 05:08

本文主要是介绍深度学习编译中间件之NNVM(四)TVM设计理念与开发者指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

参考文档

  1. http://docs.tvmlang.org/dev/index.html TVM Design and Developer Guide

本文档为官方指导手册的中文翻译版本,主要涉及到TVM的设计理念和开发者指南,适用于计划深入掌握TVM深度定制开发技术的开发者。

TVM运行时系统

TVM支持多种编程语言下的编译器堆栈开发和部署,针对本文档我们主要会介绍TVM运行时的关键组件。

这里写图片描述

我们需要满足相当多的软件需求:

  • Deployment(部署):能够通过Python/Javascript/C++来调用被编译的函数
  • Debug(调试):定义一个Python函数,被编译的函数能够调用这个Python函数
  • Link(链接):设计设备相关代码(负责调用设备特定代码,例如CUDA),并且这些代码能够被主机函数调用
  • Prototype(原型):通过Python定义一个IR Pass1,此Pass能够被C++后端调用
  • Expose(暴露接口):通过C++设计的编译器堆栈需要暴露接口给前端语言(例如Python)
  • Experiment(验证支持):主要是针对嵌入式设备设计一套RPC接口(远程调用接口)从而加速验证过程

简而言之,我们需要确保通过一种语言定义的函数能够被另外的语言调用,另外还要针对嵌入式设备最小化运行时核心。

PackedFunc

对于上面列举的软件需求,PackedFunc是一个简单却优雅的解决方案。下面列举一个C++的PackedFunc示例:

#include <tvm/runtime/packed_func.h>void MyAdd(TVMArgs args, TVMRetValue* rv) {// automatically convert arguments to desired type.int a = args[0];int b = args[1];// automatically assign value return to rv*rv = a + b;
}void CallPacked() {PackedFunc myadd = PackedFunc(MyAdd);// get back 3int c = myadd(1, 2);
}

在上面的示例代码中,我们定义了PackedFunc函数MyAdd。它带有两个参数:args表示输入参数和rv表示返回值。这个函数是无类型的,没有必要严格限制输入参数和返回值的类型。只需要在调用PackedFunc函数时,把输入参数打包到TVMArgs类型数据中,并且从TVMRetValue类型数据中获取返回值。

得益于C++的模板函数技巧,我们可以像调用普通函数一样来调用PackedFunc类型函数。因为PackedFunc类型函数是无类型的,所以Python语言无需古怪的语法就可以调用PackedFunc函数。下面通过示例来展示:

// register a global packed function in c++
TVM_REGISTER_GLOBAL("myadd")
.set_body(MyAdd);
import tvmmyadd = tvm.get_global_func("myadd")
# prints 3
print(myadd(1, 2))

PackedFunc使用便捷主要在于TVMArgsTVMRetValue的良好设计。下面列举PackedFunc函数能够传递哪些类型的数据:

  • int float and string
  • PackedFunc类型自身
  • Module for compiled modules
  • DLTensor交换格式
  • TVM结点,表示IR

在不同语言间传递上上述类型的数据时不需要进行专门的序列化处理,而对于深度学习部署这种使用场景,PackedFunc能够满足部署需求,大部分函数只需要传递DLTensor和数字类型的数据。

因为PackedFunc可以传递PackedFunc类型的参数,所以我们可以把函数从Python传递到C++

TVM_REGISTER_GLOBAL("callhello")
.set_body([](TVMArgs args, TVMRetValue* rv) {PackedFunc f = args[0];f("hello world");
});
import tvmdef callback(msg):print(msg)# convert to PackedFunc
f = tvm.convert(callback)
callhello = tvm.get_global_func("callhello")
# prints hello world
callhello(f)

TVM提供了一个最小化的C语言API,可以通过C语言API把PackedFunc嵌入到任何编程语言中。除了Python,我们还计划添加对Java和JavaScript的支持。嵌入API的设计哲学类似Lua。

关于PackedFunc有一个比较有意思的地方,就是它同时被编译器堆栈和部署堆栈使用了。

  • 所有的TVM编译器Pass函数通过PackedFunc暴露接口给前端语言
  • 已经被编译的模块也通过PackedFunc返回已编译的函数

为保证TVM Runtime的最小化,我们把运行时和IR Node隔离开。最终整个运行时的体积只有200K-600K,浮动的区间取决于包含了驱动支持(例如CUDA)。

因为只有非常少的参数在堆栈中,所以调用PackedFunc的负担和普通函数相比是小的。

模块

因为TVM需要支持多种类型的硬件设备,所以我们需要支持不同类型的驱动。我们必须使用驱动API来加载Kernel,设置Packed格式的参数和启动Kernel。我们也需要修补驱动API,以此让被暴露的函数是线程安全的。所以我们经常需要用C++来实现这些驱动胶水,并把这些暴露给用户。因为PackedFunc的原因,我们不需要为每一种类型的函数都做一个适配。

TVM定义Module作为已编译对象。用户可以通过PackedFunc从Module中获取已经编译的函数。在运行时可以动态地从Module中获取已经编译生成的代码。当代码被第一次调用之后会被缓存,以保证接下来相同代码的调用能重用已经缓存的代码。

ModuleNode是一个抽象类,被用来实现每个类型的设备驱动。到目前为止,我们已经支持了CUDA,Metal,Opencl模块。此处的抽象能够使新设备的添加变得容易,我们不需要为每个类型的设备重新设计host端代码生成逻辑。

远程部署
TVMNode和编译器堆栈

在文章的前面部分已经提到过,编译器堆栈API处于PackedFunc运行时系统之上。为了研究的需要,我们面对着一个编译器API经常需要变化的现实。我们需要一种新的IR语言,但是我们并不想大幅改变我们现有的API,我们总结我们对于编译器语言的需求:

  • 能够序列化任何语言对象和IR
  • 能够在前端语言中比较快捷地浏览、打印、操作IR对象

我们先介绍一个基类Node来满足上面的需求,在编译器堆栈中所有的语言对象都是Node类的子类。每个Node包含一个字符串type_key来惟一标识对象的类型。我们选择字符串作为type_key的类型是为了能够让新的Node类可以被添加分散管理的代码库中。为了缓解调度时的速度问题,在Runtime运行时我们也分配了一个int类型的type_index来标识对象的类型。

因为一般情况下一个Node对象可以在一种语言中的不同位置被引用,我们使用shared_ptr来记录引用。NodeRef类被用来标识Node的引用。我们也定义多个NodeRef的子类来处理Node的子类,每个Node类都需要定义VisitAttr函数。

class AttrVisitor {public:virtual void Visit(const char* key, double* value) = 0;virtual void Visit(const char* key, int64_t* value) = 0;virtual void Visit(const char* key, uint64_t* value) = 0;virtual void Visit(const char* key, int* value) = 0;virtual void Visit(const char* key, bool* value) = 0;virtual void Visit(const char* key, std::string* value) = 0;virtual void Visit(const char* key, void** value) = 0;virtual void Visit(const char* key, Type* value) = 0;virtual void Visit(const char* key, NodeRef* value) = 0;// ...
};class Node {public:virtual void VisitAttrs(AttrVisitor* visitor) {}// ...
};

每个Node的子类都会Override(重载)VisitAttrs来访问它的成员。在这里展示一个相应的示例:

class TensorNode : public Node {public:/*! \brief The shape of the tensor */Array<Expr> shape;/*! \brief data type in the content of the tensor */Type dtype;/*! \brief the source operation, can be None */Operation op;/*! \brief the output index from source operation */int value_index{0};/*! \brief constructor */TensorNode() {}void VisitAttrs(AttrVisitor* v) final {v->Visit("shape", &shape);v->Visit("dtype", &dtype);v->Visit("op", &op);v->Visit("value_index", &value_index);}
};

在上面的示例中,Operation和Array<Expr>都是NodeRef。VisitAttrs提供了一个ReflectionAPI(反射API)来访问对象里面的每一个成员。我们也可以使用这个函数来访问Node节点和递归地序列化任何语言对象。它也允许我们在前端语言中容易地获取对象的成员。例如在下面的示例中,我们存取TensorNode的op成员:

import tvmx = tvm.placeholder((3,4), name="x")
# access the op field of TensorNode
print(x.op.name)

当添加一个新的Node类型到C++时,我们不需要改变前端运行时,这使得扩展编译器堆栈变得容易。

实现细节

PackedFunc的每一个参数都包含一个联合体类型的数据TVMValue和一个类型代码。这种设计允许动态类型语言能够直接转换到相应的类型,静态类型语言可以在运行时检查数据类型。


  1. 此术语为编译器领域专用,在LLVM的架构中,Pass的作用是优化LLVM IR。详见LLVM Cookbook中文版第4章 ↩

这篇关于深度学习编译中间件之NNVM(四)TVM设计理念与开发者指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/298418

相关文章

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

SQL Server数据库迁移到MySQL的完整指南

《SQLServer数据库迁移到MySQL的完整指南》在企业应用开发中,数据库迁移是一个常见的需求,随着业务的发展,企业可能会从SQLServer转向MySQL,原因可能是成本、性能、跨平台兼容性等... 目录一、迁移前的准备工作1.1 确定迁移范围1.2 评估兼容性1.3 备份数据二、迁移工具的选择2.1

在 Windows 上安装 DeepSeek 的完整指南(最新推荐)

《在Windows上安装DeepSeek的完整指南(最新推荐)》在Windows上安装DeepSeek的完整指南,包括下载和安装Ollama、下载DeepSeekRXNUMX模型、运行Deep... 目录在www.chinasem.cn Windows 上安装 DeepSeek 的完整指南步骤 1:下载并安装

nginx-rtmp-module构建流媒体直播服务器实战指南

《nginx-rtmp-module构建流媒体直播服务器实战指南》本文主要介绍了nginx-rtmp-module构建流媒体直播服务器实战指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. RTMP协议介绍与应用RTMP协议的原理RTMP协议的应用RTMP与现代流媒体技术的关系2

Spring Boot统一异常拦截实践指南(最新推荐)

《SpringBoot统一异常拦截实践指南(最新推荐)》本文介绍了SpringBoot中统一异常处理的重要性及实现方案,包括使用`@ControllerAdvice`和`@ExceptionHand... 目录Spring Boot统一异常拦截实践指南一、为什么需要统一异常处理二、核心实现方案1. 基础组件