cuda - 简单的 CUDA 内核优化

标签 cuda gpu

在加速应用程序的过程中,我有一个非常简单的内核,它执行如下所示的类型转换:

__global__ void UChar2FloatKernel(float *out, unsigned char *in, int nElem){
    unsigned int i = (blockIdx.x * blockDim.x) + threadIdx.x;
    if(i<nElem)
        out[i] = (float) in[i];
}

全局内存访问是合并的,据我所知,使用共享内存也没有好处,因为不会多次读取同一内存。是否有人知道是否可以执行任何优化来加速该内核。输入和输出数据已经在设备上,因此不需要主机到设备的内存复制。

最佳答案

您可以对此类代码执行的最大优化是使用驻留线程并增加每个线程执行的事务数。虽然 CUDA block 调度模型非常轻量级,但它并不是免费的,启动大量包含仅执行单个内存加载和单个内存存储的线程的 block 将产生大量 block 调度开销。因此,只启动尽可能多的 block ,以“填充”您 GPU 的所有 SM,并让每个线程做更多的工作。

第二个明显的优化是为加载切换到 128 字节内存事务,这应该会给您带来切实的带宽利用率 yield 。在 Fermi 或 Kepler GPU 上,这不会像在第一代和第二代硬件上那样提供那么大的性能提升。

将其全部放入一个简单的基准测试中:

__global__ 
void UChar2FloatKernel(float *out, unsigned char *in, int nElem)
{
    unsigned int i = (blockIdx.x * blockDim.x) + threadIdx.x;
    if(i<nElem)
        out[i] = (float) in[i];
}

__global__
void UChar2FloatKernel2(float  *out, 
                const unsigned char *in, 
            int nElem)
{
    unsigned int i = (blockIdx.x * blockDim.x) + threadIdx.x;    
    for(; i<nElem; i+=gridDim.x*blockDim.x) {
        out[i] = (float) in[i];
    }
}

__global__
void UChar2FloatKernel3(float4  *out, 
                const uchar4 *in, 
            int nElem)
{
    unsigned int i = (blockIdx.x * blockDim.x) + threadIdx.x;    
    for(; i<nElem; i+=gridDim.x*blockDim.x) {
        uchar4 ival = in[i]; // 32 bit load
        float4 oval = make_float4(ival.x, ival.y, ival.z, ival.w);
        out[i] = oval; // 128 bit store
    }
}

int main(void)
{

    const int n = 2 << 20;
    unsigned char *a = new unsigned char[n];

    for(int i=0; i<n; i++) {
        a[i] = i%255;
    }

    unsigned char *a_;
    cudaMalloc((void **)&a_, sizeof(unsigned char) * size_t(n));
    float *b_;
    cudaMalloc((void **)&b_, sizeof(float) * size_t(n));
    cudaMemset(b_, 0, sizeof(float) * size_t(n)); // warmup

    for(int i=0; i<5; i++)
    {
        dim3 blocksize(512);
        dim3 griddize(n/512);
        UChar2FloatKernel<<<griddize, blocksize>>>(b_, a_, n);
    }

    for(int i=0; i<5; i++)
    {
        dim3 blocksize(512);
        dim3 griddize(8); // 4 blocks per SM
        UChar2FloatKernel2<<<griddize, blocksize>>>(b_, a_, n);
    }

    for(int i=0; i<5; i++)
    {
        dim3 blocksize(512);
        dim3 griddize(8); // 4 blocks per SM
        UChar2FloatKernel3<<<griddize, blocksize>>>((float4*)b_, (uchar4*)a_, n/4);
    }
    cudaDeviceReset();
    return 0;
}  

在小型费米设备上给我这个:

>nvcc -m32 -Xptxas="-v" -arch=sm_21 cast.cu
cast.cu
tmpxft_000014c4_00000000-5_cast.cudafe1.gpu
tmpxft_000014c4_00000000-10_cast.cudafe2.gpu
cast.cu
ptxas : info : 0 bytes gmem
ptxas : info : Compiling entry function '_Z18UChar2FloatKernel2PfPKhi' for 'sm_2
1'
ptxas : info : Function properties for _Z18UChar2FloatKernel2PfPKhi
    0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
ptxas : info : Used 5 registers, 44 bytes cmem[0]
ptxas : info : Compiling entry function '_Z18UChar2FloatKernel3P6float4PK6uchar4
i' for 'sm_21'
ptxas : info : Function properties for _Z18UChar2FloatKernel3P6float4PK6uchar4i
    0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
ptxas : info : Used 8 registers, 44 bytes cmem[0]
ptxas : info : Compiling entry function '_Z17UChar2FloatKernelPfPhi' for 'sm_21'

ptxas : info : Function properties for _Z17UChar2FloatKernelPfPhi
    0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
ptxas : info : Used 3 registers, 44 bytes cmem[0]
tmpxft_000014c4_00000000-5_cast.cudafe1.cpp
tmpxft_000014c4_00000000-15_cast.ii

>nvprof a.exe
======== NVPROF is profiling a.exe...
======== Command: a.exe
======== Profiling result:
 Time(%)      Time   Calls       Avg       Min       Max  Name
   40.20    6.61ms       5    1.32ms    1.32ms    1.32ms  UChar2FloatKernel(float*, unsigned char*, int)
   29.43    4.84ms       5  968.32us  966.53us  969.46us  UChar2FloatKernel2(float*, unsigned char const *, int)
   26.35    4.33ms       5  867.00us  866.26us  868.10us  UChar2FloatKernel3(float4*, uchar4 const *, int)
    4.02  661.34us       1  661.34us  661.34us  661.34us  [CUDA memset]

在后两个内核中,与 4096 个 block 相比,仅使用 8 个 block 提供了很大的速度提升,这证实了每个线程多个工作项是提高这种内存限制、低指令数的性能的最佳方法的想法内核。

关于cuda - 简单的 CUDA 内核优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21332040/

相关文章:

cuda - 机器上的多个 CUDA 版本 nvcc -V 混淆

cuda - 如何测量 CUDA 中内核启动的开销

cuda - 我可以在编译时通过 #define 获得 CUDA 计算能力(版本)吗?

python - 使用 MirroredStrategy : isinstance(x, dataset_ops.DatasetV2 时出现 AssertionError)

cpu - 为什么不使用 GPU 作为 CPU?

CUDA:什么是散写?

c++ - 使用内核参数在 CUDA 内核中声明数组

macos - Metal 中的线程和线程组

c++ - 如何在 MacOS 上的 C++ 计算中使用双 AMD FirePro D300 的 GPU?

gpu - 如何使用 Theano 启用 Keras 以利用多个 GPU