CUDA 编程指南 13.4.2 · 非官方中文译本
连续阅读
原著发布于 2026 年 9 月 10 日对照 NVIDIA 英文原文对应英文网页

2.2. CUDA Python 简介

本章介绍使用 Python 进行 CUDA 核函数编程。CUDA Python 生态系统涵盖了一个庞大且正在积极演进的工具与库生态系统。本章将首先介绍其中的一些组件,然后使用其中的几个组件来说明在 Python 中编写和执行 GPU 代码的方法。

在 Python 中利用 GPU 计算的方式有很多,其中许多方式不需要显式编写 GPU 核函数。CUDA Python 生态系统的一些组件提供了在 GPU 上执行操作的函数,开发者无需进行任何特定的 GPU 控制或编码。NVIDIA 加速计算中心提供了一份加速 Python 用户指南,其中介绍并讨论了许多不同的库和工具,它们能够在 Python 中实现 GPU 加速计算。对于希望尽可能快速、轻松地在 Python 中使用 GPU,而不一定要直接编写 GPU 代码的用户来说,这份资源是一个很好的起点。

另一方面,本章侧重于直接控制 GPU,以及在 Python 中编写在 GPU 上执行的核函数。本章重点介绍在 Python 中进行CUDA 单指令多线程(SIMT)编程。

2.2.1. CUDA Python 生态系统

CUDA Python 是一个由工具和库构成的生态系统,可在 Python 中实现 GPU 计算。以下列表介绍 CUDA Python 的主要组成部分,这里所涵盖的内容并不需要用到所有这些组成部分。此列表改编自CUDA Python github 仓库中的完整列表。

主要组件:用于 GPU 控制和运行库提供的 GPU 代码

  • cuda.core:用于内存管理和设备管理等 CUDA 控制操作的 Python 风格接口。它为 Python 提供的功能,相当于 CUDA Runtime 为 CUDA C++ 提供的功能。

  • cuda.compute:一个 Python 模块,提供由CUDA Core Compute Library (CCCL)提供的 GPU 加速函数。

  • CuPy:一个 Python 库,提供 Numpy 例程的 GPU 加速版本,以及ndarray数据容器的 GPU 加速版本。

核函数编写组件

  • cuda.lang:一种 Python 领域专用语言(DSL),用于使用 Python 语言的一个子集,在 SIMT 编程模型中编写 CUDA 核函数和设备函数。

  • cuda.coop:一个 Python 模块,提供可从设备调用(使用cuda.lang)的CUDA Core Compute Library (CCCL)原语。

  • cuda.tile:一种 Python 领域专用语言(DSL),用于在分块编程模型中编写 CUDA 核函数和设备函数。

其他组件

  • cuda.pathfinder:一个用于定位 Python 环境中已安装的 CUDA 组件的实用工具

  • cuda.bindings:CUDA 库和实用工具的底层 Python 绑定,包括 CUDA Driver API、CUDA Runtime API、NVRTC、NVVM 等。cuda.bindings通过 CUDA Driver 和 CUDA Runtime 组件提供与cuda.core中可用功能相同的功能。不过,cuda.bindings是通过对 C 语言 API 的 Python 封装来提供这些功能的,而不是通过原生的 Python 风格接口。

2.2.1.1. 在 Python 中使用 CUDA 库

CUDA C++ 拥有丰富的库生态系统,无需直接编写核函数或 GPU 代码即可实现 GPU 加速。CUDA C++ 于 2006 年推出时,库的数量很少,开发者基本上必须自行编写 GPU 核函数代码。此后,人们开发了大量库,使开发者无需编写太多 GPU 代码,甚至完全不必编写 GPU 代码,就能在 C++ 中利用 GPU 计算。

CUDA Python 生态系统则从另一个方向演进而来:在能够直接使用 Python 语法和语义编写自定义核函数之前,CuPy 等 Python 库就已经为 Python 开发者提供了计算和算法的 GPU 加速实现。这些库中的许多库为使用 CUDA C++ 实现的 GPU 代码提供了 Python 绑定。

在当今的 CUDA 时代,如果 GPU 加速库能够提供满足您需求所需的表达能力,那么几乎总是建议使用这些库。这些库中的许多实现都经过 GPU 计算专家的调优。当没有可用的库或库无法满足需求时,在 Python 中也可以像在 C++ 中一样,直接编写 GPU 核函数和设备函数。

2.2.1.2. 本章范围

虽然开发者应在可能的情况下优先使用库,但本章其余部分介绍的是当开发者需要用 Python 编写自定义 GPU 代码时该如何进行。本章采用与第 2.1 节介绍 C++ 时相同的方式,讲解如何在 GPU 上编写和运行自定义 Python 代码,首先介绍如何指定 GPU 核函数,然后介绍如何使用 cuPy 提供的 GPU 加速的ndarray在 GPU 上分配内存,并实现 CPU 与 GPU 之间的通信。

2.2.1.3. 环境设置

一般来说,CUDA Python 生态系统中的大多数组件都可在 PyPi 上获取,并可使用pip命令或任何流行的 Python 包管理器进行安装。所有包都要求系统上安装最新的 NVIDIA 驱动程序。编写或运行 CUDA Python 应用程序通常不需要 CUDA Toolkit。

有关在不同平台上安装和配置 CUDA Python 的信息,请参阅NVIDIA 开发者专区中的 CUDA Python。

2.2.1.4. 运行 CUDA Python 应用程序

CUDA Python 应用程序无论是使用 CUDA 加速库,还是包含用户编写的 GPU 代码,其运行方式都与常规 Python 应用程序相同。在本节中,示例始终通过命令行运行,具体做法是如下所示调用python3来执行名为cuda-python-app.py的程序。

$ python3 cuda-python-app.py

2.2.2. Python 中的 SIMT 核函数

如CUDA 编程模型的介绍中所述,在 GPU 上执行且可以从主机调用的函数称为核函数。CUDA 提供两种不同的模型:单指令多线程(SIMT)和CUDA tile。SIMT 核函数是为由许多并行线程同时运行而编写的。这一概念在 CUDA Python 和 CUDA C++ 中是相同的。本章通过 SIMT 核函数介绍 CUDA Python。

2.2.2.1. 指定核函数

在 CUDA Python 中指定核函数之前,必须导入numba.cuda包。通常采用如下所示的方式完成此操作。

from numba import cuda

这会导入numba.cuda包,并允许我们使用该包提供的cuda命名空间中的组件。

要在 CUDA Python 中将一个函数指定为核函数,请如下所示将装饰器@cuda.jit放在函数定义的上一行。

from numba import cuda

@cuda.jit
def function(input_array, output_array):
    ...

这将使核函数在第一次启动时,针对当前活动的 GPU 进行 JIT 编译。如果未指定其他 GPU,则使用默认的 CUDA 设备,本节展示的示例就是如此。

2.2.2.2. 启动核函数

执行核函数的线程数量在核函数启动时指定,是启动的一部分。这称为执行配置。每次核函数调用都可以具有独有的执行配置,例如不同的块大小或线程块数量。

2.2.2.2.1. 核函数启动

要启动核函数,需要将执行配置放在核函数名称之后、函数实参之前的方括号[ ]中。实参的顺序与第 2.1.2.2.1 节介绍的 C++ 三重尖括号表示法中的顺序相同,具体如下:

kernel_name[number_of_thread_blocks, threads_per_block](arguments, ...)

下面的代码片段展示了如何在 Python 源文件中定义核函数,然后调用它。

from numba import cuda

@cuda.jit
def my_kernel(input, output):
    ...

## launch the kernel
my_kernel[num_thread_blocks, threads_per_block](in_array, out_array)

每个块的线程数量存在限制,因为一个块中的所有线程都驻留在同一个流式多处理器(SM)上,且必须共享该 SM 的资源。在当前的 GPU 上,一个线程块最多可以包含 1024 个线程。如果资源允许,可以在一个 SM 上同时调度多个线程块。

2.2.2.2.2. 多维网格和线程块

在 CUDA 中,线程块以及由线程块组成的网格可以是一维、二维或三维的。当网格或线程块是一维时,可以在指定核函数启动的执行配置时使用整数。当线程块或网格是二维或三维时,则使用二维或三维元组,如下方的二维网格和线程块启动示例所示,其中gridX和gridY是网格的 x 和 y 维度大小,blockX和blockY是网格中每个线程块的 x 和 y 维度大小。

from numba import cuda

@cuda.jit
def function(input, output):
    ...

## launch the kernel
function[(gridX, gridY), (blockX, blockY)](in_array, out_array)

2.2.2.3. 线程和网格索引内建函数

第 1.2.2.1 节介绍了线程和网格,第 2.2.2.2 节展示了如何为核函数启动指定网格和线程块的大小。在核函数内部,每个线程都可以访问执行配置的参数,以及线程的索引和线程块在网格中的索引。

可以从核函数内部访问以下变量,以确定线程的身份:

  • cuda.threadIdx.[xyz]给出线程在其线程块内的索引。一个线程块中的每个线程都有不同的索引。

  • cuda.blockDim.[xyz]给出线程块的各维度大小,这些大小是在核函数启动的执行配置中指定的。

  • cuda.blockIdx.[xyz]给出线程块在网格中的索引。每个线程块都有不同的索引。

  • cuda.gridDim.[xyz]给出网格的各维度大小,这些大小是在核函数启动时的执行配置中指定的。

这些变量中的每一个都是一个三分量向量,具有.x、.y和.z成员。如果在核函数启动的执行配置中未指定某个维度,则对于维度大小,其默认值为 1;对于索引,其默认值为 0。

cuda.threadIdx和cuda.blockIdx的索引从零开始。也就是说,cuda.threadIdx.x的取值从 0 到cuda.blockDim.x - 1,包含上限值。.y和.z在各自的维度中采用相同的方式。

下面展示了一个简单的向量加法核函数的代码,它将两个向量逐元素相加。该函数接受三个数组A、B和C,以实现逐元素向量加法C = A + B

# C = A + B vector addition
@cuda.jit
def vecadd(A, B, C):
    idx = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x
    C[idx] = A[idx] + B[idx]

该核函数首先计算线程在网格中的唯一索引。此核函数假定它是在一维网格中以一维线程块启动的。idx变量是一个从 0 到N-1的唯一索引,其中 N 是网格中的线程总数,即N = cuda.gridDim.x * cuda.blockDim.x。

上面代码块所示的线程索引计算模式非常常见,因此 Numba 为此操作提供了一种简写语法:cuda.grid(n),其中n是维数。在上面的示例中,下列这一行

idx = cuda.threadIdx.x + cuda.blockIdx.x * cuda.blockDim.x

可以替换为简单得多的

idx = cuda.grid(1)

此核函数一个值得注意的方面是,它不检查对A、B或C的越界访问。在本章中,我们假定它们是由 cuPy 创建的ndarrays,稍后将在第 2.2.3.3 节介绍。使用 cuPyndarrays时,边界检查由数组类型隐式实现。

2.2.3. GPU 计算中的内存

注意

cuPy 等 Python 包通过直接使用 CUDA C++ API 来执行 GPU 内存管理,例如第 2.1.3.2 节所介绍的那些 API。多个 Python 包提供了用于控制 GPU 内存分配的封装和实用工具。本指南仅介绍 cuPy。大多数包的概念都相似,并且除非另有说明,否则大多数包的行为方式都与其对应的 C++ 实现相似。

如第 1.2.3 节所介绍,GPU 连接有 DRAM。将在核函数中使用的数据数组,通常需要在核函数访问它们之前就位于 GPU 的 DRAM 中。在 Python 中,控制数据在内存中的位置,即在 CPU 与 GPU 之间移动数据,是程序员的责任。这与第 2.1.3.2 节所介绍的 C++ 中的显式内存管理情况相同。

2.2.3.1. 在 GPU 上实例化数组

CuPy 提供了在 GPU 上创建指定类型和维度的ndarray对象的函数,也提供了在 CPU 与 GPU 之间复制数据的函数。cuPy 中许多函数的函数签名与 Numpy 中用于创建ndarrays的函数相似。下面展示了几个示例,说明如何使用 CuPy 在 GPU 内存中创建和填充数组。

import cupy as cp
import numpy as np

## create a matrix of zeros on the GPU
## when a datatype is not specified, float32 is used by default
A_device = cp.zeros((1024, 1024))

## create an array of 2^20 random doubles on the GPU
B_device = cp.rand.random((2**20), dtype=np.double)

## create an array of zeroes with the same shape and datatype as an existing array
C_device = cp.zeros_like(A)

2.2.3.2. 在主机内存与 GPU 内存之间复制数组

CuPy 还可用于将数据从位于 CPU 内存中的 Numpyndarrays复制到位于 GPU 内存中的 CuPy 数组。

import cupy as cp
import numpy as np

## Create an array in host memory
A_host = np.zeros((1024, 1024))
## Copy the array to the GPU
A_device = cp.array(A_host)

## Create an array in GPU memory
B_device = cp.rand.random((1024, 1024))
## copy the array to host memory
B_host = cp.asnumpy(B_device)

2.2.3.3. ndarray 对象类型

上一节展示的ndarray对象要么存在于主机内存中,要么存在于 GPU 内存中,但不会同时存在于两者中。将驻留在主机上的数组作为实参传递给核函数会导致错误。将驻留在 GPU 内存中的数组传递给普通 Python 函数,即非核函数,也会导致错误。CuPy 不会隐式执行 CPU 与 GPU 之间的复制,因为这些操作可能开销很大,而且过多的数据复制会损害性能。因此,CuPy 要求程序员有意识地决定何时在 CPU 与 GPU 之间复制数据。

在 GPU 核函数中使用ndarray类型的一个优点是,数组自身携带其各维度的范围。如第 2.2.2.3 节所示,边界检查会自动完成,当所需的线程总数略小于执行块或网格的总大小时,核函数代码无需检查越界访问。

2.2.4. 同步 CPU 与 GPU

与 C++ 一样,CUDA Python 中的核函数启动相对于主机线程是异步的。也就是说,在核函数启动之后,主机代码会继续在 CPU 上执行,而不能保证核函数已经完成执行,甚至不能保证它已经开始执行。为了保证 GPU 核函数已经执行完毕,主机线程必须与 GPU 进行某种形式的同步。

最简单的同步形式是同步整个 GPU。这种设备范围的同步是 CUDA 驱动程序提供的一种操作,cuPy 和 numba.cuda 都通过synchronize()方法将其暴露给 Python。

import cupy as cp
from numba import cuda

..

## Wait on host thread for all pending GPU work to complete
## this uses the interface provided by cupy
cp.synchronize()

## Wait on host thread for all pending GPU work to complete
## this uses the interface provided by numba.cuda
cuda.synchronize()

设备范围的同步会让主机线程等待,直到此前在 GPU 上发出的所有工作都已完成。使用 CUDA 流可以实现更细粒度的同步,如第 2.5 节所述。在 Python 中使用流时,建议使用cuda.core创建 CUDA 流,并根据需要仅与特定的流进行同步。

2.2.5. 综合运用

下面的源代码清单给出了通常用作第一个 GPU 核函数的并行向量加法核函数的 Python 形式。

import numpy as np
from numba import cuda
import cupy as cp


## Defines a CUDA kernel to perform C = A + B vector addition
@cuda.jit
def vecadd(A, B, C):
    work_index = cuda.grid(1)
    C[work_index] = A[work_index] + B[work_index]


# note that vector size is not a power of 2 nor a multiple of the block_size defined below
vector_size = 2**24 + 11

device = cp.cuda.Device()
## Create device arrays of uniform random float32 values as input, and an array of zeros 
## as the result vector
a = cp.random.uniform(-1, 1, vector_size)
b = cp.random.uniform(-1, 1, vector_size)
c = cp.zeros_like(a)

block_size = 256
grid_size = int(np.ceil(vector_size/block_size))
vecadd[grid_size, block_size](a, b, c)

## synchronize the CPU thread and the GPU to ensure that the kernel has completed
## this is included to illustrate good practices, even though the copy below would implicitly wait for
## the kernel to complete
device.synchronize()

## Copy all 3 arrays to the CPU as ndarrays
a_np = cp.asnumpy(a)
b_np = cp.asnumpy(b)
c_np = cp.asnumpy(c)

## Perform the copy on the CPU to verify the answer
expected = a_np + b_np

## Test that the answer is correct, within floating point epsilon
np.testing.assert_array_almost_equal(c_np, expected)

## The assert will print diagnostics and abort
## so this only prints if the assertion passes
print("Test succeeded")

在此示例中,输入数组A和B由 CuPy 在 GPU 上创建并初始化为随机值。在代码末尾将它们复制到 CPU,仅仅是为了让 CPU 也能执行向量加法,并验证 CPU 和 GPU 的结果是否一致。

2.2.6. CUDA Python 中的错误检查

任何影响 GPU 的操作,从内存分配和复制到核函数启动,都有可能引发错误状况。如第 2.1.7 节针对 C++ 所示,确保在与 GPU 交互的过程中没有发生错误是最佳实践。

在 Python 中,CUDA 错误会抛出异常,如果未捕获这些异常,程序就会终止。可以使用常规的 Python 语法捕获这些异常。下面的示例与上面的向量加法相同,但有意加入了一个错误:每个线程块的线程数为 2048,超过了当前任何 GPU 所能运行的数量。这会导致核函数启动失败并抛出异常,而这段代码将捕获该异常。

import numpy as np
from numba import cuda
import cupy as cp


## Defines a CUDA kernel to perform C = A + B vector addition
@cuda.jit
def vecadd(A, B, C):
    work_index = cuda.grid(1)
    C[work_index] = A[work_index] + B[work_index]


try:
    vector_size = 2**24 + 11

    device = cp.cuda.Device()
    a = cp.random.uniform(-1, 1, vector_size)
    b = cp.random.uniform(-1, 1, vector_size)
    c = cp.zeros_like(a)

    ## this block size is too large for any current GPUs
    block_size = 2048
    grid_size = int(np.ceil(vector_size/block_size))
    # Error: launching kernel with invalid block size
    vecadd[grid_size, block_size](a, b, c)

    device.synchronize()
    print("Test did not encounter any errors")

except Exception as e:
    print(f"Exception occurred: {e}")

运行这段代码会捕获并显示该错误,如下所示:

$ python3 vecadd_error.py
Exception occurred: CUDA_ERROR_INVALID_VALUE: This indicates that one or more of the parameters passed to the API call is not within an acceptable range of values.

程序捕获异常后正常退出。如果运行这段代码时没有try:和except:,它将异常退出,并将回溯信息输出到控制台,其中应显示相同的错误。

原著 © 2007 至 2026 NVIDIA Corporation 及其关联公司。保留所有权利。
中文译本仅作学习参考,非 NVIDIA 官方出版物。技术含义有疑问时请核对NVIDIA 英文原文。
关于本译本

输入关键词搜索全书。