CUDA 代码 Python 暴露实战笔记 + C++示例代码(第九章重点)

      +

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

      1. 整合 Python 与 CUDA 的核心意义

      • 价值:结合 Python 的易用性(数据处理、可视化、生态丰富)与 CUDA 的高性能(GPU 加速),让非 C++开发者也能调用 GPU 代码。

      • 核心挑战

        • 类型匹配:Python 动态类型 vs C++/CUDA 静态类型;

        • 内存管理:Python 垃圾回收 vs C++/CUDA 显式内存控制;

        • 零拷贝需求:避免数据重复拷贝(PCIe 传输瓶颈)。

      2. 两种核心暴露方法

      方法 1:Ctypes(纯 Python 方案)

      • 核心原理:Python 内置模块,直接加载 C/C++共享库,手动声明函数签名和数据类型。

      • 关键步骤

        1. 编译 CUDA 代码为共享库(.so/.dll),用`extern "C"`避免 C++名称修饰;

        2. Python 中用`ctypes.CDLL`加载共享库;

        3. 声明函数参数类型(argtypes)和返回类型;

        4. 传递数据:通过 NumPy 数组的`ctypes.data_as`获取内部指针,实现零拷贝。

      • 优缺点

        • 优点:无需额外编译(纯 Python)、集成快、轻量;

        • 缺点:手动声明类型繁琐、无类型检查、不支持 Pythonic 接口。

      方法 2:Python C API 包装(扩展方案)

      • 核心原理:用 C/C++编写 Python 扩展模块,通过 Python C API 处理 Python 对象与 C/C++类型转换,编译为 Python 可导入模块。

      • 关键步骤

        1. 编写包装函数:解析 Python 参数(列表/NumPy 数组)→ 转换为 C 类型 → 调用 CUDA 共享库;

        2. 定义模块结构(PyMethodDefPyModuleDef);

        3. 实现模块初始化函数(PyInit_xxx);

        4. 用`setup.py`编译为 Python 扩展模块。

      • 变体

        • 普通版本:处理 Python 列表(需数据拷贝,性能差);

        • NumPy 版本:通过`numpy/arrayobject.h`获取数组内部指针,实现零拷贝(推荐)。

      • 优缺点

        • 优点:支持 Pythonic 接口、类型检查、细粒度控制、NumPy 深度整合;

        • 缺点:需编写 C 包装代码、需额外编译扩展。

      3. 关键技术点

      (1)CUDA 共享库创建

      • 必须用`extern "C"`声明导出函数,避免 C++名称修饰(否则 Ctypes/Python C API 找不到函数);

      • CMake 配置:add_library(xxx SHARED) + CUDA_SEPARABLE_COMPILATION ON

      (2)内存管理

      • 主机内存:由 Python 分配(NumPy 数组/Python 列表),CUDA 库仅操作指针,不负责释放;

      • GPU 内存:CUDA 库内部分配/释放(cudaMalloc/cudaFree),避免内存泄漏;

      • 零拷贝关键:使用 NumPy 数组(连续内存)+ PyArray_DATA(获取内部指针)+ 分页锁定内存(可选,提升传输速度)。

      (3)性能对比

      方案 数据拷贝 适用场景 大数组(1000 万+元素)性能

      Ctypes + NumPy

      零拷贝

      快速集成、轻量需求

      优秀(无额外开销)

      Python C API + NumPy

      零拷贝

      需 Pythonic 接口、复杂逻辑

      优秀(类型检查开销可忽略)

      Python C API + 列表

      双重拷贝

      小型数据、简单场景

      较差(拷贝开销主导)

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

      示例 1:创建 CUDA 共享库(基础依赖)

      1.1 项目结构

      cuda_python_demo/
      ├── include/
      │   └── vector_add.h  # 头文件
      ├── src/
      │   └── vector_add.cu  # CUDA源文件
      └── CMakeLists.txt     # 构建配置

      1.2 头文件(include/vector_add.h)

      #ifndef VECTOR_ADD_H
      #define VECTOR_ADD_H
      
      // extern "C":避免C++名称修饰,确保Ctypes/Python C API能找到函数
      extern "C" {
          /**
           * @brief CUDA加速向量加法(主机端接口)
           * @param a 输入向量a(主机内存,Python分配)
           * @param b 输入向量b(主机内存,Python分配)
           * @param c 输出向量c(主机内存,Python分配)
           * @param N 向量长度
           */
          void vectorAdd(int* a, int* b, int* c, int N);
      }
      
      #endif

      1.3 CUDA 源文件(src/vector_add.cu)

      #include "../include/vector_add.h"
      #include <cuda_runtime.h>
      
      // 错误检查宏(简化CUDA API错误处理)
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              fprintf(stderr, "CUDA错误:%s(行号:%d)\n", cudaGetErrorString(err), __LINE__); \
              exit(1); \
          }
      
      // CUDA核函数:向量加法(设备端执行)
      __global__ void vectorAddKernel(int* d_a, int* d_b, int* d_c, int N) {
          int idx = threadIdx.x + blockIdx.x * blockDim.x;
          if (idx < N) {
              d_c[idx] = a[idx] + b[idx];  // 修复第九章示例中的故意bug
          }
      }
      
      // 主机端接口实现
      void vectorAdd(int* a, int* b, int* c, int N) {
          int *d_a, *d_b, *d_c;
          size_t data_size = N * sizeof(int);
      
          // 1. 分配GPU内存
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_a, data_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_b, data_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_c, data_size));
      
          // 2. 主机→设备数据拷贝(同步传输)
          CHECK_CUDA_ERR(cudaMemcpy(d_a, a, data_size, cudaMemcpyHostToDevice));
          CHECK_CUDA_ERR(cudaMemcpy(d_b, b, data_size, cudaMemcpyHostToDevice));
      
          // 3. 启动核函数(256线程/块,动态计算网格大小)
          int threadsPerBlock = 256;
          int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;
          vectorAddKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
          CHECK_CUDA_ERR(cudaGetLastError());  // 检查核函数启动错误
      
          // 4. 设备→主机数据拷贝(同步传输)
          CHECK_CUDA_ERR(cudaMemcpy(c, d_c, data_size, cudaMemcpyDeviceToHost));
      
          // 5. 释放GPU内存
          CHECK_CUDA_ERR(cudaFree(d_a));
          CHECK_CUDA_ERR(cudaFree(d_b));
          CHECK_CUDA_ERR(cudaFree(d_c));
      }

      1.4 CMake 配置(CMakeLists.txt)

      cmake_minimum_required(VERSION 3.18)
      project(cuda_vector_add LANGUAGES CXX CUDA)
      
      # 设置C++和CUDA标准
      set(CMAKE_CXX_STANDARD 11)
      set(CMAKE_CUDA_STANDARD 11)
      
      # 查找CUDA Toolkit
      find_package(CUDAToolkit REQUIRED)
      
      # 创建共享库(SHARED表示动态库)
      add_library(vector_add SHARED src/vector_add.cu)
      
      # 启用CUDA分离编译(必须,否则共享库无法正确链接CUDA核函数)
      set_target_properties(vector_add PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
      
      # 链接CUDA库
      target_link_libraries(vector_add PRIVATE CUDA::cudart)
      
      # 指定头文件目录(让外部能找到vector_add.h)
      target_include_directories(vector_add PUBLIC include)
      
      # 设置输出目录(共享库生成到build/lib)
      set_target_properties(vector_add PROPERTIES
          LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
      )

      1.5 编译共享库

      # 创建构建目录
      mkdir -p build && cd build
      
      # 配置CMake(指定CUDA Toolkit路径,可选)
      cmake .. -DCMAKE_BUILD_TYPE=Release
      
      # 编译(-j4表示4线程并行编译)
      make -j4
      • 编译成功后,在`build/lib`目录生成共享库:libvector_add.so(Linux)/libvector_add.dylib(macOS)。

      示例 2:Ctypes 调用 CUDA 共享库(纯 Python 方案)

      2.1 Python 调用代码(ctypes_demo.py)

      import ctypes
      import numpy as np
      
      def main():
          # 1. 加载CUDA共享库
          # 注意:路径需指向编译生成的libvector_add.so
          lib = ctypes.CDLL("../build/lib/libvector_add.so")
      
          # 2. 声明函数签名(必须匹配C接口,否则会内存错误)
          # void vectorAdd(int* a, int* b, int* c, int N)
          lib.vectorAdd.argtypes = [
              ctypes.POINTER(ctypes.c_int),  # a
              ctypes.POINTER(ctypes.c_int),  # b
              ctypes.POINTER(ctypes.c_int),  # c
              ctypes.c_int                  # N
          ]
          lib.vectorAdd.restype = None  # 无返回值
      
          # 3. 生成测试数据(NumPy数组,int32类型,与C接口匹配)
          N = 10_000_000  # 1000万元素(大数组测试零拷贝性能)
          a = np.random.randint(0, N, size=N, dtype=np.int32)
          b = np.random.randint(N, 2*N, size=N, dtype=np.int32)
          c = np.zeros(N, dtype=np.int32)  # 输出数组(Python分配内存)
      
          # 4. 调用CUDA函数(传递NumPy数组内部指针,零拷贝)
          lib.vectorAdd(
              a.ctypes.data_as(ctypes.POINTER(ctypes.c_int)),
              b.ctypes.data_as(ctypes.POINTER(ctypes.c_int)),
              c.ctypes.data_as(ctypes.POINTER(ctypes.c_int)),
              ctypes.c_int(N)
          )
      
          # 5. 验证结果(前10个元素)
          valid = np.allclose(a + b, c)
          print(f"结果验证:{'正确' if valid else '错误'}")
          print(f"前10个元素对比:")
          print(f"a + b: {a[:10] + b[:10]}")
          print(f"c    : {c[:10]}")
      
      if __name__ = "__main__":
          main()

      2.2 运行命令

      # 确保已编译共享库,然后运行Python脚本
      python3 ctypes_demo.py

      示例 3:Python C API 包装(扩展方案)

      3.1 普通版本(处理 Python 列表,vector_add_wrapper.c)

      #include <Python.h>
      #include <dlfcn.h>
      #include "../include/vector_add.h"
      
      // 函数指针:指向CUDA共享库中的vectorAdd
      typedef void (*VectorAddFunc)(int* a, int* b, int* c, int N);
      static VectorAddFunc vectorAdd = NULL;
      
      /**
       * @brief Python包装函数:vectorAdd(处理Python列表)
       * @param self 模块实例(未使用)
       * @param args 传入的Python参数((list_a, list_b, N))
       * @return Python列表(结果)
       */
      static PyObject* py_vector_add(PyObject* self, PyObject* args) {
          PyObject *py_a, *py_b;
          int N;
      
          // 1. 解析Python参数:"OOi"表示两个对象(列表)+ 一个整数
          if (!PyArg_ParseTuple(args, "OOi", &py_a, &py_b, &N)) {
              return NULL;  // 解析失败返回NULL,触发Python异常
          }
      
          // 2. 验证参数类型(必须是列表)
          if (!PyList_Check(py_a) || !PyList_Check(py_b)) {
              PyErr_SetString(PyExc_TypeError, "参数必须是列表");
              return NULL;
          }
      
          // 3. 分配主机内存(C侧)
          int* a = (int*)malloc(N * sizeof(int));
          int* b = (int*)malloc(N * sizeof(int));
          int* c = (int*)malloc(N * sizeof(int));
          if (!a || !b || !c) {
              PyErr_SetString(PyExc_MemoryError, "内存分配失败");
              free(a); free(b); free(c);
              return NULL;
          }
      
          // 4. Python列表 → C数组(数据拷贝,开销较大)
          for (int i = 0; i < N; ++i) {
              a[i] = (int)PyLong_AsLong(PyList_GetItem(py_a, i));
              b[i] = (int)PyLong_AsLong(PyList_GetItem(py_b, i));
          }
      
          // 5. 调用CUDA共享库函数
          vectorAdd(a, b, c, N);
      
          // 6. C数组 → Python列表(数据拷贝)
          PyObject* py_result = PyList_New(N);
          for (int i = 0; i < N; ++i) {
              PyList_SetItem(py_result, i, PyLong_FromLong(c[i]));
          }
      
          // 7. 释放C侧内存
          free(a);
          free(b);
          free(c);
      
          return py_result;
      }
      
      // 2. 定义模块函数列表
      static PyMethodDef VectorAddMethods[] = {
          {
              "vector_add",          // Python中调用的函数名
              py_vector_add,         // 对应的C包装函数
              METH_VARARGS,          // 参数传递方式(元组)
              "CUDA加速向量加法(输入Python列表)"  // 函数描述
          },
          {NULL, NULL, 0, NULL}  // 结束标记
      };
      
      // 3. 定义模块结构
      static struct PyModuleDef vector_add_module = {
          PyModuleDef_HEAD_INIT,
          "vector_add_wrapper",    // 模块名(Python中import的名称)
          "CUDA向量加法Python扩展",  // 模块描述
          -1,                     // 不维护模块状态(-1表示全局)
          VectorAddMethods        // 模块函数列表
      };
      
      // 4. 模块初始化函数(Python导入时调用)
      PyMODINIT_FUNC PyInit_vector_add_wrapper(void) {
          // 加载CUDA共享库
          void* handle = dlopen("../build/lib/libvector_add.so", RTLD_LAZY);
          if (!handle) {
              PyErr_SetString(PyExc_ImportError, dlerror());
              return NULL;
          }
      
          // 获取vectorAdd函数指针
          vectorAdd = (VectorAddFunc)dlsym(handle, "vectorAdd");
          if (!vectorAdd) {
              PyErr_SetString(PyExc_ImportError, dlerror());
              dlclose(handle);
              return NULL;
          }
      
          // 创建并返回模块
          return PyModule_Create(&vector_add_module);
      }

      3.2 NumPy 版本(零拷贝,vector_add_np_wrapper.c)

      #include <Python.h>
      #include <numpy/arrayobject.h>  // NumPy C API
      #include <dlfcn.h>
      #include "../include/vector_add.h"
      
      // 函数指针:指向CUDA共享库中的vectorAdd
      typedef void (*VectorAddFunc)(int* a, int* b, int* c, int N);
      static VectorAddFunc vectorAdd = NULL;
      
      /**
       * @brief Python包装函数:vector_add(处理NumPy数组,零拷贝)
       * @param self 模块实例(未使用)
       * @param args 传入的Python参数((np_a, np_b, np_c, N))
       * @return None
       */
      static PyObject* py_vector_add_np(PyObject* self, PyObject* args) {
          PyArrayObject *np_a, *np_b, *np_c;
          int N;
      
          // 1. 解析Python参数:"OOOi"表示三个NumPy数组 + 一个整数
          if (!PyArg_ParseTuple(args, "OOOi", &np_a, &np_b, &np_c, &N)) {
              return NULL;
          }
      
          // 2. 验证NumPy数组类型(必须是int32,连续内存)
          if (PyArray_TYPE(np_a) != NPY_INT32 || !PyArray_IS_C_CONTIGUOUS(np_a) ||
              PyArray_TYPE(np_b) != NPY_INT32 || !PyArray_IS_C_CONTIGUOUS(np_b) ||
              PyArray_TYPE(np_c) != NPY_INT32 || !PyArray_IS_C_CONTIGUOUS(np_c)) {
              PyErr_SetString(PyExc_TypeError, "NumPy数组必须是int32类型且连续内存");
              return NULL;
          }
      
          // 3. 获取NumPy数组内部指针(零拷贝,直接操作Python分配的内存)
          int* a = (int*)PyArray_DATA(np_a);
          int* b = (int*)PyArray_DATA(np_b);
          int* c = (int*)PyArray_DATA(np_c);
      
          // 4. 调用CUDA共享库函数
          vectorAdd(a, b, c, N);
      
          // 5. 返回None(结果已写入np_c)
          Py_RETURN_NONE;
      }
      
      // 2. 定义模块函数列表
      static PyMethodDef VectorAddNpMethods[] = {
          {
              "vector_add",          // Python中调用的函数名
              py_vector_add_np,      // 对应的C包装函数
              METH_VARARGS,          // 参数传递方式(元组)
              "CUDA加速向量加法(输入NumPy数组,零拷贝)"
          },
          {NULL, NULL, 0, NULL}  // 结束标记
      };
      
      // 3. 定义模块结构
      static struct PyModuleDef vector_add_np_module = {
          PyModuleDef_HEAD_INIT,
          "vector_add_np_wrapper",  // 模块名
          "CUDA向量加法Python扩展(NumPy优化)",
          -1,
          VectorAddNpMethods
      };
      
      // 4. 模块初始化函数
      PyMODINIT_FUNC PyInit_vector_add_np_wrapper(void) {
          // 加载CUDA共享库
          void* handle = dlopen("../build/lib/libvector_add.so", RTLD_LAZY);
          if (!handle) {
              PyErr_SetString(PyExc_ImportError, dlerror());
              return NULL;
          }
      
          // 获取vectorAdd函数指针
          vectorAdd = (VectorAddFunc)dlsym(handle, "vectorAdd");
          if (!vectorAdd) {
              PyErr_SetString(PyExc_ImportError, dlerror());
              dlclose(handle);
              return NULL;
          }
      
          // 初始化NumPy C API(必须调用,否则PyArray_DATA会崩溃)
          import_array();
      
          // 创建并返回模块
          return PyModule_Create(&vector_add_np_module);
      }

      3.3 扩展构建配置(setup.py)

      from setuptools import setup, Extension
      import numpy as np
      
      # 定义普通版本扩展
      extension_normal = Extension(
          name="vector_add_wrapper",  # 模块名(import时用)
          sources=["vector_add_wrapper.c"],  # C包装代码
          extra_link_args=["-ldl"],  # 链接dl库(用于dlopen加载CUDA共享库)
          include_dirs=["include"]  # 头文件目录
      )
      
      # 定义NumPy优化版本扩展
      extension_np = Extension(
          name="vector_add_np_wrapper",  # 模块名
          sources=["vector_add_np_wrapper.c"],
          extra_link_args=["-ldl"],
          include_dirs=["include", np.get_include()]  # 加入NumPy头文件目录
      )
      
      # 构建扩展
      setup(
          name="cuda_vector_add_extensions",
          version="1.0",
          description="CUDA向量加法Python扩展",
          ext_modules=[extension_normal, extension_np]
      )

      3.4 构建与运行扩展

      # 1. 构建扩展(生成.so文件,与Python脚本同目录)
      python3 setup.py build_ext --inplace
      
      # 2. Python调用脚本(extension_demo.py)

      3.5 Python 调用扩展(extension_demo.py)

      import numpy as np
      import vector_add_wrapper as vaw  # 普通版本
      import vector_add_np_wrapper as vaw_np  # NumPy优化版本
      
      def test_normal():
          """测试普通版本(Python列表,数据拷贝)"""
          N = 10_000  # 小数组(大数据组拷贝开销过大)
          a = [np.random.randint(0, N) for _ in range(N)]
          b = [np.random.randint(N, 2*N) for _ in range(N)]
          result = vaw.vector_add(a, b, N)
          valid = all(result[i] = a[i] + b[i] for i in range(N))
          print(f"普通版本(列表)结果验证:{'正确' if valid else '错误'}")
      
      def test_np():
          """测试NumPy版本(零拷贝)"""
          N = 10_000_000  # 1000万元素
          a = np.random.randint(0, N, size=N, dtype=np.int32)
          b = np.random.randint(N, 2*N, size=N, dtype=np.int32)
          c = np.zeros(N, dtype=np.int32)
          vaw_np.vector_add(a, b, c, N)
          valid = np.allclose(a + b, c)
          print(f"NumPy版本(零拷贝)结果验证:{'正确' if valid else '错误'}")
          print(f"前10个元素对比:")
          print(f"a + b: {a[:10] + b[:10]}")
          print(f"c    : {c[:10]}")
      
      if __name__ = "__main__":
          test_normal()
          test_np()

      三、关键说明

      1. 核心注意事项

      • extern "C"`的必要性:C++编译器会对函数名进行修饰(如_Z9vectorAddPiiS_`),`extern "C"`强制按 C 风格编译,确保 Ctypes/Python C API 能通过原始函数名找到函数。

      • NumPy 零拷贝关键

        • 数组必须是连续内存(`PyArray_IS_C_CONTIGUOUS`验证);

        • 数据类型必须与 C 接口一致(如`np.int32`对应`int`);

        • 必须调用`import_array()`初始化 NumPy C API,否则会触发段错误。

      • 内存安全

        • 主机内存由 Python 分配,CUDA 库仅操作指针,不负责释放,避免双重释放;

        • 大数组优先使用 NumPy 版本,避免 Python 列表的双重数据拷贝(列表 →C 数组 → 列表)。

      2. 常见问题排查

      • 共享库加载失败:检查`libvector_add.so`路径是否正确,Linux 下可通过`ldd libvector_add.so`查看依赖缺失。

      • 函数未找到:确认`extern "C"`已添加,且函数名拼写一致(C 区分大小写)。

      • 段错误(SIGSEGV)

        • 函数签名声明错误(如参数类型不匹配);

        • NumPy 数组非连续或类型不匹配;

        • 未调用`import_array()`。

      • 性能不佳:小数组用普通版本,大数组必须用 NumPy 零拷贝版本,避免数据拷贝开销。