【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(一)

简介: 【CUDA学习笔记】第四篇:线程以及线程同步(附案例代码下载方式)(一)

1、CUDA线程


   CUDA关于并行执行具有分层结构。每次内核启动时可以被切分成多个并行执行的块,而每个块又可以进一步地被切分成多个线程。

   在上一推文我们已经知道,maxThreadPerBlock属性限制了每个块能启动的线程数量。这个值对于最新的GPU卡来说是1024。类似地,第二种方式能最大启动的块数量被限制成2^31-1个。

   更加理想的则是,我们并不单独启动1个块,里面多个线程;也不启动多个块,每个里面1个线程。我们一次并行启动多个块,每个块里面多个线程(最多可以是maxThread-PerBlock那么多哦)。所以,假设上一章的那个向量加法例子你需要启动N=50000这么多的线程,我们可以这样调用内核:

   最大的块能有1024个线程。不过我们这里举例,对于N个线程来说,每个块有512个线程,则需要有N/512个块。但是如果N不是512的整数倍,那么N除以512会计算得到错误的块数量,比实际的块数量少1个。所以为了计算得到下一个最小的能满足要求的整数结果,N需要加上511,然后再除以512。这基本上是一个除法的向上取整操作。


还是直接撸代码吧:

#include "stdio.h"
#include<iostream>
#include <cuda.h>
#include <cuda_runtime.h>
//Defining number of elements in Array
#define N50000
//Defining Kernel function for vector addition
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {
//Getting block index of current kernel
int tid = threadIdx.x + blockIdx.x * blockDim.x;
while (tid < N)
{
d_c[tid] = d_a[tid] + d_b[tid];
tid += blockDim.x * gridDim.x;
}
}
int main(void) {
//Defining host arrays
int h_a[N], h_b[N], h_c[N];
//Defining device pointers
int *d_a, *d_b, *d_c;
// allocate the memory
cudaMalloc((void**)&d_a, N * sizeof(int));
cudaMalloc((void**)&d_b, N * sizeof(int));
cudaMalloc((void**)&d_c, N * sizeof(int));
//Initializing Arrays
for (int i = 0; i < N; i++) {
h_a[i] = 2 * i*i;
h_b[i] = i;
}
// Copy input arrays from host to device memory
cudaMemcpy(d_a, h_a, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, N * sizeof(int), cudaMemcpyHostToDevice);
//Calling kernels with N blocks and one thread per block, passing device pointers as parameters
gpuAdd << <512, 512 >> >(d_a, d_b, d_c);
//Copy result back to host memory from device memory
cudaMemcpy(h_c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);
cudaDeviceSynchronize();
int Correct = 1;
printf("Vector addition on GPU \n");
//Printing result on console
for (int i = 0; i < N; i++) {
if ((h_a[i] + h_b[i] != h_c[i]))
{
Correct = 0;
}
}
if (Correct == 1)
{
printf("GPU has computed Sum Correctly\n");
}
else
{
printf("There is an Error in GPU Computation\n");
}
//Free up memory
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}

   本内核的代码和上一文写过的那个很相似。但是有两处不同:

   (1)计算初始的tid的时候;

   (2)是添加了while循环部分。

   计算初始的tid的变化,是因为我们现在是启动多个块,每个里面有多个线程,直接看成ID的结构,多个块横排排列,每个块里面有N个线程,那么自然计算tid的时候是用:

当前块的ID*当前块里面的线程数量+当前线程在块中的ID

   即tid=blockIdx.x(当前块的ID)*blockDim.x(当前块里面的线程数量)+threadIdx.x(当前线程在块中的ID)。

   而while部分每次增加现有的线程数量(因为你没有启动到N),直到达到N。这就如同你有一个卡,一次最多只能启动100个块,每个块里有7个线程,也就是一次最多能启动700个线程。但N的规模是8000,远远超过700怎么办?答案是直接启动K个(K≥700),这样就能安全启动。然后里面添加一个while循环,这700个线程第一次处理[0,699),第二次处理[700,1400),第三次处理[1400,2100)……直到这8000个元素都被处理完。这就是我们本例中看到的代码。初始化时候的tid=threadIdx.x+blockDim.x*blockIdx.x,每次while循环的时候tid+=blockDim.x*gridDim.x(注意一个是=,一个是+=,后者是增加的由来)。下面的2D表格用来辅助理解。

   对于任意一个线程,使用blockIdx.x命令可以得到当前的块的ID,而使用threadIdx.x命令可以得到本线程在该块中的ID。例如,对于表格中绿色标记的线程,它的块ID是2,线程ID是1,如果想将这两个数字进行ID化,得到每个线程唯一的总ID,可以用块的ID乘以块中的线程总数,然后加上线程在这个块中的ID。数学表达式如下:

   这次的main函数,和我们上次写过的那个非常类似。唯一的不同点在于内核的启动方式。现在我们用512个块,每个块里面有512个线程启动该内核。这样N非常大的问题就得到了解决。此外,我们不再将很长的结果数组中的每个值都打印出来,只打印结果是否正确。


2、存储器架构


   在GPU上的代码执行被划分为流多处理器、块和线程。GPU有几个不同的存储器空间,每个存储器空间都有特定的特征和用途以及不同的速度和范围。这个存储空间按层次结构划分为不同的组块,比如全局内存、共享内存、本地内存、常量内存和纹理内存,每个组块都可以从程序中的不同点访问。此存储器架构如图所示:

   如图所示,每个线程都有自己的本地存储器和寄存器堆。与处理器不同的是,GPU核心有很多寄存器来存储本地数据。当线程使用的数据不适合存储在寄存器堆中或者寄存器堆中装不下的时候,将会使用本地内存。寄存器堆和本地内存对每个线程都是唯一的。寄存器堆是最快的一种存储器。同一个块中的线程具有可由该块中的所有线程访问的共享内存。全局内存可被所有的块和其中的所有线程访问。它具有相当大的访问延迟,但存在缓存这种东西来给它提速。如下表,GPU有一级和二级缓存(即L1缓存和L2缓存)。常量内存则是用于存储常量和内核参数之类的只读数据。最后,存在纹理内存,这种内存可以利用各种2D和3D的访问模式。

   所有存储器特征总结如下。

image.png

   上表表述了各种存储器的各种特性。作用范围栏定义了程序的哪个部分能使用该存储器。而生存期定义了该存储器中的数据对程序可见的时间。除此之外,L1和L2缓存也可以用于GPU程序以便更快地访问存储器。

   总之,所有线程都有一个寄存器堆,它是最快的。共享内存只能被块中的线程访问,但比全局内存块。全局内存是最慢的,但可以被所有的块访问。常量和纹理内存用于特殊用途。存储器访问是程序快速执行的最大瓶颈。


2.1、全局内存

   所有的块都可以对全局内存进行读写。该存储器较慢,但是可以从你的代码的任何地方进行读写。缓存可加速对全局内存的访问。所有通过cudaMalloc分配的存储器都是全局内存。下面的简单代码演示了如何从程序中使用全局内存:

#include <stdio.h>
#define N 5
__global__ void gpu_global_memory(int *d_a)
{
// "array" is a pointer into global memory on the device
d_a[threadIdx.x] = threadIdx.x;
}
int main(int argc, char **argv)
{
// Define Host Array
int h_a[N];
//Define device pointer
int *d_a;
cudaMalloc((void **)&d_a, sizeof(int) *N);
// now copy data from host memory to device memory 
cudaMemcpy((void *)d_a, (void *)h_a, sizeof(int) *N, cudaMemcpyHostToDevice);
// launch the kernel 
gpu_global_memory << <1, N >> > (d_a);
// copy the modified array back to the host memory
cudaMemcpy((void *)h_a, (void *)d_a, sizeof(int) *N, cudaMemcpyDeviceToHost);
printf("Array in Global Memory is: \n");
//Printing result on console
for (int i = 0; i < N; i++) {
printf("At Index: %d --> %d \n", i, h_a[i]);
}
return 0;
}

   这段代码演示了如何从设备代码中进行全局内存的写入,以及如何从主机代码中用cudaMalloc进行分配,如何将指向该段全局内存的指针作为参数传递给内核函数。内核函数用不同的线程ID的值来填充这段全局内存。然后(用cudaMemcpy)复制到内存以便显示内容。最终结果如图所示:

image.png


2.2、本地内存和寄存器堆

   本地内存和寄存器堆对每个线程都是唯一的。寄存器是每个线程可用的最快存储器。当内核中使用的变量在寄存器堆中装不下的时候,将会使用本地内存存储它们,这叫寄存器溢出。

   请注意使用本地内存有两种情况:

       (1)、寄存器不够了

       (2)、某些情况根本就不能放在寄存器中

   例如对一个局部数组的下标进行不定索引的时候。基本上可以将本地内存看成是每个线程的唯一的全局内存部分。相比寄存器堆,本地内存要慢很多。虽然本地内存通过L1缓存和L2缓存进行了缓冲,但寄存器溢出可能会影响你的程序的性能。

   下面演示一个简单的程序:

#include <stdio.h>
#define N 5
__global__ void gpu_local_memory(int d_in)
{
int t_local;
t_local = d_in * threadIdx.x;
printf("Value of Local variable in current thread is: %d \n", t_local);
}
int main(int argc, char **argv)
{
printf("Use of Local Memory on GPU:\n");
gpu_local_memory << <1, N >> > (5);
cudaDeviceSynchronize();
return 0;
}

  代码中的t_local变量是每个线程局部唯一的,将被存储在寄存器堆中。用这种变量计算的时候,计算速度将是最快速的。以上代码的输出如图所示:

image.png


2.3、高速缓冲存储器

   在较新的GPU上,每个流多处理器都含有自己独立的L1缓存,以及GPU有L2缓存。L2缓存是被所有的GPU中的流多处理器都共有的。所有的全局内存访问和本地内存访问都使用这些缓存,因为L1缓存在流多处理器内部独有,接近线程执行所需要的硬件单位,所以它的速度非常快。一般来说,L1缓存和共享内存共用同样的存储硬件,一共是64KB(注意:这是和计算能力有关,不一定共用相同的存储硬件,也不一定可以配置互相占用的比例,例如计算能力5.X和6.X的GPU卡就不能。同时L1缓存和共享内存在这两个计算能力上也不是共用的,但旧的计算能力和7.X GPU卡是如此),你可以配置L1缓存和共享内存分别在这64KB中的比例。所有的全局内存访问通过L2缓存进行。纹理内存和常量内存也分别有它们独立的缓存。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
2月前
|
数据采集 Java API
Jsoup库能处理多线程下载吗?
Jsoup库能处理多线程下载吗?
|
4月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
181 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
1月前
|
存储 监控 Java
JAVA线程池有哪些队列? 以及它们的适用场景案例
不同的线程池队列有着各自的特点和适用场景,在实际使用线程池时,需要根据具体的业务需求、系统资源状况以及对任务执行顺序、响应时间等方面的要求,合理选择相应的队列来构建线程池,以实现高效的任务处理。
118 12
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
75 3
|
3月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
54 6
|
4月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
44 1
|
4月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
42 2
|
4月前
FFmpeg学习笔记(二):多线程rtsp推流和ffplay拉流操作,并储存为多路avi格式的视频
这篇博客主要介绍了如何使用FFmpeg进行多线程RTSP推流和ffplay拉流操作,以及如何将视频流保存为多路AVI格式的视频文件。
528 0
|
5月前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
110 0
|
6月前
|
开发者 C# 存储
WPF开发者必读:资源字典应用秘籍,轻松实现样式与模板共享,让你的WPF应用更上一层楼!
【8月更文挑战第31天】在WPF开发中,资源字典是一种强大的工具,用于共享样式、模板、图像等资源,提高了应用的可维护性和可扩展性。本文介绍了资源字典的基础知识、创建方法及最佳实践,并通过示例展示了如何在项目中有效利用资源字典,实现资源的重用和动态绑定。
175 0

热门文章

最新文章