技术开发 频道

CUDA入门:CUDA程序优化步骤

  【IT168 技术】在《runtime API创建CUDA程序》中,我们做了一个计算一大堆数字的平方和的程序。不过,我们也提到这个程序的执行效率并不理想。当然,实际上来说,如果只是要做计算平方和的动作,用 CPU 做会比用 GPU 快得多。这是因为平方和的计算并不需要太多运算能力,所以几乎都是被内存带宽所限制。因此,光是把数据复制到显卡内存上的这个动作,所需要的时间,可能已经和直接在 CPU 上进行计算差不多了。

  不过,如果进行平方和的计算,只是一个更复杂的计算过程的一部份的话,那么当然在 GPU 上计算还是有它的好处的。而且,如果数据已经在显卡内存上(例如在 GPU 上透过某种算法产生),那么,使用 GPU 进行这样的运算,还是会比较快的。

  刚才也提到了,由于这个计算的主要瓶颈是内存带宽,所以,理论上显卡的内存带宽是相当大的。这里我们就来看看,倒底我们的第一个程序,能利用到多少内存带宽。

  程序的并行化

  我们的第一个程序,并没有利用到任何并行化的功能。整个程序只有一个 thread。在 GeForce 8800GT 上面,在 GPU 上执行的部份(称为 "kernel")大约花费 640M 个频率。GeForce 8800GT 的执行单元的频率是 1.5GHz,因此这表示它花费了约 0.43 秒的时间。1M 个 32 bits 数字的数据量是 4MB,因此,这个程序实际上使用的内存带宽,只有 9.3MB/s 左右!这是非常糟糕的表现。

  为什么会有这样差的表现呢?这是因为 GPU 的架构特性所造成的。在 CUDA 中,一般的数据复制到的显卡内存的部份,称为 global memory。这些内存是没有 cache 的,而且,存取 global memory 所需要的时间(即 latency)是非常长的,通常是数百个 cycles。由于我们的程序只有一个 thread,所以每次它读取 global memory 的内容,就要等到实际读取到数据、累加到 sum 之后,才能进行下一步。这就是为什么它的表现会这么的差。

  由于 global memory 并没有 cache,所以要避开巨大的 latency 的方法,就是要利用大量的 threads。假设现在有大量的 threads 在同时执行,那么当一个 thread 读取内存,开始等待结果的时候,GPU 就可以立刻切换到下一个 thread,并读取下一个内存位置。因此,理想上当 thread 的数目够多的时候,就可以完全把 global memory 的巨大 latency 隐藏起来了。

  要怎么把计算平方和的程序并行化呢?最简单的方法,似乎就是把数字分成若干组,把各组数字分别计算平方和后,最后再把每组的和加总起来就可以了。一开始,我们可以把最后加总的动作,由 CPU 来进行。

  首先,在 first_cuda.cu 中,在 #define DATA_SIZE 的后面增加一个 #define,设定 thread 的数目:

#define DATA_SIZE    1048576
#define THREAD_NUM   256

  接着,把 kernel 程序改成:

__global__ static void sumOfSquares(int *num, int* result,
    clock_t
* time)
{
    
const int tid = threadIdx.x;
    
const int size = DATA_SIZE / THREAD_NUM;
    
int sum = 0;
    
int i;
    clock_t start;
    
if(tid == 0) start = clock();
    
for(i = tid * size; i < (tid + 1) * size; i++) {
       sum
+= num[i] * num[i];
    }

    result[tid]
= sum;
    
if(tid == 0) *time = clock() - start;
}

  程序里的 threadIdx 是 CUDA 的一个内建的变量,表示目前的 thread 是第几个 thread(由 0 开始计算)。以我们的例子来说,会有 256 个 threads,所以同时会有 256 个 sumOfSquares 函式在执行,但每一个的 threadIdx.x 则分别会是 0 ~ 255。利用这个变量,我们就可以让每一份函式执行时,对整个数据不同的部份计算平方和。另外,我们也让计算时间的动作,只在 thread 0(即 threadIdx.x = 0 的时候)进行。

  同样的,由于会有 256 个计算结果,所以原来存放 result 的内存位置也要扩大。把 main 函式中的中间部份改成:

    int* gpudata, *result;
    clock_t
* time;
    cudaMalloc((
void**) &gpudata, sizeof(int) * DATA_SIZE);
    cudaMalloc((
void**) &result, sizeof(int) * THREAD_NUM);
    cudaMalloc((
void**) &time, sizeof(clock_t));
    cudaMemcpy(gpudata, data,
sizeof(int) * DATA_SIZE,
        cudaMemcpyHostToDevice);

    sumOfSquares
<<<1, THREAD_NUM, 0>>>(gpudata, result, time);

    
int sum[THREAD_NUM];
    clock_t time_used;
    cudaMemcpy(
&sum, result, sizeof(int) * THREAD_NUM,
        cudaMemcpyDeviceToHost);
    cudaMemcpy(
&time_used, time, sizeof(clock_t),
        cudaMemcpyDeviceToHost);
    cudaFree(gpudata);
    cudaFree(result);
    cudaFree(time);
 

  可以注意到我们在呼叫 sumOfSquares 函式时,指定 THREAD_NUM 为 thread 的数目。最后,在 CPU 端把计算好的各组数据的平方和进行加总:

    int final_sum = 0;
    
for(int i = 0; i < THREAD_NUM; i++) {
        final_sum
+= sum[i];
    }

    printf(
"sum: %d time: %d\n", final_sum, time_used);

    final_sum
= 0;
    
for(int i = 0; i < DATA_SIZE; i++) {
        sum
+= data[i] * data[i];
    }
    printf(
"sum (CPU): %d\n", final_sum);

  编译后执行,确认结果和原来相同。

  这个版本的程序,在 GeForce 8800GT 上执行,只需要约 8.3M cycles,比前一版程序快了 77 倍!这就是透过大量 thread 来隐藏 latency 所带来的效果。

  不过,如果计算一下它使用的内存带宽,就会发现其实仍不是很理想,大约只有 723MB/s 而已。这和 GeForce 8800GT 所具有的内存带宽是很大的差距。为什么会这样呢?

0
相关文章