CANN Samples(十一):媒体处理接口V1与V2的取舍与迁移

在CANN(Compute Architecture for Neural Networks)的开发实践中,图像与视频预处理是一个关键环节。无论是图像分类、目标检测还是其他视觉任务,都离不开对输入图片或视频流进行解码、缩放、裁剪等预处理操作。为了在昇腾(Ascend)芯片上高效完成这些任务,CANN提供了一套硬化的处理单元------DVPP(Digital Vision Pre-Processing)。

然而,与DVPP交互的API并非一成不变。它经历了从V1版本的acldvpp接口到V2版本himpi接口(通常通过AclLite库进行封装和调用)的演进。这两个版本的接口在设计哲学、使用方式和开发体验上存在差异较大。对于开发者来说,理解它们的区别,并知道在何种场景下如何选择与迁移,就成了一项非常重要的技能。

这篇文章,我们就来深入聊聊DVPP V1和V2接口的那些事儿,通过具体的代码实例,让你深入理解它们各自的特点,并为你提供一份实用的迁移指南。

1. V1接口 (acldvpp):细粒度的手动管理模式

V1接口,我们通常指的是以acldvpp开头的一系列C风格API。这套接口的设计思路是"过程式"的,它把DVPP的每一个操作步骤都分解成独立的函数调用,给了开发者较高的控制能力。但与此同时,这也意味着开发者需要手动处理大量的细节,就像开手动挡汽车,换挡、离合、油门都得自己来。

我们通过一个具体的例子来看看V1接口是如何工作的。下面的代码片段来自项目cplusplus/level2_simple_inference/0_data_process/smallResolution_cropandpaste/src/dvpp_process.cpp,它展示了使用V1接口实现一个"裁剪并拼接"(Crop and Paste)操作的完整流程。

1.1 初始化:创建DVPP通道

首先,任何DVPP操作都需要在一个"通道"(Channel)中进行。你需要先创建一个通道描述符,然后用它来创建通道。

cpp 复制代码
// 位于 cplusplus/level2_simple_inference/0_data_process/smallResolution_cropandpaste/src/dvpp_process.cpp

Result DvppProcess::InitResource()
{
    // 1. 创建通道描述符
    dvppChannelDesc_ = acldvppCreateChannelDesc();
    if (dvppChannelDesc_ == nullptr) {
        ERROR_LOG("acldvppCreateChannelDesc failed");
        return FAILED;
    }

    // 2. 基于描述符创建通道
    aclError aclRet = acldvppCreateChannel(dvppChannelDesc_);
    if (aclRet != ACL_SUCCESS) {
        ERROR_LOG("acldvppCreateChannel failed, errorCode = %d", static_cast<int32_t>(aclRet));
        return FAILED;
    }

    INFO_LOG("dvpp init resource success");
    return SUCCESS;
}
  • acldvppChannelDesc: 这是一个描述DVPP通道属性的结构体。你可以把它看作是一个配置单,告诉系统你要创建一个什么样的通道。
  • acldvppCreateChannel: 这个函数负责根据你的"配置单"在昇腾芯片上实际开辟出一个DVPP处理通道。

1.2 繁琐但必要:配置输入输出描述

接下来,你需要详细地描述你的输入数据和期望的输出数据长什么样。这包括图片格式、宽高、内存地址、内存对齐(Stride)等一系列信息。每一种信息都需要通过一个acldvppSetPicDesc*系列的函数来设置。

cpp 复制代码
// 位于 cplusplus/level2_simple_inference/0_data_process/smallResolution_cropandpaste/src/dvpp_process.cpp

Result DvppProcess::InitCropAndPasteOutputDesc()
{
    // 计算输出内存需要的对齐宽度和高度
    uint32_t vpcOutWidthStride = AlignSize(outWidth_, 16); // 16字节对齐
    uint32_t vpcOutHeightStride = AlignSize(outHeight_, 2);  // 2字节对齐
  
    // 计算输出Buffer大小
    vpcOutBufferSize_ = vpcOutWidthStride * vpcOutHeightStride * 3 / 2; // YUV420SP格式

    // 关键:为DVPP操作申请专门的内存
    aclError aclRet = acldvppMalloc(&vpcOutBufferDev_, vpcOutBufferSize_);
    if (aclRet != ACL_SUCCESS) {
        ERROR_LOG("malloc output image buffer failed, errorCode = %d", static_cast<int32_t>(aclRet));
        return FAILED;
    }

    // 创建图片描述符
    vpcOutputDesc_ = acldvppCreatePicDesc();
    if (vpcOutputDesc_ == nullptr) {
        ERROR_LOG("acldvppCreatePicDesc vpcOutputDesc_ failed");
        return FAILED;
    }

    // 设置一大堆描述信息...
    (void)acldvppSetPicDescData(vpcOutputDesc_, vpcOutBufferDev_); // 内存地址
    (void)acldvppSetPicDescFormat(vpcOutputDesc_, outFormat_);      // 格式
    (void)acldvppSetPicDescWidth(vpcOutputDesc_, outWidth_);        // 宽度
    (void)acldvppSetPicDescHeight(vpcOutputDesc_, outHeight_);      // 高度
    (void)acldvppSetPicDescWidthStride(vpcOutputDesc_, vpcOutWidthStride); // 对齐后的宽度
    (void)acldvppSetPicDescHeightStride(vpcOutputDesc_, vpcOutHeightStride);// 对齐后的高度
    (void)acldvppSetPicDescSize(vpcOutputDesc_, vpcOutBufferSize_); // 内存大小

    return SUCCESS;
}
  • acldvppMalloc : 这是V1接口中最需要注意的一个函数。DVPP处理需要使用特殊的物理连续内存,普通的aclrtMalloc申请的Device内存是不能直接用于DVPP的。acldvppMalloc就是用来申请这种专用内存的。
  • acldvppPicDesc: 图片描述符,一个非常核心的结构。它像一张图片的"元数据名片",记录了关于这张图片内存的所有元数据。无论是输入还是输出,都需要这样一份"元数据名片"。

1.3 提交任务与资源销毁

配置好一切后,就可以调用具体的DVPP功能函数(如acldvppVpcCropAndPasteAsync)来异步执行任务了。任务完成后,还需要手动销毁所有创建的描述符、通道,并释放DVPP内存。

cpp 复制代码
// 提交异步任务
aclError aclRet = acldvppVpcCropAndPasteAsync(dvppChannelDesc_, vpcInputDesc_,
    vpcOutputDesc_, cropAndPasteRoiCfg_, cropAndPasteCfg_, stream_);

// ... 在任务结束后 ...

// 销毁各种描述符和配置
acldvppDestroyPicDesc(vpcInputDesc_);
acldvppDestroyPicDesc(vpcOutputDesc_);
acldvppDestroyRoiConfig(cropAndPasteRoiCfg_);
acldvppDestroyCropAndPasteConfig(cropAndPasteCfg_);

// 销毁通道
if (dvppChannelDesc_ != nullptr) {
    aclRet = acldvppDestroyChannel(dvppChannelDesc_);
    // ...
    acldvppDestroyChannelDesc(dvppChannelDesc_);
    dvppChannelDesc_ = nullptr;
}

// 释放DVPP内存
if (vpcOutBufferDev_ != nullptr) {
    (void)acldvppFree(vpcOutBufferDev_);
    vpcOutBufferDev_ = nullptr;
}

V1接口小结

  • 优点:控制粒度极细,可以精确地配置每一步,适合需要深度定制和优化的底层开发。
  • 缺点:API繁琐,代码量大,需要手动管理大量资源(描述符、内存、通道),对齐、内存类型等概念复杂,极易出错,开发效率较低。

2. V2接口 (AclLite封装):自动管理模式

V2接口,即himpi,在设计上更加现代化。不过,我们通常不直接使用原生的himpi,而是通过官方提供的AclLite库来间接使用它。AclLite将V2接口进行了面向对象的封装,进一步简化了开发流程,使开发者可以更专注于业务逻辑本身。这类似自动化的资源管理,底层细节由系统处理。

我们来看看在inference/modelInference/sampleResnetDVPP/cppACLLite/src/sampleResnetDVPP.cpp中,使用AclLite封装的V2接口是如何完成图片预处理的。

2.1 一切从一个对象开始

在AclLite中,所有的DVPP操作都围绕一个核心类AclLiteImageProc展开。你不再需要手动创建通道,只需要实例化这个类的一个对象即可。

cpp 复制代码
// 位于 inference/modelInference/sampleResnetDVPP/cppACLLite/src/sampleResnetDVPP.cpp

class SampleResnetDVPP {
    // ...
private:
    AclLiteImageProc imageProcess_; // 核心处理对象
    // ...
};

这个imageProcess_对象的构造和析构函数会自动处理DVPP通道的创建和销毁,遵循了C++中RAII(Resource Acquisition Is Initialization)的最佳实践,有效避免了资源泄漏。

2.2 高度封装的功能调用

有了imageProcess_对象后,解码、缩放等操作就变成了简单的成员函数调用。你不再需要关心PicDescacldvppMalloc这些底层细节。

cpp 复制代码
// 位于 inference/modelInference/sampleResnetDVPP/cppACLLite/src/sampleResnetDVPP.cpp

Result SampleResnetDVPP::ProcessInput(const string testImgPath)
{
    // 1. 读取JPG图片数据到内存
    ImageData image;
    AclLiteError ret = ReadJpeg(image, testImgPath);
    // ...

    // 2. 将图片数据从Host拷贝到Device(DVPP专用内存)
    // AclLite内部封装了acldvppMalloc的逻辑
    ImageData imageDevice;
    ret = CopyImageToDevice(imageDevice, image, runMode_, MEMORY_DVPP);
    // ...

    // 3. 解码:将JPEG格式解码为YUV格式
    ImageData yuvImage;
    ret = imageProcess_.JpegD(yuvImage, imageDevice);
    if (ret != ACL_SUCCESS) {
        ACLLITE_LOG_ERROR("Convert jpeg to yuv failed, errorCode is %d", ret);
        return FAILED;
    }

    // 4. 缩放:将YUV图片缩放到模型需要的尺寸
    ret = imageProcess_.Resize(resizedImage_, yuvImage, modelWidth_, modelHeight_);
    if (ret != ACL_SUCCESS) {
        ACLLITE_LOG_ERROR("Resize image failed, errorCode is %d", ret);
        return FAILED;
    }
    return SUCCESS;
}
  • CopyImageToDevice : 这是一个AclLite提供的便利函数。你只需要告诉它目标是MEMORY_DVPP,它就会自动调用acldvppMalloc申请内存并完成数据拷贝,屏蔽了底层的复杂性。
  • imageProcess_.JpegD()imageProcess_.Resize(): 看,这就是V2接口的优势在于。解码和缩放两个复杂的操作,现在都只是一行函数调用。AclLite在函数内部为你完成了创建输入输出描述符、配置参数、提交异步任务、等待任务完成、销毁临时资源等所有V1接口中需要手动完成的繁琐工作。

V2接口 (AclLite封装) 小结

  • 优点:API高度封装,面向对象,代码简洁,开发效率高,不易出错。开发者可以更专注于实现业务逻辑。
  • 缺点:控制粒度较粗,对于一些极度复杂的、非标准的DVPP操作组合,可能不如V1接口灵活。

3. 核心差异对比与迁移指南

现在,我们可以清晰地总结出V1和V2接口(AclLite封装)的核心差异了。

特性 V1 (acldvpp) V2 (AclLite 封装)
API风格 过程式 C 风格 API 面向对象 C++ 风格 API
资源管理 手动创建/销毁 (通道, 描述符) 自动管理 (RAII)
内存管理 需显式调用acldvppMalloc 隐式处理 (e.g.,CopyImageToDevice)
代码复杂度 高,代码冗长 低,代码简洁
易用性 低,学习曲线陡峭 高,直观易用
灵活性 非常高,可精细控制每一步 较高,但部分底层细节被封装

3.1 如何选择与迁移?

那么,在实际项目中,我们应该如何选择?迁移的成本又如何呢?

  1. 对于新项目
    一般场景可选用AclLite封装的V2接口。这有助于简化开发流程、降低错误率,并提升代码可维护性。除非项目对底层DVPP特性有特殊要求,否则无需使用更复杂的V1接口。

  2. 对于旧项目维护

    如果你的项目已经基于V1接口构建,并且运行稳定,那么不一定非要立即迁移。V1接口虽然复杂,但它依然是CANN支持的一部分。只有当你需要为旧项目添加新的图像/视频预处理功能,或者发现V1接口的复杂性已经成为维护的瓶颈时,再考虑进行重构和迁移。

  3. 迁移步骤指南

    如果你决定从V1迁移到V2(AclLite),可以遵循以下步骤:

    • 第一步:引入AclLite。在你的项目中包含AclLite的头文件,并链接相应的库。
    • 第二步:替换资源管理 。移除所有acldvppCreateChannelacldvppDestroyChannel等相关代码,替换为AclLiteImageProc对象的实例化。
    • 第三步:重构功能调用 。找到项目中调用acldvppVpc*AsyncacldvppJpeg*Async等功能函数的地方。用AclLiteImageProc对象的成员函数(如Resize, JpegD, Crop等)来替换它们。
    • 第四步:简化内存操作 。将acldvppMallocaclrtMemcpy的组合,替换为AclLite提供的CopyImageToDeviceCopyImageToHost等更高级的函数。同时,移除所有手动调用acldvppFree的代码。
    • 第五步:移除描述符 。删除所有acldvppCreatePicDescacldvppSetPicDesc*acldvppDestroyPicDesc的代码。这些都由AclLite在内部自动处理了。

4. 总结

DVPP的V1和V2接口代表了两种不同的设计哲学。V1 acldvpp接口提供了很高的灵活性和控制能力,但代价是极高的复杂性。而通过AclLite封装的V2接口,则以牺牲少量灵活性为代价,带来了开发效率与代码健壮性的提高。

对于绝大多数应用开发者而言,使用AclLite的V2接口,是更为简洁、高效的选择。它使你无需再关注繁琐的底层细节,可将精力投入到业务逻辑与算法实现。提升硬件性能与简化软件使用,是计算框架演进的目标。

相关推荐
汤姆yu1 小时前
基于srpingboot心情治愈调整系统
人工智能
国科安芯1 小时前
航天医疗领域AS32S601芯片的性能分析与适配性探讨
大数据·网络·人工智能·单片机·嵌入式硬件·fpga开发·性能优化
咚咚王者1 小时前
人工智能之数据分析 Pandas:第五章 文件处理
人工智能·数据分析·pandas
zhaodiandiandian1 小时前
人工智能与就业重构:机遇、挑战与政策应对
人工智能·百度·重构
浔川python社2 小时前
浔川社团:技术创作与社区运营的双重成功
人工智能
LUU_792 小时前
Day27 机器学习管道pipeline
人工智能·机器学习
冯骐2 小时前
基于 DeepSeek V3.2 的 Native Agent 实践指南,真香
人工智能·agent·deepseek
亚马逊云开发者2 小时前
利用Amazon Bedrock构建智能报告生成Agent
人工智能
孟祥_成都2 小时前
Prompt 还能哄女朋友!你真的知道如何问 ai 问题吗?
前端·人工智能