本文是一篇简单但详细的RK3588 DMA, RGA, 零拷贝API使用指
南北,讲述了在RK3588上如何串联使用DMA、RGA、零拷贝推理 API 实现从实时视频流解码、数据预处理、数据推理等方面的优化,通过这一些列优化,可以很大程度上降低 RK3588 上 CPU 资源的消耗,节省下来的 CPU 资源能够用于其他功能。
目录
3.2 结合 RGA、DMA操作,实现零拷贝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 前处理对比
- OPENCV数据预处理操作耗时为:3-10ms左右
- RGA数据预处理操作耗时为:1ms左右,数据前处理速度方面同比提升至少3倍
RGA数据预处理操作耗时 | OPENCV数据预处理操作耗时 |
![]() | ![]() |
- OPENCV前处理CPU资源消耗约为:
- RGA前处理CPU资源消耗约为:
OPENCV前处理CPU资源消耗 | ![]() ![]() |
RGA前处理CPU资源消耗 | ![]() ![]() |
4.3 零拷贝API 推理和普通API推理对比
- 通用API推理CPU资源消耗约为:
- 零拷贝API推理CPU资源消耗约为:
通用API CPU资源消耗 | ![]() ![]() |
零拷贝API CPU资源消耗 | ![]() ![]() |