python GPU加速 以numba为例

news2024/10/6 20:40:03

GPU编程(CUDA)

GPU(图形处理单元),多核系统,而现今的大多数CPU也属于多核系统,但它们之间还是存在很大的区别:

  • CPU适合执行复杂的逻辑,比如多分支,其核心比较重(复杂)
  • GPU适合执行简单的逻辑,大量的数据计算,其吞吐量更高,但是核心比较轻(结构简单)
    GPU本职任务时做图形图像的,将数据处理成图像,再显示。其最初是不可编程的,但后面有hacker为了实现规模较大的运算,开始研究着色语言或图形处理原语来和GPU对话,英伟达发现是商机就开发了一套平台 CUDA 专门用于GPU编程,然后深度学习火了,CUDA就越来越火,再到现在的AIGC的普及化,利用CUDA优化大规模计算基本是共识了。
  • 异构运算
    处理器不是单一类型组成的,比如仅有CPU,都是异构架构,反之就是同构架构。
    异构架构比同构架构运算量更大,但是其应用复杂度也更高,控制、传输都需要人为干预,而同构架构,硬件部分自己完成控制,不需要人为设计。
    在CPU+GPU的异构架构里
    在这里插入图片描述
  • 左图 一个4核CPU 一般有4个ALU,DRAM是内存,CPU通过总线访问内存,Cache 高速缓冲存储器
  • 右图 GPU 一个绿色小方块就是一个ALU,而红框标记的SM表示这一组的ALU公用一个Control单元和Cache,就相当于一个多核CPU,但ALU更多,且control变小,导致计算力提升,控制能力减弱,所以对于控制逻辑复杂的程序,一个GPU的SM是没办法和CPU比较的,但对于逻辑简单,数据量大的任务,GPU更加高效。
    CPU和GPU之间通过PCle总线连接,用于传递指令和数据(这也是决定性能瓶颈的地方之一)

一个在异构平台上的运行的程序,应该将低并行复杂逻辑的部分在CPU上跑,将高并行简单逻辑的部分在GPU上跑
CPU和GPU的区别:

  1. CPU线程是重量级实体,操作系统交替执行线程,线程上下文切换花销很大

  2. GPU线程是轻量级的,GPU的应用一般包含成千上万的线程,多数都在排队状态,线程之间切换基本没有开销

  3. CPU的核被设计用来尽可能减少一个或两个线程运行时间的延迟,而GPU核则是大量线程,最大幅度提高吞吐量
    cuda:一种异构计算平台
    在这里插入图片描述
    CUDA是建立在Nvidia GPU上的一整套平台,支持多种语言
    CUDA的API有两种:

  4. 低级API 驱动API 使用困难

  5. 高级API 运行时API 使用简单 其实现基于驱动API
    两种API是互斥的,两者间的函数不能混合调用。
    一个CUDA程序

  6. 分配GPU内存

  7. 拷贝内存到设备

  8. 调用CUDA内核函数来执行计算

  9. 把计算完成司机拷贝回主机端

  10. 内存销毁
    其关键点在于内存和线程的管理

  • 内存
    分为共享内存(shared Memory)和全局内存(global Memory)
    共享内存在块内,线程间共享
    全局内存在grid内共享
    在这里插入图片描述 - 线程
    GPU中的线程层次结构为grid-block-thread
    一个核函数只能有一个grid,一个grid可以有很多个块,每个块可以有很多线程。
    在这里插入图片描述
    不同块内的线程间不能相互影响,它们是物理隔离的
    所有CUDA核函数的启动都是异步的

如何计算线程下标
在这里插入图片描述
譬如上图 一个grid中有4个block 一个block有8个thread
所以gridDim=4 blockDim=8 线程数=blockDim*gridDim=32
threadIdx.x 取值是0-7

而线程下标如何映射为数组下标呢?一个数组从主机传递到GPU的内存上,现在是并行计算,就不可能用for去逐个获取i值,就得依照以下公式来获取每一个i
i=threadIdx.x+blockDim.x*blockIdx.x
在这里插入图片描述
同时也容易注意到,我们线程是32,但如果数组小于32,比如20,就会有线程溢出的问题,这时该如何解决呢?方案就是,不要做任何事情!

#以python的numba库举例
@cuda.jit
def addArray(a,b,c):
	i = cuda.threadIdx.x+cuda.blockDim.x*cuda.blockIdx.x
	if i<a.size:#线程溢出的部分就不去管它了
		c[i] = a[i]+b[i]
N = 20
a = np.arange(N,dtype=np.float32)
b = np.arange(N,dtype=np.float32)
dev_c = cuda.device_array_like(a)
addArray[4,8](a,b,dev_c)
c = dev_c.copy_to_host()
"""
注:也可以使用i=cuda.grid(1) 1表示获取1维线程索引,2表示2维,3表示3维
"""

因为硬件的限制,GPU并不能运行任意数量的线程和块,通常每个块不能超过1024个线程,一个网格不能超过 2 16 − 1 = 65535 2^{16}-1=65535 2161=65535个块,同时还要考虑内存量。

在numba中硬件限制可以通过cuda.get_current_device()获取
Grid-stride循环
当每个网格的块数超过硬件设置,但显存还有剩余,我们可以使用一个线程来处理数组中的多个元素,这被称为Grid-stride。
就是在核函数里加一个循环来处理多个输入元素。

@cuda.jit
def addArrayGS(a,b,c):
	i_start = cuda.grid(1)
	thread_per_grid = cuda.blockDim.x*cuda.gridDim.x
	for i in range(i_start,a.size,threads_per_grid):
	"""这样设置能够重复利用线程,但并不是一个线程内运行0-a.size-1 而是负责其中的一部分
	比如 当threads_per_grid = 46 a.size=89时
	i_start = 0  i会取值0,46 可以以完整取完0-88的索引
	"""
		c[i]=a[i]+b[i]
threads_per_block = 256
block_per_grid_gs = 32*80
addArrayGS[block_per_grid_gs,threads_per_block](dev_a,dev_b,dev_c)

cuda内核的计算时间
因为GPU和CPU是不通信的,因此在内核启动前后分别调用time.time() 只能获得内核启动需要的时间,而不是计算运行需要的时间。
所以为了获取准确的时间,就得调用cuda.synchronize()函数,这个函数将停止主机执行任何其他代码,直到GPU完成所有核函数。(注:numba是动态编译的,第一调用核函数会计时编译步骤,要获取计算的时间 需要二次调用才是准确的)

from time import perf_counter_ns
# Compile and then clear GPU from tasks 
addArray[blocks_per_grid, threads_per_block](dev_a, dev_b, dev_c) 
cuda.synchronize() 

timing = np.empty(101) 
for i in range(timing.size): 
    tic = perf_counter_ns() 
    addArray[blocks_per_grid, threads_per_block](dev_a, dev_b, dev_c) 
    cuda.synchronize() 
    toc = perf_counter_ns() 
    timing[i] = toc - tic 
timing *= 1e-3  # convert to μs 

print(f"Elapsed time: {timing.mean():.0f} ± {timing.std():.0f} μs") 

#  Elapsed time: 201 ± 109 μs 

# Compile and then clear GPU from tasks 
addArrayGS[blocks_per_grid_gs, threads_per_block](dev_a, dev_b, dev_c) 
cuda.synchronize() 

timing_gs = np.empty(101) 
for i in range(timing_gs.size): 
    tic = perf_counter_ns() 
    addArrayGS[blocks_per_grid_gs, threads_per_block](dev_a, dev_b, dev_c) 
    cuda.synchronize() 
    toc = perf_counter_ns() 
    timing_gs[i] = toc - tic 
timing_gs *= 1e-3  # convert to μs 

print(f"Elapsed time: {timing_gs.mean():.0f} ± {timing_gs.std():.0f} μs") 

#  Elapsed time: 178 ± 141 μs

"""对于简单的内核,还可以测量算法的整个过程获得每秒浮点运算的数量。通常以GFLOP/s (giga-FLOP /s)为度量单位。加法操作只包含一个触发器:加法。因此,吞吐量由:"""

#              G * FLOP       / timing in s 
gflops    = 1e-9 * dev_a.size * 1e6 / timing.mean() 
gflops_gs = 1e-9 * dev_a.size * 1e6 / timing_gs.mean() 

print(f"GFLOP/s (algo 1): {gflops:.2f}") 
print(f"GFLOP/s (algo 2): {gflops_gs:.2f}") 

#  GFLOP/s (algo 1): 4.99 
#  GFLOP/s (algo 2): 5.63

一个二维的例子
给定一个值在0-1之间的图像I(x,y) 进行对数校正
I z ( x , y ) = λ l o g 2 ( 1 + I ( x , y ) ) I_z(x,y) = \lambda log_2(1+I(x,y)) Iz(x,y)=λlog2(1+I(x,y))

import matplotlib.pyplot as plt 
from skimage import data 
import math 

moon = data.moon().astype(np.float32) / 255. 

fig, ax = plt.subplots() 
im = ax.imshow(moon, cmap="gist_earth") 
ax.set_xticks([]) 
ax.set_yticks([]) 
ax.set_xticklabels([]) 
ax.set_yticklabels([]) 
fig.colorbar(im) 
fig.tight_layout()


# Example 1.5: 2D kernel 
@cuda.jit 
def adjustLog(inp, gain, out): 
    ix, iy = cuda.grid(2) # The first index is the fastest dimension 
    threads_per_grid_x, threads_per_grid_y = cuda.gridsize(2) #  threads per grid dimension 
 
    n0, n1 = inp.shape # The last index is the fastest dimension 
    # Stride each dimension independently 
    for i0 in range(iy, n0, threads_per_grid_y): 
        for i1 in range(ix, n1, threads_per_grid_x): 
            out[i0, i1] = gain * math.log2(1 + inp[i0, i1]) 

#设计二维的线程结构
threads_per_block_2d = (16, 16)  #  256 threads total 
blocks_per_grid_2d = (64, 64) 

moon_gpu = cuda.to_device(moon) 
moon_corr_gpu = cuda.device_array_like(moon_gpu) 

adjustLog[blocks_per_grid_2d, threads_per_block_2d](moon_gpu, 1.0, moon_corr_gpu) 
moon_corr = moon_corr_gpu.copy_to_host() 

fig, (ax1, ax2) = plt.subplots(1, 2) 
ax1.imshow(moon, cmap="gist_earth") 
ax2.imshow(moon_corr, cmap="gist_earth") 
ax1.set(title="Original image") 
ax2.set(title="Log-corrected image") 
for ax in (ax1, ax2): 
    ax.set_xticks([]) 
    ax.set_yticks([]) 
    ax.set_xticklabels([]) 
    ax.set_yticklabels([]) 
fig.tight_layout()

参考

  • 谭升的博客
  • 从头开始CUJDA编程:Numba并行编程的基本概念
  • numba文档

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

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

相关文章

JVM内部世界(内存划分,类加载,垃圾回收)

&#x1f495;"Echo"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;JVM内部世界(内存划分,类加载,垃圾回收) 关于JVM的学习主要掌握三方面: JVM内存区的划分类加载垃圾回收 一.JVM内存区的划分 当一个Java进程开始执行时,JVM会首先向操作系统申…

给数字人生成加上界面,基于ER-NeRF/RAD-NeRF/AD-NeRF,Gradio框架构建WEBUI,使用HLS流媒体,实现边推理边播放——之一:在WEBUI中实时输出服务器控制台日志

前言 目前数字人实现技术众多&#xff0c;我这里采用基于ER-NeRF&#xff0c;在这里可以看到其介绍&#xff1a;ICCV 2023 | ER-NeRF: 用于合成高保真Talking Portrait的高效区域感知神经辐射场-https://zhuanlan.zhihu.com/p/644520609ER-NeRF的项目地址&#xff1a;https://…

十四、重写与多态

重写、多态 上一讲是&#xff0c;子类对父类横向上的扩展 这一讲是&#xff0c;子类对父类纵向上的扩展 方法重写 使用override关键字重写父类的方法 将父类原本方法的逻辑更新成新版本的逻辑 注&#xff1a;仅能重写可见的父类成员&#xff0c;并且重写要保持签名一致。 签名一…

第五节 JDBC驱动程序类型

JDBC驱动程序是什么&#xff1f; JDBC驱动程序在JDBC API中实现定义的接口&#xff0c;用于与数据库服务器进行交互。 例如&#xff0c;使用JDBC驱动程序&#xff0c;可以通过发送SQL或数据库命令&#xff0c;然后使用Java接收结果来打开数据库连接并与数据库进行交互。 JDK…

Java中常见的 IO 方式

冯诺依曼结构中计算机结构被分为 5 大部分&#xff1a;运算器、控制器、存储器、输入设备、输出设备&#xff0c;输入设备向计算机输入数据&#xff0c;输出设备接收计算机输出的数据。从计算机结构的视角来看的话&#xff0c; I/O 描述了计算机系统与外部设备之间通信的过程。…

JMeter VS RunnerGo :两大主流性能测试工具对比

说起JMeter&#xff0c;估计很多测试人员都耳熟能详。它小巧、开源&#xff0c;还能支持多种协议的接口和性能测试&#xff0c;所以在测试圈儿里很受欢迎&#xff0c;也是测试人员常用的工具&#xff0c;不少企业也基于JMeter建立起自己的自动化测试能力&#xff0c;提升工作效…

体验Node.js的安装和运行

Node.js概述 Node.js是一个基于Chrome V8引擎的JavaScript运行环境。它允许JavaScript代码在服务器端运行&#xff0c;使得开发人员可以使用同一种语言编写前端和后端的代码。Node.js使用事件驱动、非阻塞I/O模型&#xff0c;使其轻量且高效&#xff0c;非常适合数据密集型的实…

java性能调优面试,程序员Java视频

前言 很多人在打算自学Java的时候或许都没有思考过Java的应用方向&#xff0c;市场需要什么样的人才&#xff0c;企业对你有什么要求等等一系列问题&#xff1b;或许你只听说这个行业薪资高…然后懵懵懂懂的上路&#xff0c;不得要害。 对于零基础来学习Java&#xff0c;你或…

怎样才能考上南京大学的计算机研究生?

附上南大与同层次学校近四年的分数线对比&#xff0c;整体很难 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 我本人是双非一本的计算机专业&#xff0c;23考研一战上岸的&#xf…

【C语言】还有柔性数组?

前言 也许你从来没有听说过柔性数组&#xff08;flexible array&#xff09;这个概念&#xff0c;但是它确实是存在的。C99中&#xff0c;结构中的最后⼀个元素允许是未知⼤⼩的数组&#xff0c;这就叫做『柔性数组』成员。 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xf…

LinkedList集合源码分析

LinkedList集合源码分析 文章目录 LinkedList集合源码分析一、字段分析二、构造函数分析三、方法分析四、总结 看到实现了Deque 就要联想到这个数据结构肯定是属于双端队列了。Queue 表示队列&#xff0c;Deque表示双端队列。 一、字段分析 LinkedList 字段很少&#xff0c;就…

Vector Search和专用Search Nodes:现已正式发布

我们非常高兴地推出了 Atlas Vector Search 和 Search Nodes 的正式发布版本 (GA)&#xff0c;为 Atlas 平台增添了更多价值。 自从在公开预览版中发布 Atlas Vector Search 和带有 Search Nodes 的专用基础架构以来&#xff0c;我们注意到&#xff0c;对于使用向量优化搜索节…

【数据集】MSWEP(多源加权集合降水)再分析数据

MSWEP全球降水数据 数据概述数据下载参考数据概述 MSWEP(Multi-Source Weighted-Ensemble Precipitation)降水数据集是一种高分辨率、全球覆盖的降水数据产品,它融合了多种来源的降水信息,包括卫星遥感数据、雷达观测、地面气象站观测数据以及数值天气预报模型的输出。MSW…

C switch 语句

一个 switch 语句允许测试一个变量等于多个值时的情况。每个值称为一个 case&#xff0c;且被测试的变量会对每个 switch case 进行检查。 语法 C 语言中 switch 语句的语法&#xff1a; switch(expression){case constant-expression :statement(s);break; /* 可选的 */ca…

Redis线程模型解析

引言 Redis是一个高性能的键值对&#xff08;key-value&#xff09;内存数据库&#xff0c;以其卓越的读写速度和灵活的数据类型而广受欢迎。在Redis 6.0之前的版本中&#xff0c;它采用的是一种独特的单线程模型来处理客户端的请求。尽管单线程在概念上似乎限制了其扩展性和并…

C++ 之AVL树

AVL树 AVL树的基本概念AVL树的平衡因子、AVL树的旋转avl的双旋旋转的4种情况 AVL树的基本概念 AVL树的平衡因子、 AVL树的旋转 当平衡因子的高度差过大时&#xff0c;就要选择。所谓的选择其实也是一种压扁的操作 在本例中 新插入的蓝色结点使得不在平衡。 我们看上图就能…

【CSP试题回顾】201604-1-折点计数

CSP-201604-1-折点计数 解题代码 #include <iostream> #include <vector> #include <algorithm> using namespace std;int n, pointSum;int main() {cin >> n;vector<int>myData(n);for (int i 0; i < n; i){cin >> myData[i];}// 统…

深色系可视化界面看腻了,来点浅色系?安排,20页来了。

只要不放在大屏上展示&#xff0c;贝格前端工场还是非常推崇浅色系的可视化界面&#xff0c;把它作为配色的首选 。浅色系可视化界面具有以下几个优势&#xff1a; 清晰明了 浅色系界面通常使用明亮的颜色&#xff0c;如白色、浅灰色等&#xff0c;使界面元素更加清晰可见。这…

文件MD5校验码的安全性及重要性

title: 文件MD5校验码的安全性及重要性 date: 2024/3/6 18:13:20 updated: 2024/3/6 18:13:20 tags: MD5原理文件校验下载验证数据库一致性安全性保障计算方法MD5安全防护 文件MD5&#xff08;Message Digest Algorithm 5&#xff09;是一种常用的哈希算法&#xff0c;用于验证…

Docker部署(ruoyi案例接上篇Docker之部署前后端分离项目)实施必会!!!!

文章目录 Docker部署前端 Docker部署前端 接上篇博主已经部署好后端Docker部署后端&#xff0c;现在来讲解怎么部署前端 MySQL和redis是不依赖其他任何一个东西的&#xff0c; ruoyi-admin是因为你启动项目的时候是必须连接数据库的 现在去单独启动它 docker start ruoyi-a…