CUDA直方图统计

直方图是非常重要的统计学概念,在深度学习、计算机视觉、数据科学和图像处理等领域中都有广泛应用。它代表了在给定的数据集中每个元素出现的频次。能展现哪些数据项出现的最频繁,哪些出现的最少。通过查看直方图数据,你能一眼就了解出数据的分布情况。本小节中,我们将实现对指定数据集(中的元素分布)进行直方图统计的算法。
我们先从开发CPU版本的直方图统计开始,以便你能大致了解如何计算它。假设我们有1000个元素,每个元素的值都在0和15之间,我们想计算具体的直方图分布情况,示例代码演示如下:
CUDA直方图统计_第1张图片
h_a数组中有1000个元素,每个元素是值在0到15之间的整数,这样一共有16种不同的值。所以直方图的统计项也是16个。这16个统计项分别代表了要这16种要计算的值的分布直方图。然后我们就(在程序里)定义了和实际直方图的统计项个数相同的( 16个元素)的数组,用来容纳最后的直方图统计结果。该数组需要初始化清空,因为每统计一个值,对应的元素位置就要增加1。具体操作是通过第一个for循环,遍历0到统计项数次,每次清除一个元素来完成的。然后为了统计这个直方图,我们需要遍历h_a中的所有1000个元素,为h_a中的每一个特定元素值,将对应的特定直方图数组索引处的结果值增加1。具体是通过第二个循环来完成的,遍历0到1000个该数组中的元素,为这1000个元素中的每个值,计算对应的直方图索引处的结果值。当第二个for循环结束后,直方图数组中就包含了每个值在0~15之间的这些元素各个值上出现的频次。

#include 
#include 

#define SIZE 1000
#define NUM_BIN 16

__global__ void histogram_without_atomic(int *d_b, int *d_a)
{
	int tid = threadIdx.x + blockDim.x * blockIdx.x;
	int item = d_a[tid];
	if (tid < SIZE)
	{
		d_b[item]++;
	}
}

__global__ void histogram_atomic(int *d_b, int *d_a)
{
	int tid = threadIdx.x + blockDim.x * blockIdx.x;
	int item = d_a[tid];
	if (tid < SIZE)
	{
		atomicAdd(&(d_b[item]), 1);
	}
}

int main()
{
	int h_a[SIZE];
	for (int i = 0; i < SIZE; i++) 
	{
		h_a[i] = i % NUM_BIN;
	}
	int h_b[NUM_BIN];
	for (int i = 0; i < NUM_BIN; i++) 
	{
		h_b[i] = 0;
	}

	// declare GPU memory pointers
	int * d_a;
	int * d_b;

	// allocate GPU memory
	cudaMalloc((void **)&d_a, SIZE * sizeof(int));
	cudaMalloc((void **)&d_b, NUM_BIN * sizeof(int));

	// transfer the arrays to the GPU
	cudaMemcpy(d_a, h_a, SIZE * sizeof(int), cudaMemcpyHostToDevice);
	cudaMemcpy(d_b, h_b, NUM_BIN * sizeof(int), cudaMemcpyHostToDevice);

	// launch the kernel
	//histogram_without_atomic << <((SIZE+NUM_BIN-1) / NUM_BIN), NUM_BIN >> >(d_b, d_a);
	histogram_atomic << <((SIZE+NUM_BIN-1) / NUM_BIN), NUM_BIN >> >(d_b, d_a);
		
	// copy back the sum from GPU
	cudaMemcpy(h_b, d_b, NUM_BIN * sizeof(int), cudaMemcpyDeviceToHost);
	printf("Histogram using 16 bin without shared Memory is: \n");
	for (int i = 0; i < NUM_BIN; i++)
	{
		printf("bin %d: count %d\n", i, h_b[i]);
	}

	// free GPU memory allocation
	cudaFree(d_a);
	cudaFree(d_b);
	return 0;
}

第一个函数是用最简单方式实现的直方图统计。每个线程读取1个元素值。使用线程ID作为输入数组的索引获取该元素的值。然后此值再将对应的d_b结果数组中的索引位置处进行+1操作。最后d_b数组应该包含输入数据中0到15之间每个值的频次。如果你回忆一下第3章中的内容,你会知道这不能给出正确结果,因为对相同的存储器位置将有大量的线程试图同时进行不安全地修改。在本例中,1000个线程试图同时在16个存储器位置上同时修改,这种场景我们就必须使用原子操作了。第二个内核函数用原子操作写的。
在main 函数的开头,我们分别定义了主机和设备上的数组,并给它们分配了内存和显存。在第一个for循环中,h_a数组用0到15之间的值进行初始化填充。我们这里用了取模运算,于是这1000个元素将被((基本)均匀地划分在0~15之间。然后我们将第二个存储直方图结果的数组,初始化清空。我们将这两个数组传输到显存,接着我们运行内核计算直方图,并将结果返回到主机上。我们将该直方图结果在控制台上打印出来。
当我们试图测量使用了原子操作的该代码的性能的时候,你会发现相比CPU的性能,对于很大规模的数组,GPU的实现更慢。这就引入了一个问题:我们真的应当使用CUDA进行直方图统计么?如果必须能否将这个计算结果更快一些?
这两个问题的答案都是:Yes。如果我们在一个块中用共享内存进行直方图统计,最后再将每个块的部分统计结果叠加到全局内存上的最终结果上去。这样就能加速该操作。这是因为整数加法满足交换律。我需要补充的是:只有当原始数据就在GPU的显存上的时候,才应当考虑使用GPU计算,否则完全不应当cudaMemcpy过来再计算,因为仅cudaMemcpy 的时间就将等于或者大于CPU计算的时间。用共享内存进行直方图统计的内核函数代码如下:

#include 
#include 

#define SIZE 1000
#define NUM_BIN 256

__global__ void histogram_shared_memory(int *d_b, int *d_a)
{
	int tid = threadIdx.x + blockDim.x * blockIdx.x;
	int offset = blockDim.x * gridDim.x;
	__shared__ int cache[256];
	cache[threadIdx.x] = 0;
	__syncthreads();
	
	while (tid < SIZE)
	{
		atomicAdd(&(cache[d_a[tid]]), 1);
		tid += offset;
	}
	__syncthreads();
	atomicAdd(&(d_b[threadIdx.x]), cache[threadIdx.x]);
}

int main()
{
	// generate the input array on the host
	int h_a[SIZE];
	for (int i = 0; i < SIZE; i++) 
	{
		//h_a[i] = bit_reverse(i, log2(SIZE));
		h_a[i] = i % NUM_BIN;
	}
	int h_b[NUM_BIN];
	for (int i = 0; i < NUM_BIN; i++) 
	{
		h_b[i] = 0;
	}

	// declare GPU memory pointers
	int * d_a;
	int * d_b;

	// allocate GPU memory
	cudaMalloc((void **)&d_a, SIZE * sizeof(int));
	cudaMalloc((void **)&d_b, NUM_BIN * sizeof(int));

	// transfer the arrays to the GPU
	cudaMemcpy(d_a, h_a, SIZE * sizeof(int), cudaMemcpyHostToDevice);
	cudaMemcpy(d_b, h_b, NUM_BIN * sizeof(int), cudaMemcpyHostToDevice);

	// launch the kernel
	histogram_shared_memory << <SIZE / 256, 256 >> >(d_b, d_a);

	// copy back the result from GPU
	cudaMemcpy(h_b, d_b, NUM_BIN * sizeof(int), cudaMemcpyDeviceToHost);
	printf("Histogram using 16 bin is: ");
	for (int i = 0; i < NUM_BIN; i++) 
	{
		printf("bin %d: count %d\n", i, h_b[i]);
	}

	// free GPU memory allocation
	cudaFree(d_a);
	cudaFree(d_b);
	return 0;
}

我们要为当前的每个块都统计一次局部结果,所以需要先将共享内存清空,然后用类似之前的方式在共享内存中进行直方图统计。这种情况下,每个块中只会统计部分结果存储在各自的共享内存中,并非像以前那样直接统计为在全局内存上的总体结果。本例中,块中的256个线程进行共享内存上的256个元素位置的访问,而原本的代码则在全局内存上的16个元素位置上进行访问。因为共享内存本身要比全局内存具有更高效的并行访问性能,同时将16个统一的竞争访问的位置放宽到了每个共享内存上的256个竞争位置,这两个因素共同缩小了原子操作累计统计的时间。最终还需要进行一次原子操作,将每个块的共享内存上的部分统计结果累加到全局内存上的最终统计结果。因为整数加法满足交换律,我们不需要担心具体每个块执行的顺序。main函数与前一个类似。

你可能感兴趣的:(CUDA,c语言,cuda)