GPU 优化的主线,是让工作与数据都少等
CUDA 不是一套“多开线程”的咒语,而是三本同时对账的账簿:Host 把工作排进哪些队列,Block 与 Warp 怎样占用执行资源,数据又以多少真实事务穿过片上存储和 Device Memory。把队列、资源、字节三条链画清楚,优化才从猜参数变成可复现的工程。
Kernel 只是队列中的一项工作;Launch 返回不等于 GPU 已经算完
Host(主机 CPU)分配内存、提交 Host-to-Device(H2D,主机到设备)拷贝、Kernel launch(内核启动)和 Device-to-Host(D2H,设备到主机)拷贝。Kernel launch 和明确的异步 API 可以入队后返回;普通 memcpy、allocation 和其他 CUDA API 是否阻塞,则取决于具体 API、方向、参数与 Host memory 类型。若计时、读回结果和错误处理忽略这份逐 API 合同,程序可以“看起来很快”,甚至在错误发生后继续运行。
| 对象 | CUDA 给你的合同 | 不保证什么 | 应怎样验证 |
|---|---|---|---|
| 同一 Stream | 工作按入队顺序执行 | Host 会等待;也不代表每个 API 都已经执行完 | 需要结果时等 Event / Stream,性能路径避免无差别 device sync |
| 显式创建的不同 Stream | 避开 legacy default stream 与其他隐式同步后,通常用 Event 建立跨队列先后;独立工作可能并发 | 并发、开始时间、优先级抢占或固定交错顺序 | 用 Nsight Systems 看实际 overlap(重叠)与资源争用 |
| Default / implicit sync | Legacy default stream 会与 blocking streams 隐式同步;allocation、memset 等操作也可能抑制并发 | 所有默认流模式都相同;per-thread default stream 有不同语义 | 记录 stream flags 与编译选项,时间线中检查意外 serialization(串行化) |
| 普通 Grid 内 Blocks | 每个 Block 最终执行并拥有块内协作边界 | Block 调度顺序和普通 Grid 的跨 Block 同步 | 跨 Block 依赖拆成多个 Kernel,或采用明确支持的协作机制 |
| Event | 记录某个 Stream 位置,可供别的 Stream 等待或做设备时间戳 | 自动全设备同步,或保证未关联工作的顺序 | 把依赖放在最窄范围,并检查 Event 所在 Stream |
Launch check 看“能否启动”,同步点看“执行中是否失败”
my_kernel<<<grid, block, 0, stream>>>(out, in, n);
CUDA_CHECK(cudaGetLastError()); // 配置、参数或此前尚未清理的错误
CUDA_CHECK(cudaStreamSynchronize(stream)); // 调试/真正依赖处:暴露异步执行错误 cudaGetLastError() 紧跟 launch 很重要,但它不能证明 Kernel 已经执行成功。生产热路径不应为了“保险”在每次 launch 后全同步;应在真实数据依赖、Event 或调试门禁处检查,并让错误带上 Stream、Shape、dtype(数据类型)、设备和构建版本。
你定义 Grid 与 Block;硬件把 32 个 Thread 组成 Warp,再让 Block 驻留 SM
SIMT(Single Instruction, Multiple Threads,单指令多线程)允许每个 Thread 有自己的索引、寄存器状态和分支,但 SM(Streaming Multiprocessor,流式多处理器)以 Warp 为核心调度粒度。一个 Thread Block(线程块)完整驻留在一个 SM;它消耗的寄存器与 Shared Memory(共享内存)决定同一 SM 还能同时容纳多少 Blocks 和 Warps。
一次 Kernel 的全部 Blocks
规模可远大于 SM 数,由硬件分批调度。普通 Grid 不能依赖“Block 0 一定先完成”。
资源与协作边界
同块 Thread 可用 Shared Memory 和块内 barrier(屏障);Block 必须能完整放进一个 SM。
固定 32 个 lane
分支路径不一致时用 active mask(活跃掩码)分批执行。Divergence(分支发散)会浪费 lane,但不自动等于错误。
逻辑工作者
常见映射是一个 Thread 负责一个或少量输出元素;地址模式比 Thread 数量更直接影响 memory transaction。
i = blockIdx.x × blockDim.x + threadIdx.x
若 N = 1,000,000、每块 256 Threads,就启动 ceil(N/256)=3,907 Blocks,并用 if (i < N) 保护尾块。256 只是 NVIDIA Best Practices 给出的常见初始实验范围 128–256 之一,不是万能最优值;最终配置必须同时看地址合并、寄存器、Shared Memory、占用和 Kernel 时间。
Compute Capability(计算能力)9.0 起可声明 Thread Block Cluster(线程块簇):一组 Blocks 被 co-schedule(协同调度)到同一个 GPC(Graphics Processing Cluster,图形处理簇),并可访问 Distributed Shared Memory(分布式共享内存)。它增加了明确的跨 Block 协作范围,但不改变普通 Grid 仍无 Block 顺序保证的事实。
| 模型 | 程序员主要表达 | 块内并行由谁决定 | 适合的学习目标 | 版本边界 |
|---|---|---|---|---|
| CUDA C++ SIMT | Thread / Block 索引、同步、warp primitive(Warp 原语)和显式地址 | 程序与编译器共同决定 | 理解底层合同、处理特殊同步和新硬件能力 | CUDA 的经典模型 |
| CUDA Tile C++ | 每 Block 的 tile(数据块)、array(数组)与 tile operation | 编译器映射块内 Threads | 在 CUDA C++ 工程中更高层表达 TMA(Tensor Memory Accelerator,张量内存加速器)与 Tensor Core 工作 | Toolkit 13.3+;C++20;-enable-tile;目标 sm_80+ |
| cuTile Python | cuda.tile package 中的 Tile kernel 与 function | 编译器映射块内 Threads | 从 PyTorch / CuPy 等 DLPack 或 CUDA Array Interface 对象进入 Tile 编程 | 独立版本;截至核查日 v1.5 支持 CC 8.x–12.x、R580+,并须满足 Quickstart 的 Toolkit / package 条件 |
CUDA Tile 与 SIMT 可以出现在同一 CUDA 程序里,但当前 Tile function 与 SIMT device function 不能直接互调。CUDA Tile C++ 和 cuTile Python 共享 Tile 思路,不共享一条简单的“13.3+”安装合同;它们是并列编程入口,也不是“SIMT 已被淘汰”。
先分清作用域、管理者与物理 backing,再谈“哪层更快”
Register、Shared、Local、Global 描述的是 CUDA 编程空间;L1/TEX、L2、Device Memory 则更接近物理资源。两组名字并不是一一对应的楼梯。尤其 Local Memory 只表示每 Thread 私有地址空间,寄存器 spill(溢出)或动态索引数组可能落到片外 Device Memory,并经 L1/L2 缓存。
Register:每 Thread 的编译器分配状态
标量、中间累加器和地址通常在 SM register file(寄存器文件)。寄存器需求过大既可能减少驻留 Blocks,也可能产生 Local Memory spill;看编译报告和 profiler,不能只数源码变量。
Shared Memory:程序显式管理的 Block 工作台
适合让一个 tile 从 Device Memory 读取一次、被多个 Threads 重用,或在线程间交换数据。它需要正确 barrier,并与 L1 的统一物理资源、carveout(容量划分)和上限一起按架构查询。
L1 / TEX 与 L2:硬件管理 cache
L1/TEX 靠近每个 SM,L2 为 GPU 范围共享。命中能减少片外流量,却不是源码层“必须命中”的接口合同;缓存行为和容量随 Compute Capability 变化。
Global / Device Memory:大容量 backing
模型权重、激活与输出通常驻留 GPU DRAM(显存,数据中心 GPU 常见 HBM,其他产品也可能是 GDDR)。高带宽不等于低延迟;要靠合并事务、片上复用和并发隐藏等待。
Pinned Host Memory:可异步 DMA 的有限资源
Page-locked(页锁定)主机内存有利于 DMA(Direct Memory Access,直接内存访问)与拷贝/计算重叠,但会占用系统不可换页资源;不能把所有 Host allocation 都长期 pin。
Unified Memory:统一地址不等于零迁移成本
统一地址和按需迁移简化编程,页迁移、预取和驻留仍会影响延迟。服务端尾延迟敏感场景要用真实访问序列 profile,而不是从 API 名字推断性能。
32-byte 单元
对 Compute Capability 6.0+,当前 coalescing 模型以 32-byte transaction(内存事务)为基本单元。一个 Warp 连续读取 32 个 FP32,共 128 bytes;对齐时覆盖 4 个事务。
4 → 32 个事务
若 32 个 lane 的地址分别落入不同 32-byte 单元,同样 128 bytes 有效数据可触发 32 个事务。CC 5.2 启用 L1 caching 时存在 128-byte aligned segment 特例,不能套用本例。
CC 5.x+ 为 32 Banks
连续 32-bit words 映射到连续 banks。同一 Warp 多地址撞同 bank 会序列化;同地址广播是例外。Transpose 常用 tile 末维 +1 改变映射。
Effective BW = (bytes read + bytes written) / kernel time
FP32 Vector Add 的最低逻辑流量是读 A、读 B、写 C,即 12N bytes;还要明确是否包含 warmup、同步、cache 热度与尾块。测得值与理论峰值的差距只能说明“还有损失”,不能单独区分事务浪费、依赖 stall、launch overhead 或测量错误。
NVIDIA Hopper Tuning Guide 给出的 H100 每 SM Shared Memory 容量为 228 KB,而 A100 为 164 KB;每 Block 可用上限、默认配置和 opt-in 条件仍不同。这组数字的意义是提醒你:在一张卡上合适的 tile,换架构后必须重新编译、查询资源并实测。
先用时间线找“空洞在哪”,再钻进热点 Kernel 解释“为什么等”
Nsight Systems 回答整机问题:CPU 是否及时提交、CUDA API 是否阻塞、拷贝和计算是否重叠、Stream 之间是否存在空洞;Nsight Compute 回答单 Kernel 问题:真实 memory traffic、cache、指令、occupancy(占用率)、scheduler 和 stall reason(停顿原因)。工具顺序反了,很容易精调一个并不重要的 Kernel。
0. 冻结测量合同
保存 GPU 与 driver、CUDA / framework 版本、power state、shape、dtype、layout / stride、warmup、同步点、采样次数、P50 / P99 和数值容差。否则结果不可比较。
1. 先过正确性
与 CPU 或成熟库参考比对,覆盖零长度、尾块、非对齐、极值、NaN / Inf 和多 Stream。跑 Compute Sanitizer,再谈更快。
2. Nsight Systems 定位全局空洞
分清 CPU launch-bound(发射受限)、copy-bound(拷贝受限)、同步链过长、Kernel 过碎与真正的 GPU 计算区间。
3. Nsight Compute 解释热点
先看 Roofline 方向与 memory workload,再看 warp state / stall;单个百分比没有上下文,不要把某个规则当万能处方。
4. 一次只改一个机制
改变 layout、融合、tile、向量化、异步拷贝或 launch 方式之一,并重新跑同一合同。保存 profiler capture,而不只抄“最快 block size”。
5. 回到端到端与架构矩阵
单 Kernel 变快可能增加编译、layout conversion(布局转换)、冷启动或寄存器压力。最终要在真实模型 Shape、并发和目标 GPU 上复测。
事务多、复用少
查 coalescing、stride、cache、Shared reuse 和实际读写字节;不要只看名义 HBM 利用率。
指令或流水不合适
查 dtype、Tensor Core path、依赖链、tile 和指令吞吐;“算力受限”也可能是执行单元不匹配。
GPU 在等 Host
查碎 Kernel、无意同步、拷贝串行与 graph capture 机会;先确认并发工作确实独立。
错误先阻断性能结论
越界、未初始化、数据竞争和 barrier 错误都能产生“更快”的错误程序。
Occupancy 通常是“活跃 Warps / 架构允许的最大活跃 Warps”。过低可能没有足够 ready Warp 隐藏延迟;把它推到 100% 又可能迫使缩小 tile、减少寄存器复用并增加 Device Memory 流量。最终裁判是同一合同下的 Kernel 与端到端时间。
大模型算子反复组合四件事:独立映射、归约、广播与分块复用
练习不是为了背模板,而是识别输出依赖:每个输出是否独立?一整行是否共享统计量?多个 Threads 是否必须交换部分和?同一片 A / B 数据能为多少输出复用?这些问题决定 Warp shuffle、Shared Memory、融合和多阶段 Kernel 的边界。
Reduction(归约):N 个值汇成一个
每 Thread 局部累加,Warp 内用 shuffle 汇总,多个 Warp 的部分和再通过 Shared Memory 合并;跨 Blocks 通常需要第二 Kernel 或受控原子操作。覆盖非 2 次幂、空输入和浮点顺序。
Softmax:Max、指数和、归一化三步
先减行最大值避免指数溢出,再归约 exp(x−max) 的和。尽量把一行留在 Register / Shared,减少阶段间写回;超长行需要多块或在线合并算法。
LayerNorm / RMSNorm:归约后广播
LayerNorm 计算均值和方差,RMSNorm 计算平方均值;统计量出来后每个元素再归一化和缩放。与 residual、bias、activation 或 quantize 融合可少搬数据,也会增加寄存器压力。
GEMM(General Matrix Multiplication,通用矩阵乘):三维 Tile
把 C 的 M×N 输出切块,让 A / B 的 K 维 panels 在片上重用并由 Register 累加。先以 cuBLAS / CUTLASS 为正确性与性能基线,再研究 layout、pipeline、Tensor Core 和 autotune。
| 练习 | 正确性最小集合 | 性能主账 | 常见假优化 |
|---|---|---|---|
| Vector / Transpose | 尾块、奇数 stride、未对齐、原地 / 非原地 | 有效带宽、global transactions、bank conflict | 少写边界判断但越界;只测 cache 热数据 |
| Reduction | 0 / 1 元素、非 2 次幂、极值、NaN 策略 | 读字节、shuffle / sync、跨块阶段 | 换加法顺序后仍要求逐 bit 相同,或反过来放宽到掩盖错误 |
| Softmax / Norm | 大幅值、全相等、长短行、dtype tolerance | HBM 往返、Register、行长分支 | 只测整齐长度;忽略数值稳定 |
| GEMM | M/N/K 尾块、转置布局、alpha / beta、累加精度 | Tensor Core、tile reuse、pipeline 与库差距 | 只测一个方阵,或把 layout conversion 排除在端到端之外 |
Tensor Core 支持的数据类型、矩阵指令 shape、累加精度、对齐与布局随 GPU 架构变化。“用了 FP16 / BF16”不等于自动走高效路径;必须确认实际指令、输入约束和数值误差。
最好的实现层级不是最低,而是证据允许的最高层
高层库和编译器把成熟调度、架构适配与维护成本打包;低层代码换来更细控制,也把编译矩阵、错误面和升级负担交给你。正确顺序是先建立可信 Eager / 库基线,再依据热点选择 torch.compile、Triton、CUDA Tile 或 CUDA C++ SIMT。
成熟库:先拿到可维护的上限
cuBLAS、cuDNN 与 CUTLASS 覆盖常见 GEMM / convolution / epilogue 路径。库没有自动包含你的前后 layout conversion、Python dispatch 和同步,所以端到端仍要测。
torch.compile:先让图级编译做融合和生成
记录 graph break(计算图断点)、guard(运行条件)、动态 Shape 重编译、冷启动和缓存。它可能生成 Triton / CUDA 后端代码,但具体内部与选项随 PyTorch 版本变化,不应写成永久合同。
Triton:第三方 Tile DSL
DSL(Domain-Specific Language,领域专用语言)以 program instance / tile 表达 load、mask、归约和 autotune,适合规则热点与融合;仍要理解地址合并、资源压力和数值。
CUDA Tile:C++ 与 Python 有两份运行合同
CUDA Tile C++ 自 Toolkit 13.3 提供,要求 C++20、-enable-tile 与 sm_80+;cuTile Python 独立发版,截至核查日 v1.5 支持 CC 8.x–12.x 与 R580+ Driver,并须满足其 Toolkit / package 条件。二者都把块内线程映射交给编译器,但不能只写一个“13.3+”。
CUDA C++ SIMT:完整 Thread / Block 控制
适合自定义 warp primitive、复杂同步、新硬件特性、非规则数据结构或现有 C++ runtime 集成。代价是更多架构分支、编译选项、sanitizer 与测试矩阵。
| 制品 | 它是什么 | 可移植性边界 | 什么时候看 |
|---|---|---|---|
| PTX | Parallel Thread Execution,面向 NVIDIA GPU 的虚拟 ISA(Instruction Set Architecture,指令集架构) | Driver 可 Just-in-Time(JIT,即时编译)到兼容目标;新指令和目标仍受 Driver / Compute Capability 限制 | 解释状态空间、编译器输出与前向兼容路径 |
| cubin | 针对一个真实 GPU architecture 的 device binary;SIMT 与 Tile 使用各自编译阶段 | 启动快、目标具体;不能假定跨不兼容架构运行 | 检查目标 code generation 与发布包覆盖 |
| SIMT fatbinary | 可携带多份 SIMT cubin 与 PTX fallback | Runtime / Driver 选择兼容 image;仍受 Driver、目标与具体特性限制 | 设计 CUDA C++ SIMT 的多 GPU 发布 |
| cuda_tile / Tile fatbinary | CUDA Tile 的中间表示和独立 Tile 打包阶段,可生成 Tile cubin | 不是“任意 PTX / cubin / Tile IR 混在同一条工具链”的保证 | 发布 CUDA Tile kernels 时核对 Tile 编译选项与目标 |
CUDA Graph 把重复的工作依赖图实例化后重放,主要减少 CPU launch overhead。Graph 可通过显式 API 或 stream capture 创建;参数和地址只能按对应 node update API 的限制更新,cudaGraphExecUpdate() 的 whole-graph update 要求 topology(拓扑)相同且依赖顺序匹配,拓扑或 node type 改变就要重新实例化。某些 PyTorch CUDA Graph 路径对稳定 Shape / 地址有更严格约束,不能把框架限制反写成所有 CUDA Graph 的统一规则。
每个阶段都交付“能算对、能测量、能解释、能复跑”的证据包
NVIDIA Best Practices 的 APOD(Assess, Parallelize, Optimize, Deploy,评估—并行化—优化—部署)不是一次性流程,而是每个热点的短循环。不要先通读整本手册;用一张明确 GPU 和一组固定 Shape,让每个概念都落到代码、测试、profiler capture 和复现实验清单。
阶段一:Vector Add + Transpose
掌握索引、尾块、Event 计时、coalescing 与 Shared padding。交付 CPU 参考、有效带宽表、memcheck 结果和 global transaction 对比。
阶段二:Reduction + Softmax
练 Warp shuffle、块内归约、两阶段跨块归约和稳定指数。交付多个长度 / dtype 的误差矩阵、时间分位数和 stall 解释。
阶段三:Norm + 融合
组合 residual、LayerNorm / RMSNorm、affine 与可选 quantize,计算理论最少 HBM 字节,再用 profiler 看融合少了多少、寄存器增加多少。
阶段四:GEMM + Tensor Core
用 cuBLAS / CUTLASS 作上限和正确参考,再比较 Triton、受支持 GPU 上的 CUDA Tile 或 CUDA C++。覆盖方阵、长瘦矩阵、尾块和多个兼容架构,而不是追一个展示数字。
阶段五:接回一个真实模型
把热点接进 PyTorch extension / custom op,测编译缓存、dispatch、layout conversion、同步、显存峰值和完整 step latency;单 Kernel 加速不是最终结论。
| 工具 | 主要找什么 | 不能替代什么 |
|---|---|---|
memcheck | 越界、misaligned(未对齐)访问和硬件异常;泄漏检查需启用 --leak-check full | 算法参考输出与所有并发语义 |
racecheck | Shared Memory 数据访问 hazard(冒险) | Global Memory 的所有高层并发协议 |
initcheck | 默认检查未初始化 Global Memory;可用 --initcheck-address-space shared/all 扩展地址空间 | Register 初始化与算法级参考输出 |
synccheck | 非法 barrier / synchronization 使用 | 业务级顺序与数值正确性 |
源码与 commit、编译命令和 architecture flags、GPU / Driver / CUDA / framework 版本、输入生成器、Shape / dtype / stride、参考实现与 tolerance、warmup / 同步 / 采样方法、P50 / P99、峰值显存、sanitizer 日志、Nsight capture 和失败样例。缺一半的“快 30%”无法复核。
Register、Shared、L2 与 Device Memory
回答时同时说编程作用域、管理者、典型物理 backing 和架构差异,不能只背“从快到慢”。
Roofline 与真实 stall
算术强度给方向,Nsight Systems / Compute 给证据;“GPU utilization 低”本身不能定位到某个 Kernel。
Reduction、Softmax 与 GEMM
把归约顺序、稳定 Softmax、矩阵分块和误差合同连起来,不能用速度掩盖数值变化。
学会 CUDA 的标志,是知道何时不该再写 CUDA
若成熟库或编译器已经满足端到端 Service-Level Objective(SLO,服务级目标),更低层实现只会增加维护面;若热点占比低、瓶颈在 CPU / 网络 / 排队,Kernel 再快也不会改变用户体验。下潜之前必须同时满足“值得、可测、可守住”三项。
热点足够大
先用时间线和 Amdahl 直觉确认:优化目标在端到端中占比足以改变 SLO,而不是 profiler 中最显眼的一行。
合同能冻结
Shape、dtype、layout、数值误差、冷热启动与目标 GPU 有可复现清单,并有成熟库或旧实现作基线。
架构矩阵守得住
有人负责新 GPU / Driver / CUDA / framework 版本回归、编译制品覆盖和 fallback,而不是一次 benchmark 后无人维护。
错误能阻断和回退
sanitizer、数值门禁、端到端灰度、遥测和旧路径回退都在发布链里;性能优化不能绕过正确性。
“这个 Kernel 为什么慢?”必须同时回答三本账
队列账:Host、Stream、Event、copy 与同步是否让 GPU 等待;资源账:Block / Warp、寄存器、Shared、指令与 occupancy 怎样限制 ready work;字节账:地址合并、cache / Shared 复用、layout conversion 与实际 memory traffic 怎样偏离最低需求。然后给出一次最小改动、对应 profiler 证据和数值回归。能这样回答,才真正走完了这条学习路线。
一手来源与证据边界
优先使用论文、官方文档、官方模型卡和代码仓库。页面中的数字只代表来源所述设置,不自动外推到其他模型与数据。
本页版本基线;CUDA 平台、执行模型、内存、异步执行、CUDA Graph 与 Tile 编程的总入口。
Host / Device 分工、Grid–Block–Thread 层级、Warp、SM、可选 Thread Block Cluster 与内存层级。
SIMT kernel、32-thread Warp、32-byte global memory transaction、合并访存和 shared-memory bank 行为。
CUDA Tile C++ 与 cuTile Python 的 Tile 编程模型、每 Block 一条逻辑控制流、由编译器映射块内线程及其与 SIMT 共存的边界。
cuTile Python 独立的 package、Compute Capability 8.x–12.x、R580+ Driver 与 Toolkit 安装兼容条件。
Stream 同队列保序、Event 依赖与计时、跨 Stream 并发条件、同步层级和异步错误暴露。
APOD 优化闭环、有效带宽、coalescing、bank conflict、occupancy、线程块初始搜索区间与验证原则。
显式 Graph API、stream capture、实例化与重复 launch,以及参数更新和拓扑变化的兼容边界。
CUDA C++ SIMT 的 PTX / cubin / fatbinary 与 CUDA Tile 的 cuda_tile IR / Tile cubin / Tile fatbinary 等独立编译阶段。
PTX(Parallel Thread Execution,虚拟指令集)定位、状态空间与目标架构接口。
应用级 CPU / CUDA API、memory operation、Stream、同步与时间线分析。
Kernel 级 Roofline、memory workload、cache、occupancy、scheduler 与 stall 指标的解释边界。
memcheck、racecheck、initcheck、synccheck 四类正确性检查及其覆盖范围。
图捕获、后端编译、动态 shape、guard 与 CUDA Graph 选项;框架行为按所用版本复核。
Vector Add、Fused Softmax、Matmul、LayerNorm、Fused Attention 和 persistent matmul 的官方练习。
GEMM 分层构造、Tile、Tensor Core、数据搬运与面向架构的成熟库基线。
Hopper Compute Capability 9.0、Thread Block Cluster、分布式 Shared Memory 与架构资源边界。