Tengine深度学习推理框架代码梳理(第一次)

review Tengine code

随便找一个测试的example看一下,init_tengine( )函数被用来初始化tengine。
来看一下init_tengine( )这个函数的构成,tengine_c_api.c中代码如下:

int DLLEXPORT init_tengine(void)
{
    // if (enable_intern_allocator)
    //     enable_intern_allocator();

    set_log_level(LOG_ERR);

    if (enable_mem_stat)
        enable_mem_stat();

    int ret = 0;

    ret = init_op_name_map();
    if (0 != ret)
    {
        TLOG_ERR("init map of operator names failed: %d\n", ret);
        return ret;
    }

    ret = init_op_registry();
    if (0 != ret)
    {
        TLOG_ERR("register operators failed: %d\n", ret);
        return ret;
    }

    ret = init_nn_dev_registry();
    if (0 != ret)
    {
        TLOG_ERR("register device failed: %d\n", ret);
        return ret;
    }

    ret = init_serializer_registry();
    if (0 != ret)
    {
        TLOG_ERR("register serializer failed: %d\n", ret);
        return ret;
    }

    ret = exec_module_init(0);
    if (0 != ret)
    {
        TLOG_ERR("init exec module failed: %d\n", ret);
        return ret;
    }

    return 0;
}

我们在这里逐句分析:

  • set_log_level(LOG_ERR);这个函数有点类似linux内核中的操作,用于设置log的等级,这样在后面就可以选择性的打印log。

跟踪进这个函数:

void DLLEXPORT set_log_level(enum log_level level)
{
    SET_LOG_LEVEL(level);
}

继续:

#define SET_LOG_LEVEL(level)                          \
    do                                                \
    {                                                 \
        struct logger* logger = get_default_logger(); \
        logger->set_log_level(logger, level);         \
    } while(0)

到这里我们看到了logger这个结构体,这个应该是整个Tengine负责日志管理的组件,查看一下logger的结构,tengine_log.h中定义如下:

struct logger
{
    const char* prefix;
    int log_level;
    struct log_option option;

    void (*output_func)(const char*);

    void (*log)(struct logger*, int level, const char* fmt, ...);
    void (*set_log_level)(struct logger*, int level);
    void (*set_output_func)(struct logger*, void (*func)(const char*));
};

现在先回到上面定义的宏SET_LOG_LEVEL中logger被get_default_logger();函数初始化了。进入函数中查看:

struct logger* get_default_logger(void)
{
    static int inited = 0;
    static struct logger default_logger;

    if (inited)
        return &default_logger;

    lock(&log_lock);

    if (!inited)
    {
        inited = 1;

        default_logger.prefix = NULL;
        default_logger.log_level = DEFAULT_LOG_LEVEL;

        default_logger.output_func = output_stderr;
        default_logger.log = do_log;
        default_logger.set_log_level = change_log_level;
        default_logger.set_output_func = set_output_func;

        default_logger.option.print_prefix = 0;
        default_logger.option.print_time = 0;
        default_logger.option.print_level = 0;
    }

    unlock(&log_lock);

    return &default_logger;
}

这个函数用inited这个变量来控制防止重复初始化这个函数,这在设计模式中被称为”单例模式“,一般在这种全局日志组件中被使用。结合上面的logger结构体来看:

  1. default_logger.prefix = NULL; 前缀为NULL

  2. default_logger.log_level = DEFAULT_LOG_LEVEL; 日志等级为DEFAULT_LOG_LEVEL,跳进去查看可以看到日志一共"EMERG", "ALERT", "CRIT", "ERROR", "WARN", "NOTICE", "INFO", "DEBUG"这几种等级。

  3. default_logger.output_func = output_stderr; 这是一个函数指针,去查看这个具体的函数实现。

    static void output_stderr(const char* msg)
    {
        fprintf(stderr, "%s", msg);
    }
    
    

    直接输出msg这个字符串到stderr标准错误流。

  4. default_logger.log = do_log; 查看do_log函数

    static void do_log(struct logger* logger, int level, const char* fmt, ...)
    {
        va_list ap;
        char msg[256];
        int max_len = 256;
        int left = max_len;
        char* p = msg;
        int ret;
    
        if (logger->log_level < level || level > LOG_DEBUG)
            return;
    #ifndef CONFIG_ARCH_CORTEX_M
        if (logger->option.print_time)
        {
            time_t t = time(NULL);
            ret = strftime(p, left, "%Y-%m-%d %X ", localtime(&t));
            left -= ret;
            p += ret;
        }
    #endif
    
        if (left <= 1)
            goto print;
    
        if (logger->option.print_level)
        {
            ret = snprintf(p, left, "%s ", map_table[level]);
            left -= ret;
            p += ret;
        }
    
        if (left <= 1)
            goto print;
    
        if (logger->option.print_prefix && logger->prefix)
        {
            ret = snprintf(p, left, "%s ", logger->prefix);
            left -= ret;
            p += ret;
        }
    
        if (left <= 1)
            goto print;
    
        va_start(ap, fmt);
    
        ret = vsnprintf(p, left, fmt, ap);
    
        va_end(ap);
    
    print:
        msg[max_len - 1] = 0x0;
    
        lock(&log_lock);
    
        logger->output_func(msg);
    
        unlock(&log_lock);
    }
    

    在这个函数下面我们发现了logger->output_func(msg);这个函数,这个是直接打印的。这个函数大家应该都能看懂,就是level小于设置level的不打印,CONFIG_ARCH_CORTEX_M这个先忽略,我们只看PC端的处理。

  5. default_logger.set_log_level = change_log_level; 还是直接去看change_log_level这个函数

    static void change_log_level(struct logger* logger, int level)
    {
        if (level < 0 || level > LOG_DEBUG)
            return;
    
        logger->log_level = level;
    }
    

    很简单,就是直接给log_level赋值。

  6. default_logger.set_output_func = set_output_func; 还是直接去看set_output_func这个函数

    static void set_output_func(struct logger* logger, void (*func)(const char*))
    {
        logger->output_func = func;
    }
    

    该函数用于设置输出函数,默认的输出是stderr,即标准错误流,可以调用set_output_func这个函数去改变输出函数,比如将日志保存在文件中。

  7. 最后这个是打印日志的选项:

​ default_logger.option.print_prefix = 0;

​ default_logger.option.print_time = 0;

​ default_logger.option.print_level = 0;

到这里其实整个logger组件已经捋了一遍,在回头看看init_tengine( )这个函数set_log_level(LOG_ERR);设置日志等级为ERROR,继续查看init_tengine中的函数:

  • ret = init_op_name_map(); 跟踪进去看这个函数:

    int init_op_name_map(void)
    {
        op_map_list = create_vector(sizeof(struct op_map_entry), NULL);
    
        if (op_map_list == NULL)
            return -1;
    
        return 0;
    }
    

    进入create_vector函数中:

    struct vector* create_vector(int elem_size, void (*free_data)(void*))
    {
        struct vector* v = ( struct vector* )sys_malloc(sizeof(struct vector));
    
        if (v == NULL)
            return NULL;
    
        v->elem_num = 0;
        v->elem_size = elem_size;
        v->free_func = free_data;
        v->entry_size = elem_size + sizeof(struct vector_entry);
        v->entry_size = (v->entry_size + VECTOR_ALIGN_SIZE) & (~(VECTOR_ALIGN_SIZE - 1));
    
        v->ahead_num = 8;
    
        v->space_num = v->ahead_num;
    
        v->real_mem = sys_malloc(v->entry_size * v->space_num + VECTOR_ALIGN_SIZE);
        v->mem = ( void* )((( long )v->real_mem) & (~(VECTOR_ALIGN_SIZE - 1)));
    
        for (int i = 0; i < v->space_num; i++)
        {
            struct vector_entry* e = get_vector_entry(v, i);
            e->valid = 0;
        }
    
        return v;
    }
    

    显然这块引入了一种数据结构vector,这是向量,是一种栈(filo):

    struct vector
    {
        int elem_size;
        int elem_num;
    
        int entry_size;
        int space_num;
        int ahead_num;
        void * real_mem;
        void* mem;
        void (*free_func)(void*);
    };
    

    init_op_name_map这个函数实际上就是初始化一个用于装载op名字的向量,这块不用细说,继续往后看。

  • ret = init_op_registry(); 跟踪进去:

    int init_op_registry(void)
    {
        op_list = create_vector(sizeof(struct op_method), NULL);
    
        if (op_list == NULL)
            return -1;
    
        return 0;
    }
    
    struct op_method
    {
        int op_type;
        int op_version;
        int (*init_op)(struct ir_op* op);
        void (*release_op)(struct ir_op* op);
        int (*access_param_entry)(void* param_mem, const char* entry_name, int entry_type, void* buf, int size, int set);
    };
    

    和上面的函数类似,这里也不做介绍,init_op_registry函数实际上就是初始化一个用于注册op的向量,继续向后看:

  • ret = init_nn_dev_registry(); 跟踪进去:

    int init_nn_dev_registry(void)
    {
        dev_list = create_vector(sizeof(struct nn_device*), NULL);
    
        if (dev_list == NULL)
        {
            set_tengine_errno(ENOMEM);
            return -1;
        }
    
        return 0;
    }
    

    和上面一样,这块也是用来初始化一个向量,用于保存nn_device,来具体看看这个结构的内容:

    struct nn_device
    {
        char* name;
    
        int (*init)(struct nn_device* dev);
        int (*prerun)(struct nn_device* dev, struct subgraph* subgraph, int num_thread, int cpu_affinity, int mode);
        int (*run)(struct nn_device* dev, struct subgraph* subgraph);
        int (*postrun)(struct nn_device* dev, struct subgraph* subgraph);
        int (*async_run)(struct nn_device* dev, struct subgraph* subgraph);
        int (*async_wait)(struct nn_device* dev, struct subgraph* subgraph, int try_wait);
        int (*release)(struct nn_device* dev);
        int (*release_exec_graph)(struct nn_device* dev, void* exec_graph);
    };
    

    这个结构里面基本上都是函数指针,先不具体看这里面,猜测这里面应该是实现了一个计算图的结构(一会再看)。

  • ret = init_serializer_registry(); 根据官方README介绍,serializer是模型解析器。追踪进去看:

    int init_serializer_registry(void)
    {
        serializer_list = create_vector(sizeof(struct serializer*), NULL);
    
        if (serializer_list == NULL)
        {
            set_tengine_errno(ENOMEM);
            return -1;
        }
    
        return 0;
    }
    

    同样是初始化一个向量,serializer的结构如下:

    struct serializer
    {
        const char* (*get_name)(struct serializer*);
    
        /* load graph from file */
        int (*load_model)(struct serializer*, struct ir_graph*, const char* fname, va_list ap);
    
        /* load graph from memory */
        int (*load_mem)(struct serializer*, struct ir_graph*, const void* addr, int size, va_list ap);
    
        /* unload graph, free serializer related and device releated  resource */
        int (*unload_graph)(struct serializer*, struct ir_graph*, void* s_priv, void* dev_priv);
    
        /* those interface exposed for operator extension */
        int (*register_op_loader)(struct serializer*, int op_type, int op_ver, void* op_load_func, void* op_type_map_func,
                                  void* op_ver_map_func);
        int (*unregister_op_loader)(struct serializer*, int op_type, int op_ver, void* op_load_func);
    
        /* interface for regiser and unregister */
        int (*init)(struct serializer*);
        int (*release)(struct serializer*);
    };
    

    还是一堆函数指针,后面要对这个结构详细分析,这里先忽略。

  • ret = exec_module_init(0); 字面意思,执行模块初始化,跟踪进去看:

    int exec_module_init(int stop_on_all_error)
    {
        for (int i = 0; i < MOD_MAX_LEVEL; i++)
        {
            struct vector* v = init_vector[i];
    
            if (v == NULL)
                continue;
    
            int vector_num = get_vector_num(v);
    
            for (int j = 0; j < vector_num; j++)
            {
                struct module_init_func_entry* e;
                int ret;
    
                e = ( struct module_init_func_entry* )get_vector_data(v, j);
                ret = e->func(e->arg);
    
                // fprintf(stderr, "on executing %s() \n", e->name);
    
                if (ret < 0 && (stop_on_all_error || e->critical))
                    return -1;
            }
        }
    
        // free the memory used
        for (int i = 0; i < MOD_MAX_LEVEL; i++)
        {
            struct vector* v = init_vector[i];
            if (v == NULL)
                continue;
            release_vector(v);
            init_vector[i] = NULL;
        }
    
        return 0;
    }
    

    今天先这样吧,基本上吧这一个初始化tengine的函数看了一下。有空再继续看(x

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容