1
1
# 现代 C++ 的 CUDA 编程
2
2
3
- ## 安装 NVIDIA 驱动
3
+ 参考资料:https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html
4
+
5
+ ## 配置 CUDA 开发环境
6
+
7
+ 硬件方面建议使用至少 GTX 1060 以上显卡,但是更老的显卡也可以运行。
8
+
9
+ 软件方面则可以尽可能最新,以获得 CUDA C++20 支持,我安装的版本是 CUDA 12.5。
10
+
11
+ 以下仅演示 Arch Linux 中安装 CUDA 的方法,因为 Arch Linux 官方源中就自带 ` nvidia ` 驱动和 ` cuda ` 包,而且开箱即用,其他发行版请自行如法炮制。
12
+
13
+ Wendous 用户可能在安装完后遇到“找不到 cuxxx.dll”报错,说明你需要拷贝 CUDA 安装目录下的所有 DLL 到 ` C:\\Windows\\System32 ` 。
14
+
15
+ WSL 用户要注意,WSL 环境和真正的 Linux 相差甚远。很多 Linux 下的教程,你会发现在 WSL 里复刻不出来。这是 WSL 的 bug,应该汇报去让微软统一修复,而不是让教程的作者零零散散一个个代它擦屁股。建议直接在 Wendous 本地安装 CUDA 反而比伺候 WSL 随机拉的 bug 省力。
16
+
17
+ Ubuntu 用户可能考虑卸载 Ubuntu,因为 Ubuntu 源中的版本永不更新。想要安装新出的软件都非常困难,基本只能安装到五六年前的古董软件,要么只能从网上下 deb 包,和 Wendous 一个软耸样。所有官方 apt 源中包的版本从 Ubuntu 发布那一天就定死了,永远不会更新了。这是为了起夜级服务器安全稳定的需要,对于个人电脑而言却只是白白阻碍我们学习,Arch Linux 这样的滚动更新的发行版才更适合个人桌面用户。
18
+
19
+ ### 安装 NVIDIA 驱动
4
20
5
21
首先确保你安装了 NVIDIA 最新驱动:
6
22
@@ -14,21 +30,74 @@ pacman -S nvidia
14
30
nvidia-smi
15
31
```
16
32
33
+ 应该能得到:
34
+
35
+ ```
36
+ Mon Aug 26 14:09:15 2024
37
+ +-----------------------------------------------------------------------------------------+
38
+ | NVIDIA-SMI 555.58.02 Driver Version: 555.58.02 CUDA Version: 12.5 |
39
+ |-----------------------------------------+------------------------+----------------------+
40
+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
41
+ | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
42
+ | | | MIG M. |
43
+ |=========================================+========================+======================|
44
+ | 0 NVIDIA GeForce RTX 4070 ... Off | 00000000:01:00.0 On | N/A |
45
+ | 0% 30C P8 17W / 285W | 576MiB / 16376MiB | 41% Default |
46
+ | | | N/A |
47
+ +-----------------------------------------+------------------------+----------------------+
48
+
49
+ +-----------------------------------------------------------------------------------------+
50
+ | Processes: |
51
+ | GPU GI CI PID Type Process name GPU Memory |
52
+ | ID ID Usage |
53
+ |=========================================================================================|
54
+ | 0 N/A N/A 583 G /usr/lib/Xorg 370MiB |
55
+ | 0 N/A N/A 740 G xfwm4 4MiB |
56
+ | 0 N/A N/A 783 G /usr/lib/firefox/firefox 133MiB |
57
+ | 0 N/A N/A 4435 G obs 37MiB |
58
+ +-----------------------------------------------------------------------------------------+
59
+ ```
60
+
17
61
如果不行,那就重启。
18
62
19
- ## 安装 CUDA
63
+ ### 安装 CUDA
20
64
21
65
然后安装 CUDA Toolkit(即 nvcc 编译器):
22
66
23
67
``` bash
24
68
pacman -S cuda
69
+ ```
70
+
25
71
打开 ` .bashrc ` (如果你是 zsh 用户就打开 ` .zshrc ` ),在末尾添加两行:
26
72
27
73
``` bash
28
74
export PATH=" /opt/cuda/bin:$PATH " # 这是默认的 cuda 安装位置
29
75
export NVCC_CCBIN=" /usr/bin/g++-13" # Arch Linux 用户才需要这一行
30
76
```
31
- # # 常见问题
77
+
78
+ 然后重启 ` bash ` ,或者执行以下命令重载环境变量:
79
+
80
+ ``` bash
81
+ source .bashrc
82
+ ```
83
+
84
+ 运行以下命令测试 CUDA 编译器是否可用:
85
+
86
+ ``` bash
87
+ nvcc --version
88
+ ```
89
+
90
+ 应该能得到:
91
+
92
+ ```
93
+ nvcc: NVIDIA (R) Cuda compiler driver
94
+ Copyright (c) 2005-2024 NVIDIA Corporation
95
+ Built on Thu_Jun__6_02:18:23_PDT_2024
96
+ Cuda compilation tools, release 12.5, V12.5.82
97
+ Build cuda_12.5.r12.5/compiler.34385749_0
98
+ ```
99
+
100
+ ### 常见问题解答
32
101
33
102
CMake 报错找不到 CUDA?添加环境变量:
34
103
@@ -37,53 +106,244 @@ export PATH="/opt/cuda/bin:$PATH" # 这里换成你的 cuda 安装位置
37
106
export NVCC_CCBIN=" /usr/bin/g++-13" # 只有 Arch Linux 需要这一行
38
107
```
39
108
40
- Clangd 报错不认识 ` -forward-unknown-to-host-compiler` 选项?
109
+ IDE 使用了 Clangd 静态检查插件, 报错不认识 ` -forward-unknown-to-host-compiler ` 选项?
41
110
42
111
创建文件 ` ~/.config/clangd/config.yaml ` :
43
112
44
113
``` yaml
45
114
CompileFlags :
46
- Add:
47
- - --cuda-gpu-arch=sm_86
48
- Remove:
115
+ Add : # 要额外添加到 Clang 的 NVCC 没有的参数
116
+ - --no- cuda-version-check
117
+ Remove : # 移除 Clang 不认识的 NVCC 参数
49
118
- -forward-unknown-to-host-compiler
50
- - --expt-relaxed-constexpr
51
- - --expt-extended-lambda
119
+ - --expt-*
52
120
- --generate-code=*
53
121
- -arch=*
122
+ - -rdc=*
123
+ ` ` `
124
+
125
+ ### 建议开启的 CMake 选项
126
+
127
+ #### CUDA 编译器路径
128
+
129
+ 如果你无法搞定环境变量,也可以通过 ` CMAKE_CUDA_COMPILER` 直接设置 `nvcc` 编译器的路径:
130
+
131
+ ` ` ` cmake
132
+ set(CMAKE_CUDA_COMPILER "/opt/cuda/bin/nvcc") # 这里换成你的 cuda 安装位置
54
133
` ` `
55
134
56
- # ## 建议开启的选项
135
+ 不建议这样写,因为会让使用你项目的人也被迫把 CUDA 安装到这个路径去。
57
136
58
- ` ` ` bash
59
- nvcc --expt-relaxed-constexpr --expt-extended-lambda
137
+ 建议是把你的 `nvcc` 安装好后,通过 `PATH` 环境变量,`cmake` 就能找到了,不需要设置这个变量。
138
+
139
+ # ### CUDA C++ 版本
140
+
141
+ CUDA 是一种基于 C++ 的领域特定语言,CUDA C++ 的版本和正规 C++ 一一对应。
142
+
143
+ 目前最新的是 CUDA C++20,可以完全使用 C++20 特性的同时书写 CUDA 代码。
144
+
145
+ - 在 `__host__` 函数(未经特殊修饰的函数默认就是此类,在 CPU 端执行)中,CUDA 和普通 C++ 没有区别,任何普通 C++ 代码,都可以用 CUDA 编译器编译。
146
+ - 在 `__device__` 函数(CUDA kernel,在 GPU 端执行)中,能使用的函数和类就有一定限制了:
147
+ - 例如你不能在 `__device__` 函数里使用仅限 `__host__` 用的 `std::cout`(但 `printf` 可以,因为 CUDA 团队为了方便用户调试,为你做了 `printf` 的 `__device__` 版特化)。
148
+ - ` __device__` 中不能使用绝大多数非 `constexpr` 的 STL 容器,例如 `std::map` 等,但是在 `__host__` 侧还是可以用的!
149
+ - 所有的 `constexpr` 函数也是可以使用的,例如各种 C++ 风格的数学函数如 `std::max`,`std::sin`,这些函数都是 `constexpr` 的,在 `__host__` 和 `__device__` 都能用。
150
+ - 如果一个容器的成员全是 `constexpr` 的,那么他可以在 `__device__` 函数中使用。例如 `std::tuple`、`std::array` 等等,因为不涉及 I/O 和内存分配,都是可以在 `__device__` 中使用的。
151
+ - 例如 C++20 增加了 constexpr-new 的支持,让 `std::vector` 和 `std::string` 变成了 `constexpr` 的容器,因此可以在 `__device__` 中使用 `std::vector`(会用到 `__device__` 版本的 `malloc` 函数,这是 CUDA 的一大特色:你可以在 kernel 内部用 `malloc` 动态分配设备内存,并且从 CUDA C++20 开始 `new` 也可以了)。
152
+ - ` std::variant` 现在也是 `constexpr` 的容器,也可以在 `__device__` 函数中使用了。
153
+ - 异常目前还不是 `constexpr` 的,因此无法在 `__device__` 函数中使用 `try/catch/throw` 系列关键字。
154
+ - 总之,随着,我们可以期待越来越多纯计算的函数和容器能在 CUDA kernel(`__device__` 环境)中使用。
155
+
156
+ 正如 `CMAKE_CXX_STANDARD` 设置了 `.cpp` 文件所用的 C++ 版本,也可以用 `CMAKE_CUDA_STANDARD` 设置 `.cu` 文件所用的 CUDA C++ 版本。
157
+
158
+ ` ` ` cmake
159
+ set(CMAKE_CXX_STANDARD 20) # .cpp 文件采用的 C++ 版本是 C++20
160
+ set(CMAKE_CUDA_STANDARD 20) # .cu 文件采用的 CUDA C++ 版本是 C++20
60
161
` ` `
61
162
62
- * --expt-relaxed-constexpr: 让所有 constexpr 函数都自动带有 __host__ __device__
63
- * --expt-extended-lambda: 允许 lambda 表达式具有 __host__ 和/或 __device__ 修饰
163
+ # ## 赋能现代 C++ 语法糖
64
164
65
- # ## 建议开启的 CMake 选项
165
+ ` ` ` cmake
166
+ set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr --expt-extended-lambda")
167
+ ` ` `
168
+
169
+ * `--expt-relaxed-constexpr`: 让所有 `constexpr` 函数默认自动带有 `__host__ __device__`
170
+ * `--expt-extended-lambda`: 允许为 lambda 表达式指定 `__host__` 或 `__device__`
171
+
172
+ # ### 显卡架构版本号
173
+
174
+ 不同的显卡有不同的“架构版本号”,架构版本号必须与你的硬件匹配才能最佳状态运行,可以略低,但将不能发挥完整性能。
66
175
67
176
` ` ` cmake
68
- set(CMAKE_CUDA_SEPARABLE_COMPILATION ON)
69
- set(CMAKE_CUDA_ARCHITECTURES 86) # 不同的显卡有不同的“架构版本号”,架构版本号必须与你的硬件匹配才能最佳状态运行,可以略低,但将不能发挥完整性能。
70
- set(CMAKE_CUDA_ARCHITECTURES native) # 如果你有 CMake 3.24 以上,可以设定本参数,让 CMake 自动检测当前显卡,并选择准确的架构版本号。
177
+ set(CMAKE_CUDA_ARCHITECTURES 86) # 表示针对 RTX 30xx 系列(Ampere 架构)生成
178
+ set(CMAKE_CUDA_ARCHITECTURES native) # 如果 CMake 版本高于 3.24,该变量可以设为 "native",让 CMake 自动检测当前显卡的架构版本号
71
179
` ` `
72
180
73
181
架构版本号:例如 75 表示 RTX 20xx 系列(Turing 架构);86 表示 RTX 30xx 系列(Ampere 架构);89 表示 RTX 40xx 系列(Ada 架构)等。
74
182
75
183
完整的架构版本号列表可以在 [CUDA 文档](https://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#virtual-architecture-feature-list) 中找到。
76
184
77
- 我的选项是:
185
+ 也可以运行如下命令(如果有的话)查询当前显卡的架构版本号:
186
+
187
+ ` ` ` bash
188
+ __nvcc_device_query
189
+ ` ` `
190
+
191
+ # ### 设备函数分离定义
192
+
193
+ 默认只有 `__host__` 函数可分离声明和定义。如果你需要分离 `__device__` 函数的声明和定义,就要开启这个选项:
194
+
195
+ ` ` ` cmake
196
+ set(CMAKE_CUDA_SEPARABLE_COMPILATION ON) # 可选
197
+ ` ` `
198
+
199
+ # ### 创建 CUDA 项目
200
+
201
+ 完成以上选项的设定后,使用 `project` 命令正式创建 CUDA C++ 项目。
202
+
203
+ ` ` ` cmake
204
+ project(这里填你的项目名 LANGUAGES CXX CUDA)
205
+ ` ` `
206
+
207
+ > {{ icon.fun }} 我见过有人照抄代码把“这里填你的项目名”抄进去的。
208
+
209
+ 如需在特定条件下才开启 CUDA,可以用 `enable_language()` 命令延迟 CUDA 环境在 CMake 中的初始化:
210
+
211
+ ` ` ` cmake
212
+ project(这里填你的项目名 LANGUAGES CXX)
213
+
214
+ ...
215
+
216
+ option(ENABLE_CUDA "Enable CUDA" ON)
217
+
218
+ if (ENABLE_CUDA)
219
+ enable_language(CUDA)
220
+ endif()
221
+ ` ` `
222
+
223
+ # ### CMake 配置总结
224
+
225
+ 注意!以上这些选项设定都必须在 `project()` 命令之前!否则设定了也无效。
226
+
227
+ 因为实际上是 `project()` 命令会检测这些选项,用这些选项来找到编译器和 CUDA 版本等信息。
228
+
229
+ 总之,我的选项是:
78
230
79
231
` ` ` cmake
80
232
cmake_minimum_required(VERSION 3.12)
81
233
82
234
set(CMAKE_CXX_STANDARD 20)
83
235
set(CMAKE_CUDA_STANDARD 20)
84
- set(CMAKE_CUDA_SEPARABLE_COMPILATION ON )
236
+ set(CMAKE_CUDA_SEPARABLE_COMPILATION OFF )
85
237
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr --expt-extended-lambda")
86
238
if (NOT DEFINED CMAKE_CUDA_ARCHITECTURES AND CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
87
239
set(CMAKE_CUDA_ARCHITECTURES native)
88
240
endif()
241
+
242
+ project(你的项目名 LANGUAGES CXX CUDA)
243
+
244
+ file(GLOB sources "*.cpp" "*.cu")
245
+ add_executable(${PROJECT_NAME} ${sources})
246
+ target_link_libraries(${PROJECT_NAME} PRIVATE cusparse cublas)
247
+ ` ` `
248
+
249
+ # # 开始编写 CUDA
250
+
251
+ CUDA 有两套 API:
252
+
253
+ - [CUDA runtime API](https://docs.nvidia.com/cuda/cuda-runtime-api/index.html):更加简单,兼顾性能,无需手动编译 kernel,都替你包办好了,但不够灵活。
254
+ - [CUDA driver API](https://docs.nvidia.com/cuda/cuda-driver-api/index.html):更加灵活多变,但操作繁琐,需要手动编译 kernel,适合有特殊需求的用户。
255
+
256
+ 他们都提供了大量用于管理 CUDA 资源和内存的函数。
257
+
258
+ 我们要学习的是比较易懂、用的也最多的 CUDA runtime API。
259
+
260
+ 使用 `<cuda_runtime.h>` 头文件即可导入所有 CUDA runtime API 的函数和类型:
261
+
262
+ ` ` ` cuda
263
+ #include <cuda_runtime.h>
264
+ ` ` `
265
+
266
+ 虽然 CUDA 基于 C++(而不是 C 语言),支持所有 C++ 语言特性。但其 CUDA runtime API 依然是仿 C 风格的接口,可能是照顾了部分从 C 语言转过来的土木老哥,也可能是为了方便被第三方二次封装。
267
+
268
+ 我们的课程主题是:用现代 C++ 赋能更好的 CUDA 开发,所以会对 CUDA 原生的 C 风格 API 做一些 C++ 封装,使其呈更直观易用的接口,帮助你避免出错(例如内存泄漏)。
269
+
270
+ # ## 认识 CUDA 语言
271
+
272
+ 一份 CUDA 源码和 C/C++ 一样,是由大量的函数组成。
273
+
274
+ 由于 GPU 编程的特殊性,GPU 代码和 CPU 代码是需要分离的,他们的指令集完全不同。
275
+
276
+ CPU 的责任是决定什么时候要“启动(launch)” GPU 代码,而 GPU 只专注于计算。
277
+
278
+ 所以 CPU 的指令集中含有大量的条件和判断,而 GPU 则以计算指令为主。
279
+
280
+ 而 C++ 中,函数就是代码,根据代码执行的位置不同,函数可以分为:
281
+
282
+ - GPU 端执行。
283
+ - CPU 端执行。
284
+
285
+ 为此,就需要对进行标识:
286
+
287
+ CUDA 中的函数分为三大类:
288
+
289
+ - ` __host__` 函数:在 CPU 端执行,只能被 CPU 端函数调用。编译器编译时,会将其编译为 CPU 的汇编(x86 汇编),可以使用所有 C++ 标准库功能(如 `std::cout`)。
290
+ - ` __device__` 函数:在 GPU 端执行,只能被 GPU 端函数调用。编译器编译时,会将其编译为 GPU 的汇编(PTX 汇编),只能使用 C++ 标准库中纯计算的部分功能(如 `std::sin`)。
291
+ - ` __global__` 函数:也是在 GPU 端执行,类似于 `__device__`。区别在于:
292
+ - ` __global__` 函数可以被 CPU 端的函数(`__host__`)调用。
293
+ - ` __device__` 只能被 GPU 端的函数(`__device__` 或 `__global__`)调用。
294
+
295
+ 因而 `__global__` 就像一座桥梁一样,是从 CPU 走向 GPU 的入口点。
296
+
297
+ > {{ icon.tip }} 但反过来,GPU 函数不能再走回 CPU。所以 `__global__` 是单向的一次性桥梁,一旦进入 GPU,就只能等整个 `__global__` 函数退出了。
298
+
299
+ ` __global__` 就像 GPU 版的 `main` 函数一样,是所有 GPU 代码的入口点。
300
+
301
+ 只不过这个入口点可以有很多个,CPU 可以多次提交不同的 `__global__` 函数,就像是创建了许多个不同的“进程”,分别有各自的 `main` 入口点一样。
302
+
303
+ 这样的一次 `__global__` 调用所产生的一个“GPU 版进程”,被称作一个“网格(grid)”。
304
+
305
+ 正如 CPU 上的单个进程由很多子线程组成一样;GPU 上的每个“网格”,由许多“块(block)”组成,“块”又进一步由许多“线程(thread)”组成。
306
+
307
+ > {{ icon.detail }} 实际上,GPU 的“块”才最接近 CPU 上线程概念,而 GPU 所谓的“线程”实际上对应于 CPU 的 SIMD 矢量,稍后会详细介绍。
308
+
309
+ 因为很多时候,人们喜欢直接在 `__global__` 函数里写上所有的计算代码,而不会再定义一个 `__device__` 函数了,所有的计算核心功能都在 `__global__` 函数中。
310
+
311
+ “网格(grid)”是“内核(kernel)”的实例,正如 CPU 上进程是可执行文件(exe)的实例一样:同一个可执行文件可以被 Shell 多次调用产生多个进程,同一个“内核”也可以被 host 函数多次调用产生多个“网格”。
312
+
313
+ 但是,CPU 上的每个进程只会调用一次 `main` 入口点,而 GPU 上的 `__global__` 会被调用 n 次(n 的大小在 host 函数中指定),所有启动的 n 个 `__global__` 函数互相之间是并行执行的,每个线程的入口点都是 `__global__`,因此一个“网格”含有多个“线程”。
314
+
315
+ ` ` ` cuda
316
+ #include <cuda_runtime.h>
317
+
318
+ /*__host__*/ void host_func() {
319
+ }
320
+
321
+ __device__ void device_func() {
322
+ }
323
+
324
+ __host__ __device__ void host_device_func() {
325
+ }
326
+
327
+ constexpr void constexpr_func() {
328
+ }
329
+
330
+ __global__ void kernel() {
331
+ device_func();
332
+ host_device_func();
333
+ constexpr_func(); // 需开启 --expt-relaxed-constexpr
334
+ auto device_lambda = [] __device__ (int i) { // 需开启 --expt-extended-lambda
335
+ return i * 2;
336
+ };
337
+ device_lambda(1);
338
+ }
339
+
340
+ int main() {
341
+ host_func();
342
+ host_device_func();
343
+ constexpr_func();
344
+ auto host_lambda = [] (int i) {
345
+ return i * 2;
346
+ };
347
+ host_lambda(1);
348
+ }
89
349
` ` `
0 commit comments