GPU × CUDA × Memory

显卡内存结构和 CUDA 基本理解

这页只讲一件事:GPU 为什么快,以及你写 CUDA 时到底在跟谁打交道。核心不是“会写 kernel”这么简单,核心是你得同时理解 计算核心怎么并行数据在内存层级里怎么流动。大多数 CUDA 优化,最后都能还原成一句话:让更多线程持续吃到数据,并且别让慢内存把快核心饿死。

从 SM / Warp / Core 讲起
从寄存器到 HBM 全部串起来
解释 CUDA 为什么这样设计
给出能真正提速的使用原则

先把结论打穿:GPU 快,不是因为“单核猛”,而是因为“同时开很多工”

CPU 的思路

少量复杂核心,擅长分支、缓存、乱序执行、低延迟响应。它追求的是:一个线程尽量快

所以 CPU 很适合操作系统、数据库调度、复杂逻辑、强分支代码。

GPU 的思路

大量相对简单的计算单元,追求高吞吐。它不想把一个任务做得特别快,而是想:一次把成千上万个相似任务一起做完

所以 GPU 特别适合矩阵乘、卷积、向量加法、attention、图像处理这类规则并行工作。

直觉版理解:CPU 像几位很强的老师傅,GPU 像一整个流水线车间。你要是让车间里每个人都做同样动作,吞吐会夸张;你要是让每个人都临场决策、各干各的,GPU 反而容易卡住。

GPU 计算核心:不是一坨“核”,而是一套分层调度系统

一个简化版 GPU 视图

GPU 由很多个 SM 组成,还连着大显存 HBM / GDDR
SM(Streaming Multiprocessor) 是 CUDA 真正工作的主战场。一个 SM 里有调度器、寄存器文件、shared memory / L1、若干执行单元
Warp 是调度基本单位,通常是 32 个线程一起走
Thread 是你在 CUDA 代码里写的那个线程,它看到自己的 threadIdx
CUDA Core / Tensor Core 真正干算术活的执行单元,负责 FMA、矩阵块运算等

最容易搞错的点

1. CUDA core 不等于 CPU core。
GPU 的所谓“核心”更像一组算术执行单元,不是一个带复杂控制逻辑的独立���脑。

2. 线程不是独立飞行。
硬件按 warp 执行。一个 warp 里 32 个线程通常同一拍执行同一条指令。

3. SM 才是资源竞争中心。
寄存器、shared memory、可驻留的 block 数量,都会限制并发度。

层级 它负责什么 你写 CUDA 时该关心什么
GPU 全局吞吐、显存带宽、SM 数量、L2 容量 问题规模够不够大,是否能把卡喂满
SM 执行 block,调度 warp,管理片上资源 occupancy、寄存器压力、shared memory 使用量
Warp 实际指令发射单位 避免 warp divergence,按 32 线程思考访存与分支
Thread 处理单个元素或小块任务 索引映射是否连续,是否越界,是否负载均衡

内存架构:CUDA 性能很多时候不是算出来的,是“搬出来的”

GPU 的麻烦在于:算得飞快,但最慢的是取数据。于是整个架构被设计成多层内存金字塔,越靠近执行单元越快、越小、越贵。

从快到慢的内存层级

寄存器 Register 每线程私有,最快,容量很小。临时变量最好待这里。
Shared Memory / L1 一个 block 内线程共享,延迟低,适合数据复用、tile、局部交换。
L2 Cache 整个 GPU 多个 SM 共享,承接对显存的访问。
Global Memory 就是大显存,容量大但延迟高。大部分 tensor 都在这里。
Host Memory CPU 内存。CPU 和 GPU 之间拷贝代价很高,PCIe/NVLink 是另一层瓶颈。

每层内存到底怎么理解

寄存器:最快,但你一旦用太多寄存器,每个 SM 能同时挂的线程就会下降,occupancy 掉下去。

Shared memory:CUDA 优化最常用武器。把会重复用的数据先搬进来,让一个 block 里多个线程反复消费。

Global memory:容量最大,也是你最容易被坑的地方。访问模式乱,带宽就被浪费;访问模式整齐,硬件才能合并事务。

Host ↔ Device:很多“GPU 没提速”的真凶不是 kernel 慢,而是数据来回拷贝把收益吃光了。

内存 作用 优点 典型坑
Register 线程局部临时值 最快 用太多会压低并发
Shared Memory block 内共享缓存 低延迟,可显式控制 bank conflict、容量有限、同步开销
L2 GPU 级缓存 减少显存访问压力 不可完全依赖,命中模式受 workload 影响大
Global Memory 主显存 高延迟,乱访问浪费带宽
Host Memory CPU 内存 CPU 侧处理方便 跨总线传输慢
一个经典原则:如果一个值会被多个线程反复使用,就想办法让它离计算更近。 这就是 tile、shared memory、cache blocking、KV cache、权重预取这些优化思想背后的共同逻辑。

为什么“连续访存”这么重要

合并访存 coalescing

一个 warp 里 32 个线程如果访问的是连续地址,硬件就能把这些小访问合并成少量大事务。这样带宽利用率高,延迟也更好隐藏。

如果 32 个线程东一榔头西一棒子地取数据,显存控制器就得处理一堆碎片事务,性能直接塌。

分支发散 divergence

同一个 warp 里如果线程走不同分支,硬件常常只能先执行一部分线程,再执行另一部分线程,等于串行化。

所以 GPU 喜欢规则、整齐、批量化的工作,不喜欢“一堆线程各有主意”。

CUDA 执行模型:你写的是 kernel,硬件看到的是 grid → block → warp → thread

1
Host 端准备数据
CPU 分配内存、把数据拷到 GPU、配置 kernel 启动参数。
2
启动 kernel
你写 kernel<<<grid, block>>>(...),把任务切成很多 block。
3
Block 被分配到不同 SM
每个 block 只会待在一个 SM 上运行,但一个 SM 可以同时驻留多个 block。
4
Block 再拆成 warp
硬件按 warp 发射指令,warp 之间交替执行,用来隐藏访存延迟。
5
线程各自算自己的数据
每个线程根据 blockIdxthreadIdx 算出全局索引,处理一个元素或一个 tile 的一部分。

最小 kernel 例子

__global__ void vec_add(const float* a, const float* b, float* c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { c[i] = a[i] + b[i]; } } int threads = 256; int blocks = (n + threads - 1) / threads; vec_add<<<blocks, threads>>>(a, b, c, n);

这个例子里真正发生了什么

threads = 256:一个 block 有 256 个线程,也就是 8 个 warp。

blocks:总任务量除以每个 block 能处理的数量。

i:把二维层级信息压成一个全局一维索引。

if (i < n):防越界。最后一个 block 往往不会正好填满。

CUDA 为什么这样设计

原因 1:让程序员描述并行

你只要说“有多少线程、每个线程干什么”,硬件负责把 block 调到各个 SM 跑。这样同一段代码可以映射到不同代 GPU。

原因 2:让硬件隐藏延迟

一个 warp 等显存时,SM 可以切去执行另一个就绪 warp。GPU 不是靠单线程低延迟赢,而是靠大量并发把等待时间盖掉。

原因 3:鼓励局部协作

block 内线程能共享 shared memory,还能同步;block 之间默认独立,方便大规模扩展和调度。

CUDA 怎么用好:把优化问题压缩成 6 个硬原则

A
先判断你是算力受限还是带宽受限
如果算得少、搬得多,重点是访存;如果矩阵乘这类算得很重,重点是 Tensor Core、tile、数据布局。
B
让 global memory 访问尽量连续
索引设计优先保证 coalescing。线程编号和数据排布要对齐,不然显存带宽白白漏掉。
C
用 shared memory 做数据复用
凡是一个 block 内多个线程都要反复读的值,都值得考虑先搬到 shared memory。
D
控制寄存器和 shared memory 的资源占用
资源用太猛,SM 同时能驻留的 warp 变少,延迟反而藏不住。occupancy 不是越高越好,但太低通常危险。
E
避免 warp divergence
把大量分支判断移到 kernel 外,或者让一个 warp 内线程尽量走同一路。
F
减少 Host ↔ Device 拷贝
一次性把更多工作留在 GPU 上,用 stream、异步拷贝、pipeline 重叠传输和计算,别跑一个小 kernel 就回 CPU 一趟。

把这些原则映射到真实工作负载

场景 真正卡在哪 常见 CUDA 思路
向量加法、简单逐元素算子 通常是显存带宽 连续访存、足够大 batch、kernel fusion,别让 launch 开销和拷贝开销盖过计算
矩阵乘 / GEMM 算力 + 数据搬运同时重要 tile、shared memory、寄存器 blocking、Tensor Core、布局对齐
卷积 数据复用和访存模式 im2col / direct conv / Winograd,核心还是提高局部性
LLM attention / KV cache 显存容量、带宽、cache 命中 paged KV、连续内存布局、prefill/decode 分治、减少无效搬运
稀疏或强分支任务 warp 发散、负载不均 重排数据、分桶、压缩表示,尽量把相似工作分到一起

一张图记住 CUDA 的本质

CPU 把任务切块 ↓ Grid / Block 定义并行形状 ↓ SM 接住多个 Block ↓ Warp 作为真正调度单位轮流执行 ↓ Core / Tensor Core 真正算 ↓ 性能上限常由“数据能不能及时送到”决定

所以,CUDA 编程不是“写一堆线程”这么简单,而是:把算法改写成适合 GPU 吞吐机器的形状,再把数据放到对的位置。