许多算法迭代直到达到某个收敛标准(例如,特定矩阵的稳定性)。在许多情况下,每次迭代必须启动一个CUDA内核。我的问题是:如何有效准确地确定矩阵在最后一次内核调用过程中是否发生了变化?以下三种可能性似乎同样令人不满意:
上述三种选择中的任何一种,或者替代方案,是否被认为是最佳做法和/或通常更有效?
答案 0 :(得分:4)
我还会回到我将在2012年发布的答案,但是浏览器崩溃了。
基本思想是你可以使用warp投票指令来执行简单,廉价的缩减,然后每个块使用零个或一个原子操作来更新主机在每个内核启动后可以读取的固定映射标志。使用映射标志消除了在每次内核启动后显式设备进行主机传输的需要。
这需要在内核中每个warp使用一个共享内存字,这是一个很小的开销,如果你提供每个块的warp数作为模板参数,一些模板技巧可以允许循环展开。
一个完整的工作示例(使用C ++主机代码,目前我无法访问正在运行的PyCUDA安装)如下所示:
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <assert.h>
__device__ unsigned int process(int & val)
{
return (++val < 10);
}
template<int nwarps>
__global__ void kernel(int *inout, unsigned int *kchanged)
{
__shared__ int wchanged[nwarps];
unsigned int laneid = threadIdx.x % warpSize;
unsigned int warpid = threadIdx.x / warpSize;
// Do calculations then check for change/convergence
// and set tchanged to be !=0 if required
int idx = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int tchanged = process(inout[idx]);
// Simple blockwise reduction using voting primitives
// increments kchanged is any thread in the block
// returned tchanged != 0
tchanged = __any(tchanged != 0);
if (laneid == 0) {
wchanged[warpid] = tchanged;
}
__syncthreads();
if (threadIdx.x == 0) {
int bchanged = 0;
#pragma unroll
for(int i=0; i<nwarps; i++) {
bchanged |= wchanged[i];
}
if (bchanged) {
atomicAdd(kchanged, 1);
}
}
}
int main(void)
{
const int N = 2048;
const int min = 5, max = 15;
std::vector<int> data(N);
for(int i=0; i<N; i++) {
data[i] = min + (std::rand() % (int)(max - min + 1));
}
int* _data;
size_t datasz = sizeof(int) * (size_t)N;
cudaMalloc<int>(&_data, datasz);
cudaMemcpy(_data, &data[0], datasz, cudaMemcpyHostToDevice);
unsigned int *kchanged, *_kchanged;
cudaHostAlloc((void **)&kchanged, sizeof(unsigned int), cudaHostAllocMapped);
cudaHostGetDevicePointer((void **)&_kchanged, kchanged, 0);
const int nwarps = 4;
dim3 blcksz(32*nwarps), grdsz(16);
// Loop while the kernel signals it needs to run again
do {
*kchanged = 0;
kernel<nwarps><<<grdsz, blcksz>>>(_data, _kchanged);
cudaDeviceSynchronize();
} while (*kchanged != 0);
cudaMemcpy(&data[0], _data, datasz, cudaMemcpyDeviceToHost);
cudaDeviceReset();
int minval = *std::min_element(data.begin(), data.end());
assert(minval == 10);
return 0;
}
这里,kchanged
是内核用来表示需要再次运行到主机的标志。内核一直运行,直到输入中的每个条目都增加到阈值以上。在每个线程处理结束时,它参与warp投票,之后每个warp中的一个线程将投票结果加载到共享内存。一个线程减少了warp结果,然后以原子方式更新kchanged
值。主机线程等待设备完成,然后可以直接从映射的主机变量中读取结果。
您应该能够根据您的应用需求进行调整
答案 1 :(得分:3)
我会回到原来的建议。我用自己的答案更新了the related question,我认为这是正确的。
在全局内存中创建一个标志:
__device__ int flag;
每次迭代,
将标志初始化为零(在主机代码中):
int init_val = 0;
cudaMemcpyToSymbol(flag, &init_val, sizeof(int));
在内核设备代码中,如果对矩阵进行了更改,请将标志修改为1:
__global void iter_kernel(float *matrix){
...
if (new_val[i] != matrix[i]){
matrix[i] = new_val[i];
flag = 1;}
...
}
调用内核后,在迭代结束时(在主机代码中),测试修改:
int modified = 0;
cudaMemcpyFromSymbol(&modified, flag, sizeof(int));
if (modified){
...
}
即使多个线程在单独的块中,甚至是单独的网格,也在写flag
值,只要他们做的唯一事情是写相同的值(在这种情况下为1),就没有危险。写入不会“丢失”,并且flag
变量中不会显示任何虚假值。
以这种方式测试float
或double
数量的相等性是值得怀疑的,但这似乎不是您的问题的重点。如果您有一个首选方法来声明“修改”,请使用它(例如,在公差范围内测试相等性)。
此方法的一些明显增强是每个线程创建一个(本地)标志变量,并让每个线程每个内核更新一次全局标志变量,而不是每次修改。这将导致每个内核每个线程最多一次全局写入。另一种方法是在共享内存中为每个块保留一个标志变量,并让所有线程只更新该变量。在块完成时,对全局存储器进行一次写入(如果需要)以更新全局标志。在这种情况下我们不需要求助于复杂的减少,因为整个内核只有一个布尔结果,并且我们可以容忍多个线程写入共享或全局变量,只要所有线程都写入相同的值。
我看不出任何使用原子的理由,或者它如何使任何事情受益。
减少内核似乎有点矫枉过正,至少与其中一种优化方法(例如每个块的共享标志)相比。它会有你提到的缺点,例如,任何小于CRC或类似复杂计算的事实都可能将两个不同的矩阵结果混淆为“相同”。