初识GPU编程

初识GPU编程

基本概念

CPU和主存被称为主机(Host),GPU和显存(显卡内存)被称为设备(Device),CPU无法直接读显存,GPU无法直接读主存,通过Bus相互通信。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ysiCWmT5-1651995724963)(image-20220507193517306.png)]

检查是否安装了CUDA

$conda install cudatoolkit

安装 Numba库

$ conda install numba

检查CUDA与Numba是否安装成功

from numba import cuda
print(cuda.gpus)

cuda会独占一张卡,默认0号,可以使用

CUDA_VISIBLE_DEVICES='5' python example.py

环境变量设置选择某张卡

GPU程序和CPU程序的区别

CPU程序是顺序执行的,一般需要:

  1. 初始化
  2. CPU计算
  3. 得到计算结果

CUDA编程中,GPU流程:

  1. 初始化, 将必要数据拷贝到GPU设别的显存
  2. CPU调用GPU函数,启动GPU多核同时计算
  3. CPU和GPU异步计算
  4. 将GPU计算结果拷贝回主机,得到计算结果

GPU程序示例

from numba import cuda

def cpu_print():
    print("print by cpu.")

@cuda.jit
def gpu_print():
    # GPU核函数
    print("print by gpu.")

def main():
    gpu_print[1, 2]()
    cuda.synchronize()
    cpu_print()

if __name__ == "__main__":
    main()

不同于从传统的Python CPU代码

  • 使用 from numba import cuda 引入cuda库
  • 在GPU函数上添加@cuda.jit装饰符,GPU函数又叫核函数
  • 主函数调用GPU核函数,添加[1, 2],告诉GPU以多大的并行粒度同时计算。gpu_print[1, 2]()表示同时开启两个线程并行执行,函数回被执行2次。
  • GPU启动异步:CPU不会等GPU执行完,必要时用cuda.synchronize()等待执行。

Thread层次结构

GPU需要定义执行配置。并行执行2次还是8次。需要明白CUDA的Thread层次结构。
在这里插入图片描述

CUDA将核函数所定义的运算称为线程,多个线程构成一个块,多个块组成网格(Grid)。两个block,每个block有4个thread,代码改为gpu_print[2, 4]()

线程是编程上的软件概念。多个block运行的grid运行在一个GPU显卡上. CUDA提供了一系列内置变量, 以记录Thread和Block的大小及索引下标.以[2, 4]配置为例: blockDim.x变量表示Block的大小是4, 每个block有4个, 即每个Block有4个线程, threadIdx.x变量是从0到blockDim.x - 1的索引下标, 记录这是第几个线程; gridDim.x变量表示Grid的大小是2, 即每个Grid有2个Block.

问题简化为8个小学生参加本此计算任务, 可以将这8个小学生分为2组, 每组4人. 整个Grid有2个Block, 即gridDim.x为2, 每组有4人, blockDim.x为4. 现在, 让第2个小学生对1和2加和, 如何定义1号学生呢?1 + 0 * blockDim.x

某个Thread在整个Grid中的位置编号是: threadIdx.x + blockIdx.x * blockDim.x

!apt-get install nvidia-cuda-toolkit
!pip3 install numba

import os
os.environ['NUMBAPRO_LIBDEVICE'] = "/usr/lib/nvidia-cuda-toolkit/libdevice"
os.environ['NUMBAPRO_NVVM'] = "/usr/lib/x86_64-linux-gnu/libnvvm.so"

from numba import cuda
import numpy as np
import time

@cuda.jit
def hello(data):
    data[cuda.blockIdx.x, cuda.threadIdx.x] = cuda.blockIdx.x

numBlocks = 5
threadsPerBlock = 10

data = np.ones((numBlocks, threadsPerBlock), dtype=np.uint8)

hello[numBlocks, threadsPerBlock](data)

print(data)

Block 大小设置

[gridDim, blockDim]的参考, 需要根据当前硬件设置block大小blockDim. 最好是32, 128, 256倍数, block最好不要超过1024

Grid的大小gridDim, 需要执行的总次数, 除以blockDim, 向上取整.

内存分配

计算时, 需要将数据从主存拷贝到显存上, 也就是将数据从主机端拷贝到设备端. CUDA能自动将数据从主机和设备间相互拷贝.

@cuda.jit
def gpu_add(a, b, result, n):
    # a, b为输入向量,result为输出向量
    # 所有向量都是n维
    # 得到当前thread的编号
    idx = cuda.threadIdx.x + cuda.blockDim.x * cuda.blockIdx.x
    if idx < n:
        result[idx] = a[idx] + b[idx]

解决极简单的问题, cuda 并不一定不 numpy快, 比如numpy.add, 已经优化到了极致.

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

    gpu_result = np.zeros(n)
    cpu_result = np.zeros(n)

    threads_per_block = 1024
    blocks_per_grid = math.ceil(n / threads_per_block)
    start = time()
    gpu_add[blocks_per_grid, threads_per_block](x, y, gpu_result, n)
    cuda.synchronize()
    print("gpu vector add time " + str(time() - start))
    start = time()
    cpu_result = np.add(x, y)
    print("cpu vector add time " + str(time() - start))

    if (np.array_equal(cpu_result, gpu_result)):
        print("result correct")

if __name__ == "__main__":
    main()
  • 上面代码使用了cuda默认的统一内存管理机制, 没有对数据拷贝优化, 统一内存系统是当GPU运行到某块数据发现不再设备端, 再去主机端将数据拷贝过来.
  • 这份代码没有做流水线优化, 并非同时计算2千万数据. 一般分批次流水: 一边对某批数据计算, 一边将下一批数据从主存拷贝. 计算占用cuda核心, 数据拷贝占用总线.

cuda方便的内存模型缺点是计算速度慢. 我们可以继续优化这个程序, 告知GPU哪些数据需要拷贝到设备, 哪些需要拷贝回主机.

Numba对NumPy比较友好, 编程中一定要使用Numpy数据类型. 用到比较多的内存分配函数有:

  • cuda.device_array(): 在设备上分配空向量, 类似于numpy.empty()

  • cuda.to_device(): 将主机的数据拷贝到设备

  • cuda.copy_to_host(): 将设备的数据拷贝到主机

高维执行配置

使用threadIdxblockIdx等参数来描述线程Thread的编号. 实际上, CUDA允许这两个变量最多为三维. 一维, 二维和三维的大小配置可以适应向量, 矩阵和张量等不同的场景.

在这里插入图片描述

一个二维的执行配置, 每个BLOCK有(3, 4)个Thread, 每个Grid有(2, 3)个Block. 二维块大小为(Dx, Dy), 某个线程号(x, y)的公式(x + yDx); 三维块大小为(Dx, Dy, Dz), 某个线程号(x, y, z)的公式为(x + yDx + zDxDy).

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值