使用本地内存来加速计算

时间:2013-11-27 23:10:00

标签: opencl

应该是一个简单的,但我的OpenCL技能完全生锈。 :)

我有一个简单的内核,可以处理两个数组的总和:

__kernel void sum(__global float* a, __global float* b, __global float* c)
{
    __private size_t gid = get_global_id(0);

    c[gid] = log(sqrt(exp(cos(sin(a[gid]))))) + log(sqrt(exp(cos(sin(b[gid])))));
}

工作正常。

现在我正在尝试使用本地内存,希望能加快速度:

__kernel void sum_with_local_copy(__global float* a, __global float* b, __global float* c, __local float* tmpa, __local float* tmpb, __local float* tmpc)
{
    __private size_t gid = get_global_id(0);
    __private size_t lid = get_local_id(0);
    __private size_t grid = get_group_id(0);
    __private size_t lsz = get_local_size(0);

    event_t evta = async_work_group_copy(tmpa, a + grid * lsz, lsz, 0);
    wait_group_events(1, &evta);

    event_t evtb = async_work_group_copy(tmpb, b + grid * lsz, lsz, 0);
    wait_group_events(1, &evtb);

    tmpc[lid] = log(sqrt(exp(cos(sin(tmpa[lid]))))) + log(sqrt(exp(cos(sin(tmpb[lid])))));

    event_t evt = async_work_group_copy(c + grid * lsz, tmpc, lsz, 0);
    wait_group_events(1, &evt);
}

但是这个内核存在两个问题:

  • 这比天真的实施慢了3倍

  • 从索引64开始的结果是错误的

我的本​​地大小是最大工作组大小。

所以我的问题是:

1)我错过了一些明显的东西,还是真的有一种微妙之处?

2)如何使用本地内存来加速计算?

3)我应该在内核中循环,以便每个工作项执行多个操作吗?

提前致谢。

3 个答案:

答案 0 :(得分:3)

你的简单内核已经是工作组性能的最佳选择。

本地内存只会在工作组中的多个工作项从本地内存中的同一地址读取的情况下提高性能。由于内核中没有共享数据,因此无法通过将数据从全局内存传输到本地内存来获得收益,从而减慢了速度。

对于第3点,您可以通过处理每个线程的多个值来看到增益(取决于您的计算成本和您拥有的硬件)。

答案 1 :(得分:2)

您可能知道在执行内核时可以使用以下方法显式设置本地工作组大小(LWS):

clEnqueueNDRangeKernel( ... bunch of args include Local Work Size ...);

discussed here但正如Kyle已经提到的那样,你真的不必这样做,因为当你为LWS参数传递NULL时,OpenCL会尝试为LWS选择最佳值。

实际上规范说:“local_work_size也可以是NULL值,在这种情况下,OpenCL实现将决定如何将全局工作项分解为适当的工作组实例。”

我很想知道你的情况如何,所以我设置你的计算来验证我的设备上OpenCL选择的默认值的性能。

如果您感兴趣我设置了一些任意数据:

int n = powl(2, 20);
float* a = (float*)malloc(sizeof(float)*n);
float* b = (float*)malloc(sizeof(float)*n);
float* results = (float*)malloc(sizeof(float)*n);

for (int i = 0; i<n; i++) {
    a[i] = (float)i;
    b[i] = (float)(n-i);
    results[i] = 0.f;
}

然后在定义了我改变的所有其他OpenCL结构之后,lws = VALUE,从2到256(我的设备上允许的最大内核),以2为幂,并测量了挂钟时间(注意:可以也使用OpenCL事件):

struct timeval timer;
int trials = 100;
gettimeofday(&timer, NULL);
double t0 = timer.tv_sec+(timer.tv_usec/1000000.0);

// ---------- Execution ---------
size_t global_work_size = n;
size_t lws[] = {VALUE}; // VALUE was varied from 2 to 256 in powers of 2.
for (int trial = 0; trial<trials; trial++) {
    clEnqueueNDRangeKernel(cmd_queue, kernel[0], 1, NULL, &global_work_size, lws, 0, NULL, NULL);
}
clFinish(cmd_queue);

gettimeofday(&timer, NULL);
double t1 = timer.tv_sec+(timer.tv_usec/1000000.0);
double avgTime = (double)(t1-t0)/trials/1.0f;
然后,我将总执行时间绘制为LWS的函数,并且正如预期的那样,性能会有很大差异,直到达到LWS的最佳值= 256。对于LWS&gt; 256,这个内核超出了我设备上的内存。

enter image description here

对于这些测试,我正在运行笔记本电脑GPU:AMD ATI Radeon HD 6750M,最大计算单位= 6,CL_DEVICE_LOCAL_MEM_SIZE = 32768(所以没有比其他GPU更大的尖叫声)

以下是原始数字:

   LWS     time(sec)
    2       14.004
    4       6.850
    8       3.431
   16       1.722
   32       0.866
   64       0.438
  128       0.436
  256       0.436

接下来,我检查了OpenCL选择的默认值(为LWS传递NULL),这对应于我通过分析找到的最佳值,即LWS = 256。

因此,在您设置的代码中,您发现了一个次优案例,如前所述,最好让OpenCL为本地工作组选择最佳值,尤其是当您的内核中没有多个工作之间的共享数据时 - 工作组中的项目。

关于你得到的错误,你可能违反了一个约束(来自规范): 工作组中的工作项总数必须小于或等于CL_DEVICE_MAX_WORK_GROUP_SIZE

您是否通过查询设备的CL_DEVICE_MAX_WORK_GROUP_SIZE来详细检查?

答案 2 :(得分:1)

添加Kyle written:它必须是多个工作项,从相同的地址读取;如果它只是每个工作项本身从同一地址多次读取 - 那么本地内存再也无法帮助你了;只需使用工作项的私有内存,即您在内核中定义的变量。

此外,一些与使用本地内存无关的要点:

  • log(sqrt(exp(x))= log(exp(x))/ 2 = x / 2 ...假设它是自然对数。
  • log(sqrt(exp(x))= log(exp(x))/ 2 = x /(2 ln(2))...假设它是基数2的对数。提前计算ln(2)当然。
  • 如果您确实拥有一些复杂的函数函数,那么最好使用Taylor series扩展。例如,您的函数扩展为1/2-x ^ 2/4 +(5 x ^ 4)/ 48 + O(x ^ 6)(阶数5)。

    最后一个术语是一个错误术语,您可以从上面绑定以选择适当的扩展顺序; “良好行为”功能的误差项不应该那么高。泰勒扩展计算甚至可能从进一步的并行化中受益(但话说再次,它可能不会)。