简介

第9章引入了3个新的概念,在之前的章节和示例中都尽量避免这方面的内容。

  • 首先是Compute Capability. NVIDIA不断发布新的GPU,计算能力增大的同时,还伴随有新功能的添加。比如本章中要介绍的原子操作。Compute Capability 1.0不支持原子操作,1.1支持Global Memory原子操作,1.2支持Shared Momory原子操作。所以要使用某项功能时,一定要对运行的硬件环境心中有数。GPU型号的Compute Capability可以在NVIDIA的官网上查到。
  • 其次是原子操作。我们对于CPU上的原子操作不会陌生。当多个线程对某个资源同时操作时,要确保一个线程完成操作之后,才让另外的线程来操作。原子操作可以保证做到这一点。前面的示例,都尽量让每个线程操作对应的内存位置,避免冲突。
  • 直方图。直方图我们可以简单地理解为,将事物进行分类,然后统计各个分类中的数量。我们要确定好分成多少类,分类的标准是什么。本章的示例,是将生成的大量数据进行分类,数据长度为1个字节。然后将数据分成256类,每个数据的数值大小作为分类的标准。

直方图CPU示例

数据获取

直方图示例其实挺简单的。唯一麻烦一点的数据从哪里来。Cuda By Example 的随书代码实现了一个生成大块数据的函数big_random_block进一步简化我们的工作。

#include "../common/book.h"
#define SIZE (100*1024*1024)
int main( void ) {
 unsigned char *buffer = (unsigned char*)big_random_block( SIZE );

生成直方图

然后定义一个长度为256的无符号整型数组,对应直方图的256个区间,统计0~255在buffer出现次数。

unsigned int histo[256];
for (int i=0; i<256; i++)
    histo[i] = 0;

for (int i=0; i<SIZE; i++)
    histo[buffer[i]]++;

histo数组的各个元素被第一个for循环初始化为0,第二个for循环遍历buffer中的元素,buffer[i]的计数放在histo[buffer[i]]中。

验证结果

生成直方图的过程到此就结束了,再做一个简单的验证,将各个区间的计数求和,所得结果应该跟buffer的SIZE相同。

long histoCount = 0;
for (int i = 0; i<256; i++)
    histoCount += histo[i];
printf("Histogram Sum: %ld\n", histoCount);

测量性能

GPU的性能我们使用CUDA提供的CudaEvent来测量, Host端的性能测量要简单得多。标准C语言库提供了一个函数clock(),通过调用此函数,我们可以取得系统自开机以来,经历的时钟周期数。在不同时刻调用这个函数,所得返回值的差即为两个时刻相隔的时钟周期数量。时钟周期数量除以每秒时钟数,我们就得到了以秒为单位的时间间隔。生成直方图消耗的时间不会太多,所以取毫秒为单位,这样还需将所得结果乘以1000。

clock()返回的数据类型clock_t实际上由整型或者浮点数的typedef而来,可以直接使用减号来计算差值。

#include <timer.h>
clock_t start, stop;
start = clock();
...
stop = clock();
float elapsedTime = (float)(stop-start)/(float)CLOCKS_PER_SEC*1000.0f;
printf( "Time to generate:  %3.1f ms\n", elapsedTime );

完整代码

#include "../common/book.h"

#define SIZE    (100*1024*1024)

int main( void ) {
    unsigned char *buffer =
                     (unsigned char*)big_random_block( SIZE );

    // capture the start time
    clock_t         start, stop;
    start = clock();

    unsigned int    histo[256];
    for (int i=0; i<256; i++)
        histo[i] = 0;

    for (int i=0; i<SIZE; i++)
        histo[buffer[i]]++;

    stop = clock();
    float   elapsedTime = (float)(stop - start) /
                          (float)CLOCKS_PER_SEC * 1000.0f;
    printf( "Time to generate:  %3.1f ms\n", elapsedTime );

    long histoCount = 0;
    for (int i=0; i<256; i++) {
        histoCount += histo[i];
    }
    printf( "Histogram Sum:  %ld\n", histoCount );

    free( buffer );
    return 0;
}

直方图GPU示例(使用全局变量)

内核函数

使用GPU生成直方图,让不同的GPU线程,检查buffer的不同部分。这个前面的例子已经讲过很多次了。这个例子不同的地方是,把结果写回histo时,线程间可能同时访问某个histo区间,从而产生冲突。为了避免出现统计错误,线程必须使用原子操作来改变histo各个元素的值。CUDA C用于加法的原子操作为:atomicAdd(addr, y)。addr为要进行原子操作的变量的地址,y为要加入到变量的数值。例子中addr为histo里buffer[i] 的计数的地址,&histo[buffer[i]]。y为1,因为对于数值buffer[i],计数histo[buffer[i]]加1.

__global__ void histo_kernel(unsigned char* buffer, 
                            long size,
                            unsigned int* histo)
{
    int i = threadIdx.x + blockIdx.x + blockDim.x;
    int stride = blockDim.x * gridDim.x;
    while (i<size) {
        atomicAdd( &(histo[buffer[i]]), 1);
        i += stride;
    }
}

数据准备

正如之前例子所作的那样,需要分配GPU内存保存buffer数据,并将数据从Host内存拷贝到GPU内存。同时也需要给histogram分配内存,供内核线程存放统计数据。这里不要忘了将histo初始化为0. CUDA C的cudaMemset()起到跟标准C的memset类似的作用。例子使用cudaMemset初始化了histo。

unsigned char *dev_buffer;
unsigned int *dev_histo;

HANDLE_ERROR( cudaMalloc( (void**)&dev_buffer, SIZE ) );
HANDLE_ERROR( cudaMemcpy( dev_buffer, buffer, SIZE, 
                           cudaMemcpyHostToDevice ) );

HANDLE_ERROR( cudaMalloc( (void**)&dev_histo, 256 * sizeof(int) ) );
HANDLE_ERROR( cudaMemset( hist_histo, 0, 256 * sizeof(int) ) );

返回histo

初始化输入和输出内存后,就可以进行直方图的计算了。就像用malloc分配了内存后,马上把释放的代码也写好是个好习惯,这里在写出计算代码之前,我们把从GPU读回结果先介绍了。GPU计算的直方图结果将传递给Host的临时数组histo,用于后面对计算结果的验证。

unsigned int histo[256];
HANDLE_ERROR( cudaMemcpy( histo, dev_histo,
                          256 * sizeof( int ),
                          cudaMemcpyDeviceToHost ) );

验证结果

CPU版本里, 验证过程是计算各个区间的和,然后把和跟数据总数量做比较,如果相同则认为是准确的。GPU版本也可以使用同样的方法来验证,不过这里有更好的方法。以GPU计算所得的直方图为基础,在Host端重新遍历输入buffer,每次从对应的区间中减去1. 遍历完之后,histo的每个元素应该全部归为零值。

for (int i=0; i<SIZE; i++)
    histo[buffer[i]]--;
for (int i=0; i<256; i++) {
    if (histo[i]) != 0)
        printf("Failare at %d!\n", i);
}    

测量性能

跟之前一样,例子使用start和stop两个cuda Event来测量直方图计算所花费的时间。因为跟前面的例子完全一样,这里不再赘述了。具体的代码可以在后面的完整代码中找到。

确定blocks

将调用kernel函数放到最后面将是有原因的,因为选择不同的blocks对性能是由影响的。除了我们首先想到的每个block使用256个thread,从而计算得出block数量,我们还有其他的选择。实验证明,将block数量定义为GPU包含的处理器的2倍,性能是最好的。

我们还是使用每个block建立256个线程,block由GPU的prop来决定。cudaGetDeviceProperties函数返回GPU的属性,用cduaDeviceProp来描述。其中multiProcessorCount指定了GPU里处理器的数量。这样就得到了下面的调用代码:

cudaDeviceProp prop;
HANDLE_ERROR( cudaGetDeviceProperties( &prop, 0 ) );
int blocks = prop.multiProcessorCount;
histo_kernel<<<blocks*2, 256>>>(dev_buffer, SIZE, dev_histo ));

完整代码

#include "../common/book.h"

#define SIZE    (100*1024*1024)


__global__ void histo_kernel( unsigned char *buffer,
                              long size,
                              unsigned int *histo ) {
    // calculate the starting index and the offset to the next
    // block that each thread will be processing
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    int stride = blockDim.x * gridDim.x;
    while (i < size) {
        atomicAdd( &histo[buffer[i]], 1 );
        i += stride;
    }
}

int main( void ) {
    unsigned char *buffer =
                     (unsigned char*)big_random_block( SIZE );

    // capture the start time
    // starting the timer here so that we include the cost of
    // all of the operations on the GPU.
    cudaEvent_t     start, stop;
    HANDLE_ERROR( cudaEventCreate( &start ) );
    HANDLE_ERROR( cudaEventCreate( &stop ) );
    HANDLE_ERROR( cudaEventRecord( start, 0 ) );

    // allocate memory on the GPU for the file's data
    unsigned char *dev_buffer;
    unsigned int *dev_histo;
    HANDLE_ERROR( cudaMalloc( (void**)&dev_buffer, SIZE ) );
    HANDLE_ERROR( cudaMemcpy( dev_buffer, buffer, SIZE,
                              cudaMemcpyHostToDevice ) );

    HANDLE_ERROR( cudaMalloc( (void**)&dev_histo,
                              256 * sizeof( int ) ) );
    HANDLE_ERROR( cudaMemset( dev_histo, 0,
                              256 * sizeof( int ) ) );

    // kernel launch - 2x the number of mps gave best timing
    cudaDeviceProp  prop;
    HANDLE_ERROR( cudaGetDeviceProperties( &prop, 0 ) );
    int blocks = prop.multiProcessorCount;
    histo_kernel<<<blocks*2,256>>>( dev_buffer, SIZE, dev_histo );
    
    unsigned int    histo[256];
    HANDLE_ERROR( cudaMemcpy( histo, dev_histo,
                              256 * sizeof( int ),
                              cudaMemcpyDeviceToHost ) );

    // get stop time, and display the timing results
    HANDLE_ERROR( cudaEventRecord( stop, 0 ) );
    HANDLE_ERROR( cudaEventSynchronize( stop ) );
    float   elapsedTime;
    HANDLE_ERROR( cudaEventElapsedTime( &elapsedTime,
                                        start, stop ) );
    printf( "Time to generate:  %3.1f ms\n", elapsedTime );

    long histoCount = 0;
    for (int i=0; i<256; i++) {
        histoCount += histo[i];
    }
    printf( "Histogram Sum:  %ld\n", histoCount );

    // verify that we have the same counts via CPU
    for (int i=0; i<SIZE; i++)
        histo[buffer[i]]--;
    for (int i=0; i<256; i++) {
        if (histo[i] != 0)
            printf( "Failure at %d!  Off by %d\n", i, histo[i] );
    }

    HANDLE_ERROR( cudaEventDestroy( start ) );
    HANDLE_ERROR( cudaEventDestroy( stop ) );
    cudaFree( dev_histo );
    cudaFree( dev_buffer );
    free( buffer );
    return 0;
}

性能测量得出的时间相比CPU版本可能不尽人意. 因为dev_histo是全局变量,计算的时候多个线程会访问dev_histo的同一个元素. 比如对于&dev_histo[1], 所有线程,无论属于哪个block, 如果它从dev_buffer中读到数据为1, 那么都要来排队访问&dev_histo[1].内存访问极大影响了性能.通过前面的学习,我们知道共享内存可以很好解决这一问题,因为共享内存不仅访问速度比全局内存快得多,而且只有同一个block的线程来访问, 比起所有的线程,竞争小多了. 只是最后需要把所有block的计数加在一起,不过这不算什么问题.

直方图GPU示例(使用共享内存)

内核函数

使用共享内存,只需要修改内核函数即可, 其他的地方不需要动.  不过需要注意,  每个block的线程数需要设定为256个,因为在下面的内核函数中, 共享内存的初始化和将计算所得结果加到dev_histo中去, 都是一个thread负责一个区间. 如果小于256则会漏掉一些区间, 如果大于256则会导致访问越界.

__global__ void histo_kernel( unsigned char* dev_buffer, 
                              long size,
                              unsigned int *dev_histo)
{
    __shared__ unsigned int temp[256];
    temp[threadIdx.x] = 0;
    __syncthreads();

    int i = threadIdx.x + blockIdx.x * blockDim.x;
    int stride = blockDim.x * gridDim.x;

    while (i < size ) {
        atomicAdd( &temp[dev_buffer[i]], 1);
        i += stride;
    }
    __syncthreads();
    
    atomicAdd(&dev_histo[threadIdx.x], temp[threadIdx.x]);
}
                              

这个函数中间的部分,跟全局版本差不多,只是计数结果不在直接放到dev_histo中,而是放入定义在函数内部的共享内存中.

开头部分申请了共享内存 temp.  对共享内存的初始化, 每个线程负责一个位置, 由线程在block中索引作为数组的索引. 而之后的__syncthreads()则是为了保证每个线程都运行了temp[threadIdx.x]=0, 将整个数组的每个元素清零后, 才开始后面运算.

第二个__syncthreads()确保了block中所有线程都完成了计算, temp得到最终值后,才加到dev_histo中去.同样每个线程负责一个区间的叠加.

完整代码

虽然改动不多,但是还是贴上完整的代码,毕竟说得再多,都不如代码直接.

#include "../common/book.h"

#define SIZE    (100*1024*1024)


__global__ void histo_kernel( unsigned char *buffer,
                              long size,
                              unsigned int *histo ) {

    // clear out the accumulation buffer called temp
    // since we are launched with 256 threads, it is easy
    // to clear that memory with one write per thread
    __shared__  unsigned int temp[256];
    temp[threadIdx.x] = 0;
    __syncthreads();

    // calculate the starting index and the offset to the next
    // block that each thread will be processing
    int i = threadIdx.x + blockIdx.x * blockDim.x;
    int stride = blockDim.x * gridDim.x;
    while (i < size) {
        atomicAdd( &temp[buffer[i]], 1 );
        i += stride;
    }
    // sync the data from the above writes to shared memory
    // then add the shared memory values to the values from
    // the other thread blocks using global memory
    // atomic adds
    // same as before, since we have 256 threads, updating the
    // global histogram is just one write per thread!
    __syncthreads();
    atomicAdd( &(histo[threadIdx.x]), temp[threadIdx.x] );
}

int main( void ) {
    unsigned char *buffer =
                     (unsigned char*)big_random_block( SIZE );

    // capture the start time
    // starting the timer here so that we include the cost of
    // all of the operations on the GPU.  if the data were
    // already on the GPU and we just timed the kernel
    // the timing would drop from 74 ms to 15 ms.  Very fast.
    cudaEvent_t     start, stop;
    HANDLE_ERROR( cudaEventCreate( &start ) );
    HANDLE_ERROR( cudaEventCreate( &stop ) );
    HANDLE_ERROR( cudaEventRecord( start, 0 ) );

    // allocate memory on the GPU for the file's data
    unsigned char *dev_buffer;
    unsigned int *dev_histo;
    HANDLE_ERROR( cudaMalloc( (void**)&dev_buffer, SIZE ) );
    HANDLE_ERROR( cudaMemcpy( dev_buffer, buffer, SIZE,
                              cudaMemcpyHostToDevice ) );

    HANDLE_ERROR( cudaMalloc( (void**)&dev_histo,
                              256 * sizeof( int ) ) );
    HANDLE_ERROR( cudaMemset( dev_histo, 0,
                              256 * sizeof( int ) ) );

    // kernel launch - 2x the number of mps gave best timing
    cudaDeviceProp  prop;
    HANDLE_ERROR( cudaGetDeviceProperties( &prop, 0 ) );
    int blocks = prop.multiProcessorCount;
    histo_kernel<<<blocks*2,256>>>( dev_buffer,
                                    SIZE, dev_histo );
    
    unsigned int    histo[256];
    HANDLE_ERROR( cudaMemcpy( histo, dev_histo,
                              256 * sizeof( int ),
                              cudaMemcpyDeviceToHost ) );

    // get stop time, and display the timing results
    HANDLE_ERROR( cudaEventRecord( stop, 0 ) );
    HANDLE_ERROR( cudaEventSynchronize( stop ) );
    float   elapsedTime;
    HANDLE_ERROR( cudaEventElapsedTime( &elapsedTime,
                                        start, stop ) );
    printf( "Time to generate:  %3.1f ms\n", elapsedTime );

    long histoCount = 0;
    for (int i=0; i<256; i++) {
        histoCount += histo[i];
    }
    printf( "Histogram Sum:  %ld\n", histoCount );

    // verify that we have the same counts via CPU
    for (int i=0; i<SIZE; i++)
        histo[buffer[i]]--;
    for (int i=0; i<256; i++) {
        if (histo[i] != 0)
            printf( "Failure at %d!\n", i );
    }

    HANDLE_ERROR( cudaEventDestroy( start ) );
    HANDLE_ERROR( cudaEventDestroy( stop ) );
    cudaFree( dev_histo );
    cudaFree( dev_buffer );
    free( buffer );
    return 0;
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部