专栏算法工具链DSP开发快速上手

DSP开发快速上手

颜值即正义2022-11-23
509
1
本文目录
1 J5 DSP简介
2 官方文档说明
3 地平线开发框架简介
4 OE包DSP开发示例简介
5 DSP最简开发流程简介(对应OE 1.1.45版本及之后)
附 旧版本OE对应的最简开发代码(OE 1.1.25a - 1.1.44

1 J5 DSP简介

地平线的 J5 芯片相比于 J3 新增了 2 颗 DSP 核,使用的是 Cadence 公司的 Tensilica Vision P6 DSP IP(下文简称 VP6,官网信息请见: Vision P6 DSP (https://www.cadence.com/en_US/home/tools/ip/tensilica-ip/vision-dsps/vision-p6.html ), 它是专用于视觉/图像处理的数字信号处理器,且已通过ISO 26262 功能安全认证。单颗 VP6 的硬件架构如下所示:
  1. 图中白色区域为 VP6 计算相关硬件,主要支持标量计算(Scalar Processing Units)、矢量浮点计算(VFPU)和矢量定点计算(Vector Processing Units),其中:

  • 红叉表示 J5 芯片的 VP6 没有做对应的硬件选型,即指令内存 IRAM 只有一个;VFPU 只支持 FP32 的浮点精度,不支持 FP16 的半精度。

  • VP6 的整型乘加器可以支持 256 个 8x8 或 128 个 8x16 或 64 个 16x16 或 16 个 16x32 MACs。

  • VP6 的计算主频为 660MHz,并且最多支持 5 路 VLIW(超长指令字)和 512bit 的 SIMD(单指令多数据流)。

  1. 图中蓝色区域为 VP6 内存相关硬件,其中:

  • 单颗 VP6 共有 2 个 128KB 可编程 DRAM(也称为 TCM),能提供与 Cache 相当的性能。

  • 芯片上的 VP6 与 ARM/BPU 共享 DDR,即支持零拷贝机制。iDMA(Integrated DMA)硬件则用于 DDR 和 TCM 之间的数据搬运(当数据量较大时还需要实现),J5 选用的是 128-bit 的 AXI4 总线接口。

下表为 VP6 主要资源汇总:

2 官方文档说明

当大家参考 J5 工具链文档完成 DSP 开发包的安装后,可以在以下两个路径查找到详细的官网文档,分别点击文件夹中的 index.html 即可查看。

  • XtDevTools/downloads/RI-2021.7/docs

  • XtDevTools/install/builds/RI-2020.4-linux/vdsp_vp6_RI4


其中很多 dsp 底层接口都可以在 proccessor ISA 文档中找到说明。

3 地平线开发框架简介

如下图所示,J5 芯片中的 VP6 与 ARM 以 Server-Client 的形式,通过 rpmsg 进业务通讯。如前文所述,因为 VP6 和 CPU/BPU 共享内存,所以数据存储在 DDR 上,rpmsg 只需要传递一些数据量很小的 meta data,比如数据地址。DSP 基于内部的 LiteMMU 硬件自动完成地址位宽(ARM 为 64 位位宽、DSP 为32 位位宽)映射后即可使用该数据。


为了方便用户使用,地平线还封装了一套基于 RPC 的开发框架,并提供了相关的 API 接口,接口说明请见工具链文档,接口使用请参考 OE 包 ddk/samples/vdsp_rpc_sample 路径提供的示例。

4 OE包DSP开发示例简介

DSP示例包展示了如何在j5上使用dsp进行任务处理。DSP示例包中包含CV示例和NN示例:

  • CV示例中封装了常见的cv api,并通过Sample介绍了各个api的使用方法。

  • NN示例中包含quantize、dequantize、nn_plugin、softmax以及pointpillar前处理算子api,并展示了示例用法。

各个示例主要分为arm侧和dsp侧两部分,其中arm侧负责准备数据然后发起rpc调用,dsp侧负责接收arm侧发来的任务,完成任务计算,将结果发送给arm。
开发者可以体验并基于这些示例进行应用开发,降低开发门槛。

5 DSP最简开发流程简介

自定义算子示例说明

目前地平线在 OE 包 ddk/samples/vdsp_rpc_sample 路径提供了 Softmax 自定义算子的 dsp 开发示例。在 该示例中,MobileNet-v1 分类模型尾部的 Softmax 原本跑在 CPU 上,我们可以通过 RPC 接口将其调度至 DSP 硬件上进行推理,其计算框架如下图所示:

其中ARM端主要负责计算资源的分配以及DSP任务的发起和回收。 DSP端主要负责执行计算逻辑,将ARM端分配的计算任务按照调度逻辑逐个完成,并返回计算结果。 算子的执行过程如下:
  1. 实现DSP端自定义算子,注册该op并启动DSP镜像;

  2. 实现ARM端用户自定义算子的推理类;

  3. ARM端初始化推理资源,准备进行模型推理;

  4. ARM端准备好DSP调度需要的资源,并封装需要传递给DSP的参数,通过用户自定义算子发起RPC调用任务;

  5. DSP schedule接收到RPC命令,根据调度优先级顺序执行已注册的DSP op进行运算;

  6. DSP计算结束后,通过RPC将计算结果返回至ARM端;

  7. ARM端接收到DSP返回的算子计算结果,根据返回值继续执行后续逻辑。

直接开发示例说明

除了模型中的 CPU 算子可以使用 DSP 进行推理外,用户也可以直接基于地平线封装的 RPC 框架及接口将模型的前后处理或者其他算法部署在 DSP 上。

该章节适用于天工开物OpenExplorer1.1.45及之后的版本,若您使用的工具链版本较老,在1.1.45之前,可以查看最文章下方历史版本对应的代码编写方法。
下文将以一个简单的示例介绍如何编写 ARM 和 DSP 侧代码,并在 J5 板端让 ARM 调用 DSP 执行计算。
对于ARM调用DSP这项任务来说,需要编写编译DSP和ARM两部分代码。
  1. 在DSP侧编写好任务的执行步骤并注册算子,编译成DSP镜像部署到J5板端。

  2. ARM侧的完整编程步骤如下方流程图所示,代码完成后,编译成可执行文件在J5板端运行。

完成以上两步后,即可在J5上使用ARM调用DSP执行自定义的计算任务。接下来,以实现两个浮点数的加法计算为例,介绍代码的具体编写方法。

DSP

首先进入dsp/src文件夹,新建add.h:

之后在同目录下新建add.cc:

typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int dsp_add(void *input, void *output, void *tm)
{
//tm用于tile_manager,本示例可忽略
addDspParam *ptr = (addDspParam *)(input);
float *dst = (float *)(output);
*dst = ptr->input1 + ptr->input2;
return 0;
}

修改dsp下的main.cc,新增头文件引用#include "src/add.h",之后在hb_dsp_start();代码前添加一行hb_dsp_register_fn(0x1200, dsp_add, 0);即可,编号0x1200可以自定义,可以选用0x1000-0xffff之间的数值。
之后运行同目录下的build_dsp.sh脚本,会在script/image目录下生成vdsp0和vdsp1镜像文件。

ARM

对于ARM侧的代码来说,需要先定义和分配输入输出内存,同时定义好DSP的调用参数,这里需要注意3点:①定义内存时使用hbSysMem,②参数拷贝时使用虚拟地址virAddr,③rpcCmd需要和DSP侧注册算子时使用的编号相同。之后,使用hbDSPRpc接口让DSP启用计算任务,使用hbDSPWaitTaskDone接口等待任务执行结束,使用hbDSPReleaseTask接口释放任务,再通过零拷贝的方式将DSP的计算结果传递回ARM侧,打印输出结果后释放申请的内存资源,从而结束整个调用流程。
进入arm/nn/src文件夹,新建test_add.cc:
typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int test_add(int argc, char **argv)
{
float a = 10;
float b = 20; //设置两个加数的值
std::cout<<"input1 = "<< a <<std::endl;
std::cout<<"input2 = "<< b <<std::endl;
std::cout<<"DSP ADD START!"<<std::endl;
hbSysMem input_mem, output_mem;
hbSysAllocMem(&input_mem, sizeof(addDspParam));
hbSysAllocMem(&output_mem, sizeof(float)); //为输入输出分配内存
addDspParam ptr = (addDspParam)(input_mem.virAddr);
ptr->input1 = a;
ptr->input2 = b; //用指针传递输入数据
hbDSPRpcCtrlParam param;
param.rpcCmd = 0x1200; //0x1200与DSP算子注册的编号一致
param.priority = 0;
param.dspCoreId = 0;
hbDSPTask_t task;
hbDSPRpc(&task, &input_mem, &output_mem, &param);
hbDSPWaitTaskDone(task, 0);
hbDSPReleaseTask(task);
std::cout<<"output = "<< (float)output_mem.virAddr <<std::endl;
std::cout<<"DSP ADD SUCCESS!"<<std::endl;
hbSysFreeMem(&input_mem); //释放内存资源hbSysFreeMem(&output_mem);
return 0;
}

修改arm/nn目录的main.cc,新增extern int32_t test_add(int argc, char **argv);并将主函数里的test_all(argc, argv);修改为test_add(argc, argv);完成ARM侧代码调整。
之后运行同目录下的build_arm.sh脚本,会在nn/script/lib中生成依赖文件,并在nn/script/bin中生成可执行文件test_nn。

板端运行

将整个script文件夹复制到J5开发板上的userdata目录,之后编写deploy.sh脚本,该脚本用于配置DSP镜像:

第三行的DSP镜像文件目录可根据实际情况修改。
之后运行export HB_DSP_ENABLE_DIRECT_MODE=true用于将DSP配置为直连模式。
先后运行sh deploy.sh和sh run_nn_test.sh,即可执行加法计算。

可以看到ARM成功调用DSP执行了多输入的加法计算任务。

附 旧版本OE对应的最简开发代码

1.1.25a - 1.1.36

DSP

在dsp目录下新建文件夹add,并建立头文件add_dsp.h和源码文件add_dsp.cpp。在头文件中,需要定义DSP侧计算的函数dsp_add,具体代码如下:

int dsp_add(void *input, void *output, void *tm)
;
#ifdef _cplusplus

}
#endif

#endif
//HOBOT_ADD_DSP_H

dsp_add的具体实现在源码add_dsp.cpp中编写。这里需要先定一个结构体,结构体里包含两个加数,在函数dsp_add中,定义指针ptr_add_param以在后续对结构体参数进行加法操作。

typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int dsp_add(void *in, void *out, void *tm) {
addDspParam *ptr_add_param = (addDspParam *)(in);
addDspParam p;
memcpy((void *)(&p), (void *)(ptr_add_param), sizeof(addDspParam));
ptr_add_param = &p; //定义指针
float *dst = (float *)(out);
*dst = ptr_add_param->input1 + ptr_add_param->input2;
return 0;
}

之后,在当前目录下创建CMakeLists.txt,内容如下:

同时,dsp目录下的CMakeLists.txt文件也进行修改,添加add_subdirectory(add)并在target_link_libraries下方添加add_dsp。
以上操作完成后,在dsp目录下的main.cc中注册刚刚编写好的dsp算子,首先添加头文件行#include "add/add_dsp.h",再在hb_dsp_env_init()后添加hb_dsp_register_fn(0x51, dsp_add, 0); 即可,这里的编号0x51可以自定义。

ARM

在arm目录下新建arm_add文件夹,建立main.cpp。
对于ARM侧的代码来说,需要先定义和分配输入输出内存,同时定义好DSP的调用参数,这里需要注意3点:①定义内存时使用hbSysMem,②参数拷贝时使用虚拟地址virAddr,③rpcCmd需要和DSP侧注册算子时使用的编号相同。之后,使用hbDSPRpc接口让DSP启用计算任务,使用hbDSPWaitTaskDone接口等待任务执行结束,使用hbDSPReleaseTask接口释放任务,再通过零拷贝的方式将DSP的计算结果传递回ARM侧,打印输出结果后释放申请的内存资源,从而结束整个调用流程。
加法计算需要两个输入值,可以定义一个结构体,让结构体包含两个加数,再给DSP传递结构体的地址。
typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int main(int argc, char **argv)
{
float a = 10.0;
float b = 20.0;
float c = 0.0; //a与b是加数,c是未计算的和

hbSysMem input_mem, output_mem;

hbSysAllocMem(&input_mem, sizeof(addDspParam));
hbSysAllocMem(&output_mem, sizeof(float)); //为输入输出分配内存
addDspParam in = {
a,
b,
}; //实例化结构体
std::cout<<"input1 = "<< a <<std::endl;
std::cout<<"input2 = "<< b <<std::endl;
std::cout<<"DSP ADD START!"<<std::endl; //输入数据,input1=10,input2=20
hbDSPRpcCtrlParam param;
param.rpcCmd = 0x51; //0x51与DSP算子注册的编号一致
param.priority = 0;
param.dspCoreId = 0;
memcpy(input_mem.virAddr, &in, sizeof(addDspParam)); //将结构体传给输入内存
hbDSPTask_t task;
hbDSPRpc(&task, &input_mem, &output_mem, &param);
hbDSPWaitTaskDone(task,0);
hbDSPReleaseTask(task);
memcpy(&c, output_mem.virAddr, sizeof(float)); //将DSP计算结果传给变量c
std::cout<<"output = "<< c <<std::endl; //输出相加结果,output=30
std::cout<<"DSP ADD SUCCESS!"<<std::endl;
hbSysFreeMem(&input_mem); //释放内存资源
hbSysFreeMem(&output_mem);
return 0;
}

在同目录下创建CMakeLists.txt文件,将工程名定为add_test,生成的可执行文件定名为run_add_test。之后将arm文件夹下的CMakeLists.txt的add_subdirectory()参数改为arm_add即可。

板端运行

在arm和dsp目录分别执行sh build.sh命令,即可得到上板运行的可执行文件和DSP镜像,将这些文件及相关依赖(arm侧的lib文件夹)复制到J5板端。
对于DSP镜像,可编写deploy.sh脚本部署,其中第三行的目录可以是板端的任意可写文件夹,用于存放vdsp0和vdsp1镜像文件。
执行脚本时,如果当前没有正在运行的DSP镜像,那么前两行stop指令会报错,但是可以忽略。
对于lib依赖库的加载及可执行文件的运行,可编写add_test.sh脚本:

执行sh deploy.sh部署DSP环境并执行sh add_test.sh

1.1.37 - 1.1.44

DSP

首先进入nn/dsp/src文件夹,新建add.h:

之后在同目录下新建add.cc:

typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int dsp_add(void *input, void *output, void *tm)
{
//tm用于tile_manager,本示例可忽略
addDspParam *ptr = (addDspParam *)(input);
float *dst = (float *)(output);
*dst = ptr->input1 + ptr->input2;
return 0;
}

修改nn/dsp下的main.cc,新增头文件引用#include "src/add.h",之后在hb_dsp_start();代码前添加一行hb_dsp_register_fn(0x800, dsp_add, 0);即可,编号0x800可以自定义,限定uint16范围。

之后运行同目录下的build_dsp.sh脚本,会在nn/script/image目录下生成vdsp0和vdsp1镜像文件。

ARM

进入nn/arm/src文件夹,新建test_add.cc:

typedef struct {
float input1;
float input2;
} addDspParam; //定义结构体,包含两个加数
int test_add(int argc, char **argv)
{
float a = 10;
float b = 20; //设置两个加数的值
std::cout<<"input1 = "<< a <<std::endl;
std::cout<<"input2 = "<< b <<std::endl;
std::cout<<"DSP ADD START!"<<std::endl;
hbSysMem input_mem, output_mem;
hbSysAllocMem(&input_mem, sizeof(addDspParam));
hbSysAllocMem(&output_mem, sizeof(float)); //为输入输出分配内存
addDspParam ptr = (addDspParam)(input_mem.virAddr);
ptr->input1 = a;
ptr->input2 = b; //用指针传递输入数据
hbDSPRpcCtrlParam param;
param.rpcCmd = 0x800; //0x800与DSP算子注册的编号一致
param.priority = 0;
param.dspCoreId = 0;
hbDSPTask_t task;
hbDSPRpc(&task, &input_mem, &output_mem, &param); //提交DSP任务
hbDSPWaitTaskDone(task, 0);
hbDSPReleaseTask(task);
std::cout<<"output = "<< (float)output_mem.virAddr <<std::endl;
std::cout<<"DSP ADD SUCCESS!"<<std::endl; //打印输出结果
hbSysFreeMem(&input_mem); //释放内存资源
hbSysFreeMem(&output_mem);
return 0;
}

修改nn/arm目录的main.cc,新增extern int32_t test_add(int argc, char **argv);并将主函数里的test_all(argc, argv);修改为test_add(argc, argv);完成ARM侧代码调整。

之后运行同目录下的build_arm.sh脚本,会在nn/script/lib中生成依赖文件,并在nn/script/bin中生成可执行文件test_nn。

板端运行

将整个nn/script文件夹复制到J5开发板上的userdata目录,之后编写deploy.sh脚本,该脚本用于配置DSP镜像:

第三行的DSP镜像文件目录可根据实际情况修改。
先后运行sh deploy.sh和sh run_nn_test.sh,执行加法计算。

由于1.1.24及之前版本的OE,使用的DSP开发软件(Xtensa Xplorer)版本较老,因此相关代码不再提供和维护。

算法工具链
官方教程
评论1
0/1000
  • hunterkan
    Lv.1

    赞,更新很及时?

    2023-01-16
    0
    0