 
				前言
本实验通过一个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。