【Atlas 200 DK玩转系列】高性能performance_sample样例中需要关注的一些点
一、内存管理相关知识
1、原生语言的内存管理接口
原生语言的内存管理接口包括malloc、free、memcpy、memset、new、delete等接口,支持C/C++等语言,由此类接口申请的内存,用户可以自行管理和控制内存使用的生命周期。用户申请内存空间小于256k时,使用原生语言的内存接口与Matrix框架提供的内存管理接口在性能上区别不大,基于简单便捷考虑,建议使用原生语言的内存管理接口。
2、Matrix框架提供的内存管理接口
框架单独提供了一套内存分配和释放接口,支持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接口,主要用于申请/释放Device侧DVPP使用的内存。通过该接口申请/释放内存,能够尽量少拷贝,减少流程处理时间。
3、应用开发过程中,Matrix对数据的处理流程分为以下几个阶段:
1.Matrix调用接口将数据(图片)从Host侧搬运到Device侧。
2.Matrix调用DVPP的接口对数据进行编解码、缩放等处理。
3.Matrix调用Framwork提供的模型管家接口对数据进行推理。
4.Matrix调用接口将推理结果从Device侧回传到Host侧。
接口调用流程图:
二、DVPP相关知识
框架提供了图像处理单元以及视频编解码能力的调用接口,用户可以根据实际情况,将图像的解码/视频的解码放到Device上,以减少从Host到Device传输的数据量,同时降低数据传输时间开销和带宽压力。
在Host侧,通过调用Matrix框架提供HIAI_DMalloc申请内存,作为图像/视频编解码的输入使用,数据存放的内存位置建议起始地址128对齐。在Device侧,DVPP完成图像/视频预处理后,调用Matrix框架提供HIAI_DVPP_DMalloc申请内存,作为图像预处理后的输出使用。
图像Crop/Resize
基于上述限制,高性能的编程方式要实现“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_DMalloc或HIAIMemory::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、该用例主要分为七个Engine(SrcEngine-JpegdEngine-VpcEngine-AIStubEngine/DstDvppEngine-DataOptEngine-DestEngine), 详细关系如下图:
7、performance_sample样例工程目录结构如same表1所示。
表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;
// 获取结构体buffer和size
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_buff和trans_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);
(3)在Device进行图片解码、视频解码以后输出为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);
(5)batch和超时
对于大部分模型,特别是小模型,一个批量的输入组成一个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;
}
使用batch为8的模型一次推理8张图片时profiling数据如下:
使用batch为1的模型一次推理一张图片时profiling数据如下:
对比可以看出,使用多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)负责接收传过来的推理结果。
- 点赞
- 收藏
- 关注作者
评论(0)