对浮点数求和的最佳 OpenCL 2 内核是什么?
What is the optimum OpenCL 2 kernel to sum floats?
C++ 17 引入了许多新算法来支持并行执行,特别是 std::reduce is a parallel version of std::accumulate,它允许 non-commutative
操作的 non-deterministic
行为,例如浮点加法。我想使用 OpenCL 2 实现一个 reduce 算法。
Intel 有一个示例 here which uses OpenCL 2 work group
kernel functions to implement a std::exclusive_scan OpenCL 2 内核。下面是基于 Intel 的 exclusive_scan
示例计算浮点数求和的内核:
kernel void sum_float (global float* sum, global float* values)
{
float sum_val = 0.0f;
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
float value = work_group_reduce_add(values[index]);
sum_val += work_group_broadcast(value, 0u);
}
sum[0] = sum_val;
}
上面的内核有效(或似乎有效!)。但是,exclusive_scan
需要 work_group_broadcast
函数将一个 work group
的最后一个值传递给下一个,而这个内核只需要将 work_group_reduce_add 的结果添加到 sum_val
,所以 atomic add
更合适。
OpenCL 2 提供了一个支持 atomic_fetch_add
的 atomic_int
。上面使用 atomic_int 的内核的整数版本是:
kernel void sum_int (global int* sum, global int* values)
{
atomic_int sum_val;
atomic_init(&sum_val, 0);
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
int value = work_group_reduce_add(values[index]);
atomic_fetch_add(&sum_val, value);
}
sum[0] = atomic_load(&sum_val);
}
OpenCL 2 也提供了 atomic_float
但它 不 支持 atomic_fetch_add
.
实现 OpenCL2 内核对浮点数求和的最佳方法是什么?
kernel void sum_float (global float* sum, global float* values)
{
float sum_val = 0.0f;
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
float value = work_group_reduce_add(values[index]);
sum_val += work_group_broadcast(value, 0u);
}
sum[0] = sum_val;
}
这有一个将数据写入 sum 的零索引元素的竞争条件,所有工作组都在做相同的计算,这使得这个 O(N*N) 而不是 O(N) 并且需要超过 1100 毫秒才能完成1M元素数组总和.
对于相同的 1-M 元素数组,this(global=1M, local=256)
kernel void sum_float2 (global float* sum, global float* values)
{
float sum_partial = work_group_reduce_add(values[get_global_id(0)]);
if(get_local_id(0)==0)
sum[get_group_id(0)] = sum_partial;
}
紧随其后(全局=4k,本地=256)
kernel void sum_float3 (global float* sum, global float* values)
{
float sum_partial = work_group_reduce_add(sum[get_global_id(0)]);
if(get_local_id(0)==0)
values[get_group_id(0)] = sum_partial;
}
在几毫秒内做同样的事情,除了第三步。第一个将每个组的总和放入其组 ID 相关项中,第二个内核将这些总和为 16 个值,这 16 个值可以很容易地通过 CPU(微秒或更短)(作为第三步)求和。
程序是这样工作的:
values: 1.0 1.0 .... 1.0 1.0
sum_float2
sum: 256.0 256.0 256.0
sum_float3
values: 65536.0 65536.0 .... 16 items total to be summed by cpu
如果你需要使用原子,你应该尽可能少地使用它。最简单的示例可以是使用局部原子对每个组的许多值求和,然后在最后一步中使用每个组的单个全局原子函数将所有值相加。我现在还没有为 OpenCL 准备好 C++ 设置,但我想当您使用具有相同内存资源(可能是流模式或 SVM)的 多个设备 时,OpenCL 2.0 原子会更好and/or a CPU 使用 C++17 函数。如果您没有多个设备同时在同一区域进行计算,那么我认为这些新原子只能在已经工作的 OpenCL 1.2 原子之上进行微优化。我没有使用这些新的原子,所以把所有这些都当作一粒盐。
C++ 17 引入了许多新算法来支持并行执行,特别是 std::reduce is a parallel version of std::accumulate,它允许 non-commutative
操作的 non-deterministic
行为,例如浮点加法。我想使用 OpenCL 2 实现一个 reduce 算法。
Intel 有一个示例 here which uses OpenCL 2 work group
kernel functions to implement a std::exclusive_scan OpenCL 2 内核。下面是基于 Intel 的 exclusive_scan
示例计算浮点数求和的内核:
kernel void sum_float (global float* sum, global float* values)
{
float sum_val = 0.0f;
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
float value = work_group_reduce_add(values[index]);
sum_val += work_group_broadcast(value, 0u);
}
sum[0] = sum_val;
}
上面的内核有效(或似乎有效!)。但是,exclusive_scan
需要 work_group_broadcast
函数将一个 work group
的最后一个值传递给下一个,而这个内核只需要将 work_group_reduce_add 的结果添加到 sum_val
,所以 atomic add
更合适。
OpenCL 2 提供了一个支持 atomic_fetch_add
的 atomic_int
。上面使用 atomic_int 的内核的整数版本是:
kernel void sum_int (global int* sum, global int* values)
{
atomic_int sum_val;
atomic_init(&sum_val, 0);
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
int value = work_group_reduce_add(values[index]);
atomic_fetch_add(&sum_val, value);
}
sum[0] = atomic_load(&sum_val);
}
OpenCL 2 也提供了 atomic_float
但它 不 支持 atomic_fetch_add
.
实现 OpenCL2 内核对浮点数求和的最佳方法是什么?
kernel void sum_float (global float* sum, global float* values)
{
float sum_val = 0.0f;
for (size_t i = 0u; i < get_num_groups(0); ++i)
{
size_t index = get_local_id(0) + i * get_enqueued_local_size(0);
float value = work_group_reduce_add(values[index]);
sum_val += work_group_broadcast(value, 0u);
}
sum[0] = sum_val;
}
这有一个将数据写入 sum 的零索引元素的竞争条件,所有工作组都在做相同的计算,这使得这个 O(N*N) 而不是 O(N) 并且需要超过 1100 毫秒才能完成1M元素数组总和.
对于相同的 1-M 元素数组,this(global=1M, local=256)
kernel void sum_float2 (global float* sum, global float* values)
{
float sum_partial = work_group_reduce_add(values[get_global_id(0)]);
if(get_local_id(0)==0)
sum[get_group_id(0)] = sum_partial;
}
紧随其后(全局=4k,本地=256)
kernel void sum_float3 (global float* sum, global float* values)
{
float sum_partial = work_group_reduce_add(sum[get_global_id(0)]);
if(get_local_id(0)==0)
values[get_group_id(0)] = sum_partial;
}
在几毫秒内做同样的事情,除了第三步。第一个将每个组的总和放入其组 ID 相关项中,第二个内核将这些总和为 16 个值,这 16 个值可以很容易地通过 CPU(微秒或更短)(作为第三步)求和。
程序是这样工作的:
values: 1.0 1.0 .... 1.0 1.0
sum_float2
sum: 256.0 256.0 256.0
sum_float3
values: 65536.0 65536.0 .... 16 items total to be summed by cpu
如果你需要使用原子,你应该尽可能少地使用它。最简单的示例可以是使用局部原子对每个组的许多值求和,然后在最后一步中使用每个组的单个全局原子函数将所有值相加。我现在还没有为 OpenCL 准备好 C++ 设置,但我想当您使用具有相同内存资源(可能是流模式或 SVM)的 多个设备 时,OpenCL 2.0 原子会更好and/or a CPU 使用 C++17 函数。如果您没有多个设备同时在同一区域进行计算,那么我认为这些新原子只能在已经工作的 OpenCL 1.2 原子之上进行微优化。我没有使用这些新的原子,所以把所有这些都当作一粒盐。