【Atlas 200 DK玩转系列】高性能performance_sample样例中需要关注的一些点

举报
_xyt 发表于 2020/04/30 11:49:55 2020/04/30
【摘要】 一、内存管理相关知识 1、原生语言的内存管理接口原生语言的内存管理接口包括malloc、free、memcpy、memset、new、delete等接口,支持C/C++等语言,由此类接口申请的内存,用户可以自行管理和控制内存使用的生命周期。用户申请内存空间小于256k时,使用原生语言的内存接口与Matrix框架提供的内存管理接口在性能上区别不大,基于简单便捷考虑,建议使用原生语言的内存管理接...

一、内存管理相关知识

 

1、原生语言的内存管理接口

原生语言的内存管理接口包括mallocfreememcpymemsetnewdelete等接口,支持C/C++等语言,由此类接口申请的内存,用户可以自行管理和控制内存使用的生命周期。用户申请内存空间小于256k时,使用原生语言的内存接口与Matrix框架提供的内存管理接口在性能上区别不大,基于简单便捷考虑,建议使用原生语言的内存管理接口。

 

2Matrix框架提供的内存管理接口

框架单独提供了一套内存分配和释放接口,支持C/C++语言,包括:

HIAI_DMalloc/HIAI_DFree接口,主要用于申请/释放内存,再配合SendData接口从Host侧搬运数据到Device侧,能够尽量少拷贝,减少流程处理时间。Host侧的Engine之间传输数据或Device侧的Engine之间传输数据,通过在Engine之间发送指针实现,避免拷贝内存。

在从host侧向Device侧搬运数据时,使用HIAI_DMalloc方式将很大的提供传输效率,建议优先使用HIAI_DMalloc,该内存接口目前支持0 – (256 × 1024 Bytes - 96 Bytes)的数据大小,如果数据超出该范围,则需要使用malloc接口进行分配;

HIAI_DVPP_DMalloc/HIAI_DVPP_DFree接口,主要用于申请/释放DeviceDVPP使用的内存。通过该接口申请/释放内存,能够尽量少拷贝,减少流程处理时间。

 

3、应用开发过程中,Matrix对数据的处理流程分为以下几个阶段:

1.Matrix调用接口将数据(图片)从Host侧搬运到Device侧。

2.Matrix调用DVPP的接口对数据进行编解码、缩放等处理。

3.Matrix调用Framwork提供的模型管家接口对数据进行推理。

4.Matrix调用接口将推理结果从Device侧回传到Host侧。

 

 

 

接口调用流程图:

 

 image.png

 

 

二、DVPP相关知识

 

框架提供了图像处理单元以及视频编解码能力的调用接口,用户可以根据实际情况,将图像的解码/视频的解码放到Device上,以减少从HostDevice传输的数据量,同时降低数据传输时间开销和带宽压力。

Host侧,通过调用Matrix框架提供HIAI_DMalloc申请内存,作为图像/视频编解码的输入使用,数据存放的内存位置建议起始地址128对齐。在Device侧,DVPP完成图像/视频预处理后,调用Matrix框架提供HIAI_DVPP_DMalloc申请内存,作为图像预处理后的输出使用。

 

图像Crop/Resize

 image.png

crop/resize对输入的限制如下:

 

· 输入数据地址(os虚拟地址):16字节对齐。

· 输入图像宽度内存限制:16字节对齐。

· 输入图像高度内存限制:2字节对齐。

 

基于上述限制,高性能的编程方式要实现“0拷贝则需要满足从Device(接收端)给用户的内存地址开始就满足限制。一般的做法根据输入不同分为以下两种做法。

 

· 方法一:在Host进行解码或者在其他硬件进行解码的应用,在Host发送端将数据就做好裁剪或者padding,满足16*2对齐,这样框架在数据接收端会自动的申请满足上述限制的数据内存。

 

· 方法二:在Device进行图片解码、视频解码以后输出为16*2对齐,可直接作为DVPP VPC(Crop&&Resize)输入。

 

 

 

 

三、performance_sample工程介绍

performance_sample示例代码的整个流程展示了“0”拷贝的思想。

所谓的“0”拷贝,指单Batch场景下Matrix在整个流程中对图像数据不做任何显式的拷贝动作。为了实现“0”拷贝,需要执行以下操作。

 

1、通过HIAI_REGISTER_SERIALIZE_ 注册序列化函数(GetSerializeFunc)和反序列化函数(GetDeserializeFunc),实现数据类型的序列化/反序列化。(调用HIAI_DMallocHIAIMemory::HIAI_DMalloc接口,同时配合使用HIAI_REGISTER_SERIALIZE_FUNC宏(对用户自定义数据类型进行序列化或反序列化),可使数据传输效率更高,性能更优。)

 

2、使用Matrix框架提供的HIAI_Dmalloc接口申请内存,再调用SendData接口,用于将Host侧数据向Device侧搬运。

 

3、通过HIAI_DMalloc接口申请的内存,作为图像/视频编解码的输入使用,无需进行数据拷贝。

 

4、使用Matrix框架提供的HIAI_DVPP_DMalloc接口申请的内存,内存地址满足DVPP的输入/输出要求,可直接作为图像/视频输出的使用。调用HIAI_DVPP_DMalloc接口申请内存后,必须使用HIAIMemory::HIAI_DVPP_DFree接口释放内存。

DVPP内部,VPC模块的输入可直接复用内存中JPEGD模块的输出数据。

 

5、通过HIAI_DVPP_DMalloc接口申请的内存,可直接作为模型推理首层的输入,无需进行数据拷贝。

 

6、该用例主要分为七个EngineSrcEngine-JpegdEngine-VpcEngine-AIStubEngine/DstDvppEngine-DataOptEngine-DestEngine, 详细关系如下图:


1 engine实现图
image.png

 

7performance_sample样例工程目录结构如same1所示。

 

1 performance_sample目录结构说明

一级目录/文件

二级目录

说明

inc

-

共用头文件夹

src

-

Device源文件、Host源文件和CMakeLists文件。

run

out/test_data/config

配置文件

out/test_data/model

模型文件

.project

-

工程信息文件。

 

 

四、关键代码

(1)使用性能优化方案传输数据,必须对发送数据的接口进行手动序列化和反序列化:

// 注:序列化函数在发送端使用,反序列化在接收端使用,所以这个注册函数最好在接收端和发送端都注册一遍;

//数据结构

typedef struct

{

    uint32_t left_offset = 0;

    uint32_t right_offset = 0;

    uint32_t top_offset = 0;

    uint32_t bottom_offset = 0;

    //下面serialize函数用于序列化结构体

    template <class Archive>

    void serialize(Archive & ar)

    {

        ar(left_offset,right_offset,top_offset,bottom_offset);

    }

} crop_rect;

 

 

 

// 注册Engine将流转的结构体

typedef struct EngineTransNew

{

    std::shared_ptr<uint8_t> trans_buff = nullptr;    // 传输Buffer

    uint32_t buffer_size = 0;                   // 传输Buffer大小

    std::shared_ptr<uint8_t> trans_buff_extend = nullptr;

    uint32_t buffer_size_extend = 0;

    std::vector<crop_rect> crop_list;

    //下面serialize函数用于序列化结构体

    template <class Archive>

    void serialize(Archive & ar)

    {

        ar(buffer_size, buffer_size_extend, crop_list);

    }

}EngineTransNewT;

 

//序列化函数

/**

* @ingroup hiaiengine

* @brief GetTransSearPtr,        序列化Trans数据

* @param [in] : data_ptr         结构体指针

* @param [out]struct_str       结构体buffer

* @param [out]data_ptr         结构体数据指针buffer

* @param [out]struct_size      结构体大小

* @param [out]data_size        结构体数据大小

*/

void GetTransSearPtr(void* data_ptr, std::string& struct_str,

    uint8_t*& buffer, uint32_t& buffer_size)

{

    EngineTransNewT* engine_trans = (EngineTransNewT*)data_ptr;

    uint32_t dataLen = engine_trans->buffer_size;

    uint32_t dataLen_extend = engine_trans->buffer_size_extend;

    // 获取结构体buffersize

    buffer_size = dataLen + dataLen_extend;

    buffer = (uint8_t*)engine_trans->trans_buff.get();

 

    // 序列化处理

    std::ostringstream outputStr;

    cereal::PortableBinaryOutputArchive archive(outputStr);

    archive((*engine_trans));

    struct_str = outputStr.str();

}

 

//反序列化函数

/**

* @ingroup hiaiengine

* @brief GetTransSearPtr,             反序列化Trans数据

* @param [in] : ctrl_ptr              结构体指针

* @param [in] : data_ptr              结构体数据指针

* @param [out]std::shared_ptr<void> 传给Engine的指针结构体指针

*/

std::shared_ptr<void> GetTransDearPtr(

    const char* ctrlPtr, const uint32_t& ctrlLen,

    const uint8_t* dataPtr, const uint32_t& dataLen)

{

    if(ctrlPtr == nullptr) {

        return nullptr;

    }

    std::shared_ptr<EngineTransNewT> engine_trans_ptr = std::make_shared<EngineTransNewT>();

    // engine_trans_ptr赋值

    std::istringstream inputStream(std::string(ctrlPtr, ctrlLen));

    cereal::PortableBinaryInputArchive archive(inputStream);

    archive((*engine_trans_ptr));

    uint32_t offsetLen = engine_trans_ptr->buffer_size;

    if(dataPtr != nullptr) {

        (engine_trans_ptr->trans_buff).reset((const_cast<uint8_t*>(dataPtr)), ReleaseDataBuffer);

        // 因为trans_bufftrans_buff_extend指向的是一块以dataPtr为首地址的连续内存空间,

        // 因此只需要trans_buff挂载析构器释放一次即可

        (engine_trans_ptr->trans_buff_extend).reset((const_cast<uint8_t*>(dataPtr + offsetLen)), SearDeleteNothing);

    }

    return std::static_pointer_cast<void>(engine_trans_ptr);

}

 

// 注册EngineTransNewT

HIAI_REGISTER_SERIALIZE_FUNC("EngineTransNewT", EngineTransNewT, GetTransSearPtr, GetTransDearPtr);

 

(2) 在发送数据时,需要使用注册的数据类型,另外配合使用HIAI_DMalloc分配数据内存,可以使性能更优

     注:在从host侧向Device侧搬运数据时,使用HIAI_DMalloc方式将很大的提供传输效率,建议优先使用HIAI_DMalloc,该内存接口目前支持0 – (256M Bytes - 96 Bytes)的数据大小,如果数据超出该范围,则需要使用malloc接口进行分配;

     // 使用Dmalloc接口申请数据内存,10000为时延,为10000毫秒,表示如果内存不足,等待10000毫秒;

     HIAI_StatusT get_ret =  HIAIMemory::HIAI_DMalloc(width*align_height*3/2,(void*&)align_buffer, 10000);

   

     // 发送数据,调用该接口后无需调用HIAI_DFree接口,10000为时延

     graph->SendData(engine_id_0, "TEST_STR", std::static_pointer_cast<void>(align_buffer), 10000);

3Device进行图片解码、视频解码以后输出为16*2对齐,可直接作为DVPP VPC(Crop&&Resize)输入。

HIAI_IMPL_ENGINE_PROCESS("JpegdEngine", JpegdEngine, 1)

{

    struct jpegd_raw_data_info jpegdInData;

    IDVPPAPI *pidvppapi = nullptr;

 

    if ( arg0 == nullptr)

    {

        HIAI_ENGINE_LOG(HIAI_JPEGD_CTL_ERROR,"jpegengine argo = 0failed");

        return HIAI_JPEGD_CTL_ERROR;

    }

    HIAI_ENGINE_LOG("[DEBUG] JpegdEngine Start Process");

    std::shared_ptr<EngineTransNewT> result =

        std::static_pointer_cast<EngineTransNewT>(arg0);

 

    // 构造输入数据

    jpegdInData.jpeg_data_size = result->buffer_size;

    jpegdInData.jpeg_data = reinterpret_cast<unsigned char*>(result->trans_buff.get());

 

    // 创建dvpp handle

    uint32_t ret = CreateDvppApi(pidvppapi);

    if (pidvppapi == nullptr || ret != DVPP_SUCCESS) {

        HIAI_ENGINE_LOG(HIAI_CREATE_DVPP_ERROR, "VPC create dvppApi failed");

        return HIAI_CREATE_DVPP_ERROR;

    }

    // 构造dvpp ctrl message

    dvppapi_ctl_msg dvppApiCtlMsg;

    jpegd_yuv_data_info* jpegdOutData = new(std::nothrow) jpegd_yuv_data_info;

    if(jpegdOutData == nullptr) {

        HIAI_ENGINE_LOG(HIAI_JPEGD_CTL_ERROR,"new jpegdOutData fail");

        if (pidvppapi != nullptr) {

            DestroyDvppApi(pidvppapi);

        }

        return HIAI_JPEGD_CTL_ERROR;

    }

    dvppApiCtlMsg.in = (void*)&jpegdInData;

    dvppApiCtlMsg.in_size = sizeof( jpegdInData );

    dvppApiCtlMsg.out = (void*)jpegdOutData;

    dvppApiCtlMsg.out_size = sizeof(jpegd_yuv_data_info);

 

    // 执行DVPP进行Jpeg解码

    if(pidvppapi != nullptr) {

        if( 0 != DvppCtl( pidvppapi, DVPP_CTL_JPEGD_PROC, &dvppApiCtlMsg ) ) {

            HIAI_ENGINE_LOG(HIAI_JPEGD_CTL_ERROR, "Jpegd Engine dvppctl error ");

            DestroyDvppApi(pidvppapi);

            return HIAI_JPEGD_CTL_ERROR;

        }

        DestroyDvppApi(pidvppapi);

    } else {

        HIAI_ENGINE_LOG(HIAI_JPEGD_CTL_ERROR, "Jpegd Engine can not open dvppapi");

        return HIAI_JPEGD_CTL_ERROR;

    }

 

    // 构造DVPP OUt数据并进行发送

    gJepgdOutWidth = jpegdOutData->img_width_aligned;

    gJepgdOutHeight = jpegdOutData->img_height_aligned;

    gJepgdInWidth = jpegdOutData->img_width;

    gJepgdInHeight = jpegdOutData->img_height;

 

    if(jpegdOutData->yuv_data == nullptr) {

        HIAI_ENGINE_LOG(HIAI_CREATE_DVPP_ERROR, "jpegdOutData->yuv_data is nullptr");

        return HIAI_CREATE_DVPP_ERROR;

    }

    std::shared_ptr<EngineTransNewT> output = std::make_shared<EngineTransNewT>();

    output->trans_buff.reset((uint8_t*)jpegdOutData, JpegdFreeBuffer);

    output->buffer_size = jpegdOutData->yuv_data_size;

    // 发送数据

    if (HIAI_OK != SendData(0, gMsgType, output)) {

        HIAI_ENGINE_LOG(HIAI_SEND_DATA_FAIL, "jpeg engine SendData wrong");

    }

    HIAI_ENGINE_LOG("[DEBUG] JpegdEngine End Process");

    return HIAI_OK;

}

4)模型转换预处理配置

1 crop/resize运行示意图中可以看到,crop/resize输出的图像是经过align_up对齐的,这种对齐会导致部分图像是经过padding的,就不是原始模型需要的输入。为了得到希望输出的图片,可以经过将这部分数据拷贝出来,放在一个新的缓冲区输入到模型推理模块,但这样引入了数据拷贝的开销。为了降低这类开销,框架提供了机制,允许输入到模型管家(modelManager)的图像带padding边,模型管家的AIPP模块会根据用户设定的宽高(在模型转换时设置),对图像进行crop,输出满足模型输入要求的图片,送到模型推理,不需要进行数据拷贝,性能得到了较大提高。

如下,假定模型推理需要输入的图像为224*224,而从DVPP的获取的数据是128*16对齐的,即256*224

    int32_t jpegdoutWidth = 256;

    int32_t jpegdoutHeight = 224;

    int32_t outImageSize = JpegdoutWidth * JpegdoutHeight * 3/2;

    hiai::Rectangle<hiai::Point2D> rectangle;

    rectangle.anchor_lt.x = 0;

    rectangle.anchor_lt.y = 0;

    rectangle.anchor_rb.x = JpegdinWidth - 1;

    rectangle.anchor_rb.y = JpegdinHeight - 1;

    // 使用AutoBuffer

    if (outBuffer_ == nullptr) {

        outBuffer_ = make_shared<AutoBuffer>();

    }

    uint32_t outWidth = MODEL_WIDTH;  // 224

    uint32_t outHeight = MODEL_WIDTH; // 224

    auto ret = CropAndResizeByDvpp(dvpp_api, inImage,(char*)vpcOutBuffer, outImageSize, rectangle, outWidth, outHeight);

 

5batch和超时

对于大部分模型,特别是小模型,一个批量的输入组成一个batch交给昇腾AI处理器做模型的推理可获得性能收益。使用batch推理将大大提高数据的吞吐率,同时也将提高昇腾AI处理器的利用率,在损失一定的时延情况下提升了整体的性能。因此,构建一个高性能应用应当在时延允许的情况下尽可能使用大batch

· 关于Matrix已接收数据的存储。用户编写代码逻辑时,可以在接收到数据时,将数据存储在队列中,等到足够的数据组成batch后再一并进行推理。这个队列可以使用框架提供的hiai::MultiTypeQueue。为了防止数据饿死在队列中,用户使用超时的设置接口,根据应用对时延的要求设置超时时间。超时时间到达时,框架会主动再次调用engine的主要处理流程。此时用户将队列中的数据取出处理,这样,数据就不会饿死在队列中了。

· 关于超时配置。用户可以在config文件中配置wait_inputdata_max_time”,用于设置超时时间t1(单位是毫秒),若t1时间段内Matrix没接收到数据,则Matrix调用HIAI_IMPL_ENGINE_PROCESS时会传入空指针,用户可以根据是否有空指针来判断是否超时,再对t1时间前已接收的数据及时处理,处理时,先将已接收的数据后补齐到对应的batch数后,再将数据发送给推理引擎,推理完成后用户仅取有效数据。如果用户需要做重复超时处理,可以在config文件配置is_repeat_timeout_flag”,每次超时后(Matrix一直未接收到数据),Matrix调用HIAI_IMPL_ENGINE_PROCESS时都会传入空指针。

· 如果模型的输入是多Batch且用户分批发送各Batch的数据给模型管家(推理Engine)做推理,则需要用户添加如下代码逻辑

1.需要用户在Device侧单独申请缓存空间存放各Batch的数据。

2.Device侧的推理Engine接收到各Batch的数据后,用户需要将各Batch的数据拼接起来存放1申请的缓存空间中。

3.Device侧的推理Engine接收的Batch个数与模型推理需要的Batch个数相等后,用户才可以使用缓存空间中的多Batch数据进行推理。

 

// 模型处理

std::vector<std::shared_ptr<hiai::IAITensor>> inputDataVec;

uint32_t buffer_size = result->buffer_size;

if (buffer_size != batchDataSize_) {

    HIAI_ENGINE_LOG(HIAI_INVALID_INPUT_MSG, "the inputbuffersize is incorrect, count is  %d", count_);

}

uint32_t memCpRet = 0;

if (batchSize_ > 1) {

    memCpRet = memcpy_s(dataPtr_ + (count_ % batchSize_) * buffer_size, buffer_size * (batchSize_ - (count_ % batchSize_)),

             result->trans_buff.get(), buffer_size);

    if (memCpRet != 0) {

        HIAI_ENGINE_LOG(HIAI_INVALID_INPUT_MSG, "batch data copy fail");

        return HIAI_INVALID_INPUT_MSG;

    }

    count_ = count_ + 1;

    HIAI_ENGINE_LOG("[DEBUG] AIStubEngine wait data count_ is %u",count_);

}

else {

    count_ = 1;

}

// 下面通过判断收到的数据是否等于batch数,当收到的数据个数等于batch数时,将数据送到模型管家推理

if ((count_ % batchSize_) == 0) {

    // 组织数据Buffer

    hiai::AITensorDescription inputTensorDesc = hiai::AINeuralNetworkBuffer::GetDescription();

    std::shared_ptr<hiai::IAITensor> inTensor = nullptr;

    if(batchSize_ > 1) {

        inTensor = hiai::AITensorFactory::GetInstance()->CreateTensor(inputTensorDesc, (void*)dataPtr_,  (uint32_t)(batchSize_ * buffer_size * sizeof(char)));

    }

    else {

        inTensor = hiai::AITensorFactory::GetInstance()->CreateTensor(inputTensorDesc, (void*)result->trans_buff.get(),  (uint32_t)(buffer_size * sizeof(char)));

    }

    // AIModelManager. fill in the input data.

    inputDataVec.push_back(inTensor);

    hiai::AIContext aiContext;

    ret = ai_model_manager_->Process(aiContext, inputDataVec, outputTensorBuffer_, 0);

    if (ret != hiai::SUCCESS) {

        HIAI_ENGINE_LOG(HIAI_AI_MODEL_MANAGER_PROCESS_FAIL, "ai model manager process failed");

        return HIAI_AI_MODEL_MANAGER_PROCESS_FAIL;

    }

    // 根据输出结果发送计算结果给到DataOptEngine

    for (uint32_t index = 0; index < outputTensorBuffer_.size(); index++) {

        std::shared_ptr<hiai::AINeuralNetworkBuffer> outputData =

            std::static_pointer_cast<hiai::AINeuralNetworkBuffer>(outputTensorBuffer_[index]);

        std::shared_ptr<EngineTransNewT> output = std::make_shared<EngineTransNewT>();

        output->trans_buff.reset((uint8_t *)outputData->GetBuffer(), DeviceDeleteNothing);

        output->buffer_size = outputData->GetSize();

        hiaiRet = SendData(0, "EngineTransNewT", std::static_pointer_cast<void>(output));

        if (HIAI_OK != hiaiRet) {

            HIAI_ENGINE_LOG(HIAI_SEND_DATA_FAIL, "fail to send data");

            return HIAI_SEND_DATA_FAIL;

        }

    }

    count_ = 0;

}

使用batch8的模型一次推理8张图片时profiling数据如下:

 

 image.png 

 

使用batch1的模型一次推理一张图片时profiling数据如下:

 

 image.png

对比可以看出,使用多batch时,推理(AIStubEngine)引擎的耗时显然比8次一次推理一张图片的耗时少很多,达到高性能的效果。

 

6)算法推理输入输出数据处理

为了避免算法推理内部可能出现的内存拷贝,在调用模型管家Process接口时,建议输入数据(输入数据一般可直接使用框架传入的内存,该内存是由框架通过HIAI_DMalloc申请得到)及输出数据都通过HIAI_DMalloc接口申请,这样就能够使能算法推理的零拷贝机制,优化Process时间。如果在推理前需要进行DVPP处理,DVPP的输入内存使用框架传入的内存,输出内存可通过HIAI_DVPP_DMalloc接口分配,并将输出内存传给推理Engine当做输入内存。

7)回传数据优化处理

当推理计算完成后,需要将推理结果或者推理结束信号发送给Host端,如果在推理Engine内部调用SendData回传数据到Host端,将会消耗推理Engine的时间。建议单独开一个专门负责回传数据的Engine(例如:DataOptEngine),当推理结束后,推理Engine将处理数据透传给DataOptEngine,由DataOptEngine负责将数据回传给Host侧,再由Host侧的Engine(例如:DstEngine)负责接收传过来的推理结果。

 

 


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。