你或许也想拥有专属于自己的AI模型文件格式(推理部署篇)-(7)

距离上次的文章,已经有一个月之久了。要是再不继续推进,那么我17个粉丝又要催更了(纯属本人瞎说,实际情况是没人催更)。

今天就别吵了,直接上路吧!

上次的文章中,说明了如何在C++代码中解析我们的专AI模模型文件格式,大概的思路无非和构建模型的时候是反着进行的。因为这份模型文件格式是完全由flatbuffers进行解析的,因此,解析的过程是一场清晰明了的。

而之所以解析模型文件,主要是这样我们就能够实现专AI模的跨平台传送,构建好的模型能够无差异地在各个设备上使用,这正是flatbuffers作为数据交换协议的功劳。另外一个比较重要的原因是我们只有解析模型后,知道了所有的网络层、有向图中数据流的走向,才能够进行运行时(就是实际部署后的模型代码构成)推理接口的搭建。

既然模型解析部分已经完成,那么我们可以如何处理数据背后的变量数据呢?或者如何将模型部署到对应的设备上?这是我们“推理部署​​”的核心部分。

推理部署理念:通过各种方式将深度学习训练好的模型部署到相应的设备上,实现模型的实际应用,并期望部署后达到最快的速度和最好的准确率。如果你没有接触过深度学习模型部署,你可能会觉得一时难以理解,但是例如:非传统的人脸解锁、非传统的指纹解锁……这些是使用最广泛的推理部署示例.

一、推理与部署的思路

如果之前了解过推理部署框架,对于诸如TVM、NCNN、TNN、MNN等等或许会有所了解。这些框架大多都遵循以下所示的系统框架构建模式,这也是一般的推理框架的主要思路。

你或许也想拥有专属于自己的AI模型文件格式(推理部署篇)-(7)

《推理部署》系列文章的主要目的是实现以上三个方面的模块。以上前端分析已经完成。本文主要针对运行时网络搭建和推理过程搭建。

2. 运行时网络建设

目的:从前端解析出来的模型的所有信息,我们需要利用这些信息来生成各个网络层的运行时核,优化这些核,最后持久化这些优化的核算子。这样就可以重复调用对应的核心算子,无需后续推理的开销,从而达到加速的目的。

    // 通过PzkM来创建一个OCL后端运行时
    bool CreateNetWork(PzkM model){
        // 1.首先构建所有的Tensor
        for(size_t i = 0; i < model.rTensors.size(); i++){
            if(CreateClMem(&model.rTensors[i]) == false){
                printf("create clmem faided in id = %d\n", model.rTensors[i].id);
                return false;
            }
        }
        // 2.设置Tensor作为输入
        std::vector<struct TensorsS*> inputs;
        for(size_t i = 0; i < model.model_runtime_input_id.size(); i++){
            for(size_t j = 0; j < model.rTensors.size(); j++){
                if (model.rTensors[j].id == model.model_runtime_input_id[i]){
                    inputs.push_back(&(model.rTensors[j]));
                }
            }
        }
        SetAsInputs(inputs);
        for(size_t i = 0; i < inputs.size(); i++){
            input_tensors[inputs[i]->id] = *inputs[i];
        }
        // 3.进行网络层的运行时构建
        if(BuildLayers(model) == false){
            printf("failed build runtime layers\n");
        }
        // 4.设置Tensor作为输出
        std::vector<struct TensorsS*> outputs;
        for(size_t i = 0; i < model.model_runtime_output_id.size(); i++){
            for(size_t j = 0; j < model.rTensors.size(); j++){
                if (model.rTensors[j].id == model.model_runtime_output_id[i]){
                    outputs.push_back(&(model.rTensors[j]));
                }
            }
        }
        SetAsOutputs(outputs);
        for(size_t i = 0; i < outputs.size(); i++){
            output_tensors[outputs[i]->id] = *outputs[i];
        }
        return true;
    }

上述的代码通过注释的方式来说了如何把一个PzkM(这是前端解析好的包含了模型信息的类实例)构建其运行时网络。而其中最为重要的是3.进行网络层的运行时构建,下面的代码展示了BuildLayers()函数的主要代码(目前只写了两种类型的网络层的核心代码,分别是img2col和conv2d):

    // 构建运行时的网络层
    bool BuildLayers(PzkM model){
        for (size_t i = 0; i < model.rLayers.size(); i++){
            // 进行各种不同类型的选择
            layer_maker onelayer = model.rLayers[i];
            if (onelayer.type == "img2col"){
                add_img2col_layer(onelayer);
            }else if (onelayer.type == "conv2d"){
                add_conv2d_layer(onelayer);
            }
            else{
                printf("unknown type = %s layer, cant't finish it\n", onelayer.type.c_str());
                return false;
            }
        }
        return true;
    }

上述的BuildLayers中,我们可以知道网络层的主要思想就是:在排布好网络层的顺序之后,按照不同的网络层实行不同的构建函数,依次构建出整个网络。

3. 在推理时构建

目的:在执行运行时构建的网络层的核心功能时,我们需要处理它们之间的依赖关系;另外,我们需要做好同步和异步接口的封装。

原因:很多推理和部署平台都与硬件密切相关,尤其是内核主要用低级编程语言编写的超快平台;更通用的平台更异构(多处理器,异步操作)。我们需要对并行编程和同步机制有非常深入的了解,才能为这一步构建框架。

特别的,我们这次使用到了OpenCL并行编程语言来作为加速手段(PS:其实在上述的算子核心编写的时候就是用OpenCL来构建的,特别是采用了OpenCL的核心运行时编译优化手段)。在推理时借助OpenCL主要完成了核心命令队列的数据依赖、同步异步的接口实现。

    // 进行推理的接口
    bool Inference(){
        // 首先input:cpu->device
        for(size_t i = 0; i < input_tensors.size(); i++){
            if(WriteCLMem(&input_tensors[i], cpu_mem[input_tensors[i].id]) == false){
                printf("write CLmem in inference, which id = %d\n", input_tensors[i].id);
                return false;
            }
        }
        // 然后进行推理
        for(size_t i = 0; i < AllLayers.size(); i++){
            AllLayers[i]->run();
        }
        // 最后ouput:device->cpu
        for(size_t i = 0; i < output_tensors.size(); i++){
            if(ReadCLMem(&output_tensors[i], cpu_mem[output_tensors[i].id]) == false){
                printf("read CLmem in inference, which id = %d\n", output_tensors[i].id);
                return false;
            }
        }
        return true;
    }

上述的Inference接口说明了推理的一般流程:把数据从cpu内存送入到异构设备;往命令推理队列中不断地下发推理指令;最后把推理结果从异构设备传到cpu内存上。

那么我们如何实现了数据依赖、以及同步异步的接口呢?答案在OpenCL的命令队列中:

// opencl推理引擎的命令空间
// 想要通过效仿acl-opencl的推理流程来构建自己的推理引擎
/*
1、希望opencl平台的设备管理等方面由命令空间管理
2、提供了对CLmem的操作手段,包括创建、读写等
3、提供了CLKernel排对进入命令队列的操作、以及对CLKernel的管理
4、命令队列查询、以及等待操作等
*/
namespace OCLEngine {
    /*---------------------------------为了能够进行事件同步而设置前向链表结构--------------------------*/
    struct wait_event{
        cl_uint num = 0;
        cl_event* event = NULL;
        wait_event* next = NULL;
    };
    // 事件同步产生的变量
    wait_event* wehead = NULL; // 链表头
    wait_event* wenow = NULL; // 目前的链表
    wait_event* weend = NULL; // 链表尾
    std::unordered_map<uint32_t, cl_event*> event_of_tensor;
    // 添加节点
    void add_wait_event_node(cl_uint event_num){
        if(wehead == NULL){
            wehead = (wait_event*)malloc(sizeof(wait_event));
            wehead->num = event_num;
            wehead->event = (cl_event*)malloc(sizeof(cl_event) * wehead->num);
            weend = wehead;
        }
        else{
            weend->next = (wait_event*)malloc(sizeof(wait_event));
            weend = weend->next;
            weend->num = event_num;
            weend->event = (cl_event*)malloc(sizeof(cl_event) * wehead->num);
        }
    }
    // 清除事件同步链表
    void CleanEvent(){
        wait_event* ptr = wehead;
        while(ptr != NULL){
            struct wait_event* ptr1 = ptr->next;
            if(ptr->event != NULL && ptr->num > 0){
                for(size_t i = 0; i < ptr->num; i++){
                    clReleaseEvent(*(ptr->event + i));
                }
                free(ptr->event);
            }
            free(ptr);
            ptr = ptr1;
        }
        return;
    }
    // 只是单纯释放
    void ReleaseEvent(){
        struct wait_event* ptr = wehead;
        while(ptr != NULL){
            struct wait_event* ptr1 = ptr->next;
            if(ptr->event != NULL && ptr->num > 0){
                for(size_t i = 0; i < ptr->num; i++){
                    clReleaseEvent(*(ptr->event + i));
                    *(ptr->event + i) = NULL;
                }
            }
            ptr = ptr1;
        }
        return;
    }
    /*---------------------------------------------opencl基本变量------------------------------------*/
    // opencl平台持久化变量
    cl_context context = 0;
    cl_command_queue commandQueue = 0;
    // cl_program program = 0;
    cl_device_id device = 0;
    // cl_kernel kernel = 0;
    std::unordered_map<uint32_t, cl_mem> clmem;
    cl_int errNum;
/*后续代码没有放出来*/
}

是的,这里的想法是将每个网络层绑定到一个事件。网络层运行成功后,该事件将被标记为成功;而网络层核心计算的开始需要将上一层的事件标记为 成功后才能开始操作。这样,就可以形成一个顺序的操作链。

如此,我们针对那个专AI模的来开发的推理引擎的大体框架就完成了。后续的任务就是算子的具体优化和开发,也就是所谓的算子开发了。

版权声明:本文为博主慷仔原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/Pengcode/article/details/123299302

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2022年3月6日 下午2:51
下一篇 2022年3月6日 下午3:08

相关推荐