我一直在学习Cuda,我仍然在处理并行问题。我目前遇到的问题是对一组值实现最大减少。这是我的内核
__global__ void max_reduce(const float* const d_array,
float* d_max,
const size_t elements)
{
extern __shared__ float shared[];
int tid = threadIdx.x;
int gid = (blockDim.x * blockIdx.x) + tid;
if (gid < elements)
shared[tid] = d_array[gid];
__syncthreads();
for (unsigned int s=blockDim.x/2; s>0; s>>=1)
{
if (tid < s && gid < elements)
shared[tid] = max(shared[tid], shared[tid + s]);
__syncthreads();
}
if (gid == 0)
*d_max = shared[tid];
}
我使用相同的方法实现了min reduce(用min替换max函数),这种方法很好。
为了测试内核,我使用串行for循环找到了最小值和最大值。最小值和最大值在内核中总是相同,但只有min reduce匹配。
有什么明显的东西我错过了/做错了吗?
答案 0 :(得分:16)
你删除的答案中的主要结论是正确的:你发布的内核并不理解在内核执行结束时,你已经做了大量的整体减少,但结果并不完全完成。必须组合每个块的结果(以某种方式)。正如评论中指出的那样,您的代码还存在一些其他问题。我们来看看它的修改版本:
__device__ float atomicMaxf(float* address, float val)
{
int *address_as_int =(int*)address;
int old = *address_as_int, assumed;
while (val > __int_as_float(old)) {
assumed = old;
old = atomicCAS(address_as_int, assumed,
__float_as_int(val));
}
return __int_as_float(old);
}
__global__ void max_reduce(const float* const d_array, float* d_max,
const size_t elements)
{
extern __shared__ float shared[];
int tid = threadIdx.x;
int gid = (blockDim.x * blockIdx.x) + tid;
shared[tid] = -FLOAT_MAX; // 1
if (gid < elements)
shared[tid] = d_array[gid];
__syncthreads();
for (unsigned int s=blockDim.x/2; s>0; s>>=1)
{
if (tid < s && gid < elements)
shared[tid] = max(shared[tid], shared[tid + s]); // 2
__syncthreads();
}
// what to do now?
// option 1: save block result and launch another kernel
if (tid == 0)
d_max[blockIdx.x] = shared[tid]; // 3
// option 2: use atomics
if (tid == 0)
atomicMaxf(d_max, shared[0]);
}
gridDim.x*blockDim.x
大于elements
,则启动的最后一个块可能不是“完整”块。 gid
)是否小于elements
,当我们将s
添加到gid
时,为了索引到共享内存,我们仍然可以在最后一个块中索引复制到共享内存中的合法值。因此,我们需要注释1中指示的共享内存初始化。atomicMax
函数,但如the documentation所示,您可以使用atomicCAS
生成任意原子函数,并且我提供了一个示例atomicMaxf
为float
提供原子最大值。但是运行1024个或更多原子功能(每个块一个)是最好的方法吗?可能不是。
在启动线程块的内核时,我们实际上只需要启动足够的线程块来保持机器忙。根据经验,我们希望每个SM至少运行4-8个warp,而更多可能是一个好主意。但是,从最初启动数千个线程块的机器利用率角度来看,没有特别的好处。如果我们选择一个像每个SM 8个线程块的数字,并且我们在GPU中最多有14-16个SM,这给了我们相对较少的8 * 14 = 112个线程块。让我们选择128(8 * 16)作为一个漂亮的圆数。这没有什么神奇的,它足以让GPU保持忙碌状态。如果我们让这128个线程块中的每一个都做了额外的工作来解决整个问题,那么我们就可以利用我们对atomics的使用而不会(或许)为这样做付出太多的代价,并避免多个内核启动。那看起来怎么样?:
__device__ float atomicMaxf(float* address, float val)
{
int *address_as_int =(int*)address;
int old = *address_as_int, assumed;
while (val > __int_as_float(old)) {
assumed = old;
old = atomicCAS(address_as_int, assumed,
__float_as_int(val));
}
return __int_as_float(old);
}
__global__ void max_reduce(const float* const d_array, float* d_max,
const size_t elements)
{
extern __shared__ float shared[];
int tid = threadIdx.x;
int gid = (blockDim.x * blockIdx.x) + tid;
shared[tid] = -FLOAT_MAX;
while (gid < elements) {
shared[tid] = max(shared[tid], d_array[gid]);
gid += gridDim.x*blockDim.x;
}
__syncthreads();
gid = (blockDim.x * blockIdx.x) + tid; // 1
for (unsigned int s=blockDim.x/2; s>0; s>>=1)
{
if (tid < s && gid < elements)
shared[tid] = max(shared[tid], shared[tid + s]);
__syncthreads();
}
if (tid == 0)
atomicMaxf(d_max, shared[0]);
}
使用这个经过修改的内核,在创建内核启动时,我们不会根据总体数据大小(elements
)决定要启动多少个线程块。相反,我们正在启动固定数量的块(例如,128,您可以修改此数字以找出最快运行的块),并让每个线程块(以及整个网格)循环通过内存,计算每个元素的部分最大操作共享内存。然后,在标有注释1的行中,我们必须将gid
变量重新设置为它的初始值。这实际上是不必要的,如果我们保证网格的大小(gridDim.x*blockDim.x
)小于elements
,则可以进一步简化块减少循环代码,这在内核启动时并不难。 / p>
请注意,使用此原子方法时,必须将结果(在本例中为*d_max
)初始化为适当的值,例如-FLOAT_MAX
。
同样,我们通常会引导人们使用原子,但在这种情况下,如果我们仔细管理它,就值得考虑,它可以让我们节省额外内核启动的开销。
有关如何进行快速平行缩减的忍者级分析,请查看Mark Harris的优秀白皮书,该白皮书可与相关的CUDA sample一起使用。
答案 1 :(得分:-1)
这是一个看起来很幼稚但实际上并非如此。这不会推广到 sum()
等其他函数,但它适用于 min()
和 max()
。
__device__ const float float_min = -3.402e+38;
__global__ void maxKernel(float* d_data)
{
// compute max over all threads, store max in d_data[0]
int i = threadIdx.x;
__shared__ float max_value;
if (i == 0) max_value = float_min;
float v = d_data[i];
__syncthreads();
while (max_value < v) max_value = v;
__syncthreads();
if (i == 0) d_data[0] = max_value;
}
是的,没错,只在初始化后同步一次,在写入结果前同步一次。该死的比赛条件!全速前进!
在你告诉我它不起作用之前,请先试一试。我已经进行了彻底的测试,它每次都可以在各种任意大小的内核上运行。事实证明,在这种情况下,竞争条件并不重要,因为 while 循环解决了它。
它的工作速度明显快于传统的减少。另一个令人惊讶的是,内核大小为 32 的平均传递次数为 4。是的,即 (log(n)-1),这似乎违反直觉。这是因为比赛条件提供了好运的机会。除了消除常规减少的开销之外,还有此好处。
对于较大的 n,无法避免每个扭曲至少进行一次迭代,但该迭代仅涉及一个比较操作,当 max_value 位于分布的高端时,该比较操作通常在整个扭曲中立即为假。您可以修改它以使用多个 SM,但这会大大增加总工作量并增加通信成本,因此不太可能有帮助。
为了简洁起见,我省略了大小和输出参数。大小只是线程数(可以是 137 或任何你喜欢的)。输出在 d_data[0]
中返回。
我已将工作文件上传到此处:https://github.com/kenseehart/YAMR