PyCuda学习三之--共享内存与Thread的同步

共享内存与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大小的一个规律

  1. 如果是计算的形式是将矩阵中的所有元素经过计算从而取得一个值, 这样的选取的block需要自己定义一个数, 一般是32或者64, 这种形式要比第二种形式快, 而且快的很明显, 我使用3072*3072的数据用量进行对比, 发现要比第二种快了10倍左右.

  2. 如果计算的形式类似与两个矩阵的加减这种形式, 可以使用block_size = int(N + thread_size - 1) / thread_size) (N: 数据量的大小, thread_size是定义的thread的大小)这种形式会比使用上一种形式的要快. 但是快的可能不太明显.

  3. 附两种比较的例子

PyCuda学习三之--共享内存与Thread的同步_第1张图片

你可能感兴趣的:(PyCuda)