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

news2024/10/6 13:15:23

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 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;
	}
		
}
  • 上述先给出了核函数,与之前程序不同的是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 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.2 矩阵加法

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

    • 通过一个块中的多个线程计算矩阵相加
    • 通过多个块的多个线程计算矩阵相加
    1. 启动一个块中的多个线程
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) {
	//Getting block index of current kernel

	int 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 kernel

	int 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 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;
}

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缓存进行,纹理内存和常量内存也分别有它们独立的缓存

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gradle的settings.gradle.kts你真的理解吗?

你还在用.gradle文件吗&#xff1f;例如build.gradle、settings.gradle&#xff0c;那么你就out了。现在我们有必要了解一下kts脚本了。在Android项目中&#xff0c;默认有3个文件可以替换成kts脚本&#xff0c;即project的build.gradle、app模块的build.gradle和project的sett…

关于在子线程中获取不到HttpServletRequest对象的问题

这篇文章主要分享一下项目里遇到的获取request对象为null的问题&#xff0c;具体是在登录的时候触发的邮箱提醒&#xff0c;获取客户端ip地址&#xff0c;然后通过ip地址定位获取定位信息&#xff0c;从而提示账号在哪里登录。 但是登录却发现获取request对象的时候报错了。 具…

数据插值之朗格朗日插值(一)

目录 一、引言 二、代码实现 2.1 Lagrange插值求插值多项式&#xff1a; 代码解析&#xff1a; 1.vpa解释 2.ploy&#xff08;x&#xff09;解释: 3.conv&#xff08;&#xff09;解释 4.poly2sym()解释 2.2 Lagrange插值求新样本值和误差估计&#xff1a; 代码解析&…

鲁教版七年级数学上册-笔记

文章目录 第一章 三角形1 认识三角形2 图形的全等3 探索三角形全等的条件4 三角形的尺规作图5 利用三角形全等测距离 第二章 轴对称1 轴对称现象2 探索轴对称的性质4 利用轴对称进行设计 第三章 勾股定理1 探索勾股定理2 一定是直角三角形吗3 勾股定理的应用举例 第四章 实数1 …

C++技能进阶指南——多态语法剖析

前言&#xff1a;多态是面向对象的三大特性之一。顾名思义&#xff0c; 多态就是多种状态。 那么是什么的多种状态呢&#xff1f; 这里的可能有很多。比如我们去买火车票&#xff0c; 有普通票&#xff0c; 学生票&#xff1b; 又比如我们去旅游&#xff0c; 有儿童票&#xff…

心链2---前端开发(整合路由,搜索页面,用户信息页开发)

心链——伙伴匹配系统 接口调试 说书人&#x1f4d6;&#xff1a;上回书说到用了两种方法查询标签1.SQL查询&#xff0c;2.内存查询&#xff1b;两种查询效率是部分上下&#xff0c;打的是难解难分&#xff0c;是时大地皴裂&#xff0c;天色聚变&#xff0c;老祖斟酌再三最后决…

数据库-SQL性能分析

SQL执行频率 慢查询日志 慢查询日志记录了所有执行时间超过指定参数&#xff08;long_query_time&#xff0c;单位&#xff1a;秒&#xff0c;默认10秒&#xff09;的所有 SQL语句的日志。 MySQL的慢查询日志默认没有开启&#xff0c;我们可以查看一下系统变量 slow_query_l…

leetcode328. 奇偶链表,附详细解析和代码注释

leetcode328. 奇偶链表 给定单链表的头节点 head &#xff0c;将所有索引为奇数的节点和索引为偶数的节点分别组合在一起&#xff0c;然后返回重新排序的列表。 第一个节点的索引被认为是 奇数 &#xff0c; 第二个节点的索引为 偶数 &#xff0c;以此类推。 请注意&#xff0…

初出茅庐的小李博客之用MQTT.fx软件进行消息发布与订阅【 基于EMQX Cloud】

MQTT.fx软件使用简单介绍 MQTT.fx 的软件界面如下图所示&#xff0c;最上方为 MQTT Broker 连接地址栏&#xff0c;及其连接配置。其下方功能 Tabs 含有 Publish 发布栏、Subscribe 订阅栏、Scripts 脚本栏、Broker Status 状态消息栏、Log 日志信息控制栏。 连接之前要明确几…

Distributed Transactions Mit 6.824

Topic1&#xff1a;distributed transactions concurrency control atomic commit 传统计划&#xff1a;事务 程序员标记代码序列的开始/结束作为事务。 事务示例 x 和 y 是银行余额——数据库表中的记录。x 和 y 位于不同的服务器上&#xff08;可能在不同的银行&#x…

NDIS小端口驱动(九)

PCIe设备难免会遇到一些重置设备的请求&#xff0c;例如重置总线的时候&#xff0c;但是由于NIC网卡的多样性&#xff0c;重置设备确实也有许多要注意的地方&#xff0c;另外还有一些包含WDM的NDIS驱动 微型端口驱动程序硬件重置 微型端口驱动程序必须向 NdisMRegisterMinipo…

10款免费黑科技软件,强烈推荐!

1.AI视频生成——巨日禄 网页版https://aitools.jurilu.com/ "巨日禄 "是一款功能强大的文本视频生成器&#xff0c;可以快速将文本内容转换成极具吸引力的视频。操作简单&#xff0c;用户只需输入文字&#xff0c;选择喜欢的样式和模板&#xff0c; “巨日禄”就会…

qemu+gdb调试linux内核

打开CONFIG_DEBUG_INFO,编译内核 通过图形菜单配置该宏,执行make menuconfig。 kernel hacking —> compile-time checks and compiler options —> compile the kernel with debug info 验证是否打开成功,grep -nr “CONFIG_DEBUG_INFO” .config。 打开成功,然后…

初识Spring Boot

初识Spring Boot SpringBoot是建立在Spring框架之上的一个项目,它的目标是简化Spring应用程序的初始搭建以及开发过程。 对比Spring Spring Boot作为Spring框架的一个模块&#xff0c;旨在简化Spring应用程序的初始搭建和开发过程&#xff0c;以下是Spring Boot相对于传统Spri…

【通义千问—Qwen-Agent系列2】案例分析(图像理解图文生成Agent||多模态助手|| 基于ReAct范式的数据分析Agent)

目录 前言一、快速开始1-1、介绍1-2、安装1-3、开发你自己的Agent 二、基于Qwen-Agent的案例分析2-0、环境安装2-1、图像理解&文本生成Agent2-2、 基于ReAct范式的数据分析Agent2-3、 多模态助手 附录1、agent源码2、router源码 总结 前言 Qwen-Agent是一个开发框架。开发…

iZotope RX 11 for Mac:音频修复的终极利器

在音频处理的世界里&#xff0c;纯净与清晰是每一个创作者追求的目标。iZotope RX 11 for Mac&#xff0c;这款专为Mac用户打造的音频修复软件&#xff0c;凭借其卓越的音频修复能力和丰富的功能&#xff0c;已经成为众多音频工程师和音乐制作人的首选工具。 iZotope RX 11 for…

线程生命周期

创建线程的两种方法 1.继承Thread类 2.实现Runnable接口 线程从创建到消亡分为新建、就绪、运行、阻塞、死亡5种状态。 新建状态 创建一个线程就处于新建状态。此时线程对象已经被分配了内存空间&#xff0c;并且私有数据也被初始化&#xff0c;但是该线程还不能运行。 就…

nodeJs学习(第一周)

文章目录 学习总结nodejs基础知识核心模块&#xff08;内置模块&#xff09;fs&#xff08;file-system&#xff09;文件系统fs增删查改urlQuery String httprequest根据不同的请求路径发送不同的响应结果requireip地址和端口号Content-Type 第三方模块 express登录接口逻辑分析…

【LeetCode:2769. 找出最大的可达成数字 + 模拟】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…