前言
本实验通过一个EDK demo来实现一系列视频处理推理操作,从而达到展示EDK基本使用方法的目的。具体的运行步骤主要包括:
程序的运行结果示例:
一、源码获取与编译运行
首先可以从https://gitee.com/SolutionSDK/easydk获取代码。
运行命令拉取代码:
git clone https://gitee.com/SolutionSDK/easydk.git
进入easydk目录,创建build目录,在build文件夹下执行命令编译easydk和samples。
mkdir build
cd build/
cmake .. -DBUILD_SAMPLES=ON -DBUILD_TESTS=ON
make
编译完成之后进入目录easydk/samples/stream-app执行 run_ssd_270.sh脚本。脚本将调用编译好的可执行文件。在运行程序之前会先从服务器下载必要的离线模型和数据。
运行结束之后将在当前目录下保存运行结果,out.avi文件。
以下的内容将展开介绍sample的代码原理。源码的具体位置在easydk/samples/stream-app下。
二、C++ 标准库的互斥锁与ffmpeg的使用
在demo中视频文件读入的部分与实际处理计算部分并行执行,之间用队列完成数据通信。另外demo中使用了ffmpeg完成视频文件的读入和基本处理。为此希望读者对相关概念和API的用法有一些基本了解。参考资料包括:
std::mutex: https://en.cppreference.com/w/cpp/thread/mutex
std::unique_lock: https://en.cppreference.com/w/cpp/thread/unique_lock
std::condition_variable: https://en.cppreference.com/w/cpp/thread/condition_variable
std::future: https://en.cppreference.com/w/cpp/thread/future
std::async: https://en.cppreference.com/w/cpp/thread/async
ffmpeg av_read_frame: https://ffmpeg.org/doxygen/2.8/group__lavf__decoding.html
三、程序的具体运行流程
MLU编程是一种异构编程,所以在程序运行过程中会涉及到数据流在Host 和 Device之间的相互拷贝。如上图所示,视频解码,图片前处理,推理这三步都在MLU上完成,所以这三步之间不需要设备和主机之间的内存拷贝。其中值得注意的是,在本例中,目标追踪的特征提取部分使用了openCV的特征提取API,也是在CPU上执行的。MLU上的特征提取和MLU 推理部分的原理类似,这里就不再赘述了。
对于一帧视频数据,更具体的处理流程如下所示:
以下我们重点讨论设备初始化,Decode,数据预处理,推理和数据拷贝这几个部分。
四、MluContext的使用
在执行各类MLU任务之前要先初始化设备。
edk::MluContext context;
// set mlu environment
context.SetDeviceId(0);
context.BindDevice();
初始化设备非常简单,值得注意的是在多卡的环境下,SetDeviceId要传入相应的卡的编号。具体的编号可以在CNMON中查看。初始化设备的API调用会在本进程中生效。
五、EasyDecode的使用
EasyDecode的使用主要包括以下几个步骤。
1. 首先初始化解码器,利用edk::EasyDecode::Attr参数创建解码器实例。
edk::EasyDecode::Attr attr;
attr.frame_geometry.w = 1920;
attr.frame_geometry.h = 1080;
attr.codec_type = edk::CodecType::H264;
attr.pixel_format = edk::PixelFmt::NV21;
attr.dev_id = 0;
attr.frame_callback = decode_output_callback;
attr.eos_callback = decode_eos_callback;
attr.silent = false;
attr.input_buffer_num = 6;
attr.output_buffer_num = 6;
decode = edk::EasyDecode::New(attr);
g_decode = decode.get();
2. 使用SendData将数据送入解码器。这里要注意解码器仅支持输入完整帧数据进行解码,建议使用 FFMpeg 进行解封装和 parse 后再送入解码器。FFMpeg相关的代码在unpack_data中。
g_decode->SendData(pending_frame)
3. 在解码完成之后可以将解码后的数据用于MLU推理或者拷回Host端做其他操作。具体的通过回调函数实现解码后的操作。
void decode_output_callback(const edk::CnFrame &info) {
std::unique_lock<std::mutex> lk(g_mut);
g_frames.push(info);
g_cond.notify_one();
}
这里将解码后的结果放入一个队列当中,供后续的推理使用。另外解码后的数据除了供推理使用以外,还要供在CPU上运行的目标追踪和后处理使用,所以在推理计算完毕之后,目标追踪和后处理执行之前需要调用API将数据拷回Host端并释放这部分MLU内存空间。
// copy out frame
decode->CopyFrameD2H(img_data, frame);
// release codec buffer
decode->ReleaseBuffer(frame.buf_id);
4. 最后一步是在整个视频数据发送完毕之后发送eos信息通知EasyDecode。EasyDecode的智能指针会在程序结束时自动析构并释放相关资源。
void send_eos(edk::EasyDecode *decode) {
edk::CnPacket pending_frame;
pending_frame.data = nullptr;
decode->SendData(pending_frame, true);
}
六、EasyBang的使用
本demo中使用了EasyBang的MluResizeConvertOp。使用过程非常简单,主要分为声明,初始化和执行三部分。
MluResizeConvertOp初始化部分的代码:
// Init resize and convert operator
std::call_once(rcop_init_flag,
[&] {
// create mlu resize and convert op
MluResizeConvertOp::Attr attr;
attr.dst_h = in_shape.h;
attr.dst_w = in_shape.w;
attr.batch_size = 1;
attr.core_version = context.GetCoreVersion();
rc_op.SetMluQueue(infer.GetMluQueue());
if (!rc_op.Init(attr)) {
THROW_EXCEPTION(edk::Exception::INTERNAL, rc_op.GetLastError());
}
});
值得注意的是,MluResizeConvertOp与后面的infer同属MLU推理任务,需要讲任务加入到MLU计算队列当中。这里将任务加到了infer的任务队列当中。在上文代码中的任务队列是在infer初始化的时候建立的。
执行部分的代码:
// run resize and convert
void *rc_output = mlu_input[0];
edk::MluResizeConvertOp::InputData input;
input.planes[0] = frame.ptrs[0];
input.planes[1] = frame.ptrs[1];
input.src_w = frame.width;
input.src_h = frame.height;
input.src_stride = frame.strides[0];
rc_op.BatchingUp(input);
if (!rc_op.SyncOneOutput(rc_output)) {
g_running = false;
g_exit = true;
decode->ReleaseBuffer(frame.buf_id);
THROW_EXCEPTION(edk::Exception::INTERNAL, rc_op.GetLastError());
}
七、EasyInfer的使用
在本小节我们简要介绍推理计算,离线模型管理,和内存管理相关API的使用。
std::shared_ptr<edk::ModelLoader> model;
edk::MluMemoryOp mem_op;
edk::EasyInfer infer;
对于一个一般的模型推理任务,一般情况下有模型管理,内存管理,推理执行等步骤。
首先就是要载入离线模型。在载入离线模型的时候除了要载入模型本身之外,还要载入一些模型本身的信息。包括了模型的input,output tensor shape,数据类型(FLOAT16,FLOAT32,INT16,UINT8……),数据顺序(NCHW,NHWC……)等等。
// load offline model
model = std::make_shared<edk::ModelLoader>(FLAGS_model_path.c_str(), FLAGS_func_name.c_str());
in_shape = model->InputShapes()[0];
out_shapes = model->OutputShapes();
在获取了所有模型相关信息之后我们就可以初始化MluMemoryOp和EasyInfer。
// prepare mlu memory operator and memory
mem_op.SetModel(model);
// init cninfer
infer.Init(model, 0);
在运行时分配相应的MLU内存空间。
void **mlu_input = mem_op.AllocMluInput();
……
mlu_output = mem_op.AllocMluOutput();
cpu_output = mem_op.AllocCpuOutput();
最后执行推理并将推理结果从Device拷回Host。
// run inference
infer.Run(mlu_input, mlu_output);
mem_op.MemcpyOutputD2H(cpu_output, mlu_output);
以上我们就完成了一个完整的EDK 解码+推理demo。