浅析GPU计算——cuda编程

NMS处理候选框(普通的NMS、numpy 、torch官方 和 GPU) 阅读详情

        在《浅析GPU计算——CPU和GPU的选择》一文中,我们分析了在遇到什么瓶颈时需要考虑使用GPU去进行计算。本文将结合cuda编程来讲解实际应用例子。(转载请指明出于breaksoftware的csdn博客)

        之前我们讲解过,CPU是整个计算机的核心,它的主要工作是负责调度各种资源,包括其自身的计算资源以及GPU的计算计算资源。比如一个浮点数相乘逻辑,理论上我们可以让其在CPU上执行,也可以在GPU上执行。那这段逻辑到底是在哪个器件上执行的呢?cuda将决定权交给了程序员,我们可以在函数前增加修饰词来指定。

关键字执行位置
__host__CPU
__global__GPU
__device__GPU

        一般来说,我们只需要2个修饰词就够了,但是cuda却提供了3个——2个执行位置为GPU。这儿要引入一个“调用位置”的概念。父函数调用子函数时,父函数可能运行于CPU或者GPU,相应的子函数也可能运行于CPU或者GPU,但是这绝不是一个2*2的组合关系。因为GPU作为CPU的计算组件,不可以调度CPU去做事,所以不存在父函数运行于GPU,而子函数运行于CPU的情况。

关键字调用位置
__host__CPU
__global__CPU
__device__GPU

        __global__描述的函数就是“被CPU调用,在GPU上运行的代码”,同时它也打通了__host__和__device__修饰的函数。

        如果一段代码既需要运行于CPU,也要运行于GPU,怎么办?难道要写两次?当然不用,我们可以同时使用__host__和__device__修饰。这样编译器就会帮我们生成两份代码逻辑。

#include "cuda.h" 
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>

__host__ __device__ int run_on_cpu_or_gpu() {
	return 1;
}

__global__ void run_on_gpu() {
	printf("run_on_cpu_or_gpu GPU: %d\n", run_on_cpu_or_gpu());
}

int main() {
	printf("run_on_cpu_or_gpu CPU: %d\n", run_on_cpu_or_gpu());
	run_on_gpu<<<1, 1>>>();
	return 0;
}

        上例中,第11行调用的是run_on_cpu_or_gpu的GPU实现,而第15行的调用的是的CPU实现。

        可以看到,为了实现上面的例子,我引入了一个__global__函数——run_on_gpu用于衔接CPU和GPU,那么有人可能会问:如果__global__和__host__、__device__一起修饰函数不就行了?实际上cuda禁止了这种方案,而且__global__不能和它们中任何一个修饰符一起使用。

        __global__这个修饰符的使命使得它足够的特殊。比如它修饰的函数是异步执行的。我们稍微修改一下上面main函数

int main() {
	printf("run_on_cpu_or_gpu CPU: %d\n", run_on_cpu_or_gpu());
	run_on_gpu<<<1, 1>>>();
	printf("will end\n");
	return 0;
}

        如果上述代码都是同步执行的,那么will end将是最后一条输出,然而输出却是

        可见__global__修饰的函数是被异步执行的。

        __global__修饰的函数只能是void类型。我们假设其可以有返回值,则其调用者是可以捕获这个值的。但是__global__函数是异步调用的,当函数返回时,接受返回值的变量可能已经被销毁了。所以设计其有返回值也没太多意义。

int main() {
	printf("run_on_cpu_or_gpu CPU: %d\n", run_on_cpu_or_gpu());
	{
		int ret = run_on_gpu<<<1, 1>>>(); // error!!!even if run_on_gpu return int!!
	}
	printf("will end\n");
	return 0;
}

        还有人会问,上面main函数怎么没有用修饰符修饰?cuda编程规定如果没有使用修饰符修饰的默认就是__host__类型。这种设计让大家熟悉的规则成为默认的规则,可以让更多第三方代码不用修改就直接被cuda编译器编译使用。

        cuda是一个GPU编程环境,所以它对__device__修饰的函数进行了比较多的优化。比如它会根据它的规则,让某个__device__修饰函数成为内联函数(inline)。这些规则是程序员不可控,但是如果我们的确对是否内联有需求,cuda也提供了方式:使用__noinline__修饰函数不进行内联优化;使用 __forceinline__修饰函数强制进行内联优化。当然这两种修饰符不能同时使用。

        也许你已经发现,__global__函数调用方式非常特别——使用“<<<>>>”标志。这儿就需要引入cuda的并行执行的线程模型来解释了。在同一时刻,一个cuda核只能运行一个线程,而线程作为逻辑的运行载体有其自己的ID。这个ID和我们在linux或windows系统上CPU相关的线程ID有着不同的表达方式。比如在Linux系统上可以使用gettid方法获取一个pid_t值,比如3075。但是cuda的表达方式是一个三维空间,表达这个空间的是一个叫block的概念。比如单个block定义其有(Dx, Dy, 0)个线程,则每个线程ID为x+yDx;再比如有(Dx, Dy, Dz)个线程,则每个线程ID为x+yDx+zDxDy。

__global__ void run_on_gpu() {
	printf("GPU thread info X:%d Y:%d Z:%d\t block info X:%d Y:%d Z:%d\n",
		threadIdx.x, threadIdx.y, threadIdx.z, blockIdx.x, blockIdx.y, blockIdx.z);
}

int main() {
	dim3 threadsPerBlock(2, 3, 4);
	int blocksPerGrid = 1;
	run_on_gpu<<<blocksPerGrid, threadsPerBlock>>>();
	return 0;
}

        其运行结果如下

        承载block的是一个叫做grid的概念。block在grid中的位置也是通过一个三维结构来表达,比如下面代码标识的是一个一个Grid包含(3,3,3)结构的Block,一个Block包含(3,3,3)结构的Thread

	dim3 blocksPerGrid(3, 3, 3);
	dim3 threadsPerBlock(3, 3, 3);
	run_on_gpu<<<blocksPerGrid, threadsPerBlock>>>();

        对于上例中的各个线程的ID算法就更加复杂了,详细的计算规则可以见《CUDA(10)之深入理解threadIdx》。

        为什么cuda的线程要设计的这么复杂?我想其可能和GPU设计的初始目的有关——图像运算。而我们肉眼的感官就是三维的,所以GPU有大量三维计算的需求。

        个人觉得大家不要拘泥于threadID的计算,而要学会如何利用blockIdx、threadIdx的三维坐标来进行并行计算。比如我们把问题的维度降低,看看一个Grid只有一个Block,一个Block中N*N个线程,计算两个N*N矩阵相加的例子

// Kernel definition 
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) { 
	int i = threadIdx.x; 
	int j = threadIdx.y; 
	C[i][j] = A[i][j] + B[i][j]; 
} 
int main() { 
	... 
	// Kernel invocation with one block of N * N * 1 threads 
	int numBlocks = 1; 
	dim3 threadsPerBlock(N, N); 
	MatAdd<<<numBlocks, threadsPerBlock>>>(A, B, C); 
	... 
}

        矩阵相加是一个非常好说明GPU并行计算的例子。结合上面的代码,我们假设GPU中有大于N*N个空闲的cuda核,且假设调度器同时让这N*N个线程运行,则整个计算的周期可以认为是一个元的计算周期。而CPU却要串行处理每个元的计算(不考虑CPU中向量计算单元)。

        那矩阵相加的什么特性让其成为一个经典的案例呢?那就是“可并行性”!因为每个元的计算都不依赖于其他元的计算结果,所以这种计算是适合并行进行的。如果一个逻辑的“可并行计算单元”越多越连续,其就越适合使用GPU并行计算来优化性能。

        可能有人提出一个疑问:假如N非常大,比如是128,那么就需要100*100=10000个线程来执行。而目前最高配的GPU只有5120个cuda核,那这些线程是如何在cuda核上调度的呢?这儿要引入一个叫做warp的概念,它是一个线程集合。假如GPU的warp包含32个线程,则我们的任务被打包成10000/32 =313个warp(除不尽。其中312个warp包含32个我们的计算线程,而剩下的16将和16个空线程被打包成一个warp)。这313个warp分批次在GPU上调度执行,这样设计有几个好处:可以成批有序的执行线程逻辑,且发生等待操作时,可以切换其他warp去工作,从而提供cuda核心的利用率。

        我们再看下计算的数据源。一般情况下,数据源是由CPU发射到GPU上去的,于是连接GPU和主板的PCIe接口带宽至关重要。目前设计中的PCIe5的理论最大带宽可以达到64GB/s(https://zh.wikipedia.org/wiki/PCI_Express),而CPU和内存之间的访问带宽也不到100GB/s。通过这两者比较,我们发现内存和PCIe之间,明显PCIe带宽是瓶颈。那这个瓶颈重要么?我们再看下目前NV的显存带宽是多少——超过700GB/s。可以说PCIe和显存带宽差一个数量级。下图可以让我们更直观的看到问题

        如果我们把数据从DRAM一次性保存到GDRAM,然后GPU直接访问GDRAM上的数据,这和频繁通过PCIe读写数据相比,无疑大幅提高了效率。等到数据都处理完毕,我们再将GDRAM的数据发送回CPU。cuda为我们提供了cudaMemcpy系列方法,让我们可以在DRAM和GDRAM之间传递数据。我们把上面矩阵相加的例子加以改造

// Device code 
__global__ void VecAdd(float* A, float* B, float* C, int N) { 
	int i = blockDim.x * blockIdx.x + threadIdx.x; 
	if (i < N) 
		C[i] = A[i] + B[i]; 
} 

// Host code 
int main() { 
	int N = ...; 
	size_t size = N * sizeof(float); 

	// Allocate input vectors h_A and h_B in host memory 
	float* h_A = (float*)malloc(size); 
	float* h_B = (float*)malloc(size); 

	// Initialize input vectors 
	... 

	// Allocate vectors in device memory 
	float* d_A; cudaMalloc(&d_A, size); 
	float* d_B; cudaMalloc(&d_B, size); 
	float* d_C; cudaMalloc(&d_C, size); 

	// Copy vectors from host memory to device memory 
	cudaMemcpy(d_A, h_A, size, cudaMemcpyHostToDevice); 
	cudaMemcpy(d_B, h_B, size, cudaMemcpyHostToDevice); 

	// Invoke kernel 
	int threadsPerBlock = 256; 
	int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock; 
	VecAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, N); 

	// Copy result from device memory to host memory 
	// h_C contains the result in host memory 
	cudaMemcpy(h_C, d_C, size, cudaMemcpyDeviceToHost); 

	// Free device memory 
	cudaFree(d_A); 
	cudaFree(d_B); 
	cudaFree(d_C); 
	// Free host memory 
	... 
}

        通过这两章的浅析,我们可以大致了解GPU并行计算的相关概念,以及使用cuda实现并行计算的基本操作。如果大家还想有更加深入的了解,可以参见《cuda c programming guide》。还可以参考周斌老师《NVIDIA CUDA初级教程视频》。

GPU大模型算法杂谈 CUDA(Compute Unified Device Architecture)是NVIDIA推出的一种并行计算平台和编程模型,允许开发者使用C/C++语言在GPU上开发并行计算算法。CUDA的并行计算能力主要体现在其能够同时执行大量的线程,从而加速数据密集型计算任务。设备管理API内存管理APIcudaMalloccudaFreecudaMemcpycudaMemset流和事件管理API核函数管理APIcudaLaunch错误处理API纹理和表面API设备属性查询API其他辅助API。 阅读详情

相关推荐

深入浅出之cuda编程概念

在CUDA编程模型中,主机通常指的是CPU及其附属的内存资源。它是执行串行代码和进行设备管理的核心。

浩瀚之水的专栏 5013

gpu 编程

由于项目原因,需要对CPU代码进行cuda代码转换,从而达到加速的目的,在这里监督介绍一下整个转换的流程。 cuda代码与c++代码其实相似性很高,其核心点在于如何在cpu调用gpu进行线程分配,从而达到加速计算的目的。 对于gpu,必须包含的头文件有cuda_runtime.h,device_launch_parameters.h 这两个头文件是用来检测gpu显卡以及对其内存进行操作。 1. 确认可使用设备 struct cudaDeviceProp properties; cuda

ltd0924的博客 8249

Nvidia显卡架构解析与cuda应用生态浅析

nvidia cuda

猛码Memmat 383

GPU基础与CUDA编程入门

预取是一种内存管理策略,旨在减少内存访问延迟,从而提高计算性能。预取机制通过预先加载数据到高速缓存(例如,从全局内存到共享内存或纹理内存)来实现这一目标,以便在执行计算任务时减少等待时间。GPU的预取机制有两种形式:硬件预取:这是由GPU硬件自动实现的预取机制,不需要程序员进行显式操作。GPU内部的内存控制器会预测内存访问模式,提前将可能需要的数据加载到高速缓存中。这种预取机制在许多现代GPU架构(如NVIDIA的Pascal、Volta和Ampere架构)中都有实现。

qq_43398345的博客 3669

Python批量注册脚本开发详细

开发一个Python批量注册脚本需要考虑多个方面,包括目标网站的注册流程、验证码处理、请求发送、数据存储等。以下是一个详细的开发流程和代码示例,用于实现基本的批量注册功能。

nsp202505的博客 3344

WPS接入DeepSeek,实现AI辅助功能

总的来说,借助 WPS 中宏功能与 DeepSeek 大模型的结合,文档处理、内容创作和数据分析等任务都能更加智能化、自动化。这不仅大幅提升了工作效率,也为办公场景带来了全新的智能化办公体验。

运维老生常谈 3804

[转]浅析GPU计算——cuda编程

在《浅析GPU计算——CPU和GPU的选择》一文中,我们分析了在遇到什么瓶颈时需要考虑使用GPU去进行计算。本文将结合cuda编程来讲解实际应用例子。(转载请指明出于breaksoftware的csdn博客) 之前我们讲解过,CPU是整个计算机的核心,它的主要工作是负责调度各种资源,包括其自身的计算资源以及GPU的计算计算资源。比如一个浮点数相乘逻辑,理论上我们可以让其在CPU上执行,也可以在GPU上执行。那这段逻辑到底是在哪个器件上执行的呢?cuda将决定权交给了程序员,...

江州司马 499

GIS常用符号化色带(超级色带)根据python制作

GIS自带的色带比较单一,今天分享一款色带,应用之后大家出图的时候可以选择更多的配色,符号化/点击色块/样式引用/将样式添加至列表即可。色带类型比较多,大家可以自己尝试下不同的风格。简单符号化一下,看看效果。以中国县级行政区划为例,

J_Giser的博客 2637

GPU加速原理浅析及代码实现

一、CUDA简介 CUDA(Compute Unified Device Architecture)是一种由NVIDIA推出的通用并行计算架构,该架构使GPU(Graphics Processing Unit)能够对复杂的计算问题做性能速度优化。 二、GPU架构特点 首先我们先来了解一下串行计算和并行计算。众所周知,高性能计算的关键是利用多核处理器进行并行计算。 当我们求解一个计算机程序任务时,我们很自然的想法就是将该任务分解成一系列小任务,把这些小任务一一完成。在串行计算时,我们的想法就是让我们的处理

小白云 1万+

Dify 第 13 篇 Dify 工作流 详解

工作流通过将复杂任务分解为节点降低复杂度,提升LLM应用的性能和稳定性。Dify工作流分为两类:Chatflow面向对话场景(如客服),支持多轮对话;Workflow面向批处理场景(如翻译),提供逻辑节点实现自动化。核心概念包括节点、变量(系统/会话/环境变量)及应用类型差异:Chatflow支持中间输出和对话历史,Workflow仅支持结束节点输出。系统变量以"sys"开头,用于全局数据传递,不同应用类型变量不同,如Chatflow的sys.query存储用户输入,Workflow的s

甘蓝的专栏 3246

Allegro如何去掉PCB内层未使用的过孔孔环?

在用Allegro进行PCB设计过程中,遇到铺铜铜箔被焊盘大量割断,导致铺铜不完整,这样就会导致铺铜过电流太小,影响板的性能,严重的会导致板工作不稳定,甚至不工作。解决办法:是去掉内层未使用的过孔孔环,增大过铜箔面积。

bhxc809751502的博客 2571

浅析GPU计算——CPU和GPU的选择

目前市面上介绍GPU编程的博文很多,其中很多都是照章宣科,让人只能感受到冷冷的技术,而缺乏知识的温度。所以我希望能写出一篇可以体现技术脉络感的文章,让读者可以比较容易理解该技术,并可以感悟到cuda编程设计及优化的原理。(转载请指明出于breaksoftware的csdn博客) 谈到计算,我们一般都会先想到CPU。CPU的全称是Central Processing...

方亮的专栏 3万+

从 on_event 到 lifespan:FastAPI 生命周期管理的旧方法与新方法对比与迁移

从 on_event 到 lifespan:FastAPI 生命周期管理的旧方法与新方法对比与迁移

XiaoqiangClub的博客 1021

CUDA计算

GPU硬件架构综述,CUDA编程模型在GPU的计算部分如何运作?以及常见GPU内存优化策略

tk真辣鸡的博客 3945

Cadence Orcad Capture原理图断线的检查与避免方法

在设计电路图中,尤其是直接借鉴别人的设计进行二次开发的过程中难免会遇到如下尴尬的情况。有没有眼神好的朋友发现图纸的哪里有不对劲的地方呢?

fydar的博客 3267

Apache Kafka Connect任意文件读取漏洞(CVE-2025-27817)

由于Apache Kafka客户端未对用户输入进行严格验证和限制,未经身份验证的攻击者可通过构造恶意配置读取环境变量或磁盘任意内容,或向非预期位置发送请求,提升REST API的文件系统/环境/URL访问权限。(CVE-2025-27817)

iSee857的博客 2026

CUDA编程学习4——程序编程范式浅析

编程模型浅析,基础原始框架展开

Mr_yangsc的博客 580

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

CUDA学习笔记(一):浅析GPU计算——CPU和GPU的选择

转载自优快云:Never-Giveup 目前市面上介绍GPU编程的博文很多,其中很多都是照章宣科,让人只能感受到冷冷的技术,而缺乏知识的温度。所以我希望能写出一篇可以体现技术脉络感的文章,让读者可以比较容易理解该技术,并可以感悟到cuda编程设计及优化的原理。 谈到计算,我们一般都会先想到CPU。CPU的全称是Central Processing...

abstract0071的博客 747

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

cuda从入门到精通(二)之函数关键字

本文系转载,出处:浅析GPU计算——cuda编程 CUDA编程模型基础 在给出CUDA的编程实例之前,这里先对CUDA编程模型中的一些概念及基础知识做个简单介绍。CUDA编程模型是一个异构模型,需要CPU和GPU协同工作。在CUDA中,host和device是两个重要的概念,我们用host指代CPU及其内存,而用device指代GPU及其内存。CUDA程序中既包含host程序,又包含device程...

xiangxianghehe的博客 3415

20220831-basic-cuda-interface

layout title categories tags date description post CUDA并行编程学习笔记 blog GPU编程 2022-08-31 12:39:39 -0700 CUDA并行编程指南 名词 ...

DeepIndeed 768

GPU内存浅析

假设所有线程都需要访问一个 constant_A 的常量,在存储介质上 constant_A 的数据只保存了一份,而内存的物理读取方式决定了多个线程不能在同一时刻读取到该变量,所以会出现先后访问的问题,这样使得并行计算的线程出现了运算时差。按照存储功能进行细分,GPU 内存可以分为:局部内存(local memory)、全局内存(global memory)、常量内存(constant memory)、共享内存(shared memory)、寄存器(register)、L1/L2 缓存等。

Eric005的博客 367

计算机组成原理视角:模型推理在GPU上的并行计算浅析

本文从计算机组成原理视角,分析了模型推理在GPU上的并行计算优势,并以cv_resnet101_face-detection_cvpr22papermogface镜像为例进行阐述。用户可在星图GPU平台上自动化部署该镜像,快速搭建高效的人脸检测环境,典型应用于安防监控、照片智能分类等需要实时或批量处理图片中的人脸识别场景。

weixin_42588555的博客 92
上一篇: 浅析GPU计算——CPU和GPU的选择
下一篇: zookeeper快速入门——简介
breaksoftware
breaksoftware 领域专家: 操作系统技术领域 领域专家: 操作系统技术领域
博客等级 码龄20年 6901粉丝 724原创
评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值