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 dataparallel loop 的向量加;说明与 CUDA 的对比
掌握库的链接与头文件性能选型的一般建议 能正确链接 -lcublas -lcufft 等;能根据问题域选库并注意复用句柄/计划

1.3 博客阅读导图(本章架构)

阅读建议:本章篇幅较长。二、四、五、九节建议精读(库概述、cuBLAS/cuFFT 与 OpenACC 最常用);三、六节(cuSPARSE、cuRAND)可按需选读;七节(CUDA 6 库特性)为可略读,了解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
第 8 章  GPU 加速库和 OpenACC

├── 二、CUDA 库概述(书 8.1 节)【精读】
│ ├── 为什么使用 GPU 加速库
│ ├── 库的分类与选用场景
│ └── 库的链接与头文件 ★ 实践必会

├── 三、cuSPARSE 库(书 8.2 节)★ 重难点【选读】
│ ├── 简介与稀疏格式(COO、CSR、CSC、HYB)
│ └── SpMV 流程与格式选择对性能的影响

├── 四、cuBLAS 库(书 8.3 节)★ 重难点【精读】
│ ├── 简介与 API 风格(句柄、列主序、设备指针)
│ ├── 完整示例:cublasSaxpy
│ └── 矩阵乘法 cublasSgemm 与 leading dimension

├── 五、cuFFT 库(书 8.4 节)【精读】
│ ├── 简介与 Plan/Execute/Destroy 流程
│ ├── 一维实数 FFT(R2C)完整示例
│ └── R2C 输出长度 N/2+1 与异步语义 ★ 易错点

├── 六、cuRAND 库(书 8.5 节)【选读】
│ ├── 设备端 vs 主机端 API
│ └── 主机端均匀分布示例与生成器类型

├── 七、CUDA 6 中的库特性(书 8.6 节)【可略读】
│ ├── 多 GPU 与库
│ └── 统一内存与库的集成

├── 八、CUDA 库性能概览(书 8.7 节)
│ ├── 性能对比的意义与有效带宽
│ └── 选型与使用建议(复用句柄/计划、结合流与多 GPU)

├── 九、使用 OpenACC(书 8.8 节)★ 重难点【精读】
│ ├── OpenACC 简介与定位(渐进式加速)
│ ├── 常用指令与数据区域(data、parallel、loop、kernels)
│ ├── 简单示例:向量加法
│ └── OpenACC 与 CUDA 的对比

└── 十、本章小结与重难点回顾

二、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
2
3
4
5
# 以 cuBLAS 为例:链接 libcublas
nvcc -o myapp myapp.cu -lcublas

# 多库时
nvcc -o myapp myapp.cu -lcublas -lcufft -lcurand

头文件通常为 cublas_v2.hcufft.hcusparse.hcurand.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
2
3
4
5
6
7
8
9
// 概念性流程:创建句柄、描述矩阵、执行 SpMV
cusparseHandle_t handle;
cusparseCreate(&handle);

// 设置 CSR 格式的 A:行指针 d_csrRowPtr,列索引 d_csrColInd,值 d_csrVal
// 调用 cusparseSpMV 或 cusparseCsrmv 等(以实际版本 API 为准)
// ...

cusparseDestroy(handle);

稀疏矩阵的格式选择(CSR、CSC、HYB 等)会显著影响性能,cuSPARSE 文档会给出各格式的适用场景与性能建议。对于 SpMV ( y \leftarrow A x ),数学上即 ( y_i = \sum_j A_{ij} x_j )(对非零 ( A_{ij} ) 求和);库内部会根据格式与矩阵结构选择最优内核,无需用户手写稀疏访问模式。

重难点:格式选择是 cuSPARSE 使用的关键。行访问为主的运算(如 SpMV 按行输出)通常用 CSR 更高效;列访问为主则考虑 CSCHYB 适合非零元分布极不均匀的矩阵。若从稠密或其它格式转换,需注意转换 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)的 axpycublasDgemm 表示双精度(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
#include <cublas_v2.h>

#define CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
printf("CUDA Error: %s:%d, %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)

#define CUBLAS_CHECK(call) \
do { \
cublasStatus_t status = call; \
if (status != CUBLAS_STATUS_SUCCESS) { \
printf("cuBLAS Error: %s:%d\n", __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while (0)

int main(int argc, char **argv) {
const int N = 1 << 20; // 1M 元素
size_t size = N * sizeof(float);
float alpha = 2.0f;

// 主机端分配并初始化
float *h_x = (float *)malloc(size);
float *h_y = (float *)malloc(size);
for (int i = 0; i < N; i++) {
h_x[i] = 1.0f;
h_y[i] = 2.0f;
}

// 设备端分配
float *d_x = NULL;
float *d_y = NULL;
CHECK(cudaMalloc((void **)&d_x, size));
CHECK(cudaMalloc((void **)&d_y, size));
CHECK(cudaMemcpy(d_x, h_x, size, cudaMemcpyHostToDevice));
CHECK(cudaMemcpy(d_y, h_y, size, cudaMemcpyHostToDevice));

// 创建 cuBLAS 句柄并执行 saxpy: y = alpha*x + y
cublasHandle_t handle;
CUBLAS_CHECK(cublasCreate(&handle));
CUBLAS_CHECK(cublasSaxpy(handle, N, &alpha, d_x, 1, d_y, 1));
CHECK(cudaDeviceSynchronize());

// 结果拷回主机
CHECK(cudaMemcpy(h_y, d_y, size, cudaMemcpyDeviceToHost));

// 简单验证:y[i] 应为 alpha*1 + 2 = 4.0
float maxError = 0.0f;
for (int i = 0; i < N; i++) {
float expected = alpha * h_x[i] + 2.0f;
float diff = fabsf(h_y[i] - expected);
if (diff > maxError) maxError = diff;
}
printf("cuBLAS Saxpy: max error = %e\n", maxError);

// 释放资源
CUBLAS_CHECK(cublasDestroy(handle));
CHECK(cudaFree(d_x));
CHECK(cudaFree(d_y));
free(h_x);
free(h_y);

return 0;
}

编译与运行:

1
2
nvcc -o cublas_saxpy cublas_saxpy.cu -lcublas
./cublas_saxpy

说明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
2
3
4
5
6
7
8
cublasStatus_t cublasSgemm(cublasHandle_t handle,
cublasOperation_t transa, cublasOperation_t transb,
int m, int n, int k,
const float *alpha,
const float *A, int lda,
const float *B, int ldb,
const float *beta,
float *C, int ldc);

其中 m, n, k 分别表示矩阵 ( C ) 的行数、列数,以及 ( A ) 的列数(也是 ( B ) 的行数);transa/transb 表示是否对 ( A )、( B ) 做转置;ldaldbldc 为各矩阵的 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 / cufftPlan3dcufftPlanMany;(2) 执行 FFT——cufftExecR2CcufftExecC2RcufftExecC2C 等;(3) 销毁计划——cufftDestroy。计划中会包含 FFT 的维度、批大小、数据类型等信息,执行阶段可多次复用同一计划,从而摊薄规划开销。

5.2 一维实数 FFT 完整示例

下面给出与书中风格一致的、一维实数 FFT(R2C) 的完整示例:对长度为 N 的实序列做正向 FFT,得到 ( N/2+1 ) 个复数(利用对称性);再做逆变换 C2R 可与原数据对比验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <cuda_runtime.h>
#include <cufft.h>

#define CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
printf("CUDA Error: %s:%d, %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)

#define N 1024

int main(void) {
size_t real_size = N * sizeof(float);
size_t complex_size = (N / 2 + 1) * sizeof(cufftComplex);

// 主机端:准备实数输入
float *h_signal = (float *)malloc(real_size);
for (int i = 0; i < N; i++)
h_signal[i] = (float)sin(2.0 * M_PI * 7.0 * i / N);

// 设备端分配
float *d_signal = NULL;
cufftComplex *d_spectrum = NULL;
CHECK(cudaMalloc((void **)&d_signal, real_size));
CHECK(cudaMalloc((void **)&d_spectrum, complex_size));
CHECK(cudaMemcpy(d_signal, h_signal, real_size, cudaMemcpyHostToDevice));

// 创建 cuFFT 计划并执行 R2C
cufftHandle plan;
cufftPlan1d(&plan, N, CUFFT_R2C, 1);
cufftExecR2C(plan, d_signal, d_spectrum);
CHECK(cudaDeviceSynchronize());

// 可选:逆变换 C2R 验证(书中可给出完整验证)
cufftDestroy(plan);

CHECK(cudaFree(d_signal));
CHECK(cudaFree(d_spectrum));
free(h_signal);

printf("cuFFT R2C 1D completed for N = %d\n", N);
return 0;
}

编译与运行:

1
2
nvcc -o cufft_r2c cufft_r2c.cu -lcufft
./cufft_r2c

说明:实数 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:使用 curandGenerateUniformcurandGenerateNormal 等,由库在 GPU 上生成随机数并写入设备内存,主机通过 cudaMemcpy 取回(若需要)。两种用法均有示例,可参考原书或库文档。

6.2 主机端 API 示例:生成均匀分布随机数

下面给出与书中风格一致的、使用 主机端 cuRAND API 在设备上生成均匀分布随机数并拷回主机的完整示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
#include <curand.h>

#define CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
printf("CUDA Error: %s:%d, %s\n", __FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)

#define CURAND_CHECK(call) \
do { \
curandStatus_t status = call; \
if (status != CURAND_STATUS_SUCCESS) { \
printf("cuRAND Error: %s:%d\n", __FILE__, __LINE__); \
exit(EXIT_FAILURE); \
} \
} while (0)

int main(void) {
const size_t N = 1000000;
size_t size = N * sizeof(float);

float *d_data = NULL;
CHECK(cudaMalloc((void **)&d_data, size));

// 创建 cuRAND 生成器(主机端 API)
curandGenerator_t gen;
CURAND_CHECK(curandCreateGenerator(&gen, CURAND_RNG_PSEUDO_XORWOW));
CURAND_CHECK(curandSetPseudoRandomGeneratorSeed(gen, 1234ULL));

// 在设备上生成 [0, 1) 均匀分布
CURAND_CHECK(curandGenerateUniform(gen, d_data, N));
CHECK(cudaDeviceSynchronize());

// 拷回主机并简单统计
float *h_data = (float *)malloc(size);
CHECK(cudaMemcpy(h_data, d_data, size, cudaMemcpyDeviceToHost));

double sum = 0.0;
for (size_t i = 0; i < N; i++) sum += h_data[i];
printf("cuRAND Uniform: mean = %.6f (expected 0.5)\n", sum / N);

CURAND_CHECK(curandDestroyGenerator(gen));
CHECK(cudaFree(d_data));
free(h_data);

return 0;
}

编译与运行:

1
2
nvcc -o curand_uniform curand_uniform.cu -lcurand
./curand_uniform

说明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 + loopkernels 加速循环,具体示例见原书。

9.3 简单 OpenACC 示例:向量加法

下面给出与书中风格一致的、用 OpenACC 实现的向量加法示例。通过 #pragma acc data 管理数据,#pragma acc parallel loop 将循环卸载到 GPU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>

#define N (1024 * 1024)

int main(void) {
float *A = (float *)malloc(N * sizeof(float));
float *B = (float *)malloc(N * sizeof(float));
float *C = (float *)malloc(N * sizeof(float));

for (int i = 0; i < N; i++) {
A[i] = 1.0f;
B[i] = 2.0f;
}

// OpenACC:数据拷入设备,计算在加速器上执行,结果拷回
#pragma acc data copyin(A[0:N], B[0:N]) copyout(C[0:N])
{
#pragma acc parallel loop
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
}

// 验证
float maxError = 0.0f;
for (int i = 0; i < N; i++) {
float e = C[i] - 3.0f;
if (e < 0) e = -e;
if (e > maxError) maxError = e;
}
printf("OpenACC vector add: max error = %e\n", maxError);

free(A);
free(B);
free(C);
return 0;
}

编译(需 OpenACC 编译器,如 NVIDIA HPC SDK 的 nvc):

1
2
nvc -acc -Minfo=accel -o vecadd_acc vecadd_acc.c
./vecadd_acc

说明#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 dataparallelloopkernels;适合渐进式加速与遗留代码,与 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 的协同——我们下一章见。


本章自测

  1. cuBLAS 的矩阵存储顺序是什么?若用 C 语言行主序存储,调用 cublasSgemm 时应注意什么?
  2. 一维实数 FFT(R2C)输出有多少个复数?为何不是 N?
  3. OpenACC 中 #pragma acc data#pragma acc parallel loop 分别负责什么?

答案与解析

  1. cuBLAS 使用列主序(column-major),与 C 的行主序相反。用 C 存矩阵时,可把 A、B 视为「逻辑上的转置」传入,或调用时交换 A/B 与 trans 参数以等效行主序乘法。
  2. 实数序列的 FFT 具有共轭对称性,只需存 N/2+1 个复数即可完整表示。因此 R2C 输出长度为 N/2+1,不是 N。
  3. #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。