JackLi · 2021年12月07日

HVX 优化技术-HVX 开发环境介绍

原文:知乎
文章发表于知乎专栏《移动端算法优化》
作者:高性能计算学院
本篇以 HVX 的开发环境配置以及应用实例编译测试为主进行讲述。

一、Hexagon SDK 下载和安装

HVX 开发工具分为 windows 和 Ubuntu环境,本专栏主要以 Ubuntu 环境为主进行介绍。
HVX 的开发工具是 Hexagon SDK(文章基于版本 Ubuntu 20.04 进行演示)。

A. SDK 下载

  • 高通开发者官网 下载 SDK 安装包(hexagon_sdk_lnx_3_5_installer_00006_1.zip),文章基于 SDK3.5.4 版本进行介绍。
    image.png

下载官网示意图

B. SDK 安装

# 解压安装包 hexagon_sdk_lnx_3_5_installer_00006_1.zip
# 在解压后的目录下进行如下操作
sudo chmod a+x ./qualcomm_hexagon_sdk_3_5_4_eval.bin
./qualcomm_hexagon_sdk_3_5_4_eval.bin

C. SDK 目录介绍

  • SDK 根目录
    image.png

hexagon_sdk 目录

image.png

  • tools 目录

image.png

hexagon_sdk tools 目录

  • tools/HEXAGON_Tools 目录
    image.png

hexagon_sdk tools HEXAGON_Tools 目录
image.png

D. Android NDK

HVX SDK 需要依赖 Andriod NDK 来进行编译测试,NDK 需放置于${HVX_SDK_PATH}/tools 目录,Android NDK 需要开发者下载配置。

Android NDK 下载,文章中使用 Linux 版本 android-ndk-r19c。(3.5.4版本 SDK 使用 android-ndk-r19c 即可。

二、Hexagon SDK 手机端运行

该部分以 ${HVX_SDK_PATH}/examples/common/gaussian7x7 为例进行说明。

A. 工程编译

  • 设置环境变量:

    cd ${HVX_SDK_PATH}  
    source setup_sdk_env.source
  • Andorid 端应用编译:

    cd ${HVX_SDK_PATH}/examples/common/gaussian7x7
    make tree V=android_Release_aarch64 CDSP_FLAG=1

    Android 端可执行程序 位于${HVX_SDK_PATH}/examples/common/gaussian7x7/android\_Release\_aarch64/ship/ gaussian7x7 目录

  • CDSP 端应用编译:

    make tree V=hexagon_Release_dynamic_toolv83_v66 VERBOSE=1

    CDSP 端算法 libgaussian7x7_skel.so 库位于${HVX_SDK_PATH}/examples/common/gaussian7x7/hexagon_Release_dynamic_toolv83_v68/ship/libgaussian7x7_skel.so

相关编译选项解释:

image.png

B. 签名

手机系统中存在安全及认证机制,CDSP 库文件需要进行签名认证,以确保可以被正确加载运行。

  • 签名方法:

签名方法通常有两种:开发签名量产签名。(sm8150 之后,可以使用 Unsiged PD 方式进行算法验证测试,但部分硬件资源使用受限)

① 开发签名:

应用计算法处在开发阶段(Debug Fuse Enabled on的阶段)时,可以采用开发签名进行调试。

开发签名需要获取 设备端的序列号,然后生成相应的签名库文件 Testsig.so。

  • 获取序列号:

    adb wait-for-device root
    adb remount
    adb push ${HVX_SDK_PATH}/3.5.4/tools/elfsigner/getserial/CDSP/android_Release/getserial /data
    adb shell chmod 777 /data/getserial
    adb shell /data/getserial 

如果getserial 失败了, 用下面的指令:

adb shell cat /sys/devices/soc0/serial_number  //这里返回的是十进制,需要转化成十六进制
  • 根据序列号生成 testsig.so 开发签名库

    cd ${HVX_SDK_PATH}/tools/elfsigner/
    elfsigner.py -t 0xXXXXXXXX
    #0xXXXXXXXX为前面获取的序列号转换成十六进制的值。
  • 将签名库 testsig.so push 至手机端:
adb wait-for-device root
adb remount
adb shell mkdir -p /vendor/lib/rfsa/adsp

#testsig-0x6E07C1CE.so 为根据测试机序列号生成的开发签名库
adb push ${HVX_SDK_PATH}/tools/elfsigner/output/testsig-0x6E07C1CE.so /vendor/lib/rfsa/adsp/   

②量产签名:

量产签名主要用于批量生产时签名,需要对 DSP firmware 进行重新编译 。firmware 编译过程中会提取指定目录下算法库文件的哈希信息,然后存储于系统中,运行时会进行检测。(该方法需要针对每次算法调整都做签名)

③Unsiged PD:

从 8150 开始,增加 Unsiged PD feature,即在 host 端进行 CDSP 初始化时开启 unsiged PD 功能。

该方式可除部分硬件资源使用受限外,对于开发者而言更加便利。

// Unsigned PD
if (1 == UNSIGNED_PD)
{
    if (remote_session_control)
    {
        struct remote_rpc_control_unsigned_module data;
        data.enable = 1;
        data.domain = CDSP_DOMAIN_ID;
        retVal = remote_session_control(DSPRPC_CONTROL_UNSIGNED_MODULE, (void*)&data, sizeof(data));
        printf("remote_session_control returned %d for configuring unsigned PD.\n", retVal);
    }
    else
    {
        printf("Unsigned PD not supported on this device.\n");
    }
}

C. 算法实机测试

首先将编译生成的测试程序及库文件 push 至测试机中,该示例的测试应用编译路径为 ${HVX_SDK_PATH}/examples/common/gaussian7x7

  • push Android 端测试程序

    adb wait-for-device root
    adb remount
    #进入gaussian7x7例子目录
    cd ${HVX_SDK_PATH}/examples/common/gaussian7x7  
    adb push android_Release_aarch64/ship/gaussian7x7 /vendor/bin/
    adb shell chmod +x /vendor/bin/gaussian7x7
  • push DSP 端算法库文件

    adb push hexagon_Release_dynamic_toolv83_v66/ship/libgaussian7x7_skel.so /vendor/lib/rfsa/adsp/
  • 运行测试

    adb push hexagon_Release_dynamic_toolv83_v66/ship/libgaussian7x7_skel.so /vendor/lib/rfsa/adsp/

执行输出如下:
image.png
运行测试结果

上述为手机端运行测试流程,基于 hexagon-sim模拟器的算法运行测试会在后续章节进行介绍。

三、 算法实例分析

继续 gaussian7x7(${HVX_SDK_PATH}/examples/common/gaussian7x7)为例进行说明。

程序代码、编译文件和运行过程。

A. SDK 工程目录结构

image.png

  • asm_src:算法 HVX 汇编代码实现
  • inc:IDL 文件
  • src:CPU 侧代码实现和算法 DSP 侧实现(包含HVX Intrinsic代码)。DSP 侧代码提供了 HVX 汇编及 HVX Intrinsic 两种代码实现)。
  • android.min:CPU 侧代码编译的 makefile 配置文件
  • hexagon.min:DSP 侧代码编译的 makefile 配置文件

处理器间(CPU,DSP)通信由 Fastrpc 完成。算法调用过程解析通过 idl 编译生成的函数接口映射来处理。

  • IDL 映射文件

inc/gaussian7x7.idl 为该例程的映射文件,用来定义 CPU 和 DSP 同步使用的接口,包括函数、结构体等。

RPC 调用过程需要调用反射机制实现,HVX 的调用反射基于 IDL 来实现,使用 IDL 来定义调用接口,以使 CPU 能完成 DSP 的函数调用。
image.png

编译器根据 idl 文件编译生成 gaussian7x7.h、gaussian7x7_stub.c 和gaussian7x7_skel.c 三个文件。
image.png

下面介绍一下gaussian7x7的idl定义:

AEEResult Gaussian7x7u8
( 
    in sequence<uint8> src, // input buffer of unsigned 8-bit values
    in uint32 srcWidth,  // width of region of interest contained in src image 
    in uint32 srcHeight, // height of region of interest contained in src image
    in uint32 srcStride,           // stride of the src image 
    rout sequence<uint8> dst,  // output buffer of unsigned 8-bit values 
    in uint32 dstStride,           // stride of the dst image 
    in int32 LOOPS,                // number of times to iterate 
    in int32 wakeupOnly,           // flag to skip processing 
    inrout int32 dspUsec,           // profiling result in uSec 
    inrout int32 dspCyc             // profiling result in cycles 
);

上述代码为 gaussian7x7 的接口定义:

  • Sequence为表示数组参数,转义接口为 data 指针及 数组 size;

in 表示为参数为输入属性,生成为 const 类型。

因此 in sequence<uint8> src 对应的接口参数为 const uint8* imgSrc, int srcLen;

  • rout sequence<uint8> dst 中 rout 表示输出属性,生成对应的接口参数为 uint8* imgDst, int dstLen。
  • in uint32 srcWidth 生成对应的接口参数为 uint32 srcWidth
  • rout int32 dspUsec 生成对应的接口参数为 int32* dspUsec

生成三个文件位于 android_Release_aarch64 和 hexagon_Release_dynamic_toolv83_v66 文件夹内,如下图所示
image.png
image.png

在编程过程中, CPU 端会将 gaussian7x7.h 和 gaussian7x7_stub.c 代码编译后链接至 CPU 端的应用程序,DSP 端会将 gaussian7x7.h 和 gaussian7x7_skel.c 代码编译后链接生成 DSP 端运行库。

基于 IDL 生成函数接口如下,位于 gaussian7x7.h 中

QAIC_HEADER_EXPORT AEEResult __QAIC_HEADER(benchmark_gaussian7x7)(remote_handle64 _h, const uint8* src, int srcLen, uint32 srcWidth, uint32 srcHeight, uint32 srcStride, uint8* dst, int dstLen, uint32 dstStride, int32 LOOPS, int32 wakeupOnly, int32* dspUsec, int32* dspCyc) __QAIC_HEADER_ATTRIBUTE;

B. CPU 端算法流程

CPU 端的流程图如下(基于 ${HVX_SDK_PATH}/examples /common /gaussian7x7/gaussian7x7.c):
image.png

  • 通常在硬件设备和用户空间共享数据时,会基于 ION(后续 DMA BUF Heap)实现共享大尺寸连续物理内存,以减少内存拷贝开销实现 zero copy。该例程中使用 rpcmem_init 函数进行初始化(sm8350不再需要 rpcmem 初始化调用)

    rpcmem_init();
  • 初始化DSP,设置时钟,带宽等参数。
// call dspCV_initQ6_with_attributes() to bump up Q6 clock frequency
// Since this app is not real-time, and can fully load the DSP clock & bus resources 
// throughout its lifetime, vote for the maximum available MIPS & BW.
dspCV_Attribute attrib[] =
{
    {DSP_TOTAL_MCPS, 1000},                 // Slightly more MCPS than are available on current targets
    {DSP_MCPS_PER_THREAD, 500},             // drive the clock to MAX on known targets
    {PEAK_BUS_BANDWIDTH_MBPS, 12000},       // 12 GB/sec is slightly higher than the max realistic max BW on existing targets.
    {BUS_USAGE_PERCENT, 100},               // This app is non-real time, and constantly reading/writing memory
};

retVal = dspCV_initQ6_with_attributes(attrib, sizeof(attrib)/sizeof(attrib[0]));
printf("return value from dspCV_initQ6() : %d \n", retVal);
VERIFY(0 == retVal);
  • 基于 rpcmem 申请 buf,高通内部使用 ION 进行硬件设备内存共享,可以有效江都 Fastrpc 通信时间,基于常规堆内存分配,会引发数据拷贝操作。
// allocate ion buffers on CDSP side
VERIFY(0 != (src = (uint8_t*)rpcmem_alloc(ION_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, srcSize)));
printf("src - allocated %d\n", (int)srcSize);
VERIFY(0 != (dst = (uint8_t*)rpcmem_alloc(ION_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, dstSize)));
printf("dst - allocated %d\n", (int)dstSize);
  • 生成伪随机图像数据

    // allocate ion buffers on CDSP side
    VERIFY(0 != (src = (uint8_t*)rpcmem_alloc(ION_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, srcSize)));
    printf("src - allocated %d\n", (int)srcSize);
    VERIFY(0 != (dst = (uint8_t*)rpcmem_alloc(ION_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, dstSize)));
    printf("dst - allocated %d\n", (int)dstSize);
  • 进行算法调用测试

    unsigned long long t1 = GetTime();
    for (i = 0; i < LOOPS; i++)
    {
    // For HVX case, note that src, srcStride, dst, dstStride all must be multiples of 128 bytes.
    // The HVX code for this example function does not handle unaligned inputs.
    retVal = gaussian7x7_Gaussian7x7u8(src, srcSize, srcWidth, srcHeight, srcStride, dst, dstSize, dstStride);
    }
    
    unsigned long long t2 = GetTime();
    VERIFY(0 == retVal);
    #ifdef __hexagon__
    printf("run time of gaussian7x7_Gaussian7x7u8: %llu PCycles (from %llu-%llu) for %d iterations\n", t2-t1, t1, t2, LOOPS);
    printf("To apply timefilter to profiling results, add this to simulation cmd line: --dsp_clock 800 --timefilter_ns %d-%d\n", (int)(t1/0.8), (int)(t2/0.8));
    #else
    printf("run time of gaussian7x7_Gaussian7x7u8: %llu microseconds for %d iterations\n", t2-t1, LOOPS);
    #endif
    printf("return value from gaussian7x7_Gaussian7x7u8: %d \n", retVal);
    
    // validate results
    Gaussian7x7u8_ref(src, srcWidth, srcHeight, srcStride, ref, dstStride);
  • 运算结果比较

    int bitexactErrors = 0;
    printf( "Checking for bit-exact errors... \n");
    for (j = 3; j < dstHeight-3; j++)
    {
    for (i = 3; i < dstWidth-3; i++)
    {
        if (ref[j * dstStride + i] != dst[j * dstStride + i])
        {
            bitexactErrors++;
        }
    }
    }
    printf( "Number of bit-exact errors: %d \n", bitexactErrors);
    VERIFY(0 == bitexactErrors);
  • 释放资源

    int bitexactErrors = 0;
    printf( "Checking for bit-exact errors... \n");
    for (j = 3; j < dstHeight-3; j++)
    {
    for (i = 3; i < dstWidth-3; i++)
    {
        if (ref[j * dstStride + i] != dst[j * dstStride + i])
        {
            bitexactErrors++;
        }
    }
    }
    printf( "Number of bit-exact errors: %d \n", bitexactErrors);
    VERIFY(0 == bitexactErrors);

    C. DSP 端算法流程:

DSP 端的流程图如下(基于 ${HVX_SDK_PATH}/examples /common /gaussian7x7/gaussian7x7_imp.c):
image.png

  • 回调函数的流程图如下
    image.png
    DSP 端函数接口如下:
AEEResult gaussian7x7_Gaussian7x7u8(const uint8* imgSrc, int srcLen, 
    uint32 srcWidth, uint32 srcHeight, uint32 srcStride, uint8* imgDst, 
    int dstLen, uint32 dstStride) 
  • 系统架构及参数有效性检测
// only supporting HVX version in this example.
#if (__HEXAGON_ARCH__ < 60)
return AEE_EUNSUPPORTED;
#endif

// record start time (in both microseconds and pcycles) for profiling
#ifdef PROFILING_ON
uint64 startTime = HAP_perf_get_time_us();
uint64 startCycles = HAP_perf_get_pcycles();
#endif
// Only supporting 128-byte aligned!!
if (!(imgSrc && imgDst && ((((uint32)imgSrc | (uint32)imgDst | srcWidth | srcStride | dstStride) & 127) == 0)
        && (srcHeight >= 7)))
{
    return AEE_EBADPARM;
}

以上是异常检测的代码实现,包括有:

① 如果 DSP 版本小于 60,没有 HVX 硬件,退出。
② 如果 src,dst 地址是NULL,退出。
③ 如果 src,dst 地址不对齐,退出,因为代码实现(Gaussian7x7)只支持128对齐的数据。
④ 如果输入图像高度小于7,退出,Gaussian7x7代码无法正确运行。

  • 初始化并发参数

    // only supporting HVX version in this example.
    #if (__HEXAGON_ARCH__ < 60)
    return AEE_EUNSUPPORTED;
    #endif
    
    // record start time (in both microseconds and pcycles) for profiling
    #ifdef PROFILING_ON
    uint64 startTime = HAP_perf_get_time_us();
    uint64 startCycles = HAP_perf_get_pcycles();
    #endif
    // Only supporting 128-byte aligned!!
    if (!(imgSrc && imgDst && ((((uint32)imgSrc | (uint32)imgDst | srcWidth | srcStride | dstStride) & 127) == 0)
        && (srcHeight >= 7)))
    {
    return AEE_EBADPARM;
    }
  • 设置 HVX 运行模式为 DSPCV\_HVX\_MODE\_128B,早期的 HVX 有 128B 和 64B 两种模式,sm845之后只有 128B 模式
// for sake of example, assume only 128B implementation is available (i.e. intrinsics)
hvxInfo.mode = DSPCV_HVX_MODE_128B;
// Call utility function to prepare for a multi-threaded HVX computation sequence.
dspCV_hvx_prepare_mt_job(&hvxInfo);

// Check results and react accordingly. Treat failure to acquire HVX as a failure
if (hvxInfo.numUnits <= 0)
{
    dspCV_hvx_cleanup_mt_job(&hvxInfo);
    return AEE_EFAILED;
}

int numWorkers = hvxInfo.numThreads;
// split src image into horizontal stripes, for multi-threading.
dspCV_worker_job_t   job;
dspCV_synctoken_t    token;

// init the synchronization token for this dispatch. 
dspCV_worker_pool_synctoken_init(&token, numWorkers);
创建线程,以 gaussian7x7_callback 为回调函数。主线程使用 worker_pool_synctoken_wait(&token); 进行线程同步,该函数基于下述dspCV_worker_pool_synctoken_jobdone 来同步任务完成状态。

unsigned int i;
for (i = 0; i < numWorkers; i++)
{
    // for multi-threaded impl, use this line.
   (void) dspCV_worker_pool_submit(job);
    // This line can be used instead of the above to directly invoke the 
    // callback function without dispatching to the worker pool. 
    //job.fptr(job.dptr);
}
dspCV_worker_pool_synctoken_wait(&token);

创建线程,以 gaussian7x7_callback 为回调函数。主线程使用 worker_pool_synctoken_wait(&token); 进行线程同步,该函数基于下述dspCV_worker_pool_synctoken_jobdone 来同步任务完成状态。

unsigned int i;
for (i = 0; i < numWorkers; i++)
{
    // for multi-threaded impl, use this line.
   (void) dspCV_worker_pool_submit(job);
    // This line can be used instead of the above to directly invoke the 
    // callback function without dispatching to the worker pool. 
    //job.fptr(job.dptr);
}
dspCV_worker_pool_synctoken_wait(&token);
  • 回调函数(gaussian7x7_callback), 使用 dspCV_hvx_lock 锁 HVX 资源;使用 dspCV_worker_pool_synctoken_jobdone 函数结束子线程任务运算。
static void gaussian7x7_callback(void* data)
{
    gaussian7x7_callback_t    *dptr = (gaussian7x7_callback_t*)data;

    // lock HVX, 128B mode preferred. Main thread has already confirmed HVX reservation.
    int lockResult = dspCV_hvx_lock(DSPCV_HVX_MODE_128B, 0);
    // 64B mode is also acceptable
    if (0 > lockResult) 
    {
        lockResult = dspCV_hvx_lock(DSPCV_HVX_MODE_64B, 0);
    }
    
    if (0 > lockResult)
    {
         // this example doesn't handle cases where HVX could not be locked
         FARF(ERROR,"Warning - HVX is reserved but could not be locked. Worker thread bailing!");
         return;
    }
    // ....
    // ....
    
    // If HVX was locked, unlock it.
    dspCV_hvx_unlock();
    // release multi-threading job token
    dspCV_worker_pool_synctoken_jobdone(dptr->token);
}
  • 回调函数内部循环体

循环体中通过 unsigned int jobCount = worker_pool_atomic_inc_return(&(dptr->jobCount)) - 1; 通过原子计数来计算当前任务的执行数据地址偏移。

算法实现主要位于 Gaussian7x7u8PerRow 函数中,函数采用逐行实现的思路。

// atomically add 1 to the job count to claim a stripe.
unsigned int jobCount = dspCV_atomic_inc_return(&(dptr->jobCount)) - 1;

// if all horizontal stripes have been claimed for processing, break out and exit the callback
if (jobCount * dptr->rowsPerJob >= dptr->height)
{
    break;
}

// Set pointers to appropriate line of image for this stripe
unsigned char *src = dptr->src + (dptr->srcStride * dptr->rowsPerJob * jobCount);
unsigned char *dst = dptr->dst + (dptr->dstStride * dptr->rowsPerJob * jobCount);
// ...
Gaussian7x7u8PerRow(pSrc, dptr->srcWidth, dst, lockResult);
//....
其他
  • 其他

DSP 端进行数据处理前,可以通过 L2 预取操作以加速数据的存取。

数据预取操作会使用硬件提前完成数据从 DDR 到 L2 cache 的搬运操作,有效提高数据 load 的效率。

通常会采用 Ping-Pong 的思想进行数据预取,DSP 侧使用 L2fetch 函数在当前循环操作中预取下一次循环的数据,以使得数据搬运和数据运行并行化。

// initiate L2 prefetch (first 7 rows)
long long L2FETCH_PARA = CreateL2pfParam(dptr->srcStride, dptr->srcWidth, 7, 0);
L2fetch( (unsigned int)src, L2FETCH_PARA);
// next prefetches will just add 1 row
L2FETCH_PARA = CreateL2pfParam(dptr->srcStride, dptr->srcWidth, 1, 0);

四、总结

通过前面的介绍我们了解到了高通Hexagon SDK Linux/windows环境下的下载和安装,工程编译,手机签名以及工程在手机上的运行,同时还有实例的分析,这些都是工程的实际运用,需要自己多去试验。hexagon-sim模拟器的使用在后续篇章会详细介绍。

期望大家都能有所收获。

推荐阅读

更多嵌入式AI技术相关内容请关注嵌入式AI专栏。
推荐阅读
关注数
18808
内容数
1351
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息