EDK 的基本使用

EDK 的基本使用

本教程通过一个Easy Development Kit Demo来实现一系列视频处理推理操作,从而达到展示EDK基本使用方法的目的。
难易程度: |实验人次:4655

前言

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

申 请 试 用