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