简介:一份面向Windows平台开发者的Qt Creator集成CUDA参考资源,专门解决在Qt Creator中搭建CUDA编译与运行环境、配置nvcc自定义构建步骤等问题。资源以小型Qt项目源码形式呈现,共7个文件,压缩包仅5KB,涵盖mainwindow.cpp、mainwindow.h、your_cuda_file.cu、界面ui文件及pro工程配置等。其中.pro文件展示了CUDA库链接与自定义构建命令的关键写法,.cu文件则提供了可直接参考的核函数示例,适合希望快速理解Qt与CUDA混合项目结构的入门及进阶用户。目前已有971人学习下载。借助此项目,读者可以获得一个可复用的Qt+CUDA项目骨架,直观看到普通C++文件与.cu核函数如何在同一个工程中关联编译,并据此迁移到自己的GPU并行计算任务中。使用时只需确保本机装有NVIDIA显卡及匹配的CUDA工具包,即可按示例构建运行。 如果你和我一样,日常工作基本都在 Qt Creator 里完成,突然某天想把手边的 NVIDIA 显卡真正利用起来——做个图像处理、数据并行加速,甚至跑一点自己的深度学习推理——那么你首先就会卡在一个很现实的问题上:Windows 上的 Qt Creator 默认压根不会编译.cu文件。
这篇东西就是我踩完坑之后的完整记录。它能让你在半小时内,把 CUDA 编译流程完整塞进 Qt Creator 的构建系统:.pro文件怎么写、nvcc怎么调用、.cu文件如何在 C++ 里声明、运行时有哪些坑。适合两类人:一是 Qt 程序员想把计算密集部分搬到 GPU,二是 CUDA 新手希望有一个带界面的试验环境。
1. 准备工作与工具链选型
1.1 工具链选型:为什么必须是 MSVC + CUDA
Windows 下 Qt 的编译器有两个主流方向:MSVC(Visual Studio 工具链)和 MinGW(GCC/LLVM 系)。很多人平时偷懒装的是 MinGW 版 Qt,结果一到 CUDA 就翻车。
原因很简单:NVIDIA 的nvcc编译器在 Windows 上只对 MSVC 做了官方支持,它内部调用的是cl.exe来做 C++ 预处理和编译链接。MinGW 那边生成的目标文件格式、库依赖规则和 MSVC 完全不同,nvcc根本不理你。打个比方,nvcc 只会说“英文”,MinGW 一直在讲“中文”,你怎么喊它它都不应。
所以第一步就是回到 MSVC 工具链。在 Qt 安装包里,建议勾选Qt 6.x MSVC 2019/2022 64-bit版本,而不是MinGW 64-bit。同时要装对应版本的 Visual Studio,至少包含“使用 C++ 的桌面开发”工作负载。别问能不能用 VS Code 替代,实际开发里 Qt Creator 的调试补全和项目管理更顺手,而 VS 只需要假装它是一个“编译器提供者”就行。
1.2 CUDA Toolkit 安装与路径要点
CUDA 分两部分:显卡驱动和 CUDA Toolkit。驱动负责底层执行,Toolkit 里的nvcc负责把.cu编译成 GPU 可执行代码。nvidia-smi显示的是驱动支持的 CUDA 最高版本,而你真正编译用的nvcc --version才是 Toolkit 版本。
安装 Toolkit 时最容易被忽略的一个点:安装目录默认是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.6,目录名里带空格。后面写.pro的时候不加引号,路径就会崩。另外,安装完务必确认环境变量CUDA_PATH被设置到了当前用的版本。
如果你机器上有多个 CUDA 版本,也别慌,这就是下面的问题。
1.3 多版本 CUDA 共存的落地方式
很多人机器上不止一个 CUDA 版本,比如 PyTorch 自带的 runtime 是 12.1,自己开发想用 12.6。其实 CUDA Toolkit 多版本共存完全可行,关键是两件事:
第一,环境变量CUDA_PATH永远指向默认版本,哪个项目想用特定版本就在项目配置里写全路径覆盖它。第二,涉及 Python 的 PyTorch/TensorFlow 时,它们各自的编译产物会自动匹配 CUDA runtime,和系统路径关系不大,真正影响开发的是你手动调用的nvcc。
我在.pro里一般不会直接用$$(CUDA_PATH),因为一旦默认版本切了,项目就跟着变。比较稳妥的做法是写死当前项目的版本路径,比如:
CUDA_DIR = "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6"这样项目构建环境是可复现的,哪天想换成 v12.1,只需改这一个变量,下面所有编译参数都会跟着走。
2. Qt Creator 项目配置:让 qmake 认识 .cu 文件
2.1 配置方案对比
在 Qt Creator 里塞进 CUDA,主流做法有三种:
| 方案 | 说明 | 适用场景 |
|---|---|---|
修改.pro+ QMAKE_EXTRA_COMPILERS | qmake 原生扩展,编译目标自动进入链接 | 最推荐,配置一次后一劳永逸 |
| 自定义构建步骤 | 在 Qt Creator 项目设置里加额外命令 | 快速验证,但目标文件链接有麻烦 |
| 改用 CMake 构建 | CMake 原生支持 CUDA | 新项目/跨平台项目首选 |
我自己日常用 qmake 比较多,下面先详细拆第一种,这是现阶段最好落地的方式。
2.2 QMAKE_EXTRA_COMPILERS 配置详解
核心思路是告诉 qmake:除了 C++ 源文件,你还要额外用另一条命令编译.cu文件,并且把生成的.obj当作整个项目的一部分交给 MSVC 链接器。
先看一份完整的.pro配置:
# CUDA setup CUDA_DIR = "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6" CUDA_SOURCES += $$PWD/vec_add.cu CUDA_INC = -I$${CUDA_DIR}/include CUDA_FLAGS = -gencode arch=compute_89,code=sm_89 --machine 64 CUDA_COMPILER.command = $${CUDA_DIR}/bin/nvcc.exe -c -o ${QMAKE_FILE_OUT} ${QMAKE_FILE_IN} $$CUDA_INC $$CUDA_FLAGS CUDA_COMPILER.dependency_type = TYPE_C CUDA_COMPILER.input = CUDA_SOURCES CUDA_COMPILER.output = $${OBJECTS_DIR}/${QMAKE_FILE_IN_BASE}$${QMAKE_EXT_OBJ} QMAKE_EXTRA_COMPILERS += CUDA_COMPILER # link CUDA_LIBS = -L$${CUDA_DIR}/lib/x64 -lcudart LIBS += $$CUDA_LIBS INCLUDEPATH += $${CUDA_DIR}/include拆开讲几个关键变量:
CUDA_SOURCES:列出项目里所有.cu文件,qmake 不会自动扫描,必须手写。CUDA_FLAGS:-gencode arch=compute_89,code=sm_89是给 RTX 40 系(Ada 架构)用的。如果你用的显卡是 30 系(Ampere),改成compute_86,code=sm_86;20 系(Turing)改成compute_75,code=sm_75。这个参数不写对,后面大概率会遇到no kernel image is available的崩溃。CUDA_COMPILER.command:qmake 会把${QMAKE_FILE_IN}替换成具体输入文件,把${QMAKE_FILE_OUT}替换成输出目标文件。-c表示只编译不链接,生成的.obj由 qmake 自动加进链接流程。CUDA_COMPILER.output:定义了目标文件输出的位置,放在OBJECTS_DIR下,和普通.obj混在一起,之后 MSVC 链接器自然会找到它。
这里有一个很常见的坑:CUDA_DIR变量我用引号包住了。Windows 路径里的Program Files带空格,nvcc 命令行不带引号直接裂开。但如果路径最后带反斜杠,引号又会出现问题,所以我刻意用了正斜杠。
2.3 用自定义构建步骤快速验证
如果你不想马上重构成 QMAKE_EXTRA_COMPILERS,只想快速看看.cu文件能不能编译好,可以在 Qt Creator 项目设置的“构建步骤”里加一个“自定义处理步骤”。
命令填nvcc.exe的完整路径,参数类似:
-c "${sourceDir}/vec_add.cu" -o "${buildDir}/vec_add_cuda.obj" -I"${CUDA_PATH}/include" -gencode arch=compute_89,code=sm_89 --machine 64注意一个问题:自定义构建步骤生成的.obj不会自动进入链接阶段,你还得手动把这个 obj 加入 “链接器额外输入”或者转成静态库。所以这种方式只适合验证“我的 CUDA 代码本身有没有语法/编译错误”,真正工程化,还是老老实实用 QMAKE_EXTRA_COMPILERS。
2.4 CMake 方案的替代写法
如果你已经在用 CMake,那现在更省事。CMake 从 3.18 开始原生支持 CUDA 语言扩展,不需要搞一堆命令拼字符串。
cmake_minimum_required(VERSION 3.20) project(MyQtCudaDemo LANGUAGES CXX CUDA) set(CMAKE_CUDA_ARCHITECTURES 89) find_package(Qt6 REQUIRED COMPONENTS Widgets) find_package(CUDAToolkit REQUIRED) add_executable(app main.cpp vec_add.cu) target_link_libraries(app PRIVATE Qt6::Widgets CUDA::cudart)重点在于project(... LANGUAGES CXX CUDA)和set(CMAKE_CUDA_ARCHITECTURES 89)。前者告诉 CMake 把nvcc当作编译器,后者对应前面compute_89的参数。剩下 Qt 的链接逻辑和普通项目一样,CMake 会处理.cu文件的编译依赖。
从维护角度讲,CMake 这条路径明显更清爽,尤其是在项目要跨 Windows/Linux 的情况下。我自己现在的习惯是:老项目继续用 qmake 补丁式升级,新项目一律 CMake。
3. 编写并调用第一个 CUDA 函数
3.1 .cu 文件声明与头文件封装
CUDA 代码通常放在.cu文件里,里面同时包含 GPU 侧核函数(kernel)和 CPU 侧的调用函数。关键问题是:.cu文件中的函数有 C++ 名字修饰,而 main.cpp 是纯 C++,两者都能编译成 MSVC 目标文件,但链接时名字符号必须匹配。
最省事的办法是在.cu里写extern "C"导出 API:
// vec_add.cu #include <cuda_runtime.h> #include <cstdio> __global__ void vecAddKernel(const float* a, const float* b, float* c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { c[i] = a[i] + b[i]; } } extern "C" int runVectorAdd(const float* a, const float* b, float* c, int n) { float *d_a, *d_b, *d_c; cudaMalloc(&d_a, n * sizeof(float)); cudaMalloc(&d_b, n * sizeof(float)); cudaMalloc(&d_c, n * sizeof(float)); cudaMemcpy(d_a, a, n * sizeof(float), cudaMemcpyHostToDevice); cudaMemcpy(d_b, b, n * sizeof(float), cudaMemcpyHostToDevice); int threads = 256; int blocks = (n + threads - 1) / threads; vecAddKernel<<<blocks, threads>>>(d_a, d_b, d_c, n); cudaMemcpy(c, d_c, n * sizeof(float), cudaMemcpyDeviceToHost); cudaFree(d_a); cudaFree(d_b); cudaFree(d_c); return 0; }然后在 C++ 侧写一个头文件:
// vec_add.h #pragma once extern "C" int runVectorAdd(const float* a, const float* b, float* c, int n);extern "C"去掉 C++ 的修饰后缀,两边符号就一致了。这样.cu文件只暴露一个普通 C 接口,Qt 的源代码完全不用关心 CUDA 细节。
3.2 在 Qt Widgets 界面中调用 CUDA
在 GUI 程序里调用 CUDA 要特别注意一方:不要在 UI 线程里跑大计算。CUDA 调用自带阻塞性质,尤其在cudaDeviceSynchronize或大量cudaMemcpy时,界面会直接卡死。正确姿势是丢到线程里跑,跑完通过信号把结果传回界面。
下面这段演示了一个最简单的按钮触发流程:
// mainwindow.cpp #include "mainwindow.h" #include "ui_mainwindow.h" #include "vec_add.h" #include <QThread> #include <QDebug> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); connect(ui->calcButton, &QPushButton::clicked, this, [=](){ QThread* t = QThread::create([=]() { const int n = 1024 * 1024; float* a = new float[n]; float* b = new float[n]; float* c = new float[n]; for (int i = 0; i < n; ++i) { a[i] = 1.0f * i; b[i] = 2.0f * i; } runVectorAdd(a, b, c, n); qDebug() << "c[42] =" << c[42]; delete[] a; delete[] b; delete[] c; }); t->start(); }); }这里面QThread::create会把整段计算放到工作线程,界面按钮不会卡住。实际项目里可以把结果用QRunnable或QtConcurrent::run封装,再发信号回界面刷新。
3.3 显存管理与错误检查
CUDA 开发最隐蔽的问题就是“忘记检查返回值”。cudaMalloc失败、cudaMemcpy数据量不对、核函数里越界,这些错误不主动检查根本不会弹窗,程序只会给出一个诡异的崩溃或者黑屏。
我在实际代码里会这样做:
#define CUDA_CHECK(call) \ do { \ cudaError_t err = (call); \ if (err != cudaSuccess) { \ fprintf(stderr, "CUDA error at %s:%d - %s\n", \ __FILE__, __LINE__, cudaGetErrorString(err)); \ return -1; \ } \ } while (0)然后在每个 CUDA API 外圈套上CUDA_CHECK:
CUDA_CHECK(cudaMalloc(&d_a, n * sizeof(float))); CUDA_CHECK(cudaMemcpy(d_a, a, n * sizeof(float), cudaMemcpyHostToDevice));另外,cudaDeviceSynchronize是抓核函数异步错误的关键。很多人发现核函数明明写错了却不报错,就是因为没同步就cudaFree,异常被吞了。在核函数调用后加一次cudaDeviceSynchronize,任何启动错误都会立刻暴露。
还有一种情况是核函数计算结果全部为 0 或错误值,先别急着怀疑 CUDA,大多数是内存没有 Host 到 Device 拷贝完整,或者 grid/block 维度不够大导致部分数据没处理。用cudaMemcpy的返回值逐一排查比瞎猜快得多。
4. 常见问题与排查实录
4.1 编译期报错自查表
以下是 Windows 上 Qt Creator + CUDA 最常见的五个编译问题以及解决办法:
| 报错现象 | 原因 | 解决办法 |
|---|---|---|
nvcc fatal : Cannot find compiler 'cl.exe' | nvcc 找不到 MSVC | 安装 Visual Studio 的 C++ 桌面开发组件,或在“项目环境”里加上 VS 的 PATH |
fatal error C1083: Cannot open include file: 'cuda_runtime.h' | .pro没加 CUDA include 路径 | 在.pro里写上INCLUDEPATH += $${CUDA_DIR}/include |
error LNK2019: unresolved external symbol | .cu函数没有被链接,或符号不匹配 | 检查 QMAKE_EXTRA_COMPILERS 是否生效,检查extern "C"声明 |
cuda_runtime.h与host_config.h版本冲突 | CUDA Toolkit 和 MSVC 版本过新/过旧 | 更新 CUDA Toolkit 到 12.x,或更换受支持的 VS 版本 |
Detected 14.4x.y.z is not supported | nvcc 检查到未知的 MSVC 版本 | 升级 CUDA 到对应新版本,不推荐魔改 host_config.h |
通过 QMAKE_EXTRA_COMPILERS 生成的.obj路径如果不对,经常出现“源文件 .cu 编译了但链接不到函数”的情况。你可以在“编译输出”面板里搜nvcc,如果根本没看到 nvcc 命令,说明.pro里没有正确匹配输入源文件,检查CUDA_SOURCES += $$PWD/vec_add.cu是否写对了。
4.2 运行期报错与性能问题
运行期最经典的错误就是:
CUDA error: no kernel image is available for execution on the device这个报错翻译成人话:你编译出来的 GPU 代码和当前显卡的架构不匹配。最常见原因就是-gencode参数没给对。比如你的 GPU 是 RTX 4060 Ti(算力 8.9),却用默认参数编译成了sm_50,那到了运行时驱动发现没有对应的 image,直接罢工。
我的建议是不要用-arch=sm_89这种写死的形式,而用-gencode arch=compute_89,code=sm_89。前者只生成对应 SASS,后者同时保留 PTX,驱动在遇到新架构时还能做 JIT 兼容,灵活性更好。
另一个容易翻车的地方是 32 位和 64 位混用。Qt 是 64 位编译,却去链接了lib/x86下的 CUDA 库,链接器不会立刻告诉你,但运行时各种崩溃都来了。记住统一用lib/x64。
如果程序发布后在其他机器上提示缺少cudart64_12.dll,说明没有把 CUDA runtime 动态库带去。最简单的处理是把%CUDA_PATH%\bin\cudart64_*.dll放到可执行文件同目录,或者用静态库cudart_static.lib替代cudart.lib。个人项目用前者更省心,软件分发给别人以前注意确认目标机器 NVIDIA 驱动够新。
4.3 我的几条实战心得
第一,一开始先在文本框里输出 GPU 信息。调用cudaGetDeviceProperties把显卡名、算力、显存、驱动版本打出来,确认环境通了再写实际算法,能省掉一半的排查时间。
第二,在.pro里配置 QMAKE_EXTRA_COMPILERS 时,调试和 Release 共用一套 nvcc 参数没问题,但建议加一个变量控制是否启用-G调试符号。-G会显著降低 GPU 代码性能,忘了去掉会让性能测试完全失真。
第三,在 Qt 集成 CUDA 的项目里,至少把 GPU 计算封装成独立模块。趁项目还没长大时,让 GUI 层和 CUDA 层之间只隔一层简单 C 接口,这样哪怕以后把 GPU 代码拆出去做库,Qt 端一行不用改。
5. 结合 WSL2 与 Docker 的扩展思路
如果你已经搞定了 Qt + CUDA 的 Windows 原生方案,并且机器上还装有 WSL2,那么还有一个很有意思的玩法:把 CUDA 计算逻辑放进 WSL2 里跑。
Windows 下安装 WSL2 之后,NVIDIA 驱动会直接透传给 Linux 子系统,也就是说 WSL2 里不需要再装显卡驱动,只需要在 WSL2 里安装 CUDA Toolkit。这样你可以在 Windows 端写 Qt 界面,通过子进程或者 gRPC 方式调用 WSL2 里的 CUDA 计算服务。虽然网络通信有一点开销,但对重计算、轻交互的场景非常合适,尤其是部署深度学习模型推理时特别省心。
另一种常见做法是用 Docker 跑 CUDA 容器。Windows Docker 可以安装 NVIDIA Container Toolkit 来让容器访问 GPU,配合pytorch/pytorch这类官方镜像,开发环境和部署环境完全一致,省掉“在我电脑上明明能运行”的尴尬。这两种方式和原生 Qt Creator 集成并不冲突,只是多提供了一条隔离性更好的路径。我自己的选择是:原生环境用来写代码调试,WSL2/Docker 用来做最终验证。
本文还有配套的精品资源,点击获取