CUDA编程模型与硬件执行层级对应关系
- CUDA编程模型与硬件执行层级
-
- 1) grid size、block size 决定"有多少线程",并给出索引体系 grid size、block size 决定"有多少线程",并给出索引体系)
- 2) block size 如何对应到 warp(一个 block 被切成多个 warp) block size 如何对应到 warp(一个 block 被切成多个 warp))
- 3) grid size 如何对应到 SM(block 被分配到哪些 SM) grid size 如何对应到 SM(block 被分配到哪些 SM))
- 4) warp 和 "宽 SIMD 向量单元"怎么对应 warp 和 "宽 SIMD 向量单元"怎么对应)
- 5) 用你的内置变量,把"block→warp→lane"串起来(1D) 用你的内置变量,把"block→warp→lane"串起来(1D))
- 6) 典型经验:block size 往往取 32 的倍数 典型经验:block size 往往取 32 的倍数)
- 7) 1000×1000 矩阵乘法的理论性能分析 1000×1000 矩阵乘法的理论性能分析)
-
- 理论最小计算时间(理想情况)
-
- [FP32 需要的最短时间:](#FP32 需要的最短时间:)
- [FP16/BF16(Tensor Core)需要的最短时间:](#FP16/BF16(Tensor Core)需要的最短时间:)
- 8) 10000×10000 大规模矩阵乘法的性能分析 10000×10000 大规模矩阵乘法的性能分析)
-
- 1) 显存占用分析(能否放进 4090 24GB 显存?) 显存占用分析(能否放进 4090 24GB 显存?))
- 2) 计算量 计算量)
- 3) 理论极限时间(按峰值算力下界估算) 理论极限时间(按峰值算力下界估算))
-
- [FP32 用时下界:](#FP32 用时下界:)
- [FP16/BF16(Tensor Core)用时下界:](#FP16/BF16(Tensor Core)用时下界:)
CUDA编程模型与硬件执行层级
grid size / block size 是编程模型 的层级;SM (Streaming Multiprocessor) / warp / 宽SIMD (lanes) 是硬件执行层级。它们的对应关系可以按下面这条链理解:
grid → block → warp → lane(类似SIMD通道) \text{grid} \rightarrow \text{block} \rightarrow \text{warp} \rightarrow \text{lane(类似SIMD通道)} grid→block→warp→lane(类似SIMD通道)
1) grid size、block size 决定"有多少线程",并给出索引体系
以 1D 为例,你启动内核:
cpp
kernel<<<gridDimX, blockDimX>>>();
gridDim.x = gridDimX:一共有多少个 blockblockDim.x = blockDimX:每个 block 有多少个线程
线程索引:
blockIdx.x∈ [0,gridDim.x-1]threadIdx.x∈ [0,blockDim.x-1]
全局线性线程 id 常用:
g = b l o c k I d x . x ⋅ b l o c k D i m . x + t h r e a d I d x . x g = blockIdx.x \cdot blockDim.x + threadIdx.x g=blockIdx.x⋅blockDim.x+threadIdx.x
2) block size 如何对应到 warp(一个 block 被切成多个 warp)
在 NVIDIA GPU 上通常:
warpSize = 32- 一个 block 里有多少个 warp:
warpsPerBlock = ⌈ b l o c k D i m . x 32 ⌉ \text{warpsPerBlock} = \left\lceil \frac{blockDim.x}{32} \right\rceil warpsPerBlock=⌈32blockDim.x⌉
并且:
-
warpId(在 block 内) :
w a r p I d = ⌊ t h r e a d I d x . x 32 ⌋ warpId = \left\lfloor \frac{threadIdx.x}{32} \right\rfloor warpId=⌊32threadIdx.x⌋ -
laneId(在线程所在 warp 内的位置) :
l a n e I d = t h r e a d I d x . x m o d 32 laneId = threadIdx.x \bmod 32 laneId=threadIdx.xmod32
所以:
blockDim.x直接决定了一个 block 内"会生成多少个 warp",进而影响调度开销与资源占用。
3) grid size 如何对应到 SM(block 被分配到哪些 SM)
硬件执行时:
- 调度单位是 block:一个 block 会被整体放到某个 SM 上运行(不会把同一个 block 拆到多个 SM)。
- 一个 SM 同时可以驻留多个 block(受寄存器、shared memory、最大线程数等限制)。
gridDim.x决定 block 总数:如果 block 多于 SM 数量,就会分批次运行。
可以把它想象成:
- grid 里有很多 block 排队
- 多个 SM 像"工人"
- 每个 SM 一次接若干个 block 进去执行,执行完再取新的 block
所以 grid size 不直接"对应某个 SM",它只是提供足够多的 block 让所有 SM 忙起来(提高占用与吞吐)。
4) warp 和 "宽 SIMD 向量单元"怎么对应
这是最容易混的点:
- 你写的是 thread(标量线程代码)
- 硬件以 warp(32线程) 为基本执行批次:同一个 warp 内通常是"同一条指令、32 个 lane 同步执行"(SIMT 表象,SIMD 本质)
- 你可以把一个 warp 看成一个 逻辑上的 32-wide SIMD 向量
laneId就是"SIMD lane 编号"- 分支 divergence 就像 SIMD 的 mask 执行
注意 :底层 SM 里究竟是不是"一个固定 32 宽向量 ALU"取决于微架构;但对 CUDA 程序员可见的对应关系就是:
warp = 32 lanes的锁步执行与掩码。
5) 用你的内置变量,把"block→warp→lane"串起来(1D)
在 kernel 里你可以直接算出这些"硬件语义相关"的编号:
cpp
int g = blockIdx.x * blockDim.x + threadIdx.x; // 全局线程ID
int warpId = threadIdx.x >> 5; // 等价于 threadIdx.x / 32
int laneId = threadIdx.x & 31; // 等价于 threadIdx.x % 32
并且:
- 同一个
warpId的线程通常更"锁步" laneId连续的线程往往更适合做连续内存访问(利于合并访存)
6) 典型经验:block size 往往取 32 的倍数
因为 warp 是 32:
blockDim.x不是 32 倍数会导致最后一个 warp "不满"(有些 lane inactive),浪费部分执行槽位。- 常见取值:128、256、512(视寄存器/共享内存/占用而定)。
7) 1000×1000 矩阵乘法的理论性能分析
对于 1000×1000 的矩阵乘法 C = A × B C = A \times B C=A×B(标准 GEMM),计算量近似为:
FLOPs ≈ 2 n 3 = 2 × 1000 3 = 2 × 10 9 \text{FLOPs} \approx 2n^3 = 2 \times 1000^3 = 2 \times 10^9 FLOPs≈2n3=2×10003=2×109
RTX 4090 的常见理论峰值(口径不同会略有差异):
- FP32(CUDA cores) :约 82.6 TFLOP/s = 82.6 × 10 12 FLOP/s 82.6\ \text{TFLOP/s} = 82.6 \times 10^{12}\ \text{FLOP/s} 82.6 TFLOP/s=82.6×1012 FLOP/s
- FP16/BF16(Tensor Cores,非稀疏常见口径) :约 330 TFLOP/s 330\ \text{TFLOP/s} 330 TFLOP/s
理论最小计算时间(理想情况)
假设数据已在显存、实现完美、忽略一切开销:
FP32 需要的最短时间:
t min ≈ 2 × 10 9 82.6 × 10 12 ≈ 2.42 × 10 − 5 s ≈ 24 μ s t_{\min} \approx \frac{2 \times 10^9}{82.6 \times 10^{12}} \approx 2.42 \times 10^{-5}\ \text{s} \approx 24\ \mu s tmin≈82.6×10122×109≈2.42×10−5 s≈24 μs
FP16/BF16(Tensor Core)需要的最短时间:
t min ≈ 2 × 10 9 330 × 10 12 ≈ 6.06 × 10 − 6 s ≈ 6 μ s t_{\min} \approx \frac{2 \times 10^9}{330 \times 10^{12}} \approx 6.06 \times 10^{-6}\ \text{s} \approx 6\ \mu s tmin≈330×10122×109≈6.06×10−6 s≈6 μs
补充说明:这是理论下界。实际使用 cuBLAS 时,1000×1000 这种规模往往会受到 kernel launch、调度、数据读写等开销影响,真实耗时通常会比上述微秒级下界更高一些(常见到几十微秒量级,取决于是否批处理、数据是否已在 GPU、是否使用合适的 GEMM 接口等)。
8) 10000×10000 大规模矩阵乘法的性能分析
1) 显存占用分析(能否放进 4090 24GB 显存?)
元素数: n 2 = 10 8 n^2 = 10^8 n2=108
-
FP32 :单矩阵大小 10 8 × 4 B = 400 MB 10^8 \times 4\text{B} = 400\ \text{MB} 108×4B=400 MB
A + B + C ≈ 1.2 GB A + B + C \approx 1.2\ \text{GB} A+B+C≈1.2 GB(能放下) -
FP16/BF16 :单矩阵 200 MB 200\ \text{MB} 200 MB
A + B + C ≈ 600 MB A + B + C \approx 600\ \text{MB} A+B+C≈600 MB(能放下)
所以这是"可以在显存内做"的规模,比较接近纯算力上限。
2) 计算量
FLOPs ≈ 2 n 3 = 2 × ( 10 4 ) 3 = 2 × 10 12 \text{FLOPs} \approx 2n^3 = 2 \times (10^4)^3 = 2 \times 10^{12} FLOPs≈2n3=2×(104)3=2×1012
3) 理论极限时间(按峰值算力下界估算)
以 RTX 4090 常见峰值口径:
- FP32 峰值 ≈ 82.6 TFLOP/s \approx 82.6\ \text{TFLOP/s} ≈82.6 TFLOP/s
- FP16/BF16 Tensor Core 峰值 ≈ 330 TFLOP/s \approx 330\ \text{TFLOP/s} ≈330 TFLOP/s
FP32 用时下界:
t min ≈ 2 × 10 12 82.6 × 10 12 ≈ 2.42 × 10 − 2 s ≈ 24 ms t_{\min} \approx \frac{2 \times 10^{12}}{82.6 \times 10^{12}} \approx 2.42 \times 10^{-2}\ \text{s} \approx 24\ \text{ms} tmin≈82.6×10122×1012≈2.42×10−2 s≈24 ms
FP16/BF16(Tensor Core)用时下界:
t min ≈ 2 × 10 12 330 × 10 12 ≈ 6.06 × 10 − 3 s ≈ 6 ms t_{\min} \approx \frac{2 \times 10^{12}}{330 \times 10^{12}} \approx 6.06 \times 10^{-3}\ \text{s} \approx 6\ \text{ms} tmin≈330×10122×1012≈6.06×10−3 s≈6 ms
说明 :以上是"理论下界"。实际跑 cuBLAS 通常会比它慢一些(但 10000×10000 规模已足够大,通常能接近峰值的相当一部分)。如果你告诉我矩阵是否在 GPU 上、使用
cublasGemmEx还是sgemm、以及是否允许 TF32,我可以把估算再贴近实际。