CPU和主存被称为主机(Host),GPU和显存(显卡内存)被称为设备(Device),CPU无法直接读显存,GPU无法直接读主存,通过Bus相互通信。
检查是否安装了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
环境变量设置选择某张卡
CPU程序是顺序执行的,一般需要:
CUDA编程中,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库@cuda.jit
装饰符,GPU函数又叫核函数[1, 2]
,告诉GPU以多大的并行粒度同时计算。gpu_print[1, 2]()
表示同时开启两个线程并行执行,函数回被执行2次。cuda.synchronize()
等待执行。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)
[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哪些数据需要拷贝到设备, 哪些需要拷贝回主机.
Numba对NumPy比较友好, 编程中一定要使用Numpy数据类型. 用到比较多的内存分配函数有:
cuda.device_array()
: 在设备上分配空向量, 类似于numpy.empty()
cuda.to_device()
: 将主机的数据拷贝到设备
cuda.copy_to_host()
: 将设备的数据拷贝到主机
使用threadIdx
和blockIdx
等参数来描述线程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).