cuBLAS开发指南中文版
2.使用cuBLAS API
本节介绍如何使用 cuBLAS 库 API。
2.1.1. Error status
所有 cuBLAS 库函数调用都返回错误状态 cublasStatus_t。
2.1.2. cuBLAS context
应用程序必须通过调用 cublasCreate()
函数来初始化 cuBLAS 库上下文的句柄。然后,将句柄显式传递给每个后续的库函数调用。一旦应用程序完成使用该库,它必须调用函数cublasDestroy()
以释放与 cuBLAS 库上下文关联的资源。
这种方法允许用户在使用多个主机线程和多个 GPU 时显式控制库设置。例如,应用程序可以使用 cudaSetDevice()
将不同的设备与不同的主机线程相关联,并且在每个主机线程中,它可以初始化 cuBLAS 库上下文的唯一句柄,它将使用与该主机线程关联的特定设备。然后,使用不同句柄进行的 cuBLAS 库函数调用将自动将计算分派到不同的设备。
假定与特定 cuBLAS 上下文关联的设备在相应的 cublasCreate()
和 cublasDestroy()
调用之间保持不变。为了让 cuBLAS 库在同一主机线程中使用不同的设备,应用程序必须通过调用 cudaSetDevice()
设置要使用的新设备,然后通过调用创建另一个与新设备关联的 cuBLAS 上下文cublasCreate()
。
cuBLAS 库上下文与 cublasCreate()
调用时当前的 CUDA 上下文紧密耦合。使用多个 CUDA 上下文的应用程序需要为每个 CUDA 上下文创建一个 cuBLAS 上下文,并确保前者永远不会超过后者。
2.1.3. Thread Safety
该库是线程安全的,即使使用相同的句柄,也可以从多个主机线程调用其函数。 当多个线程共享同一个句柄时,当句柄配置发生更改时需要格外小心,因为该更改可能会影响所有线程中的后续 cuBLAS 调用。 对于手柄的破坏更是如此。 所以不建议多个线程共享同一个 cuBLAS 句柄。
2.1.4. Results reproducibility
根据设计,给定工具包版本的所有 cuBLAS API 例程在具有相同架构和相同数量 SM 的 GPU 上执行时,每次运行都会生成相同的按位结果。但是,不能保证跨工具包版本的逐位再现性,因为实现可能会因某些实现更改而有所不同。
此保证仅在单个 CUDA 流处于活动状态时有效。如果多个并发流处于活动状态,则库可以通过选择不同的内部实现来优化总体性能。
注意:多流执行的不确定行为是由于库优化为并行流中运行的例程选择内部工作区。为了避免这种影响,用户可以:
- 使用
cublasSetWorkspace()
函数为每个使用的流提供单独的工作区,或 - 每个流有一个 cuBLAS 句柄,或
- 使用 cublasLtMatmul() 代替
*gemm*()
系列函数并提供用户拥有的工作空间,或 - 将调试环境变量 CUBLAS_WORKSPACE_CONFIG 设置为“:16:8”(可能会限制整体性能)或“:4096:8”(将 GPU 内存中的库占用量增加大约 24MiB)。
即使多个并发流共享一个 cuBLAS 句柄,这些设置中的任何一个都将允许确定性行为。
预计此行为将在未来版本中更改。
对于一些例程,例如 cublas<t>symv
和 cublas<t>hemv
,可以使用例程 cublasSetAtomicsMode()
选择一个替代的明显更快的例程。在这种情况下,不能保证结果是按位可重现的,因为原子用于计算。
2.1.5. A.5. Scalar Parameters
有两类使用标量参数的函数:
- 通过引用主机或设备上的 alpha 或 beta 参数作为缩放因子的函数,例如 gemm。
- 在主机或设备上返回标量结果的函数,例如 amax()、amin、asum()、rotg()、rotmg()、dot() 和 nrm2()。
对于第一类函数,当指针模式设置为 CUBLAS_POINTER_MODE_HOST
时,标量参数 alpha 或 beta 可以在堆栈上或在堆上分配,不应放在托管内存中。下面,与这些功能相关的 CUDA 内核将以 alpha 或 beta 的值启动。因此,如果它们是在堆上分配的,即使内核启动是异步的,它们也可以在调用返回后立即释放。当指针模式设置为 CUBLAS_POINTER_MODE_DEVICE
时,alpha 或 beta 必须可以在设备上访问,并且在内核完成之前不应修改它们的值。请注意,由于 cudaFree()
执行隐式 cudaDeviceSynchronize()
,因此 cudaFree()
仍然可以在调用后立即在 alpha 或 beta 上调用,但在这种情况下它会破坏使用此指针模式的目的。
对于第二类函数,当指针模式设置为 CUBLAS_POINTER_MODE_HOST
时,这些函数会阻塞 CPU,直到 GPU 完成计算并将结果复制回 Host。当指针模式设置为 CUBLAS_POINTER_MODE_DEVICE
时,这些函数会立即返回。在这种情况下,与矩阵和向量结果类似,标量结果仅在 GPU 上的例程执行完成时才准备就绪。这需要适当的同步才能从主机读取结果。
在任何一种情况下,指针模式 CUBLAS_POINTER_MODE_DEVICE
允许库函数与主机完全异步执行,即使 alpha 或 beta 是由先前的内核生成的。例如,当使用 cuBLAS 库实现求解线性系统和特征值问题的迭代方法时,可能会出现这种情况。
2.1.6 流并行
如果应用程序使用由多个独立任务计算的结果,则可以使用 CUDA™ 流来重叠在这些任务中执行的计算。
应用程序可以在概念上将每个流与每个任务相关联。为了实现任务之间的计算重叠,用户应该使用函数 cudaStreamCreate()
创建 CUDA™ 流,并在调用实际 cuBLAS 例程之前通过调用 cublasSetStream()
设置每个单独的 cuBLAS 库例程使用的流.请注意,cublasSetStream()
将用户提供的工作空间重置为默认工作空间池;请参见 cublasSetWorkspace()。然后,在单独的流中执行的计算将尽可能在 GPU 上自动重叠。当单个任务执行的计算量相对较小且不足以使 GPU 充满工作时,这种方法特别有用。
我们建议使用带有标量参数的新 cuBLAS API,并在设备内存中通过引用传递结果,以在使用流时实现计算的最大重叠。
下一节将描述流的特定应用,即多个小内核的批处理。
2.1.7 批处理内核
在本节中,我们将解释如何使用流来批处理小内核的执行。例如,假设我们有一个应用程序,我们需要用密集矩阵进行许多小的独立矩阵乘法。
很明显,即使有数百万个小的独立矩阵,我们也无法达到与一个大矩阵相同的 GFLOPS 速率。例如,单个 n ∗ n n*n n∗n大型矩阵-矩阵乘法针对 n 2 n^2 n2输入大小执行 n 3 n^3 n3运算,而 1024 个 n 32 ∗ n 32 \frac{n}{32} *\frac{n}{32} 32n∗32n小型矩阵-矩阵乘法针对相同输入大小执行 1024 ( n 32 ) 3 = n 3 32 1024(\frac{n}{32})^3=\frac{n^3}{32} 1024(32n)3=32n3运算。然而,同样清楚的是,与单个小矩阵相比,我们可以使用许多小的独立矩阵实现显着更好的性能。
GPU 的架构系列允许我们同时执行多个内核。因此,为了批量执行独立内核,我们可以在单独的流中运行它们中的每一个。特别是,在上面的示例中,我们可以使用函数 cudaStreamCreate()
创建 1024 个 CUDA™ 流,然后在每个对 cublas<t>gemm()
的调用前加上对 cublasSetStream()
的调用,并为每个矩阵使用不同的流 -矩阵乘法(注意 cublasSetStream()
将用户提供的工作空间重置为默认工作空间池,请参阅 cublasSetWorkspace()
)。这将确保尽可能同时执行不同的计算。尽管用户可以创建许多流,但实际上不可能同时执行超过 32 个并发内核。
2.1.8 缓存配置
在某些设备上,L1 缓存和共享内存使用相同的硬件资源。 可以直接使用 CUDA Runtime 函数 cudaDeviceSetCacheConfig
设置缓存配置。 也可以使用例程 cudaFuncSetCacheConfig
专门为某些功能设置缓存配置。 有关缓存配置设置的详细信息,请参阅 CUDA 运行时 API 文档。
因为从一种配置切换到另一种配置会影响内核并发性,所以 cuBLAS 库不设置任何缓存配置首选项,而是依赖于当前设置。 然而,一些 cuBLAS 例程,尤其是 Level-3 例程,严重依赖共享内存。 因此,缓存首选项设置可能会对它们的性能产生不利影响。
2.1.9 静态库支持
从 6.5 版开始,cuBLAS 库也在 Linux 和 Mac OS 上以静态形式作为 libcublas_static.a
提供。 静态 cuBLAS 库和所有其他静态数学库依赖于一个名为 libculibos.a
的通用线程抽象层库。
例如,在 Linux 上,要使用 cuBLAS 编译一个小型应用程序,针对动态库,可以使用以下命令:
nvcc myCublasApp.c -lcublas -o myCublasApp
而要针对静态 cuBLAS 库进行编译,必须使用以下命令:
nvcc myCublasApp.c -lcublas_static -lculibos -o myCublasApp
也可以使用本机 Host C++ 编译器。 根据主机操作系统,链接行可能需要一些额外的库,如 pthread 或 dl。 建议在 Linux 上使用以下命令:
g++ myCublasApp.c -lcublas_static -lculibos -lcudart_static -lpthread -ldl -I <cuda-toolkit-path>/include -L <cuda-toolkit-path>/lib64 -o myCublasApp
请注意,在后一种情况下,不需要库 cuda。 如果需要,CUDA 运行时将尝试显式打开 cuda 库。 在没有安装 CUDA 驱动程序的系统的情况下,这允许应用程序优雅地管理此问题,并可能在仅 CPU 路径可用时运行。
从 11.2 版开始,使用类型化函数而不是扩展函数 (cublas**Ex()) 有助于在链接到静态 cuBLAS 库时减少二进制文件大小。
2.1.10 GEMM 算法数值行为
一些 GEMM 算法沿维度 K 拆分计算以增加 GPU 占用率,特别是当维度 K 与维度 M 和 N 相比较大时。当 cuBLAS 启发式或用户明确选择此类算法时,结果每个拆分确定性地求和到结果矩阵中以获得最终结果。
对于例程 cublas<t>gemmEx
和 cublasGemmEx
,当计算类型大于输出类型时,拆分块的总和可能会导致一些中间溢出,从而产生最终的结果矩阵,其中包含一些溢出。如果所有点积在最终转换为输出类型之前已在计算类型中累积,则可能不会发生这些溢出。当 computeType 为 CUDA_R_32F
且 Atype
、Btype
和 Ctype
在 CUDA_R_16F
中时,这种计算副作用很容易暴露。可以使用带有 cublasSetMathMode()
的计算精度模式 CUBLAS_MATH_DISALLOW_REDUCED_PRECISION_REDUCTION
来控制此行为
2.1.11 Tensor Core使用
Tensor Core首次与 Volta GPU 一起引入(计算能力>=sm_70)并显着加速矩阵乘法。 从 cuBLAS 版本 11.0.0 开始,该库将尽可能自动使用 Tensor Core 功能,除非通过选择 cuBLAS 中的迂腐计算模式明确禁用它们(参见 cublasSetMathMode()
, cublasMath_t
)。
应该注意的是,该库将选择启用 Tensor Core 的实现,只要它确定它将提供最佳性能。
从 cuBLAS 版本 11.0.0 开始,使用Tensor Core不再对矩阵尺寸和内存对齐有任何限制。 但是,当矩阵维度和指针满足一定的内存对齐要求时,可以实现使用 Tensor Cores 时的最佳性能。 具体来说,必须满足以下所有条件才能充分利用 Tensor Cores 的性能:
- m % 8 == 0
- k % 8 == 0
- op_B == CUBLAS_OP_N || n%8 == 0
- intptr_t(A) % 16 == 0
- intptr_t(B) % 16 == 0
- intptr_t© % 16 == 0
- intptr_t(A+lda) % 16 == 0
- intptr_t(B+ldb) % 16 == 0
- intptr_t(C+ldc) % 16 == 0
2.1.12 CUDA 图支持
在大多数情况下,可以在 CUDA Graph 流捕获中捕获 cuBLAS 例程,而不受限制。
例外是将结果输出到主机缓冲区的例程(例如,配置指针模式 CUBLAS_POINTER_MODE_HOST
时的 cublas<t>dot
),因为它强制同步。
对于输入系数(如 alpha、beta),行为取决于指针模式设置:
- 在
CUBLAS(LT)_POINTER_MODE_HOST
的情况下,图中捕获了系数值。 - 在带有设备指针的指针模式的情况下 - 在图形执行时使用设备指针访问系数值。
注意:每次在新的 CUDA 图表中捕获 cuBLAS 例程时,cuBLAS 都会在设备上分配工作区内存。 仅当删除捕获期间使用的 cuBLAS 句柄时,才会释放此内存。 为避免这种情况,请使用 cublasSetWorkspace()
函数来提供用户拥有的工作区内存。