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 集成的核心流程:

CMake 与 CUDA 集成核心流程 启用 CUDA 语言 project(LANGUAGES CUDA) 设置 GPU 架构 CMAKE_CUDA_ARCHITECTURES 混合编译 .cu + .cpp 编译选项控制 生成器表达式区分语言 链接 CUDA 库 CUDA::cudart / cublas 最终产出:支持 CUDA 的可执行文件 / 库 自动处理 .cu 编译、架构兼容、链接依赖 注意:CMAKE_CUDA_ARCHITECTURES 支持多个架构,用分号分隔

六、避坑指南

最后,分享几个我实际项目中遇到的坑:

  • 路径问题: 我曾经在 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 核函数了。


公众号:蓝海资料掘金营,微信deep3321