随着人工智能的发展,科学计算(尤其是矩阵/张量计算)越来越重要;因此,基于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
#include <cmath>
#include <iostream>
#include <vector>
// Step 2: Define add function
void add_cpu(std::vector<float> &c, const std::vector<float> &a, const std::vector<float> &b) {
// CPU use loop to calculate
for (size_t i = 0; i < a.size(); i++) {
c[i] = a[i] + b[i];
}
}
int main() {
// Step 1: Prepare & initialize data
constexpr size_t N = 1 << 20; // ~1M elements
// Initialize data
const std::vector<float> a(N, 1);
const std::vector<float> b(N, 2);
std::vector<float> c(N, 0);
// Step 3: Call the cpu addition function
add_cpu(c, a, b);
// Step 4: Check for errors (all values should be 3.0f)
float maxError = 0.0f;
for (int i = 0; i < N; i++) {
maxError = fmax(maxError, fabs(c[i] - 3.0f));
}
std::cout << "Max error: " << maxError << std::endl;
}
主要分为以下几个步骤:
- 准备和初始化数据;
- 定义加法函数
- 靠循环来进行所有的元素加法
- 调用函数
- 验证结果
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
// Step 1: Prepare & initialize data
constexpr size_t N = 1 << 20; // ~1M elements
constexpr size_t size_bytes = sizeof(float) * N;
// Initialize data
const std::vector<float> h_a(N, 1);
const std::vector<float> h_b(N, 2);
std::vector<float> h_c(N, 0);
此时在 Host 端的 RAM 分配内存;
(3)数据传输到 GPU
数据传输到 GPU 使用 CUDA 提供的函数:
- 使用
cudaMalloc
分配显存; - 使用
cudaMemcpy
复制数据;
add_cuda.cu
float *d_a, *d_b, *d_c;
CUDA_CHECK(cudaMalloc(&d_a, size_bytes));
CUDA_CHECK(cudaMalloc(&d_b, size_bytes));
CUDA_CHECK(cudaMalloc(&d_c, size_bytes));
CUDA_CHECK(cudaMemcpy(d_a, h_a.data(), size_bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_b, h_b.data(), size_bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_c, h_c.data(), size_bytes, cudaMemcpyHostToDevice));
这里使用了:
CUDA_CHECK
宏进行校验;cudaMemcpyHostToDevice
指定数据流方向;
CUDA_CHECK 宏定义如下:
#define CUDA_CHECK(call) \
{ \
cudaError_t err = call; \
if (err != cudaSuccess) { \
std::cerr << "CUDA Error at " << __FILE__ << ":" << __LINE__ \
<< " - " << cudaGetErrorString(err) << std::endl; \
} \
}
(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 加法函数;
层级结构定义:
// Set up kernel configuration
dim3 block_dim(256);
dim3 grid_dim((N + block_dim.x - 1) / block_dim.x);
核函数定义:
template<typename T>
__global__ void add_kernel(T *c, const T *a, const T *b, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
只能通过指针的方式传递!
因为像是 vector 等数据结构,都是在 Host 端定义的,并不能在 Global Memory 中分配!
核函数调用:
// Call cuda add kernel
add_kernel<<<grid_dim, block_dim>>>(d_c, d_a, d_b, N);
其中:
dim3
:CUDA 表示线程层级结构的类型(包括:x、y、z 三个维度);<<<>>>
:传递线程层级信息给核函数;核函数(kernel)
:设备侧的入口函数;__global__
:表示这是个核函数;blockldx
:表示 block 的编号,第几个 block;blockDim
:表示 block 的大小,一个 block 多少个线程;threadldx
:表示 thread 的编号,表示第几个 thread;
(5)将 GPU 数据传输回 CPU
同样,使用 cudaMemcpy
:
CUDA_CHECK(cudaMemcpy(h_c.data(), d_c, size_bytes, cudaMemcpyDeviceToHost));
(6)验证结果,释放内存
验证结果使用已经复制到 h_c
中的数据;
释放内存使用 cudaFree
:
add_cuda.cu
float maxError = 0.0f;
for (int i = 0; i < N; i++) {
maxError = fmax(maxError, fabs(h_c[i] - 3.0f));
}
std::cout << "Max error: " << maxError << std::endl;
if (d_a) {
CUDA_CHECK(cudaFree(d_a));
}
if (d_b) {
CUDA_CHECK(cudaFree(d_b));
}
if (d_c) {
CUDA_CHECK(cudaFree(d_c));
}
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)编译命令
通过:
nvcc add_cuda.cu -o add_cuda
即可编译!
运行:
./add_cuda
(三)、GPU性能测试
可以通过:
nvidia-smi
观察 GPU 利用率!
1、并行加法性能对比
分别对比:
- CPU;
<<<1,1>>>
;<<<256,256>>>
;- GPU 满载;
代码如下:
add_cuda_profiling.cu
#include <cmath>
#include <iostream>
#include <vector>
#define CUDA_CHECK(call) \
{ \
cudaError_t err = call; \
if (err != cudaSuccess) { \
std::cerr << "CUDA Error at " << __FILE__ << ":" << __LINE__ \
<< " - " << cudaGetErrorString(err) << std::endl; \
} \
}
// Step 3: Define add kernel
template<typename T>
__global__ void add_kernel(T *c, const T *a, const T *b, const size_t n, const size_t step) {
int idx = blockIdx.x * blockDim.x + threadIdx.x + step;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
template<typename T>
void vector_add(T *c, const T *a, const T *b, size_t n, const dim3& grid_dim, const dim3& block_dim) {
size_t step = grid_dim.x * block_dim.x;
for (size_t i = 0; i < n; i += step) {
add_kernel<<<grid_dim, block_dim>>>(c, a, b, n, i);
}
}
int main() {
// Step 1: Prepare & initialize data
constexpr size_t N = 1 << 20; // ~1M elements
constexpr size_t size_bytes = sizeof(float) * N;
// Initialize data
const std::vector<float> h_a(N, 1);
const std::vector<float> h_b(N, 2);
std::vector<float> h_c(N, 0);
// Step 2: Allocate device memory & transfer to global memory
float *d_a, *d_b, *d_c;
CUDA_CHECK(cudaMalloc(&d_a, size_bytes));
CUDA_CHECK(cudaMalloc(&d_b, size_bytes));
CUDA_CHECK(cudaMalloc(&d_c, size_bytes));
CUDA_CHECK(cudaMemcpy(d_a, h_a.data(), size_bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_b, h_b.data(), size_bytes, cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_c, h_c.data(), size_bytes, cudaMemcpyHostToDevice));
// Step 4: Call the cpu addition function
// Set up kernel configuration
dim3 block_dim(1);
dim3 grid_dim(1);
// Call cuda add kernel
vector_add(d_c, d_a, d_b, N, block_dim, grid_dim);
// Step 5: Transfer data from global mem to host mem
CUDA_CHECK(cudaMemcpy(h_c.data(), d_c, size_bytes, cudaMemcpyDeviceToHost));
// Step 6: Check for errors (all values should be 3.0f)
float sumError = 0.0f;
for (int i = 0; i < N; i++) {
sumError += fabs(h_c[i] - 3.0f);
}
std::cout << "Sum error: " << sumError << std::endl;
if (d_a) {
CUDA_CHECK(cudaFree(d_a));
}
if (d_b) {
CUDA_CHECK(cudaFree(d_b));
}
if (d_c) {
CUDA_CHECK(cudaFree(d_c));
}
}
可以修改其中的:
dim3 block_dim(1); dim3 grid_dim(1);
不同情况下的性能如下:
可以使用 Nsight Systems(nsys,NVIDIA系统级性能分析工具)来分析;
启动 profiling:
nsys profile -t cuda,nvtx,osrt -o add_cuda_profiling -f true ./add_cuda_profiling
解析并统计性能信息:
nsys stats add_cuda_profiling.nsys-rep
** OS Runtime Summary (osrt_sum):
Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ------------- ------------- ---------- ----------- ------------- ----------------------
56.2 7,592,724,284 84 90,389,574.8 100,130,776.0 2,330 370,626,986 45,049,255.4 poll
42.4 5,736,493,727 26 220,634,374.1 189,702,756.5 41,077,614 752,975,386 124,762,585.8 sem_wait
1.2 164,252,099 543 302,490.1 13,509.0 529 111,402,991 4,818,716.4 ioctl
0.1 14,968,499 38 393,907.9 131,267.0 135 5,539,804 890,642.6 pthread_rwlock_wrlock
......
** CUDA API Summary (cuda_api_sum):
Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ------------ ----------- -------- ----------- ------------- ----------------------
96.9 6,504,565,162 1,048,576 6,203.2 5,159.0 2,928 37,814,020 99,097.6 cudaLaunchKernel
3.0 203,141,797 3 67,713,932.3 103,908.0 73,162 202,964,727 117,130,625.1 cudaMalloc
0.1 4,017,591 4 1,004,397.8 1,012,632.0 941,545 1,050,782 45,652.8 cudaMemcpy
0.0 524,788 3 174,929.3 136,182.0 122,785 265,821 78,999.0 cudaFree
0.0 2,584 1 2,584.0 2,584.0 2,584 2,584 0.0 cuModuleGetLoadingMode
......
各个类型 API Summary 分析结果如下:
可以看到:<<<1,1>>>
cudaLaunchKernel 占比非常高这是由于:
核函数调用有开销,在外面多次循环调用开销巨大!
因此,需要进行优化!
2、将循环放入核函数(Grid-strided loop)
由于在循环中频繁的调用核函数具有巨大的性能开销,因此可以将循环放入核函数中:
template<typename T>
__global__ void add_kernel_inner_loop(T *c, const T *a, const T *b, const size_t n, const size_t step) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
for (size_t i = idx; i < n; i += step) {
if (i < n) {
c[i] = a[i] + b[i];
}
}
}
template<typename T>
void vector_add(T *c, const T *a, const T *b, size_t n, const dim3& grid_dim, const dim3& block_dim) {
size_t step = grid_dim.x * block_dim.x;
add_kernel_inner_loop<<<grid_dim, block_dim>>>(c, a, b, n, step);
}
分析后结果如下图:
同时使用 nsys 分析:
** CUDA API Summary (cuda_api_sum):
Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ------------ ----------- -------- ----------- ------------- ----------------------
55.4 204,935,456 3 68,311,818.7 104,741.0 79,097 204,751,618 118,160,333.0 cudaMalloc
44.4 164,057,041 4 41,014,260.3 1,000,521.5 926,775 161,129,223 80,076,651.2 cudaMemcpy
0.2 653,441 3 217,813.7 204,732.0 194,409 254,300 32,016.9 cudaFree
0.1 264,055 1 264,055.0 264,055.0 264,055 264,055 0.0 cudaLaunchKernel
0.0 2,429 1 2,429.0 2,429.0 2,429 2,429 0.0 cuModuleGetLoadingMode
可以看到 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 的输出结果:
** CUDA GPU Kernel Summary (cuda_gpu_kern_sum):
Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name
-------- --------------- --------- ------------- ------------- ----------- ----------- ----------- ---------------------------------------------------------------------------------------------
100.0 160,054,287 1 160,054,287.0 160,054,287.0 160,054,287 160,054,287 0.0 void add_kernel_inner_loop<float>(T1 *, const T1 *, const T1 *, unsigned long, unsigned long)
Processing [add_cuda_profiling2.sqlite] with [/usr/local/cuda-12.1/nsight-systems-2023.1.2/host-linux-x64/reports/cuda_gpu_mem_time_sum.py]...
** CUDA GPU MemOps Summary (by Time) (cuda_gpu_mem_time_sum):
Time (%) Total Time (ns) Count Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Operation
-------- --------------- ----- --------- --------- -------- -------- ----------- ------------------
78.4 2,318,310 3 772,770.0 763,159.0 761,400 793,751 18,191.4 [CUDA memcpy HtoD]
21.6 640,473 1 640,473.0 640,473.0 640,473 640,473 0.0 [CUDA memcpy DtoH]
总体的耗时应当是三个部分:
- 总耗时 = 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 版本使用:
# CUDA版本
nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2023 NVIDIA Corporation
Built on Tue_Feb__7_19:32:13_PST_2023
Cuda compilation tools, release 12.1, V12.1.66
Build cuda_12.1.r12.1/compiler.32415258_0
可以看到 CUDA 为 12.1!
而 nvidia-smi
命令输出的是:驱动支持的的最高版本,而非实际正在使用的版本!
Tue Jul 29 09:30:09 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.15 Driver Version: 550.54.15 CUDA Version: 12.4 |
|-----------------------------------------+------------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+========================+======================|
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 38C P8 10W / 70W | 0MiB / 15360MiB | 0% Default |
| | | N/A |
+-----------------------------------------+------------------------+----------------------+
+-----------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=========================================================================================|
| No running processes found |
+-----------------------------------------------------------------------------------------+
可以看到,最高支持 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