0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

介绍CUDA编程模型及CUDA线程体系

冬至子 来源:指北笔记 作者:张北北 2023-05-19 11:32 次阅读

可伸缩的编程模型

CUDA 编程模型主要有三个关键抽象:层级的线程组,共享内存和栅同步(barrier synchronization)。

这些抽象提供了细粒度的数据并行和线程并行,可以以嵌套在粗粒的数据并行和任务并行中。它们鼓励将问题分解为子问题。每个子问题可以独立的在block threads中并行解决。同时每个子问题分成更细的部分,可以由块中的所有线程并行地合作解决。

这种分解通过允许线程在解决每个子问题时进行协作来保留语言的表达性,同时支持自动可伸缩性。实际上,每个线程块都可以在GPU中任何可用的多处理器上调度,以任何顺序、并发或顺序,因此编译的CUDA程序可以在任意数量的多处理器上执行,如图所示,而且只有运行时系统需要知道物理多处理器的数量。

图片

图1 Automatic Scalability

Note: A GPU is built around an array of Streaming Multiprocessors (SMs) (see Hardware Implementation for more details). A multithreaded program is partitioned into blocks of threads that execute independently from each other, so that a GPU with more multiprocessors will automatically execute the program in less time than a GPU with fewer multiprocessors.

Kernels

CUDA c++ 通过允许程序员定义 c++ 函数( 称为kernel )来扩展 c++,当调用这些函数时,由 N 个不同的 CUDA 线程并行执行 N 次,而不是像常规 c++ 函数那样只执行一次。

使用_ global _ 声明说明符定义内核,并使用新的<<<…>>>执行配置语法(参见c++语言扩展)。每个执行内核的线程都有一个惟一的线程ID,可以在内核中通过内置变量访问该ID。

下面的示例代码使用内置变量 threadIdx ,将两个大小为N的向量A和B相加,并将结果存储到向量C中:

// Kernel definition
__global__ void VecAdd(float* A, float* B, float* C)
{
    int i = threadIdx.x;
    C[i] = A[i] + B[i];
}

int main()
{
    ...
    // Kernel invocation with N threads
    VecAdd<<<1, N>>>(A, B, C);
    ...
}

执行VecAdd()的N个线程中的每一个都执行一次成对的相加。

Thread Hierarchy

为了方便起见,threadIdx 是一个三分量的向量,因此可以使用一维、二维或三维线程索引来标识线程,从而形成一维、二维或三维线程块,称为线程块。这提供了一种很自然的方法来调用跨域元素(如向量、矩阵或体)的计算。

线程的索引和线程 ID 以一种直接的方式相互关联:

  • 对于一维块,它们是相同的
  • 对于大小为(Dx, Dy)的二维块,索引为(x, y)的线程ID为(x + y Dx);
  • 对于大小为Dx, Dy, Dz的三维块,索引为(x, y, z)的线程ID为(x + y Dx + z Dx Dy)。

例如,下面的代码将两个大小为NxN的矩阵A和B相加,并将结果存储到矩阵C中:

// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
                       float C[N][N])
{
    int i = threadIdx.x;
    int j = threadIdx.y;
    C[i][j] = A[i][j] + B[i][j];
}

int main()
{
    ...
    // Kernel invocation with one block of N * N * 1 threads
    int numBlocks = 1;
    dim3 threadsPerBlock(N, N);
    MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
    ...
}

每个块的线程数量是有限制的,因为一个块的所有线程都驻留在同一个流多处理器核心上,必须共享该核心的有限内存资源。 在当前的gpu上,一个线程块可能包含多达1024个线程

但是,一个内核可以由多个形状相同的线程块执行,这样 线程总数就等于每个块的线程数乘以块的数量

块被组织成一维、二维或三维的线程块网格,如图所示。 网格中线程块的数量通常由正在处理的数据的大小决定 ,数据的大小通常超过系统中处理器的数量。

图片

图2 Grid of Thread Blocks

每个块的线程数和每个网格的块数在<<<…>>>语法的类型可以是int或dim3。二维块或网格可以像上面的例子中那样指定。

网格中的每个块都可以通过一个一维、二维或三维的惟一索引来标识。该索引可以通过内核中内置的blockIdx变量访问。 线程块的维度可以在内核中通过内置的blockDim变量访问

扩展前面的MatAdd()示例以处理多个块,代码如下所示。

// Kernel definition
__global__ void MatAdd(float A[N][N], float B[N][N],
float C[N][N])
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    int j = blockIdx.y * blockDim.y + threadIdx.y;
    if (i < N && j < N)
        C[i][j] = A[i][j] + B[i][j];
}

int main()
{
    ...
    // Kernel invocation
    dim3 threadsPerBlock(16, 16);
    dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
    MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C);
    ...
}

线程块大小为16x16(256个线程),虽然在本例中是任意的,但却是常见的选择 。网格是用足够的块创建的,每个矩阵元素都有一个线程。

为了简单起见,本示例假设每个维度中每个网格的线程数可以被该维度中每个块的线程数整除,尽管事实并非如此。

线程块需要独立执行 :必须能够以任何顺序执行它们,并行或串行。这种独立性要求允许线程块在任意数量的核上以任意顺序调度,如图1所示,这使程序员能够编写随核数量扩展的代码。

块中的线程可以通过共享内存共享数据,并通过同步它们的执行来协调内存访问,从而进行协作 。更精确地说,可以通过调用__syncthreads()内部函数来指定内核中的同步点;__syncthreads()充当一个屏障,在允许任何线程继续之前,块中的所有线程都必须等待。除了__syncthreads()之外,Cooperative Groups API还提供了一组丰富的线程同步原语。

为了高效合作,共享内存应该是每个处理器核心附近的低延迟内存(很像L1缓存),并且__syncthreads()应该是轻量级的。

Thread Block Clusters

随着NVIDIA Compute Capability 9.0的引入 ,CUDA编程模型引入了一个可选的层次结构级别,称为 线程块集群,它由线程块组成与线程块中的线程被保证在流多处理器上同步调度类似,集群中的线程块也被保证在GPU中的GPU处理集群(GPC)上同步调度

与线程块类似,集群也被组织成一维、二维或三维,如图3所示。一个集群中的线程块数量可以由用户定义, CUDA支持一个集群中最多8个线程块作为可移植的集群大小 。线程块集群大小是否超过8取决于体系结构,可以使用cudaoccuancymaxpotentialclustersize API进行查询。

图片

图3 Grid of Thread Block Clusters

Note: In a kernel launched using cluster support, the gridDim variable still denotes the size in terms of number of thread blocks , for compatibility purposes. The rank of a block in a cluster can be found using the Cluster Group API.

线程块集群可以在内核中使用编译器时间内核属性__cluster_dims__(X,Y,Z)或使用CUDA内核启动API cudaLaunchKernelEx来启用。下面的示例展示了如何使用编译器时间内核属性启动集群。使用内核属性的集群大小在编译时固定,然后可以使用经典的<<<,>>>启动内核。如果内核使用编译时集群大小,则在启动内核时无法修改集群大小。

  • 编译期指定
// Kernel definition
// Compile time cluster size 2 in X-dimension and 1 in Y and Z dimension
__global__ void __cluster_dims__(2, 1, 1) cluster_kernel(float *input, float* output)
{

}

int main()
{
    float *input, *output;
    // Kernel invocation with compile time cluster size
    dim3 threadsPerBlock(16, 16);
    dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);

    // The grid dimension is not affected by cluster launch, and is still enumerated
    // using number of blocks. 
    // The grid dimension must be a multiple of cluster size.
    cluster_kernel<<
  • 运行期指定

线程块集群大小也可以在运行时设置,并且可以使用CUDA内核启动API cudaLaunchKernelEx启动内核。下面的代码示例展示了如何使用可扩展API启动集群内核。

// Kernel definition
// No compile time attribute attached to the kernel
__global__ void cluster_kernel(float *input, float* output)
{
  
}

int main()
{
    float *input, *output;
    dim3 threadsPerBlock(16, 16);
    dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
    cluster_kernel<<

在具有9.0计算能力的GPU中,集群中的所有线程块都被保证在单个GPU处理集群(GPC)上共同调度,并允许集群中的线程块使用cluster Group API cluster.sync()执行硬件支持的同步 。集群组还提供了成员函数,分别使用num_threads()和num_blocks() API根据线程数或块数查询集群组的大小。可以分别通过dim_threads()和dim_blocks() API查询集群组中线程或块的级别。

属于一个集群的线程块可以访问 分布式共享内存 。集群中的线程块能够对分布式共享内存中的任何地址进行读、写和执行原子操作。分布式共享内存给出了一个在分布式共享内存中执行直方图的示例。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 多处理器
    +关注

    关注

    0

    文章

    22

    浏览量

    8862
  • C++语言
    +关注

    关注

    0

    文章

    146

    浏览量

    6878
  • CUDA
    +关注

    关注

    0

    文章

    119

    浏览量

    13462
收藏 人收藏

    评论

    相关推荐

    CUDA编程教程

    Nvidia CUDA 2.0编程教程
    发表于 03-05 07:30

    LInux安装cuda sdk

    1.安装toolkit(1)cd /home/CUDA_train/software/cuda4.1(2)./cudatoolkit_4.1.28_linux_64_rhel6.x.run
    发表于 07-24 06:11

    CUDA教程之Linux系统下CUDA安装教程

    CUDA教程之1:Linux系统下CUDA安装教程
    发表于 06-02 16:53

    什么是CUDA

    的时间尽可能清晰的了解这个深度学习赖以实现的基础概念。本文在以下资料的基础上整理完成,感谢以下前辈提供的资料:CUDA——“从入门到放弃”我的CUDA学习之旅——启程介绍一篇不错的CUDA
    发表于 07-26 06:28

    什么是CUDA

    什么是CUDA
    发表于 09-28 07:37

    cuda程序设计

      •GPGPU及CUDA介绍   •CUDA编程模型   •多
    发表于 11-12 16:12 0次下载

    CUDA 6中的统一内存模型

    NVIDIA在CUDA 6中引入了统一内存模型 ( Unified Memory ),这是CUDA历史上最重要的编程模型改进之一。在当今典型
    的头像 发表于 07-02 14:08 2435次阅读

    CUDA学习笔记第一篇:一个基本的CUDA C程序

    1、CUDA的简介 2、GPU架构和CUDA介绍3、CUDA架构4、开发环境说明和配置5、开始第一个Hello CUDA程序    5.1、
    的头像 发表于 12-14 23:40 687次阅读

    并行计算平台和NVIDIA编程模型CUDA的更简单介绍

      这篇文章是对 CUDA 的一个超级简单的介绍,这是一个流行的并行计算平台和 NVIDIA 的编程模型。我在 2013 年给 CUDA
    的头像 发表于 04-11 09:46 1157次阅读
    并行计算平台和NVIDIA<b class='flag-5'>编程</b><b class='flag-5'>模型</b><b class='flag-5'>CUDA</b>的更简单<b class='flag-5'>介绍</b>

    CUDA并行计算平台的C/C++接口的简单介绍

    CUDA 编程模型是一个异构模型,其中使用了 CPU 和 GPU 。在 CUDA 中, host 指的是 CPU 及其存储器, device
    的头像 发表于 04-11 10:13 1253次阅读

    CUDA简介: CUDA编程模型概述

    CUDA 编程模型中,线程是进行计算或内存操作的最低抽象级别。 从基于 NVIDIA Ampere GPU 架构的设备开始,CUDA
    的头像 发表于 04-20 17:16 2498次阅读
    <b class='flag-5'>CUDA</b>简介: <b class='flag-5'>CUDA</b><b class='flag-5'>编程</b><b class='flag-5'>模型</b>概述

    CUDA编程模型如何在c++实现

      NVIDIA GPU 架构围绕可扩展的多线程流式多处理器 (SM: Streaming Multiprocessors) 阵列构建。当主机 CPU 上的 CUDA 程序调用内核网格时,网格
    的头像 发表于 04-21 16:13 1335次阅读
    <b class='flag-5'>CUDA</b><b class='flag-5'>编程</b><b class='flag-5'>模型</b>如何在c++实现

    如何使用CUDA使warp级编程安全有效

      NVIDIA GPUs 以 SIMT (单指令,多线程)方式执行称为 warps 的线程组。许多 CUDA 程序通过利用 warp 执行来获得高性能。在这个博客中,我们将展示如何使用 CU
    的头像 发表于 04-28 16:09 2391次阅读
    如何使用<b class='flag-5'>CUDA</b>使warp级<b class='flag-5'>编程</b>安全有效

    使用CUDA进行编程的要求有哪些

    CUDA是NVIDIA的一种用于GPU编程的技术,CUDA核心是GPU上的一组小型计算单元,它们可以同时执行大量的计算任务。
    的头像 发表于 01-08 09:20 2012次阅读

    CUDA核心是什么?CUDA核心的工作原理

    CUDA核心(Compute Unified Device Architecture Core)是NVIDIA图形处理器(GPU)上的计算单元,用于执行并行计算任务。每个CUDA核心可以执行单个线程的指令,包括算术运算、逻辑操作
    发表于 09-27 09:38 4902次阅读
    <b class='flag-5'>CUDA</b>核心是什么?<b class='flag-5'>CUDA</b>核心的工作原理