如何适配新架构?TPU-MLIR代码生成CodeGen全解析!

背景介绍tpu-mlir的codegen是bmodel生成的最后一步,该过程目的是将mlir文件转换成最终的bmodel。本文介绍了codegen的基本原理和流程,并记录了针对bm1684x等新架构的codegen重构过程。
与后端的关系由于一些历史的因素,mlir文件中的每个op对应的指令并不直接在tpu-mlir工程中生成,而是需要调用后端的函数完成最终指令的生成,这也带来了两个问题
如何设计编译器与后端的接口生成指令的数据结构存在后端还是编译器中关于问题1,目前的设计是采用codegen与后端隔离的形式,也就是codegen过程不直接调用后端函数,而是将不同处理器的相应函数全部封装到类中,在codegen中调用类方法间接使用后端接口,达成解耦。
而关于问题2,依据不同的处理器其数据结构位置也不同,1684的数据结构放在编译器这边,而bm1684x等新架构的处理器数据结构放在后端。无论放在哪里,其全部封装于问题1答案中的相应类中,对于codegen过程来说,看到的接口是一样的。
一个op生成指令的大致流程代码位置:lib/dialect/tpu/transforms/codegen/bm168xcodegen.cpp
该流程忽略codegen代码内部细节,这里只讲解类似于把大象装冰箱主要分几步这样的通俗介绍。
在bm168xcodegen.cpp会遇到某个op时会调用该op的codegen_local_bm168x/codegen_global_bm168x,算子的这个函数都在lib/dialect/tpu/interfaces/中在具体op中会设置一些参数,然后调用到后端的具体op的指令生成,比如conv2d算子会调用后端函数backend_api_conv_global(后端过程)直接做一系列检查后,会直接生成指令(二进制码),这些二进制码会通过store_cmd存储在指定数据结构中,等所有op的二进制码全部都生成完毕后,在编译器会调用bm1684x系列类中封装的函数取走指令,生成bmodel
做个形象点的例子:
原来 装冰箱只需要我,现在我嫌大象沉,我叫个张三帮我装。
我:张三,你把这个大象给我装冰箱里
张三吭哧吭哧帮我装完了
我:行了,张三,你走吧;我自己把装的运走。
指令生成所需要的数据结构指令依据处理器的engine不同而有所差别,比如1684有gdma和tiu,而新架构的处理器sg2260会存在sdma、cdma等engine。这里拿最通用的两种engine即bdc(后更名为tiu)和gdma为例:
std::vector bdc_buffer;
std::vector gdma_buffer;
uint32_t gdma_total_id = 0;
uint32_t bdc_total_id = 0;
std::vector gdma_group_id;
std::vector bdc_group_id;
std::vector gdma_bytes;
std::vector bdc_bytes;
int cmdid_groupnum = 0;
cmd_id_node *cmdid_node;
cmd_id_node *bdc_node;
cmd_id_node *gdma_node;
bdc_buffer和gdma_buffer:放指令gdma_total_id和bdc_total_id:存指令总数目,因为指令不一定是32位的,因此使用buffer的长度不能获取到指令的总数目gdma_group_id和bdc_group_id:每个group中的指令数,这个group是什么意思待调查清楚,后端对其进行控制时代码如下所示
cmdid_overflow
cmdid_groupnum:group的数量gdma_bytes和bdc_bytes:内部是每个group中指令的字节(bytes)数cmdid_node、bdc_node和gdma_node:这个node是为了并行生成groupop内部所需要指令而形成指的,具体机制还有待研究tpu-mlir中layer group和上述行为中group的概念区分tpu-mlir中的layer group是指可以存放在lmem的一系列算子,组成一个group op。
而上述的group,指的是指令组。这个指令组存在的意义是防止内存不够用,比方说1684只有16位寻址空间,那么大于这个数字的指令无法一次性全部搬运到内存,所以当指令超出某个数时候,就会重新建一个组。
tpu-mlir中bm168x及其相关类这边的相关类tpu-mlir/include/tpu_mlir/backend该文件夹下定义的类,目的就是将不同的处理器后端封装,从而实现后端于codegen过程的隔离。
其继承关系为:
在一次运行中只存在一个类(设计模式中单例),该类初始化时候会经过:读取后端动态链接库、加载函数(设置后端的函数指针)、指令数据结构的初始化、设置一些处理器相关的参数例如npu_num、l2_sram起始地址等。
后端函数的加载后端作为一个动态库放入了tpu-mlir工程里,具体在third_party/nntoolchain/lib/libbackend_xxx.so。在我们要使用backend时候,先在需要函数的类中定义好函数指针,然后再将动态库加载后,使函数指针指向动态库中真正的函数。
以同步函数tpu_sync_all为例,由于之后要加上多核支持的,所以需要在相关后端bmodel库中定义好,
注意必须和后端的函数名和参数保持一致typedef void (*tpu_sync_all)();在类内部加入该函数成员tpu_sync_all, dl_tpu_sync_all;有成员后,在该类load_functions函数的实现中加入宏,cast_function(tpu_sync_all);该宏可以将dl_tpu_sync_all指向动态库中真正的函数这时候在我们获得到该类实例后即可使用动态库中的函数了。
后端store_cmd设计后端的store_cmd功能是指编译器调用算子的过程中,把配置的指令保存到约定空间的过程。(以下是后端代码,以后会选择性开放)。后端的重点函数在store_cmd.cpp中,以cmodel/src/store_cmd.cpp;cmodel/include/store_cmd.h为例
注:store_cmd类设计的非常复杂,参杂各种设计模式在里面,只大概梳理一下类之间关系
store_cmd分别有enginestorer系列类和cmdstorer系列类:
enginestoreinterface(接口类)、继承于enginestoreinterface接口的gdmaenginestorer、bdenginestorer等具体类、enginestorerdecorator(装饰类接口)、继承于enginestorerdecorator的vectordumpenginestorerdecorator等具体装饰类cmdstorerinterface(接口)、继承于接口的concretcmdstorer、storerdecorator:装饰接口、vectordumpstorerdecorator具体装饰类。关于类之间的关系与逻辑使用单例设计模式,在store_cmd中只存在一个concretcmdstorer类,该类中会存所有enginestorer的类,当调用不同的engine时,会调用不同eenginestorer,如下代码virtual void store_cmd(int engine_id, void *cmd, cmd_id_node *cur_id_node,
                       int port) override
{
    switch (engine_id)
    {
    case engine_bd:
    case engine_gdma:
    case engine_hau:
    case engine_sdma:
        port = 0;
        break;
    case engine_cdma:
        assert(port get(engine_id, port)->store(cmd, cur_id_node);
}
cmd装饰类的作用是将所有的enginestorer套上其装饰器的壳子(目的实现其他功能),以vectordumpstorerdecorator为例,会使用宏为每个enginestorer、套上vectordumpenginestorerdecorator的壳子。    void decorate_engines()
    {
#define decor_storer(name, idx)                                              \
    if (outputs_[engine_##name][idx])                                        \
    {                                                                        \
        auto name##_str = std::make_shared( \
            storerdecorator::get(engine_##name, idx),                        \
            &(outputs_[engine_##name][idx]));                                \
        storerdecorator::get(engine_##name, idx) = name##_str;               \
        engine_decorators_.push_back(name##_str);                            \
    }
        decor_storer(bd, 0)
        decor_storer(gdma, 0)
        decor_storer(hau, 0)
        decor_storer(sdma, 0)
        for (int i = 0; i < cdma_num; i++)
        {
            decor_storer(cdma, i)
        }
#undef decor_storer
    }
每个具体的enginestorer,注意其功能并非把命令存下来,他只干解析命令,比方说拿到一条320位的命令(瞎说的),enginestorer会将其解析成长度为10的32位数组(std::vector)。
真正存命令是使用vectordumpenginestorerdecorator,装饰器的作用是:执行被装饰类的特定函数时,进行更多的操作,具体可以《设计模式》的书。这点对于理解store_cmd非常重要,作者在设计store_cmd时,使用了很多装饰器、为每个enginestorer赋予了额外的功能,其中把指令储存也看作一个装饰器。vectordumpenginestorerdecorator该装饰器执行enginestorer类中的store函数后,会追加执行take_cmds函数,该函数将所有指令存储到output_中。
class vectordumpenginestorerdecorator : public enginestorerdecorator
{
private:
    std::vector *&output_;
void take_cmds()
    {
        auto cmds = enginestorerdecorator::get_cmds();
        (*output_).insert((*output_).end(), cmds.begin(), cmds.end());
    }
public:
    vectordumpenginestorerdecorator(componentptr component,
                                    std::vector **output)
        : enginestorerdecorator(component), output_(*output) {}
virtual void store(void *cmd, cmd_id_node *cur_id_node) override
    {
        enginestorerdecorator::store(cmd, cur_id_node);
        if (!enabled_)
            return;
        this->take_cmds();
    }
virtual void store_cmd_end(unsigned dep) override
    {
        enginestorerdecorator::store_cmd_end(dep);
        this->take_cmds();
    }
};
store_cmd中类与暴露给编译器接口的关系实际上上述的各种类不能直接暴露给编译器,因为必须传的是c函数的函数接口,因此必须将类中各种函数封装进c语言函数形式,以store_cmd为例,get_storer会获得唯一的concretcmdstorer类
void store_cmd(void *cmd, int engine_id, cmd_id_node *cur_id_node, int port,
               int thread_id)
{
    get_storer()->store_cmd(engine_id, cmd, cur_id_node, port);
}
tpu-mlir中的重构修改分为三部分:bm168x及派生类、bm168xcodegen
对于bm168x派生类来说,后端工程中添加了很多新的函数,这些函数主要是将存指令的数据结构放入了后端管理,涉及的后端有1684x之后架构的处理器,而1684并不适配新的函数。这意味着:
存储指令的数据结构需要发生改变,许多数据结构已经不需要。如下图所示:
需要添加新的接口函数,即使获取指令的方式不同,但是在codegen过程看到的应该是一样的行为。
这里传入的参数是const char*是为了简化参数定义,可以用特定格式字符串来指定后端engine。如gdma1,这里gdma表示gdma engine, 0表示第0个gdma engine(一个tpu内可能有多个相同的engine), 1表示第0号gdma engine的第1个线程(每个engine可能支持多线程)。
对于bm168xcodegen,之前是需要在上面的code结构体获取相关的数据,而修改后须使用新接口,
修改前:auto gdma_ptr = (uint8_t *)(*bm168x)->gdma_buffer.data();修改后:auto gdma_ptr = (uint8_t *)(*bm168x).get_inst_data(gdma0);
并且对于后续架构处理器的指令生成来说,目前需要存储sdma和hau的指令,所以相关指令也需要添加入bmodel。如下图所示(这里主要用到了flatbuffer操作):
codegen_save
总结从中可以看出,tpu-mlir虽然能够满足当前tpu上的基本需求,但随着应用场景的扩展和tpu架构的不断演进,其需要满足很多新的要求。这就需要开发者不断思考和挖掘新的接口和架构,使其具有一定的扩展性和适应性。欢迎并感谢各位有识之士为tpu-mlir多提建议,贡献代码!

采用OPA128的精密光电检测电路
京东自营Nvidia显卡不再支持7天无理由退货:原因竟是这个?
计算机系统中哈希表的优化
光控电路图解析
液压拉力机的工作原理及技术参数
如何适配新架构?TPU-MLIR代码生成CodeGen全解析!
伟世通使用NI LabVIEW控制设计和仿真模块简化汽车动力总成控制
电瓶车投诉中电池问题占6成
工程师的鄙视链,很多人已躺枪
在开关模式电源中,当脉冲被忽略时......
关于感应位置传感器和电路的常见问题的解答
信号的抽样与恢复(PAM)
李开复“口误”引科技界恐慌?用户隐私泄露再被推上风口浪尖
领邦仪器总经理崔忠伟:如何应对兵器检测设备
爱立信南京工厂成功部署了第1000个NB-IoT终端应用
老年人没手机也能用健康码的五种方法
探析内窥镜行业现状
亚马逊云科技通过大语言模型及知识库接入,构建智能客服并丰富对话内容
固态电容的作用_固态电容的优点
华米科技CES2020新发布产品汇总