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

Python Numba多流和共享内存CUDA优化技术学习记录

CUDA优化方向
  • 充分利用GPU的多核心,最大化并行执行度
  • 优化内存使用,最大化数据吞吐量,减少不必要的数据拷贝
并行计算优化
网格跨度(grid-stride loop)

CUDA的执行配置:[gridDim, blockDim]中的blockDim最大只能是1024,gridDim最大为一个32位整数的最大值,也就是2,147,483,648,大约二十亿。这个数字已经非常大了,足以应付绝大多数的计算,但是如果对并行计算的维度有更高需求呢?网格跨度有更好的并行计算效率。
在这里插入图片描述

这里仍然以[2, 4]的执行配置为例,该执行配置中整个grid只能并行启动8个线程,假如我们要并行计算的数据是32,会发现后面8号至31号数据共计24个数据无法被计算。

在这里插入图片描述

我们可以在0号线程中,处理第0、8、16、24号数据,就能解决数据远大于执行配置中的线程总数的问题,用程序表示,就是在核函数里再写个for循环。

from numba import cuda
# 一维
@cuda.jit
def gpu_print(N):
    idxWithinGrid = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x # 某个thread在整个grid的位置
    gridStride = cuda.gridDim.x * cuda.blockDim.x # 每个grid的block数量*每个block的thread数量
    # 从 idxWithinGrid 开始
    # 每次以整个网格线程总数为跨步数
    for i in range(idxWithinGrid, N, gridStride):
        print(i)
 # 二维
 def fun():
    Thread_x = cuda.blockIdx.x* duda.blockDim.x+cuda.threadIdx.x
    Thread_y = cuda.blockIdx.y* cuda.blockDim.y+cuda.threadIdx.y
    stride_x = blockDim.x * gridDim.x
    stride_y = blockDIm.y * gridDim.y
    for(int y = Thread_y; y<height; y+=stride_y)
        for(int x = Thread_x;x<width; x+=stride_x)

def main():
    gpu_print[2, 4](32)
    cuda.synchronize()

if __name__ == "__main__":
    main()

注意,跨步大小为网格中线程总数,用gridDim.x * blockDim.x来计算。for循环的step是网格中线程总数,这也是为什么将这种方式称为网格跨步如果网格总线程数为1024,那么0号线程将计算第0、1024、2048…号的数据。这里我们也不用再明确使用if (idx < N)来判断是否越界,因为for循环也有这个判断。

  • 优势
    1. 扩展性:可以解决数据量比线程数大的问题
    2. 线程复用:CUDA线程启动和销毁都有开销,主要是线程内存空间初始化的开销;不使用网格跨步,CUDA需要启动大于计算数的线程,每个线程内只做一件事情,做完就要被销毁;使用网格跨步,线程内有for循环,每个线程可以干更多事情,所有线程的启动销毁开销更少。
    3. 方便调试:我们可以把核函数的执行配置写为[1, 1],如下所示,那么核函数的跨步大小就成为了1,核函数里的for循环与CPU函数中顺序执行的for循环的逻辑一样,非常方便验证CUDA并行计算与原来的CPU函数计算逻辑是否一致。
      kernel_function[1,1](...)
多流

之前我们讨论的并行,都是线程级别的,即CUDA开启多个线程,并行执行核函数内的代码。GPU最多就上千个核心,同一时间只能并行执行上千个任务。当我们处理千万级别的数据,整个大任务无法被GPU一次执行,所有的计算任务需要放在一个队列中,排队顺序执行。CUDA将放入队列顺序执行的一系列操作称为流(Stream)

由于异构计算的硬件特性,CUDA中以下操作是相互独立的,通过编程,是可以操作他们并发地执行的:

  • 主机端上的计算设备端的计算(核函数)数据
  • 从主机和设备间相互拷贝数据
  • 从设备内拷贝或转移数据
  • 从多个GPU设备间拷贝或转移
    针对这种互相独立的硬件架构,CUDA使用多流作为一种高并发的方案:把一个大任务中的上述几部分拆分开,放到多个流中,每次只对一部分数据进行拷贝、计算和回写,并把这个流程做成流水线。因为数据拷贝不占用计算资源,计算不占用数据拷贝的总线(Bus)资源,因此计算和数据拷贝完全可以并发执行。如图所示,将数据拷贝和函数计算重叠起来的,形成流水线,能获得非常大的性能提升。实际上,流水线作业的思想被广泛应用于CPU和GPU等计算机芯片设计上,以加速程序
    在这里插入图片描述

默认情况下,CUDA使用0号流,又称默认流。不使用多流时,所有任务都在默认流中顺序执行,效率较低。在使用多流之前,必须先了解多流的一些规则

  • 给定流内的所有操作会按序执行。

  • 非默认流之间的不同操作,无法保证其执行顺序。

  • 所有非默认流执行完后,才能执行默认流;默认流执行完后,才能执行其他非默认流。
    在这里插入图片描述

  • 非默认流1中,根据进流的先后顺序,核函数1和2是顺序执行的。

  • 无法保证核函数2与核函数4的执行先后顺序,因为他们在不同的流中。他们执行的开始时间依赖于该流中前一个操作结束时间,例如核函数2的开始依赖于核函数1的结束,与核函数3、4完全不相关。

  • 默认流有阻塞的作用。如图中红线所示,如果调用默认流,那么默认流会等非默认流都执行完才能执行;同样,默认流执行完,才能再次执行其他非默认流。

可见,某个流内的操作是顺序的,非默认流之间是异步的,默认流有阻塞作用

定义流:stream = numba.cuda.stream()

CUDA的数据拷贝以及核函数都有专门的stream参数来接收流,以告知该操作放入哪个流中执行:

  • numba.cuda.to_device(obj, stream=0, copy=True, to=None)

  • numba.cuda.copy_to_host(self, ary=None, stream=0)
    核函数调用的地方除了要写清执行配置,还要加一项stream参数:

  • kernel[blocks_per_grid, threads_per_block, stream=0]
    根据这些函数定义也可以知道,不指定stream参数时,这些函数都使用默认的0号流。
    向量加法案例:

from numba import cuda
import numpy as np
import math
from time import time

@cuda.jit
def gpu_add(a, b, result, n):
    idx = cuda.threadIdx.x + cuda.blockDim.x * cuda.blockIdx.x
    if idx < n :
        result[idx] = a[idx] + b[idx]

def main():
    n = 20000000
    x = np.arange(n).astype(np.int32)
    y = 2 * x
    
    start = time()
    x_device = cuda.to_device(x)
    y_device = cuda.to_device(y)
    out_device = cuda.device_array(n)

    threads_per_block = 1024
    blocks_per_grid = math.ceil(n / threads_per_block)
    
    # 使用默认流
    gpu_add[blocks_per_grid, threads_per_block](x_device, y_device, out_device, n)
    gpu_result = out_device.copy_to_host()
    cuda.synchronize()
    print("gpu vector add time " + str(time() - start))
    
    start = time()

    # 使用5个stream
    number_of_streams = 5
    # 每个stream处理的数据量为原来的 1/5
    # 符号//得到一个整数结果
    segment_size = n // number_of_streams
    
    # 创建5个cuda stream
    stream_list = list()
    for i in range (0, number_of_streams):
        stream = cuda.stream()
        stream_list.append(stream)
    
    threads_per_block = 1024
    # 每个stream的处理的数据变为原来的1/5
    blocks_per_grid = math.ceil(segment_size / threads_per_block)
    streams_out_device = cuda.device_array(segment_size)
    streams_gpu_result = np.empty(n)
    
    # 启动多个stream
    for i in range(0, number_of_streams):
        # 传入不同的参数,让函数在不同的流执行
        x_i_device = cuda.to_device(x[i * segment_size : (i + 1) * segment_size], stream=stream_list[i])
        y_i_device = cuda.to_device(y[i * segment_size : (i + 1) * segment_size], stream=stream_list[i])
        
        gpu_add[blocks_per_grid, threads_per_block, stream_list[i]](
                x_i_device, 
                y_i_device, 
                streams_out_device,
                segment_size)
        
        streams_gpu_result[i * segment_size : (i + 1) * segment_size] = streams_out_device.copy_to_host(stream=stream_list[i])

    cuda.synchronize()
    print("gpu streams vector add time " + str(time() - start))

if __name__ == "__main__":
    main()

在上面的程序中,我将向量分拆成了5份,同时也创建了5个流,每个流执行1/5的“拷贝、计算、回写”操作,多个流之间异步执行,最终得到非常大的性能提升。对于计算密集型的程序,这种技术非常值得认真研究。


http://www.kler.cn/a/509984.html

相关文章:

  • java图像文件的显示
  • 物联网网关Web服务器--Boa服务器移植与测试
  • 某讯一面,感觉问Redis的难度不是很大
  • 使用 Docker 部署 Java 项目(通俗易懂)
  • git操作(Windows中GitHub)
  • 基于华为云车牌识别服务设计的停车场计费系统【华为开发者空间-鸿蒙】
  • eBay账号安全攻略:巧妙应对风险
  • python如何设计矩阵
  • RPA编程实践:Electron简介
  • 国产化中间件东方通TongWeb环境安装部署(图文详解)
  • 【机器学习:二十五、处理倾斜数据集的完整指南】
  • Linux网络connect断线重连
  • 机器学习08-Transfomer注意力机制
  • 比postman还好用的接口调用工具APIPOST
  • 重学设计模式-单例模式
  • 掌握 TypeScript 的 `Pick` 工具类型:轻松提取所需属性
  • 1.8 GPT-4:开创人工智能的新纪元
  • AI Agent:AutoGPT的使用方法
  • 【机器学习实战入门】使用Pandas和OpenCV进行颜色检测
  • 【PyCharm】快捷键使用
  • CentOS 9 Stream 上安装飞书客户端
  • Android SystemUI——CarSystemBar车载状态栏(九)
  • QT笔记- Qt6.8.1 Android编程 手机震动实现
  • PyQt5学习-QPushButton
  • PyTorch使用教程(7)-数据集处理
  • 2.7 实战项目: GitHub openai-quickstart