查看: 1222|回复: 0

Glow模型的动态加载初探

[复制链接]
  • TA的每日心情
    开心
    2024-3-26 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3300

    主题

    6547

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    32032
    最后登录
    2024-4-26
    发表于 2021-4-1 13:26:37 | 显示全部楼层 |阅读模式
    Glow模型的动态加载初探

    上一篇文章中《编译模型的编译器—glow推理引擎初探》,小编为大家介绍了如何使用NXP官方的Glow工具进行模型的编译,并部署到设备上。
    相信大家还记得,Glow所编译出来的模型文件需要添加到工程中,进行编译方可运行,而这也被称作静态链接,即需要将所用到的文件全部导入到工程中,编译之后下载到设备中。如果只使用单一的模型时还好,我们只需要编译一次即可一劳永逸。试想,如果我们想要对模型进行反复更换呢?岂不是每更换一次模型,就要重新编译一次模型,费时费力。

    在这里我们引出另一个与之相对的概念,动态链接。所谓动态链接,就是不事先将所用到的模型文件编译到工程中,而是将这些模型文件包装成一个模块,当程序需要时,才将其加载进内存中运行,这样的好处不言而喻,最重要的是,更换模型之后,只需要重新生成对应的模块,而无需重新编译工程,而前者所消耗的时间往往会低于编译完整工程,可谓省时省力。

    不过,还是要先泼一盆冷水,很可惜的是,原生的Glow只支持这种静态链接的方式进行模型的部署,现在还不支持动态链接。那难道我们就不能体验动态链接的便捷了吗?当然是可能的了,下面小编就为大家讲解如何实现Glow模型的动态链接。
    首先,我们要参考上一篇文章,先对模型进行编译,生成对应的模型文件,当然,这一步是不能省略的,实现动态链接的原理就是对Glow所生成的模型文件进行分析,以生成可供动态链接的模块。
    这里要提前说明下,以下操作需要借助于Keil工具,同学们要自行安装。需要实现说明,本文需要用到一些elf以及动态链接的相关知识,本文不会涉及,感兴趣的同学需要自行学习。

    动态链接的实现原理分析
    在这里,需要用到的文件有三个
    其一是Network.h,需要从中解析出一些模型运行时的所需信息:
    2.1.png

    其二是Network.weights.bin,无需多言,模型的所有权重数据。
    其三,也是最重要的Network.o,这里面包含了运行模型所需的所有函数,而他也正是实现动态加载的关键。让我们先来看一下都有我们都需要调用哪些函数,打开控制台输入readelf.py Network.o -s查看:
    2.2.png

    最后两行,我们注意到有两个看似像是函数,但是类型又不是FUNC的东东,这些东西就是阻挠我们前进的大石头,可以这样理解,FUNC类型的函数是包含在文件中的,而NOTYPE的东东说明我们会用到但是没有包含在文件中,也就是说,文件中缺少这两个东东的实现,那也意味着我们无法成功运行我们的函数。我们再次输入readelf.py Network.o -h查看以下Network.o文件的属性:
    2.3.png

    文件类型为REL,即Relocatablefile,即可重定位文件,输入readelf.py Network.o -r查看重定位表:
    2.4.png

    表中有一些东西,说明有一些函数是外部依赖的,而这也印证了我们之前说的,的确有一些函数没有被包含在文件中。而且是不是看到了神奇的事情?没错,正是上面看到的两个表注类型为NOTYPE的小家伙,这说明,除了一些模型运行相关的函数,还需要一些外部函数,这些函数没有被包含在这个.o文件中,换句话说,文件中不包含这两个函数的代码实现,需要通过一些手段导入其中。

    有些同学可能注意到了,这两个家伙怎么这么像是系统库函数?没错,说对了,这些正式一些库函数,只不过expf需要进行封装,命名为rely.c:
    2.5.png

    而memset函数无需如此,发现了这个秘密之后,仿佛一切都明朗了,我们用某种方式拿到这两个函数的实现不就好了?没错,就是这样。在这里小编采用了一个相对简单的方案,我们利用keil提供的编译器对Network.o以及rely.c进行编译生成一个elf文件,因为用到的都是库函数,编译器都会提供:
    2.6.png

    注意这里armclang.exe的路径要替换为自己的。这样让我们再来看看那两个小家伙,先来看memset:
    2.7.png

    之后是expf:
    2.8.png

    让我们开心的事情发生了,这两个小家伙的类型都成了FUNC,说明这两个函数及其实现都已经被包含在文件中了。因为我们只会用到其中的代码数据,因此我们只需要通过lief工具将其中的代码段ER_RO提取出来即可,通过输入readelf.py Network.o -S查看所有段:
    2.9.png

    接下来,让我们思考一个事情,无论所需要调用的函数有多少,我们所要实现的功能是什么?我们要运行一个模型,那么一定是存在一个主函数,负责调用其他函数。通过分析,我们可以发现,Glow所生成的代码,其入口函数是根据模型名字而定的,我们这里的模型名字是Network,即函数入口是Network:
    2.10.png

    动态链接的实现原理分析
    那么至此,万事具备,让我们来准备合成完整的可以动态加载的module,我们的模块文件分为三部分,其一是控制块,存储了运行Network函数所需要用到一些数据,我们所做的一切都是为他服务的,Network函数的输入参数有三个,分别是模型权重,输入输出数据内存以及存储模型各层输出的内存地址,其函数形式为:
    2.11.png

    控制块共包含15个变量:
    2.14.png

    这里比较特殊的是weights_offset,因为我们将模型权重放在模块文件的最后部分,因此这个数值是sizeof(控制块) + sizeof(ER_RO)。根据mut_mem_size, act_mem_size进行内存分配,随后即可将函数地址作为Network函数的第二以及第三个参数地址传入。对于权重地址的处理比较特殊,有两种方案,第一种是根据const_mem_size分配内存空间随后通过memcpy,根据weights_offset将权重数据拷贝到目标地址,之后传入Network;第二种方案则无需额外开辟空间,因为程序运行期间,模块本身会被直接load进内存中,那么我们就可以直接根据weights_offset的值,将模型权重数据的地址传入Network,减少内存消耗,不过这样一来,可能会影响一些系统性能,毕竟模块本身一般不会被放到快速内存中。
    模块的第二部分就是所提取出的代码段,随后第三部分紧跟着存放的是模型权重数据。

    这样一来,所有模型运行所需要的代码和数据都已经集成到了模块中,正所谓,万事俱备,就等东风。
    模块有了,下一个需要考虑的问题就是怎么让我们的模块运行呢?我们还需要在程序中编写一个简单的解析器,对load进内存的模块进行解析,随后直接根据entry_offset找到函数入口,进行调用即可。这块儿小编就不再展开说明了,有兴趣的同学可以自行分析下代码。
    ‍‍‍‍
    结果验证
    小编在此分别测试了两个模型,都是我们所耳熟能详的,其一是cifar10,其二是lenet,其程序运行结果以及对应所使用的脚本如下,达到了我们的预期:
    2.12.png

    2.13.png

    至此,我们就成功实现了glow模型的动态加载,正如前面所说的,对比静态编译的方式,采用动态加载,更换模型之后无需再对工程进行编译,只需要从存储介质中进行文件读取,将模型文件load到内存中即可进行模型推理。大大提高了程序的灵活性,提高了开发效率。不过,想要着手体验的朋友们,可能还要再等等了,程序包将会包含在openART软件包中,等待openART揭开面纱之时,同学们即可一探龙穴。
    最后还要重申,本文涉及到一些elf镜像以及动态链接的一些知识点,小编没有展开说明,需要同学们自己来探索了。

    来源: 恩智浦MCU加油站

    签到签到
    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-27 03:36 , Processed in 0.103704 second(s), 20 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表