《CUDA C编程权威指南》学习笔记 chap1-3

《CUDA C编程权威指南》示例代码下载

第二章 CUDA编程模型

2.1 CUDA编程模型概述

内存管理

  1. GPU内存分配:cudaMalloc
cudaError_t cudaMalloc(void** devPtr, size_t size)

在这里插入图片描述

该函数负责向设备分配一定字节的线性内存,并以devPtr的形式返回指向所分配内存 的指针。cudaMalloc与标准C语言中的malloc函数几乎一样,只是此函数在GPU的内存里分 配内存。通过充分保持与标准C语言运行库中的接口一致性,可以实现CUDA应用程序的 轻松接入。
2. 主机和设备之间的数据传输:cudaMemcpy

cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)

此函数从src指向的源存储区复制一定数量的字节到dst指向的目标存储区。复制方向 由kind指定。

  • kind值
    在这里插入图片描述
  • 返回值
    GPU分配内存成功–cudaSuccess
    失败–cudaErrorMemoryALlocation
    可以使用以下CUDA运行时函数将错误代码转化为可读的错误消息:
char* cudaGetErrorString(cudaError_t error)
  1. 内存层次结构
    在这里插入图片描述

线程管理

由一个内核启动所产生的所有线程统称为一个网格。同一网格中的所有线程共享相同 的全局内存空间。一个网格由多个线程块构成,一个线程块包含一组线程。同一个块中的线程之间可以相互协作,不同块内的线程不能协作。
在这里插入图片描述
线程格和线程块均使用3个dim3类型的无符号整型字段,而未使用的字段将被初始化 为1且忽略不计
threadIdx blockIdx blockDim gridDim

编写核函数

  1. 启动核函数
hernel_name <<<grid, block>>>(argument list);

执行配置的第一个值是网格维度,也就是启动块的数目。第二 个值是块维度,也就是每个块中线程的数目。通过指定网格和块的维度
2. 异步控制
核函数的调用和主线程是异步的,可以使用如下函数强制主机端程序等待所有的核函数执行结束:

cudaError_t cudaDeviceSynchronize(void);

cudaMemcpy函数式隐式同步的,使用该函数在主机和设备之间拷贝数据时,主机端隐式同步,即主机端程序必须等待数据拷贝完成后才能继续执行程序。

cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
  1. 编写核函数
    返回值必须为void
__global__ void kernel_name(argument list);

在这里插入图片描述
__device__和__host__限定符可以一齐使用,这样函数可以同时在主机和设备端进行编译。

核函数的限制
  • 只能访问设备内存
  • 必须具有void返回类型
  • 不支持可变数量的参数
  • 不支持静态变量
  • 显式异步行为
__global__ void sumArraysOnGPU(float* A< float* B, float* C){
	int i = threadIdx.x;
	C{i] = A[i] + B[i];
}
  1. 处理错误
    使用宏
#define CHECK(call)                 \
{                                   \
    const cudaError_t error = call; \
    if(error != cudaSuccess){       \
        printf("Error: %s:%d, ", __FILE__, __LINE__);                       \
        printf("code:%d, reason:%s\n", error, cudaGerErrorString(error));   \
        exit(1);                    \
    }                               \
}

使用宏

// 1
CHECK(cudaMemcpy(d_C,gpuRef, nBytes, cudaMemcpyHostToDevice));
// 2
kernel_function<<<grid, block>>>(argument list);
CHECK(cudaDeciveSynchronize());
  1. 示例代码
    NVIDIA提供了一个名为nvprof的命令行分析工具,可以帮助从应 用程序的CPU和GPU活动情况中获取时间线信息,其包括内核执行、内存传输以及CUDA API的调用。其用法如下。
nvprof [nvprof_args] <application> [application_args] 	## 具体用法
nvprof --help										 	## 获取nvprof帮助信息
nvprof ./sumArraysOnGPU									## 测试内核
#include <cuda_runtime.h>
#include <stdio.h>

#define CHECK(call)                 \
{                                   \
    const cudaError_t error = call; \
    if(error != cudaSuccess){       \
        printf("Error: %s:%d, ", __FILE__, __LINE__);                       \
        printf("code:%d, reason:%s\n", error, cudaGerErrorString(error));   \
        exit(1);                    \
    }                               \
}
// 验证核函数
void checkResult(float* hostRef, float* gpuRef, const int N){
    double epsilon = 1.0E-8;
    int match = 1;
    for(int i = 0; i < N; i++){
        if(abs(hostRef[i] - gpuRef[i]) > epsilon){
            match = 0;
            printf("Arrays do not match!\n");
            printf("host %5.2f gpu %5.2f at current %d\n", hostRef[i], gpuRef[i], i);
            break;
        }
    }
    if(match) printf("Arrays match.\n");
}

void initialData(float* ip, int size){
    time_t t;
    srand((unsigned int) time(&t));
    for(int i = 0; i < size; i++){
        ip[i] = (float)(rand() & 0xff) / 10.0f;
    }
}

void sumArraysOnHost(float* A, float* B, float* C, const int N){
    for(int i = 0; i < N; i++){
        C[i] = A[i] + B[i];
    }
}
__global__ void sumArraysOnGPU(float* A, float* B, float* C){
    int i = threadIdx.x;
    C[i] = A[i] + B[i];
}

int main(int argc, char** argv){
    printf("%s Starting...\n", argv[0]);
    // set up device
    int dev = 0;
    cudaSetDevice(dev);

    //set up data size of vectors
    int nElem = 32;
    printf("Vector size %d\n", nElem);

    // malloc host mmeory
    size_t nBytes = nElem * sizeof(float);
    float* h_A, *h_B, *hostRef, *gpuRef;
    h_A     = (float*)malloc(nBytes);
    h_B     = (float*)malloc(nBytes);
    hostRef = (float*)malloc(nBytes);
    gpuRef  = (float*)malloc(nBytes);

    // init data at host side
    initialData(h_A, nElem);
    initialData(h_B, nElem);

    // memset函数功能:为内存填充某个特定的值
    memset(hostRef, 0, nBytes);
    memset(gpuRef , 0, nBytes);

    // malloc device global memory
    float* d_A, *d_B, *d_C;
    cudaMalloc((float**)&d_A, nBytes);
    cudaMalloc((float**)&d_B, nBytes);
    cudaMalloc((float**)&d_C, nBytes);
    
    // transfer data from host to device
    cudaMemcpy(d_A, h_A, nBytes, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, h_B, nBytes, cudaMemcpyHostToDevice);

    // invoke kernel at host side
    // 放入一个块内, 一个块有32个线程
    dim3 block(nElem);
    dim3 grid(nElem/block.x);
    sumArraysOnGPU<<<grid, block>>>(d_A, d_B, d_C);
    printf("Execution configuration<<%d, %d>>>\n", grid.x, block.x);

    // copy kernel result back to host side
    cudaMemcpy(gpuRef, d_C, nBytes, cudaMemcpyDeviceToHost);

    // add vector at host side for result checks
    sumArraysOnHost(h_A, h_B, hostRef, nElem);

    // check device results
    checkResult(hostRef, gpuRef, nElem);

    //free
    cudaFree(d_A);
    cudaFree(d_B);
    cudaFree(d_C);
    
    free(h_A);
    free(h_B);
    free(gpuRef);
    free(hostRef);

    return 0;
}

2.2 核函数计时

  • cpu计时
double time = cpuSecond();
  • nvprof工具计时
    在这里插入图片描述在这个例子中,主机和设备之间的数据传输需要的时间比内核执行的时间要多
    在第6章中,你将会学习如何使用CUDA流和事件来压缩 计算量和通信量。

2.3 组织并行线程

矩阵加法
·由二维线程块构成的二维网格
·由一维线程块构成的一维网格
·由一维线程块构成的二维网格
管理3种索引
·线程和块索引
·矩阵中给定点的坐标
·全局线性内存中的偏移量
对于一个给定的线程,首先可以通过把线程和块索引映射到矩阵坐标上来获取线程块 和线程索引的全局内存偏移量,然后将这些矩阵坐标映射到全局内存的存储单元中。
第一步,可以用以下公式把线程和块索引映射到矩阵坐标上:
在这里插入图片描述
第二步,可以用以下公式把矩阵坐标映射到全局内存中的索引/存储单元上:
在这里插入图片描述
在这里插入图片描述
·改变执行配置对内核性能有影响
·传统的核函数实现一般不能获得最佳性能
·对于一个给定的核函数,尝试使用不同的网格和线程块大小可以获得更好的性能

2.4 设备管理

  1. 选择最优GPU(如果有多个)
    过比较GPU包含的多处理器的数量选出计算能力最佳的GPU
    在这里插入图片描述

  2. CUDA运行时API函数

cudaError_t cudaGetDeviceProperties(cudaDeviceProp* prop, int device);

cudaDeviecProp结构体返回GPU设备属性
在这里插入图片描述

  1. NVIDIA系统管理界面
nvidia-smi -q -i 0

-i,–id= Target a specific GPU
-L, --list-gpus Display a list of GPUs connected to the system
-q, --query display GPU or Unit info
-d,–display= Display only selected information: MEMORY UTILIZATION ECC TEMPERATURE POWER CLOCK COMPUTE PIDS PREEREORMANCE SUPPORTED_CLOCKS PAGE_RETIREMENT ACCOUNTING…
内存 设备。。。

运行时设置设备: : CUDA_VISIBLE_DEVICES=*
如果想测试多个设备**=2,3**,nvidia驱动程序将只使用 ID为2和3的设备,并且会将设备ID分别映射为0和1

第3章 CUDA执行模型

3.1 CUDA执行模型概述

1. 概述

在这里插入图片描述
grid block SM 都有啥区别和联系 线程束 线程块 为啥所有线程束要执行相同指令
找到了一个不错的解答:CUDA中grid、block、thread、warp与SM、SP的关系
GPU中的每一个流式处理器(SM)都能支持数百个线程并发执行,每个GPU通常有多个SM
CUDA采用单指令多线程(SIMT)架构来管理和执行线程,每32个线程为一组,被称为线程束(warp)。线程束中的所有线程同时执行相同的指令,每个SM都将分配给它的线程块划分到包含32个线程的线程束中,然后在可用的硬件资源上调度执行
一个线程块只能在一个SM上被调度。一旦线程块在一个SM上被调度,就会保存在该 SM上直到执行完成。在同一时间,一个SM可以容纳多个线程块。
在这里插入图片描述

2. Fermi架构

在这里插入图片描述Fermi的特征是多达512个加速器核心,这被称为CUDA核心。每个CUDA 核心都有一个全流水线的整数算术逻辑单元(ALU)和一个浮点运算单元(FPU),在这 里每个时钟周期执行一个整数或是浮点数指令。CUDA核心被组织到16个SM中,每一个 SM含有32个CUDA核心。Fermi架构有6个384位的GDDR5 DRAM存储器接口,支持多达 6GB的全局机载内存,这是许多应用程序关键的计算资源。主机接口通过PCIe总线将GPU 与CPU相连。GigaThread引擎(图示左侧第三部分)是一个全局调度器,用来分配线程块 到SM线程束调度器上。

3. Kepler架构

包含了15个SM 和6个64位的内存控制器
在这里插入图片描述
每个Kepler SM单元包含192个单精度CUDA核心,64个双精度单元,32 个特殊功能单元(SFU)以及32个加载/存储单元(LD/ST)
在这里插入图片描述
动态并行是Kepler GPU的一个新特性,它允许GPU动态启动新的网格。有了这个特 点,任一内核都能启动其他的内核,并且管理任何核间需要的依赖关系来正确地执行附加 的工作。
Hyper-Q技术增加了更多的CPU和GPU之间的同步硬件连接,以确保CPU核心能够在 GPU上同时运行更多的任务。当使用Kepler GPU时,既可以增加GPU的利用率,也 可以减少CPU的闲置时间。

4. 配置文件驱动优化

CUDA提供了两 个主要的性能分析工具:nvvp,独立的可视化分析器;nvprof,命令行分析器

  • nvvp是可视化分析器,它可以可视化并优化CUDA程序的性能。这个工具会显示CPU 与GPU上的程序活动的时间表,从而找到可以改善性能的机会。此外,nvvp可以分析应用 程序潜在的性能瓶颈,并给出建议以消除或减少这些瓶颈。该工具既可作为一个独立的应 用程序,也可作为Nsight Eclipse Edition(nsight)的一部分。 nvprof在命令行上收集和显示分析数据。
  • nvprof是和CUDA 5一起发布的,它是从一个 旧的命令行CUDA分析工具进化而来的。跟nvvp一样,它可以获得CPU与GPU上CUDA关 联活动的时间表,其中包括内核执行、内存传输和CUDA的API调用。它也可以获得硬件 计数器和CUDA内核的性能指标。

有3种常见的限制内核性能的因素: ·存储带宽 ·计算资源 ·指令和内存延迟

3.2 线程束执行的本质

1. 线程束和线程块

线程束是SM中基本的执行单元。当一个线程块的网格被启动后,网格中的线程块分 布在SM中。一旦线程块被调度到一个SM上,线程块中的线程会被进一步划分为线程束。 一个线程束由32个连续的线程组成,在一个线程束中,所有的线程按照单指令多线程 (SIMT)方式执行;也就是说,所有线程都执行相同的指令,每个线程在私有数据上进 行操作。

从逻辑角度来看,线程块是线程的集合,它们可以被组织为一维、二维或三维布局。 从硬件角度来看,线程块是一维线程束的集合。在线程块中线程被组织成一维布局, 每32个连续线程组成一个线程束。
在这里插入图片描述

2. 线程束分化
// 不同线程cond值可能不同,导致执行不同分支指令
if(cond){...}
else{...}

在同一线程束中的线程执行不同的指令,被称为线程束分化。
如果一个线程束中的线程产生分化,线程束将连续执行每一个分支路径,而禁用不执行这一路径的线程。线程束分化会导致性能明显地下降。条件分支越多,并行性削弱越严重。在不同的线程束中,不同的条件值不会 引起线程束分化。
为了获得最佳的性能,应该避免在同一线程束中有不同的执行路径。

3. 资源分配

程序计数器 寄存器 共享内存
计算资源(如寄存器和共享内存)已分配给线程块时,线程块被称为活跃的块。它所包含的线程束被称为活跃的线程束。活跃的线程束可以进一步被分为以下3种类型:

类型解释
选定的线程束活跃执行的线程束被称为选定的线程束。
阻塞的线程束如果一个线程束没有做好执行的准备,它是一个阻塞的 线程束。
符合条件的线程束如果一个活跃的线程束准备执行但尚未执行,它是一个符合条件的线程束。

如果同时满足以下两个条件则线程束符合执行条件。
·32个CUDA核心可用于执行 ·当前指令中所有的参数都已就绪

4.延迟隐藏

在指令发出和完成之间的时钟周期被定义为指令延迟,GPU的指令延迟被其他线程束的计算隐藏。指令可分为算术指令内存指令
估算隐藏延迟需要的活跃的线程束的数量:所需线程束数量=延迟×吞吐量(没懂

5.占用率

在这里插入图片描述

6.同步

两个执行级别:
·系统级:等待主机和设备完成所有的工作
·块级:在设备执行过程中等待一个线程块中所有线程到达同一点
对于主机来说,由于许多CUDA API调用和所有的内核启动不是同步的, cudaDeviceSyn-chronize函数可以用来阻塞主机应用程序,直到所有的CUDA操作(复制、 核函数等)完成:

cudaError_t cudaDeviceSynchronize(void);

在内核中标记同步点,当__syncthreads被调用时,在同一个线程块中每个线程都必须等待直至该线程块中所 有其他线程都已经达到这个同步点。
在不同的块之间没有线程同步。

__device__ void __syncthreads(void);

线程块中的线程可以通过共享内存和寄存器来共享数据。当线程之间共享数据时,要 避免竞争条件。

3.3 并行性的表现

1.用nvprof检测活跃的线程束

运行sumMatrix.cu
运行时间:
在这里插入图片描述
并行度:
并行性可以用nvprof和achieved_occupancy指标来验证。一个内核 的可实现占用率被定义为:每周期内活跃线程束的平均数量与一个SM支持的线程束最大 数量的比值。
在这里插入图片描述
·因为第二种情况中的块数比第一种情况的多,所以设备就可以有更多活跃的线程 束。其原因可能是第二种情况与第一种情况相比有更高的可实现占用率和更好的性能。
·第四种情况有最高的可实现占用率,但它不是最快的,因此,更高的占用率并不一 定意味着有更高的性能。肯定有其他因素限制GPU的性能。

2.用nvprof检测内存操作

gld_throughput指标检查内 核的内存读取效率
在这里插入图片描述
更高的加载吞吐量并不一定意味着更高的性能。
gld_efficiency指标检测全局加载效率(不懂),即被请求的全局加载吞吐量占所需的全局加载吞吐量的比值。
在这里插入图片描述

3.增大并行性

**线程块最内层维度的大小对性能起着的关键的作用。**一个块的最内层维数(block.x)应该是线程束大小的倍数,这样能极大地提高了加载效率。
在这里插入图片描述
线程块数量和可实现占用率关系:没啥直接关系
在这里插入图片描述

**总结:最好的执行配置既不具有最高的可实现占用率,也不具有最高的加载 吞吐量。**从这些实验中可以推断出,没有一个单独的指标能直接优化性能。我们需要在几 个相关的指标间寻找一个恰当的平衡来达到最佳的总体性能。

3.4 避免分支分化

1.并行归约问题

求矩阵和:在这里插入图片描述
交错匹配C语言实现:

int recursiveReduce(int *data, int const size){
   // terminate check
    if(size == 1) return data[0];
    // renew the stride 
    int const stride = size / 2;
    // in-place reduction
    for(int i = 0; i < stride; i++)
        data[i] += data[i + stride];
    // call recursiveley
    return recursiveReduce(data, stride);
}

在向量中执行满足交换律和结合律的运算,被称为归约问题。并行归约问题是这种运 算的并行执行。

2.并行归约中的分化

相邻配对方法的内核实现流程:
在这里插入图片描述
交错匹配实现:

__global__ void reduceNeighbored (int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if (idx >= n) return;

    // in-place reduction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2)
    {
        if ((tid % (2 * stride)) == 0)
        {
            idata[tid] += idata[tid + stride];
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

在这里插入图片描述
因为线程块间无法同步,所以每个线程块产生的部分和被复制回了主机,并且在那儿进行串行求和

3.改善并行归约后的分化
if((tid % (2 * stride)) == 0)

会导致很高的线程束分化。在并行归 约的第一次迭代中,只有ID为偶数的线程执行这个条件语句的主体,但是所有的线程都必 须被调度。
改进:
在这里插入图片描述

__global__ void reduceNeighboredLess (int *g_idata, int *g_odata,
                                      unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if(idx >= n) return;

    // in-place reduction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2)
    {
        // convert tid into local array index
        int index = 2 * stride * tid;

        if (index < blockDim.x)
        {
            idata[index] += idata[index + stride];
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

inst_per_warp指标来 查看每个线程束上执行指令数量的平均值

nvprof --metrics inst_per_warp ./reduceInteger

gld_throughput指标来查看内存加载吞吐量:

nvprof --metrics gid_throughput ./reduceInteger
4.交错配对的归约

在这里插入图片描述

__global__ void reduceInterleaved (int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if(idx >= n) return;

    // in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)
    {
        if (tid < stride)
        {
            idata[tid] += idata[tid + stride];
        }
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

reduceInterleaved函数和reduceNeigh- boredLess函数维持相同的线程束分化。

3.5 展开循环

1.展开的归约

循环展开是一个尝试通过减少分支出现的频率和循环维护指令来优化循环的技术。循环体的复制数量被称为循环展开因子,迭代次数就变为了原始循环迭代次数除以循环展开因子

__global__ void reduceUnrolling2 (int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 2 + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x * 2;

    // unrolling 2
    if (idx + blockDim.x < n) g_idata[idx] += g_idata[idx + blockDim.x];

    __syncthreads();

    // in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)
    {
        if (tid < stride)
        {
            idata[tid] += idata[tid + stride];
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

每个线程都添加一个来自于相邻数据块的元素,为现在每个线程块处理两个数据块。

if (idx + blockDim.x < n) g_idata[idx] += g_idata[idx + blockDim.x];

在这里插入图片描述

2.展开线程的归约

只剩下32个或更少线程(即一个线程束)的情况,因为线程束的执 行是SIMT(单指令多线程)的,每条指令之后有隐式的线程束内同步过程。因此,归约 循环的最后6个迭代可以用下述语句来展开:

__global__ void reduceUnrollWarps8 (int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    // unrolling 8
    if (idx + 7 * blockDim.x < n)
    {
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + 2 * blockDim.x];
        int a4 = g_idata[idx + 3 * blockDim.x];
        int b1 = g_idata[idx + 4 * blockDim.x];
        int b2 = g_idata[idx + 5 * blockDim.x];
        int b3 = g_idata[idx + 6 * blockDim.x];
        int b4 = g_idata[idx + 7 * blockDim.x];
        g_idata[idx] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;
    }

    __syncthreads();

    // in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 32; stride >>= 1)
    {
        if (tid < stride)
        {
            idata[tid] += idata[tid + stride];
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // unrolling warp
    if (tid < 32)
    {
        volatile int *vmem = idata;
        vmem[tid] += vmem[tid + 32];
        vmem[tid] += vmem[tid + 16];
        vmem[tid] += vmem[tid +  8];
        vmem[tid] += vmem[tid +  4];
        vmem[tid] += vmem[tid +  2];
        vmem[tid] += vmem[tid +  1];
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

意变量vmem是和volatile修饰符一起被声明的,它告诉编译器每次赋值时必须将 vmem[tid]的值存回全局内存中。如果省略了volatile修饰符,这段代码将不能正常工作, 因为编译器或缓存可能对全局或共享内存优化读写。如果位于全局或共享内存中的变量有 volatile修饰符,编译器会假定其值可以被其他线程在任何时间修改或使用。因此,任何 参考volatile修饰符的变量强制直接读或写内存,而不是简单地读写缓存或寄存器。

3.完全展开的归约

如果编译时已知一个循环中的迭代次数,就可以把循环完全展开

__global__ void reduceCompleteUnrollWarps8 (int *g_idata, int *g_odata,
        unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    // unrolling 8
    if (idx + 7 * blockDim.x < n)
    {
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + 2 * blockDim.x];
        int a4 = g_idata[idx + 3 * blockDim.x];
        int b1 = g_idata[idx + 4 * blockDim.x];
        int b2 = g_idata[idx + 5 * blockDim.x];
        int b3 = g_idata[idx + 6 * blockDim.x];
        int b4 = g_idata[idx + 7 * blockDim.x];
        g_idata[idx] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;
    }

    __syncthreads();

    // in-place reduction and complete unroll
    if (blockDim.x >= 1024 && tid < 512) idata[tid] += idata[tid + 512];

    __syncthreads();

    if (blockDim.x >= 512 && tid < 256) idata[tid] += idata[tid + 256];

    __syncthreads();

    if (blockDim.x >= 256 && tid < 128) idata[tid] += idata[tid + 128];

    __syncthreads();

    if (blockDim.x >= 128 && tid < 64) idata[tid] += idata[tid + 64];

    __syncthreads();

    // unrolling warp
    if (tid < 32)
    {
        volatile int *vsmem = idata;
        vsmem[tid] += vsmem[tid + 32];
        vsmem[tid] += vsmem[tid + 16];
        vsmem[tid] += vsmem[tid +  8];
        vsmem[tid] += vsmem[tid +  4];
        vsmem[tid] += vsmem[tid +  2];
        vsmem[tid] += vsmem[tid +  1];
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}
4.模板函数的归约

使用模板函数有助于进一步减少分支消耗,可以指定块的大小作为模板函数的参数。
相比reduceCompleteUnrollWarps8,唯一的区别是使用了模板参数替换了块大小。检查 块大小的if语句将在编译时被评估,如果这一条件为false,那么编译时它将会被删除,使得内循环更有效率。

template <unsigned int iBlockSize>
__global__ void reduceCompleteUnroll(int *g_idata, int *g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    // unrolling 8
    if (idx + 7 * blockDim.x < n)
    {
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + 2 * blockDim.x];
        int a4 = g_idata[idx + 3 * blockDim.x];
        int b1 = g_idata[idx + 4 * blockDim.x];
        int b2 = g_idata[idx + 5 * blockDim.x];
        int b3 = g_idata[idx + 6 * blockDim.x];
        int b4 = g_idata[idx + 7 * blockDim.x];
        g_idata[idx] = a1 + a2 + a3 + a4 + b1 + b2 + b3 + b4;
    }

    __syncthreads();

    // in-place reduction and complete unroll
    if (iBlockSize >= 1024 && tid < 512) idata[tid] += idata[tid + 512];

    __syncthreads();

    if (iBlockSize >= 512 && tid < 256)  idata[tid] += idata[tid + 256];

    __syncthreads();

    if (iBlockSize >= 256 && tid < 128)  idata[tid] += idata[tid + 128];

    __syncthreads();

    if (iBlockSize >= 128 && tid < 64)   idata[tid] += idata[tid + 64];

    __syncthreads();

    // unrolling warp
    if (tid < 32)
    {
        volatile int *vsmem = idata;
        vsmem[tid] += vsmem[tid + 32];
        vsmem[tid] += vsmem[tid + 16];
        vsmem[tid] += vsmem[tid +  8];
        vsmem[tid] += vsmem[tid +  4];
        vsmem[tid] += vsmem[tid +  2];
        vsmem[tid] += vsmem[tid +  1];
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

在这里插入图片描述
在这里插入图片描述

3.6 动态并行

CUDA的动态并行允许在GPU端直接创建和同步新的GPU内核。
有了动态并行,可以推迟到运行时决定需要在GPU上创建多少个块和网格,可以动态地利用GPU硬件调度器和加载平衡器,并进行调整以适应数据驱动或工作负载。

1.嵌套执行

子网格必须在父线 程、父线程块或父网格完成之前完成。只有在所有的子网格都完成之后,父母才会完成。
主机线程配置和启动父网格,父网格配置 和启动子网格。
在这里插入图片描述
设备线程中的网格启动,在线程块间是可见的。在线程块中,只有当所有线程创建的所 有子网格完成之后,线程块的执行才会完成。
当父母启动一个子网格,父线程块与孩子显式同步之后,孩子才能开始执行。
父网格和子网格共享相同的全局和常量内存存储,但它们有不同的局部内存和共享内 存。

2.在GPU上嵌套Hello World
__global__ void nestedHelloWorld(int const iSize, int iDepth)
{
    int tid = threadIdx.x;
    printf("Recursion=%d: Hello World from thread %d block %d\n", iDepth, tid,
           blockIdx.x);

    // condition to stop recursive execution
    if (iSize == 1) return;

    // reduce block size to half
    int nthreads = iSize >> 1;

    // thread 0 launches child grid recursively
    if(tid == 0 && nthreads > 0)
    {
        nestedHelloWorld<<<1, nthreads>>>(nthreads, ++iDepth);
        printf("-------> nested execution depth: %d\n", iDepth);
    }
}

int main(int argc, char **argv)
{
    int size = 8;
    int blocksize = 8;   // initial block size
    int igrid = 1;

    if(argc > 1)
    {
        igrid = atoi(argv[1]);
        size = igrid * blocksize;
    }

    dim3 block (blocksize, 1);
    dim3 grid  ((size + block.x - 1) / block.x, 1);
    printf("%s Execution Configuration: grid %d block %d\n", argv[0], grid.x,
           block.x);

    nestedHelloWorld<<<grid, block>>>(block.x, 0);

    CHECK(cudaGetLastError());
    CHECK(cudaDeviceReset());
    return 0;
}

在这里插入图片描述

3.嵌套归约

累了,毁灭吧

__global__ void gpuRecursiveReduce (int *g_idata, int *g_odata,
                                    unsigned int isize)
{
    // set thread ID
    unsigned int tid = threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * blockDim.x;
    int *odata = &g_odata[blockIdx.x];

    // stop condition
    if (isize == 2 && tid == 0)
    {
        g_odata[blockIdx.x] = idata[0] + idata[1];
        return;
    }

    // nested invocation
    int istride = isize >> 1;

    if(istride > 1 && tid < istride)
    {
        // in place reduction
        idata[tid] += idata[tid + istride];
    }

    // sync at block level
    __syncthreads();

    // nested invocation to generate child grids
    if(tid == 0)
    {
        gpuRecursiveReduce<<<1, istride>>>(idata, odata, istride);

        // sync all child grids launched in this block
        cudaDeviceSynchronize();
    }

    // sync at block level again
    __syncthreads();
}

在这里插入图片描述
特点:慢
当一个子网格被调用后,它看到的内存与父线程是完全一样的。因为每一个子线程只 需要父线程的数值来指导部分归约,所以在每个子网格启动前执行线程块内部的同步是没 有必要的。所以可以去除所有同步操作,但还是很慢
需要考虑如何减少由大量的子网格启动引起的消耗。

在这里插入图片描述

__global__ void gpuRecursiveReduce2(int *g_idata, int *g_odata, int iStride,
                                    int const iDim)
{
    // convert global data pointer to the local pointer of this block
    int *idata = g_idata + blockIdx.x * iDim;

    // stop condition
    if (iStride == 1 && threadIdx.x == 0)
    {
        g_odata[blockIdx.x] = idata[0] + idata[1];
        return;
    }

    // in place reduction
    idata[threadIdx.x] += idata[threadIdx.x + iStride];

    // nested invocation to generate child grids
    if(threadIdx.x == 0 && blockIdx.x == 0)
    {
        gpuRecursiveReduce2<<<gridDim.x, iStride / 2>>>(g_idata, g_odata,
                iStride / 2, iDim);
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值