TheLudGamer 发表于 2021-11-27 14:26

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

版权声明 本文首发于知乎专栏:移动端算法优化
本专栏「移动端算法优化」所有文章著作权归作者所有。
允许个人直接分享本专栏文章到个人微博、朋友圈。但媒体(包括但不限于网站、微信公众号、微博营销号)转载需事先征得专栏同意。转载需在正文开头显著位置注明出处,给出原始链接,注明「发表于知乎专栏《移动端算法优化》」,并不得进行任何形式的修改演绎。<hr/>本篇以 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 版本进行介绍。



下载官网示意图

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.binC. SDK 目录介绍


[*]SDK 根目录



hexagon_sdk 目录

文件夹描述build编译所需的编译脚本文件docsHVX 开发相关说明文档examplesSDK 例子,HVX 样例位于 common 文件夹incsSDK 头文件目录,包含 HVX 函数及指令等头文件libsSDK 必需库文件目录,包含 HVX 开发运行所涉及的必备库及部分实现代码,例如 dspcv,fastcv,hexagon_nn 等scripts常用脚本目录setup_sdk_env.source环境变量设置脚本tools常用工具集合,包含 ndk,qaic 及签名工具(elfsigner)等

[*]tools 目录



hexagon_sdk tools 目录


[*]tools/HEXAGON_Tools 目录



hexagon_sdk tools HEXAGON_Tools目录

功能路径HVX 模拟器仿真样例${HVX_SDK_PATH}\tools\HEXAGON_Tools\8.3.07\Examples\HVXDSP 开发手册文档${HVX_SDK_PATH}\tools\HEXAGON_Tools\8.3.07\Documents\Hexagon_Document_Bundle.pdfD. 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=1Android 端可执行程序 位于${HVX_SDK_PATH}/examples/common/gaussian7x7/android_Release_aarch64/ship/ gaussian7x7 目录

[*]CDSP 端应用编译:
make tre V=hexagon_Release_dynamic_toolv83_v66 VERBOSE=1CDSP 端算法 libgaussian7x7_skel.so 库位于${HVX_SDK_PATH}/examples/common/gaussian7x7/hexagon_Release_dynamic_toolv83_v68/ship/libgaussian7x7_skel.so
相关编译选项解释:
toolv83表示 tools 版本是 8.3V66表示 DSP 架构版本是 V66(SM8150, SM8250 使用 V66, SM8350 使用 V68)CDSP_FLAG=1表示引用加载至 CDSP 端运行。SOC 中存在多个 DSP(如 ADSP,CDSP 等),编译时需显式指定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 shell
cd /vendor/bin
./gaussian7x7执行输出如下:



运行测试结果

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

继续 gaussian7x7(${HVX_SDK_PATH}/examples/common/gaussian7x7)为例进行说明。
程序代码、编译文件和运行过程。
A. SDK 工程目录结构




[*]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 的函数调用。


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


下面介绍一下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;

[*]routsequence<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 文件夹内,如下图所示




在编程过程中, 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):



[*]通常在硬件设备和用户空间共享数据时,会基于 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));
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);

[*]生成伪随机图像数据
// populate src buffer (with a simple pattern)
for (j = 0; j < srcHeight; j++)
{
    uint8_t *ptr = &src;
    for (i = 0; i < srcWidth; i++)
    {
      *ptr++ = i + j;             // some incrementing pattern fill
    }
}

[*]进行算法调用测试
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 != dst)
      {
            bitexactErrors++;
      }
    }
}
printf( "Number of bit-exact errors: %d \n", bitexactErrors);
VERIFY(0 == bitexactErrors);

[*]释放资源
if(src)
{
    rpcmem_free(src);
}
if(dst)
{
    rpcmem_free(dst);
}
// free ion buffers
rpcmem_deinit();

if(ref)
{
    free(ref);
}

printf("calling dspCV_deinitQ6()... \n");
retVal = dspCV_deinitQ6();
printf("return value from dspCV_deinitQ6(): %d \n", retVal);

if (0 == (retVal | nErr))
{
    printf("- success\n");
    return 0;
}
else
{
    printf("- failure\n");
    return -1;
}C. DSP 端算法流程:

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



[*]回调函数的流程图如下


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代码无法正确运行。

[*]初始化并发参数
// Determine if it is safe (from an audio/voice/camera concurrency perspective) to run a compute function now
dspCV_ConcurrencyAttribute attrib =
{
    {COMPUTE_RECOMMENDATION, 0},// query for compute concurrency recommendation
};
dspCV_concurrency_query(attrib, 1);
if (COMPUTE_RECOMMENDATION_NOT_OK == attrib.value)
{
    // return error back to application
    return AEE_EBADSTATE;
}

// Determine if HVX is available and in what configuration
dspCV_hvx_config_t hvxInfo = {0};

[*]设置 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), 使用 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模拟器的使用在后续篇章会详细介绍。
期望大家都能有所收获。
未完待续。。。

Doris232 发表于 2021-11-27 14:27

我们建了一个群,期待志同道合的您加入[握手]查看图片

mastertravels77 发表于 2021-11-27 14:32

dspCV已经没了,高通坑爹的兼容性

JamesB 发表于 2021-11-27 14:42

[大笑]可以基于 workpool 封装成 dspCV 原有部分接口,隐藏符号表,兼容原有实现
页: [1]
查看完整版本: HVX 优化技术-HVX 开发环境介绍