CUDA编程入门指南
CUDA 是连接 AI 算法与 GPU 硬件的桥梁,负责把高层的数学计算翻译成 GPU 能最高效执行的机器指令。本文从编程模型、内存模型讲起,到 Reduce/GEMM/Softmax 三大经典算子的实现与优化,再到 FlashAttention 系列 Attention 算子和 Triton 编译器,系统覆盖 AI Infra 从业者需要掌握的 CUDA 编程基础。
📑 目录
- 1. 环境配置
- 2. CUDA 编程模型
- 3. 内存模型
- 4. 关键概念
- 5. 第一个完整程序:向量加法
- 6. 常见算子实现与优化
- 7. Attention 算子
- 8. AI 编译器
- 9. 性能分析工具
- 10. 自我检验清单
- 参考资料
1. 环境配置
1.1 硬件要求
- NVIDIA GPU(Compute Capability >= 7.0,即 Volta 架构及以上)
- 建议至少 8GB 显存用于算子开发和调试
1.2 软件安装
安装 CUDA Toolkit
1 | # Ubuntu 22.04 |
验证安装
1 | nvcc --version # 查看 CUDA 编译器版本 |
1.3 编译基础
CUDA 程序使用 .cu 扩展名,用 nvcc 编译:
1 | # 基础编译 |
2. CUDA 编程模型
2.1 核函数(Kernel)
核函数是在 GPU 上执行的函数,使用 __global__ 关键字声明。调用时通过 <<<gridDim, blockDim>>> 语法指定并行规模:
1 | // 声明核函数 |
CUDA 有三种函数修饰符:
| 修饰符 | 执行位置 | 调用方 | 说明 |
|---|---|---|---|
__global__ |
GPU | CPU(或 GPU) | 核函数,启动 GPU 并行执行 |
__device__ |
GPU | GPU | 设备函数,只能被核函数或其他设备函数调用 |
__host__ |
CPU | CPU | 普通 CPU 函数(默认,可省略) |
__host__ 和 __device__ 可以同时使用,让编译器为 CPU 和 GPU 各生成一份代码。
2.2 Grid / Block / Thread 三层线程层级
CUDA 将线程组织为三层结构,这是理解并行编程的关键。可以把它比喻为”学校 / 班级 / 学生”——Grid 是整个学校,Block 是一个班级,Thread 是班里的每个学生,每个学生独立做自己那份作业,但同一个班级的学生可以通过”黑板”(Shared Memory)互相交流。
1 | Grid(网格)—— 一次 kernel 启动的所有线程 |
维度与索引
Grid 和 Block 都支持 1D、2D、3D 维度。全局线程索引的计算方式:
1 | // 1D 情况(最常用) |
设计约束
| 参数 | 限制 |
|---|---|
| Block 内线程数 | 最多 1024 |
| Grid 每个维度的 Block 数 | 最多 2^31 - 1 (x), 65535 (y, z) |
| 每 SM 活跃线程数 | 最多 2048(64 个 Warp) |
| 每 SM 活跃 Block 数 | 最多 32 |
Block Size 选择经验
- 256:大多数场景的最佳起点(8 个 Warp)
- 128:访存密集型任务,需要更多 Block 并发
- 512-1024:计算密集型任务,注意寄存器和共享内存压力
- 始终是 32 的倍数:与 Warp 大小对齐,避免浪费
2.3 内存管理
CUDA 需要显式管理 CPU(Host)和 GPU(Device)之间的内存:
1 | float* d_data; // d_ 前缀表示 device 内存 |
异步传输与 Stream
默认情况下,cudaMemcpy 是同步的(会阻塞 CPU)。使用 Stream 可以实现异步传输和计算重叠:
1 | cudaStream_t stream; |
使用异步传输时,CPU 端内存必须是锁页内存(Pinned Memory),否则传输速度会大幅下降:
1 | float* h_pinned; |
2.4 错误处理
CUDA API 调用可能失败,生产代码中必须检查错误:
1 |
|
3. 内存模型
CUDA 的内存层次是性能优化的核心。**”内存访问模式决定运行速度”**——这是 CUDA 编程最重要的直觉。
3.1 存储层次总览
1 | 寄存器 (Registers) ← 最快,每线程私有 |
3.2 寄存器(Registers)
寄存器是最快的存储,kernel 中的局部变量默认分配在寄存器中:
1 | __global__ void kernel(float* data, int n) { |
注意事项:
- 每个 SM 的寄存器总量有限(H100 每 SM 65536 个 32-bit 寄存器)
- 每个线程用的寄存器越多,SM 上能同时运行的线程就越少(影响 Occupancy)
- 寄存器用完会”溢出”到 Local Memory(实际是 HBM),速度骤降
查看寄存器使用:
1 | nvcc -Xptxas -v kernel.cu |
3.3 共享内存(Shared Memory)
共享内存是程序员可控的片上高速缓存,相当于同一个 Block 内线程共享的”黑板”——任何一个线程往上面写了东西,同 Block 的其他线程都能看到,而且读写速度比全局内存快得多。它的典型用途是缓存从全局内存加载的数据,供 Block 内线程重复使用。
1 | __global__ void sharedMemDemo(float* input, float* output, int n) { |
动态共享内存——大小在 kernel 启动时指定:
1 | extern __shared__ float dynamic_smem[]; |
3.4 全局内存(Global Memory / HBM)
全局内存就是”显存”,容量最大但速度最慢。cudaMalloc 分配的内存、kernel 参数中的指针都指向全局内存。
合并访问(Coalesced Access) 是全局内存优化的黄金法则:同一个 Warp 内的 32 个线程应该访问连续的内存地址,这样硬件可以将多次访问合并为少量内存事务。打个比方,合并访问就像一排人依次从传送带上拿东西,一次就能拿完;如果每个人跳着拿,传送带要来回好多次,效率大打折扣。
1 | // 好:合并访问——相邻线程访问相邻地址 |
数据布局优化——AoS 转 SoA:
1 | // AoS (Array of Structures) —— 对 GPU 不友好 |
3.5 常量内存(Constant Memory)
适合所有线程读取相同值的场景(广播模式),有专用缓存:
1 | __constant__ float coefficients[256]; // 最大 64KB |
3.6 内存模型总结
| 存储类型 | 位置 | 作用域 | 速度 | 容量 | 程序员控制 |
|---|---|---|---|---|---|
| 寄存器 | 片上 | 线程私有 | 最快 | 每线程 ~255 个 | 自动分配 |
| 共享内存 | 片上 | Block 共享 | 极快 | 每 SM ~228KB | 手动管理 |
| L1/L2 Cache | 片上 | 自动 | 快 | L2 ~50MB | 自动(可提示) |
| 全局内存 | HBM | 所有线程 | 慢 | 80-192GB | 手动管理 |
| 常量内存 | HBM + 缓存 | 所有线程(只读) | 广播时快 | 64KB | 手动管理 |
4. 关键概念
4.1 Warp
Warp 是 GPU 执行的最小调度单位,由 32 个连续线程 组成。同一个 Warp 内的线程在同一时刻执行相同的指令(SIMT)。可以把 Warp 想象成一排 32 个士兵齐步走,步调必须一致——如果有人要向左转、有人要向右转,就只能先让一拨人转完,再让另一拨人转,效率减半。
1 | Block (256 threads) |
Warp Divergence(分支发散)
当 Warp 内的线程走不同的 if/else 分支时,两个分支必须串行执行,导致性能减半:
1 | // 坏:Warp 内一半线程走 if,一半走 else |
Warp 级原语
Warp 内线程可以直接交换数据,无需共享内存:
1 | // Warp Shuffle:线程间直接交换寄存器值 |
4.2 Bank Conflict
共享内存被分为 32 个 Bank,每个 Bank 宽度为 4 字节。同一 Warp 内的不同线程如果访问同一 Bank 的不同地址,就会产生 Bank Conflict,访问变为串行。可以把 Shared Memory 的 32 个 Bank 想象成银行的 32 个柜台,如果多个线程同时排到同一个柜台,就得排队等候;理想情况是每个线程各去一个柜台,大家同时办完。
1 | __shared__ float smem[32][32]; |
检测 Bank Conflict
1 | ncu --metrics l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum ./app |
4.3 Occupancy
Occupancy = 实际活跃 Warp 数 / SM 最大 Warp 数。它反映了 GPU 并行度的利用程度。
影响 Occupancy 的三个因素:
| 因素 | 影响 | 调优方向 |
|---|---|---|
| 每线程寄存器数 | 寄存器多 → 每 SM 能容纳的线程少 | 减少局部变量、控制循环展开 |
| 每 Block 共享内存 | 共享内存多 → 每 SM 能容纳的 Block 少 | 合理分配,避免浪费 |
| Block 大小 | 太小或太大都不好 | 256 是常用起点 |
1 | // 让编译器帮你计算最优 Block Size |
重要提醒:Occupancy 不是越高越好。对于 Tensor Core 密集型 kernel,25-50% 的 Occupancy 就可能达到峰值性能。先保证没有寄存器溢出,再考虑 Occupancy。
5. 第一个完整程序:向量加法
把前面的知识串起来,写一个完整的向量加法程序:
1 |
|
编译运行:
1 | nvcc -arch=sm_80 vector_add.cu -o vector_add && ./vector_add |
6. 常见算子实现与优化
掌握以下三个经典算子的实现与优化,是 CUDA 编程的必修课。它们覆盖了归约、矩阵乘法、归一化三种最基础的计算模式。
6.1 Reduce(并行归约)
Reduce 是将一组数据聚合为一个值(如求和、求最大值)的操作。它是理解 CUDA 并行思维的最佳入门。
朴素实现
1 | // 树形归约:每轮一半线程退出 |
优化 1:消除 Bank Conflict
朴素版本中,前半部分线程在低地址,后半部分在高地址,不同步长下访问模式不同。可以改为交错访问:
1 | // 交错归约:减少 Bank Conflict |
优化 2:Warp Shuffle 避免最后几轮同步
当归约到最后 32 个元素(1 个 Warp)时,不需要 __syncthreads():
1 | // 最后 Warp 用 Shuffle 归约 |
优化 3:每个线程先做一轮累加
让每个线程加载并累加多个元素,减少 Block 数量和全局内存写回次数:
1 | __global__ void reduceV3(float* input, float* output, int n) { |
6.2 GEMM(矩阵乘法)
GEMM(General Matrix Multiply)是大模型的计算核心——线性层和 Attention 本质上都是矩阵乘法。
朴素实现
1 | // C = A * B,A: MxK, B: KxN, C: MxN |
问题:每个线程独立从全局内存读取 A 的一行和 B 的一列,大量重复读取。
优化 1:Shared Memory Tiling(分块)
核心思想:将大矩阵分成小块(Tile),每次将一个 Tile 加载到 Shared Memory 中,Block 内所有线程共享使用。这就像搬家时家具太多一次搬不完,于是分批搬到桌上再整理——每批数据搬进快速的 Shared Memory 后,所有线程可以反复使用,避免每次都回慢速的全局内存去取。
1 |
|
为什么分块有效?
- 不分块:每个元素从 HBM 读取 K 次,总数据搬运 = MNK
- 分块后:每个 Tile 从 HBM 读 1 次,在 Shared Memory 中被复用 TILE_SIZE 次
- 数据复用率提升 TILE_SIZE 倍
更高级的优化方向:
| 优化技术 | 效果 | 说明 |
|---|---|---|
向量化加载 (float4) |
提升 HBM 带宽利用率 | 一条指令加载 16 字节 |
| 寄存器分块 | 每线程计算多个输出元素 | 减少 Shared Memory 访问次数 |
| 双缓冲(Double Buffering) | 加载与计算重叠 | 一个 Tile 计算时预加载下一个 |
| Tensor Core (WMMA/CUTLASS) | 数量级提升 | 硬件矩阵乘加速 |
实际工程中,一般直接使用 cuBLAS(NVIDIA 官方高度优化的 BLAS 库)或 CUTLASS(可定制的模板库):
1 |
|
6.3 Softmax
Softmax 是 Attention 中的核心操作。高效的 Softmax 实现需要解决数值稳定性和并行归约两个问题。
数学公式
1 | softmax(x_i) = exp(x_i - max(x)) / sum(exp(x_j - max(x))) |
减去 max(x) 是为了数值稳定性,防止 exp 溢出。
朴素实现(两趟)
1 | // 第一趟:求 max 和 sum |
Online Softmax(一趟)
NVIDIA 提出的 Online Normalizer Calculation 方法,可以在一趟遍历中同时维护 max 和 sum,减少一次全局内存读取:
1 | // 核心思想:在线更新 max 和 sum |
这个技巧是 FlashAttention 的数学基础之一。
6.4 算子融合(Kernel Fusion)
算子融合是将多个小操作合并为一个 kernel 执行,避免中间结果写回全局内存。就像做菜时一次洗好所有菜,而不是做一道菜洗一次——每次写回全局内存再读回来,就像反复跑去水龙头前洗菜,白白浪费时间在”搬运”上。
1 | 未融合: |
对于 memory bound 的逐元素操作(ReLU、Add、LayerNorm),融合可以获得数倍加速,因为瓶颈在 HBM 读写而不是计算。
7. Attention 算子
Attention 是 Transformer 的核心操作,也是大模型计算量和显存占用的主要来源。理解 Attention 算子的优化是 AI Infra 的必修课。
7.1 标准 Attention 的问题
标准 Attention 的计算公式:
1 | Attention(Q, K, V) = softmax(Q @ K^T / sqrt(d)) @ V |
计算流程中需要存储完整的 QK^T 矩阵(大小为 seq_len x seq_len),对于长序列场景,这个矩阵巨大。以 seq_len = 8192、batch = 32 为例:
1 | QK^T 矩阵大小 = 32 * 128 * 8192 * 8192 * 2 bytes (FP16) ≈ 128 GB |
这远超单卡 80GB 显存。即使显存够用,反复在 HBM 和 SRAM 之间搬运这个巨大矩阵也会严重拖慢速度。
7.2 FlashAttention
FlashAttention 的核心思想:通过 Tiling(分块)避免在 HBM 中存储完整的 QK^T 矩阵,将所有中间计算保持在 Shared Memory 中。
1 | 标准 Attention: |
关键技术:
- Tiling:将 Q、K、V 分成小块,每块能装进 Shared Memory
- Online Softmax:在分块计算中正确维护 softmax 的全局 max 和 sum
- 重计算(Recomputation):反向传播时不存储中间 Attention 矩阵,而是重新计算(用计算换显存)
IO 复杂度对比:
| 方法 | HBM 访问量 | 额外显存 |
|---|---|---|
| 标准 Attention | O(N^2 * d) | O(N^2) |
| FlashAttention | O(N^2 * d^2 / SRAM_SIZE) | O(N) |
对于 d=128、SRAM=192KB 的典型配置,FlashAttention 的 HBM 访问量减少 ~8 倍。
7.3 FlashAttention V2
V2 在 V1 基础上进一步优化了并行策略:
- 前向传播:在序列长度维度(而非 batch/head 维度)并行,提升长序列效率
- 反向传播:减少非矩阵乘法操作的比例,更好地利用 Tensor Core
- Block 划分:Q 和 K/V 用不同的 Block 大小,更适配硬件
实测 V2 比 V1 快 ~2 倍,达到 A100 理论峰值的 50-73%。
7.4 FlashAttention-3
针对 Hopper 架构(H100)的进一步优化:
- 利用 TMA(Tensor Memory Accelerator):硬件加速的异步数据搬运
- WGMMA 指令:Warp Group 级别的矩阵乘加速
- Ping-Pong 流水线:生产者-消费者模型,搬数据和算数据完全重叠
7.5 Flash-Decoding
FlashAttention 主要优化 Prefill 阶段(Q 矩阵很大)。Decode 阶段 Q 只有一个 token,瓶颈不同。
Flash-Decoding 的核心思想:将 KV Cache 沿序列维度分割,多个 Block 并行计算 Attention,最后归约合并。
1 | 标准 Decode Attention: |
7.6 FlashInfer
FlashInfer 是一个面向 Serving 场景的可定制 Attention 引擎:
- 支持 PagedAttention(vLLM 风格的分页 KV Cache)
- 支持多种 KV 布局(连续、分页、分块)的可组合格式
- 支持 KV Cache 量化(FP8、INT4)
- 支持 Prefill 和 Decode 的统一接口
7.7 PagedAttention
vLLM 提出的 PagedAttention 将操作系统的虚拟内存分页思想引入 KV Cache 管理:
- KV Cache 不再要求连续内存,而是分成固定大小的”页”
- 页可以按需分配和回收,消除内存碎片
- 不同请求可以共享相同的 KV Cache 页(如共享的 system prompt)
8. AI 编译器
手写 CUDA kernel 门槛高、调试难。AI 编译器正在降低高效 GPU 编程的门槛。
8.1 Triton
Triton 是 OpenAI 开源的 GPU 编程语言,使用 Python 语法编写 GPU kernel,编译器自动处理内存合并、共享内存管理、Warp 调度等底层细节。
Triton vs CUDA 对比:以向量加法为例
1 | import triton |
Triton 的 Softmax 实现
1 |
|
Triton 的优势:
- Python 语法,学习曲线平缓
- 编译器自动处理内存合并、共享内存 Tiling、Warp 调度
- 性能可以达到手写 CUDA 的 80-95%
- FlashAttention 的原始实现就使用了 Triton
8.2 torch.compile
PyTorch 2.x 引入了 torch.compile,可以自动将 PyTorch 代码编译为高效的 GPU 代码:
1 | import torch |
torch.compile 的后端链路:
1 | PyTorch 代码 → TorchDynamo(图捕获)→ TorchInductor(代码生成)→ Triton kernel |
Graph Break:当 torch.compile 遇到无法编译的操作(如 Python 副作用、动态控制流),会”打断”计算图,降低优化效果。排查方法:
1 | TORCH_LOGS="graph_breaks" python your_script.py |
8.3 TVM / XLA
| 编译器 | 核心特点 | 适用场景 |
|---|---|---|
| TVM (Apache) | 可移植的张量编译器,自动搜索最优配置 | 需要跨硬件平台的推理优化 |
| XLA (Google) | JAX/TensorFlow 的 JIT 编译器 | JAX 生态,TPU 优化 |
| Triton (OpenAI) | Python 化的 GPU 编程 | 自定义算子开发 |
| TorchInductor | PyTorch 原生编译器 | PyTorch 生态的整图优化 |
9. 性能分析工具
写出能跑的 kernel 只是第一步,知道瓶颈在哪 才是优化的关键。
9.1 Nsight Compute(Kernel 级分析)
1 | # 对指定 kernel 做详细分析 |
核心指标:
| 指标 | 含义 | 目标 |
|---|---|---|
| Compute (SM) Throughput | 计算单元利用率 | Compute bound 时应 >70% |
| Memory Throughput | HBM 带宽利用率 | Memory bound 时应 >70% |
| Achieved Occupancy | 活跃 Warp 占比 | >25%(非越高越好) |
| Warp Execution Efficiency | 分支发散程度 | >85% |
| L2 Hit Rate | L2 缓存命中率 | >70% |
| Shared Memory Bank Conflicts | Bank 冲突次数 | 0 |
9.2 Nsight Systems(系统级分析)
1 | nsys profile -o timeline ./your_app |
用来分析全局时序:
- Kernel 之间是否有 CPU 端空隙(launch overhead)
- 数据传输是否与计算重叠
- 多 Stream 是否真正并发
- 瓶颈在 Host 端还是 Device 端
9.3 编译器输出
1 | # 查看寄存器和共享内存使用 |
10. 自我检验清单
完成本文学习后,你应该能够:
- 能解释 Grid → Block → Thread 的三层结构,并根据数据规模配置合适的 Block 大小
- 能区分 GPU 的 5 种内存类型(寄存器、Local、Shared、Global、Constant),并说明各自的作用域和生命周期
- 能解释什么是 Warp、合并访存(Coalesced Access)和 Bank Conflict
- 能编写一个基本的 CUDA Kernel(如向量加法),并用 nvcc 编译运行
- 能独立编写一个正确的 Reduce kernel,并做至少两轮优化(Warp Shuffle + 多元素累加)
- 能实现 Tiled GEMM 并解释为什么 Tiling 能减少全局内存访问
- 能写出 Online Softmax 的算法流程,解释为什么它比 Naive Softmax 更好
- 能解释 FlashAttention 的核心思想(Tiling + Online Softmax + 不存中间矩阵)
- 能使用 Triton 编写一个简单的 kernel(如向量加法或 Softmax),并与 PyTorch 结果对比
- 能用 Nsight Compute 分析自己写的 kernel,判断是 memory bound 还是 compute bound
📚 参考资料
- NVIDIA CUDA C++ Programming Guide
- NVIDIA CUDA C++ Best Practices Guide
- Nsight Compute Documentation
- Nsight Systems User Guide
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness
- FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning
- FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision
- Flash-Decoding for long-context inference - Stanford CRFM
- FlashInfer: Efficient and Customizable Attention Engine for LLM Inference Serving
- FlashInfer - GitHub
- vLLM: Efficient Memory Management for Large Language Model Serving with PagedAttention
- Online normalizer calculation for softmax
- Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations
- Triton Tutorials
- CUTLASS - GitHub
- 猛猿:图解FlashAttention V1/V2 系列
- 方佳瑞:深入浅出理解PagedAttention CUDA实现
- 猛猿:从啥也不会到CUDA GEMM优化