👉 ThreadsPerBlock和Blocks的数量受哪些条件约束?
1. 硬件限制(上限)
(1) 线程与块维度约束
- 每个 Block 的最大线程数:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ThreadsPerBlock ≤ maxThreadsPerBlock \text{ThreadsPerBlock} \le \text{maxThreadsPerBlock} </math>ThreadsPerBlock≤maxThreadsPerBlock
(通过 cudaGetDeviceProperties
查询 maxThreadsPerBlock
)
- Grid 的 Block 维度上限:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> GridDim x ≤ maxGridSize [ 0 ] , GridDim y ≤ maxGridSize [ 1 ] , GridDim z ≤ maxGridSize [ 2 ] \begin{align*} \text{GridDim}_x &\le \text{maxGridSize}[0], \\ \text{GridDim}_y &\le \text{maxGridSize}[1], \\ \text{GridDim}_z &\le \text{maxGridSize}[2] \end{align*} </math>GridDimxGridDimyGridDimz≤maxGridSize[0],≤maxGridSize[1],≤maxGridSize[2]
(2) SM 资源约束
-
每个 SM 的最大 Block 数:
<math xmlns="http://www.w3.org/1998/Math/MathML"> BlocksPerSM ≤ maxBlocksPerMultiProcessor \text{BlocksPerSM} \le \text{maxBlocksPerMultiProcessor} </math>BlocksPerSM≤maxBlocksPerMultiProcessor -
每个 SM 的最大线程数:
<math xmlns="http://www.w3.org/1998/Math/MathML"> BlocksPerSM × ThreadsPerBlock ≤ maxThreadsPerMultiProcessor \text{BlocksPerSM} \times \text{ThreadsPerBlock} \le \text{maxThreadsPerMultiProcessor} </math>BlocksPerSM×ThreadsPerBlock≤maxThreadsPerMultiProcessor -
共享内存限制:
<math xmlns="http://www.w3.org/1998/Math/MathML"> SharedMemUsedPerBlock ≤ sharedMemPerBlock \text{SharedMemUsedPerBlock} \le \text{sharedMemPerBlock} </math>SharedMemUsedPerBlock≤sharedMemPerBlock -
寄存器限制:
<math xmlns="http://www.w3.org/1998/Math/MathML"> RegsPerThread × ThreadsPerBlock ≤ maxRegistersPerBlock \text{RegsPerThread} \times \text{ThreadsPerBlock} \le \text{maxRegistersPerBlock} </math>RegsPerThread×ThreadsPerBlock≤maxRegistersPerBlock
可通过编译选项 -maxrregcount
指定线程寄存器数上限)
2. 算法/问题需求(下限与结构)
-
线程总数要求:
<math xmlns="http://www.w3.org/1998/Math/MathML"> TotalThreads ≥ DataSetSize \text{TotalThreads} \ge \text{DataSetSize} </math>TotalThreads≥DataSetSize (覆盖完整数据集)
-
Block 数量要求:
<math xmlns="http://www.w3.org/1998/Math/MathML"> GridSize ≥ SMCount × BlocksPerSM \text{GridSize} \ge \text{SMCount} \times \text{BlocksPerSM} </math>GridSize≥SMCount×BlocksPerSM(充分利用所有 SM)
-
线程组织:
维度结构(1D/2D/3D)需匹配数据访问模式(如合并内存访问)
-
特殊优化需求:
<math xmlns="http://www.w3.org/1998/Math/MathML"> ThreadsPerBlock \text{ThreadsPerBlock} </math>ThreadsPerBlock 需为 WarpSize的倍数(避免资源浪费)
👉 理论计算占用率公式
占用率(Occupancy) 定义为 SM 中活跃 Warp 数与最大支持 Warp 数的比例:
分量计算
- 每个 Block 的 Warp 数:
- 每个 SM 的活跃 Warp 数:
- 每个 SM 的最大 Warp 数:
关键说明
- 实际限制:
受线程数、共享内存、寄存器等资源约束。 - 性能权衡:
100%占用率非绝对最优,有时需降低占用率以换取更高寄存器用量或减少 Bank Conflict。 - 工具支持:
可通过cudaOccupancyMaxActiveBlocksPerMultiprocessor
或 Nsight Compute 预测/实测占用率。
公式应用示例
假设:
- GPU 的
maxThreadsPerMultiProcessor = 2048
,WarpSize = 32
- Kernel 配置:
ThreadsPerBlock = 256
,则:- 若资源允许每个 SM 驻留 8 个 Block(即 ),则:
<math xmlns="http://www.w3.org/1998/Math/MathML"> M a x W a r p s P e r S M = 2048 32 = 64 ; W a r p s P e r B l o c k = ⌈ 256 32 ⌉ = 8 MaxWarpsPerSM = \frac{2048}{32} = 64; WarpsPerBlock = \left \lceil \frac{256}{32} \right \rceil = 8 </math>MaxWarpsPerSM=322048=64;WarpsPerBlock=⌈32256⌉=8 - 若资源允许每个 SM 驻留 8 个 Block(即 BlocksPerSM = 8), 则:
<math xmlns="http://www.w3.org/1998/Math/MathML"> A c t i v e W a r p s P e r S M = 8 × 8 = 64 ; O c c u p a n c y = 64 64 = 100 % ActiveWarpsPerSM = 8 \times 8 = 64; Occupancy = \frac{64}{64} = 100 \% </math>ActiveWarpsPerSM=8×8=64;Occupancy=6464=100%
- 若资源允许每个 SM 驻留 8 个 Block(即 ),则:
👉 CUDA汇编-PTX与SASS
-
PTX (Parallel Thread eXecution):
-
虚拟指令集(类似汇编)
- 跨平台(
nvcc -ptx
生成)
- 跨平台(
-
SASS (Streaming ASSembly):
-
真实GPU机器码(架构相关)
- 通过
cuobjdump --dump-sass
反汇编
- 通过
-
关系 :
CUDA C/C++
→ PTX → (JIT编译) → SASS → GPU执行通常称PTX 是高层次汇编,而SASS是低层次汇编
👉 浮点格式对比
格式 | 位宽 | 指数位 | 应用场景 |
---|---|---|---|
FP32 | 32-bit | 8-bit | 通用计算 |
TF32 | 19-bit | 8-bit | Ampere矩阵乘法 |
FP16 | 16-bit | 5-bit | AI训练/推理 |
BF16 | 16-bit | 8-bit | 替代FP32 (范围保留) |
👉 CUDA的内存模型
CUDA的内存模型Memory类型
内存类型 | 位置 | 延迟 | 带宽 | 作用域 |
---|---|---|---|---|
寄存器 | 片内 | 1 cycle | ∞ | 线程私有 |
共享内存 | 片内 | ~20 cycles | 高 | Block共享 |
L1缓存 | 片内 | ~30 cycles | 高 | SM私有 |
L2缓存 | 片内 | ~200 cycles | 中 | GPU全局 |
全局内存 | 片外 | 200-400 cycles | 中高 | 所有线程 |
常量内存 | 片外+缓存 | 广播:快 随机:慢 | 变 | 只读 |
纹理内存 | 片外+缓存 | 空间局部性优 | 中 | 特殊访问 |
Global memory的访存合并
- 定义: Global Memory
访存合并 (Coalesced Access)
是指同一个Warp内的多个线程 对连续的、对齐的Global Memory地址 的访问请求,被硬件合并 成一个或少量的内存事务 (Memory Transaction) 来处理的技术。 - 目的: 最大化Global Memory带宽利用率,减少内存访问次数,隐藏高延迟。
- 要求 (理想情况):
- 连续性 (Contiguous): Warp中第
T
个线程访问地址BaseAddress + T * sizeof(type)
。 - 对齐 (Aligned): 合并访问的起始地址通常是32字节、64字节或128字节对齐的 (取决于架构和事务大小)。
- 级别:访问能被合并成32字节、64字节、128字节的事务。访问模式越连续、越对齐,合并效果越好,使用的内存事务越少。
- 未合并访问的后果: 一个Warp的访问可能被拆分成多个(最多32个!)独立的小内存事务,极大浪费带宽并显著增加延迟。
- 连续性 (Contiguous): Warp中第
- 如何实现:
- 确保线程访问的数据在Global Memory中是连续存储的 (数组结构体SoA优于结构体数组AoS)。
- 确保线程的索引
threadIdx.x
对应于访问地址的低位连续部分。 - 使用合适的数据类型和对齐属性 (
__align__
或cudaMallocPitch
/cudaMalloc3D
用于2D/3D数据)。
寄存器(Register)与本地内存(Local Memory)的变量分配规则
-
寄存器分配条件
- 标量局部变量(如
int i
) - 小型数组(索引编译时确定)
- 未超出寄存器限制(通过
--maxrregcount
控制)
- 标量局部变量(如
-
Local Memory分配条件:
- 大型数组或动态索引数组
- 寄存器溢出(变量过多/复杂)
- 取地址操作的变量(如
&local_var
)
-
关键区别:寄存器访问延迟≈1 cycle,Local Memory访问延迟≈200~400 cycles(需经L1/L2缓存)
👉 Bank Conflict 的优化方案
Bank Conflict 本质
- 当同一个Warp内的多个线程访问同一Bank(主要是shared memory)的不同地址。
- 后果:访问请求被串行化,性能急剧下降
- 检测指标 :
shared_ld_bank_conflict
和shared_st_bank_conflict
(Nsight Compute)
避免方案(16种专业技巧)
类别 | 方案 | 适用场景 | 示例 |
---|---|---|---|
布局优化 | 1. 地址偏移 | 矩阵访问 | arr[tid][col + 1] (添加1列padding) |
2. 存储转置 | 行列访问转换 | 行列索引交换:arr[col][row] → arr[row][col] |
|
3. Z-order曲线 | 2D/3D数据结构 | index = (row * 32) ^ col (Morton编码) |
|
数据格式 | 4. Bank对齐 | 64位数据访问 | 使用__shared__ double 替代两个float |
5. 压缩存储 | 16位数据类型 | 将两个FP16打包为uint32_t 存储 |
|
访问模式 | 6. 广播优化 | 常量/共享值读取 | 所有线程读取shared_data[0] |
7. SIMD向量访问 | 连续数据加载 | float4 vec = *reinterpret_cast<float4*>(&data[tid*4]) |
|
8. 交错存储 | 多维数据 | 物理行号 = (row%4)*32 + row/4 |
|
算法优化 | 9. 归约树重构 | 并行归约操作 | 步长2访问:sdata[tid*2] += sdata[tid*2 + stride] |
10. Bank感知扫描 | 前缀和计算 | 奇偶分离扫描算法 | |
动态调整 | 11. 动态偏移 | 多Block协作 | index = (tid*4 + blockIdx.x%16) % size |
12. Warp旋转 | Warp间数据交换 | rotated_id = (tid + warp_id) % 32 |
|
高级技术 | 13. Bank重映射 | 复杂访问模式 | 自定义映射表:remap_table[8] = {0,5,2,7,4,1,6,3} |
14. 双缓冲隔离 | 流水线处理 | bufferA[128+2] 使用中间区域[1..128] |
|
15. 比特反转索引 | 随机访问优化 | index = __brev(tid) >> 27 (5位反转) |
|
终极方案 | 16. Bank冲突免疫 | 极端敏感场景 | 按Bank分组序列化写入 |
👉 CUDA程序的调度和计算单元
Block在SM上的调度执行机制
- 资源分配:
- SM检查Block所需的资源(线程数/共享内存/寄存器)
- 资源充足则分配Slot(最大Block数由架构决定,如Ampere为16/SM)
- Warp调度:
- Block被划分为Warps(32线程组)
- Warp Scheduler按周期发射指令(如Volta每SM4个调度器)
- 延迟隐藏:
- 当Warp等待内存时,SM立即切换其他就绪Warp
- 需满足:
Active Warps >= 延迟周期 / 切换开销
(通常需20+ Warps/SM)
CUDA Core vs Tensor Core
特性 | CUDA Core | Tensor Core |
---|---|---|
计算粒度 | 标量运算 | 矩阵乘法累加(MMA) |
典型操作 | FP32/INT32标量计算 | 4x4 FP16矩阵乘 |
吞吐量 | 1 OP/cycle/core | 64 FP16 FMA/cycle/core |
架构支持 | 所有GPU | Volta及更新架构 |
GPU分支预测与指令级并行
分支预测
GPU 上的分支预测非常有限,不是主要的优化手段;不支持复杂分支预测器。 通过Warp内所有线程执行所有分支路径(Divergence开销大)
指令级并行(ILP)
- 流水线并行:指令分成取指/解码/执行等阶段
- 多发射机制:Volta+架构支持每周期发射2条独立指令
- 依赖消除:编译器重排指令避免停顿
🟢 GPU 如何实现 ILP?
GPU 没有 传统意义上的单线程乱序执行!
但它有另一种形式的 ILP,称为:
"隐式指令级并行"(Implicit ILP via Multi-Warp Scheduling)
✅ 核心机制:
- 每个 SM 可以同时驻留多个 warp(几十个)。
- 当一个 warp 因内存访问、长延迟指令(如除法)而阻塞时,Warp Scheduler 会立即切换到另一个就绪的 warp 执行其下一条指令。
- 对于 GPU 来说,这种"上下文切换 "发生在一个时钟周期内,几乎无开销。
分支发散(branch divergence)
- GPU 中线程以 warp(32 线程) 为单位执行。
- 同一个 warp 中 的所有线程必须同时执行同一条指令(Single Instruction, Multiple Threads)。
- 如果
warp 内部
线程对某个if
条件判断结果不同 → 发生分支发散(Branch Divergence), 由原本的一次并行执行 --> 至少两次以上的串行执行。
有 if
的问题示例:
ini
__global__ void kernel(float* data, float* result) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 假设 warp 中的32个线程 一半 >0,一半 <=0
if (data[idx] > 0.0f) {
result[idx] = sqrt(data[idx]);
} else {
result[idx] = 0.0f;
}
}
⚠️ 执行过程
- Warp 中有 16 个线程满足
data[idx] > 0
,另外 16 个不满足。 - GPU 执行引擎:
- 先执行
if
分支(计算sqrt
),但屏蔽掉那 16 个不满足条件的线程 → 它们空转; - 再执行
else
分支(赋值 0),但屏蔽掉那 16 个满足条件的线程 → 它们也空转。
- 结果 :原本可以并行执行的 32 个线程,变成了串行执行两遍 16 个线程 → 性能下降接近 50%!
🔥 这就是分支发散的代价:浪费了 50% 的计算资源!
使用"掩码"用它代替 if
- 🎯 掩码(Mask)的本质:
用布尔运算 + 数学表达式代替条件判断,让所有线程都执行相同路径,只是通过"乘法开关"控制是否生效。
- ✅ 改写示例(用掩码代替
if
):
cpp
__global__ void kernel(float* data, float* result) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 生成掩码:满足条件为 1.0,否则为 0.0
float mask = (data[idx] > 0.0f) ? 1.0f : 0.0f;
// 用数学方式合并结果:无分支!
result[idx] = mask * sqrtf(data[idx]) + (1.0f - mask) * 0.0f;
}
💡 效果分析:
- 所有 32 个线程都执行
sqrtf(data[idx])
和比较操作; - 但那些不满足条件的线程,其结果被乘以 0,最终输出为 0;
- 没有分支发散!所有线程同步执行相同指令!
- 性能提升显著:从 50% 利用率 → 100% 利用率!
- ⚠️ 虽然 line5 使用了 三元条件表达式
?:
,看起来像 if...else... , 但是 GPU 编译器会对其进行优化,编译成无分支的纯算术/位操作指令,而不是条件跳转 ,所以不会产生分支发散。
Float/Int指令周期与融合操作
指令延迟对比(Ampere架构)
指令类型 | 操作 | 吞吐量(每SM每周期) | 延迟(周期) |
---|---|---|---|
FP32 FMA | a*b + c |
128 | 4 |
FP32 ADD | a + b |
128 | 4 |
FP32 MUL | a * b |
128 | 4 |
INT32 ADD | a + b |
256 | 1 |
INT32 MUL | a * b |
64 | 10 |
FP16 FMA | a*b + c |
512 | 2 |
INT8 DP4A | 4元素点积 | 1024 | 4 |
融合操作示例
cpp
// FP32 乘加融合(单指令):
float fma = a * b + c; // 生成FFMA指令(4周期)
// INT32 无融合(多指令):
int mul_add = a * b + c; // 分解为IMUL(10周期) + IADD(1周期)
// FP16 矩阵乘融合(Tensor Core):
__hmma_m16n16k16_fma(a, b, c); // 单指令完成16x16x16矩阵乘加
👉 Roofline模型及计算优化
Roofline Model 深度解析
Roofline Model
- X 轴:计算强度(Arithmetic Intensity,单位:OPs/Byte)。
- Y 轴:可达性能(GFLOPs),此可达性能对于不同的数据类型不同,比如float和int的可达性能不同,并且和使用的计算单元相关,比如使用或者不使用simd。
- 屋顶线:由 内存带宽(斜线)和 算力峰值(水平线)构成。
模型核心要素

关键参数计算
- 计算强度 (Arithmetic Intensity) AI = 总操作数(FLOPs)/ 总字节数(Bytes)
- 理论性能峰值
ini
Peak Perf = min(
内存带宽 × 计算强度,
硬件算力峰值
)
-
- 实际性能测量
bash
# 获取FLOPs
ncu --metrics smsp__sass_thread_inst_executed_op_fadd_pred_on,smsp__sass_thread_inst_executed_op_fmul_pred_on,smsp__sass_thread_inst_executed_op_ffma_pred_on ./app
# 获取字节数
ncu --metrics dram__bytes_read.sum,dram__bytes_write.sum ./app
优化决策树

典型场景优化策略
区域 | 特征 | 优化策略 |
---|---|---|
内存瓶颈区 | 低计算强度 (<10 FLOP/Byte) | • Global Memory合并访问 • 共享内存Bank优化 • 数据压缩(FP16/BF16) • Kernel融合减少数据搬运 |
计算瓶颈区 | 高计算强度 (>100 FLOP/Byte) | • 算子优化(循环展开/指令选择) • Tensor Core利用 • 增加ILP(指令级并行) • 降低精度(FP32→TF32) |
过渡区 | 中等计算强度 (10-100) | • 双缓冲技术 • 数据分块计算 • 混合精度计算 |
Kernel Fusion的优势与示例
核心原理与工作机制
-
传统多Kernel执行的瓶颈
- Kernel Launch开销:每次启动一个CUDA kernel需6--10μs,频繁启动小kernel时,该开销占比显著。
- 全局内存(Global Memory)访问:每个kernel需将中间结果写入全局内存,后续kernel再读取,导致高延迟和带宽压力。
-
Fusion的优化逻辑
- 合并计算逻辑:将多个操作(如逐元素乘法 + 求和)合并为一个kernel,避免中间结果写回全局内存,改用寄存器或共享内存暂存数据。
- 减少Launch次数:多个操作仅需一次kernel启动。
核心优势
- 降低延迟与带宽压力
- 数据在寄存器或共享内存中直接传递,减少全局内存访问次数,提升计算效率。
- 例如:向量点积中,融合乘法和求和可避免中间结果写回。
- 隐藏Kernel Launch开销
- 对执行时间短的kernel(如深度学习中的小算子),融合后性能提升显著(20--50%)。
- 提升硬件利用率
- 通过合并无数据依赖的kernel(Horizontal Fusion),并行执行独立任务,提高SM(流多处理器)资源利用率。
技术实现方式
- 垂直融合(Vertical Fusion)
- 合并具有数据依赖的连续操作(如
Conv → ReLU
),通过共享内存或寄存器传递中间结果。
- 水平融合(Horizontal Fusion)
- 合并无数据依赖的独立kernel(如同时执行多个矩阵乘法,将多个shape较小的矩阵乘法合并成一个大的矩阵乘法),需统一线程组织结构(如调整Block/Grid尺寸)。
Double Buffer原理与实现
- 原理 :
使用两块缓冲区交替进行数据加载和计算,隐藏内存传输延迟 - CUDA实现:
cpp
__shared__ float buffer0[BLOCK_SIZE];
__shared__ float buffer1[BLOCK_SIZE];
float* load_buf = buffer0;
float* compute_buf = buffer1;
for (int i = 0; i < iter; i++) {
// 异步加载数据到 load_buf
loadData(load_buf, i);
__syncthreads(); // 等待加载完成
// 处理 compute_buf 数据
compute(compute_buf);
// 交换缓冲区指针
float* temp = load_buf;
load_buf = compute_buf;
compute_buf = temp;
}