Hybrid programming of SIMD and SIMT on the AI Core combines the parallel compute capability of SIMD with the discrete memory access advantage of SIMT, achieving efficient collaboration between vector-level parallelism and thread-level parallelism. Currently, this technology is supported only by Atlas 350 Accelerator Card.
The entire execution process uses the Vector Function (VF) as the basic scheduling unit. A VF is a basic function block. SIMD and SIMT programming supports flexible switching between SIMD and SIMT execution modes within the same operator. Two different types of VFs can be quickly switched. Each VF represents an independent compute task segment, which usually corresponds to a segment of logic that can be processed in parallel within the operator. This achieves a better balance between performance, energy efficiency, and development efficiency. In SIMD and SIMT programming:
In SIMD and SIMT programming, SIMT simplifies the development of complex operators and irregular control flows. SIMD, on the other hand, implements efficient data parallel processing based on vector registers and instructions, allowing a single instruction to process multiple pieces of data and improving the throughput per cycle. SIMD and SIMT programming allows you to perform refined mapping based on operator characteristics. Regular element-wise operations achieve high bandwidth and compute power utilization through SIMD, while irregular or branch-containing computations are handled by SIMT to mitigate divergence and control complexity. At the system level, this helps improve hardware utilization and energy efficiency. It also facilitates optimizations such as operator fusion and data reuse. An operator can contain both continuous regular computations that are well-suited to SIMD and discrete access tasks that are better handled by SIMT, thereby leveraging the advantages of both SIMD and SIMT within the same operator.
As shown in Figure 1, the internal execution processes of SIMD and SIMT are as follows:
The following table describes the differences between SIMD and SIMT programming.
|
Dimension |
SIMD |
SIMT |
|---|---|---|
|
Programming model |
Single instruction, multiple data (SIMD), which is based on vector registers and vector instructions. |
Single instruction, multiple threads (SIMT), which is executed in parallel by thread. |
|
Data movement mode |
Data is explicitly moved from the Unified Buffer to the vector register through Load/Store. Data cannot be directly moved from the Global Memory to the SIMD vector register. |
Data in the Global Memory or Unified Buffer can be directly read and written. |
|
Application scenario |
Regular and continuous element-wise operations, such as convolution, matrix multiplication, and vector operations. |
Complex logic like irregular, branch-containing, and dynamic access logic, such as attention mechanism and sparse operations |
Although SIMD and SIMT differ significantly in their programming models and execution mechanisms, they share the following key resources at the hardware level:
In the SIMD and SIMT programming scenario, multiple types of memory spaces can be accessed. The following table summarizes the scopes and lifecycles of common memory types.
|
Memory Type |
Scope |
Lifecycle |
Physical Location |
Characteristic |
|---|---|---|---|---|
|
Global memory |
All kernel functions |
Application |
Device |
Large capacity and low bandwidth |
|
Shared memory |
Single-core kernel function |
Kernel function |
Vector Core |
Small capacity and high bandwidth |
|
SIMT register |
Thread |
SIMT VF function |
Device |
Extremely small capacity and extremely high bandwidth |
|
SIMD register |
Register file |
SIMD VF function |
Vector Core |
Extremely small capacity and extremely high bandwidth |
The following figure shows the overall memory architecture.
In SIMT mode, the working process of each memory is as follows:
In SIMD mode, the working process of each memory is as follows:
The total memory space of the UB is 256 KB. As shown in Figure 3, the UB is divided into four main areas based on functions: static memory, dynamic memory, reserved space, and data cache, in ascending order of addresses. The structure is as follows:
1 2 |
// The static memory is allocated through arrays. Example: __ubuf__ char staticBuf[1024]; |
// The dynamic memory is allocated through dynamic arrays. Example: extern __ubuf__ char dynamicBuf[];
All the preceding three methods allocate memory starting from the end position of the static memory. Address space overlapping will occur if these methods are used at the same time, causing undefined behavior. Therefore, you can use only one of the methods to allocate the dynamic memory.
The method of allocating the dynamic memory through dynamic arrays is under development and will be supported in later versions.
When defining a SIMT VF kernel function, __launch_bounds__(thread_num) is optional and is used to specify the maximum number of threads to be started by the kernel function during compilation. If thread_num is not configured, the default value 1024 is used.
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); |
The input parameters of SIMD_VF and SIMT_VF support only the Plain Old Data (PoD) data type.
1
|
__global__ __aicore__ void my_kernel(__gm__ float*,...) |
1
|
kernel_name<<<numBlocks, dynUBufSize, stream>>>(args...) |
The following figure shows the supported call relationship.
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); ... } |