RK3588 DMA, RGA, 零拷贝API使用指北

        本文是一篇简单但详细的RK3588 DMA, RGA, 零拷贝API使用指北,讲述了在RK3588上如何串联使用DMA、RGA、零拷贝推理 API 实现从实时视频流解码、数据预处理、数据推理等方面的优化,通过这一些列优化,可以很大程度上降低 RK3588 上 CPU 资源的消耗,节省下来的 CPU 资源能够用于其他功能。

目录

1. DMA缓冲区创建

2. RGA数据预处理

3.零拷贝API执行推理

3.1 零拷贝 API 输入输出数据设置

 3.2 结合 RGA、DMA操作,实现零拷贝API数据预处理与数据传输

 3.3 执行零拷贝API推理

 3.4 获取输出结果

4.优化前后 CPU 消耗对比

4.1 普通拷贝操作和DMA数据传输对比

4.2 RGA前处理和OPENCV前处理对比

4.3 零拷贝API 推理和普通API推理对比


1. DMA缓冲区创建

        DMA(Direct Memory Access,直接内存访问)是指在不经过 CPU 的情况下,由硬件控制器直接在内存与外设之间,或内存与内存之间传输数据的一种机制。普通拷贝操作(如:memcpy)由CPU执行,需要占用CPU时间进行操作,尤其是在多线程的情况下,会极大消耗CPU资源。

        在RK3588上使用DMA创建缓冲区流程如下:

// 创建前需要指定每个 dma 缓冲区的宽、高、size等信息,然后使用 dma_buf_alloc 申请 dma 缓冲区  
// 此处的 dma_heap_path 为  "/dev/dma_heap/system-uncached-dma32"    
for (int i = 0; i < BUFFER_COUNT; ++i) 
{
    buffers[i].index = i;
    buffers[i].ref_count = 0;
    buffers[i].frame_id = 0;
    buffers[i].width = width;
    buffers[i].height = height;
    buffers[i].size = size;
    buffers[i].width_stride = width_stride;
    buffers[i].height_stride = height_stride;
    dma_buf_alloc(dma_heap_path, size, &buffers[i].fd, &buffers[i].va);
}

2. RGA数据预处理

        通常情况,在进行深度学习的数据预处理时,我们一般采用 OPENCV 进行,但是当摄像头输入路数增加时,并行的预处理线程增加时,OPENCV 进行预处理则会占用较大的CPU资源。将有关的数据操作由 OPENCV 替换成了RGA,包括但不限于 letter_box 操作、resize 操作、图像拼接操作、OSD图像叠加操作。同时通过DMA以及零拷贝API的使用,整条数据通路全程由硬件负责,极大优化了CPU资源的占用。

        RGA操作需要的数据类型要求是 rga_buffer_t ,因此在使用 dma 创建完缓冲区 buffer 后,可以将 buffer 指向的文件描述符通过 wrapbuffer_fd 包装成 rga_buffer_t 类型,具体操作如下:        

// 使用 wrapbuffer_fd 同样需要指定数据的宽、高以及格式信息
rga_buffer_t dst = wrapbuffer_fd(buffer->fd, width, height, RK_FORMAT_RGB_888);

        将 dma 缓冲区包装成 rga_buffer_t 类型的数据后,就可以徍 dma 缓冲区中写入需要的数据了,以 RK3588 MPP 硬件解码为例,解码后的数据为 YUV420SP 类型,通常也能直接得到对应解码后数据缓冲区的 fd,此时也可以将此 fd 包装成 rga_buffer_t 类型,并和 dma 缓冲区进行数据交互,具体操作如下:

// 此处假设 fd 来源于 MPP 解码缓冲区,宽、高、步长等信息均可由 MPP 相关接口获得
rga_buffer_t src = wrapbuffer_fd(fd, width, height, RK_FORMAT_YCbCr_420_SP, width_stride, height_stride);
// 然后使用 rga 将解码后的数据转换成深度学习需要用到的 RGB 类型,rga 中有支持格式转换的接口 imcvtcolor
IM_STATUS ret = imcvtcolor(src, dst, RK_FORMAT_YCbCr_420_SP, RK_FORMAT_RGB_888, IM_YUV_TO_RGB_BT601_FULL);
// IM_STATUS 代表正常的返回值竟然有两个不同的,委实令人费解,可以像我这样都写出来,也可以使用 > 0 进行判断操作是否执行成功
 if (ret != IM_STATUS_SUCCESS && ret != IM_STATUS_NOERROR) return;

        经过上述步骤后,已经得到了能够用于深度学习推理的 RGB 格式数据,接下来需要使用 letterbox 、resize等操作将数据转换为我们需要的大小,由于零拷贝API推理框架的使用仅支持输入数据有物理地址或者fd的情况,故前处理部分将放入零拷贝API推理部分实现。

3. 零拷贝API执行推理

        RK3588 推理框架支持通用API以及零拷贝API,其中通用API调用简单,但是每次执行都需要更新帧数据,需要将外部模块分配的数据拷贝到NPU运行时的输入内存,而零拷贝API调用比较复杂,但是会使用预先分配的内存,减少了内存拷贝的开销,性能更优,带宽更少。但是零拷贝API仅支持输入数据有物理地址或者fd的情况。

        零拷贝 API 推理框架在加载模型、查询输入输出基本信息部分和通用 API 推理框架没有区别,此处不做赘述,接下来着重讲解零拷贝 API 不同的地方:

3.1 零拷贝 API 输入输出数据设置

  • 零拷贝 API 需要使用 rknn_create_mem 申请输入和输出的内存
  • 零拷贝 API 需要使用 rknn_set_io_mem 设置申请好的输入输出内存的基本属性
// 由于通常模型只有一个输入,故此处写死了,若有多个输入的情况,请参考设置输出属性的相关信息
input_attrs[i].type = RKNN_TENSOR_UINT8;
input_mems_[0] = rknn_create_mem(rknn_ctx_, input_attrs[0].size_with_stride);

for (int i = 0; i < io_num.n_output; i++) 
{    
    if (output_attrs[i].type == RKNN_TENSOR_FLOAT16)
    {
        printf("yolov8 output tensor type is float16, want type set to float32");
        output_attrs[i].type = RKNN_TENSOR_FLOAT32;
        output_mems_[i] = rknn_create_mem(rknn_ctx_, output_attrs[i].n_elems * sizeof(float));
    }
    else
    {
        output_mems_[i] = rknn_create_mem(rknn_ctx_, output_attrs[i].size_with_stride);
    }
}
// 由于通常模型只有一个输入,故此处写死了,若有多个输入的情况,请参考设置输出属性的相关信息
ret = rknn_set_io_mem(rknn_ctx_, input_mems_[0], &input_attrs[0]);
if (ret < 0) 
{
    printf("rknn_set_io_mem for input failed! ret=%d", ret);
    return -1;
}
for (int i = 0; i < io_num.n_output; i++) 
{
    ret = rknn_set_io_mem(rknn_ctx_, output_mems_[i], &output_attrs[i]);
    if (ret < 0) 
    {
        printf("rknn_set_io_mem for output[%d] failed! ret=%d", i, ret);
        return -1;
    }
}

 3.2 结合 RGA、DMA操作,实现零拷贝API数据预处理与数据传输

        在前文中,已经通过DMA、RGA 操作获得了能够进行深度学习推理的 RGB 格式,接下来,正式开启前处理操作: 

// 注意前文中已经使用了 imcvtcolor 将解码后的数据转换成了 RGB 格式数据并写入了 dst 中, 为了便于理解,此处不修改名字直接使用
rga_buffer_t dst = wrapbuffer_fd(buffer->fd, width, height, RK_FORMAT_RGB_888);
// 此处的 input_mems_[0] 对应的即为 3.1 中通过 rknn_create_mem 创建的输入缓冲区
rga_buffer_t preprocess_dst = wrapbuffer_fd(input_mems_[0], target_width, target_height, RK_FORMAT_RGB_888);

int ret = Preprocess_RGA(dst, img_width, img_height, preprocess_dst, target_width, target_height, letter_box_info_); 
if (ret != 0) return -1;

        Preprocess_RGA ,通过 RGA improcess 函数先后实现 resize 操作和 letter_box 操作,然后将数据写入目标缓冲区 dst,即 零拷贝API 推理缓冲区!,具体操作如下:

int Preprocess_RGA(
    rga_buffer_t src, int src_width, int src_height,
    rga_buffer_t dst, int dst_width, int dst_height,
    LetterBoxInfo& letter_box_info)
{
    float target_wh_ratio = static_cast<float>(dst_width) / dst_height;
    float src_wh_ratio = static_cast<float>(src_width) / src_height;
    int letterbox_dst_width, letterbox_dst_height;
    int dst_padding_hor = 0, dst_padding_ver = 0;
    if (src_wh_ratio > target_wh_ratio) 
    {
        // 填充上下
        letterbox_dst_width = dst_width;
        letterbox_dst_height = dst_width / src_wh_ratio;
        dst_padding_ver = (dst_height - letterbox_dst_height) / 2;
        letter_box_info.hor = false;
        letter_box_info.pad = dst_padding_ver;
    } 
    else 
    {
        // 填充左右
        letterbox_dst_height = dst_height;
        letterbox_dst_width = dst_height * src_wh_ratio;
        dst_padding_hor = (dst_width - letterbox_dst_width) / 2;
        letter_box_info.hor = true;
        letter_box_info.pad = dst_padding_hor;
    }
    im_rect src_rect = {0, 0, src_width, src_height};
    im_rect dst_rect = {dst_padding_hor, dst_padding_ver, letterbox_dst_width, letterbox_dst_height};
    int ret = imcheck(src, dst, src_rect, dst_rect);
    if (ret != IM_STATUS_NOERROR) return -1;
    IM_STATUS status = improcess(src, dst, {0}, src_rect, dst_rect, {0}, IM_SYNC);
    if (status != IM_STATUS_NOERROR && status != IM_STATUS_SUCCESS) return -1;
    return 0;
}

 3.3 执行零拷贝API推理

         在经过前处理后,模型的输入缓冲区 input_mems_[0] 已经放入供算法推理的数据,此时只需要调用推理接口即可进行推理,具体代码如下:

int ret = rknn_run(rknn_ctx_, NULL);
if (ret < 0)
{
    printf("rknn_run fail! ret=%d", ret);
    return -1;
}
return 0;

 3.4 获取输出结果

        推理完成后,数据会被送入预先创建的输出缓冲区,即 output_mems_[i],取出数据后即进行后续后处理操作,具体代码如下:

// 此处的 output_num_ 为模型的实际输出数量
void *output_data[output_num_];
for (int i = 0; i < output_num_; i++)
{
    output_data[i] = (void *)output_mems_[i]->virt_addr;
}

4. 优化前后 CPU 消耗对比

4.1 普通拷贝操作和DMA数据传输对比

  • 普通拷贝操作下的数据传输,CPU资源消耗约为:95.4%
  • DMA操作下的数据传输,CPU资源消耗约为:33%,数据传输方面同比节省约3倍的CPU资源

普通拷贝操作下的数据传输

CPU资源消耗

DMA操作下的数据传输

CPU资源

4.2 RGA 前处理和 OPENCV 前处理对比

以下是使用RGA 进行数据预处理操作和使用opencv进行数据预处理操作的耗时对比
  • OPENCV数据预处理操作耗时为:3-10ms左右
  • RGA数据预处理操作耗时为:1ms左右,数据前处理速度方面同比提升至少3倍

RGA数据预处理操作耗时

OPENCV数据预处理操作耗时

以下是使用 OPENCV 前处理与 RGA 前处理的 CPU 资源消耗对比
  • OPENCV前处理CPU资源消耗约为:
前处理CPU占用 - 拉流CPU占用 = 145.9% - 95.4% = 50.5%
  • RGA前处理CPU资源消耗约为:
前处理CPU占用 - 拉流CPU占用 = 48% - 33% = 15%,数据前处理方面同比节省约 3倍的CPU资源

 

OPENCV前处理CPU资源消耗

RGA前处理CPU资源消耗

4.3 零拷贝API 推理和普通API推理对比

以下是通用API以及零拷贝API推理时的CPU资源消耗对比
  • 通用API推理CPU资源消耗约为:
通用API推理CPU占用 - 前处理CPU占用 = 223.1% - 145.9% = 77.2%
  • 零拷贝API推理CPU资源消耗约为:
通用API推理CPU占用 - 前处理CPU占用 = 88% - 48% = 40%,数据前处理方面同比节省约 1.5倍

 

通用API CPU资源消耗

零拷贝API CPU资源消耗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不想起名字呢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值