先说结论:用最笨的加锁方法实现块同步就行。
cuda9之前都没提供块间同步功能,手工实现块间同步有两种思路:加锁、不加锁,如图所示:
加锁方法只有一个计数器,每个块执行完,用原子操作给计数器加一,然后等待计数器的值到达期望后,再进行下一步。
不加锁方法每个块分配一个计数器,每个块执行完,给自己的计数器加一,然后等待所有计数器都加一后,再进行下一步。
按照我的理解,不加锁方法避免了原子操作,开销会更小,实验验证一下。首先是随便写一个核函数,功能是:每个block计算0到1023的和,再把每个block的结果求和,代码如下:
// 无同步
__global__ void kernel_sum0(int *lock, int *sum) {
// 本地缓存赋值
__shared__ int localSum[1024];
localSum[threadIdx.x] = threadIdx.x; // 赋初值
// 规约
for (int offset=blockDim.x>>1; offset>0; offset>>=1) {
if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset];
__syncthreads(); // 保证每轮循环前上轮循环都结束了
}
if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果
// 计算结果
if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果
int sum0 = 0;
for (int i=0; i sum[0] = sum0; } } 这个核函数得到的结果是错误的,因为进行block求和之前,有些block可能还没得到结果。所以要在block间同步,保证所有block都得到结果后再求和。用加锁的方法实现一下,代码如下: // lock方法 __global__ void kernel_sum1(int *lock, int *sum) { // 本地缓存赋值 __shared__ int localSum[1024]; localSum[threadIdx.x] = threadIdx.x; // 赋初值 // 规约 for (int offset=blockDim.x>>1; offset>0; offset>>=1) { if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset]; __syncthreads(); // 保证每轮循环前上轮循环都结束了 } if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果 // 等待同步 if (threadIdx.x == 0) { int lockTemp = lock[0]; lockTemp = (lockTemp/gridDim.x+1)*gridDim.x; atomicAdd(&lock[0], 1); // 写锁 int volatile temp = lock[0]; while(temp != lockTemp) temp = lock[0]; // 直到所有结果相同 } __syncthreads(); // 计算结果 if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果 int sum0 = 0; for (int i=0; i sum[0] = sum0; } } 以上代码和原始版本的区别,只是加了等待同步的7行代码。加锁有原子操作,可能对性能有影响,改进一下,代码如下: // lock-free的方法 __global__ void kernel_sum2(int *lock, int *sum) { // 本地缓存赋值 __shared__ int localSum[1024]; localSum[threadIdx.x] = threadIdx.x; // 赋初值 // 规约 for (int offset=blockDim.x>>1; offset>0; offset>>=1) { if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset]; __syncthreads(); // 保证每轮循环前上轮循环都结束了 } if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果 // 等待同步 if (threadIdx.x == 0) { lock[blockIdx.x] += 1; // 加锁 int lockTemp = lock[blockIdx.x]; for (int i=0; i int volatile temp = lock[i]; while(temp != lockTemp) temp = lock[i]; // 直到所有结果相同 } } __syncthreads(); // 计算结果 if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果 int sum0 = 0; for (int i=0; i sum[0] = sum0; } } 以上代码和加锁版本相比,只是等待同步部分的8行代码不同。 有了许多版本,可以开始测试了,代码如下: #include #include #include //--------计时器--------// class Timer { // 计时器 private: std::chrono::system_clock::time_point now; // 当前时间 std::time_t time_now; // 时间戳的格式化版 std::chrono::steady_clock::time_point t1; // 计时开始时刻 std::chrono::steady_clock::time_point t2; // 计时结束时刻 std::chrono::duration int p; // 是否暂停了 public: void getTimeNow() { // 输出当前时间戳 now = std::chrono::system_clock::now(); time_now = std::chrono::system_clock::to_time_t(now); std::cout << ctime(&time_now); } void start() { // 开始计时 t1 = std::chrono::steady_clock::now(); duration = std::chrono::duration p = 0; // 不暂停 } void pause() { // 暂停计时 t2 = std::chrono::steady_clock::now(); duration += std::chrono::duration_cast p = 1; } void resume() { // 恢复计时 t1 = std::chrono::steady_clock::now(); p = 0; } void getDuration() { // 输出耗时 if (p == 0) pause(); // 没暂停需要先暂停 std::cout << duration.count() << " seconds.\n"; } }; // 无同步 __global__ void kernel_sum0(int *lock, int *sum) { // 本地缓存赋值 __shared__ int localSum[1024]; localSum[threadIdx.x] = threadIdx.x; // 赋初值 // 规约 for (int offset=blockDim.x>>1; offset>0; offset>>=1) { if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset]; __syncthreads(); // 保证每轮循环前上轮循环都结束了 } if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果 // 计算结果 if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果 int sum0 = 0; for (int i=0; i sum[0] = sum0; } } // lock方法 __global__ void kernel_sum1(int *lock, int *sum) { // 本地缓存赋值 __shared__ int localSum[1024]; localSum[threadIdx.x] = threadIdx.x; // 赋初值 // 规约 for (int offset=blockDim.x>>1; offset>0; offset>>=1) { if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset]; __syncthreads(); // 保证每轮循环前上轮循环都结束了 } if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果 // 等待同步 if (threadIdx.x == 0) { int lockTemp = lock[0]; lockTemp = (lockTemp/gridDim.x+1)*gridDim.x; atomicAdd(&lock[0], 1); // 写锁 int volatile temp = lock[0]; while(temp != lockTemp) temp = lock[0]; // 直到所有结果相同 } __syncthreads(); // 计算结果 if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果 int sum0 = 0; for (int i=0; i sum[0] = sum0; } } // lock-free的方法 __global__ void kernel_sum2(int *lock, int *sum) { // 本地缓存赋值 __shared__ int localSum[1024]; localSum[threadIdx.x] = threadIdx.x; // 赋初值 // 规约 for (int offset=blockDim.x>>1; offset>0; offset>>=1) { if (threadIdx.x < offset) localSum[threadIdx.x] += localSum[threadIdx.x+offset]; __syncthreads(); // 保证每轮循环前上轮循环都结束了 } if (threadIdx.x == 0) sum[blockIdx.x] = localSum[0]; // block的结果 // 等待同步 if (threadIdx.x == 0) { lock[blockIdx.x] += 1; // 加锁 int lockTemp = lock[blockIdx.x]; for (int i=0; i int volatile temp = lock[i]; while(temp != lockTemp) temp = lock[i]; // 直到所有结果相同 } } __syncthreads(); // 计算结果 if (threadIdx.x == 0 && blockIdx.x == 0) { // 最后得到结果 int sum0 = 0; for (int i=0; i sum[0] = sum0; } } int main() { // 声明变量 Timer timer; // 随便用什么自己实现的计时器 int *lock_h, *lock_d, *sum_h, *sum_d; cudaMallocHost((void**)&lock_h, sizeof(int)*65536); cudaMalloc((void**)&lock_d, sizeof(int)*65536); cudaMallocHost((void**)&sum_h, sizeof(int)*65536); cudaMalloc((void**)&sum_d, sizeof(int)*65536); // 测试原始函数 for (int i=0; i<1000; i++) kernel_sum0<<<1024, 1024>>>(lock_d, sum_d); // 热身 cudaDeviceSynchronize(); std::cout << "no sync\n"; for (int block=64; block<65537; block*=2) { // 计时 timer.start(); for (int i=0; i<1000*10; i++) kernel_sum0<< cudaDeviceSynchronize(); std::cout << block << ":\t"; timer.getDuration(); } // 测试加锁函数 for (int i=0; i<1000; i++) kernel_sum1<<<1024, 1024>>>(lock_d, sum_d); // 热身 cudaDeviceSynchronize(); std::cout << "lock sync\n"; for (int block=64; block<65537; block*=2) { // 计时 timer.start(); for (int i=0; i<1000*10; i++) kernel_sum1<< cudaDeviceSynchronize(); std::cout << block << ":\t"; timer.getDuration(); } // 测试无锁函数 for (int i=0; i<1000; i++) kernel_sum2<<<1024, 1024>>>(lock_d, sum_d); // 热身 cudaDeviceSynchronize(); std::cout << "lock-free sync\n"; for (int block=64; block<65537; block*=2) { // 计时 timer.start(); for (int i=0; i<1000*10; i++) kernel_sum2<< cudaDeviceSynchronize(); std::cout << block << ":\t"; timer.getDuration(); } // 收尾 cudaMemcpy(sum_h, sum_d, sizeof(int)*1, cudaMemcpyDeviceToHost); std::cout << "sum:\t" << sum_h[0] << "\n"; return 0; } 我的测试平台是3090,有82个sm,threads数都是1024,block数冲64到65536,每次翻倍,测试结果如下: block641282565121024204840968192163843276865536no sync0.040.050.080.130.220.430.841.663.296.5613.10lock sync0.040.050.080.130.230.440.851.683.346.6613.29lock-free sync0.040.050.080.130.250.470.911.803.597.1514.28 上面表格中时间单位是秒,可以看到,最简单的加锁块同步,性能开销很小,实现简单,直接用就行了。 另外如果是cuda9以上的话,还可以用Cooperative Groups协作组,还没仔细学习,看手册上说是可以块同步,性能应该更好,不过代码的可移植性就没了。
发表评论