#include "TopKV2Execution.hpp" #include namespace MNN { namespace CUDA { // Sift Down template __device__ inline void siftDown(const int K, const int descendFlag, valueT * valuesThread, indexT * indicesThread) { int parent = 0; while (true) { int child = 2 * parent + 1; if (child >= K) break; if (child + 1 < K && (valueT)(descendFlag) * valuesThread[child + 1] < (valueT)(descendFlag) * valuesThread[child]) { child++; } if ((valueT)(descendFlag) * valuesThread[parent] > (valueT)(descendFlag) * valuesThread[child]) { valueT tmpV = valuesThread[parent]; valuesThread[parent] = valuesThread[child]; valuesThread[child] = tmpV; indexT tmpI = indicesThread[parent]; indicesThread[parent] = indicesThread[child]; indicesThread[child] = tmpI; parent = child; } else { break; } } } // rank TopK in the corresponding thead template __device__ void TopKInThread(const valueT * inputDevice, indexT * indicesThread, valueT * valuesThread, const int K, const int numElePerRow, const valueT minValue, const int descendFlag) { for (int i = 0 ; i < K; i++) { indicesThread[i] = -1; valuesThread[i] = (valueT)(descendFlag) * minValue; } int idxFirstEleInRow = threadIdx.x + blockIdx.x * blockDim.x; // Check if we can use vectorized load (address alignment and size) // We assume inputDevice is aligned to at least 4 bytes. // Note: Since we use grid-stride loop, the data accessed by a single thread is NOT contiguous. // So we cannot achieve true vectorized load (LD.E.128) which requires contiguous memory. // However, manual unrolling helps Instruction-Level Parallelism (ILP) and Latency Hiding. // Main loop with unrolling (Process 4 elements per step if possible) indexT i = idxFirstEleInRow; /* Note: The data[0..3] are separated by gridDim.x * blockDim.x, so they are not contiguous. We unroll the loop to issue multiple independent memory loads, allowing the GPU to hide memory latency and improve ILP. */ for (; i + gridDim.x * blockDim.x * 3 < numElePerRow; i += gridDim.x * blockDim.x * 4) { valueT data[4]; data[0] = inputDevice[i]; data[1] = inputDevice[i + gridDim.x * blockDim.x]; data[2] = inputDevice[i + gridDim.x * blockDim.x * 2]; data[3] = inputDevice[i + gridDim.x * blockDim.x * 3]; #pragma unroll for (int k = 0; k < 4; ++k) { valueT val = data[k]; // Compare with the root of the heap if ((valueT)(descendFlag) * val > (valueT)(descendFlag) * valuesThread[0]) { valuesThread[0] = val; indicesThread[0] = i + gridDim.x * blockDim.x * k; siftDown(K, descendFlag, valuesThread, indicesThread); } } } // Handle remaining elements for (; i < numElePerRow; i += gridDim.x * blockDim.x) { valueT data = inputDevice[i]; // Compare with the root of the heap (which is the minimum of the current Top K) // If data is larger than the minimum, it might be in the Top K. if ((valueT)(descendFlag) * data > (valueT)(descendFlag) * valuesThread[0]) { // Replace root valuesThread[0] = data; indicesThread[0] = i; siftDown(K, descendFlag, valuesThread, indicesThread); } } // Sort the Heap to produce a sorted array (Descending) // We pop elements from the Min-Heap one by one. // The popped element is the minimum of the remaining heap. // We place it at the end of the array (filling from K-1 down to 0). // This results in a descending sorted array: [Max, ..., Min] for (int i = K - 1; i > 0; i--) { // Move current root (min) to the end (i) valueT tmpV = valuesThread[0]; valuesThread[0] = valuesThread[i]; valuesThread[i] = tmpV; indexT tmpI = indicesThread[0]; indicesThread[0] = indicesThread[i]; indicesThread[i] = tmpI; // Restore heap property for the remaining [0, i) elements siftDown(i, descendFlag, valuesThread, indicesThread); } return; } // reduce TopK results of two offsets template __device__ void ReduceTopK(indexT * indicesArray, valueT * valuesArray, const int offset1, const int offset2, const int K, const int descendFlag) { indexT idx1 = offset1 + K - 1; indexT idx2 = offset2 + K - 1; indexT idxVirtual = offset1 + 2 * K -1; while (idx2 >= offset2) { if (idx1 < offset1) { while (idxVirtual >= offset1) { indicesArray[idxVirtual] = indicesArray[offset2 + (idxVirtual - offset1)]; valuesArray[idxVirtual] = valuesArray[offset2 + (idxVirtual - offset1)]; idxVirtual --; } break; } if ((valueT)(descendFlag) * valuesArray[idx1] <= (valueT)(descendFlag) * valuesArray[idx2]) { if (idxVirtual <= offset1 + K - 1) { indicesArray[idxVirtual] = indicesArray[idx1]; valuesArray[idxVirtual] = valuesArray[idx1]; } idx1 --; } else { if (idxVirtual <= offset1 + K - 1) { indicesArray[idxVirtual] = indicesArray[idx2]; valuesArray[idxVirtual] = valuesArray[idx2]; } idx2 --; } idxVirtual --; } return; } // get results of all blocks' TopK in one row template __device__ void TopKOneRow(const valueT * inputDevice, indexT * indicesBlock, valueT * valuesBlock, indexT * tempIndicesDevice, valueT * tempValuesDevice, const int K, const int lengthRow, valueT minValue, const int descendFlag) { indexT * indicesThread = indicesBlock + threadIdx.x * K; valueT * valuesThread = valuesBlock + threadIdx.x * K; // rank TopK TopKInThread(inputDevice, indicesThread, valuesThread, K, lengthRow, minValue, descendFlag); __syncthreads(); // reduce for(int stride = (blockDim.x >> 1); stride > 0; stride >>= 1) { if(threadIdx.x < stride) { ReduceTopK(indicesBlock, valuesBlock, threadIdx.x * K, (threadIdx.x + stride) * K, K, descendFlag); } __syncthreads(); } // move data from block's smem to global memory(prepare for the next kernel function) if (threadIdx.x == 0) { for(int i = 0; i < K; i++) { tempIndicesDevice[K * blockIdx.x + i] = indicesBlock[i]; tempValuesDevice[K * blockIdx.x + i] = valuesBlock[i]; } } return; } // get results of the final TopK from all block's TopK in a row template __device__ void GetResultOneRow(indexT * outputIndicesDevice, valueT * outputValuesDevice, indexT * tempIndicesDevice, valueT * tempValuesDevice, indexT * finalIndices, valueT * finalValues, const int K, const int reduceLength, const int descendFlag) { // move data from global memory to a block's smem if (threadIdx.x < reduceLength) { for (int i = 0; i < K; i++) { finalIndices[threadIdx.x * K + i] = tempIndicesDevice[threadIdx.x * K + i]; finalValues[threadIdx.x * K + i] = tempValuesDevice[threadIdx.x * K + i]; } } __syncthreads(); // the first round of reducing needs special action int stride = blockDim.x >> 1; if ((threadIdx.x < stride) && (threadIdx.x + stride < reduceLength)) { ReduceTopK(finalIndices, finalValues, threadIdx.x * K, (threadIdx.x + stride) * K, K, descendFlag); } __syncthreads(); stride >>= 1; // the remaining rounds of reducing for (; stride > 0; stride >>= 1) { if (threadIdx.x < stride) { ReduceTopK(finalIndices, finalValues, threadIdx.x * K, (threadIdx.x + stride) * K, K, descendFlag); } __syncthreads(); } //move data from a block's smem to global memory if (threadIdx.x == 0) { for (int i = 0; i < K; i++) { outputIndicesDevice[i] = finalIndices[i]; outputValuesDevice[i] = finalValues[i]; } } return; } // allocate addresses for each row and call template __global__ void TopKAllRows(const valueT * inputDevice, indexT * tempIndicesDevice, valueT * tempValuesDevice, const int K, const int lengthRow, valueT minValue, const int descendFlag) { extern __shared__ char smem[]; indexT * indicesBlock = reinterpret_cast(smem); valueT * valuesBlock = reinterpret_cast(&smem[blockDim.x * K * sizeof(indexT)]); int idxRow = blockIdx.y; const valueT * inputDeviceThisRow = inputDevice + idxRow * lengthRow; indexT * tempIndicesDeviceThisRow = tempIndicesDevice + idxRow * gridDim.x * K; valueT * tempValuesDeviceThisRow = tempValuesDevice + idxRow * gridDim.x * K; TopKOneRow(inputDeviceThisRow, indicesBlock, valuesBlock, tempIndicesDeviceThisRow, tempValuesDeviceThisRow, K, lengthRow, minValue, descendFlag); __syncthreads(); return; } // allocate addresses for each row and call // This kernel assumes that each row of data corresponds to one block. template __global__ void GetResultAllRows(indexT * outputIndicesDevice, valueT * outputValuesDevice, indexT * tempIndicesDevice, valueT * tempValuesDevice, const int K, const int numBlockPerRow, const int descendFlag) { extern __shared__ char smem[]; indexT * finalIndices = reinterpret_cast(smem); valueT * finalValues = reinterpret_cast(&smem[numBlockPerRow * K * sizeof(indexT)]); int idxRow = blockIdx.x; // each block corresponds to a row indexT * outputIndicesDeviceThisRow = outputIndicesDevice + idxRow * K; valueT * outputValuesDeviceThisRow = outputValuesDevice + idxRow * K; indexT * tempIndicesDeviceThisRow = tempIndicesDevice + idxRow * numBlockPerRow * K; valueT * tempValuesDeviceThisRow = tempValuesDevice + idxRow * numBlockPerRow * K; GetResultOneRow(outputIndicesDeviceThisRow, outputValuesDeviceThisRow, tempIndicesDeviceThisRow, tempValuesDeviceThisRow, finalIndices, finalValues, K, numBlockPerRow, descendFlag); return; } // The inequality "numThreadPerBlock * K * (sizeof(indexT) + sizeof(valueT)) <= smemPerBlock" must be guaranteed, which means numThreadPerBlock depends on K. template int CalculateNumThreadPerBlock(const int K, const int smemPerBlock) { int temp = smemPerBlock / (K * (sizeof(indexT) + sizeof(valueT))); int numCalculate = std::pow(2, (std::floor(std::log2(temp)))); int numLimit = 1024; return ALIMIN(numLimit, numCalculate); } // The inequality "numBlockPerRow * K * (sizeof(indexT) + sizeof(valueT)) <= smemPerBlock" must be guaranteed by restricting numElePerThread. template int CalcualteNumElePerThread(const int K, const int numElePerRow, const int numThreadPerBlock, const int smemPerBlock) { int numLimit = K; int numCalculate = UP_DIV(numElePerRow, (smemPerBlock / (K * (sizeof(indexT) + sizeof(valueT))))-1); return ALIMAX(numLimit,numCalculate); } TopKV2Execution::TopKV2Execution(const Op* op, Backend* backend) : Execution(backend) { mOp = op; } ErrorCode TopKV2Execution::onResize(const std::vector &inputs, const std::vector &outputs) { // prepare some params for the kernel function Tensor * inputTensor = inputs[0]; int lengthRow = inputTensor->buffer().dim[inputTensor->buffer().dimensions - 1].extent; int numRow = inputTensor->elementSize() / lengthRow; mParams.mLengthRow = lengthRow; mParams.mNumRow = numRow; auto boolDescendFlag = mOp->main_as_TopKV2(); if (boolDescendFlag != nullptr) { mParams.mDescendFlag = boolDescendFlag ? 1 : -1; } mParams.mNumElePerRow = mParams.mLengthRow; mParams.mNumK = outputs[0]->buffer().dim[outputs[0]->buffer().dimensions-1].extent; auto smemLimit = static_cast(backend())->getCUDARuntime()->smemPerBlock(); if (inputTensor->getType().code == halide_type_int && inputTensor->getType().bits == 32) { mParams.mNumThreadPerBlock = CalculateNumThreadPerBlock(mParams.mNumK, smemLimit); mParams.mNumElePerThread = CalcualteNumElePerThread(mParams.mNumK, mParams.mNumElePerRow, mParams.mNumThreadPerBlock, smemLimit); } else if (static_cast(backend())->useFp16()) { mParams.mNumThreadPerBlock = CalculateNumThreadPerBlock(mParams.mNumK, smemLimit); mParams.mNumElePerThread = CalcualteNumElePerThread(mParams.mNumK, mParams.mNumElePerRow, mParams.mNumThreadPerBlock, smemLimit); } else { mParams.mNumThreadPerBlock = CalculateNumThreadPerBlock(mParams.mNumK, smemLimit); mParams.mNumElePerThread = CalcualteNumElePerThread(mParams.mNumK, mParams.mNumElePerRow, mParams.mNumThreadPerBlock, smemLimit); } mParams.mNumElePerBlock = mParams.mNumElePerThread * mParams.mNumThreadPerBlock; mParams.mNumBlockPerRow = (mParams.mNumElePerRow - 1 + mParams.mNumElePerBlock) / mParams.mNumElePerBlock; mParams.mNumBlockFinal = mParams.mNumRow; mParams.mNumThreadFinal = std::pow(2, (std::ceil(std::log2(mParams.mNumBlockPerRow)))); mParams.mNumBlockTotal = mParams.mNumBlockPerRow * mParams.mNumRow; // prepare temp buffer auto pool = static_cast(backend())->getBufferPool(); if (inputTensor->getType().code == halide_type_int && inputTensor->getType().bits == 32) { auto bufferIndices = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(int)); mParams.mBufferIndices = (void*)((uint8_t*)bufferIndices.first + bufferIndices.second); auto bufferValues = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(int)); mParams.mBufferValues = (void*)((uint8_t*)bufferValues.first + bufferValues.second); pool->free(bufferIndices); pool->free(bufferValues); } else if (static_cast(backend())->useFp16()) { auto bufferIndices = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(int)); mParams.mBufferIndices = (void*)((uint8_t*)bufferIndices.first + bufferIndices.second); auto bufferValues = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(half)); mParams.mBufferValues = (void*)((uint8_t*)bufferValues.first + bufferValues.second); pool->free(bufferIndices); pool->free(bufferValues); } else { auto bufferIndices = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(int)); mParams.mBufferIndices = (void*)((uint8_t*)bufferIndices.first + bufferIndices.second); auto bufferValues = pool->alloc(mParams.mNumBlockTotal * mParams.mNumK * sizeof(float)); mParams.mBufferValues = (void*)((uint8_t*)bufferValues.first + bufferValues.second); pool->free(bufferIndices); pool->free(bufferValues); } return NO_ERROR; } ErrorCode TopKV2Execution::onExecute(const std::vector &inputs, const std::vector &outputs) { // get input and output pointers void * inputDeviceAddr = reinterpret_cast(inputs[0]->deviceId()); void * outputIndicesDeviceAddr = reinterpret_cast(outputs[1]->deviceId()); void * outputValuesDeviceAddr = reinterpret_cast(outputs[0]->deviceId()); // configure threads dim3 grid1 = {(unsigned int)mParams.mNumBlockPerRow, (unsigned int)mParams.mNumRow}; dim3 block1 = {(unsigned int)mParams.mNumThreadPerBlock, (unsigned int)1}; int smemSize1 = mParams.mNumThreadPerBlock * mParams.mNumK; dim3 grid2 = {(unsigned int)mParams.mNumBlockFinal}; dim3 block2 = {(unsigned int)mParams.mNumThreadFinal}; int smemSize2 = mParams.mNumBlockPerRow * mParams.mNumK; if (inputs[0]->getType().code == halide_type_int && inputs[0]->getType().bits == 32) { TopKAllRows<<>>(static_cast(inputDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mLengthRow, mParams.mMinInt, mParams.mDescendFlag); checkKernelErrors; GetResultAllRows<<>>(static_cast(outputIndicesDeviceAddr), static_cast(outputValuesDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mNumBlockPerRow, mParams.mDescendFlag); checkKernelErrors; } else if (static_cast(backend())->useFp16()) { TopKAllRows<<>>(static_cast(inputDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mLengthRow, mParams.mMinHalf, mParams.mDescendFlag); checkKernelErrors; GetResultAllRows<<>>(static_cast(outputIndicesDeviceAddr), static_cast(outputValuesDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mNumBlockPerRow, mParams.mDescendFlag); checkKernelErrors; } else { TopKAllRows<<>>(static_cast(inputDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mLengthRow, mParams.mMinFloat, mParams.mDescendFlag); checkKernelErrors; GetResultAllRows<<>>(static_cast(outputIndicesDeviceAddr), static_cast(outputValuesDeviceAddr), static_cast(mParams.mBufferIndices), static_cast(mParams.mBufferValues), mParams.mNumK, mParams.mNumBlockPerRow, mParams.mDescendFlag); checkKernelErrors; } return NO_ERROR; } class TopKV2Creator : public CUDABackend::Creator { public: virtual Execution* onCreate(const std::vector& inputs, const std::vector& outputs, const MNN::Op* op, Backend* backend) const override { return new TopKV2Execution(op, backend); } }; static CUDACreatorRegister __init(OpType_TopKV2); } }