CUDA C编程权威指南-第八章:GPU加速库和OpenACC
CUDA C编程权威指南-第八章:GPU加速库和OpenACC
系列导航:导读 | 上一篇:第7章 调整指令级原语 | 下一篇:第9章 多GPU编程
系列第 8 篇。前面一直在手写核函数;这一章换条路——用现成 GPU 加速库和指令式并行:cuBLAS、cuFFT、cuSPARSE、cuRAND 等,以及 CUDA 6 的库特性与 OpenACC,少写甚至不写核函数也能拿到 GPU 加速。
前言:从手写内核到库与指令
前几章我们一直在写自己的核函数:从向量加法、矩阵加法到归约、转置,再到共享内存、流与并发、指令级原语。这条路径能让你对 GPU 的编程模型和执行模型有深刻理解。这一章会说明:在实际项目中,很多问题——线性代数(矩阵运算、求解器)、快速傅里叶变换(FFT)、稀疏矩阵运算、随机数生成——已有高度优化的现成实现;直接使用 CUDA 加速库,往往能以更少的代码、更短的开发时间获得接近甚至优于手写内核的性能。同时,OpenACC 等指令式方法允许在保留原有 C/Fortran 代码结构的前提下,通过编译器指令将热点循环或区域卸载到 GPU,适合遗留代码的渐进式加速。
本章将系统学习以下内容:
- CUDA 库概览:库的定位、与手写内核的取舍、常见库的分类与选用场景。
- cuSPARSE:稀疏线性代数库,稀疏矩阵存储格式与典型运算(如 SpMV)。
- cuBLAS:密集线性代数库,对应 BLAS 的 GPU 实现(saxpy、gemm 等)。
- cuFFT:快速傅里叶变换库,一维/多维 FFT 的 GPU 加速。
- cuRAND:随机数生成库,设备端或主机端高质量随机数。
- CUDA 6 库特性:多 GPU 支持、统一内存与库的集成。
- 库性能概览:各库性能关注点与选型建议。
- OpenACC:指令式并行编程模型,
#pragma acc与数据/计算区域、与 CUDA 的对比。
通过本章,你将在「手写核函数」之外,掌握用库和指令快速获得 GPU 加速的两种主流方式。
一、本章在全书中的位置与学习目标
1.1 为什么要学「GPU 加速库和 OpenACC」
第 2~7 章建立了从编程模型、执行模型、内存层次、流与并发到指令级原语的完整「手写内核」能力。在工程实践中,优先考虑现成 GPU 库能显著减少开发与维护成本,并在多数场景下获得接近最优的性能;只有在问题高度定制化或对延迟与资源有极端要求时,才需要手写内核。OpenACC 则提供了另一条路径——通过编译器指令将现有 C/Fortran 代码中的热点区域卸载到 GPU,适合遗留代码的渐进式迁移或快速原型。因此,理解各库的适用域、API 风格(句柄/计划、设备指针、数据布局),以及 OpenACC 的指令语义与适用场景,是写出可维护、高性能 GPU 应用的重要一环;本章与第 2 章(性能模型)、第 6 章(流与库结合)、第 9 章(多 GPU 与库)紧密衔接。
1.2 学完本章,你应该能回答
| 学习目标 | 检验方式 |
|---|---|
| 理解为何优先用库及库与手写内核的取舍 | 能说明「能库则库、不能库再手写」的理由;能列举各库的典型用途 |
| 掌握 cuBLAS 的 API 风格(句柄、设备指针、列主序)及 saxpy/gemm 用法 | 能写出最小可运行的 cublasSaxpy 示例;说明列主序与 C 行主序的差异 |
| 掌握 cuFFT 的 Plan → Execute → Destroy 流程及 R2C 输出长度 | 能说明一维实数 FFT 输出为何是 N/2+1 个复数;知道 cufftExec* 为异步 |
| 理解 cuSPARSE 的稀疏格式(CSR 等)与 SpMV 流程 | 能说出 CSR 与 COO 的适用场景;知道格式选择对性能的影响 |
| 掌握 cuRAND 的主机端与设备端用法及典型 API | 能写出主机端 curandGenerateUniform 示例;知道设备端在核函数内初始化的模式 |
| 理解 CUDA 6 中多 GPU、统一内存与库的集成(书中涉及部分) | 能简述多 GPU 下每设备独立句柄、统一内存在库中的支持情况 |
| 理解 OpenACC 的定位与常用指令(data、parallel、loop、kernels) | 能写出带 #pragma acc data 与 parallel loop 的向量加;说明与 CUDA 的对比 |
| 掌握库的链接与头文件、性能选型的一般建议 | 能正确链接 -lcublas -lcufft 等;能根据问题域选库并注意复用句柄/计划 |
1.3 博客阅读导图(本章架构)
阅读建议:本章篇幅较长。二、四、五、九节建议精读(库概述、cuBLAS/cuFFT 与 OpenACC 最常用);三、六节(cuSPARSE、cuRAND)可按需选读;七节(CUDA 6 库特性)为可略读,了解即可。
1 | 第 8 章 GPU 加速库和 OpenACC |
二、CUDA 库概述(书 8.1 节)
本节对应书中对「为什么要用 CUDA 库」以及「库的分类与选用」的总体介绍,为全章奠定概念基础。
2.1 为什么使用 GPU 加速库
手写 CUDA 核函数能带来最大的灵活性和学习价值,但在工程实践中会面临开发成本高、维护难、性能调优复杂等问题。这一章会说明:NVIDIA 提供的 CUDA 加速库针对常见域(线性代数、FFT、稀疏运算、随机数等)做了深度优化,并随 CUDA 工具包一起发布,与驱动和硬件保持兼容。使用这些库可以:
- 减少开发时间:无需从零实现并调试复杂算法(如 FFT、稀疏求解器)。
- 获得经过验证的性能:库内部通常针对多种 GPU 架构做了调优,包括内存访问模式、占用率、算法选择等。
- 保持可移植性:同一套库 API 可在不同代际的 GPU 上运行,由库内部适配硬件。
当然,库也有其适用范围:若问题非常定制化、或对延迟与资源有极端要求,手写内核仍是必要的。优先考虑库,再考虑手写,是多数应用开发的最佳策略。
2.2 库的分类与选用场景
原书按功能域对常用 CUDA 库进行了分类。下表归纳常用库及其典型用途:
| 库名 | 全称 / 含义 | 典型用途 |
|---|---|---|
| cuBLAS | CUDA Basic Linear Algebra Subprograms | 密集向量/矩阵运算:axpy、gemv、gemm 等 |
| cuFFT | CUDA Fast Fourier Transform | 一维/多维 FFT、实数和复数 FFT |
| cuSPARSE | CUDA Sparse matrix library | 稀疏矩阵存储与运算(SpMV、稀疏求解等) |
| cuRAND | CUDA Random number generation | 设备端/主机端高质量随机数生成 |
| Thrust | 模板库(C++) | 排序、归约、扫描等通用并行算法(若原书涉及) |
选用原则:若你的问题可归结为稠密线性代数,优先考虑 cuBLAS;若涉及频谱分析、卷积、滤波等,考虑 cuFFT;若矩阵稀疏且规模大,考虑 cuSPARSE;若需要随机数(蒙特卡洛、随机算法),考虑 cuRAND。后续各节会分别介绍各库的 API 风格与完整示例。
2.3 库的链接与头文件
使用 CUDA 库时,需要包含对应头文件并在链接时指定库。各库的头文件与库文件名与 BLAS/FFT 等标准命名对应,便于在现有构建系统中集成。典型编译命令例如:
1 | # 以 cuBLAS 为例:链接 libcublas |
头文件通常为 cublas_v2.h、cufft.h、cusparse.h、curand.h 等。库的 API 多数采用**句柄(handle)或计划(plan)**管理状态:先创建句柄/计划,在其上调用运算,最后销毁。这种设计便于多线程或多流场景下为每个流维护独立状态。这一章会说明:链接时若缺少对应库(如 -lcublas),会报未定义引用错误;在 Windows 下需在项目属性中添加相应库依赖,与第 2 章中 nvcc 编译流程一致。
理解与体会:本章的「哲学」是:能库则库,不能库再手写。库不是替代你对 CUDA 执行模型和内存模型的理解,而是让你把精力放在业务逻辑和系统集成上;只有在库无法覆盖或需要极致定制时,才回到手写内核。选用库时,先看问题域(稠密/稀疏、FFT、随机数),再查对应库的 API 与数据布局(如 cuBLAS 的列主序),可少走弯路。
三、cuSPARSE 库(书 8.2 节)
本节对应书中对 cuSPARSE 的讲解:稀疏矩阵的存储格式、描述符与典型运算(如 SpMV)。
3.1 cuSPARSE 简介与稀疏格式
cuSPARSE 提供针对稀疏矩阵的线性代数运算,适用于非零元远少于总元素的大型矩阵。这一章会说明:稠密矩阵运算用 cuBLAS,稀疏矩阵运算用 cuSPARSE,二者互补。cuSPARSE 支持多种稀疏存储格式,常见包括:
| 格式 | 全称 | 特点 |
|---|---|---|
| COO | Coordinate | 三元组 (row, col, value),简单但未针对运算优化 |
| CSR | Compressed Sparse Row | 按行压缩,行指针 + 列索引 + 值,SpMV 等常用 |
| CSC | Compressed Sparse Column | 按列压缩 |
| HYB | Hybrid | 部分 ELL + 部分 COO,库可自动选择 |
SpMV(Sparse Matrix-Vector multiplication)( y \leftarrow A x ) 是 cuSPARSE 中最常用的运算之一,对应 cusparseSpMV 或旧版 API 中的 cusparseSpMV_bufferSize + cusparseSpMV。使用前需用 cusparseSpMatDescr_t(或旧版的矩阵描述符)描述矩阵的格式、维度与数据指针。
3.2 使用流程概述与重难点
典型流程(与书中一致):(1) 创建 cuSPARSE 句柄 cusparseCreate();(2) 创建矩阵描述符并设置格式、维度、设备指针(行偏移、列索引、值);(3) 分配并传入向量 ( x )、( y ) 的设备指针;(4) 调用 SpMV 或其它稀疏运算;(5) 销毁描述符与句柄。概念性代码框架如下(具体 API 以 cuSPARSE 版本文档为准):
1 | // 概念性流程:创建句柄、描述矩阵、执行 SpMV |
稀疏矩阵的格式选择(CSR、CSC、HYB 等)会显著影响性能,cuSPARSE 文档会给出各格式的适用场景与性能建议。对于 SpMV ( y \leftarrow A x ),数学上即 ( y_i = \sum_j A_{ij} x_j )(对非零 ( A_{ij} ) 求和);库内部会根据格式与矩阵结构选择最优内核,无需用户手写稀疏访问模式。
重难点:格式选择是 cuSPARSE 使用的关键。行访问为主的运算(如 SpMV 按行输出)通常用 CSR 更高效;列访问为主则考虑 CSC。HYB 适合非零元分布极不均匀的矩阵。若从稠密或其它格式转换,需注意转换 API 与描述符的一致性,避免指针或维度错误。
四、cuBLAS 库(书 8.3 节)
本节对应书中对 cuBLAS 的讲解:API 风格、Level-1/2/3 运算概念,以及完整示例(saxpy、gemm)。
4.1 cuBLAS 简介与 API 风格
cuBLAS 是 NVIDIA 实现的 BLAS(Basic Linear Algebra Subprograms) 的 GPU 版本,提供与 BLAS 三个层次对应的例程:Level-1(向量与向量,如 saxpy)、Level-2(矩阵与向量,如 gemv)、Level-3(矩阵与矩阵,如 gemm)。这一章会说明:自 cuBLAS 5 起,API 采用 cuBLAS 2 风格:使用 cublasHandle_t 句柄管理库状态,支持多流与更好的异步行为;数据指针均为设备指针,由调用者在 GPU 上分配好内存后再传入。
cuBLAS 的命名规则与 BLAS 类似:例如 cublasSaxpy 表示单精度(S)的 axpy;cublasDgemm 表示双精度(D)的 gemm(矩阵乘法)。axpy 的数学形式为:
[
y \leftarrow \alpha x + y \quad \text{(逐元素: } y_i = \alpha x_i + y_i \text{)}
]
即向量 (x) 缩放后加至向量 (y)。下表归纳常用运算与典型 API(以单精度为例):
| 运算 | BLAS 含义 | 典型 cuBLAS API(单精度) |
|---|---|---|
| axpy | ( y \leftarrow \alpha x + y ) | cublasSaxpy |
| dot | ( \alpha = x^T y ) | cublasSdot |
| gemv | ( y \leftarrow \alpha A x + \beta y ) | cublasSgemv |
| gemm | ( C \leftarrow \alpha A B + \beta C ) | cublasSgemm |
4.2 完整示例:cublasSaxpy
下面给出使用 cublasSaxpy 实现 ( y \leftarrow \alpha x + y ) 的完整示例。包含:创建 cuBLAS 句柄、在设备上分配并初始化向量、调用 cublasSaxpy、将结果拷回主机并验证。
1 |
|
编译与运行:
1 | nvcc -o cublas_saxpy cublas_saxpy.cu -lcublas |
说明:cublasSaxpy(handle, N, &alpha, d_x, incx, d_y, incy) 中,incx/incy 为步长(通常为 1)。cuBLAS 默认使用列主序(column-major)存储矩阵;对于一维向量,顺序与 C 数组一致。所有指针必须是设备指针,且由调用者保证分配尺寸正确。
4.3 矩阵乘法与 cublasSgemm
矩阵乘法是 Level-3 BLAS 的核心,( C \leftarrow \alpha A B + \beta C )。cuBLAS 提供 cublasSgemm(单精度)、cublasDgemm(双精度)等。接口形式(与书中及 cuBLAS 文档一致):
1 | cublasStatus_t cublasSgemm(cublasHandle_t handle, |
其中 m, n, k 分别表示矩阵 ( C ) 的行数、列数,以及 ( A ) 的列数(也是 ( B ) 的行数);transa/transb 表示是否对 ( A )、( B ) 做转置;lda、ldb、ldc 为各矩阵的 leading dimension(列主序下为行数,或转置后的逻辑行数)。使用 cuBLAS 做矩阵乘法时,通常能获得接近 GPU 峰值性能的吞吐,无需手写 gemm 核函数。
重难点:(1) 列主序:cuBLAS 与 Fortran BLAS 一致,矩阵按列优先存储;若你的数据是 C 语言行主序,要么在调用前转置,要么利用
transa/transb用逻辑上的「转置」来匹配。(2) 设备指针:所有A、B、C、x、y必须指向设备内存,传主机指针会导致未定义行为。(3) leading dimension:在列主序下,lda通常为矩阵的行数(即连续存储的一列的长度),用于表达子矩阵或 strided 布局。
五、cuFFT 库(书 8.4 节)
本节对应书中对 cuFFT 的讲解:库的定位、一维/多维 FFT 的规划与执行,以及完整示例。
5.1 cuFFT 简介
cuFFT 是 NVIDIA 提供的 快速傅里叶变换(FFT) GPU 库,支持一维、二维、三维 FFT,以及实数到复数(R2C)、复数到实数(C2R)、复数到复数(C2C)等多种类型。这一章会说明:FFT 在信号处理、图像处理、求解偏微分方程等领域应用广泛,手写高性能 FFT 核函数难度大,使用 cuFFT 可以快速获得接近最优的性能。
cuFFT 的典型流程为:(1) 创建计划(plan)——cufftPlan1d / cufftPlan2d / cufftPlan3d 或 cufftPlanMany;(2) 执行 FFT——cufftExecR2C、cufftExecC2R、cufftExecC2C 等;(3) 销毁计划——cufftDestroy。计划中会包含 FFT 的维度、批大小、数据类型等信息,执行阶段可多次复用同一计划,从而摊薄规划开销。
5.2 一维实数 FFT 完整示例
下面给出与书中风格一致的、一维实数 FFT(R2C) 的完整示例:对长度为 N 的实序列做正向 FFT,得到 ( N/2+1 ) 个复数(利用对称性);再做逆变换 C2R 可与原数据对比验证。
1 |
|
编译与运行:
1 | nvcc -o cufft_r2c cufft_r2c.cu -lcufft |
说明:实数 FFT 结果具有共轭对称性,cuFFT 只输出 ( N/2+1 ) 个复数,节省存储与带宽。cufftExec* 为异步调用,若需与主机或其他内核同步,应在后续使用 cudaDeviceSynchronize() 或流/事件。
5.3 cuFFT 与公式对应及易错点
离散傅里叶变换(DFT)的正向形式(书中常给出)为:
[
X_k = \sum_{n=0}^{N-1} x_n , e^{-2\pi i k n / N}, \quad k = 0, \ldots, N-1.
]
对于实数输入,( X_k = \overline{X_{N-k}} ),因此只需存储 ( k = 0, \ldots, N/2 )。cuFFT 的 R2C 输出即对应 ( X_0, \ldots, X_{N/2} )。逆变换 C2R 由 ( X_0, \ldots, X_{N/2} ) 恢复实序列 ( x_n )。单位与归一化(如是否除以 ( N ))以 cuFFT 文档为准。
易错点:(1) R2C 输出长度:一维实数 FFT 输出是 N/2+1 个复数,不是 N;分配频谱缓冲区时必须按此大小,否则越界。(2) 异步执行:
cufftExec*与 cuBLAS 类似,默认在当前流上异步执行;若紧接着在主机上使用结果或启动依赖该结果的内核,需显式同步或使用流与事件(第 6 章)。
六、cuRAND 库(书 8.5 节)
本节对应书中对 cuRAND 的讲解:在设备端或主机端生成随机数、生成器类型与完整示例。
6.1 cuRAND 简介
cuRAND 提供在 GPU 上或由 GPU 支持的主机端高质量随机数生成。这一章会说明:蒙特卡洛模拟、随机算法、随机测试数据生成等场景都需要大量随机数,在设备端用 cuRAND 生成可以避免主机到设备的大规模传输,并可与核函数融合使用。cuRAND 支持多种生成器(如 XORWOW、MRG32k3a、Philox 等),以及均匀分布、正态分布等不同分布。
典型用法分为两类:(1) 设备端 API:在核函数或设备代码中调用 curand_init 初始化状态,再用 curand 系列函数生成随机数;(2) 主机端 API:使用 curandGenerateUniform、curandGenerateNormal 等,由库在 GPU 上生成随机数并写入设备内存,主机通过 cudaMemcpy 取回(若需要)。两种用法均有示例,可参考原书或库文档。
6.2 主机端 API 示例:生成均匀分布随机数
下面给出与书中风格一致的、使用 主机端 cuRAND API 在设备上生成均匀分布随机数并拷回主机的完整示例。
1 |
|
编译与运行:
1 | nvcc -o curand_uniform curand_uniform.cu -lcurand |
说明:curandGenerateUniform(gen, d_data, N) 在设备内存 d_data 中写入 N 个 ([0,1)) 均匀分布的单精度随机数。其它分布(如 curandGenerateNormal)以及设备端在核函数内调用 cuRAND 的写法可参考原书或 cuRAND 文档。
七、CUDA 6 中的库特性(书 8.6 节)
本节对应书中对 CUDA 6 引入的、与库相关特性的介绍:多 GPU 支持、统一内存(Unified Memory) 与库的集成等。
7.1 多 GPU 与库
这一章会说明:自 CUDA 6 起,部分库开始更好地支持多 GPU 场景。例如,应用可以在不同设备上创建各自的 cuBLAS 句柄,并配合 cudaSetDevice() 与多流,将不同线性代数任务分配到不同 GPU 上执行。库的 API 通常与单 GPU 一致,多 GPU 的协调由应用程序通过设备选择与流来管理。这与第 9 章多 GPU 编程的主题相衔接。
7.2 统一内存与库
统一内存(Unified Memory) 允许使用同一指针在主机与设备间透明迁移数据,减少显式的 cudaMemcpy。CUDA 6 中库对统一内存的支持方面:部分库在传入「托管」指针时能正确在设备上访问数据,从而简化主机端代码。具体支持情况以各库文档为准。开发者在使用库时,可优先查阅当前 CUDA 版本文档中「Unified Memory」与各库的兼容说明,以便在简化代码与性能之间做出选择。
八、CUDA 库性能概览(书 8.7 节)
本节对应书中对库性能调查的总结:各库在典型问题规模下的性能关注点与选型建议。
8.1 性能对比的意义
cuBLAS、cuFFT、cuSPARSE 等在典型基准测试下的性能数据(如 GFlops、带宽利用率、与手写内核或 CPU 实现的对比)。目的是说明:正确使用库往往能达到或接近 GPU 理论峰值,且远快于未优化的 CPU 实现。与第 2 章、第 4 章一致,有效带宽可用来评估内存受限型库例程的表现:
[
\text{有效带宽(GB/s)} = \frac{(\text{读字节数} + \text{写字节数})}{\text{运行时间(秒)}}
]
下表为概念性归纳(与书中库性能调查对应;具体数值以原书该章性能表为准):
| 库 | 典型运算 | 性能关注点 |
|---|---|---|
| cuBLAS | gemm, axpy | 接近峰值 TFlops,带宽利用率 |
| cuFFT | 1D/2D/3D FFT | 与 FFT 规模、批大小相关 |
| cuSPARSE | SpMV | 与稀疏模式、格式选择相关 |
| cuRAND | 均匀/正态分布 | 生成吞吐(样本/秒) |
8.2 选型与使用建议
选型与使用建议可归纳为:(1) 优先选用与问题匹配的库,避免重复造轮子;(2) 注意数据布局与 API 约定(如列主序、复数格式);(3) 复用计划/句柄,减少重复创建与销毁的开销;(4) 结合流与多 GPU(第 6 章、第 9 章)实现更大规模的并行与重叠。这些与前面章节形成呼应。
九、使用 OpenACC(书 8.8 节)
本节对应书中对 OpenACC 的讲解:指令式并行编程模型、常用指令与数据管理、与 CUDA 的对比。
9.1 OpenACC 简介
OpenACC 是一种基于编译器指令的并行编程模型,允许程序员在现有 C/C++ 或 Fortran 代码中通过 pragma 标注并行区域和数据映射,由支持 OpenACC 的编译器(如 PGI/NVIDIA HPC SDK 的 nvc/nvc++)自动生成主机与设备代码,并将指定区域卸载到 GPU 执行。这一章会说明:OpenACC 的定位是渐进式加速——不需要重写整个程序为 CUDA,只需在热点循环或区域添加指令,适合遗留代码或希望减少编程负担的场景。
OpenACC 与 CUDA 的关系可以概括为:OpenACC 是指令层抽象,CUDA 是显式 API 与核函数。OpenACC 编译器最终可能生成 CUDA 代码或直接调用 CUDA 运行时;使用 OpenACC 不要求开发者直接写 __global__ 核函数,但理解 GPU 执行模型有助于写出更高效的指令(如循环划分、数据局部性)。
9.2 常用指令与数据区域
常用 OpenACC 指令包括:
| 指令类型 | 典型写法 | 含义 |
|---|---|---|
| 并行区域 | #pragma acc parallel |
后续结构化块在加速器上并行执行 |
| 循环 | #pragma acc loop |
标注可并行化的循环,由编译器映射到 GPU 线程 |
| 数据区域 | #pragma acc data copyin(A) copyout(B) |
进入区域时将 A 拷入设备,离开时将 B 拷回主机 |
| 计算区域 | #pragma acc kernels |
由编译器自动分析并并行化区域内代码 |
数据子句常见有:copyin(进入时主机→设备)、copyout(离开时设备→主机)、copy(进入拷入、离开拷出)、create(在设备上分配,不拷回)等。可用 parallel + loop 或 kernels 加速循环,具体示例见原书。
9.3 简单 OpenACC 示例:向量加法
下面给出与书中风格一致的、用 OpenACC 实现的向量加法示例。通过 #pragma acc data 管理数据,#pragma acc parallel loop 将循环卸载到 GPU。
1 |
|
编译(需 OpenACC 编译器,如 NVIDIA HPC SDK 的 nvc):
1 | nvc -acc -Minfo=accel -o vecadd_acc vecadd_acc.c |
说明:#pragma acc data 划定数据生命周期;copyin(A[0:N], B[0:N]) 表示将 A、B 拷入设备;copyout(C[0:N]) 表示在离开区域时将 C 拷回主机。#pragma acc parallel loop 表示接下来的 for 循环在加速器上并行执行。实际性能取决于编译器对循环的映射与数据移动策略,可通过 -Minfo=accel 查看编译器生成的加速信息。
9.4 OpenACC 与 CUDA 的对比
OpenACC 与 CUDA 的简要对比归纳如下:
| 维度 | OpenACC | CUDA C |
|---|---|---|
| 编程方式 | 指令标注,编译器生成并行代码 | 显式写核函数与内存/流 API |
| 可移植性 | 可面向多厂商加速器(若编译器支持) | 面向 NVIDIA GPU |
| 控制粒度 | 较粗,依赖编译器优化 | 细粒度,可精确控制线程与内存 |
| 适用场景 | 遗留代码渐进式加速、快速原型 | 高性能需求、深度调优、库开发 |
理解两者关系后,可以在同一项目中混用:热点用 CUDA 或库,其余用 OpenACC 快速移植。
理解与体会:OpenACC 的数据子句是易错点:
copyin/copyout/copy/create决定了数据何时在主机与设备间移动,写错会导致结果未更新或重复拷贝。另外,OpenACC 依赖支持 OpenACC 的编译器(如 nvc),与纯 nvcc 的 CUDA 项目在构建链上不同;若团队已有大量 Fortran/C 科学计算代码,用 OpenACC 做「最小侵入」的 GPU 加速往往比全量重写为 CUDA 更现实。
十、本章小结与重难点回顾
10.1 知识小结(与书中对应)
- CUDA 库:cuBLAS(稠密线性代数)、cuFFT(FFT)、cuSPARSE(稀疏运算)、cuRAND(随机数);优先用库可减少开发成本并获接近最优性能。选用时按问题域匹配库,注意链接(-lcublas -lcufft 等)与头文件。
- 库的通用模式:句柄/计划管理、设备指针、正确链接;注意数据布局(如 cuBLAS 列主序)与 API 约定;多数库调用为异步,需在需要时同步或配合流。
- cuBLAS:Level-1/2/3(saxpy、gemv、gemm);列主序、leading dimension;所有数据指针为设备指针。
- cuFFT:Plan → Execute → Destroy;R2C 输出长度为 N/2+1;cufftExec* 为异步。
- cuSPARSE:稀疏格式(CSR、COO、CSC、HYB)与 SpMV;格式选择对性能影响大。
- cuRAND:主机端 API(curandGenerateUniform 等)与设备端在核函数内初始化;多种生成器与分布。
- CUDA 6:多 GPU 与统一内存在库中的支持,便于多设备与简化数据管理。
- OpenACC:指令式并行,
#pragma acc data、parallel、loop、kernels;适合渐进式加速与遗留代码,与 CUDA 可互补。
10.2 重难点速查
| 重难点 | 要点 |
|---|---|
| 库的选用 | 稠密线性代数→cuBLAS;FFT→cuFFT;稀疏→cuSPARSE;随机数→cuRAND。能库则库,不能库再手写。 |
| cuBLAS 列主序与设备指针 | 矩阵按列主序;所有 A、B、C、x、y 为设备指针;C 行主序数据需转置或用 trans 参数。 |
| cuFFT R2C 长度与异步 | 一维实数 FFT 输出为 N/2+1 个复数;cufftExec* 异步,需同步或流。 |
| cuSPARSE 格式选择 | CSR 常用于行访问为主的 SpMV;格式选择显著影响性能。 |
| OpenACC 数据子句 | copyin/copyout/copy/create 决定数据移动时机;写错会导致结果错误或多余拷贝。 |
| OpenACC 与 CUDA 的取舍 | 遗留代码、快速移植用 OpenACC;高性能、深度调优、库开发用 CUDA。 |
10.3 学习思考
- 与第 2 章的衔接:第 2 章介绍了性能模型与「计算时间 vs 传输时间」;使用库时,同样要关注数据在主机与设备间的移动(cudaMemcpy 或 OpenACC 的 data 子句),以及库调用与流的重叠(第 6 章),才能发挥库的吞吐优势。
- 与第 6 章的关系:库的 API 多数支持将句柄与流绑定(如 cublasSetStream),从而在多流上重叠多个库调用或库与手写内核的重叠;第 9 章多 GPU 则涉及每设备独立句柄与流。
- 实践建议:(1) 新项目优先查是否有现成库可覆盖问题域;(2) 使用库时严格遵循文档中的数据布局(列主序、R2C 长度等)和指针要求(设备指针);(3) 复用句柄/计划,避免在循环内反复创建销毁;(4) 需要与主机或其他内核同步时,显式调用 cudaDeviceSynchronize 或使用事件与流。
下表为本章自检要点(与书中描述一致):
| 要点 | 说明 |
|---|---|
| 库的选用 | cuBLAS/cuFFT/cuSPARSE/cuRAND 按问题域选用;能库则库 |
| cuBLAS | 句柄、设备指针、列主序;saxpy、gemm 的典型参数 |
| cuFFT | Plan → Exec → Destroy;R2C 输出 N/2+1;异步 |
| cuSPARSE | CSR/COO 等格式;SpMV 流程;格式影响性能 |
| cuRAND | 主机端 curandGenerateUniform;设备端 curand_init + curand |
| OpenACC | data copyin/copyout;parallel loop;与 CUDA 对比 |
| 链接 | -lcublas -lcufft -lcurand 等;头文件 cublas_v2.h、cufft.h 等 |
下一章预告
在下一篇博客中,我们将进入第 9 章:多 GPU 编程:
- 多 GPU 系统拓扑与通信(PCIe、NVLink 等)
- 单机多 GPU 的线程与进程模型(单进程多设备、多进程)
- 多 GPU 间的数据划分与负载均衡
- 多 GPU 与库、流的结合
从单 GPU 的库与指令,扩展到多 GPU 的协同——我们下一章见。
本章自测
- cuBLAS 的矩阵存储顺序是什么?若用 C 语言行主序存储,调用 cublasSgemm 时应注意什么?
- 一维实数 FFT(R2C)输出有多少个复数?为何不是 N?
- OpenACC 中
#pragma acc data与#pragma acc parallel loop分别负责什么?
答案与解析
- cuBLAS 使用列主序(column-major),与 C 的行主序相反。用 C 存矩阵时,可把 A、B 视为「逻辑上的转置」传入,或调用时交换 A/B 与 trans 参数以等效行主序乘法。
- 实数序列的 FFT 具有共轭对称性,只需存 N/2+1 个复数即可完整表示。因此 R2C 输出长度为 N/2+1,不是 N。
#pragma acc data定义数据区域,在进入区域时把数据拷到设备、离开时拷回(或指定 copyin/copyout 等)。#pragma acc parallel loop将紧接着的循环在设备上并行执行,可配合 data 区域使用。
系列导航:导读 | 上一篇:第7章 调整指令级原语 | 下一篇:第9章 多GPU编程
本文为「CUDA C编程权威指南」系列博客第 8 篇,共 10 章。基于《Professional CUDA C Programming》by John Cheng, Max Grossman, Ty McKercher。
