CUDA 进阶应用与测试实战笔记 + C++示例代码(第十章重点)

      +

      一、核心笔记(整合第十章核心)

      1. 现有 GPU 库与框架使用

      核心库介绍

      • cuBLAS:NVIDIA 官方线性代数库(BLAS 标准实现),优化程度极高,支持矩阵乘法、向量运算等,适合科学计算、AI 领域。

        • 关键特性:默认列优先存储(Fortran 风格),C/C++行优先数据需通过`CUBLAS_OP_T`(转置)适配;

        • 核心函数:cublasSgemm(单精度矩阵乘法)、cublasDgemm(双精度),需手动管理 GPU 内存和数据传输。

      • Thrust:类 C++ STL 的并行算法库,封装 CUDA 底层细节,简化 GPU 编程。

        • 核心功能:向量操作、排序、归约、扫描等;

        • 优势:语法贴近 STL,自动管理 GPU 内存(thrust::device_vector),适合快速开发和原型验证。

      库使用核心注意事项

      • 内存管理:cuBLAS 需手动分配/拷贝 GPU 内存;Thrust 自动处理,但可通过`data()`获取原始指针手动控制;

      • 数据格式:cuBLAS 列优先存储,需注意矩阵转置避免结果错误;

      • 性能权衡:库函数优化充分(如 cuBLAS 矩阵乘法比自定义内核快数倍),但灵活性不足,无法适配特殊数据结构或算法。

      2. 自定义内核 vs 使用现有库

      何时使用现有库

      • 问题属于通用场景(如矩阵乘法、排序、线性代数运算);

      • 追求开发效率,无需深入优化;

      • 团队缺乏 CUDA 底层优化经验。

      何时自定义 CUDA 内核

      • 问题有特殊数据结构(如自定义复杂类型)或算法逻辑(非通用数值计算);

      • 库函数无法适配数据格式(如复杂数精度转换、非标准存储方式);

      • 需极致优化(库函数的通用性可能带来额外开销)。

      混合策略

      • 拆分任务:通用部分用库函数(如 cuBLAS 矩阵乘法),特殊部分用自定义内核;

      • 利用流重叠:通过 CUDA 流实现库函数调用与自定义内核执行、数据传输的并行。

      3. 串行代码迁移到 GPU

      核心思路

      • 避免频繁数据传输:串行步骤若依赖 GPU 中间结果,优先将串行代码迁移到 GPU,减少 Device-Host-Device 传输;

      • 性能权衡:CUDA 核心擅长并行计算,串行代码在 GPU 上可能比 CPU 慢,但整体可减少传输开销,提升端到端性能;

      • 实现策略:

        1. 直接在 GPU 核函数中编写串行逻辑(适用于短串行步骤);

        2. 利用流重叠:GPU 并行计算与 CPU 串行处理部分数据并行执行。

      4. 代码测试与质量保障

      C++测试(GTest)

      • 核心逻辑:测试 CUDA 包装函数(而非直接测试核函数),通过 CPU 计算预期结果,对比 GPU 输出;

      • 关键步骤:编译时链接 GTest 库和 CUDA 库,通过 CMake 配置测试目标,支持 CI/CD 集成。

      Python 测试(Pytest)

      • 核心逻辑:基于第九章的共享库暴露方法(Ctypes),Python 调用 CUDA 共享库,用 NumPy 计算预期结果;

      • 优势:覆盖 Python 调用场景,验证跨语言集成正确性。

      二、C++(CUDA)示例代码

      示例 1:cuBLAS 矩阵乘法(列优先适配+转置处理)

      核心功能

      使用 cuBLAS 实现矩阵乘法,处理列优先存储与行优先数据的适配,呼应第十章“cuBLAS 使用”核心知识点。

      #include <cuda_runtime.h>
      #include <cublas_v2.h>
      #include <iostream>
      #include <vector>
      #include <cmath>
      
      using namespace std;
      
      // 错误检查宏
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              cerr << "CUDA错误:" << cudaGetErrorString(err) << "(行号:" << __LINE__ << ")" << endl; \
              exit(1); \
          }
      
      #define CHECK_CUBLAS_ERR(err) \
          if (err != CUBLAS_STATUS_SUCCESS) { \
              cerr << "cuBLAS错误:" << err << "(行号:" << __LINE__ << ")" << endl; \
              exit(1); \
          }
      
      int main() {
          const int N = 1024;  // 矩阵尺寸(1024×1024)
          const size_t size = N * N * sizeof(float);
      
          // 1. 主机内存分配与初始化(行优先存储)
          vector<float> h_A(N * N, 1.0f);  // 矩阵A全1
          vector<float> h_B(N * N, 2.0f);  // 矩阵B全2
          vector<float> h_C(N * N, 0.0f);  // 结果矩阵C
          vector<float> h_C_ref(N * N, 0.0f);  // CPU计算的参考结果
      
          // 2. CPU计算参考结果(验证cuBLAS正确性)
          for (int i = 0; i < N; ++i) {
              for (int j = 0; j < N; ++j) {
                  for (int k = 0; k < N; ++k) {
                      h_C_ref[i * N + j] += h_A[i * N + k] * h_B[k * N + j];
                  }
              }
          }
      
          // 3. GPU内存分配
          float *d_A, *d_B, *d_C, *d_C_fixed;
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_A, size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_B, size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_C, size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_C_fixed, size));
      
          // 4. 数据拷贝(主机→设备)
          CHECK_CUDA_ERR(cudaMemcpy(d_A, h_A.data(), size, cudaMemcpyHostToDevice));
          CHECK_CUDA_ERR(cudaMemcpy(d_B, h_B.data(), size, cudaMemcpyHostToDevice));
      
          // 5. cuBLAS初始化与矩阵乘法
          cublasHandle_t cublas_handle;
          CHECK_CUBLAS_ERR(cublasCreate(&cublas_handle));
      
          const float alpha = 1.0f;
          const float beta = 0.0f;
          // cuBLAS_Sgemm参数说明:
          // 转置标识(CUBLAS_OP_T=转置)、矩阵维度、alpha、输入矩阵、leading dimension、beta、输出矩阵
          CHECK_CUBLAS_ERR(cublasSgemm(
              cublas_handle,
              CUBLAS_OP_T, CUBLAS_OP_T,  // A和B均转置(适配列优先存储)
              N, N, N,                  // C(N×N) = A(N×N) × B(N×N)
              &alpha,
              d_A, N,                   // A的leading dimension(行优先时为N)
              d_B, N,                   // B的leading dimension
              &beta,
              d_C, N                    // 输出矩阵C(列优先存储)
          ));
      
          // 6. 转置C为行优先(适配C++存储)
          CHECK_CUBLAS_ERR(cublasSgeam(
              cublas_handle,
              CUBLAS_OP_T, CUBLAS_OP_N,  // 仅转置d_C
              N, N,
              &alpha,
              d_C, N,
              &beta,
              nullptr, N,
              d_C_fixed, N
          ));
      
          // 7. 数据拷贝(设备→主机)
          CHECK_CUDA_ERR(cudaMemcpy(h_C.data(), d_C_fixed, size, cudaMemcpyDeviceToHost));
      
          // 8. 结果验证(前10个元素)
          bool valid = true;
          for (int i = 0; i < 10; ++i) {
              if (abs(h_C[i] - h_C_ref[i]) > 1e-5) {
                  valid = false;
                  break;
              }
          }
          cout << "cuBLAS矩阵乘法结果验证:" << (valid ? "正确" : "错误") << endl;
          cout << "前10个元素参考值:";
          for (int i = 0; i < 10; ++i) cout << h_C_ref[i] << " ";
          cout << endl;
          cout << "前10个元素cuBLAS结果:";
          for (int i = 0; i < 10; ++i) cout << h_C[i] << " ";
          cout << endl;
      
          // 9. 资源释放
          CHECK_CUBLAS_ERR(cublasDestroy(cublas_handle));
          CHECK_CUDA_ERR(cudaFree(d_A));
          CHECK_CUDA_ERR(cudaFree(d_B));
          CHECK_CUDA_ERR(cudaFree(d_C));
          CHECK_CUDA_ERR(cudaFree(d_C_fixed));
      
          return 0;
      }

      编译与运行

      • 依赖:安装 CUDA Toolkit(含 cuBLAS);

      • 编译命令:nvcc cublas_matrix_mul.cu -o cublas_matrix_mul -lcublas

      • 运行命令:./cublas_matrix_mul

      • 预期输出:结果验证正确,前 10 个元素均为`2048.0`(1024×1×2)。

      示例 2:Thrust 库排序(简化 GPU 编程)

      核心功能

      使用 Thrust 库实现 GPU 排序,呼应第十章“Thrust 并行算法库”知识点,对比 CPU 排序性能。

      #include <<thrust/device_vector.h>
      #include <<thrust/sort.h>
      #include <<thrust/host_vector.h>
      #include <iostream>
      #include <vector>
      #include <chrono>
      #include <algorithm>
      
      using namespace std;
      using namespace chrono;
      
      int main() {
          const int N = 33'554'432;  // 3200万元素(书中测试规模)
          cout << "排序元素数量:" << N << endl;
      
          // 1. 主机内存初始化(随机浮点数)
          host_vector<float> h_data(N);
          generate(h_data.begin(), h_data.end(), []() { return rand() / (float)RAND_MAX; });
          vector<float> h_data_cpu = h_data;  // CPU排序用副本
      
          // 2. CPU排序(STL,单线程)
          auto cpu_start = high_resolution_clock::now();
          sort(h_data_cpu.begin(), h_data_cpu.end());
          auto cpu_time = duration_cast<milliseconds>(high_resolution_clock::now() - cpu_start).count();
          cout << "CPU排序耗时:" << cpu_time << " ms" << endl;
      
          // 3. Thrust GPU排序
          auto gpu_start = high_resolution_clock::now();
      
          // 主机→设备数据拷贝(thrust::device_vector自动管理GPU内存)
          device_vector<float> d_data = h_data;
      
          // GPU排序(类似STL sort,自动优化)
          thrust::sort(d_data.begin(), d_data.end());
      
          // 设备→主机数据拷贝
          thrust::copy(d_data.begin(), d_data.end(), h_data.begin());
      
          auto gpu_time = duration_cast<milliseconds>(high_resolution_clock::now() - gpu_start).count();
          cout << "GPU排序(Thrust)总耗时(含传输):" << gpu_time << " ms" << endl;
      
          // 4. 结果验证(前10个元素)
          bool valid = true;
          for (int i = 0; i < 10; ++i) {
              if (abs(h_data[i] - h_data_cpu[i]) > 1e-5) {
                  valid = false;
                  break;
              }
          }
          cout << "Thrust排序结果验证:" << (valid ? "正确" : "错误") << endl;
      
          return 0;
      }

      编译与运行

      • 依赖:CUDA Toolkit(Thrust 随 CUDA 安装);

      • 编译命令:nvcc thrust_sort.cu -o thrust_sort

      • 运行命令:./thrust_sort

      • 预期输出:GPU 排序总耗时(含传输)远低于 CPU,结果验证正确。

      示例 3:GTest 测试 CUDA 向量加法(C++级测试)

      3.1 项目结构

      cuda_vector_add_test/
      ├── include/
      │   └── vector_add.h  # 向量加法头文件
      ├── src/
      │   └── vector_add.cu  # CUDA向量加法实现
      ├── test/
      │   └── test_vector_add.cpp  # GTest测试代码
      └── CMakeLists.txt  # 编译配置

      3.2 头文件(include/vector_add.h)

      #ifndef VECTOR_ADD_H
      #define VECTOR_ADD_H
      
      extern "C" {
          /**
           * @brief CUDA向量加法(主机端接口)
           * @param a 输入向量a(行优先,主机内存)
           * @param b 输入向量b(行优先,主机内存)
           * @param c 输出向量c(行优先,主机内存)
           * @param N 向量长度
           */
          void vectorAdd(const float* a, const float* b, float* c, int N);
      }
      
      #endif

      3.3 CUDA 实现(src/vector_add.cu)

      #include "../include/vector_add.h"
      #include <cuda_runtime.h>
      
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              fprintf(stderr, "CUDA错误:%s(行号:%d)\n", cudaGetErrorString(err), __LINE__); \
              exit(1); \
          }
      
      __global__ void vectorAddKernel(const float* d_a, const float* d_b, float* d_c, int N) {
          int idx = threadIdx.x + blockIdx.x * blockDim.x;
          if (idx < N) {
              d_c[idx] = d_a[idx] + d_b[idx];
          }
      }
      
      void vectorAdd(const float* a, const float* b, float* c, int N) {
          float *d_a, *d_b, *d_c;
          size_t size = N * sizeof(float);
      
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_a, size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_b, size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_c, size));
      
          CHECK_CUDA_ERR(cudaMemcpy(d_a, a, size, cudaMemcpyHostToDevice));
          CHECK_CUDA_ERR(cudaMemcpy(d_b, b, size, cudaMemcpyHostToDevice));
      
          int threadsPerBlock = 256;
          int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
          vectorAddKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
          CHECK_CUDA_ERR(cudaGetLastError());
      
          CHECK_CUDA_ERR(cudaMemcpy(c, d_c, size, cudaMemcpyDeviceToHost));
      
          CHECK_CUDA_ERR(cudaFree(d_a));
          CHECK_CUDA_ERR(cudaFree(d_b));
          CHECK_CUDA_ERR(cudaFree(d_c));
      }

      3.4 GTest 测试代码(test/test_vector_add.cpp)

      #include <gtest/gtest.h>
      #include "../include/vector_add.h"
      
      // 测试套件:VectorAddTest,测试用例:SimpleAddition
      TEST(VectorAddTest, SimpleAddition) {
          int N = 5;
          const float a[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
          const float b[] = {10.0f, 20.0f, 30.0f, 40.0f, 50.0f};
          float c[N] = {0.0f};
      
          // 调用CUDA向量加法
          vectorAdd(a, b, c, N);
      
          // 验证结果(浮点型用EXPECT_FLOAT_EQ,允许微小误差)
          for (int i = 0; i < N; ++i) {
              EXPECT_FLOAT_EQ(c[i], a[i] + b[i]);
          }
      }
      
      // 测试边界情况:空向量
      TEST(VectorAddTest, EmptyVector) {
          int N = 0;
          float a[] = {};
          float b[] = {};
          float c[] = {};
          vectorAdd(a, b, c, N);  // 无崩溃即通过
          SUCCEED();
      }
      
      // 测试大数据量(10000元素)
      TEST(VectorAddTest, LargeVector) {
          int N = 10000;
          float* a = new float[N];
          float* b = new float[N];
          float* c = new float[N];
      
          // 初始化数据
          for (int i = 0; i < N; ++i) {
              a[i] = i * 0.1f;
              b[i] = (N - i) * 0.1f;
          }
      
          vectorAdd(a, b, c, N);
      
          // 验证前10个和后10个元素
          for (int i = 0; i < 10; ++i) {
              EXPECT_NEAR(c[i], a[i] + b[i], 1e-5);
              EXPECT_NEAR(c[N-10+i], a[N-10+i] + b[N-10+i], 1e-5);
          }
      
          delete[] a;
          delete[] b;
          delete[] c;
      }

      3.5 CMake 配置(CMakeLists.txt)

      cmake_minimum_required(VERSION 3.18)
      project(CUDAVectorAddTest LANGUAGES CXX CUDA)
      
      # 启用测试(CTest)
      enable_testing()
      
      # 查找GTest库
      find_package(GTest REQUIRED)
      # 查找CUDA Toolkit
      find_package(CUDAToolkit REQUIRED)
      
      # 生成共享库(供测试和后续Python调用)
      add_library(vector_add SHARED src/vector_add.cu)
      set_target_properties(vector_add PROPERTIES
          CUDA_SEPARABLE_COMPILATION ON
          LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
      )
      target_include_directories(vector_add PUBLIC include)
      target_link_libraries(vector_add PRIVATE CUDA::cudart)
      
      # 生成测试可执行文件
      add_executable(vector_add_test test/test_vector_add.cpp)
      target_include_directories(vector_add_test PUBLIC include)
      target_link_libraries(vector_add_test
          GTest::GTest
          GTest::Main
          vector_add
          CUDA::cudart
      )
      
      # 注册测试(CTest)
      add_test(NAME VectorAddAllTests COMMAND vector_add_test)

      3.6 编译与运行

      • 依赖:安装 GTest(sudo apt install libgtest-dev)、CUDA Toolkit;

      • 编译步骤:

        mkdir -p build && cd build
        cmake .. -DCMAKE_BUILD_TYPE=Release
        make -j4
      • 运行测试:

        # 直接运行测试可执行文件
        ./vector_add_test
        # 或通过CTest运行(CI/CD常用)
        ctest
      • 预期输出:所有测试用例通过([ PASSED ] 3 tests.)。

      示例 4:Pytest 测试 Python 调用 CUDA 代码(跨语言测试)

      4.1 Python 包装模块(vector_add_py.py)

      import ctypes
      import numpy as np
      
      # 加载CUDA共享库(编译后生成的libvector_add.so)
      lib = ctypes.CDLL("../build/lib/libvector_add.so")
      
      # 声明函数签名(适配NumPy数组)
      lib.vectorAdd.argtypes = [
          np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"),
          np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"),
          np.ctypeslib.ndpointer(dtype=np.float32, flags="C_CONTIGUOUS"),
          ctypes.c_int
      ]
      lib.vectorAdd.restype = None
      
      def vector_add(a: np.ndarray, b: np.ndarray) -> np.ndarray:
          """Python包装函数:调用CUDA向量加法"""
          # 验证输入(维度一致、float32、连续内存)
          assert a.shape = b.shape, "输入向量维度不一致"
          assert a.dtype = np.float32 and b.dtype = np.float32, "输入必须是float32类型"
          assert a.flags["C_CONTIGUOUS"] and b.flags["C_CONTIGUOUS"], "输入必须是C连续内存"
      
          N = a.size
          c = np.empty_like(a)  # 输出向量
          lib.vectorAdd(a, b, c, N)
          return c

      4.2 Pytest 测试代码(test_vector_add_py.py)

      import numpy as np
      from vector_add_py import vector_add
      
      def test_simple_addition():
          """测试简单向量加法"""
          a = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float32)
          b = np.array([5.0, 6.0, 7.0, 8.0], dtype=np.float32)
          expected = a + b
          result = vector_add(a, b)
          # 验证所有元素误差在1e-5内
          np.testing.assert_allclose(result, expected, rtol=1e-5)
      
      def test_large_vector():
          """测试大数据量向量加法"""
          N = 1000000  # 100万元素
          a = np.random.rand(N).astype(np.float32)
          b = np.random.rand(N).astype(np.float32)
          expected = a + b
          result = vector_add(a, b)
          np.testing.assert_allclose(result, expected, rtol=1e-5)
      
      def test_empty_vector():
          """测试空向量"""
          a = np.array([], dtype=np.float32)
          b = np.array([], dtype=np.float32)
          result = vector_add(a, b)
          assert result.size = 0
      
      def test_non_contiguous_vector():
          """测试非连续内存(应抛出断言错误)"""
          a = np.array([1.0, 2.0, 3.0], dtype=np.float32)[::2]  # 非连续切片
          b = np.array([4.0, 5.0, 6.0], dtype=np.float32)[::2]
          try:
              vector_add(a, b)
          except AssertionError as e:
              assert "C连续内存" in str(e)
          else:
              assert False, "未抛出非连续内存断言错误"

      4.3 运行测试

      • 依赖:安装 Python、NumPy、Pytest(pip install numpy pytest);

      • 运行命令:

        # 运行所有测试
        pytest test_vector_add_py.py -v
        # 详细输出(调试用)
        pytest test_vector_add_py.py -s
      • 预期输出:所有测试用例通过(4 passed in 0.5s)。

      三、关键说明

      1. 核心知识点关联

      • 示例 1(cuBLAS):演示现有库的高效使用,解决第十章“通用线性代数运算”场景,重点处理列优先与行优先的适配;

      • 示例 2(Thrust):展示高层库简化 GPU 编程,呼应“快速开发与原型验证”需求,无需手动管理 GPU 内存;

      • 示例 3(GTest):实现 C++级 CUDA 代码测试,确保自定义内核正确性,支持 CI/CD 集成;

      • 示例 4(Pytest):跨语言测试 Python 调用 CUDA 共享库,覆盖第十章“代码多层面测试”要求。

      2. 关键注意事项

      • cuBLAS 矩阵乘法:必须通过`CUBLAS_OP_T`转置,否则因列优先存储导致结果错误;

      • Thrust 性能:`device_vector`自动优化内存访问,但大数据量下需关注数据传输耗时;

      • 测试最佳实践:覆盖简单场景、边界情况(空向量)、大数据量,浮点型验证用`EXPECT_NEAR`/assert_allclose

      • 共享库兼容性:Python 调用时需确保共享库路径正确,NumPy 数组需为连续内存(C_CONTIGUOUS)。

      3. 常见问题排查

      • cuBLAS 错误:检查`cublasCreate`/`cublasSgemm`返回值,确保 CUDA 设备可用;

      • Thrust 编译错误:确认 CUDA Toolkit 路径正确,`nvcc`版本支持 Thrust;

      • GTest 链接错误:确保`find_package(GTest)找到库文件,链接`GTest::GTest`和`GTest::Main

      • Python 调用崩溃:验证 NumPy 数组类型、维度、连续性,共享库函数签名与 C 接口一致。