计算库层

C++计算库

以算法的执行为例,按照算法的执行先后顺序展开介绍:

1.加载bmodel模型

2.预处理

3.推理

加载bmodel模型

首先是加载 bmodel 模型、管理 bmruntime 的初始化、提取网络信息,并为模型推理阶段准备输入输出张量。

  • 构造函数 BMNNContext:这是一个类的构造函数,用于初始化类实例。它接收一个 BMNNHandlePtr 类型的句柄 handle 和一个字符串指针 bmodel_file,代表要加载的 bmodel 文件路径。m_handlePtr(handle):将传入的 handle 赋值给类成员 m_handlePtr。bm_handle_t hdev = m_handlePtr->handle();:通过句柄获取硬件设备句柄 hdev。bmrt_create(hdev):基于设备句柄创建 bmruntime 上下文对象。若创建失败,会打印错误信息并退出。bmrt_load_bmodel:从指定的 bmodel 文件中加载模型。如果失败,输出错误信息。load_network_names():调用该函数来加载网络的名字。
  • bmrt_get_network_number:获取当前 bmodel 中包含的网络数量。bmrt_get_network_names:获取所有网络的名称,保存在 names 中。m_network_names.push_back(names[i]):将网络名称保存到类成员变量 m_network_names 的容器中。free(names):释放 names 指针的内存。
  • BMNNNetwork 构造函数:通过模型名称 name 来创建一个网络实例,并初始化相关的张量。bmrt_get_bm_handle(bmrt):获取 bmruntime 的设备句柄,并将其转换为 bm_handle_t 类型。bmrt_get_network_info:通过模型名称获取该模型的详细信息,存储在 m_netinfo 中。m_max_batch:用来记录模型支持的最大批次。batches.push_back:遍历所有的阶段,获取输入形状的第一个维度(通常是批次大小),并将其添加到批次列表中。记录最大的批次大小。m_inputTensors 和 m_outputTensors:分别为输入和输出张量分配内存空间。张量初始化:为每个输入张量设置数据类型(dtype)、形状(shape)、存储模式(st_mode),并将其初始化为空的设备内存。
BMNNContext(BMNNHandlePtr handle, const char* bmodel_file):m_handlePtr(handle){

    bm_handle_t hdev = m_handlePtr->handle();

    // init bmruntime contxt
    m_bmrt = bmrt_create(hdev);
    if (NULL == m_bmrt) {
    std::cout << "bmrt_create() failed!" << std::endl;
    exit(-1);
    }

    // load bmodel from file
    if (!bmrt_load_bmodel(m_bmrt, bmodel_file)) {
    std::cout << "load bmodel(" << bmodel_file << ") failed" << std::endl;
    }

    load_network_names();

}

...

void load_network_names() {

    const char **names;
    int num;

    // get network info
    num = bmrt_get_network_number(m_bmrt);
    bmrt_get_network_names(m_bmrt, &names);

    for(int i=0;i < num; ++i) {
    m_network_names.push_back(names[i]);
    }

    free(names);
}

...

BMNNNetwork(void *bmrt, const std::string& name):m_bmrt(bmrt) {
    m_handle = static_cast<bm_handle_t>(bmrt_get_bm_handle(bmrt));

    // get model info by model name
    m_netinfo = bmrt_get_network_info(bmrt, name.c_str());

    m_max_batch = -1;
    std::vector<int> batches;
    for(int i=0; i<m_netinfo->stage_num; i++){
        batches.push_back(m_netinfo->stages[i].input_shapes[0].dims[0]);
        if(m_max_batch<batches.back()){
            m_max_batch = batches.back();
        }
    }
    m_batches.insert(batches.begin(), batches.end());
    m_inputTensors = new bm_tensor_t[m_netinfo->input_num];
    m_outputTensors = new bm_tensor_t[m_netinfo->output_num];
    for(int i = 0; i < m_netinfo->input_num; ++i) {

        // get data type
        m_inputTensors[i].dtype = m_netinfo->input_dtypes[i];
        m_inputTensors[i].shape = m_netinfo->stages[0].input_shapes[i];
        m_inputTensors[i].st_mode = BM_STORE_1N;
        m_inputTensors[i].device_mem = bm_mem_null();
    }

...

- FFALIGN(m_net_w, 64):这行代码用于对 m_net_w(网络的宽度)进行64字节对齐。FFALIGN 函数通常用于确保内存地址或大小是指定字节的倍数,以便提高内存访问的效率。
aligned_net_w:对齐后的网络宽度。


}

预处理

代码首先对网络的宽度进行64字节对齐,以确保高效的内存访问。然后根据对齐后的宽度创建图像对象,用于存储推理结果和推理输入。 图像对象的内存分配是连续的,确保批处理时数据在内存中紧凑排列,减少内存碎片。数据类型处理:根据模型的输入数据类型(FP32 或 INT8),动态调整输入图像的存储格式,保证图像数据与推理需求匹配。

  • FFALIGN(m_net_w, 64):这行代码用于对 m_net_w(网络的宽度)进行64字节对齐。FFALIGN 函数通常用于确保内存地址或大小是指定字节的倍数,以便提高内存访问的效率。 aligned_net_w:对齐后的网络宽度。
  • bm_image_create:为每个批次创建 bm_image 对象。此函数将生成 max_batch 数量的图像对象,每个图像的尺寸为 m_net_h x m_net_w,格式为 FORMAT_RGB_PLANAR,数据类型为 DATA_TYPE_EXT_1N_BYTE(1字节无符号整数)。m_resized_imgs[i]:图像数组中的每个元素将用于存储推理结果。strides:使用上面定义的步长数组,确保每个通道数据的对齐。assert(BM_SUCCESS == ret):检查图像创建是否成功,若不成功则程序将终止。
  • bm_image_alloc_contiguous_mem:为 m_resized_imgs 数组中的图像分配连续的内存。这样做的好处是,多个图像共享同一段连续的内存区域,这可能会提高推理和数据传输的效率,尤其在批量处理时。max_batch:一次为所有批次的图像分配内存。m_resized_imgs.data():提供图像数组的起始地址。
  • bm_image_data_format_ext img_dtype:定义输入图像的格式,这里初始为 DATA_TYPE_EXT_FLOAT32,表示数据类型为 32 位浮点数。if (tensor->get_dtype() == BM_INT8):检查模型输入的张量类型。如果张量的数据类型为 BM_INT8(8位整型),则将 img_dtype 修改为 DATA_TYPE_EXT_1N_BYTE_SIGNED(1字节有符号整数)。这一步是根据推理引擎支持的输入数据类型动态调整图像的存储格式。
  • bm_image_create_batch:用于创建一个批次的 bm_image 对象,所有批次的图像共用同样的格式和大小:m_net_h, m_net_w:图像的高度和宽度。FORMAT_RGB_PLANAR:图像格式为分离的 RGB 通道。img_dtype:图像的实际数据类型(根据推理需求为 FP32 或 INT8)。m_converto_imgs.data():用于存储批次图像的数组。max_batch:批次大小,即一次推理的输入图像数量。
int aligned_net_w = FFALIGN(m_net_w, 64);
int strides[3] = {aligned_net_w, aligned_net_w, aligned_net_w};
for(int i=0; i<max_batch; i++){

    // init bm images for storing results
    auto ret= bm_image_create(m_bmContext->handle(), m_net_h, m_net_w,
        FORMAT_RGB_PLANAR,
        DATA_TYPE_EXT_1N_BYTE,
        &m_resized_imgs[i], strides);
    assert(BM_SUCCESS == ret);
}
bm_image_alloc_contiguous_mem (max_batch, m_resized_imgs.data());

// bm images for storing inference inputs
bm_image_data_format_ext img_dtype = DATA_TYPE_EXT_FLOAT32;   //FP32


if (tensor->get_dtype() == BM_INT8) {   // INT8
    img_dtype = DATA_TYPE_EXT_1N_BYTE_SIGNED;
}

auto ret = bm_image_create_batch(m_bmContext->handle(), m_net_h, m_net_w,
    FORMAT_RGB_PLANAR,
    img_dtype,
    m_converto_imgs.data(), max_batch);
assert(BM_SUCCESS == ret);

解码视频或图片帧

从视频图片或摄像头中读取一帧图像,并将其存储到 std::vectorcv::Mat 向量中。读取失败时,程序会输出错误信息并终止。如果成功读取到帧,可以使用 images 向量进行后续图像处理或分析。

  • cv::Mat:cv::Mat 是 OpenCV 中用来存储图像的主要数据结构。它可以表示一个图像、视频帧或其他矩阵数据。
  • cap.read(img):cap 是一个 cv::VideoCapture 对象,用于读取视频或摄像头流中的帧。read() 函数从视频流中读取一帧,并将其存储到 img 中(即 cv::Mat 类型)。如果读取成功,img 将包含该帧的图像数据。如果读取失败或到达文件结尾,cap.read(img) 会返回 false。
  • std::vectorcv::Mat images:定义一个 cv::Mat 类型的向量,表示图像列表。这个向量可以存储多帧图像。images.push_back(img):将刚刚读取的帧 img 添加到 images 向量中。这样可以累积从视频流中读取的多帧图像,用于后续处理或推理操作。


// get one mat
cv::Mat img;
if (!cap.read(img)) { //check
    std::cout << "Read frame failed or end of file!" << std::endl;
    exit(1);
}

std::vector<cv::Mat> images;
images.push_back(img);

图像处理

根据图像的宽度和高度进行裁剪和填充操作,确保图像符合推理网络的输入尺寸。将图像数据转换为模型所需的格式,并进行归一化处理。将转换后的图像数据存储在设备内存中,并将其与模型的输入张量对接,保证推理时的数据一致性。

  • bmcv_padding_atrr_t:这是用于图像填充操作的结构体,定义了填充区域的属性。memset:初始化 padding_attr,将所有字段设置为 0。dst_crop_stx 和 dst_crop_sty:指定裁剪(crop)区域的起始位置,初始化为 0 表示从图像左上角开始。padding_b/g/r:定义填充的颜色,这里填充为 RGB 值 (114, 114, 114),通常用于保持图像背景的一致性。if_memset:标识是否使用填充。
  • 当 isAlignWidth 为 true 时,图像高度保持比例缩放,裁剪宽度与网络输入宽度 (m_net_w) 对齐;反之则裁剪高度与网络输入高度 (m_net_h) 对齐。
  • bmcv_rect_t:定义一个矩形区域,这里指定从 (0, 0) 开始,裁剪整个图像区域。
  • 调用 BMCV 函数,将图像进行填充、调整大小和裁剪。image_aligned:输入图像。m_resized_imgs[i]:输出图像存储位置。padding_attr:定义了裁剪和填充的属性。crop_rect:指定裁剪区域。
  • input_scale:从输入张量中获取缩放系数,并除以 255(标准化处理),用于将像素值缩放到模型输入所需的范围。bmcv_convert_to_attr:这是 BMCV 中的图像格式转换结构体,定义了转换的缩放系数(alpha)和偏移量(beta)。在这里,分别为三个通道(R、G、B)设置相同的缩放和偏移。
  • 如果当前输入的 image_n 不等于模型的最大批次大小,则通过 get_nearest_batch 获取与当前输入最接近的批次大小。使用 bm_image_get_contiguous_device_mem 获取图像在设备中的连续内存区域,并将其赋值给 input_dev_mem。将获取到的设备内存附加到输入张量上,确保模型在推理时能够正确使用输入数据。通过 set_shape_by_dim 方法调整张量的批次维度,确保模型在推理时使用正确的批次大小。
// set padding_attr
bmcv_padding_atrr_t padding_attr;
memset(&padding_attr, 0, sizeof(padding_attr));
padding_attr.dst_crop_sty = 0;
padding_attr.dst_crop_stx = 0;
padding_attr.padding_b = 114;
padding_attr.padding_g = 114;
padding_attr.padding_r = 114;
padding_attr.if_memset = 1;
if (isAlignWidth) {
  padding_attr.dst_crop_h = images[i].rows*ratio;
  padding_attr.dst_crop_w = m_net_w;

  int ty1 = (int)((m_net_h - padding_attr.dst_crop_h) / 2);
  padding_attr.dst_crop_sty = ty1;
  padding_attr.dst_crop_stx = 0;
}else{
  padding_attr.dst_crop_h = m_net_h;
  padding_attr.dst_crop_w = images[i].cols*ratio;

  int tx1 = (int)((m_net_w - padding_attr.dst_crop_w) / 2);
  padding_attr.dst_crop_sty = 0;
  padding_attr.dst_crop_stx = tx1;
}

// do not crop
bmcv_rect_t crop_rect{0, 0, image1.width, image1.height};

auto ret = bmcv_image_vpp_convert_padding(m_bmContext->handle(), 1, image_aligned, &m_resized_imgs[i],
    &padding_attr, &crop_rect);

...

// set converto_attr
float input_scale = input_tensor->get_scale();
input_scale = input_scale* (float)1.0/255;
bmcv_convert_to_attr converto_attr;
converto_attr.alpha_0 = input_scale;
converto_attr.beta_0 = 0;
converto_attr.alpha_1 = input_scale;
converto_attr.beta_1 = 0;
converto_attr.alpha_2 = input_scale;
converto_attr.beta_2 = 0;

// do converto
ret = bmcv_image_convert_to(m_bmContext->handle(), image_n, converto_attr, m_resized_imgs.data(), m_converto_imgs.data());

// attach to tensor
if(image_n != max_batch) image_n = m_bmNetwork->get_nearest_batch(image_n);
bm_device_mem_t input_dev_mem;
bm_image_get_contiguous_device_mem(image_n, m_converto_imgs.data(), &input_dev_mem);
input_tensor->set_device_mem(&input_dev_mem);
input_tensor->set_shape_by_dim(0, image_n);  // set real batch number

推理

预处理过程的output是推理过程的input,当推理过程的input数据准备好后,就可以进行推理。

Python计算库

SOPHONSDK通过SAIL库向用户提供Python编程接口。SAIL(SOPHON Artificial Intelligent Library)是 SOPHON Inference 的核心组件。它封装了 SOPHONSDK 中的 BMLib、sophon-mw、BMCV 和 BMRuntime,将复杂的底层操作简化为易于使用的 C++ 接口。通过 SAIL,用户可以轻松实现诸如“加载 bmodel 并运行智能视觉深度学习处理器进行推理”、“结合 VPP 进行图像处理”、“使用 VPU 进行图像和视频解码”等功能。此外,SAIL 还通过 pybind11 进一步封装,提供简洁直观的 Python 接口,大大提升了开发效率和用户体验。

模型加载

  • sophon.sail:这是 SOPHON SAIL 的 Python 接口模块,通过 pybind11 封装了底层 C++ API,使得用户可以在 Python 环境中使用 SAIL 提供的功能。
  • sail.Engine:Engine 是 SAIL 中用于推理的核心类,它负责加载模型、管理设备、以及执行推理操作。model_path:模型文件的路径,通常为已编译的 bmodel 文件。device_id:指定使用的设备 ID(通常指的是 SOPHON 处理器的编号),用于在多设备环境中选择推理设备。io_mode:输入输出模式,指定如何传递输入数据和接收推理结果。例如,可以通过同步或异步方式传递数据。
import sophon.sail as sail

...

engine = sail.Engine(model_path, device_id, io_mode)

...

预处理

实现了一个图像预处理类 PreProcess,主要负责对输入图像进行尺寸调整和归一化操作,以便为后续的推理模型准备数据。

  • width 和 height:表示目标图像的宽度和高度,用于调整输入图像的尺寸。batch_size:处理图像的批量大小,通常用于一次处理多张图像。img_dtype:图像的数据类型,决定了图像在内存中的存储方式(如 FP32 或 INT8)。input_scale:用于图像归一化的缩放因子。如果未指定,默认设置为 1.0。self.std:标准差数组,初始化为 [255., 255., 255.],用于将像素值从 [0, 255] 转换到 [0, 1]。self.use_resize_padding:决定是否在调整图像尺寸时保持纵横比并进行填充。self.use_vpp:指定是否使用 VPP(视频处理器)来加速图像处理操作。
  • use_resize_padding 为 True:保持图像的纵横比,可能会在图像周围添加边框以适应目标尺寸。具体步骤如下:根据图像的原始宽高和目标宽高,计算图像在目标尺寸中的缩放比率 r_w 和 r_h。计算填充位置和大小,利用 sail.PaddingAtrr 设置填充属性,颜色为 114(通常是灰色背景)。使用 bmcv.crop_and_resize_padding 或 bmcv.vpp_crop_and_resize_padding 函数进行裁剪和调整。
  • use_resize_padding 为 False:直接将图像调整为目标尺寸,不做填充处理。
  • bm_array:根据批量大小创建一个 BMImageArray 对象,用于存储批量预处理后的图像。a = 1 / self.std:计算每个颜色通道的缩放因子,将像素值从 [0, 255] 归一化到 [0, 1]。alpha_beta:alpha 表示缩放因子,beta 表示偏移量。在图像归一化过程中,像素值将按比例缩放,同时可以设置偏移量以适应模型的需求。bmcv.convert_to:该函数将归一化后的图像存储在 preprocessed_imgs 中,最终返回该批量图像。
class PreProcess:
    def __init__(self, width, height, batch_size, img_dtype, input_scale=None):

        self.std = np.array([255., 255., 255.], dtype=np.float32)
        self.batch_size = batch_size
        self.input_scale = float(1.0) if input_scale is None else input_scale
        self.img_dtype = img_dtype

        self.width = width
        self.height = height
        self.use_resize_padding = True
        self.use_vpp = False
        ...

    def resize(self, img, handle, bmcv):

        if self.use_resize_padding:
            img_w = img.width()
            img_h = img.height()
            r_w = self.width / img_w
            r_h = self.height / img_h

            if r_h > r_w:
                tw = self.width
                th = int(r_w * img_h)
                tx1 = tx2 = 0
                ty1 = int((self.height - th) / 2)
                ty2 = self.height - th - ty1

            else:
                tw = int(r_h * img_w)
                th = self.height
                tx1 = int((self.width - tw) / 2)
                tx2 = self.width - tw - tx1
                ty1 = ty2 = 0

            ratio = (min(r_w, r_h), min(r_w, r_h))
            txy = (tx1, ty1)
            attr = sail.PaddingAtrr()
            attr.set_stx(tx1)
            attr.set_sty(ty1)
            attr.set_w(tw)
            attr.set_h(th)
            attr.set_r(114)
            attr.set_g(114)
            attr.set_b(114)

            tmp_planar_img = sail.BMImage(handle, img.height(), img.width(),
                                      sail.Format.FORMAT_RGB_PLANAR, sail.DATA_TYPE_EXT_1N_BYTE)
            bmcv.convert_format(img, tmp_planar_img)
            preprocess_fn = bmcv.vpp_crop_and_resize_padding if self.use_vpp else bmcv.crop_and_resize_padding
            resized_img_rgb = preprocess_fn(tmp_planar_img,
                                        0, 0, img.width(), img.height(),
                                        self.width, self.height, attr)
        else:
            r_w = self.width / img.width()
            r_h = self.height / img.height()
            ratio = (r_w, r_h)
            txy = (0, 0)
            tmp_planar_img = sail.BMImage(handle, img.height(), img.width(),
                                        sail.Format.FORMAT_RGB_PLANAR, sail.DATA_TYPE_EXT_1N_BYTE)
            bmcv.convert_format(img, tmp_planar_img)
            preprocess_fn = bmcv.vpp_resize if self.use_vpp else bmcv.resize
            resized_img_rgb = preprocess_fn(tmp_planar_img, self.width, self.height)
        return resized_img_rgb, ratio, txy

    ...

    def norm_batch(self, resized_images, handle, bmcv):

        bm_array = eval('sail.BMImageArray{}D'.format(self.batch_size))

        preprocessed_imgs = bm_array(handle,
                                 self.height,
                                 self.width,
                                 sail.FORMAT_RGB_PLANAR,
                                 self.img_dtype)

        a = 1 / self.std
        b = (0, 0, 0)
        alpha_beta = tuple([(ia * self.input_scale, ib * self.input_scale) for ia, ib in zip(a, b)])

        # do convert_to
        bmcv.convert_to(resized_images, preprocessed_imgs, alpha_beta)
        return preprocessed_imgs

推理

SophonInference 类通过封装 Sophon SDK 的功能,简化了模型推理的过程。该类支持输入和输出张量的自动管理,方便在深度学习应用中进行快速推理。整个类设计关注于使用简单的接口进行复杂的深度学习推理操作,使得用户能够更轻松地利用 Sophon 平台的硬件加速性能。

  • 初始化 SophonInference 类的实例,设置模型路径、设备 ID 和 I/O 模式。self.io_mode:设置 I/O 模式为 SYSIO,表示使用系统 I/O。self.engine:创建 sail.Engine 实例,加载模型并准备进行推理。self.handle:获取与模型引擎相关联的句柄,以便在后续操作中使用。self.graph_name:从引擎中获取第一个图的名称,通常这是我们要进行推理的图。self.bmcv:创建 sail.Bmcv 实例,提供图像处理功能。
  • 据输入的形状和数据类型,创建 sail.Tensor 对象并存储在 input_tensors 字典中。True 参数表示张量需要进行内存分配。
  • 获取模型输出的名称、形状、数据类型和缩放因子,并创建相应的输出张量。所有输出张量都被存储在 output_tensors 字典中,以便在推理后提取结果。
  • 调用 self.engine.process 方法执行推理,该方法接收图名称、输入张量和输出张量作为参数。创建一个 OrderedDict 来存储推理结果,确保输出的顺序与输入一致。从 output_tensors 中提取结果,调用 asnumpy() 方法将张量转换为 NumPy 数组,最后应用输出缩放因子。
  • 函数返回一个字典,包含所有输出张量及其对应的结果。
class SophonInference:
    def __init__(self, **kwargs):

        ...

        self.io_mode = sail.IOMode.SYSIO
        self.engine = sail.Engine(self.model_path, self.device_id, self.io_mode)
        self.handle = self.engine.get_handle()
        self.graph_name = self.engine.get_graph_names()[0]
        self.bmcv = sail.Bmcv(self.handle)

        ...

        input_names = self.engine.get_input_names(self.graph_name)
        for input_name in input_names:

            input_shape = self.engine.get_input_shape(self.graph_name, input_name)
            input_dtype = self.engine.get_input_dtype(self.graph_name, input_name)
            input_scale = self.engine.get_input_scale(self.graph_name, input_name)
            ...
            if self.input_mode:
                input = sail.Tensor(self.handle, input_shape, input_dtype, True, True)
            ...
            input_tensors[input_name] = input
            ...

        output_names = self.engine.get_output_names(self.graph_name)

        for output_name in output_names:

            output_shape = self.engine.get_output_shape(self.graph_name, output_name)
            output_dtype = self.engine.get_output_dtype(self.graph_name, output_name)
            output_scale = self.engine.get_output_scale(self.graph_name, output_name)
            ...
            if self.input_mode:
                output = sail.Tensor(self.handle, output_shape, output_dtype, True, True)
            ...
            output_tensors[output_name] = output
            ...
    def infer_bmimage(self, input_data):
        self.get_input_feed(self.input_names, input_data)

        #inference
        self.engine.process(self.graph_name, self.input_tensors, self.output_tensors)
        outputs_dict = OrderedDict()
        for name in self.output_names:
            outputs_dict[name] = self.output_tensors[name].asnumpy().copy() * self.output_scales[name]
        return outputs_dict