NVIDIA CUDA 高度并行处理器编程(一):CUDA简介

2023-10-26

1. 数据并行性

数据并行性是一种属性,这种属性支持算数操作按照程序的数据结构同时安全的执行。CUDA设备通过采用大量的数据并行性的方式来加快应用程序的执行速度。
 在并行编程中,数据并行并不是唯一一种广泛使用的并行性,任务并行性在并行编程中也有广泛的使用。任务并行性通常对应用进行任务分解得到。例如,对于向量加法和矩阵向量乘法的简单应用来说,每个操作都可以看做一个任务,如果这两个任务可以独立执行,那么就能得到任务并行性。
 一般情况下,数据并行性是并行程序可拓展性地的主要来源。对于大型数据集很容易找到大量的数据并行性,以充分利用大规模并行处理器,随着每一代硬件提供更多执行单元,应用的性能也能大幅度提升。然而,任务并行性对于性能的提升也很重要,在介绍CUDA流的时候再介绍。

 我们利用上图阐述数据并行性的概念,C[i]是A[i]与B[i]相加后得到的,此向量相加操作可以并行执行。

2. CUDA的程序结构

 CUDA程序结构反映了在计算机中有一个主机(CPU)和一个或多个设备(GPU)。每个CUDA源文件包含主机代码和设备代码。默认情况下,任何只包含主机代码的C程序都可以看做CUDA程序。可以对任何C源文件添加设备函数和设备数据声明。针对设备的函数的数据声明都带有CUDA关键字标记。这些函数通常体现了丰富的数据并行性。
 一旦设备函数和数据声明添加进C源文件中,编不能通过gcc或其他编译器的编译。这些代码需要用能够识别这些设备函数和数据声明的编译器编译,比如NVCC(NVIDIA C Compiler)。如下图上部所示

 NVCC处理程序时通过CUDA关键字区分主机程序和设备程序。主机代码由主机标准的C或C++编译器编译,而设备代码用CUDA关键字来标示数据并行函数 (称为kernel) 。通常由NVCC编译器进一步编译,并在GUP上执行,如果没有GPU或者kernel更适合在CPU上执行,则可以通过MCUDA等工具将kernel函数转到CPU上执行。
  典型的CUDA程序的执行过程如下图所示。这是个简化的执行过程,其中GPU和CPU的执行过程没有重叠,然而很多模型都会采用CPU和GPU重叠执行的模型,以充分利用两者。
  执行过程始于主机,遇到kernel函数时函数转移到设备上大量线程同时执行。在调用kernel函数时生成的所有线程统称网络,下图就包含了两个网络。当kernel函数中所有线程都完成他们的执行任务后,相应的网格也会终止,在调用下一个kernel函数时程序会转到主机上继续执行。

 启动一个kernel通常会产生大量线程,以充分利用数据并行性,向量加法中,线程数与向量长度相同。在高效的硬件支持下这些线程的生成和调度需要很少的时钟周期完成,但在CPU中通常需要数千个时钟周期。

3. 向量加法kernel函数

 在主机代码的每一段中,给主机处理的变量名加上前缀h_,在设备要处理的变量名前加上前缀d_,以示区别。先看传统的C程序:

// Compute vector sum h_C = h_A+h_B
void vecAdd(float* h_A, float* h_B, float* h_C, int n)
{
	for (inti= 0; i < n; i++) h_C[i] = h_A[i] + h_B[i];
}
int main()
{
	// Memory allocation for h_A, h_B, and h_C
	// I/O to read h_A and h_B, N elements eachvecAdd(h_A, h_B, h_C, N);
}

 此程序通过for循环顺序执行向量加法,在第[i]轮循环中,计算A[i]和B[i]的和并存入C[i]。并行执行向量加法的简单方法是修改vecAdd函数:

#include <cuda.h>void vecAdd(float* A, float* B, float* C, int n)
{
	int size = n* sizeof(float);
	float *d_A *d_B, *d_C;1. // Allocate device memory for A, B, and C
	// copy A and B to device memory 
	2. // Kernel launch code – to have the device
	// to perform the actual vector addition
	3. // copy C from the device memory
	// Free device vectors
}

 注意,这里要添加一个预处理命令:#include<cuda.h>,该头文件定义了CUDA API函数和内置变量。第一部分:在设备上分配内存空间,存储向量d_A, d_B, d_C并将主机存储器(内存)中的向量复制到设备存储器中。第二部分:在设备上启动实际向量加法kernel函数。第三部分将设备存储器中的C向量复制到主机存储器。

4. 设备全局存储器与数据传输

  通常来讲,设备就是一种有DRAM的硬件卡。例如NVIDIA的GTX480处理器配备了称为全局存储器的4GB的DRAM芯片。
 主机存储器和设备存储器模型的框架如下图所示,这种模型为分配,移动和使用设备上各种存储器类型的设备。主机可以访问设备全局存储器,与设备之间传输和复制数据。

  1.3节中的A, B, C数组在设备存储器上的的内存分配、A, B, C在主机存储器与设备存储器之间的传输与设备存储器的内存释放都需要用到CUDA提供的API函数。

 下图展示了从在设备存储器中分配和释放内存的两个API函数,

 从主机调用 cudaMalloc() 函数时用来为对象在全局存储器中分配存储空间。cudaMalloc() 函数有两个参数:
  • 指针变量的地址(也就是**),这个指针变量指向分配存储空间后的对象。指针变量的地址应被强制转化为(void**)的形式,因为 cudaMalloc() 的返回值是泛型指针。
  • 对象所需的空间大小,以字节为单位,
      下面演示 cudaMalloc() 函数如何使用。首先定义了一个单精度浮点指针 d_A, 将 d_A 的地址(即&d_A)强制转化为 void 指针后,作为 cudaMalloc() 函数的第一个参数传入。 即 d_A 指向在设备全局存储器中分配给 A 数组的空间,长度为单精度浮点数长度的 n 倍。计算结束后,调用 cudaFree() 释放设备全局存储器中 A 数组的空间。
float *d_A;
int size=n * sizeof(float);
cudaMalloc((void**)&d_A, size);
...      //computing
cudaFree(d_A);

  d_A, d_B 和 d_C 的地址是设备存储器上的地址,主机代码不能解引用这些地址,否则会引起异常或运行时类型错误。
  在设备全局存储器分配空间后, 就可以调用CUDA API函数从主机存储器向设备传输数据了,下图展示了 cudaMemcpy() 函数。cudaMemcpy() 函数接受4个参数。

  • 第一个参数是指针,指向数据复制操作的目的地址。
  • 第二个参数指向要复制的源数据对象。
  • 第三个参数指定要复制数据的大小(以字节为单位)。
  • 第四个参数指出复制中所涉及的存储器的类型:从主机存储器到主机存储器、从设备存储器到设备存储器、从主机存储器到设备存储器和从设备存储器到主机存储器。但 cudaMemcpy() 不能用于多GPU系统中GPU与GPU之间的数据复制
所以d_A,d_B 和 d_C 在主机与设备的复制可以通过以下语句完成:
cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice); //cudaMemcpyHostToDevice is constant, no need to define
cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
cudaMemcpy(C, d_C, size, cudaMencpyDeviceToHost); //cudaMencpyDeviceToHost is constant too

自己动手实现一下CUDA 版 vecAdd() 的第1、3步吧!

5. kernel 函数与线程

  CUDA采用 SPMD 的并行编程风格。SPMD 与 SIMD 不同,SPMD 系统中,并行处理单元在数据的多个部分处理相同的程序,但不用执行同一指令。SIMD 系统中在任意时刻所有并行处理单元都在执行同一指令。
  当主机代码启动一个 kernel 函数时,CUDA运行时系统产生一个两级层级结构的网络。每个网络是线程块组成的数组,所有线程块大小一样,每个线程块最多有1024个线程。每个线程块的线程数是 kernel 函数启动时主机函数指定的。同一个 kernel 函数可以用在主机代码中用不同的线程数启动。对于给定一个网格一个线程块可用的线程数信息在 blockDim 变量中保存。下图中的 blockDim.x 的值为256。一般地,线程块的大小都是32的倍数。

  线程块中每个线程都有唯一一个 threadIdx 值。例如线程块 0 中的第 1 个线程的 threadIdx 为 0,第 2 个线程块的 threadIdx 为 1 ,以此类推。通过组合 threadIdx 与 blockIdx 可以为每个线程在整个网络中创建唯一一个索引。上图中的索引 i 可以通过 i = blockIdx.x * blockDim.x + threadIdx.x 得出。所以线程块 1 中的 i 值取值范围为256 ~ 511。

 下面是向量加法的 kernel 函数。采用 ANSI C 编写。在 vecAddKernel 函数声明前的关键字__global__表示此函数时 kernel 函数且从主机调用它时会在设备上生成网络线程。

//compute vector sum C = A + B
//Each thread performs one one pair-wise addition
__global__ void vecAddKernel(float *A, float *B, float *C, int n){
	int i = threadId.x + blockDim.x * blockIdx.x;
	if(i < n) C[i] = A[i] + B[i];
}

kernel函数解释:
 一般情况下,CUDA 对 C 函数的声明的拓展只有3个限定符关键字:

函数声明拓展 执行位置 调用位置
__device__ 设备 设备
__global__ 设备 主机
__host__ 主机 主机

  __global__指出它声明的函数为 CUDA 的 kernel 函数。__device__表明声明的函数为 CUDA 的设备函数,该函数在设备上执行,且只能在 kernel 函数或其他设备函数中调用。_\host__声明的函数就是普通的主机函数,与C函数相同,通常可以省略。__device__和__host__可以同时使用,同时使用时出发编译系统,从而生成同一个函数的两个不同版本,一个只能在主机上执行,另一个只能在设备上执行。
  kernel函数中的局部变量 i ,局部变量对于每个线程都是私有的,每个线程都会创建 i 的副本,假如有1000个线程,那么 i 就有 1000 个副本,它们的值分别为 0 ~ 999。
 大家可能发现,vecAddKernel 中没有循环,传统的 C vecAdd 有一层循环,vecAddKernel 中少的一层循环被过程网格替代了。整个网格等价于一重循环,网格中的每个线程相当于每次循环中的一次迭代。
 if(i < n) 语句是因为不一定所有向量长度都为块大小的整数倍,该语句是为了确保最后一个线程只有前 n - n / blockDim.x 个线程被调用。
kernel函数调用:
当主机代码启动一个kernel函数时,通过执行配置参数来设置网格和线程块的大小:

int vecAdd(float *A, float *B, float *C, int n)
{
	//...
	vecAddKernel<<<ceil(n/256.0), 256>>>(d_A, d_B, d_C, n);
	//...
}

第一个参数为线程块的数目,第二个参数为线程块中的线程数。
vecAdd() 函数的最终主机代码:

#include <cuda.h>void vecAdd(float* A, float* B, float* C, int n)
{
	int size = n* sizeof(float);
	float *d_A *d_B, *d_C;
	cudaMalloc((void**)&d_A, size);
	cudaMalloc((void**)&d_B, size);
	cudaMalloc((void**)&d_C, size);
	cudaMemcpy(d_A, A, size, cudaMemcpyHostToDevice); 
	cudaMemcpy(d_B, B, size, cudaMemcpyHostToDevice);
	vecAddKernel<<<ceil(n/256.0), 256>>>(d_A, d_B, d_C, n);
	cudaMemcpy(C, d_C, size, cudaMencpyDeviceToHost);
	cudaFree(d_A);
	cudaFree(d_B);
	cudaFree(d_C);
}

 程序所需的线程块数取决于数组长度 n 的大小,小型 GPU 只有一到两个线程块, 大型的 GPU 可能有 64 或 128 个线程块。所以程序的执行时间取决于 GPU 的性能。
 该CUDA vecAdd() 函数的执行速度可能比传统 C vecAdd() 慢,因为相对大量时间浪费在设备存储器上的内存分配与数据传输,而少量时间用来计算浮点加法。当线程进行的计算较复杂时,用于设备存储器上的内存分配与数据传输的比例会减少,速度也就相比于传统 C 更快。

参考:大规模并行编程处理器实战(第二版)David B. Kirk, Wen-mei W. Hwu 著,赵开勇 汪朝晖 程亦超
Programming Massively Parallel Processors A Hands-on Approach(3-rd)David B. Kirk, Wen-mei W. Hwu

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

NVIDIA CUDA 高度并行处理器编程(一):CUDA简介 的相关文章

  • Java中对象比较

    如果现在要想确定两个对象是否相等 那么应该比较的是对象的完整信息 而对象的完整信息就是对象的属性 所以所谓的对象比较指的就是两个对象的属性进行比较 对象比较的实现形式一 class Person private String name pr

随机推荐

  • 图解使用两块 Survivor 区的原因

    如果只使用一块 Survivor 区 当发生一次 Minor GC 时 回收 Eden 区的垃圾 把存活的对象复制到 Survivor 区 如下图 ok 目前看起来并没有什么区别 但是当发生第二次 Minor GC 时 问题就出现了 我们来
  • Kerberos认证流程及基本操作

    一 Kerberos的三个角色 Kerberos主要是有三个重要的角色 1 访问服务的Client 2 提供服务的Server 3 KDC Key Distribution Center 密钥分发中心 其中报错AS authorizatio
  • vim 的 auto-pairs 设置

    https github com jiangmiao auto pairs 转载于 https www cnblogs com zach0812 p 11514673 html
  • RestTemplate详细配置及日志打印

    目录 1 RestTemplate配置类 2 RestTemplateLog拦截器类 3 使用 1 RestTemplate配置类 包括连接池 超时时间 拦截器 异常处理 字符集 response多次读取 使用httpclient进行配置
  • node.js中时间戳和日期之间的转换

    今天在做项目的时候遇到可需要在时间戳和日期之间转换的问题 忙了很长时间 这里总结记录一下 首先 我们来了解一下js里面的Date对象 Date对象是JavaScript提供的日期和时间的操作接口 它有多种用法 1 new Date mill
  • 解决Windows10中Virtualbox安装虚拟机没有64位选项

    今天想在Windows 10系统安装完Virtualbox虚拟机 然后打算装一个CENTOS系统 但是选择安装系统的时候竟然没有64位操作系统的选项 经过一阵Google 终于解决了 在这里盘点一下出现这种情况的几种原因 解决Windows
  • 《深度学习入门》(鱼书)笔记 第一章 Python入门

    coding utf 8 class Man 示例类 示例类 def init self name self name name print Initilized def hello self print Hello self name d
  • Springboot缓存 注解@CachePut,@Cacheable,@CacheEvict 的作用及用法。

    在Spring Boot中 提供了一些用于缓存处理的注解 包括 CachePut Cacheable 和 CacheEvict 这些注解可以帮助简化缓存操作 并与底层缓存框架 如Ehcache Redis等 集成使用 1 CachePut
  • jenkins部署聚合项目报错[FATAL] Non-resolvable parent POM for xxx: Could not find artifact xxx

    项目改为聚合工程后使用jenkins部署时报错 如下 ERROR The build could not read 2 projects gt Help 1 org apache maven project ProjectBuildingE
  • 蓝桥杯 全球变暖【BFS】

    题目链接 AcWing 1233 全球变暖 你有一张某海域 N N像素的照片 表示海洋 表示陆地 如下所示 其中 上下左右 四个方向上连在一起的一片陆地组成一座岛屿 例如上图就有 2座岛屿 由于全球变暖导致了海面上升 科学家预测未来几十年
  • HTML5网页播放pcm语音流 VUE JS播放pcm语音流

    git源码地址 https github com SunnyWoo pcm voice git 效果图 将下面的组件文件直接放到我们的vue项目中运行即可看到效果
  • 计算机的工作原理

    文章目录 前言 一 计算机组成 二 工作原理 1 首先指令输入 由鼠标 键盘完成 2 计算机对指令 输出的处理 由CPU完成 3 计算机对信息的储存 由内存 磁盘完成 4 计算机输出信息 由显卡 显示器完成 总结 前言 电脑最直白 人话的描
  • Metasploit中meterpreter/reverse_tcp+exploit/multi/handler攻击实战教程

    由于是实战 肯定要一个服务器对吗 为了不搞破坏 我这里就不用服务器了 我现在已经用另一台电脑搭建了一个网站 用phpstudy快速搭建的 这台电脑IP是 192 168 1 103 我用的是渗透测试系统Kali Linux 由于此系统是自带
  • (十八)LCD1602实验

    本节我们来完成另一种在单片机上的显示的编程 使用LCD1602液晶显示器来显示我们想要输出的字符 输出 I LOVE MCU 和 MCU LOVE I 这两个短字符串 完成我们51单片机的最后一个简单实验 之后可能就会直接放几个简单项目在上
  • redis是单线程为什么速度还快

    1 完全基于内存 绝大部分请求是纯粹的内存操作 非常快 2 数据结构简单 对数据操作也简单 Redis中的数据结构是专门进行设计的 3 采用单线程 避免了不必要的上下文切换和竞争条件 也不存在多进程或者多线程导致的切换而消耗CPU 不用去考
  • windows server 2008修改远程桌面连接数

    安装了windows server 2008 R2 现在要远程连接 开启了服务器上的远程桌面连接 使用管理员远程登录 默认情况下windows server 2008只允许一个连接 默认一个账号最大2个连接 因需要两台电脑连接 在windo
  • 悬浮窗_今天聊聊悬浮窗搜题

    相信大部分的小伙伴都已经结课吧 忙着复习 忙着考试接下来给大家推荐一个 实用的软件 答题助手 答题助手有什么用呢 他可以帮你在手机上考试的时候 自己又不会的题目可以进行悬浮窗搜索 功能还是不错的 答题助手具体的功能介绍 1 悬浮窗扫题 大家
  • 混沌电路学习笔记

    历年电赛仪表 信号题 混沌信号产生实验装置 任务 要求 说明 思路 关键字提取 非线性系统的动态方程 蔡氏电路Chua s circuit wiki版本蔡氏电路 LTspice仿真实践 范德坡电路Vanderbilt circuit 考比兹
  • S-100电子海图标准简述 电子海图开发一百篇第五十篇

    通用海道测量数据模型 S 100 是国际海道测 量组织 IHO 以 ISO 19100 系列标准为基础 用面向对象的表示方法 以组件式理论框架将海道测量地理空间相关数据进行组织和交换的标准 IHO 于 2010 年 1 月发布 S 100
  • NVIDIA CUDA 高度并行处理器编程(一):CUDA简介

    NVIDIA CUDA 高度并行处理器编程 一 CUDA简介 1 数据并行性 2 CUDA的程序结构 3 向量加法kernel函数 4 设备全局存储器与数据传输 5 kernel 函数与线程 1 数据并行性 数据并行性是一种属性 这种属性支