以太工坊

以太工坊

以太工坊

实验目的

  • 掌握CUDA网格和线程块的设计
  • 实践CUDA运行时API的使用
  • 通过编写核函数,掌握利用GPU众核对大规模问题的求解加速的方法
  • 体会CPU多核与GPU众核这两种不同体系结构带来的程序设计上的差异

实验内容

矩阵转置

算法流程

  1. 将需要转置的矩阵存储到GPU内存中。
  2. 在GPU上分配空间存储转置后的矩阵。
  3. 定义CUDA核函数来实现矩阵转置。该核函数应该使用线程块和线程格的概念来处理矩阵中的所有元素。在每个线程块中,线程可以使用共享内存来处理数据。最后,利用全局内存将结果写回到GPU。
  4. 调用CUDA核函数以执行矩阵转置。
  5. 将转置后的矩阵从GPU内存复制到主机内存中。
  6. 释放GPU内存

代码实现

在传统代码的基础上,利用共享内存优化访问全局内存的方式,同时使用padding操作,避免bank冲突。

const int N = 10000;
const int TILE_DIM = 32;
const int grid_size_x = (N + TILE_DIM - 1) / TILE_DIM;
const int grid_size_y = grid_size_x;
const dim3 block_size(TILE_DIM, TILE_DIM);
const dim3 grid_size(grid_size_x, grid_size_y);
__global__ void transpose(const float *A, float *B, const int N){  
    __shared__ float S[TILE_DIM][TILE_DIM+1];
    int bx = blockIdx.x * TILE_DIM;
    int by = blockIdx.y * TILE_DIM;
    //顺序读
    int nx1 = bx + threadIdx.x;
    int ny1 = by + threadIdx.y;
    if (nx1 < N && ny1 < N)
        S[threadIdx.y][threadIdx.x] = A[ny1 * N + nx1];
    __syncthreads();
    //顺序写
    int nx2 = bx + threadIdx.y;
    int ny2 = by + threadIdx.x;
    if (nx2 < N && ny2 < N)
        B[nx2 * N + ny2] = S[threadIdx.x][threadIdx.y];
}

int main(){  
    int nn = N * N;
    int nBytes = nn * sizeof(float);
    float a = 1.1;
    float b = 2.2;
    //cpu空间分配
    float* A = (float*)malloc(nBytes);
    float* B = (float*)malloc(nBytes);
    //矩阵初始化
    for (int i = 1; i <= nn; i++) {
        if (i % 2 == 0)
            A[i-1] = a;
        else
            A[i-1] = b;
    }
    //gpu空间分配(需要先定义)
    float* d_A, * d_B;
    cudaMalloc((void**)&d_A, nBytes);
    cudaMalloc((void**)&d_B, nBytes);
    cudaMemcpy(d_A, A, nBytes, cudaMemcpyHostToDevice);
    transpose<<<grid_size, block_size >>> (d_A, d_B, N);
    cudaMemcpy(B, d_B, nBytes, cudaMemcpyDeviceToHost);
    free(A);
    free(B);
    cudaFree(d_A);
    cudaFree(d_B);
    return 0;
}

矩阵相乘

核函数设计

共享内存的使用:应该将两个原始矩阵的一部分抽取出来,当作瓦片,放入共享内存,减少对全局内存的访问,从而提高程序性能。
非方阵处理:非方阵的两个矩阵相乘,需要调整共享内存中瓦片的尺寸,同时需要添加判断语句,防止发生越界错误。

代码实现

__global__ void matrixmul(const float *A, const float *B, float *C,const int N,const int M,const int K){   //N*M x M*K
    __shared__ float A_S[TILE_DIM][TILE_DIM];
    __shared__ float B_S[TILE_DIM][TILE_DIM];
    int tx = threadIdx.x;
    int ty = threadIdx.y;
    int bx = blockIdx.x;
    int by = blockIdx.y;
    int row = by * TILE_DIM + ty;
    int col = bx * TILE_DIM + tx;
    float value = 0;
    for (int ph = 0; ph < M / TILE_DIM+1; ph++) {
        if (row < N && ph * TILE_DIM + tx < M)
            A_S[ty][tx] = A[row * M + ph * TILE_DIM + tx];
        else
            A_S[ty][tx] = 0.0;
        if (col < K && ph * TILE_DIM + ty < M) 
            B_S[ty][tx] = B[(ph * TILE_DIM + ty) * K + col];
        else
            B_S[ty][tx] = 0.0;
        __syncthreads();

        for (int k = 0; k < TILE_DIM; k++)
            value += A_S[ty][k] * B_S[k][tx];
        __syncthreads();
    }

    if (row < N && col < K)
        C[row*K+col]=value;
}

统计直方图

共享内存的使用:在共享内存开辟临时数组,减少对全局内存的访问,从而提高程序性能。
跨网格循环:数据量很大,一个线程要处理多个数据。
原子操作:无论是共享内存还是全局内存的结果,都需要使用原子操作,避免冲突

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

规约求和

跨网格循环:对于海量数据(>100w),即使是GPU众核,也做不到这么多的线程,因此一个线程要处理多个数据。
共享内存优化:将每个线程对多个元素求和的结果存于共享内存,可以减少对全局内存的访问。
交错配对法:利用交错配对法进行规约,可以缓解一部分的线程束分化现象。
原子操作:将结果累加到全局内存中,可能会发生冲突,需要使用原子操作。

__global__ void _sum_gpu(int *input, int count, int *output)
{
    __shared__ int sum_per_block[BLOCK_SIZE];

    int temp = 0;
    for (int idx = threadIdx.x + blockDim.x * blockIdx.x;
         idx < count; idx += gridDim.x * blockDim.x
	)
        temp += input[idx];
    sum_per_block[threadIdx.x] = temp;
    __syncthreads();

    //**********shared memory summation stage***********
    for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2)
    {
        int double_kill = -1;
	if (threadIdx.x < length)
	    double_kill = sum_per_block[threadIdx.x] + sum_per_block[threadIdx.x + length];
	__syncthreads();  //why we need two __syncthreads() here, and,
	
	if (threadIdx.x < length)
	    sum_per_block[threadIdx.x] = double_kill;
	__syncthreads(); 
    } //the per-block partial sum is sum_per_block[0]

    if (threadIdx.x == 0) atomicAdd(output, sum_per_block[0]);

TOP K 问题

TOP K问题是指在一组数据中,找到前K个最大或最小的元素。利用CUDA规约计算可以高效地解决TOP K问题。
以下是利用CUDA规约计算来实现排序和选择前K个最大/最小元素的详细步骤:

  1. 定义一个二元组类型,包含值和索引,用于存储原始数据及其索引(为了在排序后恢复原始数据)
  2. 对原始数据进行遍历,将数据存储到二元组中
  3. 对二元组进行归约操作,得到前K个最大/最小值的索引
  4. 恢复原始数据并按照索引排序,得到前K个最大/最小值

具体实现流程如下:

  1. 将数据复制到GPU显存中
    float *d_data; cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
  2. 将数据存储到二元组中
    typedef struct {     float value;     int index; } Tuple;  
    Tuple *d_tuples; 
    int threadsPerBlock = 256; 
    int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
    initializeTuples<<<blocksPerGrid, threadsPerBlock>>>(d_data, d_tuples, n);
    ```   
    3.  对二元组进行归约操作,得到前K个最大/最小值的索引
    ```cpp
    int *d_indices;
    kReduceKernel<<<blocksPerGrid, threadsPerBlock>>>(d_tuples, d_indices, n, k);
    __global__ void kReduceKernel(Tuple *input, int *output, int n, int k) {
        extern __shared__ Tuple shared[];
        int tid = threadIdx.x;
        int i = blockIdx.x * blockDim.x + threadIdx.x;
        shared[tid] = (i < n) ? input[i] : Tuple{0, 0};
        __syncthreads();
        for (int s = blockDim.x / 2; s > 0; s >>= 1) {
            if (tid < s)
                shared[tid] = (shared[tid].value > shared[tid + s].value) ? shared[tid] : shared[tid + s];
            __syncthreads();
        }
    
        if (tid == 0)
            output[blockIdx.x] = shared[0].index;
    }
  3. 在CPU中恢复原始数据并按照索引排序,得到前K个最大/最小值
    cudaMemcpy(h_indices, d_indices, size, cudaMemcpyDeviceToHost);  
    for (int i = 0; i < k; ++i) {     
        int index = h_indices[i];     
        h_result[i] = h_data[index]; }  
    std::sort(h_result, h_result + k);
    完成以上步骤后,就可以得到排序后的前K个最大/最小值了。

实验感受

主要参考

贝叶斯公式

贝叶斯学派认为没有什么是随机的,如果有,那一定是信息不够(香农信息论);
贝叶斯学派(统计学)-> 贝叶斯学习(机器学习)

贝叶斯公式给了我们一种能力,即在事件发生后,通过事件发生前的各种概率进行推理的能力。

无意中使用贝叶斯的例子:一个笑话——水是剧毒的,因为罹患癌症的人都喝过水。
无意中被贝叶斯欺骗的例子:检出率很高的诊疗方法(准确率99.9%),误诊率是极高的(>50%)。因为自然人群中患病率(<1%)。
概率论统计学真是任人装扮的小姑娘。


从机器学习的角度来理解贝叶斯公式(朴素贝叶斯分类器)
读作P c given x, 左侧是后验概率,是先验概率prior, 是似然值(likelihood),是模型重点学习的部分。对于所有的输入样本都是一样的,是用来归一化的(计算时用全概率公式展开);对的估计可以采用极大似然估计Maximum Likelihood Estimation的方法。(西瓜书P148)
从一般的角度来理解(可能不太准确)
是一件事的原始概率,当发生了一些事之后(或是我们知道它发生了,这就跑到贝叶斯学派和频率学派的分歧点了),是被修正的概率,修正因子就是

太深了,浅看就一公式,深层竟然是世界观方法论,越看越迷糊

FAQ

  • 什么是先验概率?
    事情未发生,只根据以往数据统计,分析事情发生的可能性,即先验概率。
    指根据以往经验和分析。在实验或采样前就可以得到的概率。
    先验概率是指根据以往经验和分析得到的概率,如全概率公式,它往往作为”由因求果”问题中的”因”出现。

  • 什么是后验概率?与先验概率的关系?

  1. 后验概率
    事情已发生,已有结果,求引起这事发生的因素的可能性,由果求因,即后验概率。
    指某件事已经发生,想要计算这件事发生的原因是由某个因素引起的概率。
    后验概率是指依据得到”结果”信息所计算出的最有可能是那种事件发生,如贝叶斯公式中的,是”执果寻因”问题中的”因”。
  2. 与先验概率的关系
    后验概率的计算,是以先验概率为前提条件的。如果只知道事情结果,而不知道先验概率(没有以往数据统计),是无法计算后验概率的。
    后验概率的计算需要应用到贝叶斯公式。
  • 全概率公式、贝叶斯公式与先验、后验概率的关系?
    全概率公式,总结几种因素,事情发生的概率的并集。由因求果。
    贝叶斯公式,事情已经发生,计算引起结果的各因素的概率,由果寻因。同后验概率。
    全概率是用原因推结果,贝叶斯是用结果推原因
0%