tvm 中的python bindings是如何与 C++ 进行交互的呢

我们知道,tvm 使用 python 作为前端编程语言,好处是 python 简单易用,生态强大,且学习成本较低。而实际的代码,都是 c++ 代码。
源码编译 tvm,编译完成之后,会在 build 目录下生成 libtvm.so 和 libtvm_runtime.so 两个文件。
使用 tvm 编译时,需要 libtvm.so,而加载编译后的 so 库实际运行时,需要 libtvm_runtime.so。

tvm 对模型进行编译的过程,可以这么来理解。高级编程语言是符合一定上下文无关文法规则的语言,通过编译器翻译成机器码。而模型 ,可以理解成是一大堆的数学公式,而参数就是数学公式的系数。也就是说是按照一定顺序,有确定参数的数学公式的计算。可以把模型想象成是一个超大的函数,这个函数的函数体部分就是一大堆的数学计算。这样理解的话,就相当于是将这样一个有大量数学计算的超大的函数编译成机器码,翻译成能在特定硬件上运行的二进制代码。

我的理解就是,不管是高级编程语言,还是机器学习模型,本质上都是对计算的一种描述,而编译器所做的事情,就是识别这种对计算的描述,然后经过多层 lowering 的过程,翻译成特定芯片所能识别的指令,能够在特定硬件上运行。

tvm 将模型编译成一个二进制文件,在 linux 系统中就是 elf 格式的文件,比如 shared library。而这个 shared library 的内容是 tvm 的 api 组成的,所以加载这个 library 也需要使用 tvm 的api,这就需要使用 libtvm_runtime.so 了。

好了,言归正传,当我们使用 tvm 的 python dsl 来编译模型时,python 接口是如何与 libtvm.so 进行交互的呢?

tvm ffi

在 tvm 的 python modules 中,封装了一个 _ffi 的模块。该模块中使用到了 ctypes 库,用来与 c 代码进行交互。
ctypes is a foreign function library for Python。ctypes 提供了 C 兼容的数据类型,允许调用 DLL 或者 shared libraries 中的函数。支持将这些 c 中的函数封装到纯 python 中进行使用。

比如使用 ctypes 加载 libc.so

import ctypes
libc = ctypes.CDLL("libc.so")

使用 libc 中的随机函数

print(libc.rand())

tvm/_ffi/base.py 中,定义了 _load_lib 函数,用来加载动态库。
load lib

代码中调用了 ctypes.CDLL 方法来加载库。而 lib_path 是通过 libinfo.find_lib_path() 方法返回的。可以继续看下这个函数的实现。该方法定义在 tvm/_ffi/libinfo.py 中。

首先通过 get_all_directories() 获取动态库可能存在的所有可能路径,分别从

  • TVM_LIBRARY_PATH 环境变量
  • PATH 环境变量
  • LD_LIBRARY_PATH 环境变量(linux)或者 DYLD_LIBRARY_PATH 环境变量(mac)
  • 构建目录 build, build/Release, 安装目录 install_dir
  • TVM_HOME 环境变量
    中进行查找。并根据不同的平台获取不同的库的名称,linux 下为
libtvm.so
libtvm_runtime.so

通过各种路径搜索,找到实际库所在的位置。

__init_api

看一个实际调用,比如 tvm/relay/transform/_ffi_api.py 中,
transform ffi

首先 import 了 tvm._ffi,这就是上面我们分析的 tvm 的 ffi,然后调用了 __init_api 方法进行了初始化,实质上就是注册。这里来分析一下这个函数。
_init_api

形参对应关系

namespace: relay._transform
target_module_name: __name__

__name__ 是 python 内置的变量,值就是当前模块的名称,也就是 tvm/relay/transform/__ffi_api。由于 namespace 不是以 tvm. 开头,所以执行 else 分支

_init_api_prefix
通过 list_global_func_names() 找到所有的全局符号名称,然后逐个遍历,如果名称前缀是 relay._transform 就获取该名称的函数,比如 relay._transform.InferType,然后将函数以属性的方式加入到 target_module 中。最后一行代码,就是为 target_module 设置一个属性。相当于

_transform."InferType" = ff

这样,在 transformer.py 中,对该函数再做了一层封装,
transform InterType
在 python 模块中,就可以直接使用 transform.InferType() 了。

虽然这里已经见到了 tvm 对 c++ 函数的封装,但是并没有看到 c++ 函数是如何交互起来的。而主要就是上面函数中,通过名称获取全局符号的过程。我们重点来分析一下。

_get_global_func

get_global_func 定义在 tvm/_ffi/registry.py 中,实际调用的是 tvm/_ffi/_ctypes/packed_func.py 中的 _get_global_func
_get_global_func
handle 其实是一个 ctypes 中的 void。

_LIB 就是上面分析的,tvm 中使用 ctypes.CDLL 来加载动态库后返回的对象。而 _LIB.TVMFuncGetGlobal 实际上就是调用 so 库中的 TVMFuncGetGlobal 函数,这个在 src/runtime/registry.cc 中定义。该函数通过名称获取注册的全局符号。
get global func

通过函数名,在 fmap 中寻找,返回函数。这个函数都是经过封装的 PackedFunc 指针。

tvm c++ 端的函数注册

还是以上面 InferType 为例。在 tvm 的 src/relay/transforms/type_infer.cc 中,调用宏对 Infertype 进行了注册。

TVM_REGISTER_GLOBAL("relay._transform.Infertype").set_body_typed([]() { return InferType(); });

TVM_REGISTER_GLOBAL 就是注册一个全局函数

#define TVM_REGISTER_GLOBAL(OpName) \
  TVM_STR_CONCAT(TVM_FUNC_REG_VAR_DEF, __COUNTER__) = ::tvm::runtime::Registry::Register(OpName)

OpName 对应为算子名称,也就是需要注册的函数,为 relay._transform.Infertype,而函数体部分为实际的 InferType() 函数调用。上面的红展开,就是

TVM_STR_CONCAT(TVM_FUNC_REG_VAR_DEF, __COUNTER__) = ::tvm::runtime::Registry::Register("relay._transform.Infertype").set_body_typed([]() {
  return InferType();
});

这里调用了 Register 方法进行注册。
register
将函数加入到 Global 中。这样就前后对应起来了。

总结

tvm python bindings 通过 ctypes 库,加载 libtvm_runtime.so,通过 _ffi 模块,将 so 库中的所有注册的全局符号都加载到 python,同时对这些 c 函数进行封装,封装成 python 可以直接调用的 python function 的形式。这样在 pure python 中就可以直接使用了。

在 c++ 端的代码中,通过注册机制将函数注册到一个 Global model 中。注册的函数都被封装成了 PackedFunc 的形式。这种形式,可以比较方便的处理 c++ 与 c mangling 不同的问题,因为这里不是使用的编译器编译后的符号,而是经过封装后,tvm自己建立起的通过名字与函数指针之间的对应关系,自己来管理。

c++ 代码中将函数经过封装,以名字和方法映射的方式进行注册。而在 python 中通过加载动态库后,将所有注册的函数再次进行封装,使得 Python 中可以直接调用。这样就完成了 python 与 c++ 动态库之间的交互。

那再多思考一个问题:
python 语言,比如 cython,是解释执行的。而通过 ctypes 的方式加载的动态库,是经过 aot 的方式进行编译的,为什么这里可以直接执行呢?

我的理解是,这里可以将 c++ 代码做一个类比。比如 c++ 中,动态加载动态库可以通过 dlopen 的方式打开,通过编译器 aot 编译成可执行文件,然后运行。
而 python,是解释执行的。通过 python 二进制文件经过前端处理翻译成字节码的形式,在虚拟机中解释执行。可以将 ctypes 加载动态库的操作 CDLL 看成是 dlopen,实际也确实是这样来实现的。那 python 虚拟机在解释执行时,如果刚好运行到这个 c 函数,其实就相当于获取到这个c函数的地址,直接转到这个c函数对应的机器码处执行。

而在虚拟机或者 JIT 机制中,比如 jvm,会将 hot code 编译成机器码直接执行,所以直接执行这个机器码是可以的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/343882.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据挖掘笔记1

课程:清华大学-数据挖掘:理论与算法(国家级精品课)_哔哩哔哩_bilibili 一、Learning Resources 二、Data 数据是最底层的一种表现形式。数据具有连续性。从存储上来讲,数据分为逻辑上的和物理层的。大数据&#xff1…

JAVA算法—排序

目录 *冒泡排序: *选择排序: 插入排序: 快速排序: 总结: 以下全部以升序为例 *冒泡排序: 引用: 在完成升序排序时,最大的元素会经过一轮轮的遍历逐渐被交换到数列的末尾&#…

苹果眼镜(Vision Pro)的开发者指南(6)-实战应用场景开发 - 游戏、协作、空间音频、WebXR

第一部分:【构建游戏和媒体体验】 了解如何使用visionOS在游戏和媒体体验中创建真正身临其境的时刻。游戏和媒体可以利用全方位的沉浸感来讲述令人难以置信的故事,并以一种新的方式与人们联系。将向你展示可供你入门的visionOS游戏和叙事开发途径。了解如何使用RealityKit有…

项目难点和优化

难点: 对于同一个位置百度地图定位的经纬度和腾讯地图定位的经纬度不一样? 解决:由于两者所用的算法不同,计算出来的经纬度也是不一样的,将百度地图的经纬度转换成腾讯地图的经纬度/腾讯的经纬度转化百度的经纬度 export functi…

在Spring Boot中使用ZXing开源库生成带有Logo的二维码

在上一篇文章的基础上,我们将进一步扩展功能,实现在生成的二维码中嵌入Logo图片。这样的二维码更具个性化和识别度。让我们逐步完成这个功能。 第一步:引入Logo图片 首先,准备一张用作Logo的图片,并确保它的大小适中…

图像处理------负片

什么是负片? 负片是经曝光和显影加工后得到的影像,其明暗与被摄体相反,其色彩则为被摄体的补色,它需经印放在照片上才还原为正像。我们平常所说的用来冲洗照片的底片就是负片。 """将彩色图像转换成负片 "&…

Java的异常 Exception

从继承关系可知:Throwable 是异常体系的根,它继承自Object 。Throwable 有两个体系: Error 和Exception. Error表示严重的错误,程序对此一般无能为力,例如: OutOfMemoryError :内存耗尽NoClassDefFoundError :无法加载某个ClassStackOverflowError :虚…

V∗: Guided Visual Search as a Core Mechanism in Multimodal LLMs

摘要 当我们环顾四周并执行复杂任务时,我们如何看待和选择性地处理我们所看到的是至关重要的。然而,这种视觉搜索机制的缺乏,在目前的多模态LLM(MLLM)阻碍了他们的能力,专注于重要的视觉细节,特…

c++:类和对象(2),对象的初始化和清理

目录 构造函数和析构函数 构造函数语法:类名(){} 析构函数语法: ~类名 () {} 例子: 构造函数的分类及调用 两种分类的方式: 三种调用方法: 括号法​编辑 显示法 隐式转换法 拷贝构造函数调用时…

Python range函数

Python中的range()函数是一个强大的工具,用于生成一系列的整数。它在循环、迭代和序列生成等方面都有广泛的应用。本文将深入探讨range()函数的用法,提供详细的示例代码,并讨论其在Python编程中的实际应用。 什么是range()函数? …

springboot导出数据到excel模板,使用hutool导出数据到指定excel,java写入数据到excel模板

最近遇到一个需求,需要从数据库查询数据,写入到对应的excel导入模板中。再把导出的数据进行修改,上传。 我们项目用的是easyExcel,一顿百度搜索,不得其法。 主要是要把数据填充到指定单元格中,跟平时用到的…

计算机网络实验二:Packet Tracer的简单使用

目录 实验二:Packet Tracer的简单使用 2.1 实验目的 2.2 实验步骤 2.2.1 构建网络拓扑 2.2.2 配置各网络设备 2.2.3 网络功能验证测试 2.3 实验总结 实验二:Packet Tracer的简单使用 2.1 实验目的 ①练习packet tracer仿真软件的安装&#xff1…

mc我的世界服务器多少钱一个月?

我的世界服务器多少钱一个月?低至7元一个月,阿里云和腾讯云均可以选择mc服务器,阿里云2核2G3M轻量服务器87元一年、腾讯云轻量2核2G3M服务器88元一年,阿里云ECS云服务器2核2G3M带宽99元一年,腾讯云2核4G5M带宽轻量应用…

活动回顾丨云原生技术实践营上海站「云原生 AI 大数据」专场(附 PPT)

AI 势不可挡,“智算”赋能未来。2024 年 1 月 5 日,云原生技术实践营「云原生 AI &大数据」专场在上海落幕。活动聚焦容器、可观测、微服务产品技术领域,以云原生 AI 工程化落地为主要方向,希望帮助企业和开发者更快、更高效地…

菜鸡后端的前端学习记录

前言 记录一下看视频学习前端的的一些笔记,以前对Html、Js、CSS有一定的基础(都认得,没用过),现在不想从头再来了,学学Vue框架,不定时更新,指不定什么时候就鸽了。。。。 Vue2 01…

初识汇编指令

1. ARM汇编指令 目的 认识汇编, 从而更好的进行C语言编程 RAM指令格式: 了解 4字节宽度 地址4字节对齐 方便寻址 1.1 指令码组成部分 : condition: 高4bit[31:28] 条件码 0-15 (16个值 ) 条件码: 用于指令的 条件执行 , ARM指定绝大部分 都可…

Linux之快速入门(CentOS 7)

文章目录 一、Linux目录结构二、常用命令2.1 切换用户2.2查看ip地址2.3 cd2.4 目录查看2.5 查看文件内容2.6 创建目录及文件2.7 复制和移动2.82.93.0 一、Linux目录结构 目录作用/bin是 Binaries (二进制文件) 的缩写,这个目录存放着最经常使用的命令/dev是 Device(设备) 的缩写…

【GitHub项目推荐--不错的Rust开源项目】【转载】

01 Rust 即时模式 GUI 库 egui 是一个简单、快速且高度可移植的 Rust 即时模式 GUI 库,可以轻松地将其集成到你选择的游戏引擎中,旨在成为最易于使用的 Rust GUI 库,以及在 Rust 中制作 Web 应用程序的最简单方法。 项目地址:ht…

go 依赖注入设计与实现

在现代的 web 框架里面,基本都有实现了依赖注入的功能,可以让我们很方便地对应用的依赖进行管理,同时免去在各个地方 new 对象的麻烦。比如 Laravel 里面的 Application,又或者 Java 的 Spring 框架也自带依赖注入功能。 今天我们…

【ASOC全解析(一)】ASOC架构简介和欲解决的问题

【ASOC全解析(一)】ASOC架构简介和欲解决的问题 一、什么是ASOC以及ASOC解决的三个问题二、ASOC的组成与功能解决第一个问题解决第二个问题解决第三个问题 三、ASOC基本工作原理 /********************************************************************…