首页 / 资讯中心 / 文章详情

Windows下Qt Creator接入CUDA:qmake/CMake构建避坑指南

Windows下Qt Creator接入CUDA:qmake/CMake构建避坑指南 ★ FEATURED ARTICLE
简介面向 Windows 平台 Qt 开发者的 CUDA 集成示例工程解决在 Qt Creator 中引入 GPU 并行计算时工程结构不清、编译配置无从下手的问题适合具备一定 C 与 Qt 基础、希望快速跑通 CUDA 项目的初中级开发者。压缩包共 7 个文件、约 5KB包含 2 个 cpp 源文件、1 个 cu 核函数文件、1 个 h 头文件、1 个 ui 界面文件、1 个 pro 工程文件及 user 配置体量轻巧却覆盖了从界面到 GPU 代码的完整最小闭环。已有 971 人学习下载。读者可据此查看 pro 工程中如何登记 cu 文件与链接 CUDA 库对照主窗口与入口文件的调用关系理解主机端与设备端代码的组织方式并以示例核函数为模板替换成自己的计算逻辑同时可参考 user 配置了解构建与运行参数的设置思路减少环境搭建与编译报错时的试错成本为后续扩展图像处理、矩阵运算等并行任务提供可复用起点。1. Qt Creator 里加 CUDA 卡在哪Windows 下的构建图问题在 Qt Creator 里按下构建控制台冒出nvcc fatal : Cannot find compiler cl.exe in PATH或者更难受的unresolved external symbol __cudaRegisterFatBinary——这是我在 Windows 上做 Qt CUDA 项目最常见的两个开场。qtcreator 中添加 cuda 这件事真正卡人的地方不在 kernel 写得对不对而在于把一个由 nvcc 驱动的编译单元塞进 qmake 或 CMake 的构建图里同时让 MSVC 工具集、Qt Kit、CUDA Toolkit 三方的版本、位数、运行库全部对上。对不上症状就是满屏红波浪线、链接期炸库、调试器一进 kernel 就断。这篇笔记按我自己的落地顺序走先把工具链对齐再分别给 qmake 和 CMake 两套工程模板然后集中收拾链接与架构类的坑最后用一段冒烟代码确认整条链路真的通了。适合手上已有 Qt 上位机、想用 GPU 做图像预处理或矩阵运算的从业者也适合第一次在 Windows 上把 .cu 文件拖进 Qt 工程的人。2. 先对齐工具链MSVC、nvcc 与 Qt Kit 的三方约束2.1 三个不能碰的硬约束在 Windows 上CUDA 的主机编译器只有 MSVC 一条路走得顺clang-cl 那条线另说工程里少见。这意味着 Qt 的 Kit、nvcc 调用的 cl.exe、CUDA Toolkit 自身支持的最高 MSVC 版本必须是同一套。约束有三条第一位数必须一致。Qt 6 在 Windows 上只发 64 位预编译包CUDA 运行时库也走lib/x64任何一环选了 32 位链接期必然LNK1112: module machine type x86 conflicts with target machine type x64。这条最好查反而最少翻车。第二MSVC 工具集版本要在 CUDA 的接受范围内。常见情况是 CUDA 12 系列认 VS 2019 的 v142 和 VS 2022 的 v143CUDA 11.x 对 v143 属于「能编但会警告」需要给 nvcc 加-allow-unsupported-compiler才能过去。我一般会先看nvcc -V输出里的 host compiler 一行再看 VS 安装目录下VC\Tools\MSVC\里那个版本号两者对上再动手。第三Qt Kit 里的编译器绝对路径要指向同一个 MSVC 实例。机器上装了两个 VS 版本时Qt Creator 自动检测出来的 Kit 可能挂在旧的 v142 上而 nvcc 实际调的是新的 v143两边 STL 头文件混用报错位置会飘到xmemory里去非常难查。提示装完 CUDA Toolkit 后不要急着在 Qt Creator 里加 Kit先在普通命令行里把下面三条命令跑通跑不通就说明系统层面还没对齐进 IDE 只会更乱。2.2 命令行三连验证打开「x64 Native Tools Command Prompt」不是普通 cmd依次执行:: 1. 确认 nvcc 版本与它默认使用的主机编译器 nvcc -V :: 2. 确认当前环境里的 cl.exe 就是 Qt Kit 要用的那个 where cl cl :: 3. 确认 Qt 的 qmake 版本与位数 qmake -vnvcc -V的输出里会有一行Cuda compilation tools, release 12.x, V12.x.xx同时如果主机编译器有问题它会直接告诉你Cannot find compiler cl.exe in PATH。where cl返回的路径要和 Qt Creator 里 Kit 显示的编译器路径前缀一致比如都在...\VC\Tools\MSVC\14.3x.xxxxx\bin\Hostx64\x64\下面。qmake -v的Using Qt version一行末尾会标_64没标的赶紧换 Kit。这三条命令的作用是把「IDE 里看不到的环境变量」摊到明面上。Qt Creator 从桌面快捷方式启动时继承的是用户级环境变量不是 VS 开发者命令行那套。所以哪怕你在命令行里nvcc跑得好好的进 Qt Creator 照样找不到 cl.exe——这不是玄学是环境变量作用域问题后面第 3 章会用-ccbin彻底绕开它。2.3 在 Qt Creator 里锁定 Kit打开「工具 → 选项 → Kits」先看「编译器」页里 C 那一栏有几个 MSVC 条目记住你打算用的那个的完整路径。再回到「构建套件」页新建或复制一个 Kit把以下几项固定下来配置项取值说明编译器 C上一步记下的 cl.exe必须和 nvcc 的 host compiler 同源Qt 版本Qt 6.x MSVC2022 64bit位数与 CUDA 运行库一致调试器cdb.exeWindows SDK 自带gdb 调不了 MSVC 产物CMake 生成器Ninja比 NMake 快且对 CUDA 语言支持更稳环境手动加入 CUDA 的 bin 目录见下方说明「环境」那一栏点「更改」把C:\Program Files\CUDA\Toolkit\v12.x\bin追加到PATH前面。这样做的目的是让 Qt Creator 启动的子进程能找到nvcc.exe但注意——它仍然不能保证 nvcc 找到 cl.exe因为 cl.exe 不在 PATH 里只在 VS 的开发者环境里。这个缺口必须靠-ccbin补下面第 3 章会给出写法。3. qmake 工程接入用 QMAKE_EXTRA_COMPILERS 挂 nvcc3.1 为什么必须用 EXTRA_COMPILERSqmake 不认识.cu后缀。你把 .cu 文件写进SOURCESqmake 会把它当未知文件默默忽略最后链接期报一堆unresolved external symbol——典型的黑匣子现象编译日志干干净净链接一塌糊涂。所以正确姿势是用QMAKE_EXTRA_COMPILERS声明一条自定义编译规则把.cu → .obj这一步交给 nvcc。这条规则的几个关键字段cuda.input匹配哪些源文件通常写CUDA_SOURCES变量。cuda.output产出 obj 的路径必须落在OBJECTS_DIR或OUT_PWD下否则清理构建时会残留。cuda.commands真正执行的命令行nvcc 的路径建议写绝对路径。cuda.variable_out把产物挂到OBJECTS变量上链接器才看得见。cuda.name在编译输出面板里显示的名字排错时有用。3.2 一份可直接抄的 .pro 片段# ---- CUDA 环境定义路径按实际安装位置改 ---- CUDA_DIR C:/Program Files/CUDA/Toolkit/v12.4 CUDA_ARCH sm_75 # 按显卡算力改20系75/8630系8640系89 CUDA_BIN $$CUDA_DIR/bin # nvcc 找不到 cl.exe 的根治办法把 Qt Kit 的 C 编译器目录喂给 -ccbin MSVC_BIN $$dirname(QMAKE_CXX) # QMAKE_CXX 是 cl.exe 全路径 INCLUDEPATH $$CUDA_DIR/include DEPENDPATH $$CUDA_DIR/include # 运行库动态链接 cudartDebug 版 Qt 下用 cudartd 并不存在统一用 cudart LIBS -L$$CUDA_DIR/lib/x64 -lcudart # ---- 声明 .cu 文件 ---- CUDA_SOURCES kernels/gpu_smooth.cu \ kernels/gpu_blur.cu # ---- 自定义编译规则 ---- cuda.input CUDA_SOURCES cuda.name CUDA_COMPILE cuda.output $$OUT_PWD/$$OBJECTS_DIR/${QMAKE_FILE_BASE}_cuda.obj cuda.variable_out OBJECTS cuda.dependency_type TYPE_C # 注意-Xcompiler 后的 MSVC 选项要加引号否则 qmake 会吃掉斜杠 cuda.commands $$CUDA_BIN/nvcc -c \ -arch$$CUDA_ARCH \ -ccbin \$$MSVC_BIN\ \ -Xcompiler \/MD /EHsc /Zi\ \ -I$$CUDA_DIR/include \ -I$$PWD \ --use_fast_math \ ${QMAKE_FILE_IN} -o ${QMAKE_FILE_OUT} QMAKE_EXTRA_COMPILERS cuda逐段解释几个容易被忽略的点。$$dirname(QMAKE_CXX)这一步是整个片段的核心。它从 qmake 已知的 C 编译器路径里反推出 MSVC 的 bin 目录再通过-ccbin传给 nvcc。这样无论 Qt Creator 是正常启动还是从任务栏启动nvcc 都能找到 cl.exe不用去改系统 PATH也不会污染全局环境。前提是你的 Kit 里编译器配置是对的如果 Kit 挂的是 mingw-ccbin会指向一个跟 MSVC 无关的目录nvcc 立刻报错——这也算一种快速自检。-Xcompiler /MD /EHsc /Zi里的/MD必须和 Qt 的构建配置一致。Qt 官方 MSVC 包是动态运行库Release 下是/MDDebug 下是/MDd。nvcc 默认按/MD编所以 Debug 构建时 .cu 产出的 obj 用/MD、Qt 侧用/MDd混进同一个 exe 里会出现 CRT 堆不匹配症状是运行时在free或operator delete处随机崩堆栈指向ntdll。稳妥写法是按构建类型分支CONFIG(debug, debug|release) { CUDA_RUNTIME /MDd } else { CUDA_RUNTIME /MD } cuda.commands ... -Xcompiler \$$CUDA_RUNTIME /EHsc /Zi\ ...cuda.output里的${QMAKE_FILE_BASE}是 qmake 内置变量取的是不含扩展名的文件名。如果你的工程里有两个不同目录下的同名 .cu比如ui/render.cu和core/render.cu这个命名会撞车后编译的覆盖前面的链接时用的可能是错的 obj。血泪经验要么保证文件名全局唯一要么在 output 里拼上$$dirname(QMAKE_FILE_IN)的处理结果。3.3 头文件与接口边界.cu 文件里不要 include Qt 的任何头文件。nvcc 前端对某些 C 模板特性和 Qt 的宏展开容忍度不高一旦 Q_OBJECT 宏进到 .cu 里报错信息会指向 moc 生成文件完全看不出源头。我一般把边界划成两层// kernels/gpu_smooth.h —— 纯 C 接口Qt 侧只认这个头 #pragma once #ifdef __cplusplus extern C { #endif // 输入输出都用裸指针 尺寸不出现 QString / QImage int gpu_smooth_rgba(unsigned char* d_src, unsigned char* d_dst, int width, int height, int radius); #ifdef __cplusplus } #endifQt 侧拿到 QImage 后用bits()取裸指针传给封装函数由.cu内部完成cudaMalloc/cudaMemcpy或直接操作设备指针。这样切的好处是 .cu 完全独立于 Qt可以单独用 nvcc 命令行编译测试出问题能二分定位是 CUDA 侧还是 Qt 侧。.cu文件也要记得加进DISTFILES否则 Qt Creator 的项目树里看不到它改完不会触发重新构建你会以为改了没生效。3.4 增量编译的坑qmake 的自定义编译器默认不带依赖生成改 .cu 里 include 的头文件不会触发重编。常见做法是给规则加依赖命令但 nvcc 的-M在 Windows 上输出格式和 qmake 期望的不完全兼容容易引入新的解析错误。我的处理是放弃自动依赖改成在 .pro 里显式列头文件cuda.depends $$CUDA_DIR/include/cuda_runtime.h \ $$PWD/kernels/gpu_common.h数量不多时这样最省心。真要上规模还是转 CMake那边 CUDA 作为一等语言依赖追踪是现成的。4. CMake 工程接入enable_language(CUDA) 与 Qt 的协同4.1 CMake 侧的三条选型理由新工程我一律推荐 CMake理由不是时髦而是三条具体收益。一是 CUDA 在 CMake 3.8 之后是官方支持的语言.cu自动识别不用手写编译规则。二是依赖追踪自动生成改头文件会正确触发重编。三是架构列表通过CMAKE_CUDA_ARCHITECTURES统一管理多卡分发和多架构 fatbin 只要改一行。代价是 CMake 版本必须跟上。CMAKE_CUDA_ARCHITECTURES从 3.18 引入并且受策略 CMP0104 控制3.18 之后如果这个变量为空配置阶段直接报错。所以cmake_minimum_required(VERSION 3.20)是比较安全的起点。4.2 一份可用的 CMakeLists.txtcmake_minimum_required(VERSION 3.20) # 关键CUDA 必须在 project() 里声明为语言后面 .cu 才会被自动识别 project(GpuQtDemo LANGUAGES CXX CUDA) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 目标显卡架构不写会走 CMake 的策略默认值可能编不出你要的 fatbin set(CMAKE_CUDA_ARCHITECTURES 75 86) set(CMAKE_CUDA_STANDARD 17) # Qt 部分 find_package(Qt6 REQUIRED COMPONENTS Widgets) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) # CUDA 运行时库用 imported target 比手写 -lcudart 稳 find_package(CUDAToolkit REQUIRED) add_executable(GpuQtDemo src/main.cpp src/mainwindow.cpp kernels/gpu_smooth.cu # 直接列进来即可不需要 set_source_files_properties ) target_include_directories(GpuQtDemo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/kernels ${CUDAToolkit_INCLUDE_DIRS} ) target_link_libraries(GpuQtDemo PRIVATE Qt6::Widgets CUDA::cudart ) # Debug 构建时给 nvcc 加 -G否则断点进不了 kernel target_compile_options(GpuQtDemo PRIVATE $$COMPILE_LANGUAGE:CUDA:-Xptxas-v $$AND:$COMPILE_LANGUAGE:CUDA,$CONFIG:Debug:-G )几个参数值得单独说。LANGUAGES CXX CUDA里 CUDA 的顺序无所谓但一定要在project()里出现。漏了这一步后面add_executable里放 .cu 会被 CMake 当成未知扩展名忽略跟 qmake 那个坑一模一样。CMAKE_CUDA_ARCHITECTURES 75 86生成的是多架构 fatbin同一个 exe 可以跑在 20 系和 30 系上。代价是编译时间和产物体积上升。只在自己机器上跑写一个就行。写成native是 3.24 之后的能力自动探测本机算力单机开发时很方便但产物换机器就可能跑不了。CUDA::cudart是 CUDAToolkit 包提供的 imported target它会自动带上正确的 include 路径、库目录和 64/32 位区分。手写-lcudart时如果漏了-L或者路径写了lib/x86链接期就炸。用 target 能避开这一类。-Xptxas-v会在编译时打印每个 kernel 的寄存器用量和 shared memory 占用调优时非常有用。-G只在 Debug 下开注意它和--use_fast_math互斥同时开会导致 nvcc 直接报错。4.3 在 Qt Creator 里打开 CMake 工程「文件 → 打开文件或项目」选 CMakeLists.txtQt Creator 会让你选 Kit。这一步有两个细节一是生成器选 Ninja。NMake 在 CUDA 多配置场景下容易出问题尤其是 Debug/Release 切换时 obj 目录混淆症状是改了代码不生效。Qt Creator 默认一般就是 Ninja但如果你是从旧工程复制过来的检查一下构建设置里有没有残留的 NMake 配置。二是首次配置完成后去「项目 → 构建 → CMake」里看一眼CMAKE_CUDA_COMPILER是否被正确探测到。如果显示CMAKE_CUDA_COMPILER-NOTFOUND说明 CMake 找不到 nvcc手动加一条缓存变量指到nvcc.exe全路径即可。这里同样可能遇到 host compiler 不匹配CMake 会给出明确提示按提示设CMAKE_CUDA_HOST_COMPILER指向 cl.exe。混合工程里还有一处容易翻车Qt 的AUTOMOC扫描头文件时会把kernels/gpu_smooth.h也扫一遍。如果那个头里出现了Q_OBJECT之外的 Qt 宏比如误 include 了QStringmoc 生成的代码会被塞进 CUDA 的编译单元nvcc 立刻报一堆语法错。解法是把纯 C 接口的头文件排除出 moc 扫描范围或者干脆不给它加任何 Qt 依赖。5. 避坑与排查五个真实翻车现场5.1 编译期nvcc 找不到主机编译器现象Qt Creator 构建输出第一行就是nvcc fatal : Cannot find compiler cl.exe in PATH但你在自己的命令行里跑nvcc -V完全正常。原因Qt Creator 进程的环境变量里没有 VS 开发者命令行注入的PATHINCLUDE、LIB也是空的。nvcc 靠这些变量定位 cl.exe 和标准库。解决按第 3 章的写法用-ccbin显式指定 MSVC 的 bin 目录CMake 工程则设CMAKE_CUDA_HOST_COMPILER。不要图省事去改系统 PATH 加 VS 目录那会污染所有进程后面装别的工具链时冲突。5.2 链接期unresolved external symbol __cudaRegisterFatBinary现象所有 .cu 都编译通过链接阶段报找不到__cudaRegisterFatBinary、cudaMalloc之类的符号。原因有两个常见来源。一是.cu根本没被编译——qmake 下漏了QMAKE_EXTRA_COMPILERS或者 CMake 下project()里没写 CUDA导致 .cu 被当成资源文件跳过但头文件里的声明还在所以编译期不报错。二是链接时少了cudart.lib或者位数选错。解决先看构建目录里有没有生成*_cuda.obj没有就是没编译回头检查规则有 obj 还报错就是链接参数问题检查LIBS里的-L路径是否指向lib/x64以及-lcudart是否被加进了最终链接命令行。CMake 下直接用CUDA::cudart别手写。5.3 编译期Unsupported gpu architecture现象nvcc fatal : Unsupported gpu architecture compute_89或者反过来 kernel 跑起来报no kernel image is available for execution on the device。原因前者是 CUDA Toolkit 版本太老不认识新显卡的算力编号后者是编译时指定的架构和实际运行的显卡不匹配。这两个是一体两面本质都是-arch参数和硬件对不上。解决先用设备的算力编号查清楚再改CUDA_ARCH。运行时可以用一段代码打印实际设备的算力跟编译参数对照// 放在 main 最前面一行输出定位架构问题 #include cuda_runtime.h #include cstdio static void printDeviceInfo() { int dev 0; cudaGetDevice(dev); // 当前使用的设备号 cudaDeviceProp prop{}; cudaGetDeviceProperties(prop, dev); // 拿设备属性 printf(device: %s, sm_%d%d, driver API %d.%d\n, prop.name, prop.major, prop.minor, prop.major, prop.minor); }prop.major和prop.minor拼起来就是算力编号比如 8 和 6 对应sm_86。把CUDA_ARCH改成这个值重新编译问题基本消失。要兼顾多台机器就写多个架构代价是编译变慢。5.4 运行期Debug 构建下堆崩溃现象Release 正常Debug 一跑就崩在free或operator delete调用栈里有ntdll和ucrtbase。原因CRT 运行库不一致。Qt Debug 用的是/MDdnvcc 默认给的是/MD两个不同的 CRT 实例在同一进程里各自维护堆跨模块释放内存必然出事。这个坑很隐蔽因为编译和链接都不报错。解决按第 3.2 节的写法按构建类型分支-Xcompiler的运行库参数。CMake 侧更省事set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$$CONFIG:Debug:DebugDLL)一行统一 C 和 CUDA 两侧。5.5 调试期断点进不了 kernel现象host 代码断点正常一进 kernel 就卡住或者直接跑完cuda-gdb在这台机器上不可用Windows 上没有。原因Qt Creator 配的调试器是 cdb它只能调 host 代码看不到设备执行流。而且 Debug 构建时如果没给 nvcc 加-Gkernel 是优化过的行号和变量都对不上。解决Windows 上做 kernel 级调试走图形化工具那条线更实际——在代码里加cudaDeviceSynchronize()配合cudaGetLastError()把错误定位到具体的 launch 语句再配合-lineinfo加工具做性能分析。日常排查我依赖的是「每次 kernel 调用后立刻检查错误」这个习惯// 每个 kernel launch 后立刻检查把异步错误变成同步报错 myKernelgrid, block(d_ptr, n); cudaError_t err cudaGetLastError(); if (err ! cudaSuccess) { qFatal(kernel launch failed: %s, cudaGetErrorString(err)); } cudaError_t sync cudaDeviceSynchronize(); // 只在调试期加发布版去掉 if (sync ! cudaSuccess) { qFatal(kernel exec failed: %s, cudaGetErrorString(sync)); }不加同步时kernel 里的越界访问会延后到下一次 API 调用才报错栈已经指到别处去了这是最典型的黑匣子场景。6. 验证与进阶一段冒烟测试把链路跑通配置完别急着写业务代码。我现在的固定动作是先跑一段冒烟测试把「能编译、能链接、能分配设备内存、能启动 kernel、能回读结果」这五件事一次性验掉。这段代码单独建一个控制台工程不掺 Qt最小化变量// smoke.cu —— 独立编译nvcc -archsm_75 smoke.cu -o smoke.exe #include cstdio #include cuda_runtime.h __global__ void addOne(int* d, int n) { int i blockIdx.x * blockDim.x threadIdx.x; if (i n) d[i] 1; // 边界判断别省越界是异步报错 } int main() { const int n 8; int h[n]; for (int i 0; i n; i) h[i] i; int* d nullptr; if (cudaMalloc(d, n * sizeof(int)) ! cudaSuccess) { printf(malloc failed: %s\n, cudaGetErrorString(cudaGetLastError())); return 1; } cudaMemcpy(d, h, n * sizeof(int), cudaMemcpyHostToDevice); addOne(n 31) / 32, 32(d, n); // 向上取整算 block 数 if (cudaGetLastError() ! cudaSuccess) { // launch 参数错误在这里暴露 printf(launch failed: %s\n, cudaGetErrorString(cudaGetLastError())); return 2; } if (cudaDeviceSynchronize() ! cudaSuccess) { printf(exec failed: %s\n, cudaGetErrorString(cudaGetLastError())); return 3; } cudaMemcpy(h, d, n * sizeof(int), cudaMemcpyDeviceToHost); for (int i 0; i n; i) printf(%d , h[i]); printf(\n); cudaFree(d); return 0; }这段能跑通说明驱动、Toolkit、架构参数三层都对。跑不通就按返回码定位1 是设备不可用或显存不足2 是 launch 配置错block 超过 1024、shared memory 超限3 是 kernel 内部越界或非法访存。冒烟过了之后再把同样的逻辑搬进 Qt 工程。搬的时候只替换入口Qt 侧拿到 QImage 的bits()指针调.cu导出的 C 接口内核逻辑一个字不改。这样一旦出问题你立刻知道是新引入的构建配置还是原本就有的逻辑。进阶上有两个我会常备的技巧。一个是-Xptxas -v看寄存器用量如果某个 kernel 的 register 数超过 64occupancy 会明显掉改用__launch_bounds__限制一下往往有惊喜。另一个是多架构分发用CMAKE_CUDA_ARCHITECTURES 75;86;89一次编出三份 fatbin发给不同显卡的同事时不用重编代价是编译时间大概翻三倍——本地开发时我会临时改回单架构出包时再放开。从那以后我每次新建 Qt CUDA 工程不管是 qmake 还是 CMake都先把冒烟测试跑一遍再动业务代码。这个习惯帮我省掉的返工时间比任何调试技巧都多——因为构建配置类的问题一旦混进业务代码里你就再也分不清是逻辑错还是环境错了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站