高通处理器片上HexagonDSP计算
Published: 3/23/2024
高通骁龙处理器上搭载了一款多媒体信号处理器Hexagon DSP,其内部分为cDSP,aDSP与mDSP与sDSP,其中aDSP用于音频处理,mDSP用于调制和解调,sDSP用于传感器,cDSP用于图像处理和计算。其处理器(SnapDragon 800为例)如下所示:
其上包含了SnapDragon CPU本身,Adreno GPU以及本文主要涉及的Hexagon DSP。
Hexagon DSP简介
Hexagon DSP同样由高通研发,其架构如下所示:
尽管不同版本的Hexagon DSP架构可能有所差别,但基本架构是相似的。上面以Version 6(也就是V65~V69)为例, Hexagon DSP拥有多个硬件线程,每个硬件线程有自己的通用寄存器与谓词寄存器,以及计算时使用的ALU;Hexagon DSP拥有L1 cache与 L2 cache,其中L1 cache为每硬件线程独占,L2 cache为所有硬件线程共享,其使用的内存与CPU使用的内存相同,但处于不同的地址空间,也就是说CPU地址空间中的虚拟地址经过MMU转换后得到物理地址,该物理地址还需要经过Hexagon DSP的MMU转化为Hexagon DSP的地址空间中的虚拟地址(如果存在映射的话),才能被Hexagon DSP访问,Hexagon DSP还提供了一个DMA引擎,支持从内存到Hexagon DSP的DMA传输以及从Hexagon DSP到内存的DMA传输。值得注意的是,Hexagon DSP拥有一个SIMD协处理器,在V6版本中,该SIMD协处理器的寄存器长1024b,并提供了HVX指令集,该指令集提供了对SIMD协处理器的访问与操作。
Hexagon DSP的特点就是低时钟频率,多字节指令集。比CPU(3.xGHz)与GPU(1.xGHz)低得多的低时钟频率(800MHz)使得Hexagon DSP的功耗更低;多字节指令集则使得 Hexagon DSP的指令并发度更高,这些同时也是其在移动端被设计的初衷,提供低功耗 的一定计算能力。
性能简单对比
事实上,如今的设备上Hexagon DSP的计算能力较CPU与GPU低很多,但随着计算精度降低, 这一差距可能会缩小。实测1024X1024 INT8 GEMM 在SnapDragon 8G1上41.37ms;在Adreno 730 GPU上为36.78ms;在Hexagon V69 DSP上为131.07ms。CPU端使用OpenMP并行化,根据L1/L2 cache进行分块计算来掩盖访存延迟,GPU端则利用OpenCL同样根据Cache大小将矩阵分块计算,DSP则使用4硬件线程以及HVX指令集加速计算。如果是INT32或者是FP32 GEMM,差距会成指数增大(FP32时CPU,GPU,DSP端分别为148.87ms,220.78ms,7s多,前面两个或许还能额外优化,但是Hexagon V6 DSP确实不适合计算浮点数或者是高精度数)。
使用
使用的工具链包括Hexagon SDK与Android NDK。
高通提供的Hexagon SDK包含了Hexagon DSP的编译器,调试器以及运行时库等。可以从高通官网-SDK下载,下载需要注册用户,并且其中一些运行时库并不开源^^,下载后参照说明文档安装即可,需要注意的是除去SDK本体外,其还会克隆一些子模块,但这些子模块并非必须,因此即使因网络问题克隆失败也可以继续使用SDK。开发时仍需要Android 工具链,Hexagon SDK中的子模块包含了某版本的Android NDK,你也可以选择自行安装并配置相关环境变量。
Hexagon SDK中提供了完备的文档,并且example
文件夹包含了诸多示例程序,可以作为参考,事实上参考这些已经能够写出较为完整的计算程序,下面的内容仅作为记录。
编译
使用Hexagon DSP进行计算需要分别编译CPU端程序与DSP端程序,编译方式提供了 make,cmake,ninja总计三种,似乎cmake最为方便。只需要在CMakeLists.txt中 引入以下内容,即可使用SDK提供的util函数。
if(HEXAGON_SDK_ROOT)
include(${HEXAGON_SDK_ROOT}/build/cmake/hexagon_fun.cmake)
else()
include(${HEXAGON_CMAKE_ROOT}/hexagon_fun.cmake)
endif()
主要用到的函数包括link_custom_library
与build_idl
: link_custon_library
用于链接SDK提供的库,build_idl
用于编译IDL文件生成双端需要的接口原文件,比如:
link_custom_library(qqq rpcmem)
# ....
build_idl(inc/qqq.idl qqq)
对于CMake, Hexagon DSP提供了编译双端代码的工具,位于$HEXAGON_SDK_ROOT/build/cmake/Ubuntu/build_cmake
,使用如下:
## 编译android端代码,生成可执行文件以及所需的CPU端Stub库
$HEXAGON_SDK_ROOT/build/cmake/Ubuntu/build_cmake android BUILD_Release BUILD_OUTPUT_DIR=android
## 编译DSP端代码,生成DSP端Skel库
$HEXAGON_SDK_ROOT/build/cmake/Ubuntu/build_cmake hexagon BUILD_Release BUILD_OUTPUT_DIR=hexagon DSP_TYPE=V69
build_cmake
工具实际上是通过python打包的脚本,调用SDK中的$HEXAGON_SDK_ROOT/build/cmake/cmake_configure.bash
来完成具体构建逻辑。
IDL定义
Hexagon SDK使用qaic IDL定义CPU端与DSP端所需的接口,使用FastRPC或是DMA在双端之间进行数据通信。
IDL中接口的定义在SDK文档中已经注明,但值得注意的是<sequence>
与in
,rout
的使用。
#include "AEEStdDef.idl"
#include "remote.idl"
interface calculator_plus : remote_handle64{
long sum(in sequence<long> vec, rout long long res);
long static_sum(in sequence<long> vec, rout long long res1, rout long long res2);
long iostream_sum(in string filename, rout long long res);
long uppercase_count(in string name, rout long res);
long test_tls(rout long long res);
};
上述IDL定义可以使用SDK提供的IDL编译器qaic编译,取自Hexagon SDK例子calculator_c++
中的calculator_plus.idl
,如果使用FastRPC进行通信,那么IDL中的interface需要继承remote.idl
中的remote_handle64
,其中每个参数前使用in
,rout
,inrout
修饰:in
表示传值,数据传入,rout
表示传引用(指针),数据传出,inrout
则数据既传入又传出,
传引用(指针) ,当需要传递一块内存区域时,就需要使用rout
或是inrout
,但这里必须使用<sequence>
修饰符定义该参数,否则只会传入一个元素,因为<sequence>
在编译时除了生成指针参数外,还会添加额外的内存区域长度参数表示该内存区域的长度,如果不使用<sequence>
修饰符,那么就会将该参数当作一个单独的元素传入。
IDL中使用的数据类型与C/C++中的数据类型映射同样可在文档中找到。对于上述文件,通过build_idl
会在指定文件夹生成calculator_plus.h
,calculator_plus_skel.c
与
calculator_plus_stub.c
,分别用于后续生成skel库与stub库,skel.c主要在DSP端调用自定义实现的DSP端函数,stub.c则主要用于使用FastRPC调用DSP端接口。
如果继承的是remote_handle64,那么在build_idl
时还会生成<interface_name>_open
以及<interface_name>_close
接口,这两个函数真正的实现位于DSP端由实现者给出,
用于创建或是释放FastRPC所使用的remote_handle64句柄,创建时可以将DSP端计算所需数据分配给该句柄。事实上,这个remote_handle64就是个64位unsigned long long,
open函数在DSP端则主要分配一块空间并将地址填入其中。
FastRPC与RPCmem
如之前所言,DSP与CPU处于不同地址空间,rpcmem是SDK提供的库,用于分配CPU与DSP端都映射的内存,其本质是利用ION 内存分配器分配内存,并使用内存映射映射到DSP中,
提供的接口为rpcmem_alloc
与rpcmem_free
,其内部分装调用了memalign_alloc
与free
(对于linux环境)。
rpcmem_alloc
支持使用不同的堆内存进行分配,支持的堆有RPCMEM_HEAP_ID_SYSTEM
(系统堆),RPCMEM_HEAP_ID_CONTIG
(物理连续堆,适用于mDSP以及sDSP);通过参数
可以指定分配的内存是否可被Cache缓存。
QURT RTOS线程管理
cDSP上运行着QURT RTOS可以管理所运行的线程(毕竟上面有四个硬件线程),线程的使用可以参考example中的multithreading,以下是例子:
/// QURT 头文件
#include "qurt.h"
/// 初始化线程环境
qurt_thread_attr_t attr1;
void* thread_stack_addr;
thread_stack_addr= malloc(STACK_SIZE);
qurt_thread_attr_init(&attr1);
qurt_thread_attr_set_name(&attr1, (char *)"cntr1");
qurt_thread_attr_set_stack_addr(&attr1, thread_stack_addr);
qurt_thread_attr_set_stack_size(&attr1, STACK_SIZE);
qurt_thread_attr_set_priority(&attr1, QURT_THREAD_ATTR_PRIORITY_DEFAULT/2);
/// 通过该方法启动线程,并传入参数,线程函数参数为void*指针
retcode = qurt_thread_create(&tid1, &attr1, square_sum, (void *) &arg_1);
/// 等待线程结束运行,获取返回值
qurt_thread_join(tid1, &thread_exit_status);
HVX指令集
HVX指令集相关的内容主要位于hvx_hexagon_protos.h
,hvx_protos.h
,hexagon_types.h
三个头文件中,这三个头文件提供了基于HVX指令集的intrinsic宏。至于
微架构层面的HVX汇编指令,这里暂不做讨论。V6下的DSP HVX仅支持INT8,INT16,INT32等类型的向量指令,对于FP32等类型的支持则在V73时才被提供。
事实上,高通同样基于HVX指令集提供了数学库QHL(qualcomn hexagon library),具体使用内容参考qhmath.h
与qhblas.h
以及example中的qhl_hvx
。
具体来说,SDK提供的计算库位于$HEXAGON_SDK_ROOT/libs/qhl
以及$HEXAGON_SDK_ROOT/libs/qhl_hvx
中,前者主要使用汇编实现标量计算,后者则
使用V66以上的HVX intrinsic实现了若干计算函数,V68及以上则包括了浮点数。
qhl_hvx
中主要包含qhmath
(常用数学操作),qhblas
(线性代数),qhdsp
(信号处理相关);qhl
主要包含qhmath
,qhblas
,qhdsp
,qhcomplex
(复数),
其中使用的DSP端汇编可在对应版本的Hexagon DSP编程手册中找到相关信息,
至于和手工实现相比的性能提升倒还没有测试。
运行
运行时环境如果不是高通开发板而是移动端(比如手机),就需要Root才能调用DSP端,可以通过adb或是termux在移动端运行。
由于高通限制除cDSP以外的DSP都不能使用unsigned 库(也就是未被制造厂商签名的库),即使是cDSP,也需要在运行时通过
remote_session_control
来开启使用unsigned 库。remote_sesstion_control
主要用于查询配置DSP端设置,
包括是否支持或是使用unsigned库,配置线程优先级等。
DSP端字符输出主要通过FARF
宏(位于hap_farf.h
中),并提供了不同级别的日志输出,最高为ALWAYS
,通过对应文件名的.farf文件控制输出级别阈值。
输出的内容并不会直接经过控制台,而是通过logcat输出。
值得一提的是,adb提供了server功能,如果有windows平台需要在wsl中使用的情况,
可以在windows中使用adb -a -P <port> nodaemon server
开启server,其中
<port>
必须是未被占用未被windows屏蔽的端口,之后就可以在wsl中通过该
端口使用adb了,如adb -H <ip> -P <port> shell
,其中<ip>
指的是
wsl的网关,也就是windows本身,<port>
则是之前指定的端口。
HAP perf
Hexagon SDK中提供了一些profiling用的工具,具体可以参考example中的profile,
主要内容位于HAP_perf.h
中,常用的有:
static inline uint64 HAP_perf_get_time_us(void);
static inline uint64 HAP_perf_get_qtimer_count(void);
uint64 HAP_perf_qtimer_count_to_us(uint64 count);
结论
上述内容主要记录了使用HexagonDSP的基础,至于qhl以及更深层的HVX指令集并没有深入探索,有时间再看看吧。