27、CMake与CUDA:enable_language(CUDA)、CUDA架构设置、混合编译
说实话,CUDA 和 CMake 的搭配,在几年前还是个让人头疼的事。那时候你要么手写 Makefile,要么用第三方工具。但现在不一样了,CMake 从 3.8 版本开始原生支持 CUDA,3.18 之后更是成熟了很多。我个人是从 CMake 3.10 开始尝试把 CUDA 项目迁移过来的,踩过不少坑,今天把这些经验分享给你。
一、enable_language(CUDA) —— 让 CMake 认识 CUDA
要让 CMake 处理 .cu 文件,第一步就是告诉它:嘿,我要用 CUDA 了。这行代码很简单:
cmake_minimum_required(VERSION 3.18)
project(MyCudaProject LANGUAGES CXX CUDA)
# 或者单独启用
enable_language(CUDA)
嗯,这里有个细节。如果你在 project() 里已经写了 LANGUAGES CUDA,那就不需要再写 enable_language(CUDA) 了。但如果你是在一个已有的 C++ 项目里,后面才想加 CUDA 支持,那就用 enable_language(CUDA) 单独启用。
project() 里直接声明 CUDA,这样更清晰。如果你用 enable_language(),记得它必须在 project() 之后调用,否则 CMake 会报错。
启用之后,CMake 会自动寻找系统上的 CUDA 工具包。它会检查 nvcc 编译器、CUDA 头文件、以及 cudart 库。如果你装了多个版本的 CUDA,可以通过设置 CUDAToolkit_ROOT 来指定路径:
set(CUDAToolkit_ROOT "/usr/local/cuda-11.8")
enable_language(CUDA)
二、CUDA 架构设置 —— 别让你的代码跑不起来
这是最容易踩坑的地方。CUDA 代码编译时,需要指定目标 GPU 架构。说白了,就是告诉 nvcc:你的代码要跑在哪种显卡上。
架构设置不对,会出现两种情况:
- 编译出来的代码在老显卡上跑不了(报错 "no kernel image")
- 或者在新显卡上性能没发挥出来
CMake 提供了两个变量来控制:
# 设置计算能力
set(CMAKE_CUDA_ARCHITECTURES "75;80;86")
# 或者用 native 自动检测当前显卡
set(CMAKE_CUDA_ARCHITECTURES "native")
这里的数字对应 GPU 的计算能力:
| 计算能力 | 对应显卡 |
|---|---|
| 75 | Turing 架构(RTX 20系列) |
| 80 | Ampere 架构(RTX 30系列、A100) |
| 86 | Ampere 架构(RTX 30系列移动版) |
| 89 | Ada Lovelace 架构(RTX 40系列) |
| 90 | Hopper 架构(H100) |
CMAKE_CUDA_ARCHITECTURES="80",结果测试同事的 RTX 2060(计算能力 75)直接跑不起来。后来我改成 "75;80;86",兼容性就好了。如果你不确定用户用什么显卡,建议多写几个架构。
还有一个坑:native 虽然方便,但它只针对你当前编译机器上的显卡。如果你在开发机上编译,然后部署到其他显卡上,那就会出问题。所以 native 只适合个人开发用,团队项目还是老老实实写具体架构号。
三、混合编译 —— C++ 和 CUDA 代码一起玩
一个典型的 CUDA 项目,通常是 C++ 代码调用 CUDA 核函数。CMake 处理这种混合编译非常自然:
add_library(my_lib
main.cpp
kernel.cu
utils.cu
)
target_link_libraries(my_lib
PRIVATE
CUDA::cudart
CUDA::cublas
)
CMake 会自动识别 .cu 文件,并用 nvcc 编译它们。而 .cpp 文件还是用普通的 C++ 编译器。你不需要手动区分,CMake 全包了。
但这里有个细节:分离编译和统一编译。默认情况下,CMake 会把所有 .cu 文件单独编译成 .o 文件,然后链接。这没问题。但如果你想让 nvcc 一次性编译多个 .cu 文件(比如为了更好的优化),可以用:
set(CMAKE_CUDA_SEPARABLE_COMPILATION ON)
我个人习惯是保持默认的分离编译,除非你明确知道需要统一编译。因为分离编译的构建速度更快,而且增量编译也更友好。
四、实战:一个完整的 CUDA 混合编译示例
来看一个完整的 CMakeLists.txt,把上面说的知识点串起来:
cmake_minimum_required(VERSION 3.18)
project(CudaMixDemo LANGUAGES CXX CUDA)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 目标架构:兼容 RTX 20/30/40 系列
set(CMAKE_CUDA_ARCHITECTURES "75;80;86;89")
# 启用 CUDA 可分离编译
set(CMAKE_CUDA_SEPARABLE_COMPILATION ON)
# 添加可执行文件
add_executable(demo
main.cpp
vector_add.cu
matrix_mul.cu
)
# 链接 CUDA 运行时库
target_link_libraries(demo
PRIVATE
CUDA::cudart
)
# 如果用了 cuBLAS,可以这样链接
# find_package(CUDAToolkit REQUIRED)
# target_link_libraries(demo PRIVATE CUDA::cublas)
# 设置 CUDA 编译选项
target_compile_options(demo PRIVATE
$<$<COMPILE_LANGUAGE:CUDA>:--use_fast_math>
$<$<COMPILE_LANGUAGE:CUDA>:-lineinfo>
)
注意最后那个 target_compile_options 的写法。它用了生成器表达式,意思是:只对 CUDA 文件应用这些编译选项,不影响 C++ 文件。这是 CMake 3.18 之后才支持的特性,非常实用。
- 用
project()或enable_language()启用 CUDA - 用
CMAKE_CUDA_ARCHITECTURES指定目标 GPU 架构 - .cu 文件和 .cpp 文件可以混在一起编译,CMake 自动处理
- 用生成器表达式区分 CUDA 和 C++ 的编译选项
五、SVG 知识结构图
下面这张图总结了 CMake 与 CUDA 集成的核心流程:
六、避坑指南
最后,分享几个我实际项目中遇到的坑:
- 路径问题: 我曾经在 Windows 上遇到 CMake 找不到 CUDA 的情况。后来发现是 CUDA 安装路径里有空格。解决办法:安装 CUDA 时选一个没有空格的路径,或者手动设置
CUDAToolkit_ROOT。 - 编译器冲突: 如果你同时装了 MSVC 和 Clang,CMake 可能会选错 CUDA 的宿主编译器。可以用
set(CMAKE_CUDA_HOST_COMPILER clang++)来指定。 - 静态链接: 默认情况下,CMake 会动态链接 CUDA 运行时库。如果你想静态链接,可以设置
set(CMAKE_CUDA_RUNTIME_LIBRARY Static)。这样部署时就不用带 CUDA 的 DLL 了。
嗯,差不多就这些。CUDA 和 CMake 的配合,说白了就是三件事:启用语言、设置架构、混合编译。把这三步走稳了,剩下的就是写你的 CUDA 核函数了。