This sample aims to demonstrate the SIMD and SIMT programming model through a simple operator implementation, rather than providing the best practice of the operator functionality.
The following figure shows the process for implementing vector operator kernel functions based on the SIMD and SIMT programming model.
The following uses the gather & adds operator as an example to detail the preceding steps. The gather & adds operator extracts 8,192 pieces of data with specified indexes from a 1D vector with a length of 100,000 and adds 1 to each piece of extracted data. For details about the complete code of the operator described in this sample, see the sample of implementing the gather&adds operator using hybrid programming of SIMD and SIMT.
The operator analysis procedure is as follows:
The i-th piece of data in output of the gather & adds operator is computed as follows:
output[i] = input[index[i]] + 1
Compute logic:
The operation of adding 1 in simd_adds can be quickly implemented in the simt_gather function. This sample aims to demonstrate the SIMD and SIMT programming model through a simple operator implementation, rather than providing the best practice of the operator functionality.
In this sample, the shape of the operator input index is 8192. You can set the number of cores to 8 and the amount of data processed by each core to 1,024.
For SIMT implementation, the number of threads can be set to 1,024, and each thread processes one piece of data. A single core needs to call the simt_gather function only once to complete the gather operation.
For SIMD Reg vector compute implementation, the amount of data processed by a single core is 1,024. The length of data processed by the Reg vector compute API at a time (one_repeat_size) is GetVecLen/sizeof(float), and the number of cyclic call times of the API (repeat_times) is 1024/one_repeat_size.
Based on the preceding analysis, the design specifications of the Ascend C gather & adds operator are as follows.
|
Name |
Shape |
Data Type |
Format |
|---|---|---|---|
|
input (input) |
100000 |
float |
ND |
|
index (input) |
8192 |
uint32_t |
ND |
|
output (output) |
8192 |
float |
ND |
Define kernel functions based on the rules described in Kernel Function.
In this sample, the function name is gather_and_adds_kernel (the kernel function name can be customized). Based on the preceding analysis, the function prototype is defined as follows:
1 2 3 |
__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) { } |
1
|
uint32_t index_total_length_per_block = index_total_length / AscendC::GetBlockNum(); |
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 |
constexpr uint32_t THREAD_COUNT = 1024; __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) { // Set the kernel type to AIV_ONLY. KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY); // Define the UB memory allocation object. AscendC::LocalMemAllocator<AscendC::Hardware::UB> ub_allocator; // Compute the amount of data to be processed by a single core. uint32_t index_total_length_per_block = index_total_length / AscendC::GetBlockNum(); // Allocate UB memory for the output of simt_gather. AscendC::LocalTensor<float> gather_output = ub_allocator.Alloc<float>(index_total_length_per_block); // 1. Call the SIMT function to obtain 1,024 pieces of data with specified indexes. 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. Call the SIMD function to add 1 to the data. ... // 3. Move the data to the GM. ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
__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) { // 1. Call the SIMT function to obtain 1,024 pieces of data with specified indexes. ... // Allocate UB memory for the output of simd_adds. AscendC::LocalTensor<float> adds_output = ub_allocator.Alloc<float>(index_total_length_per_block); // Compute the amount of data processed by the Reg vector compute API per call. constexpr uint32_t one_repeat_size = AscendC::GetVecLen() / sizeof(float); // Compute the number of cyclic call times of the Reg vector compute API. uint16_t repeat_times = (index_total_length_per_block + one_repeat_size - 1) / one_repeat_size; // 2. Call the SIMD function to add 1 to the data. 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); // Depend on the synchronization between the PIPE_V and PIPE_MTE3 pipelines. AscendC::SetFlag<AscendC::HardEvent::V_MTE3>(0); AscendC::WaitFlag<AscendC::HardEvent::V_MTE3>(0); // 3. Move the data to the GM. ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
__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) { // 1. Call the SIMT function to obtain 1,024 pieces of data with specified indexes. ... // 2. Call the SIMD function to add 1 to the data. ... // 3. Move the data to the GM. // Define a GlobalTensor object for data movement. AscendC::GlobalTensor<float> output_global_tensor; // Initialize the GlobalTensor address based on the core offset. output_global_tensor.SetGlobalBuffer(output + index_total_length_per_block * AscendC::GetBlockIdx()); // Call the data movement API to move the data to the GM. AscendC::DataCopy(output_global_tensor, adds_output, index_total_length_per_block); } |
Define the SIMT VF function prototype based on the preceding parameter analysis of the SIMT VF function. Use the __simt_vf__ function type qualifier to identify the entry of the SIMT VF kernel function so that it can be called by asc_vf_call.
In SIMT programming, __launch_bounds__(thread_num) is optional and is used to specify the maximum number of threads that can be started by the kernel function during compilation. If this parameter is not configured, the default value of thread_num is 1024. Ensure that the value of thread_num ≥ x * y * z (that is, the first parameter of asc_vf_call: dim3{x, y, z}). The value of thread_num ranges from 1 to 2,048. The maximum number of threads determines the number of registers that can be allocated to each thread. For details about the mapping, see Table 5. Registers are used to store local variables in threads. If the number of local variables exceeds the number of registers, issues such as stack overflow may occur.
1 2 3 4 5 6 7 8 9 10 11 |
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* gather_output, uint32_t input_total_length, uint32_t index_total_length, uint32_t output_total_length) { } |
The simt_gather function implementation obtains data with specified indexes from input (Global Memory). Based on the preceding data tiling policy, compute the index of the data to be processed by a thread first, and then store the data to the Unified Buffer through assignment operations.
1
|
int idx = blockIdx.x * blockDim.x + threadIdx.x; |
blockIdx is used to obtain the current core ID. blockDim is used to obtain the 3D thread hierarchy {x, y, z}. In this example, the hierarchy is {1024, 1, 1}, where both the second and third dimensions are 1. A 1D hierarchy is used, so the number of threads can be expressed as blockDim.x. threadIdx is used to obtain the 3D thread index {x, y, z}. In this example, only the first dimension x is used. You can obtain the ID of the current thread through threadIdx.x.
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 |
__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) { // Check for exceptions to prevent out-of-bounds access. if (threadIdx.x >= output_total_length) { return; } // Compute the index of the data to be processed by the thread in index. int idx = blockIdx.x * blockDim.x + threadIdx.x; // Check for exceptions to prevent out-of-bounds access. if (idx >= index_total_length) { return; } // Read the index of the data to be obtained by the thread in input. uint32_t gather_idx = index[idx]; // Check for exceptions to prevent out-of-bounds access. if (gather_idx >= input_total_length) { return; } // Store the data whose index is gather_idx in input to the UB. gather_output[threadIdx.x] = input[gather_idx]; } |
1 2 3 4 |
__simd_vf__ inline void simd_adds(__ubuf__ float *output, __ubuf__ float *input, uint32_t count, uint32_t one_repeat_size, uint16_t repeat_times) { } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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) { // Initialize the Reg vector compute unit as the source operand. AscendC::Reg::RegTensor<float> src_reg0; // Initialize the Reg vector compute unit as the destination operand. AscendC::Reg::RegTensor<float> dst_reg0; // Initialize the Reg vector compute mask register. AscendC::Reg::MaskReg mask; for (uint16_t i = 0; i < repeat_times; i++) { // Move the data from the UB to the basic Reg vector compute unit. mask = AscendC::Reg::UpdateMask<float>(count); AscendC::Reg::LoadAlign(src_reg0, input + i * one_repeat_size); // Call Adds to add 1 to the data. AscendC::Reg::Adds(dst_reg0, src_reg0, ADDS_ADDEND, mask_reg); // Move the data from the basic Reg vector compute unit to the UB. AscendC::Reg::StoreAlign(output + i * one_repeat_size, dst_reg0, mask_reg); } } |