随着人工智能的发展,科学计算(尤其是矩阵/张量计算)越来越重要;因此,基于CUDA的张量编程也越来越重要。
在上一篇笔记中翻译了《An Even Easier Introduction to CUDA》,但是感觉作者写的不是很好;
这里重新写了一篇。同时,也作为CUDA和并行编程的开篇。
源代码:
一、并行编程导论与CUDA入门
温馨提示:本文章配合 Colab 一同执行学习效果更佳:
(一)、CUDA编程概述
1、什么是CUDA
CUDA 是 NVIDIA 开发的并行计算平台和编程模型;
具有以下特点:
- C/C++ 语法;
- SIMT(Single Instruction Multiple Threads)模式:一个指令会被多个线程同时执行!
- 需要与CPU协作:CPU负责整理结果、处理逻辑;
- 自动调度:根据设定的执行参数,自动调度;
2、CUDA 运算硬件单元
(1)SM 单元
下面是一个 GPU 硬件单元:

每个核心中包含了多个 SM(Stream Multi-processor),任务在 SM 中处理;
SM 中包含了:
- CUDA Core/SP:进行并行的加减法等计算;
- Tensor Core:张量计算
- ……
(2)CPU与GPU协作
CPU 与 GPU 协同工作的流程如下:

首先,习惯上将:
- CPU 所在端称为:Host 端,对应内存为 RAM;
- GPU 所在称为:Device 端,对应内存为 Global Memory(通常对应 GPU RAM,显存);
通常,Global Memory 在其范围和生命周期中是全局的!
也就是说,每个在thread block grid 中的 thread 都可以访问Global Memory,并且生命周期与程序的执行时间一样长!
更多内容:
CUDA 程序执行时主要分为以下几个步骤:
- CPU 准备(CPU Prepare):在主机端(Host ,包含 CPU 和 RAM 主存 ),CPU 负责初始化数据、设置计算参数等准备工作,为后续在 GPU 上的运算任务做铺垫,确定要处理的数据和运算逻辑;
- CPU 传输数据至 GPU(CPU Transfers Data to GPU):通过总线(Bus),CPU 把主存(RAM)中准备好的数据传输到 GPU 端的全局内存(Global Memory ,GM),因为 GPU 运算需要的数据要先存放到其可访问的内存空间;
- 从 GM 读数据(Read Data from GM):GPU(如 NVIDIA A100 )从自身的全局内存中读取需要参与运算的数据,将数据加载到运算单元可处理的位置;
- 运算(Compute):NVIDIA A100 等 GPU 设备利用自身的并行运算核心,对读取的数据执行 CUDA 核函数定义的运算操作,发挥 GPU 并行计算优势,高效处理大规模数据计算任务;
- 写回 GM(Write Back to GM):运算完成后,GPU 将运算结果写回到全局内存中,暂存运算产出的数据;
- GPU 传输数据至 CPU(GPU Transfers Data to CPU):再次通过总线,GPU 把全局内存中存储的运算结果传输回主机端的主存(RAM),供 CPU 进一步处理(如数据展示、后续其他主机端逻辑运算等 ),完成一次 CUDA 编程的计算流程;
CUDA 这种流程实现了 CPU 与 GPU 协同,让 GPU 承担并行计算 heavy - lifting ,提升计算密集型任务效率,广泛用于深度学习训练推理、科学计算等场景!
(二)、CUDA运算示例:加法
1、CPU加法
add_cpu.cpp
1 |
|
主要分为以下几个步骤:
- 准备和初始化数据;
- 定义加法函数
- 靠循环来进行所有的元素加法
- 调用函数
- 验证结果
2、修改为GPU加法(重点!)
分为以下几个步骤:
- 修改文件名为
*.cu:例如add_cuda.cu(表示 CUDA 程序) - 准备和初始化数据(CPU):使用 vector 等进行 Host 端的 RAM 分配;
- 数据传输到 GPU:使用
cudaMalloc分配显存、使用cudaMemcpy复制数据等; - GPU 从 GM 中读取并计算后写回(调用核(kernel)函数计算):
- 修改核函数声明:
- 修改调用方式:
- 将 GPU 数据传输回 CPU:
- 验证结果
下面分别来看;
(1)修改文件名为 *.cu
CUDA 规定其文件扩展名为 *.cu,语法和 C++ 类似!
(2)准备和初始化数据(CPU)
这步比较简单:
add_cuda.cu
1 | // Step 1: Prepare & initialize data |
此时在 Host 端的 RAM 分配内存;
(3)数据传输到 GPU
数据传输到 GPU 使用 CUDA 提供的函数:
- 使用
cudaMalloc分配显存; - 使用
cudaMemcpy复制数据;
add_cuda.cu
1 | float *d_a, *d_b, *d_c; |
这里使用了:
CUDA_CHECK宏进行校验;cudaMemcpyHostToDevice指定数据流方向;
CUDA_CHECK 宏定义如下:
1 |
(4:补)CUDA层级结构
i.线程层级结构
在 CPU 中,使用循环进行执行;
而在 GPU 中,使用的是 SIMT,即:一条命令会同时被多个线程执行!
此时需要指挥每个线程:组织结构和编号!
在 CUDA 中,包含:
- Grid:
- Block:
- Thread:
如下图:

其中:每一个 Grid 中包含多个已编号的 Block,而每一个 Block 中包含多个已编号的 Thread!
同时,每个 Block 中包含的线程数是一样的!
一共有:0~N-1个Thread(假设每个 Block 包含 N 个 Thread);

ii.线程索引计算方法
在 CUDA 中:
- 每个线程都有 独一无二 的编号索引(idx);
idx = BlockID * BlockSize + ThreadID;
如下图:

(4)编写和调用核函数
相对于 CPU 中使用循环的方式执行,在 GPU 中主要使用的是:多线程并行;
步骤如下:
- 定义 block 的数量和大小来指挥线程、进行/并行计算;
- 定义 GPU 上的加法函数(核函数);
- 结合定义的信息调用 GPU 加法函数;
层级结构定义:
1 | // Set up kernel configuration |
核函数定义:
1 | template<typename T> |
只能通过指针的方式传递!
因为像是 vector 等数据结构,都是在 Host 端定义的,并不能在 Global Memory 中分配!
核函数调用:
1 | // Call cuda add kernel |
其中:
dim3:CUDA 表示线程层级结构的类型(包括:x、y、z 三个维度);<<<>>>:传递线程层级信息给核函数;核函数(kernel):设备侧的入口函数;__global__:表示这是个核函数;blockldx:表示 block 的编号,第几个 block;blockDim:表示 block 的大小,一个 block 多少个线程;threadldx:表示 thread 的编号,表示第几个 thread;
(5)将 GPU 数据传输回 CPU
同样,使用 cudaMemcpy:
1 | CUDA_CHECK(cudaMemcpy(h_c.data(), d_c, size_bytes, cudaMemcpyDeviceToHost)); |
(6)验证结果,释放内存
验证结果使用已经复制到 h_c 中的数据;
释放内存使用 cudaFree:
add_cuda.cu
1 | float maxError = 0.0f; |
3、编译&运行CUDA程序
需要使用 NVCC(NIVIDEA CUDA Compiler) 的编译器来编译程序;
NVCC 是 CUDA Toolkit 的一部分:
(1)编译流程
如下图所示:

流程如下:
- 每个 cu:Host 代码与 Device 代码分离(部分在CPU执行、部分在GPU执行)
- 每个虚拟架构:Device 代码编译出 fatbin
- Host 端使用系统的 C++ 编译器(如 g++)
- 链接(device,host)
- 最终获得可使用 GPU 的可执行二进制文件
补:GPU虚拟架构
NVIDIA 不同年代生产的GPU可能有不同的架构,如下图所示:

以 A100 为例,A100 为 Ampere 架构;同时,各个架构间有区别;
因此提出:Compute Capability (CC)
- 类似版本,表示能支持的功能和指令集合
- A100 (Ampere 架构)是:cc8.0
虽然 A100 举例,但从 CUDA 编程的角度目前各种架构没有本质区别!
正因为如此,所以说CUDA是一个编程平台
同时,在编译时也可以指定架构编译选项:
-arch:指定虚拟架构,PTX生成目标。决定代码中可使用的CUDA 功能;-code:指定实际架构,生成针对特定 GPU 硬件的二进制机器码(cubin);
(2)编译命令
通过:
1 | nvcc add_cuda.cu -o add_cuda |
即可编译!
运行:
1 | ./add_cuda |
(三)、GPU性能测试
可以通过:
1 | nvidia-smi |
观察 GPU 利用率!
1、并行加法性能对比
分别对比:
- CPU;
<<<1,1>>>;<<<256,256>>>;- GPU 满载;
代码如下:
add_cuda_profiling.cu
1 |
|
可以修改其中的:
1
2 dim3 block_dim(1);
dim3 grid_dim(1);
不同情况下的性能如下:

可以使用 Nsight Systems(nsys,NVIDIA系统级性能分析工具)来分析;
启动 profiling:
1 | nsys profile -t cuda,nvtx,osrt -o add_cuda_profiling -f true ./add_cuda_profiling |
解析并统计性能信息:
1 | nsys stats add_cuda_profiling.nsys-rep |
各个类型 API Summary 分析结果如下:

可以看到:<<<1,1>>> cudaLaunchKernel 占比非常高这是由于:
核函数调用有开销,在外面多次循环调用开销巨大!
因此,需要进行优化!
2、将循环放入核函数(Grid-strided loop)
由于在循环中频繁的调用核函数具有巨大的性能开销,因此可以将循环放入核函数中:
1 | template<typename T> |
分析后结果如下图:

同时使用 nsys 分析:
1 | ** CUDA API Summary (cuda_api_sum): |
可以看到 cudaLaunchKernel 的确少了很多!
这说明:
核函数的发射数量减少,因此总体执行时间降低!
3、CUDA并行加法性能评估(加速比)
指标:
加速比 = Tcpu / Tgpu
其中:
- T_cpu 是任务在 CPU 上的执行时间;
- T_gpu 是任务在 GPU 上的执行时间;
理想加速比与实际加速比
- 理想加速比:当任务完全并行化且没有任何开销时,加速比等于处理器核心数之比。例如,一个具有 1000 个 CUDA 核心的 GPU 理论上可以实现 1000 倍的加速(相对于单核 CPU)。
- 实际加速比:由于以下因素,实际加速比通常远低于理想值:
- 任务中存在无法并行化的串行部分
- 数据在 CPU 和 GPU 之间的传输开销
- 线程同步和内存访问延迟
- 算法在 GPU 架构上的效率低下
为什么<<<1,1>>> 比 CPU慢?
这是由于,单个 GPU 的核心实际上要比 CPU 的能力要弱!
实际上,GPU 是由于干活的人多,所以快!
4、CUDA并行加法性能评估(总耗时)
实际上观察 nsys 的输出结果:
1 | ** CUDA GPU Kernel Summary (cuda_gpu_kern_sum): |
总体的耗时应当是三个部分:
- 总耗时 = TH2D + Tkernel + TD2H
并且,对于 <<<256,256>>> 来说:HtoD 和 DtoH 的耗时会远大于 kernel 的运行时间!
这就说明,来回的移动和复制数据比计算更消耗时间!
能否对这一部分进行优化呢?
后面的文章中会讲解!
(四)、设备信息
对于 GPU 而言:越多的线程 => 越大的并行度 => 越好的性能
GPU 最大可以启动的线程数可以参考:
- 官网查询硬件:https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#features-and-technical-specifications
- 代码动态获取(cudaDeivceProp):https://docs.nvidia.com/cuda/cuda-runtime-api/index.html
也可以参考:
1、cudaDeivceProp
重点的几个参数:
- maxGridSize(int[3]):x、y、z三个方向分别最多可支持的 block 数;
- maxBlockSize(int[3]):每个 Block中x、y、z三个方向分别最多可支持的线程数;
- maxThreadsPerBlock(int):每个 block 中最多可有的线程数
其中:Blocksize 需同时满足这两组条件:maxBlockSize、maxThreadsPerBlock:
- x、y、z加起来不超过:maxThreadsPerBlock;
- x、y、z各个方向不超过:maxBlockSize;
2、CUDA版本
查看 CUDA 版本使用:
1 | # CUDA版本 |
可以看到 CUDA 为 12.1!
而 nvidia-smi 命令输出的是:驱动支持的的最高版本,而非实际正在使用的版本!
1 | Tue Jul 29 09:30:09 2025 |
可以看到,最高支持 12.4!
后记
更加详细的内容,可以看:
附录
源代码:
参考文章:
- https://beta.infinitensor.com/camp/summer2025/stage/1/course/cuda-programming
- https://qiankunli.github.io/2025/03/22/cuda.html#simd%E5%92%8Csimt
- https://www.nvidia.cn/docs/IO/51635/NVIDIA_CUDA_Programming_Guide_1.1_chs.pdf