Skip to content

算子实现

以下内容以gather & adds算子为例,介绍SIMD与SIMT混合算子的实现过程。完整代码请参见SIMD与SIMT混合编程实现gather&adds算子样例

说明

本样例的目的是通过一个简单的算子实现展示SIMD与SIMT混合编程方式,仅作为功能介绍。

算子功能

gather & adds算子从长度为100000的一维向量input中,按index指定的位置提取8192个数据,并对提取后的数据执行加1计算。算子输出output中第i个数据为:

C++
output[i] = input[index[i]] + 1

计算过程可拆分为gather和adds两个阶段:gather阶段根据index从input中离散读取数据,并将结果写入Unified Buffer(UB);adds阶段对UB上的连续中间数据执行加1计算,计算结果原地写回UB,最后将结果数据搬运到GM上的output中。

其中,gather阶段包含索引映射和离散访问,使用SIMT表达每个线程的数据访问逻辑;adds阶段处理连续数据,使用SIMD完成向量计算。

算子分析

图 1 gather & adds算子整体执行流水

gather & adds算子整体执行流水如图1所示。simt_gather和simd_adds同属Vector流水,按调用顺序执行;将adds结果从UB搬出到GM属于MTE3搬出流水。由于Vector流水与MTE3搬出流水异步并行执行,MTE3搬出操作可能在Vector流水写入adds结果完成前读取UB数据,因此需在搬出前插入同步指令,确保adds结果写入UB后再启动搬出。

图 2 gather & adds算子执行流程

gather & adds算子的详细执行流程如图2所示。基于该流程,按照输入输出、核函数(Kernel)参数、执行配置和VF函数参数的顺序分析gather & adds算子的实现方式。

输入输出分析: gather & adds算子有两个输入input和index,输出为output。其中,input为原始数据,index表示需要从input中读取的数据位置,output保存读取并加1后的结果。

本样例中,input的数据类型为float,shape为[100000];index的数据类型为uint32_t,shape为[8192],取值范围为[0, 100000);output的数据类型与input相同,shape与index相同。算子输入输出format均为ND。

核函数(Kernel)名称和参数: 根据上述输入输出和执行流程,本样例中核函数(Kernel)命名为gather_and_adds_kernel,函数原型如下。

C++
__global__ __vector__ void gather_and_adds_kernel(
    __gm__ float* input, __gm__ uint32_t* index, __gm__ float* output, uint32_t input_total_length,
    uint32_t index_total_length)

当前SIMD与SIMT混合编程仅使用Vector Core(AIV核),核函数(Kernel)可使用__vector__标识只启动AIV核。

其中,input_total_length和index_total_length分别表示input和index的长度。gather阶段需要根据index访问input,input_total_length用于校验index中保存的input访问位置是否越界,index_total_length用于校验线程访问index的位置是否越界。

执行配置: 明确SIMT线程配置和分核策略。

本例中每个SIMT线程处理1个数据,单核线程数设置为1024,即单核处理1024个数据。算子输入index长度为8192,核数为8192 / 1024 = 8,单个核调用1次simt_gather函数即可完成本核的gather运算。

SIMT VF函数名称和参数: 根据SIMT线程配置策略,本样例中SIMT VF函数命名为simt_gather,参数说明如下。

参数参数类型说明
input__gm__ float*输入input的GM地址
index__gm__ uint32_t*输入index的GM地址
local_output__ubuf__ float*SIMT gather阶段的输出地址
input_total_lengthuint32_tinput的数据长度
index_total_lengthuint32_tindex的数据长度
output_total_lengthuint32_t单核上local_output的数据长度

SIMD VF函数名称和参数: 本样例中SIMD VF函数命名为simd_adds,参数说明如下。

参数参数类型说明
output__ubuf__ float*SIMD adds阶段的输出地址
input__ubuf__ float*SIMD adds阶段的输入地址
countuint32_t单核处理的数据总量
one_repeat_sizeuint32_t每轮处理的float元素个数
repeat_timesuint16_t完成count个元素所需的循环次数

通过以上分析,得到Ascend C gather & adds算子的设计规格如下:

表 1 gather & adds算子设计规格

算子类型(OpType)Gather_Adds
算子输入nameshapedata typeformat
input100000floatND
index8192uint32_tND
算子输出output8192floatND
核函数(Kernel)名gather_and_adds_kernel
SIMT VF函数名simt_gather
SIMD VF函数名simd_adds

核函数(Kernel)定义与实现

根据核函数(Kernel)中介绍的规则进行核函数(Kernel)的定义。本样例中,定义gather_and_adds_kernel核函数(Kernel),代码如下:

C++
__global__ __vector__ void gather_and_adds_kernel(
    __gm__ float* input, __gm__ uint32_t* index, __gm__ float* output, uint32_t input_total_length,
    uint32_t index_total_length)
{
    ...
    // 1. 调用SIMT函数获取指定索引的数据
    asc_vf_call<simt_gather>(
        dim3(THREAD_COUNT), input, index, local_output, input_total_length,
        index_total_length, index_total_length_per_block);

    ...
    // 2. 调用SIMD函数完成加1操作
    asc_vf_call<simd_adds>(
        local_output, local_output, index_total_length_per_block, one_repeat_size, repeat_times);

    // 依赖PIPE_V和PIPE_MTE3流水同步
    asc_sync_notify(PIPE_V, PIPE_MTE3, EVENT_ID0);
    asc_sync_wait(PIPE_V, PIPE_MTE3, EVENT_ID0);

    // 3. 将数据从UB搬运到GM
    asc_copy_ub2gm_align(output + index_total_length_per_block * block_idx, local_output, 1,
        index_total_length_per_block * sizeof(float), asc_store_l2_cache_mode::NORMAL_FIRST_VICTIM, 0, 0);
}

核函数(Kernel)中先后启动SIMT VF函数simt_gather和SIMD VF函数simd_adds:simt_gather将GM中的离散数据读取到UB,simd_adds对UB上的gather结果执行加1计算,并将结果原地写回UB。

对于基于SIMD编程计算并写入UB的结果local_output,调用asc_copy_ub2gm_align接口搬出到GM前,需要通过asc_sync_notify/asc_sync_wait接口控制Vector计算流水和MTE3搬出流水的同步,确保adds计算结果写入UB后再启动搬出。

SIMT VF实现

根据上述对SIMT VF函数的参数分析,定义SIMT VF函数原型,代码如下:

C++
constexpr uint32_t THREAD_COUNT = 1024;

__simt_vf__ __launch_bounds__(THREAD_COUNT) inline void simt_gather(
    __gm__ float* input, __gm__ uint32_t* index, __ubuf__ float* local_output, uint32_t input_total_length,
    uint32_t index_total_length, uint32_t output_total_length)
{
    ...
    // 根据当前核号和线程号计算全局index数组中的元素位置
    uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;
    ...
    // 读取线程应获取的数据在输入input中的索引
    uint32_t gather_idx = index[idx];
    ...
    // 将input中索引为gather_idx的数据存到UB上
    local_output[threadIdx.x] = input[gather_idx];
}

说明

在SIMT编程中,__launch_bounds__(thread_num)是可选配置,用于在编译期指定核函数(Kernel)启动的最大线程数(如果不配置,thread_num默认为1024),使用时请注意:thread_num >= x * y * z (即:asc_vf_call的第一个参数:dim3{x, y, z}), 线程数thread_num的取值范围为1到2048。最大线程数决定了每个线程可分配的寄存器数量,具体对应关系请见表5,寄存器用于存储线程中的局部变量,若局部变量的个数超出寄存器个数,容易出现栈溢出等问题。

使用__simt_vf__函数类型限定符标识SIMT VF函数入口,使其可以被asc_vf_call调用。

simt_gather函数实现从输入input中获取指定索引的数据。函数中首先计算线程应处理数据的索引,然后通过赋值操作将数据存储到UB上。

本例中核数设置为8,线程的层次结构为{1024, 1, 1},数据总量为8192(8 * 1024)。每个线程处理一个index元素,全局index位置计算逻辑如下:

C++
uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;

其中,blockIdx用于获取当前核id。blockDim用于获取线程三维层次结构{x, y, z},本例中为{1024, 1, 1},其中第2,3维度均为1,使用一维层次结构,因此线程数可写作blockDim.x。threadIdx用于获取三维线程索引{x, y, z},本例中仅使用第1维x,可通过threadIdx.x获取当前线程的id。

SIMD VF实现

根据上述对SIMD VF函数的参数分析,定义SIMD VF函数原型,代码如下:

C++
constexpr float ADDS_ADDEND = 1.0f;

__simd_vf__ inline void simd_adds(
    __ubuf__ float* output, __ubuf__ float* input, uint32_t count, uint32_t one_repeat_size, uint16_t repeat_times)
{
    // 初始化SIMD矢量数据寄存器,源操作数
    vector_float src_reg0;
    // 初始化SIMD矢量数据寄存器,目的操作数
    vector_float dst_reg0;
    // 初始化SIMD矢量计算标记寄存器
    vector_bool mask_reg;

    for (uint16_t i = 0; i < repeat_times; i++) {
        mask_reg = asc_update_mask_b32(count);
        // 从UB搬运数据到SIMD矢量数据寄存器
        asc_loadalign(src_reg0, input + i * one_repeat_size);
        // 调用asc_add_scalar接口完成加1运算
        asc_add_scalar(dst_reg0, src_reg0, ADDS_ADDEND, mask_reg);
        // 从SIMD矢量数据寄存器搬运数据到UB
        asc_storealign(output + i * one_repeat_size, dst_reg0, mask_reg);
    }
}

使用__simd_vf__函数类型限定符标识SIMD VF函数入口,使其可以被asc_vf_call调用。

simd_adds函数实现对UB上的数据做加1计算。函数中循环repeat_times次完成单核数据处理。input为SIMT阶段写入UB的gather结果,output为加1后的UB输出;本样例调用时input和output均传入local_output。one_repeat_size表示每轮处理的float元素个数,repeat_times表示完成count个元素所需的循环次数。

循环中,使用asc_loadalign接口将数据从UB搬运到SIMD矢量数据寄存器,使用asc_add_scalar接口完成数据加1运算,使用asc_storealign接口将数据从SIMD矢量数据寄存器搬运到UB。

免责声明:本站内容由 asc-devkit 仓 master 分支自动编译生成,属于持续开发版本,可能存在缺陷,仅供预览与参考。如需稳定及商用资料,请查阅官方 昇腾社区