AI Core上SIMD(Single Instruction Multiple Data,单指令多数据)与SIMT(Single Instruction Multiple Thread,单指令多线程)混合编程,结合了SIMD多数据并行计算能力与SIMT离散访存的优势,实现了向量级并行与线程级并行的高效协同。目前,该技术仅支持Atlas 350 加速卡。
整个执行过程以Vector Function(VF)为基本调度单位,VF为一个基本函数块。SIMD与SIMT混合编程支持在同一算子中灵活切换SIMD与SIMT执行方式,两种不同类型的VF可以快速切换,每个VF代表一个独立的计算任务片段,通常对应算子中的一段可并行处理的逻辑,从而在性能、能效与开发效率之间取得更优平衡。在SIMD与SIMT混合编程中:
在SIMD与SIMT混合编程中,SIMT能够简化复杂算子与不规则控制流的开发;而SIMD基于向量寄存器与指令,实现高效的数据并行处理,即单指令处理多数据,提升每周期的吞吐量。SIMD与SIMT混合编程支持开发者根据算子特征进行精细化映射:规则的逐元素elementwise操作通过SIMD获得高带宽和高算力利用率,不规则或包含分支的计算通过SIMT来缓解发散和控制复杂度。在系统层面,这有利于提高硬件利用率和能效;同时,也更便于进行算子融合和数据复用等优化。同一个算子中既包含SIMD擅长的连续规整计算,也包含SIMT擅长的离散访问等任务,从而在同一算子中同时利用SIMD和SIMT的优势。
如图1所示,SIMD和SIMT的内部执行流程为:
SIMD与SIMT编程存在以下差异:
|
维度 |
SIMD |
SIMT |
|---|---|---|
|
编程模型 |
单指令多数据(SIMD),基于向量寄存器与向量指令。 |
单指令多线程(SIMT),以线程为单位并行执行。 |
|
数据搬运方式 |
通过显式Load/Store将数据从Unified Buffer搬运到向量寄存器。 不支持直接从Global Memory搬运数据到SIMD的向量寄存器。 |
支持直接读写Global Memory或Unified Buffer中的数据。 |
|
适用场景 |
规则、连续的逐元素操作(elementwise),如卷积、矩阵乘法、向量操作等。 |
不规则、含分支、动态访问等复杂逻辑,如注意力机制、稀疏操作等。 |
尽管SIMD与SIMT在编程模型和执行机制上有显著差异,但在硬件层面上共享以下关键资源:
在SIMD与SIMT混合编程场景下,可以访问多种内存空间,下表汇总了常见内存类型的作用域及其生命周期。
|
内存类型 |
作用域 |
生命周期 |
物理位置 |
特点 |
|---|---|---|---|---|
|
全局内存 |
所有核函数 |
应用程序 |
Device |
大容量,低带宽 |
|
共享内存 |
单核核函数 |
核函数 |
Vector Core |
小容量,高带宽 |
|
SIMT寄存器 |
Thread |
SIMT VF函数 |
Device |
极小容量,极高带宽 |
|
SIMD寄存器 |
Register File |
SIMD VF函数 |
Vector Core |
极小容量,极高带宽 |
整体内存架构如下图所示:
在SIMT工作模式下,各个内存的工作流程如下:
在SIMD工作模式下,各个内存的工作流程如下:
UB(即Unified Buffer)内存空间总大小为256KB,参考图3,按功能划分为四个主要区域,从低地址向高地址依次为静态内存、动态内存、 预留空间 、Data Cache,具体结构如下:
1 2 |
// 静态内存通过数组分配,例如: __ubuf__ char staticBuf[1024]; |
// 动态内存通过动态数组分配,例如: extern __ubuf__ char dynamicBuf[];
由于上述三种方法申请动态内存时均从静态内存结束位置之后开始分配,如果同时使用可能会导致地址空间重叠,从而引发未定义行为,因此只能选择其中一种方法进行申请。
动态内存的动态数组分配方式目前开发中,将在后续版本中支持,请关注后续版本。
定义SIMT VF核函数时,__launch_bounds__(thread_num)是可选配置,用于在编译期指定核函数启动的最大线程数,如果不配置thread_num,thread_num默认为1024。
1
|
__simt__vf__ __launch_bounds__(thread_num) inline void simt_vector_function(__ubuf__ float* input, …) |
1
|
__simd_vf__ inline void my_kernel(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z); |
SIMD_VF和SIMT_VF的入参只支持PoD(Plain Old Data)数据类型。
1
|
__global__ __aicore__ void my_kernel(__gm__ float*,…) |
1
|
kernel_name<<<numBlocks, dynUBufSize, stream>>>(args...) |
具体支持的调用关系图如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
__simt_vf__ __launch_bounds__(THREAD_COUNT) inline void simt_gather( __gm__ float* input, __gm__ uint32_t* index, __ubuf__ float* gather_output, uint32_t input_total_length, uint32_t index_total_length, uint32_t output_total_length) { if (threadIdx.x >= output_total_length) { return; } // blockIdx will be supported later. int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= index_total_length) { return; } uint32_t gather_idx = index[idx]; if (gather_idx >= input_total_length) { return; } gather_output[threadIdx.x] = input[gather_idx]; } __simd_vf__ inline void simd_adds(__ubuf__ float* output, __ubuf__ float* input, uint32_t count, uint32_t one_repeat_size, uint16_t repeat_times) { AscendC::Reg::RegTensor<float> src_reg0; AscendC::Reg::RegTensor<float> dst_reg0; AscendC::Reg::MaskReg mask_reg; for (uint16_t i = 0; i < repeat_times; i++) { mask_reg = AscendC::Reg::UpdateMask<float>(count); AscendC::Reg::LoadAlign(src_reg0, input + i * one_repeat_size); AscendC::Reg::Adds(dst_reg0, src_reg0, ADDS_ADDEND, mask_reg); AscendC::Reg::StoreAlign(output + i * one_repeat_size, dst_reg0, mask_reg); } } __global__ __aicore__ 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) { KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY); AscendC::LocalMemAllocator<AscendC::Hardware::UB> ub_allocator; // 1. gather numbers from input. uint32_t index_total_length_per_block = index_total_length / AscendC::GetBlockNum(); AscendC::LocalTensor<float> gather_output = ub_allocator.Alloc<float>(index_total_length_per_block); asc_vf_call<simt_gather>(dim3(THREAD_COUNT),input, index, (__ubuf__ float *)gather_output.GetPhyAddr(), input_total_length, index_total_length, index_total_length_per_block); // 2. use reg compute api to do addition. AscendC::LocalTensor<float> adds_output = ub_allocator.Alloc<float>(index_total_length_per_block); constexpr uint32_t one_repeat_size = AscendC::GetVecLen() / sizeof(float); uint16_t repeat_times = (index_total_length_per_block + one_repeat_size - 1) / one_repeat_size; asc_vf_call<simd_adds>((__ubuf__ float *)adds_output.GetPhyAddr(), (__ubuf__ float *)gather_output.GetPhyAddr(), index_total_length_per_block, one_repeat_size, repeat_times); AscendC::SetFlag<AscendC::HardEvent::V_MTE3>(0); AscendC::WaitFlag<AscendC::HardEvent::V_MTE3>(0); // 3. copy data to global memory. AscendC::GlobalTensor<float> output_global_tensor; output_global_tensor.SetGlobalBuffer(output + index_total_length_per_block * AscendC::GetBlockIdx()); AscendC::DataCopy(output_global_tensor, adds_output, index_total_length_per_block); } int main(int argc, char *argv[]) { … //numBlocks only supports one dimension currently. gather_and_adds_kernel<<<numBlocks, dynUBufSize, stream>>>(input_device, index_device, output_device, input_total_length, index_total_length); … } |