第6板块·第4节:构建系统与多文件项目

学习目标

学完本节你将能够:

  • 使用 CMake 管理 CUDA 项目,包括编译、链接和依赖
  • 组织多文件 CUDA 项目,合理划分头文件和源文件
  • 创建和使用静态库、动态库,并在项目中正确链接
  • 处理跨平台 CUDA 项目的配置差异
  • 理解编译依赖和增量构建,提高开发效率

1. CUDA 项目的文件组织

1.1 典型目录结构

一个中大型 CUDA 项目通常包含以下组成部分:

复制代码
project/
├── CMakeLists.txt
├── include/
│   ├── common.h          # 公共宏、工具函数
│   ├── kernel_utils.cuh  # 设备端辅助函数声明
│   └── ...
├── src/
│   ├── main.cu           # 主机端主程序
│   ├── kernels.cu        # 核心 Kernel 实现
│   ├── memory.cu         # 内存管理封装
│   └── ...
├── tests/
│   ├── test_kernels.cu
│   └── ...
└── libs/
    ├── mylib/
    │   ├── CMakeLists.txt
    │   ├── include/
    │   └── src/
  • 头文件(.h.cuh :声明函数、类型、宏。设备端代码通常放在 .cuh 中,以便被多个 .cu 包含。
  • 源文件(.cu:包含主机端和设备端实现。
  • :将可复用的组件编译为静态库(.a)或动态库(.so)。

1.2 头文件中的设备代码

如果设备端函数需要在多个 .cu 文件中使用,应该放在头文件中并用 __device____global__ 修饰。链接时使用 -rdc=true 支持可重定位设备代码。

复制代码
// kernel_utils.cuh
#pragma once

__device__ __forceinline__ float square(float x) {
    return x * x;
}

2. CMake 构建配置

2.1 基础 CMakeLists.txt

现代 CMake(3.18+)原生支持 CUDA,可以通过 enable_language(CUDA) 启用。

复制代码
cmake_minimum_required(VERSION 3.18)
project(MyCudaProject LANGUAGES CXX CUDA)

# 设置 CUDA 标准
set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)

# 添加可执行文件
add_executable(myapp
    src/main.cu
    src/kernels.cu
)

# 设置目标架构
set_target_properties(myapp PROPERTIES
    CUDA_ARCHITECTURES "70;80"   # 为 sm_70 和 sm_80 生成代码
)

# 链接 CUDA 运行时
target_link_libraries(myapp cudart)

2.2 指定计算能力

使用 CUDA_ARCHITECTURES 属性可以为目标生成多架构代码:

复制代码
set_target_properties(myapp PROPERTIES
    CUDA_ARCHITECTURES "native"   # 自动检测本机架构
)

或者手动指定:

复制代码
set_target_properties(myapp PROPERTIES
    CUDA_ARCHITECTURES "70;75;80;86"
)

2.3 编译选项

复制代码
# 优化级别
target_compile_options(myapp PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-O3>)

# 调试版本
target_compile_options(myapp_debug PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-g -G>)

# 快速数学
target_compile_options(myapp PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-use_fast_math>)

3. 静态库与动态库

3.1 创建静态库

复制代码
# libs/mylib/CMakeLists.txt
add_library(mylib STATIC
    src/kernel1.cu
    src/kernel2.cu
)

target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(mylib PROPERTIES
    CUDA_ARCHITECTURES "70;80"
    CUDA_SEPARABLE_COMPILATION ON   # 等价于 -rdc=true
)

3.2 创建动态库

复制代码
add_library(mylib SHARED
    src/kernel1.cu
    src/kernel2.cu
)

target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(mylib PROPERTIES
    CUDA_ARCHITECTURES "70;80"
    CUDA_SEPARABLE_COMPILATION ON
    POSITION_INDEPENDENT_CODE ON
)

3.3 链接库到可执行文件

复制代码
# 主项目 CMakeLists.txt
add_subdirectory(libs/mylib)

add_executable(myapp src/main.cu)
target_link_libraries(myapp PRIVATE mylib)

4. 可重定位设备代码与动态并行

4.1 什么是可重定位设备代码

默认情况下,设备代码在编译时直接生成最终的可执行文件,不允许跨编译单元的 __device__ 函数调用。使用 -rdc=true(或 CMake 的 CUDA_SEPARABLE_COMPILATION ON)后,设备代码编译为可重定位对象,最终链接时解析设备端符号。

需要 -rdc=true 的场景:

  • 多个 .cu 文件之间调用 __device__ 函数
  • 使用动态并行(Kernel 内启动 Kernel)
  • 使用协作组跨 Block 的全局同步(某些情况)

4.2 CMake 设置

复制代码
set_target_properties(myapp PROPERTIES
    CUDA_SEPARABLE_COMPILATION ON
)

5. 跨平台 CUDA 项目配置

5.1 处理不同操作系统

CMake 可以自动处理大部分平台差异,但有些细节需要注意:

  • Windows :默认使用 MSVC,需要正确设置 CMAKE_CUDA_HOST_COMPILER
  • Linux:常用 GCC,注意 CUDA 版本对 GCC 版本有要求。
  • macOS:CUDA 已停止支持 macOS,通常只考虑 Windows/Linux。

5.2 检测 CUDA 可用性

复制代码
find_package(CUDAToolkit REQUIRED)
if(CUDAToolkit_FOUND)
    message(STATUS "CUDA Toolkit found: ${CUDAToolkit_VERSION}")
endif()

5.3 设置主机编译器

复制代码
# 指定主机编译器
set(CMAKE_CUDA_HOST_COMPILER /usr/bin/g++)

6. 编译依赖与增量构建

CMake 自动管理依赖关系,当文件修改后只重新编译受影响的目标。对于 CUDA 项目,如果启用了可分离编译(-rdc=true),设备代码的链接步骤会更加耗时,此时增量构建的优势明显。

6.1 加快构建的技巧

  • 使用 Ninja 生成器:cmake -G Ninja ..
  • 并行构建:cmake --build . -- -j$(nproc)
  • 使用 ccache 缓存编译结果

6.2 编译数据库

生成 compile_commands.json 便于代码分析:

复制代码
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..

7. 代码演示:完整的多文件项目

7.1 项目结构

复制代码
demo/
├── CMakeLists.txt
├── include/
│   └── vector_ops.cuh
├── src/
│   ├── main.cu
│   └── vector_ops.cu

7.2 CMakeLists.txt

复制代码
cmake_minimum_required(VERSION 3.18)
project(CudaVectorDemo LANGUAGES CXX CUDA)

set(CMAKE_CUDA_STANDARD 17)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)

include_directories(${CMAKE_SOURCE_DIR}/include)

add_executable(vector_demo
    src/main.cu
    src/vector_ops.cu
)

set_target_properties(vector_demo PROPERTIES
    CUDA_ARCHITECTURES "native"
    CUDA_SEPARABLE_COMPILATION ON
)

target_compile_options(vector_demo PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-O3>)

7.3 头文件 include/vector_ops.cuh

复制代码
#pragma once

__global__ void vectorAdd(const float *a, const float *b, float *c, int N);

7.4 Kernel 实现 src/vector_ops.cu

复制代码
#include "vector_ops.cuh"

__global__ void vectorAdd(const float *a, const float *b, float *c, int N) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < N) {
        c[idx] = a[idx] + b[idx];
    }
}

7.5 主程序 src/main.cu

复制代码
#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>
#include "vector_ops.cuh"

#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)); \
            exit(EXIT_FAILURE); \
        } \
    } while (0)

int main() {
    const int N = 1 << 20;
    size_t bytes = N * sizeof(float);

    float *h_a = (float*)malloc(bytes);
    float *h_b = (float*)malloc(bytes);
    float *h_c = (float*)malloc(bytes);
    for (int i = 0; i < N; i++) {
        h_a[i] = (float)i;
        h_b[i] = 2.0f * i;
    }

    float *d_a, *d_b, *d_c;
    CUDA_CHECK(cudaMalloc(&d_a, bytes));
    CUDA_CHECK(cudaMalloc(&d_b, bytes));
    CUDA_CHECK(cudaMalloc(&d_c, bytes));
    CUDA_CHECK(cudaMemcpy(d_a, h_a, bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(d_b, h_b, bytes, cudaMemcpyHostToDevice));

    int block = 256;
    int grid = (N + block - 1) / block;
    vectorAdd<<<grid, block>>>(d_a, d_b, d_c, N);
    CUDA_CHECK(cudaDeviceSynchronize());

    CUDA_CHECK(cudaMemcpy(h_c, d_c, bytes, cudaMemcpyDeviceToHost));
    printf("h_c[0] = %f, h_c[N-1] = %f\n", h_c[0], h_c[N-1]);

    free(h_a); free(h_b); free(h_c);
    CUDA_CHECK(cudaFree(d_a));
    CUDA_CHECK(cudaFree(d_b));
    CUDA_CHECK(cudaFree(d_c));
    return 0;
}

7.6 构建与运行

复制代码
mkdir build && cd build
cmake ..
make -j
./vector_demo

8. 课后练习

练习1:创建自己的静态库

将前面章节中的矩阵转置或归约 Kernel 封装成静态库,并在一个新项目中链接使用。

练习2:多文件项目

将一个大 Kernel 拆分为多个 .cu 文件,使用可分离编译,确保 __device__ 函数可以跨文件调用。

练习3:CMake 配置不同架构

修改 CMakeLists.txt,为你的 GPU 和一个其他架构(如 sm_75)生成多架构代码,并在不同 GPU 上测试运行。

练习4:动态库链接

创建一个动态库,并在主程序中动态加载(使用 dlopenLoadLibrary),实现插件式加载 CUDA Kernel。

练习5:优化构建流程

使用 Ninja 和 ccache 加速构建,测量全量构建和增量构建的时间差异。


9. 第6板块总结

恭喜你完成第6板块「编译、调用、错误捕获、调试工具(工程基础)」的全部内容!

回顾核心知识:

  1. ✅ NVCC 编译选项与代码生成深入
  2. ✅ CUDA 运行时与驱动 API
  3. ✅ 错误处理与调试工具(compute‑sanitizer、cuda‑gdb)
  4. ✅ 构建系统与多文件项目

下一步

下一板块将进入 通用算子 Kernel 分类 & CUTLASS 模板进阶,你将学习:

  • 通用算子的分类与设计模式
  • CUTLASS 的核心抽象与优化策略
  • 使用 CUTLASS 编写高性能 GEMM
  • 将 CUTLASS 集成到实际项目中
相关推荐
冬奇Lab1 小时前
开源项目第222期:security-audit — Cloudflare 的安全审计 Skill,把 Coding Agent 变成六阶段安全审计器
人工智能·开源·资讯
卤煮最下饭2 小时前
在眼镜上背单词:一个 AIUI 对话式智能体的诞生
人工智能
Java后端的Ai之路2 小时前
Python进阶探索17 - Python中的深拷贝与浅拷贝
人工智能·python·ai·浅拷贝·深拷贝
知识分享小能手2 小时前
深度学习学习教程,从入门到精通,深度生成模型 —— 知识点详解与代码实现(20)
人工智能·深度学习·学习
hzxxxz2 小时前
26%的研发交给AI之后-人的位置换到了哪里
人工智能
汉克老师2 小时前
GESP2026年9月认证C++三级( 第三部分编程题(2、分割字符串))精讲
c++·gesp·小学生·学c++编程
m0_587383002 小时前
深圳24小时自助健身房系统软件开发实战:架构设计与部署指南
人工智能·数据挖掘·系统架构·需求分析
Joy T2 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
估值探索者2 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘