找回密码
 立即注册
首页 业界区 业界 Cython与CUDA之Add

Cython与CUDA之Add

拓炊羡 2025-6-1 23:34:41
技术背景

在前一篇文章中,我们介绍过使用Cython结合CUDA实现了一个Gather算子以及一个BatchGather算子。这里我们继续使用这一套方案,实现一个简单的求和函数,通过CUDA来计算数组求和。由于数组求和对于不同的维度来说都是元素对元素进行求和,因此高维数组跟低维数组没有差别,这里我们全都当做是一维的数组输入来处理,不做Batch处理。
头文件

首先我们需要一个CUDA头文件cuda_add.cuh来定义CUDA函数的接口:
  1. #include <stdio.h>
  2. extern "C" float Add(float *A, float *B, float *res, int N);
复制代码
其他头文件如异常捕获,可以参考这篇文章,CUDA函数计时可以参考这篇文章。
CUDA文件

CUDA文件cuda_add.cu中包含了核心部分的算法:
  1. // nvcc -shared ./cuda_add.cu -Xcompiler -fPIC -o ./libcuadd.so
  2. #include <stdio.h>
  3. #include "cuda_add.cuh"
  4. #include "error.cuh"
  5. #include "record.cuh"
  6. __global__ void AddKernel(float *A, float *B, float *res, int N) {
  7.     int tid = blockIdx.x * blockDim.x + threadIdx.x;
  8.     // 每个线程处理多个元素
  9.     int stride = blockDim.x * gridDim.x;
  10.     for (int i = tid; i < N; i += stride) {
  11.         res[i] = A[i] + B[i];
  12.     }
  13. }
  14. extern "C" float Add(float *A, float *B, float *res, int N){
  15.     float *A_device, *B_device, *res_device;
  16.     CHECK(cudaMalloc((void **)&A_device, N * sizeof(float)));
  17.     CHECK(cudaMalloc((void **)&B_device, N * sizeof(float)));
  18.     CHECK(cudaMalloc((void **)&res_device, N * sizeof(float)));
  19.     CHECK(cudaMemcpy(A_device, A, N * sizeof(float), cudaMemcpyHostToDevice));
  20.     CHECK(cudaMemcpy(B_device, B, N * sizeof(float), cudaMemcpyHostToDevice));
  21.     int block_size, grid_size;
  22.     cudaOccupancyMaxPotentialBlockSize(&grid_size, &block_size, AddKernel, 0, N);
  23.     grid_size = (N + block_size - 1) / block_size;
  24.     float timeTaken = GET_CUDA_TIME((AddKernel<<<grid_size, block_size>>>(A_device, B_device, res_device, N)));
  25.     CHECK(cudaGetLastError());
  26.     CHECK(cudaDeviceSynchronize());
  27.     CHECK(cudaMemcpy(res, res_device, N * sizeof(float), cudaMemcpyDeviceToHost));
  28.     CHECK(cudaFree(A_device));
  29.     CHECK(cudaFree(B_device));
  30.     CHECK(cudaFree(res_device));
  31.     return timeTaken;
  32. }
复制代码
此处代码是部分经过DeepSeek优化的,例如在核函数中使用for循环对多个数据进行处理,而不是只处理一个数据。另外block_size由cudaOccupancyMaxPotentialBlockSize自动生成,也避免了手动设定带来的一些麻烦。不过这里我们没有使用Stream来优化,只是简单的演示一个功能算法。
Cython接口文件

由于我们的框架是通过Cython来封装CUDA函数,然后在Python中调用,所以这里需要一个Cython接口文件wrapper.pyx。
  1. # cythonize -i -f wrapper.pyx
  2. import numpy as np
  3. cimport numpy as np
  4. cimport cython
  5. cdef extern from "<dlfcn.h>" nogil:
  6.     void *dlopen(const char *, int)
  7.     char *dlerror()
  8.     void *dlsym(void *, const char *)
  9.     int dlclose(void *)
  10.     enum:
  11.         RTLD_LAZY
  12. ctypedef float (*AddFunc)(float *A, float *B, float *res, int N) noexcept nogil
  13. cdef void* handle_add = dlopen('/path/to/cuda/libcuadd.so', RTLD_LAZY)
  14. @cython.boundscheck(False)
  15. @cython.wraparound(False)
  16. cpdef float[:] cuda_add(float[:] x, float[:] y):
  17.     cdef:
  18.         AddFunc Add
  19.         float timeTaken
  20.         int N = x.shape[0]
  21.         float[:] res = np.zeros((N, ), dtype=np.float32)
  22.     Add = dlsym(handle_add, "Add")
  23.     timeTaken = Add(&x[0], &y[0], &res[0], N)
  24.     print (timeTaken)
  25.     return res
  26. while not True:
  27.     dlclose(handle)
复制代码
Python调用文件

最后,我们写一个Python的案例test_add.py来调用Cython封装后的CUDA函数:
  1. import numpy as np
  2. np.random.seed(0)
  3. from wrapper import cuda_add
  4. N = 1024 * 1024 * 100
  5. x = np.random.random((N,)).astype(np.float32)
  6. y = np.random.random((N,)).astype(np.float32)
  7. np_res = x+y
  8. res = np.asarray(cuda_add(x, y))
  9. print (res.shape)
  10. print ((res==np_res).sum())
复制代码
运行python文件即可获得CUDA核函数的耗时,以及相应的返回结果输出。
查看GPU信息

为了更加深刻的理解一下CUDA计算的性能,我们可以查看GPU的一些关键参数,以此来推理CUDA运算的理论运算极限。在一些版本的CUDA里面会自带一个deviceQuery:
  1. $ cd /usr/local/cuda-10.1/samples/1_Utilities/deviceQuery
复制代码
里面包含有一些可以查询获取本地GPU配置参数的文件:
  1. $ ll
  2. 总用量 44
  3. drwxr-xr-x 2 root root  4096 7月  13  2021 ./
  4. drwxr-xr-x 8 root root  4096 7月  13  2021 ../
  5. -rw-r--r-- 1 root root 12473 7月  13  2021 deviceQuery.cpp
  6. -rw-r--r-- 1 root root 10812 7月  13  2021 Makefile
  7. -rw-r--r-- 1 root root  1789 7月  13  2021 NsightEclipse.xml
  8. -rw-r--r-- 1 root root   168 7月  13  2021 readme.txt
复制代码
可以将这些文件进行编译,但是因为这些代码强行指定了nvcc的地址在/usr/local/cuda下,所以如果本地没有这个路径的,可能需要使用ln -s来创建一个路径软链接:
  1. $ sudo ln -s /usr/local/cuda-10.1 /usr/local/cuda
复制代码
然后再执行编译指令:
  1. $ sudo make
  2. /usr/local/cuda/bin/nvcc -ccbin g++ -I../../common/inc  -m64    -gencode arch=compute_30,code=sm_30 -gencode arch=compute_35,code=sm_35 -gencode arch=compute_37,code=sm_37 -gencode arch=compute_50,code=sm_50 -gencode arch=compute_52,code=sm_52 -gencode arch=compute_60,code=sm_60 -gencode arch=compute_61,code=sm_61 -gencode arch=compute_70,code=sm_70 -gencode arch=compute_75,code=sm_75 -gencode arch=compute_75,code=compute_75 -o deviceQuery.o -c deviceQuery.cpp
  3. /usr/local/cuda/bin/nvcc -ccbin g++   -m64      -gencode arch=compute_30,code=sm_30 -gencode arch=compute_35,code=sm_35 -gencode arch=compute_37,code=sm_37 -gencode arch=compute_50,code=sm_50 -gencode arch=compute_52,code=sm_52 -gencode arch=compute_60,code=sm_60 -gencode arch=compute_61,code=sm_61 -gencode arch=compute_70,code=sm_70 -gencode arch=compute_75,code=sm_75 -gencode arch=compute_75,code=compute_75 -o deviceQuery deviceQuery.o
  4. mkdir -p ../../bin/x86_64/linux/release
  5. cp deviceQuery ../../bin/x86_64/linux/release
复制代码
编译完成后直接执行编译好的可执行文件:
  1. $ ./deviceQuery
  2. ./deviceQuery Starting...
  3. CUDA Device Query (Runtime API) version (CUDART static linking)
  4. Detected 2 CUDA Capable device(s)
  5. Device 0: "Quadro RTX 4000"
  6.   CUDA Driver Version / Runtime Version          12.2 / 10.1
  7.   CUDA Capability Major/Minor version number:    7.5
  8.   Total amount of global memory:                 7972 MBytes (8358723584 bytes)
  9.   (36) Multiprocessors, ( 64) CUDA Cores/MP:     2304 CUDA Cores
  10.   GPU Max Clock rate:                            1545 MHz (1.54 GHz)
  11.   Memory Clock rate:                             6501 Mhz
  12.   Memory Bus Width:                              256-bit
  13.   L2 Cache Size:                                 4194304 bytes
  14.   Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  15.   Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  16.   Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  17.   Total amount of constant memory:               65536 bytes
  18.   Total amount of shared memory per block:       49152 bytes
  19.   Total number of registers available per block: 65536
  20.   Warp size:                                     32
  21.   Maximum number of threads per multiprocessor:  1024
  22.   Maximum number of threads per block:           1024
  23.   Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  24.   Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  25.   Maximum memory pitch:                          2147483647 bytes
  26.   Texture alignment:                             512 bytes
  27.   Concurrent copy and kernel execution:          Yes with 3 copy engine(s)
  28.   Run time limit on kernels:                     Yes
  29.   Integrated GPU sharing Host Memory:            No
  30.   Support host page-locked memory mapping:       Yes
  31.   Alignment requirement for Surfaces:            Yes
  32.   Device has ECC support:                        Disabled
  33.   Device supports Unified Addressing (UVA):      Yes
  34.   Device supports Compute Preemption:            Yes
  35.   Supports Cooperative Kernel Launch:            Yes
  36.   Supports MultiDevice Co-op Kernel Launch:      Yes
  37.   Device PCI Domain ID / Bus ID / location ID:   0 / 3 / 0
  38.   Compute Mode:
  39.      < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >
  40. Device 1: "Quadro RTX 4000"
  41.   CUDA Driver Version / Runtime Version          12.2 / 10.1
  42.   CUDA Capability Major/Minor version number:    7.5
  43.   Total amount of global memory:                 7974 MBytes (8361738240 bytes)
  44.   (36) Multiprocessors, ( 64) CUDA Cores/MP:     2304 CUDA Cores
  45.   GPU Max Clock rate:                            1545 MHz (1.54 GHz)
  46.   Memory Clock rate:                             6501 Mhz
  47.   Memory Bus Width:                              256-bit
  48.   L2 Cache Size:                                 4194304 bytes
  49.   Maximum Texture Dimension Size (x,y,z)         1D=(131072), 2D=(131072, 65536), 3D=(16384, 16384, 16384)
  50.   Maximum Layered 1D Texture Size, (num) layers  1D=(32768), 2048 layers
  51.   Maximum Layered 2D Texture Size, (num) layers  2D=(32768, 32768), 2048 layers
  52.   Total amount of constant memory:               65536 bytes
  53.   Total amount of shared memory per block:       49152 bytes
  54.   Total number of registers available per block: 65536
  55.   Warp size:                                     32
  56.   Maximum number of threads per multiprocessor:  1024
  57.   Maximum number of threads per block:           1024
  58.   Max dimension size of a thread block (x,y,z): (1024, 1024, 64)
  59.   Max dimension size of a grid size    (x,y,z): (2147483647, 65535, 65535)
  60.   Maximum memory pitch:                          2147483647 bytes
  61.   Texture alignment:                             512 bytes
  62.   Concurrent copy and kernel execution:          Yes with 3 copy engine(s)
  63.   Run time limit on kernels:                     Yes
  64.   Integrated GPU sharing Host Memory:            No
  65.   Support host page-locked memory mapping:       Yes
  66.   Alignment requirement for Surfaces:            Yes
  67.   Device has ECC support:                        Disabled
  68.   Device supports Unified Addressing (UVA):      Yes
  69.   Device supports Compute Preemption:            Yes
  70.   Supports Cooperative Kernel Launch:            Yes
  71.   Supports MultiDevice Co-op Kernel Launch:      Yes
  72.   Device PCI Domain ID / Bus ID / location ID:   0 / 166 / 0
  73.   Compute Mode:
  74.      < Default (multiple host threads can use ::cudaSetDevice() with device simultaneously) >
  75. > Peer access from Quadro RTX 4000 (GPU0) -> Quadro RTX 4000 (GPU1) : Yes
  76. > Peer access from Quadro RTX 4000 (GPU1) -> Quadro RTX 4000 (GPU0) : Yes
  77. deviceQuery, CUDA Driver = CUDART, CUDA Driver Version = 12.2, CUDA Runtime Version = 10.1, NumDevs = 2
  78. Result = PASS
复制代码
这里就输出了两块GPU的相关参数。其中Memory Bus Width:                              256-bit表示总位宽,数值越高越好。Memory Clock rate:                             6501 Mhz表示显存的访问速率,经常被用于估计GPU的性能,因为很多时候GPU的性能瓶颈可能在内存-显存的传输上。GPU Max Clock rate:                            1545 MHz (1.54 GHz)可以用来估计显存操作速率。
性能估算

以普通的CUDA加法为例,有效速率的大致公式为:

\[有效速率(Gbps)=\frac{物理频率\times 2}{1000}\]
进而可以计算带宽:

\[带宽(GB/s)=\frac{有效速率\times 总线宽度}{8}\]
最后,根据带宽估算计算速率的上限,也就等同于估算一个CUDA加法的计算耗时的下限:

\[计算耗时(s)=\frac{总操作数据量(B)}{带宽(B/s)}\]
实际计算的话,单次的加法操作涉及到四个步骤:读取数组A元素,读取数组B元素,加和,写入C数组。也就是说,涉及到3次内存操作和1次加和操作。关于内存部分的耗时(假定N=1024*1024*100):

\[T_{mem}=\frac{N*4*3}{\frac{\frac{6501}{1000}*256}{8}*10^9}\approx 0.0030243 s\]
耗时估计在3ms左右(这里的4是单精度浮点数到Byte的换算)。至于单次的加法运算耗时,其实可以忽略不计,因为指令吞吐率大概为:

\[指令吞吐率(TFLOPS)=核心数\times 时钟频率=2304\times 1.54e09\approx 3.55\]
那么理论最小耗时为(假定N=1024*1024*100):

\[T_{theo}(s)=\frac{总计算量}{指令吞吐率}\approx 2.95e-05\]
指令运算部分耗时大约在0.03 ms,跟显存IO部分的耗时3 ms比起来可以忽略的量级。
真实测试

运行Python代码输出的结果为:
  1. $ python3 test_add.py
  2. 3.3193600177764893
  3. (104857600,)
  4. 104857600
复制代码
这个数据3.32 ms已经很接近于极限速率3 ms了,应该说在这样的算法框架下已经很难再往下去优化了,更多时候优化点还是在于CPU到GPU的内存传输效率上。
总结概要

本文介绍了使用CUDA和Cython来实现一个CUDA加法算子的方法,并介绍了使用CUDA参数来估算性能极限的算法。经过实际测试,核函数部分的算法性能优化空间已经不是很大了,更多时候可以考虑使用Stream来优化Host和Device之间的数据传输。
版权声明

本文首发链接为:https://www.cnblogs.com/dechinphy/p/cuda-cython-add.html
作者ID:DechinPhy
更多原著文章:https://www.cnblogs.com/dechinphy/
请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html
参考链接


  • https://blog.csdn.net/sunyuhua_keyboard/article/details/145633805

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册