ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从入门到避坑:用 nvcc 与 CMake 打通 CUDA 编译链路

从入门到避坑:用 nvcc 与 CMake 打通 CUDA 编译链路 搜索框里打着“nvcc 编译”的人多半是刚刚装好 CUDA手里捏着一个.cu文件却被命令行和各种报错折腾得怀疑人生的那一批。我自己第一次跑 CUDA 程序时也干过这种蠢事在 Visual Studio 里建了个控制台工程放了一个.cpp文件照着书里敲__global__编译器直接甩脸identifier __global__ is undefined。后来才明白CUDA 代码必须交给 nvcc 编译.cu后缀是关键而 nvcc 的用法也不只是nvcc xxx.cu这么简单。这篇文章我打算从 nvcc 在 CUDA 工具链里的定位讲起把环境准备、基础命令、架构参数、CMake 工程组织以及 Windows、WSL2 下常见的坑全部串一遍。适合刚接触 CUDA、还没把 nvcc 和普通 C 编译器区分开来的开发者也适合那些装完 CUDA 却不知道怎么验证、怎么开始的人。1. 在 CUDA 生态里nvcc 到底是个什么角色1.1 它不是独立的编译器而是“编译器驱动”很多教程把 nvcc 叫 “CUDA 编译器”这个说法不够准确。更准确地说nvcc 是一个编译器驱动工具。一个.cu文件里混着两种完全不同目标的代码一种是给 CPU 跑的主机代码host code比如main函数、数据分配逻辑另一种是给 GPU 跑的设备代码device code比如用__global__声明的 kernel 函数。这两种代码没法用同一个编译器一步编译完。nvcc 做的事情是先把这两种代码从同一个源码文件里分离出来再把设备代码交给 NVIDIA 自家的 ptxas 后端去编译把主机代码转交给系统里已有的 C 编译器去处理。在 Linux 上这个“接盘”的编译器通常是 g在 Windows 上则是 MSVC 的cl.exe。可以把它看作一个包工头自己不动手干活把活儿拆成几份GPU 部分交给 ptxasCPU 部分交给 gcc 或 MSVC最后再把两拨人的成果封装成一个完整的可执行文件。这也是为什么 Windows 上如果只装了 CUDA Toolkit、没有装 Visual Studio 的 C 桌面开发组件运行 nvcc 大概率会报Cannot find compiler cl.exe。不是 nvcc 自己不能编是它找不到可以托付主机代码的编译器。1.2 完整编译链路从 .cu 到 fatbin 再到可执行文件nvcc 走完一轮完整编译内部的流程大致是这样的先做预处理处理#include、宏展开。解析.cu源码把 host 代码和 device 代码分开。device 代码先被编译成 PTXParallel Thread Execution这是 NVIDIA 定义的一种中间指令集类似 Java 的字节码不绑定具体的显卡。再经过 ptxas 把 PTX 编译成 SASS也就是某种具体 GPU 架构的最终机器码比如sm_89对应的指令。这些编译产物会被打包进一个叫 fatbin 的二进制仓库嵌入到最终的主机目标文件里。host 代码部分交给外部的 C 编译器编译。最后链接生成可执行文件。运行时GPU 驱动从 fatbin 里取出 SASS 直接加载执行。如果 fatbin 里没有当前显卡架构的 SASS但有 PTX驱动可以用 JIT 方式现场把 PTX 编译成 SASS 再执行。PTX 和 SASS 的关系可以类比成“可移植的 C 代码”和“编译后的机器码”前者更通用后者性能更好。理解了这条链路后面看-arch、-gencode这些参数就不会晕了。1.3 为什么 nvcc 不拿来编译纯 .cpp 文件.cu文件能直接用 nvcc 编是因为 nvcc 能识别__global__、__device__、...这些 CUDA 语法。纯.cpp文件虽然也合法但里面没有这些扩展语法直接用 nvcc 去编属于杀鸡用牛刀而且还会引入不必要的 CUDA runtime 依赖。实际项目中的通常做法是.cu文件里实现 kernel 和 host 封装函数外部.cpp文件只需要声明接口并参与链接。也就是说 nvcc 生成的.o文件可以和 g 生成的.o文件混在一起链接。多文件工程里这是一个非常重要的认知不是整个项目全部交给 nvcc而是每个源文件各自用合适的编译器编译最后统一链接。2. 写代码之前先把这三件事确认明白2.1 驱动版本和 Toolkit 版本不是一回事热词里出现频率极高的一个问题是“我明明装好了 CUDA为什么编译报错”。这里面有一大半是把驱动和 Toolkit 搞混了。打开终端执行nvidia-smi右上角会显示一个 CUDA 版本号比如 12.4。这个数字代表当前驱动最高支持的 CUDA runtime 版本并不代表你实际安装的 CUDA Toolkit 版本。Toolkit 才是包含 nvcc 编译器、CUDA 头文件、各类 GPU 库cuBLAS、cuFFT 等等的那一整套开发环境。驱动只负责运行不负责编译。所以正确的关系是驱动向下兼容老版本。如果你系统里驱动支持到 12.4那你装 11.8 的 Toolkit 完全没问题反过来如果驱动只支持到 11.8却装了 12.4 的 Toolkit编译可能没问题但运行程序时会报CUDA driver version is insufficient for CUDA runtime version。处理方式很简单装新版驱动或者降级 Toolkit。2.2 安装完 Toolkit先验证 nvcc 可用装完 Toolkit 后第一件事永远是跑nvcc --version如果能输出类似Cuda compilation tools, release 12.4, V12.4.131的内容说明编译器已经就绪。如果提示command not found不用怀疑八成是 PATH 环境变量没配好。Linux 下的典型配置是把下面几行写进~/.bashrc或~/.zshrcexport PATH/usr/local/cuda/bin:$PATH export LD_LIBRARY_PATH/usr/local/cuda/lib64:$LD_LIBRARY_PATH export CUDA_HOME/usr/local/cudaWindows 下则是在系统环境变量里把C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\bin加到 PATH 里。改完环境变量后新开的终端才会生效。另外 Windows 下建议同时确认 Visual Studio 的cl.exe可用因为前面说过 nvcc 在 Windows 上需要 MSVC 接管主机代码编译。2.3.run文件解压报错的真相热词里有一条 “cuda .run gzip: stdin: invalid compressed>sudo rm /usr/local/cuda sudo ln -s /usr/local/cuda-11.8 /usr/local/cuda配合前面说的 PATH 环境变量切换完软链接后新终端里nvcc --version就能看到对应版本。Windows 下的机制类似安装不同版本后需要修改系统 PATH 中哪个版本排在最前面同时 Visual Studio 的 CUDA 工程属性里也能指定具体版本。3. 第一个 hello.cu从命令行到可执行文件的完整旅程3.1 一个最小可编译的 CUDA 程序先给一个足够简单但完整的示例后续所有命令行演示都基于它。创建一个文件叫hello.cu#include cstdio #include cuda_runtime.h __global__ void hello_kernel() { printf(Hello from GPU, thread %d\n, threadIdx.x); } int main() { hello_kernel1, 4(); cudaDeviceSynchronize(); cudaError_t err cudaGetLastError(); if (err ! cudaSuccess) { printf(CUDA error: %s\n, cudaGetErrorString(err)); return 1; } return 0; }注意两点一是文件后缀必须是.cu如果把上面的代码存成.cpp绝大多数编译器直接报错不识别__global__和...二是代码里加了cudaGetLastError()来捕获 kernel 启动阶段的错误这个习惯从第一天就该养成后面会省掉无数排查时间。1, 4是 CUDA 的启动配置语法分别代表 grid 维度和 block 维度这里意思是启动 1 个 block每个 block 里 4 个线程。3.2 三条最常用命令编译、指定输出、只编不链最基本的编译命令是nvcc hello.cu默认会在当前目录生成a.outWindows 上是类似a.exe或 hello.exe 的输出取决于 nvcc 版本但没人喜欢这种命名。更常见的写法是nvcc hello.cu -o hello-o指定输出文件名这条命令会完成前面说的全流程分离代码、编译 device 部分、调用主机编译器、链接最后得到可执行文件。如果工程里有多个.cu文件或者你想把.cu编译成目标文件再和别的语言的代码对接就要用到“只编译不链接”模式nvcc -c hello.cu -o hello.o这条命令生成hello.o不会做链接。后续可以和其他.o文件一起用g hello.o other.o -o app -lcudart这种形式组合或者继续用 nvcc 链接nvcc hello.o another.o -o app3.3 第一次就遇到 cl.exe 找不到怎么办在 Windows 上跑nvcc hello.cu -o hello新手最常碰到的报错是nvcc fatal : Cannot find compiler cl.exe in PATH原因前面已经讲过nvcc 在 Windows 上需要 MSVC 的cl.exe来处理主机代码。解决办法不是去下载什么“绿色版 cl.exe”而是使用 Visual Studio 自带的开发者命令行工具。打开“开始菜单”找到 “x64 Native Tools Command Prompt for VS 2022”版本号随你的 VS 版本变化在这个终端里执行 nvcc 就不会报这个错。或者手动执行vcvars64.bat来设置环境变量。如果你希望在 VS 图形界面里编译 CUDA那就不需要和命令行纠缠CUDA Toolkit 装好后会自动在 VS 里注册 CUDA C/C 的工程模板和规则文件。提示如果 VS 里新建工程时找不到 CUDA 相关模板去检查安装 CUDA Toolkit 时是否勾选了 “Visual Studio Integration” 组件。3.4 编译通过但运行时报错的经典场景编译出来了一运行却报这种错CUDA error: no kernel image is available for execution on the device意思是程序里嵌入的 GPU 机器码SASS跟你当前显卡的架构不匹配。最常见的场景是你用默认参数编了一个针对老架构的 kernel然后在较新的显卡上运行。这不是语法问题是编译目标架构问题。默认情况下 nvcc 会选择某个它能识别的“通用”架构但到了不同 GPU 世代这个默认值往往不是你想要的。要彻底缓解这类问题必须理解-arch和-gencode也就是下一节的内容。4. 参数不只是 -o架构选择、优化与调试选项4.1 认识显卡架构代号你的显卡是 sm_XXCUDA 文档里那张显卡架构对应表值得花十分钟认真看一遍。我把常用的列出来架构代号计算能力代表显卡sm_707.0V100sm_757.5RTX 20 系列sm_808.0A100sm_868.6RTX 30 系列sm_898.9RTX 40 系列sm_909.0H100、H200sm_10010.0RTX 50 系列Blackwell热词里有人问 “4060ti 支持的 CUDA 版本”其实更准确的问题是“4060 Ti 的计算能力”。它采用的是 Ada Lovelace 架构计算能力是 8.9也就是编译时要指定sm_89。如果你的 CUDA 版本比较老可能不认识新的架构代号。比如 CUDA 11.8 不认识sm_89得升级到 CUDA 12.x 才能正确编译 40 系显卡。这就是所谓的“老 CUDA 不支持新显卡”的真实含义显卡本身没那么挑是编译器不认识它的指令集。4.2-arch和-gencode为当前显卡编译还是为未来显卡编译最简单的写法是nvcc hello.cu -o hello -archsm_89这条命令的含义是生成针对sm_89架构的 SASS 代码并且只生成这一种。如果程序被拷贝到一台只有sm_86显卡的机器上运行时就会触发前面说的no kernel image is available。更稳妥的发布方式是用-gencode同时嵌入多个架构的代码并带上 PTX 作为兜底nvcc hello.cu -o hello \ -gencode archcompute_86,codesm_86 \ -gencode archcompute_89,codesm_89 \ -gencode archcompute_89,codecompute_89这里三行参数分别表示针对 30 系显卡生成 SASS针对 40 系显卡生成 SASS再额外嵌入一份 8.9 计算能力的 PTX。最后那一行是“为未来可能的新驱动做 JIT 准备”如果程序跑在一张未来架构的显卡上驱动可以把 PTX 现场编成新机器的 SASS。提示-arch是-gencode的简化形式。-archsm_89相当于-gencode archcompute_89,codesm_89它生成的是针对该架构的 SASS不含 PTX。想同时保留 JIT 能力必须显式写-gencode。本人日常开发时的习惯是本机测试直接用-archsm_89简单直接要发给别人运行的程序才用-gencode多架构版本避免用户在不同的显卡上踩no kernel image的坑。4.3 优化与调试选项-O3、-G、-lineinfo编译 CUDA 代码时-O3和普通 C/C 编译一样用于开启优化。但设备端代码的很多优化场景和主机代码完全不同这里要引入另一个重要参数nvcc hello.cu -o hello -archsm_89 -O3 --ptxas-options-v--ptxas-options-v的作用是让 nvcc 在编译设备代码时输出寄存器数量、静态共享内存占用、局部内存溢出spill等信息。这是调优 CUDA kernel 时最高频使用的参数之一。输出大概长这样ptxas info : Used 12 registers, 360 bytes smem, 40 bytes cmem[0]如果寄存器用量逼近 255 且出现大量 stack frame局部内存溢出kernel 性能会严重下降。很多“为什么我的 CUDA 程序比不过别人”的问题第一步就看这个输出。调试模式下加-G会保留设备端调试信息可以用 cuda-gdb 或 Nsight 断点调试。但代价是性能会明显下降所以调试完发布版本时一定要去掉-G。还有一个折中方案-lineinfo它只保留行号信息性能影响很小适合 profiling 时定位热点代码。4.4 如何链接第三方 GPU 库除了 CUDA runtime你还可能用到 cuBLAS、cuFFT 这类库。编译链接的方式和链接普通 C 库一样nvcc cublas_example.cu -o cublas_example -lcublas -lcudart-l后面接库名如果库不在默认搜索路径里用-L指定路径nvcc cublas_example.cu -o cublas_example -L/usr/local/cuda/lib64 -lcublasWindows 上对应的库文件一般是cublas.lib或cublas.dll由 CUDA Toolkit 安装到C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.4\lib\x64VS 工程里通常不需要你手动记这些导入库会自动配置。还有一个常见场景编译时想给主机编译器传参数比如-fPIC需要用-Xcompiler转发。发布共享库时非常常见nvcc -c kernel.cu -o kernel.o -Xcompiler -fPIC-Xcompiler后面紧跟的参数会原样传给 g/cl.exe。同理-Xlinker可以转发链接参数理解原理后就能摆脱各种“照着别人的命令抄却不知道什么意思”的状态。5. 从单文件到真实工程让 CMake 接管 nvcc5.1 为什么大型项目不手敲 nvcc单文件编译用命令行很顺手但真实项目往往有几十个.cu文件还要链接不同版本的第三方库。手写一长串 nvcc 命令不仅难维护而且换台机器就编译失败。行业里更通行的做法是用 CMake 来组织 CUDA 工程。CMake 从 3.8 开始引入了对 CUDA 的原生支持从 3.18 开始趋于稳定。新项目不建议再用老式的find_package(CUDA)写法直接让 CMake 把它当成一种编译语言来处理。5.2 新版 CMake 的 CUDA 工程模板一个最小可用的CMakeLists.txt如下cmake_minimum_required(VERSION 3.18) project(cuda_demo LANGUAGES CXX CUDA) set(CMAKE_CUDA_ARCHITECTURES 89) set(CMAKE_CUDA_STANDARD 17) set(CMAKE_CUDA_STANDARD_REQUIRED ON) add_executable(demo main.cpp kernel.cu) target_link_libraries(demo PRIVATE cudart)关键的几行分别是project(... LANGUAGES CXX CUDA)声明这是一个既包含 C 又包含 CUDA 的工程。set(CMAKE_CUDA_ARCHITECTURES 89)指定编译架构为sm_89。也可以写成86;89来同时支持多架构或者用all-major自动包含当前所有主流架构。add_executable(demo main.cpp kernel.cu)让 CMake 自动判断.cu文件用 nvcc 编译.cpp文件用 C 编译器编译最后统一链接。CMake 配置和构建过程依然是cmake -S . -B build cmake --build build如果 CMake 找不到 nvcc会用类似下面的配置命令指定cmake -S . -B build -DCMAKE_CUDA_COMPILER/usr/local/cuda/bin/nvcc5.3 老式 find_package 写法的迁移老项目里经常能看到这种写法find_package(CUDA REQUIRED) cuda_add_executable(demo main.cpp kernel.cu)这种风格在 CMake 3.x 里已经标记为 deprecated新项目没必要用。迁移时只需要把find_package(CUDA)删掉改成在project()里声明LANGUAGES CUDA再把cuda_add_executable改成普通add_executable就行。-lcublas这种库也改成标准写法target_link_libraries(demo PRIVATE cublas)CMake 原生的好处是依赖解析、生成器表达式、多架构配置都跟普通 C 工程统一了团队里其他成员接手成本低很多。5.4 一个能处理多 .cu 文件的工程结构实际项目里建议把 kernel 实现集中在kernels/目录下接口用头文件暴露。我常用的目录结构是project/ ├── CMakeLists.txt ├── main.cpp ├── include/ │ └── kernels.h └── kernels/ ├── add.cu └── dot.cu对应的 CMakeLists 可以是cmake_minimum_required(VERSION 3.18) project(multi_gpu_demo LANGUAGES CXX CUDA) set(CMAKE_CUDA_ARCHITECTURES 86 89) set(CMAKE_CUDA_STANDARD 17) add_library(mykernels STATIC kernels/add.cu kernels/dot.cu ) target_include_directories(mykernels PUBLIC include) add_executable(demo main.cpp) target_link_libraries(demo PRIVATE mykernels cudart).cu文件被打包成静态库mykernelsmain.cpp只需要 include 头文件链接时自动带上。这样核心逻辑和入口分离后续单元测试、Python 绑定接入都会轻松很多。6. 换环境就翻车Windows、WSL2 和多版本共存的实战经验6.1 Windows Visual Studio 集成失败的几类原因很多人装完 CUDA Toolkit 后在 VS 里新建 CUDA 工程发现根本没有对应的工程模板或者报CUDA visual studio integration no supported version of visual studio was found。这个报错通常是两个原因之一。一是 Toolkit 与 VS 版本相差太远。CUDA Toolkit 官方支持列表一般覆盖当前 VS 版本和前两三个大版本你用 VS2010 去配 CUDA 12.x它当然找不到受支持的版本。遇到这种情况要么升级 VS要么装一个老一点的 CUDA Toolkit比如 10.x来配合老 VS。二是装 Toolkit 时没有勾选 Visual Studio Integration 组件。重装一次 Toolkit把该勾的勾上就能自动注册。热词里那条 “vs2010编译报error MSB6006 cmd.exe已退出代码为3”本质上是同一个问题老版本 VS 拿到新版本 CUDA 的编译规则后内部调用链出了问题。对这种跨了十几年版本的组合我的建议是不要在兼容性上硬耗时间给老项目单独配一个老 CUDA 环境或者把项目迁移到新 VS。CUDA 开发最大的敌人之一就是工具链版本错配。6.2 WSL2 里用 nvcc 的正确姿势WSL2 里跑 CUDA方向对了很顺畅方向错了能折腾一整天。核心原则显卡驱动和 CUDA Toolkit 是两回事WSL2 里不用装驱动。只要 Windows 侧装好了 NVIDIA 驱动WSL2 里直接就能用 GPU但 nvcc 编译器需要自己在 WSL2 里安装 CUDA Toolkit。验证命令是nvidia-smi nvcc --version第一条能显示 GPU 信息说明驱动通了第二条能显示版本号说明编译器就绪。如果第二条提示command not found回到第 2.2 节去处理 PATH。另外 WSL2 里千万别尝试去装什么nvidia-driver那完全是多余的装错了还会跟 Windows 侧驱动冲突。有人问为什么在 WSL2 里跑nvcc --version能显示 CUDA 12.x 但nvidia-smi显示的 CUDA 版本好像不一样这不奇怪一个是 Toolkit 版本一个是驱动支持的版本两者本来就不是同一个概念。6.3 换电脑编译时最该检查的三样东西换一台机器重新编译 CUDA 项目失败的大部分原因都集中在这三处第一架构参数。新机器是 40 系显卡但 CMakeLists 里还写着CMAKE_CUDA_ARCHITECTURES 75编出来的程序在新显卡上跑不了。反过来给老机器编译时写了新架构代码也会报错。发布程序前想清楚目标机器是什么显卡。第二Toolkit 版本。有些新架构代号在老版本 nvcc 里根本不认识。36 系、40 系这种新卡拖一个 CUDA 11.0 来编基本都会死在参数解析阶段。解决方法是升级 Toolkit或者使用-archall-major这种自动包含主流架构的写法前提是 nvcc 版本够新。第三第三方库路径。-lcublas这种库在不同版本的 Toolkit 里都在但库版本可能不兼容。加载运行时报找不到.so文件或 DLL通常去查LD_LIBRARY_PATH或 PATH 里的 CUDA 库路径就行。6.4 热词里那些高级话题本质上还是在啃 nvcc搜 “opencv编译cuda” 的人最终会碰到 OpenCV 的 CUDA 模块需要在编译期调用 nvcc搜 “comfyui源码编译” 的人也会遇到 PyTorch/CUDA 扩展的编译问题。这些看着是不同技术栈的问题底层绕不过去的一环全是 nvcc。比如给 OpenCV 开启 CUDA 支持时CMake 配置界面会要求指定 CUDA 的架构列表底层逻辑跟CMAKE_CUDA_ARCHITECTURES一样。如果 nvcc 本身不可用或者架构配置错误整个 OpenCV 构建会直接失败。所以我的观点是花一两个小时彻底搞懂 nvcc 的基础用法后续不管是玩深度学习、图像处理还是 GPU 加速都能省下大量排查时间。7. 我现在的日常编译习惯供你参考最后说点个人经验。我用 nvcc 的时间不短踩过的坑也够多目前逐渐固定成了一套比较顺手的习惯。在项目里我用 CMake 作为唯一构建工具.cu文件按模块放在独立目录编译架构用86;89;90这种显式列表而不是全交给默认值。单独写点小测试代码的时候才会直接敲命令行而且一定会带上-arch参数。调试阶段我基本不用-G太影响性能多数情况用-lineinfo加打印输出就能定位 90% 的问题。只有当内核逻辑实在诡异、cuda-memcheck和nsight都查不出来时才会开-G上 cuda-gdb 断点。发布给别人用的版本我会同时嵌入 SASS 和 PTX避免对方显卡架构没被覆盖到。环境变量方面Linux 下每次换 CUDA 版本只改软链接、重开终端不在一堆环境变量里来回折腾。还有一条我觉得最值得强调遇到 nvcc 相关报错第一反应不要是去百度复制命令。先搞清楚你手上的 CUDA 版本、显卡架构、驱动支持版本这三个数字任何报错的排查都会快很多。nvcc 的官方文档确实写得复杂但nvcc --help输出的信息量足够覆盖 95% 的日常场景没事的时候翻一翻比到处搜答案靠谱得多。
返回列表