为 GPU 优化 opencl 中的内核代码

Optimizing kernel code in opencl for a GPU

截至目前,就内核执行时间而言,我的 GPU 比我的 CPU 慢。我想也许因为我是用一个小样本进行测试,所以 CPU 由于启动开销较小而最终完成得更快。然而,当我用几乎是样本大小 10 倍的数据测试内核时,CPU 仍然完成得更快,而 GPU 落后了将近 400 毫秒。

2.39MB 文件的运行时间 CPU:43.511 毫秒 GPU:65.219 毫秒

32.9MB 文件的运行时间 CPU:289.541 毫秒 GPU:605.400 毫秒

我试过使用本地内存,尽管我 100% 确定我用错了,运行 分为两个问题。内核在 1000-3000 毫秒之间的任何地方完成(取决于我为 localWorkSize 设置的大小)或者我 运行 进入状态代码 -5,即 CL_OUT_OF_RESOURCES.

这是一位 SO 成员帮助我完成的内核。

__kernel void lowpass(__global float *Array, __global float *coefficients, __global float *Output) {

int globalId = get_global_id(0); 
float sum=0.0f;
for (int i=0; i< 65; i++)
{
    float tmp=0;
    if (globalId+i > 63)
    {
        tmp=Array[i+globalId-64]*coefficients[64-i];    

    }

    sum += tmp;

}
Output[globalId]=sum;
}

这是我尝试使用本地内存。第一位将是主机代码的片段,接下来的部分是内核。

//Set the size of localMem
status |= clSetKernelArg(
    kernel,
    2,
    1024, //I had num_items*(float) but it gave me a -5. Num items is the amount of elements in my array (around 1.2 million elements)
    null);
printf("Kernel Arg output status: %i \n", status);

//set a localWorkSize
localWorkSize[0] = 64;

//execute the kernel with localWorkSize included
status = clEnqueueNDRangeKernel(
    cmdQueue,
    kernel,
    1,
    NULL,
    globalWorkSize,
    localWorkSize,
    0,
    NULL,
    &someEvent);


 //Here is what I did to the kernel*************************************** 
__kernel void lowpass(__global float *Array, __global float *coefficients, __global float *Output, __local float *localMem) {

int globalId = get_global_id(0);
int localId = get_local_id(0);  

localMem[localId] = globalId[globalId];

float sum=0.0f;
for (int i=0; i< 65; i++)
{
    float tmp=0;
    if (globalId+i > 63)
    {
        tmp=localMem[i+localId-64]*coefficients[64-i];  

    }

    sum += tmp;

}
Output[globalId]=sum;
}

参考link我在尝试设置局部变量时使用的: How do I use local memory in OpenCL?

Link 用于查找 kernelWorkGroupSize(这就是我在 kernelArg 中设置 1024 的原因): CL_OUT_OF_RESOURCES for 2 millions floats with 1GB VRAM?

我看到其他人也有类似的问题,其中 GPU 比 CPU 慢,但对于他们中的许多人来说,他们使用的是 clEnqueueKernel 而不是 clEnqueueNDRangeKernel。

如果您需要有关此内核的更多信息,请参阅我之前的问题:

还发现了一些针对 GPU 的优化技巧。 https://developer.amd.com/wordpress/media/2012/10/Optimizations-ImageConvolution1.pdf

已编辑代码;错误仍然存​​在

__kernel void lowpass2(__global float *Array, __global float *coefficients, __global float *Output) {

int globalId = get_global_id(0); 
float sum=0.0f;
float tmp=0.0f;
for (int i=64-globalId; i< 65; i++)
{

tmp = 0.0f;
tmp=Array[i]*coefficients[i];    
sum += tmp;

}
Output[globalId]=sum;
}

在介绍本地内存之前,让我们先将 if 语句移出循环:

__kernel void lowpass(__global float *Array, __global float *coefficients, __global float *Output) 
{
int globalId = get_global_id(0); 
float sum=0.0f;
int start = 0;
if(globalId < 64)
    start = 64-globalId;
for (int i=start; i< 65; i++)
    sum += Array[i+globalId-64] * coefficients[64-i];    
Output[globalId]=sum;
}

那么引入本地内存可以这样实现:

__kernel void lowpass(__global float *Array, __global float *coefficients, __global float *Output) 
{
    int globalId = get_global_id(0);
    int local_id = get_local_id(0);

    __local float local_coefficients[65];
    __local float local_array[2*65];

    local_coefficient[local_id] = coefficients[local_id];
    if(local_id == 0)
        local_coefficient[64] = coefficients[64];
    for (int i=0; i< 2*65; i+=get_local_size(0))
    {
        if(i+local_id < 2*65)
            local_array[i+local_id] = Array[i+global_id];
    }
    barrier(CLK_LOCAL_MEM_FENCE);

    float sum=0.0f;
    int start = 0;
    if(globalId < 64)
        start = 64-globalId;
    for (int i=start; i< 65; i++)
        sum += local_array[i+local_id] * local_coefficient[64-i];    
    Output[globalId]=sum;
}

P.S。那里可能存在一些错误,例如全局到本地索引的重新计算等。(我现在要睡觉了:))尽管如此,上面的实现应该让您了解如何开始使用本地内存的正确方向。

运行为 2400 万个元素数组设置以下内核

__kernel void lowpass(__global float *Array, __global float *coefficients, __global float *Output) {

int globalId = get_global_id(0); 
float sum=0.0f;
for (int i=0; i< 65; i++)
{
    float tmp=0;
    if (globalId+i > 63)
    {
        tmp=Array[i+globalId-64]*coefficients[64-i];    

    }

    sum += tmp;

}
Output[globalId]=sum;
}

对于 25 个计算单元的设备池在 200 毫秒内完成,但对于 8 核 cpu 则超过 500 毫秒。

要么你有一个高端 cpu 和一个低端 gpu,要么 gpu 驱动程序被弄脏了,要么 gpu 的 pci-e 接口停留在 pci-e 1.1 @ 4x 带宽,所以阵列之间复制主机和设备有限。

另一方面,这个优化版本:

__kernel void lowpass(__global __read_only float *Array,__constant  float *coefficients, __global __write_only float *Output) {

        int globalId = get_global_id(0); 
        float sum=0.0f;
        int min_i= max(64,globalId)-64;
        int max_i= min_i+65;
        for (int i=min_i; i< max_i; i++)
        {
            sum +=Array[i]*coefficients[globalId-i];    
        }
        Output[globalId]=sum;
}

cpu(8 个计算单元)计算时间不到 150 毫秒,GPU(25 个计算单元)计算时间不到 80 毫秒。每个项目的工作只有 65 次。使用 __constant 和 __read_only 以及 __write_only 参数说明符和一些整数工作减少可以很容易地加速这种少量的操作。

使用 float4 而不是 float 类型的数组和输出应该将 cpu 和 gpu 的速度提高 %80,因为它们是 SIMD 类型和矢量计算单元。

这个内核的瓶颈是:

  • 每个线程只有 65 次乘法和 65 次求和。
  • 但数据仍然通过 pci-express 接口传输,很慢。
  • 还有 1 个条件检查(i < max_i)每个浮点操作很高,需要循环展开。
  • 尽管您的 cpu 和 gpu 是基于向量的,但一切都是标量。

一般:

  • 运行ning内核第一次触发opencl的just in time编译器优化,速度慢。 运行 至少 5-10 次才能准确计时。
  • __constant space 只有 10 - 100 kB,但比 __global 快,适合 amd 的 hd5000 系列。
  • 内核开销为 100 微秒,而 65 次缓存操作少于此值,并且被内核开销时间(甚至更糟的是 pci-e 延迟)所掩盖。
  • 工作项太少占用率低,速度慢

另外:

  • 由于分支预测、总缓存带宽、指令延迟和无 pcie 延迟。
  • HD5000 系列 amd 卡是传统卡,与 gimped 相同。
  • HD5450 有 166 GB/s 恒定内存带宽
  • 也只有83 GB/s LDS(本地内存)带宽
  • 它也有 83 GB/s L1 和 L2 缓存带宽所以让它在 __global 驱动程序优化而不是 LDS 上工作,除非你计划升级你的计算机。(对于当然是数组)也许,来自 LDS 的奇数元素,来自 __global 的偶数元素可能有 83+83 = 166 GB/s 带宽。你可以试试。也许就银行冲突而言,两个两个比交替更好。

  • 使用系数作为 __constant (166 GB/s) 和数组作为 __global 应该给你 166 + 83 = 249 GB/s 组合带宽。

  • 每个系数元素每个线程只使用一次所以我不建议使用私有寄存器 (499 GB/s)