共享内存与Thread的同步
-
给出3072*3072大小的数组, 每一个元素都是整数, 现在要做的就是, 将每个元素的立方相加, 并求出最终的结果. 首先,我们先用PyCuda基础知识写出来一个可以运行的程序.
import time import numpy as np import pycuda.autoinit import pycuda.driver as cuda from pycuda.compiler import SourceModule mod = SourceModule(""" __global__ void sumOfSquares(int* num, int *result, size_t N) { int index = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; int sum = 0; for (int i = index; i < N; i += stride) { sum += num[i]*num[i]*num[i]; } result[index] = sum; } """) def test(N, np_seed): np.random.seed(np_seed) a = np.random.randint(1, 10, N) N = np.int32(N) thread_size = 256 # block_size = int((N + thread_size - 1) / thread_size) # power = len(str(block_size)) - 2 # block_size = int(block_size / (10**power)) block_size = 32 b = np.empty(thread_size*block_size, dtype=np.int32) sumOfSquares = mod.get_function("sumOfSquares") sumOfSquares( cuda.In(a), cuda.Out(b), N, block=(thread_size, 1, 1), grid=(block_size, 1) ) t_sum = 0 for item in b: t_sum += item print('sum: %d'%(t_sum)) if __name__ == "__main__": N = 3072*3072 time_sum = 0 cnt = 10 for i in range(cnt): time_start = time.time() test(N, i) time_run = time.time()-time_start time_sum += time_run print('time: %f'%(time_sum/cnt))
从上面的代码看, 我们写并行的形式是将3072*3072(943718)的数据分成了256*32(8192)份让它们并行计算, 然后再将计算出来的8192份结果再串行向加得出最终的结果.这样就保留出了一个问题就是如果数据非常多的情况下, 我们cpu计算的压力会非常大. 为了解决这个问题, 我们就可以使用共享内存与线程同步.
-
ShareMemory 和Thread同步
shareMemory: 而接下来我们要使用的shared memory,是一个 block 中每个 thread 都共享的内存。它会使用在 GPU 上的内存,所以存取的速度相当快,不需要担心 latency 的问题。声明一块ShareMemory也是十分简单的:
__shared__ int sharedata[128];
Thread同步: 因为我们要想让每个block把自己Thread的结果加起来,需要等到所有的Thread都将自己的结果结算出来。不过同步问题也没什么好说的,因为这是无论使用哪种语言在使用多线程时都需要考虑的一个问题。在Cuda编程中只需要调用一个函数:
__syncthreads()
-
我们现在要做的是将每个block中的每个Thread的结果再相加到一起, 也就是现在CPU只需要将每个block中的结果相加就行了, 只需要进行32次的加运算而不是8192次了. 所以我们对上面的程序要进行两方面的修改, 一方面是减小b数组的大小, 一方面是在CUDA程序中申请共享内存并且实现Thread的同步.
import time import sys import numpy as np import pycuda.autoinit import pycuda.driver as cuda from pycuda.compiler import SourceModule mod = SourceModule(""" __global__ void sumOfSquares(int* num, int *result, size_t N) { extern __shared__ int shared[256]; int index = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; int tid = threadIdx.x; int bid = blockIdx.x; shared[tid] = 0; for (int i = index; i < N; i += stride) { shared[tid] += num[i]*num[i]*num[i]; } //同步 保证每个 thread 都已经把结果写到 shared[tid] 里面 __syncthreads(); if (tid == 0) { for (int i = 1; i < blockDim.x; i++) { shared[0] += shared[i]; } result[bid] = shared[0]; } } """) def test(N, np_seed): np.random.seed(np_seed) a = np.random.randint(1, 10, N) N = np.int32(N) thread_size = 256 block_size = int((N + thread_size - 1) / thread_size) power = len(str(block_size)) - 2 block_size = int(block_size / (10**power)) # block_size = 64 b = np.empty(block_size, dtype=np.int32) sumOfSquares = mod.get_function("sumOfSquares") # b = np.int32(b) sumOfSquares( cuda.In(a), cuda.Out(b), N, block=(thread_size, 1, 1), grid=(block_size, 1) ) # t_sum = np.sum(b) t_sum = 0 for item in b: t_sum += item print('sum: %d'%(t_sum)) if __name__ == "__main__": N = 3072*3072 time_sum = 0 cnt = 10 for i in range(cnt): time_start = time.time() test(N, i) time_run = time.time()-time_start time_sum += time_run print('time: %f'%(time_sum/cnt))
注: 在实现上述代码时我还发现了一个定义block大小的一个规律
-
如果是计算的形式是将矩阵中的所有元素经过计算从而取得一个值, 这样的选取的block需要自己定义一个数, 一般是32或者64, 这种形式要比第二种形式快, 而且快的很明显, 我使用3072*3072的数据用量进行对比, 发现要比第二种快了10倍左右.
-
如果计算的形式类似与两个矩阵的加减这种形式, 可以使用
block_size = int(N + thread_size - 1) / thread_size)
(N: 数据量的大小, thread_size是定义的thread的大小)这种形式会比使用上一种形式的要快. 但是快的可能不太明显. -
附两种比较的例子
来源:https://blog.csdn.net/jsmok_xingkong/article/details/99413400