您的下一代自定义FFT内核¶
在实际应用场景中,我们通常需要不止一个内核。为了在多种架构上实现最佳性能,单个用例可能需要多种不同的实现方案。cuFFTDx的设计初衷就是自动处理这种复杂性,同时让用户能完全掌控实现细节。
使用最优参数¶
cuFFTDx允许用户将实现中的某些细节定义(例如每个线程计算的FFT元素数量或每个块的FFT数量)交由库来决定。ElementsPerThread的默认值是建议值,它基于一个启发式算法,该算法取决于FFT本身(大小、精度、GPU架构等)。然而,对于选定的ElementsPerThread(无论是默认值还是手动设置的值),可以通过FFT::suggested_ffts_per_block静态字段获取建议的FFTsPerBlock。FFTsPerBlock的默认值为1。
#include <cufftdx.hpp>
using namespace cufftdx;
template<class FFT>
__global__ void block_fft_kernel(FFT::value_type* data, typename FFT::workspace_type workspace) {
using complex_type = typename FFT::value_type;
// Registers
complex_type thread_data[FFT::storage_size];
// Local batch id of this FFT in CUDA block, in range [0; FFT::ffts_per_block)
const unsigned int local_fft_id = threadIdx.y;
// Global batch id of this FFT in CUDA grid is equal to number of batches per CUDA block (ffts_per_block)
// times CUDA block id, plus local batch id.
const unsigned int global_fft_id = (blockIdx.x * FFT::ffts_per_block) + local_fft_id;
// Load data from global memory to registers
const unsigned int offset = cufftdx::size_of<FFT>::value * global_fft_id;
const unsigned int stride = FFT::stride;
unsigned int index = offset + threadIdx.x;
for (unsigned int i = 0; i < FFT::elements_per_thread; i++) {
// Make sure not to go out-of-bounds
if ((i * stride + threadIdx.x) < cufftdx::size_of<FFT>::value) {
thread_data[i] = data[index];
index += stride;
}
}
// FFT::shared_memory_size bytes of shared memory
extern __shared__ __align__(alignof(float4)) complex_type shared_mem[];
// Execute FFT
FFT().execute(thread_data, shared_mem, workspace);
// Save results
index = offset + threadIdx.x;
for (unsigned int i = 0; i < FFT::elements_per_thread; i++) {
if ((i * stride + threadIdx.x) < cufftdx::size_of<FFT>::value) {
data[index] = thread_data[i];
index += stride;
}
}
}
void introduction_example(cudaStream_t stream = 0) {
// Base of the FFT description
using FFT_base = decltype(Size<128>() + Precision<float>() + Type<fft_type::c2c>()
+ Direction<fft_direction::forward>()
/* Notice lack of ElementsPerThread and FFTsPerBlock operators */
+ SM<700>() + Block());
// FFT description with suggested FFTs per CUDA block for the default (optimal) elements per thread
using FFT = decltype(FFT_base() + FFTsPerBlock<FFT_base::suggested_ffts_per_block>());
// Allocate managed memory for input/output
complex_type* data;
auto size = FFT::ffts_per_block * cufftdx::size_of<FFT>::value;
auto size_bytes = size * sizeof(complex_type);
cudaMallocManaged(&data, size_bytes);
// Generate data
for (size_t i = 0; i < size; i++) {
data[i] = complex_type {float(i), -float(i)};
}
cudaError_t error_code = cudaSuccess;
auto workspace = make_workspace<FFT>(error_code, stream);
// Invokes kernel with FFT::block_dim threads in CUDA block
block_fft_kernel<FFT><<<1, FFT::block_dim, FFT::shared_memory_size, stream>>>(data, workspace);
cudaDeviceSynchronize();
cudaFree(data);
}
要获取最优的FFTsPerBlock值,我们需要一个完整的执行描述符(如cufftdx::is_complete_fft_execution所示)。这是因为某些细节只有在FFT操作被完整描述、且目标架构和执行模式选定后才会确定。SM Operator在主机端编译时,允许用户查询特定架构的启动参数。
幕后原理¶
- Expression templates
cuFFTDx API采用了一种称为表达式模板的C++技术变体。我们利用表达式模板让用户能够构建描述待计算FFT的编译时对象。通过编译时C++机制,cuFFTDx可以将优化的FFT例程附加到该对象上,并将其作为用户可调用的计算方法公开。
- Header only
cuFFTDx FFT例程以优化的内联PTX形式提供。
为什么?¶
要使一个库真正实用,它需要以面向未来的方式抽象功能。所谓面向未来,是指现有用户代码在未来不应需要修改,而新功能应作为现有代码的简单扩展来实现。在CUDA平台上,这要求适应快速演进的GPU硬件。
cuFFTDx通过两种方式实现面向未来的设计。一方面,该API作为源代码级抽象层,使库与ABI变更解耦。结合头文件中的PTX代码,cuFFTDx可向前兼容任何支持其目标硬件的CUDA工具包、驱动程序和编译器。CUDA编译器可重新编译PTX代码以在未来GPU架构上运行。
另一方面,API的组织方式可以保留描述计算内容和计算方式的运算符。如果代码将实现选择委托给库,那么依赖于类型的新特性可以自动获取;否则,就需要向现有表达式添加运算符。