当前位置: 首页 > news >正文

[5] CUDA线程调用与存储器架构

CUDA线程调用与存储器架构

  • 前几节简单讲了如何编写CUDA程序,利用GPU的处理能力并行执行多个线程和块。
  • 之前所有程序里的线程是相互独立的,没有多个线程之间的通信
  • 多是实际应用程序需要中间线程之间的通信,本文将仔细讲解线程调用以及CUDA的分层存储架构,以及加速CUDA代码是使用不同存储器之间的区别。

1. 线程调用

  • CUDA 关于并行执行具有分层结构。每次内核启动时可以被切分成多个并行执行的块,而每个块又可以进一步的被切分成多个线程
  • GPU 中 1个块中的线程可以相互通信,即启动 1 个具有多个线程的块让里面的线程能够相互通信是一个优势
  • 最大的块能有1024个线程,但是我们在执行程序时需要开启所有线程吗?答案时不一定的,可以同时启动 多个块+块中的多个线程。假设一个向量加法例子需要启动 N=50000 这么多的线程,可以这样调用内核:
//如果块数量 N 不是512的倍数,需要加上511,然后再除以512
gpu<< <(N + 511) /512), 512 > >>(d_a,d_b,d_c)

1.1 向量加法

  • 简单描述一下GPU的结构,众所周知它是由很多块组成的计算单元,它的形态可以看作好多个魔方(立方体)拼接而成的结构,因此每个块可以通过x,y,z三个方向来确定它的位置或者id,下边举例说明一个通过启动 x 方向多个块 和 块中多个线程来实现向量加法,并给出详细分析
#include "stdio.h"
#include<iostream>
#include <cuda.h>
#include <cuda_runtime.h>
//Defining number of elements in Array
#define N	50000
//Defining Kernel function for vector addition
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {//Getting block index of current kernelint tid = threadIdx.x + blockIdx.x * blockDim.x;	while (tid < N){d_c[tid] = d_a[tid] + d_b[tid];tid += blockDim.x * gridDim.x;}}
  • 上述先给出了核函数,与之前程序不同的是tid的计算方式,即 tid = blockIdx.x(当前块的ID) * blockDim.x(当前快里面的线程数量) + threadIdx.x(当前线程在块中的ID)
  • 另一个不同的是 while 部分每次增加现有的线程数量(因为你没有启动到N),知道达到N。这就如同你有一个卡,一次最多只能启动100个块,每个块里有7个线程,也就是一次最多启动700个线程。但N的规模是8000,远远超过700,因此通过while循环可以实现第一次处理[0,699],第二次处理[700,1400],第三次处理[1400,2100]…直到这8000个元素全都被处理完
  • 因此,计算那每一个线程的总ID,可以通过如下数学表达式:
tid = hreadIdx.x + blockIdx.x * blockDim.x 
  • main函数掉调用如下:
int main(void) {//Defining host arraysint h_a[N], h_b[N], h_c[N];//Defining device pointersint *d_a, *d_b, *d_c;// allocate the memorycudaMalloc((void**)&d_a, N * sizeof(int));cudaMalloc((void**)&d_b, N * sizeof(int));cudaMalloc((void**)&d_c, N * sizeof(int));//Initializing Arraysfor (int i = 0; i < N; i++) {h_a[i] = 2 * i*i;h_b[i] = i;}// Copy input arrays from host to device memorycudaMemcpy(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 parametersgpuAdd << <512, 512 >> >(d_a, d_b, d_c);//Copy result back to host memory from device memorycudaMemcpy(h_c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);cudaDeviceSynchronize();int Correct = 1;printf("Vector addition on GPU \n");//Printing result on consolefor (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 memorycudaFree(d_a);cudaFree(d_b);cudaFree(d_c);return 0;
}

在这里插入图片描述

1.2 矩阵加法

  • 下边再给两个例子做比较吧

    • 通过一个块中的多个线程计算矩阵相加
    • 通过多个块的多个线程计算矩阵相加
    1. 启动一个块中的多个线程
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {//Getting block index of current kernelint i = threadIdx.x;int j = threadIdx.y;C[i][j] = A[i][j] + B[i][j];
}int main()
{   int numblocks = 1;dim3 threadsPerblock(N, N);MatAdd << <numblocks, threadsPerblock >> > (d_a, d_b, d_c);...
}
    1. 启动多个块中的多个线程
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {//Getting block index of current kernelint 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()
{//...//核函数定义dim3 threadsPerBlock(16, 16);dim3 numsBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);MatAdd << < numsBlocks, threadsPerBlock >> > (A, B, C);
}

2. 存储器架构

  • 在GPU上的代码执行被划分为流多处理器、块和线程。
  • GPU有几个不同的存储器空间,每个存储器空间都有特定的特征和用途以及不同的速度和范围。这个存储空间按层次结构被分为不同的组块,比如全局内存、共享内存、本地内存、常量内存和纹理内存,每个组块都可以从程序中的不同点访问。
  • 存储器架构如图:
    在这里插入图片描述
  • 如图所示,每个线程都有自己的本地存储器和寄存器堆。与处理器不同的是,GPU核心有很多寄存器来存储本地数据
  • 当线程使用的数据不适合存储在寄存器堆中或者寄存器堆中装不下的时候,将会使用本地内存。
  • 寄存器堆和本地内存对每个线程都是唯一的,寄存器堆时最快的一种存储器
  • 同一块中的线程具有可有该块中所有线程访问的共享内存。全局内存可被所有的线程访问,它具有相当大的访问延迟,但存在缓存这种东西来给他提速
  • GPU有以及和二级缓存(即L1缓存和L2缓存)。常量内存则是用于存储常量和内核参数之类的只读数据
  • 纹理内存可以利用各种2D和3D的访问模式
  • 所有存储区的特征总结如下:
    在这里插入图片描述

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 deviced_a[threadIdx.x] = threadIdx.x;
}int main(int argc, char **argv)
{// Define Host Arrayint 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 memorycudaMemcpy((void *)h_a, (void *)d_a, sizeof(int) *N, cudaMemcpyDeviceToHost);printf("Array in Global Memory is: \n");//Printing result on consolefor (int i = 0; i < N; i++) {printf("At Index: %d --> %d \n", i, h_a[i]);}return 0;
}

2.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变量是每个线程中局部唯一的,将被存储在寄存器堆中。用这种变量计算的时候,计算速度将是最快速的
    在这里插入图片描述

2.3 高速缓冲存储器

  • 在较新的GPU上,每个流多处理器都含有自己独立的L1缓存,以及GPU有L2缓存,L2缓存是被所有的GPU中的流多处理器都共有的,所有的全局内存访问和本地内存访问都使用这些内存,因为L1缓存在流多处理器内部独有,接近下称执行所需要的硬件单位,所以它的速度非常快
  • 一般来说,L1缓存和共享内存公用同样的存储硬件,一共是64KB,可以配置它们所占内存的比例
  • 所有的全局内存通过L2缓存进行,纹理内存和常量内存也分别有它们独立的缓存

相关文章:

  • OC笔记之foundation框架
  • 插件:MediaPipe
  • Linux:Linux系统项目配置
  • 智能代理四大范式解析
  • esp32-S3 使用自带的大模型,实现本地文字转语言tts
  • 练习题(2024/5/24)
  • 在ubuntu22.04里网站源码连不上mysql数据库
  • ES6 笔记04
  • 开发“校园帮”小程序:从构思到上线的完整指南
  • 基于springboot+vue的招聘信息管理系统
  • 按月爬取天气数据可视化展示
  • 7B2 PRO主题5.4.2免授权直接安装
  • Linux网络编程:HTTP协议
  • 【漏洞复现】英飞达医学影像存档与通信系统 WebJobUpload 任意文件上传漏洞
  • YoloV8改进策略:Neck层改进、注意力改进|HCANet全局与局部的注意力模块CAFM|二次创新|即插即用
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • 《Javascript数据结构和算法》笔记-「字典和散列表」
  • input的行数自动增减
  • Java教程_软件开发基础
  • js递归,无限分级树形折叠菜单
  • Just for fun——迅速写完快速排序
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • Node 版本管理
  • Spring声明式事务管理之一:五大属性分析
  • Sublime Text 2/3 绑定Eclipse快捷键
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 力扣(LeetCode)21
  • 深度学习中的信息论知识详解
  • 温故知新之javascript面向对象
  • 小而合理的前端理论:rscss和rsjs
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • k8s使用glusterfs实现动态持久化存储
  • #if 1...#endif
  • #include到底该写在哪
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • $.ajax,axios,fetch三种ajax请求的区别
  • (1)无线电失控保护(二)
  • (3)(3.5) 遥测无线电区域条例
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (LeetCode 49)Anagrams
  • (附源码)ssm学生管理系统 毕业设计 141543
  • (规划)24届春招和25届暑假实习路线准备规划
  • (精确度,召回率,真阳性,假阳性)ACC、敏感性、特异性等 ROC指标
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • (转) ns2/nam与nam实现相关的文件
  • (转)大型网站架构演变和知识体系
  • (转)项目管理杂谈-我所期望的新人
  • .Net IOC框架入门之一 Unity
  • .net 提取注释生成API文档 帮助文档
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • .NET中统一的存储过程调用方法(收藏)