初识CUDA网格与线程块

简介: 初识CUDA网格与线程块

在实际编写调用CUDA的内核函数中,对线程块和网格的划分往往是十分困难的,不同的划分方法经常会造成很大的性能差异,其划分与具体的硬件以及算法kernel的实现有很大关系,属于性能调优中一个比较重要的环节。

线程块划分

线程块划分其实质就是划分每个线程块包含的线程数量,以及线程块数量,与硬件有很大关系。

首先看一个线程块包含多少个线程合适,《CUDA中的一些基本概念》文章中了解到一些基本的GPU硬件知识,再次搬出下图:

GPU运行的线程的最小单位就是线程束,线程束数目代表的是一个SM内的硬件core的数量即一个SM的实际并发的线程数。

一个线程块只能在一个SM进行调度,不可能存在一个线程块在多个SM上同时执行的场景,这是由硬件调度决定的,后面再介绍为什么会设计这样的调度策略。在理想状态下,或当做指令只需要一次访问内存,然后将获取到的指令广播到这个线程束中的所有硬件core中即SP。这种模式比CPU模式更加高效,CPU是通过获取单独的执行流来支持任务级的并行。

既然一个线程块只能在一个SM上执行,线程块中的线程数量应该尽可能大的发挥出全部硬件特性,所以线程块中的线程数量划分第一个原则:一个线程块中的线程数量应该线程束的整数倍,如果不是整数倍,最后SM调度中,将会有core处于闲置状态,从而浪费性能。

为了减少内存读取延迟造成的GPU失速问题,一个SM应该尽可能多的 线程束即线程数量,这样才能用较多的指令来隐藏内存延迟问题,尽最大可能提高GPU利用率。一般在程序中, 192是线程块包含的最少的线程数目。

一个SM中的线程数量并不是无限大,每个SM最多能处理的线程数量都是有限制的,在费米架构硬件上,每个SM每次最多能执行1536个线程,而在G80硬件上,只能执行768个线程。

在实际设置线程块包含的线程数目是,往往并不是设置成线程束的整数倍,实际运行就能够调用最大core数量,还受到编写的kernel函数使用的硬件资源数目,尤其是寄存器数目。每个架构的GPU硬件,每个SM内的寄存器都是有一定数量的限制虽然与CPU相比,GPU的寄存器较多但也不是无限多,如果kernel中的寄存器使用数量超过SM数量,则即使按照线程束的整数倍进行设置,运行过程中也不可能调用最大core数量,因为硬件资源受到限制,比如一个SM的线程束目为32,但是实际kernel使用硬件资源过多,SM调度过程中可能一次性只能并发执行30个或者更少,剩余的线程在运行完成之后才会被调度到,实际运行效果将会比较差,这时就要优化kernel函数,尽可能使用少的硬件资源使能够调用到32个线程并发,或者将该kernel进一步拆分成两步进行计算。

网格划分

一个内核函数的网格划分,其实质就是划分成若干个线程块,每个线程块都可以是一维、二维或者三维,划分网格即线程块时尽量要做到线程与内存一一映射,这样才能避免同步以及内存不一致性问题。

如上所图为一个常见的二维数据划分的网格,每个X轴和Y轴都网格都包含若干个线程块,每个线程块在X轴或Y轴都包含若干个线程。

以一个32*16维的数组大小为例,共有32*16=512个元素,根据设计原则尽量将数据与线程做到一一对应,每个线程处于一个元素数据,共需要512个线程。假设使用的GPU的线程束大小为32,线程块包含的线程数目应该为线程束的整数倍,此时选择线程块中的线程数目为128个(当然实际硬件中可能一个线程块就可以搞定),需要的线程块数目为4个,剩下的工作就是网格的划分,将相应的数据划分到相应的数据块中,网格的划分有多种形式,可是是条纹形状,也可以为方框形状。

第一种网格划分方法,如下图所示:

数组每行共32个元素,每个元素大小为一个字节, 按照列划分线程块,每行可以分成四等分量,在每行中每块划分成8个线程,上幅图中按照列划分成分。该划分方法虽然线程0~7之前在访问内存时可以进行内存合并,一次性最多只能合并8个元素,在线程7和8直接访问的内存不连续无法进行内存合并,整个线程块可以将内存访问合并成16次。但是在英伟达GPU中,如果内存最大按照256个字节进行访问,显然该该方法只能合并成64个字节,并不能充分发挥其特性,在内存存储效率并不是很高。一般在网格划分中并不建议按照列划分,很容易造成内存访问不连续,使程序的性能指数级地下降。

进一步对网格划分进行优化,按照行划分线程块:

将32*16数据按照行划分,在同一个线程0~31中访问的内存都是连续的,其内存完全可以合并成一个读内存操作,相比较列划分方案,其内存读取效率要成倍提高。

第三种分法,线程块中即包含部分列也包含部分行:

第三种分法在有些GPU内存是按照128个字节分配,其内存合并效率与第二种方法一样,如果是有些GPU内存是按照256个字节读取则内存合并效率并没有第二种方法高。如果数据量为64*16,则第三种分支也是可以和第二种分发有相同的效率。

网格划分的两个原则:内存和线程最好建立一一映射,内存合并效率要尽量最高

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
5月前
|
设计模式 SQL 安全
Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
Java面试题:设计一个线程安全的内存管理器,使用观察者模式来通知所有线程内存使用情况的变化。如何确保在添加和移除内存块时的线程安全?如何确保任务的顺序执行和调度器的线程安全?
44 0
|
7月前
|
并行计算 编译器 C++
CUDA 中的线程组织
CUDA 中的线程组织
143 0
|
缓存 并行计算 算法
【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(二)
【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(二)
390 0
|
存储 缓存 并行计算
【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(一)
【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(一)
622 0
|
并行计算 调度 异构计算
|
并行计算 程序员 异构计算
CUDA存储单元的使用数据与线程之间的对应关系
CUDA存储单元的使用数据与线程之间的对应关系
169 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
24 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
38 2