DeepDriving | CUDA编程-03:线程层级

本文来源公众号“DeepDriving”,仅用于学术分享,侵权删,干货满满。

原文链接:CUDA编程-03:线程层级

DeepDriving | CUDA编程-01: 搭建CUDA编程环境-CSDN博客

DeepDriving | CUDA编程-02: 初识CUDA编程-CSDN博客

1 GPU架构概述

英伟达GPU的架构是围绕一个流式多处理器(Streaming Multiprocessors,SM)的可扩展阵列构建的,通过复制这种架构的构建来实现GPU的硬件并行。一个典型的SM包括以下几个组件:

  • 核心

  • 共享内存/一级缓存

  • 寄存器文件

  • 加载/存储单元

  • 特殊功能单元

  • 线程束调度器

一个GPU中通常有多个SM,每个SM上支持许多个线程并发地执行,CUDA采用单指令多线程(Single-Instruction Multiple-Thread,SIMT)来管理和执行GPU上的众多线程,并提出一个两级的线程层级结构的概念以便组织线程。由一个内核启动所产生的所有线程统称为一个线程网格,同一网格中的所有线程共享全局内存空间,一个网格由多个线程块组成,一个线程块包含一组线程,同一线程块内的线程通过同步和共享内存的方式实现协作,不同块内的线程不能协作。当host通过内核函数启动一个内核网格时,这个内核网格的线程块就被分配到可用的SM上来执行,一个线程块内的多个线程在SM上并发执行,多个线程块可以并发地在一个SM上执行,当线程块终止时,新的线程块又可以在腾出的SM上启动执行。

2 线程

线程是并行程序的基础,并行化的方式一般有两种:任务并行和数据并行。任务并行是将一个计算任务分解为几个子任务,通过不同的线程分别执行各个子任务,最后汇总结果;数据并行是将一个总任务在数据粒度上进行划分,然后每个线程处理一份数据,每个线程上执行的计算任务是一样的。

举个搬砖的例子:

假设我们的任务是将100个砖从A点搬到B点,搬砖的任务分为3个子任务:把砖从A点装车、从A点运送到B点、在B点把砖从车上卸下来。如果采用任务并行方式,那么可以请多个工人,然后把他们分为3个组,每个组负责一个子任务 ;如果是采用数据并行,那么可以请100个工人,每个人负责1个砖,每个人的任务都是把砖从A点搬到B点。

GPU采用数据并行的模式,它可以运行成千上万的线程用于运行大量逻辑比较简单的计算任务以实现高效的并行化计算。在上一篇文章中,我介绍了一个数组相加的例子,本文继续以这个例子来介绍GPU中以多线程实现并行化的方式。

先来看一下CPU实现数组相加的方式:

void VectorAddCPU(const float *const a, const float *const b, float *const c,
                  const int n) {
  for (int i = 0; i < n; ++i) {
    c[i] = a[i] + b[i];
  }
}

CPU的代码默认是单线程执行模式,要想实现含多个数据的数组相加任务,就必须以循环的方式实现(相当于一个人要把所有的砖搬完)。

再来看GPU的实现方式:

__global__ void VectorAddGPU(const float *const a, const float *const b,
                             float *const c, const int n) {
  int i = blockDim.x * blockIdx.x + threadIdx.x; // 线程ID
  if (i < n) {
    c[i] = a[i] + b[i]; //每个线程需要做的事情
  }
}

可以看到,GPU代码中并不需要循环,只是需要一个线程ID来进行索引,并告诉每个线程需要做的事情。线程依靠两个内置变量来进行区分:

  • blockIdx: 线程块在线程网格中的索引

  • threadIdx: 线程块内的线程索引

这两个CUDA内置变量是基于uint3定义的向量类型,是一个包含x,y,z三个无符号整数字段的结构。

在调用内核函数的时候,会在<<< >>>内设置两个参数,分别代表线程网格的维度和线程块的维度。CUDA可以组织三维的线程网格和线程块,它们的维度由下列两个内置变量来决定:

  • blockDim: 线程块的维度,用每个线程块中的线程数量来表示

  • gridDim: 线程网格的维度,用每个线程网格中的线程块数量来表示

它们是基于uint3定义的dim3结构类型的变量,用于表示维度,每个维度可通过x,y,z字段获得,未被初始化的字段会被初始化为1且忽略不计。通常情况下,一个线程网格会被组织成线程块的二维数组形式,一个线程块会被组织成线程的三维数组形式。

const size_t size = 1024;
dim3 thread_per_block(256);
dim3 block_per_grid((size + thread_per_block.x - 1) / thread_per_block.x);
printf("thread_per_block: %d, block_per_grid: %d \n", thread_per_block.x,
        block_per_grid.x);
VectorAddGPU<<<block_per_grid, thread_per_block>>>(da, db, dc, size);

在上面的例子中,我只初始化了线程网格和线程块的第一维x,相当于设定线程网格中的线程块是以一维的形式排列,每个线程块中的线程也是以一维的形式排列,在内核函数中每个线程的ID可以这样得到:

const unsigned int id = blockDim.x * blockIdx.x + threadIdx.x; 

我们可以在内核函数中打印gridDim,blockDim,blockIdx,threadIdx这些信息看一下:

......
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(1 0 0), threadIdx:(29 0 0)
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(1 0 0), threadIdx:(30 0 0)
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(1 0 0), threadIdx:(31 0 0)
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(0 0 0), threadIdx:(0 0 0)
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(0 0 0), threadIdx:(1 0 0)
gridDim:(4 1 1), blockDim:(256 1 1), blockIdx:(0 0 0), threadIdx:(2 0 0)
......

thread_per_block设置为512再看一下:

......
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(1 0 0), threadIdx:(93 0 0)
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(1 0 0), threadIdx:(94 0 0)
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(1 0 0), threadIdx:(95 0 0)
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(0 0 0), threadIdx:(416 0 0)
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(0 0 0), threadIdx:(417 0 0)
gridDim:(2 1 1), blockDim:(512 1 1), blockIdx:(0 0 0), threadIdx:(418 0 0)
......

可以看到,启动内核函数的时候在<<< >>>内设置不同的执行参数,内核中线程的布局是不一样的。

3 线程束

CUDA采用SIMT架构来管理和执行线程,将线程块中的线程每32个(记住这个神奇的数字)为一组进行划分,每一组被称为一个线程束(warp)。线程束的大小warpSizeCUDA中的一个内部属性,可以通过以下方式获得:

cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
printf("warpSize: %d\n", prop.warpSize);

线程束是GPU的基本执行单元,当线程网格启动后,网格中的线程块被分配到SM中执行,一旦线程块被调度到一个SM上,线程块中的线程就会被进一步划分为线程束,每个线程束中的所有线程执行相同的命令,每个线程拥有自己的指令地址计数器和寄存器状态,利用自己的私有数据执行当前的指令。线程块的逻辑视图和硬件视图之间的关系如下:

从逻辑角度看,线程块是线程的集合,它们可以被组织成一维、二维或者三维的布局形式;从硬件角度来看,线程块是一维线程束的集合,线程块中的线程被组织成一维布局,每32个连续的线程组成了一个线程束。

由于在硬件上线程块中的线程会被划分为线程束,而线程束不会在不同线程块之间分离,也就是说同一个线程束中的线程不会同属于两个线程块。如果线程块的大小不是线程束大小的偶数倍,那么最后一个线程束里就会有些线程没有用,但是它们依然会消耗SM的资源,所以在设置线程块大小的时候,最好设置为32的倍数。下图展示了一个线程块中包含80个线程时的情况,硬件为这些线程分配了3个线程束,最后一个线程束中有些线程是没有用的。

4 线程块

对于一份给定的数据,确定网格和块的维度的一般步骤为:

  1. 确定块的维度大小;

  2. 在已知数据大小和块大小的基础上计算网格的维度。

如何确定一个块的维度大小,通常需要考虑内核的性能特性和GPU的资源限制,比如寄存器和共享内存的大小,使用合适的网格和块大小来组织线程可以对内核性能产生较大的影响。在程序中,应该尽量避免使用小的线程块,因为这样无法充分利用硬件资源。为了防止不合理的内存合并,我们需要尽量做到数据内存的分布与线程的分布达到一一映射的关系。CUDA的设计思想是将数据分解到并行的线程和线程块中,使得程序结构与内存数据的分布能够建立一一映射的关系。假如我们需要计算二维数组的相加,那么可以将线程网格和线程块划分为二维:

这种情况下计算线程的ID会稍微复杂一点,首先计算当前的行索引,然后乘以每一行的线程总数,最后加上X轴方向上的偏移,这样就能计算出线程相对于整个线程网格的绝对线程索引:

const unsigned int idx = blockDim.x * blockIdx.x + threadIdx.x;
const unsigned int idy = blockDim.y * blockIdx.y + threadIdx.y;
const unsigned int thread_id = (gridDim.x * blockDim.x) * idy + idx;

当然,二维线程块的布局方式也有多种,比如下面这两种,它们的线程总数是一样的,但左图的布局要比右图的更高效。因为无论是在CPU还是在GPU中都是以行的方式进行内存访问,以右图的布局方式,同一行的数据需要被2个线程块访问2次,而左图的布局同一行的数据只需要访问1次即可。

5 参考资料

  • Professional CUDA C Programming

  • CUDA C Programming Guide

  • CUDA Programming:A Developer's Guide to Parallel Computing with GPUs

THE END !

文章结束,感谢阅读。您的点赞,收藏,评论是我继续更新的动力。大家有推荐的公众号可以评论区留言,共同学习,一起进步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/637969.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【搜索】BFS

#include <iostream> #include <cstring> #include <queue>using namespace std;const int N 110;typedef pair<int, int> PII;int n, m; int g[N][N], d[N][N];//存放地图//存每一个点到起点的距离int bfs() {queue< PII > q;q.push({0, 0});m…

变量命名的艺术:让你的代码更具可读性

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;为何变量命名如此重要&#xff1f; 二、变量命名的基本规则 1. 避免数…

Threejs路径规划_基于A*算法案例完整版

上节利用了A*实现了基础的路径规划&#xff0c;这节把整个功能完善好&#xff0c;A*算法一方面是基于当前点找到可以到达的点&#xff0c;计算从出发点到此点&#xff0c;以及此点到目的地的总成本&#xff0c;比较出最小的那个&#xff0c;再用最小成本的点继续找到它可以到达…

无线领夹麦克风哪个品牌音质最好,揭秘无线领夹麦哪个牌子好用

​随着社交媒体和内容创作的兴起&#xff0c;清晰可靠的音频捕捉已成为打造高品质作品的关键要素。无线领夹麦克风因其轻巧设计和用户友好的接口而受到青睐&#xff0c;它能够确保你的声音在任何环境下都能被完美捕捉。经过精心测试和对比&#xff0c;以下几款无线领夹麦克风是…

用手机打印需要下载什么软件

在快节奏的现代生活中&#xff0c;打印需求无处不在&#xff0c;无论是工作文件、学习资料还是生活小贴士&#xff0c;都可能需要一纸呈现。然而&#xff0c;传统的打印方式往往受限于时间和地点&#xff0c;让人倍感不便。今天&#xff0c;就为大家推荐一款便捷又省钱的手机打…

解锁合同管理的新路径:低代码与定制开发的完美结合

引言 合同管理在企业中扮演着至关重要的角色。无论是与供应商、客户还是合作伙伴之间的合作&#xff0c;合同都是约束双方责任和权利的关键文档。然而&#xff0c;随着业务的不断增长和全球化的发展&#xff0c;合同管理变得越来越复杂。传统的合同管理方法往往面临着诸多挑战&…

影响程序员发展,首个关于“软件供应链安全”国家标准发布,你该知道的10个问题!【附标准全文】

近日&#xff0c;GB/T 43698-2024《网络安全技术 软件供应链安全要求》作为国内首个软件供应链安全的国标&#xff0c;对于程序员的影响深远。该标准的实施&#xff0c;不仅为程序员提供了明确的软件安全开发指导&#xff0c;还强化了他们在软件开发过程中对安全性的重视。程序…

第十三节:带你梳理Vue2 : watch侦听器

官方解释:> 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式&#xff0c;用一个函数取代<br/>## 1. 侦听器的基本使用侦听器可以监听data对象属性或者计算属性的变化watch是观察属性的…

哈夫曼树的介绍

引入 概述 基本概念 示例 算法实现 存储结构 具体步骤 示例 初始化 合并 示例 代码整合&#xff1a; //哈夫曼树的建立 //定义类型:权值双亲结点左右孩子结点 typedef struct {int weight;int parent;int lchild,rchild; }Hnode,*huffmantree; //建立 1.判断有结点&#xf…

入门四认识HTML

一、HTML介绍 1、Web前端三大核心技术 HTML&#xff1a;负责网页的架构 CSS&#xff1a;负责网页的样式、美化 JS&#xff1a;负责网页的行动 2、什么是HTML HTML是用来描述网页的一种语言。 3、Html标签 单标签<html> 双标签<h>内容</h> 4、标…

如何零基础快速制作商业画册?这篇攻略帮你搞定

随着社会经济的发展&#xff0c;商业画册作为企业形象和产品介绍的重要载体&#xff0c;越来越受到重视。然而&#xff0c;很多企业和个人由于没有设计背景&#xff0c;在面对制作商业画册时往往感到困惑。本文将为你介绍零基础快速制作商业画册的攻略&#xff0c;让你轻松搞定…

Live800:提升客服服务质量,企业应从这几个方面下功夫

客户服务质量&#xff0c;是企业为客户提供优质服务的一个重要衡量指标。通常来说&#xff0c;一个企业的客服部门在其经营活动中发挥着重要作用&#xff0c;是客户与企业之间沟通的桥梁。良好的客服服务&#xff0c;不仅能够提高客户满意度&#xff0c;还能增强企业品牌的美誉…

Java中String类常用方法

写笔记记录自己的学习记录以及巩固细节 目录 1.String类的常用方法 1.1 字符串构造 1.2 String对象的比较 1.2.1 比较两个字符串是否相等 1.2.2 比较两个字符串的大小 1.3 字符串查找 1.4 字符串的转化 1.4.1 字符串转整数 1.4.2 字符串转数字 1.4.3 大小写的转换 1…

Mysql注入详细讲解

特殊字符 0x3a:0x7e~0x23# 注入基础 联合查询注入(union) :::tips 页面将SQL查询内容显示出来&#xff0c;即为有回显&#xff0c;可以尝试联合查询注入 利用关键字union &#xff0c;union all 拼接恶意SQL语句 ::: 注入流程 有报错&#xff0c;可以利用报错。如&#xff…

day 38 435.无重叠区间 763.划分字母区间 56. 合并区间 738.单调递增的数字 968.监控二叉树

435.无重叠区间 思路 为了使区间尽可能的重叠所以排序来使区间尽量的重叠&#xff0c;使用左边界排序来统计重叠区间的个数与452. 用最少数量的箭引爆气球恰好相反。 代码 class Solution {public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals,(a,…

不同类型的区块链钱包有什么特点和适用场景?

区块链钱包是用于存储和管理加密货币的重要工具&#xff0c;市面上有许多不同类型的区块链钱包可供选择。以下是几种主要类型的区块链钱包及其特点和适用场景。 1.软件钱包&#xff1a; 特点&#xff1a;软件钱包是最常见的一种区块链钱包&#xff0c;通常作为软件应用程序提供…

系统工程 | 系统工程概识

系统工程是为了最好地实现系统的目的&#xff0c;对系统的组成要素、组织结构、信息流、控制机构等进行分析研究的科学方法。 它运用各种组织管理技术&#xff0c;使系统的整体与局部之间的关系协调和相互配合&#xff0c;实现总体的最优运行。 系统工程不同于一般的传统工程…

指针数组与数组指针的理解

typedef struct vexnode {int key;struct arcnode *next; }vexnode, adjlist[MVNUM]; void init(adjlist *list); void init(adjlist *list) {for(size_t i 0; i < MVNUM; i){list[i].key i;list[i].next NULL;} }上述代码编译的时候没有报错&#xff0c;但是运行的时候&…

数据仓库和数据挖掘基础

文章目录 1. 数据仓库基础知识1.1 数据仓库的基本特性1.2 数据仓库的数据模式1.3 数据仓库的体系结构 2. 数据挖掘基础知识2.1 数据挖掘的分类2.2 数据挖掘技术2.3 数据挖掘的应用过程 传统数据库在联机事务处理(OLTP)中获得了较大的成功&#xff0c;但是对管理人员的决策分析要…

LeetCode刷题笔记第2769题:找到最大的可达成数字

LeetCode刷题笔记第2769题&#xff1a;找到最大的可达成数字 题目&#xff1a; 想法&#xff1a; 从题目中可以看出&#xff0c;num经过t次增减变为x&#xff0c;x即为可达成数字。因为要求最大的可达成数字&#xff0c;需要满足num一直增加&#xff0c;x一直减少&#xff0c…