文章目录
- 一、现代 GPU
- 1. GPU 的流式处理与硬件 “瘦身” 设计
- 2. SIMT(单指令多线程):比 SIMD 更灵活的并行模式
- 3. GPU 解决流水线停顿:增加执行上下文(类似超线程)
- 4. GPU 存储层次与访存瓶颈根源
- 二、内存控制器
- 核心职能
- 映射策略决定的三大关键性能指标
- 1. Row Buffer 命中率
- 2. Bank 并发度
- 3. 访问延迟
- 常见地址映射模式对比
- 软件层面的优化启示
- 常见认知误区
- 三、高性能 GPU 访存编程实战的 12 条核心法则
- 核心观点
- 法则一:永远保证 Stride-1 访存(Memory Coalescing)
- 补充:非 CUDA 推理芯片的对齐要求
- 法则二:用 Shared Memory Tiling 实现数据复用
- 法则三:用 cp.async 实现异步拷贝,隐藏访存延迟
- 法则四:用 Warp Shuffle 替代 Shared Memory Reduction
- 法则五:为 Tensor Core 准备正确的数据布局
- 法则六:AoS → SoA 数据布局转换
- 法则七:利用 L2 Cache Residency 控制热点数据驻留
- 法则八:LLM 推理中的 KV Cache 优化 ——PagedAttention
- 法则九:掌握性能分析工具,用数据驱动优化
- 法则十:软件流水线 —— 让数据搬运和计算永远并行
- 法则十一:缓存感知编程 —— 用加载限定符控制数据在 Cache 中的行为
- 法则十二:Kernel 融合与 L2 感知分块 —— 最大化缓存时间局部性
- 法则补充
- 本质与总结
- 参考资料
一、现代 GPU
GPU 的核心优势是大规模并行处理,其硬件设计和执行模式与 CPU 有显著差异:通过 “流式处理” 简化控制逻辑,用 SIMT(单指令多线程)实现灵活并行,同时借鉴超线程思路解决流水线停顿问题。
1. GPU 的流式处理与硬件 “瘦身” 设计
GPU 的处理过程是典型的流式处理(Stream Processing),针对批量数据执行相同计算任务,无需复杂的分支控制或依赖调度,因此硬件设计可以做一次 “瘦身”:
精简控制电路:砍掉 CPU 中复杂的分支预测、乱序执行等控制逻辑,仅保留计算相关的核心电路;
核心组件抽象:简化后,GPU 的硬件被抽象为三个核心部分:取指令和指令译码模块、ALU(算术逻辑单元)阵列、执行上下文(线程上下文);
设计目标:通过简化控制逻辑,腾出更多晶体管资源,用于增加 ALU 数量,支撑大规模并行运算。
从整体架构看,GPU 由大量重复的流多处理器(Streaming Multiprocessor, SM)组成,每个 SM 都是一个独立的小型计算核心,拥有专属的指令调度单元、ALU 阵列、寄存器堆与片上缓存。GPU 的大规模并行能力,本质是通过数十到上百个 SM 同时工作堆叠而来;而所有 Kernel 的线程调度、片上资源分配、访存执行,最终都落到单个 SM 上完成,绝大多数访存优化技巧,本质都是针对单 SM 的资源与执行特性设计的。
2. SIMT(单指令多线程):比 SIMD 更灵活的并行模式
SIMT(Single Instruction, Multiple Threads,单指令多线程)是 GPU 的核心并行模式,比 SIMD(单指令多数据流)更灵活,核心差异和特点如下:
SIMD 的局限:CPU 的 SIMD 模式中,一次性读取固定长度的多个数据,用一条指令同时处理,所有数据必须执行相同指令,灵活性低,无法应对数据差异导致的分支;
SIMT 的核心逻辑:将多条数据分配给不同线程处理,每个线程拥有独立的执行上下文,线程间共享指令流,但可根据数据差异走不同分支(如部分线程执行 if 分支,部分执行 else 分支);
并行执行优化:取指令和译码阶段取出的指令,可同时分发给多个 ALU 并行运算,因此一个 GPU 核心可容纳更多 ALU,实现更高的并行度。
SIMT 模式下,GPU 的最小执行调度单元是Warp,一个 Warp 固定包含 32 个线程,它们共享同一条指令流,以锁步方式同步执行。当 Warp 内的线程出现条件分支分歧(即部分线程进入 if 分支、部分进入 else 分支)时,硬件会串行执行所有分支路径,关闭不在当前路径上的线程,这种现象称为Warp Divergence(Warp 发散)。
Warp 发散不仅会直接降低计算单元利用率,还会间接破坏访存合并效果:分支后的线程访问地址往往不再连续,导致原本可合并的访存请求被拆分为大量零散事务,带宽效率大幅下降。因此减少分支发散,也是访存优化的重要前置条件。
3. GPU 解决流水线停顿:增加执行上下文(类似超线程)
GPU 砍掉了分支预测电路,指令遇到条件分支时会出现流水线停顿,导致 ALU 空闲。为解决这个问题,GPU 借鉴了 CPU 超线程的思路:
核心问题:分支指令会导致流水线停顿,ALU 因等待指令或数据而空闲,造成硬件资源浪费;
解决方案:增加 GPU 核心内的执行上下文(线程上下文)数量,让上下文数量多于 ALU 数量;
工作逻辑:当一个线程的指令因分支或数据依赖停顿时,GPU 调度器可切换到其他就绪的线程上下文,让 ALU 执行其他线程的指令,避免硬件空闲;
与超线程的共性:和 CPU 超线程一样,需要为不同任务提供独立的执行上下文,才能实现快速任务切换,因此上下文数量必须多于 ALU 数量。
在 GPU 的各类流水线停顿中,对于访存密集型 Kernel(如 GEMM、Attention 等带宽受限算子),访存等待通常是占比最高的停顿来源:全局内存访问的延迟可达数百时钟周期,若没有足够的就绪线程切换,ALU 会长时间处于空闲。因此,通过多线程上下文隐藏访存延迟,是 GPU 发挥算力的核心机制;而访存优化的目标,就是进一步减少访存次数、降低访存延迟,让计算单元更少等待。
4. GPU 存储层次与访存瓶颈根源
与 CPU 缓存主导的存储体系不同,GPU 采用分层的存储金字塔,从寄存器到全局显存,速度逐级递减、容量逐级递增,访存开销差异可达数百倍:
寄存器:每个线程私有,单 SM 拥有数万个 32 位寄存器,访问延迟约 1 个时钟周期,是最快的存储层级;容量由单 SM 总寄存器数与线程数瓜分,溢出后会降级到片外显存(Local Memory),性能暴跌。
Shared Memory / L1 Cache:SM 内共享的片上存储,延迟约 10~20 个时钟周期,带宽远高于片外显存。Shared Memory 由程序员显式管理,L1 Cache 由硬件自动管理,二者共享同一块片上存储资源,可通过编译选项配置比例。
L2 Cache:全 GPU 共享的片上缓存,延迟约百级时钟周期,是全局内存访问的必经之路,负责缓存热点数据、合并访存请求。
HBM 全局内存:片外显存,容量最大但延迟最高,约数百到上千个时钟周期,也是绝大多数算子的性能瓶颈所在。
GPU 的算力增长速度远高于显存带宽的增长速度,单精度算力可达每秒数十到上百 TFLOPS,而 HBM 带宽通常仅为每秒数 TB,计算单元往往处于 “等数据” 的状态 —— 这就是访存墙问题,也是所有 GPU 访存优化的核心背景。
二、内存控制器
核心职能
内存控制器(Memory Controller, MC)的核心作用是将线性逻辑地址翻译为 HBM 的 Bank / Row / Column / Die / Channel 物理坐标,是衔接计算单元与显存介质的核心硬件模块。地址映射策略的设计,直接决定了显存访问的实际带宽与延迟表现。
从物理结构上,现代 HBM 显存的地址层级从顶层到底层依次为:Stack(堆栈)→ Die(裸片)→ Channel(通道)→ Bank Group(存储体组)→ Bank(存储体)→ Row(行)→ Column(列)。内存控制器的地址映射,本质是将连续的线性地址按预设策略打散到所有层级,在「行缓存命中率」和「多单元并发度」之间做硬件权衡。
与 CPU 面向低延迟优化的内存控制器不同,GPU 的内存控制器是高带宽导向设计:依赖数量更多的独立通道与 Bank,通过大规模并行访问堆叠总带宽,天然适配连续大块的流式访存;对随机小粒度访问的延迟优化能力远弱于 CPU —— 这也是 GPU 访存优化必须高度重视连续布局的底层原因。
映射策略决定的三大关键性能指标
1. Row Buffer 命中率
DRAM 每个 Bank 内部自带 Row Buffer(行缓冲区):
一次 ACT(激活)命令,会把一整行(完整 Row,几 KB ~ 几十 KB)的数据加载到 Row Buffer;
后续同 Bank 同行内的列读 / 写(READ/WRITE),不需要再次 ACT,直接从 Row Buffer 读取数据,速度极快;
访问同行以外的其他行,必须先发 PRE(预充电)关闭当前行,再 ACT 激活新行,硬件开销巨大。
命中定义:本次访问和上一次访问属于同一个 Bank + 同一个 Row,无需重新激活行。
命中率的核心成因:
地址映射将连续虚拟 / 物理地址排布在同一行 → 顺序访问(数组遍历、流式 IO)持续命中 Buffer,命中率极高;
地址跨行随机跳转 → 每次访问都要触发 ACT/PRE,命中率暴跌。
2. Bank 并发度
DRAM 内部分割成多个独立 Bank(常见 4/8/16 个 Bank),不同 Bank 可以独立执行 ACT、读、写、预充电操作,实现硬件真正并行;同一 Bank 内的命令只能串行排队执行,无法并发。
请求均匀分散到各个 Bank → 多 Bank 同时工作,总线吞吐叠加,并发度高;
大量请求集中在同一个 Bank,其余 Bank 空闲 → 命令串行排队,并发度极低,带宽被严重浪费。
3. 访问延迟
同一个 Bank 上,两次连续内存访问指向不同 Row 时,会触发 Row Conflict(行冲突):上一次访问行 R0,本次要访问同 Bank 行 R1,必须执行「PRE(预充电)→ ACT(激活新行)→ 读写」的完整流程,多出两条硬件命令,产生固定的额外延迟开销。
同 Bank 同行的连续访问则无冲突,可直接读写,无额外开销。
常见地址映射模式对比
工业界常见的地址映射策略主要分为两类,二者的优化方向完全不同:
行优先映射(Row-Bank-Column):连续地址先填满同一行的所有列,再切换到下一个 Bank,最后切换行。
特点:连续地址高度集中在同一行,Row Buffer 命中率极高;但连续访问会集中在少量 Bank,并发度低。
适用场景:小尺寸热点数据、随机访问密集的查找表类算子。
Bank 交错映射(Bank-Row-Column):连续地址先交错分配到不同 Bank,填满所有 Bank 的同一行后,再切换到下一行。
特点:连续访问均匀分散到所有 Bank,多 Bank 并发拉满带宽;但单 Bank 的行命中率低。
适用场景:大张量流式搬运、矩阵乘、卷积等带宽受限的大块访存算子。
现代 GPU 的 HBM 控制器普遍采用Channel+Bank 双层交错的映射策略,优先保证大流量连续访问的带宽发挥,这也是编程中默认要做连续对齐访存的底层逻辑。
软件层面的优化启示
了解 MC 的地址映射策略,可以指导我们设计更优的数据布局(Data Layout)。例如,若 MC 采用 Row–Bank–Column 映射,在 CUDA 中将热点数据按 Row 对齐并连续存放,可以最大化 Row Hit 率 —— 这种映射模式下,连续内存地址会先填满同一 Row 的所有列,才会切换到下一个 Bank,同一个 Bank 内部的连续地址高度集中在同一行。
以典型参数为例(⚠️ 具体数值请以厂商技术文档为准):Channel Interleave = 256B,Bank Interleave = 1KB,共 4 个 Channel。一段 4KB 的连续读取请求,会被拆分为 16×256B 的小块,按 CH0→CH1→CH2→CH3 循环轮转分配到 4 个内存通道;每 4 个 256B(合计 1KB)为一组做 Bank 交错,4 组 1KB 数据分别落入不同 Bank。
由此可提炼三条工程优化规则:
Kernel 单次批量处理的数据总量设置为 256/512/1024B 等整数倍,同时地址对齐到 256B;
一个 Warp(32 线程)批量读取的数据总大小设为 256B 整数倍,保证单次访存事务刚好填满整数个 Channel 块;
流式大规模数据搬运、矩阵乘、卷积等带宽受限算子:严格遵守 256B 对齐,牺牲少量 Row Buffer 命中率换取满带宽;极小热点查找表、反复随机小粒度查表算子:不必强求通道对齐,转而做行内连续排布提升 Row Hit 率。
常见认知误区
不要盲目追求 100% 的 Row Buffer 命中率。对于带宽受限的大块流式访存,满带宽的多 Bank 并发收益,远高于行命中率提升带来的延迟收益;过度追求行连续反而会导致 Bank/Channel 负载不均,总带宽上不去。只有小粒度、高频率的随机访问场景,才应以行命中率为首要优化目标。
三、高性能 GPU 访存编程实战的 12 条核心法则
内存控制器的地址映射规律与 GPU 访存瓶颈根源,是所有访存优化的底层硬件依据。而 12 条核心编程法则围绕「匹配硬件特性、减少全局访存、隐藏访存延迟」三大目标展开。
核心观点
GPU 计算核心的算力远超 HBM 的喂数据速度 —— 这是所有访存优化的根本意义。
法则一:永远保证 Stride-1 访存(Memory Coalescing)
硬件规则:当一个 Warp 的 32 个线程访问连续的内存地址时,GPU 硬件会将这些请求合并为最少数量的 32 字节事务(Transaction)。
编程守则:
Block 维度(尤其是
blockDim.x)必须是 32 的倍数(CUDA 硬件调度最小单元是 Warp = 固定 32 线程)。cudaMalloc返回的地址保证 256 字节对齐。
现代 NVIDIA GPU 的全局内存事务以 L2 缓存的 32 字节扇区为最小颗粒度,4 个扇区组成一条 128 字节缓存行(硬件实际发起的事务基本单位);而cudaMalloc返回的地址按 256 字节对齐,是内存控制器为进一步降低跨缓存行/跨 Bank 概率而设定的更宽松的对齐目标,并不意味着每次事务都是 256 字节——三者是包含关系:32B(最小可调度颗粒)⊂ 128B(实际事务/缓存行单位)⊂ 256B(分配对齐粒度,用于减少边界问题)。Warp 内连续访存时,硬件会自动合并请求,理想状态下 32 个线程的连续 float 访问(共 128 字节)仅触发 1 个缓存行、4 个扇区事务;非连续访问会导致扇区浪费,带宽效率近似随步长线性下降。对于跨步访问,带宽效率近似为1 / stride(stride 为访问步长,单位为元素个数);当步长达到 32 时,等价于随机访存,带宽利用率通常不足 10%。
补充:非 CUDA 推理芯片的对齐要求
⚠️ 以下讨论针对搭载 DDR/LPDDR 作为系统主存的非 CUDA 推理芯片场景,其存储介质、Row Buffer 大小、地址空间管理方式与 GPU+HBM 场景存在差异,请勿与前文 HBM 参数混用对照。
在搭载 DDR/LPDDR 作为系统主存的非 CUDA 推理芯片中,权重张量的地址与大小对齐要求除非 CUDA 推理芯片本身要求意外,考虑以下三点:
DRAM 硬件层面:DDR4/DDR5/LPDDR 单 Bank 行缓冲区(Row Buffer)常见规格为 2KB/4KB/8KB,具体大小由颗粒密度、位宽与 Bank 架构决定;不对齐会导致长 Burst DMA 加载跨行,触发频繁的预充电、行重激活操作,同 Bank 访存串行阻塞,访存延迟显著抬升。
操作系统虚拟内存层面:Linux 默认内存页大小为 4KB;对齐后权重分片不会跨物理内存页,TLB 虚实地址映射仅需一次查询,减少 TLB Miss 与缺页开销(拥有独立地址空间的片上设备显存如 GPGPU 的 HBM 除外)。
AXI DMA 传输约束:AXI 总线标准的长 Burst 传输禁止跨越 4KB 页边界;未对齐张量会被硬件自动拆成两次独立 Burst 传输,额外增加总线握手开销,部分嵌入式 DMA 控制器还会出现传输异常。
法则二:用 Shared Memory Tiling 实现数据复用
核心思想:将 HBM 中的数据分块(Tile)加载到 Shared Memory,在片上进行多次重复计算,避免反复访问 HBM。
⚠️ GPU Tiling vs NPU Tiling 核心差异:GPU Tiling:可选优化,不分块能正常算出正确结果,只是访存爆炸、性能拉胯,核心只为削减 HBM 访存次数;NPU Tiling:强制必要步骤,不分块会因片上存储上限导致算子无法运行、结果非法,既要保证计算可执行(正确性),同时顺带减少外部内存访存。
分层优化细则:
基础 Tiling(算法层优化)
将 HBM 全局内存中的张量分块分批加载到片上 Shared Memory,同一块片上缓存数据被多次复用计算,规避高频次重复访问高延迟的 HBM,有效削减全局内存总访存量。空间并行(算力横向扩张)
把拆分后的各个独立 Tile 分发至多个独立 SM 并行运算,完成线程块并行扩张,充分利用多 SM 硬件并行算力。时间流水(双重收益:隐藏搬移延迟 + 规避内存溢出)
单个 SM 内部搭建流水线,借助 Ring Buffer(环形缓冲区)异步并发执行:下一块数据 DMA 搬运、上一块数据片上计算,二者时间重叠,隐藏数据传输时延,消除计算单元 IO 空闲等待;
同时采用流式逐块加载方案,任意时刻片上仅常驻少量 Tile 数据,严格约束 Shared Memory 占用峰值,既防止片上缓存溢出,也能避免设备显存发生 OOM。
Shared Memory 优化的核心性能陷阱是 Bank 冲突。Shared Memory 被划分为 32 个独立 Bank(注:此处为片上存储的独立硬件单元,与第二章 HBM/DRAM 的 Bank 为不同物理实体,仅复用同一术语),每个 Bank 单周期仅能响应一次访问。当 Warp 内多个线程访问同一 Bank 的不同地址时,请求会串行执行,最坏情况(32 线程全冲突)访存延迟扩大 32 倍。
典型规避手段包括:分块矩阵增加列方向 Padding(如 Tile 列数 +1),让相邻行的同列地址自然错开 Bank;矩阵转置类操作采用分块转置 + 对角线访问,避免全 Bank 冲突;对 FP16/BF16 等 16 位数据,利用每个 Bank 的双字宽特性做交错排布;使用__restrict__指针限定符辅助编译器做地址优化。
法则三:用 cp.async 实现异步拷贝,隐藏访存延迟
核心思想:cp.async指令直接从 Global Memory 拷贝数据到 Shared Memory,绕过寄存器和 L1 Cache,同时允许计算与数据传输重叠执行。
在 Hopper 及更新架构中,该能力已升级为 TMA(Tensor Memory Accelerator)硬件级异步 DMA 引擎,支持批量、多维张量的全局内存→Shared Memory 拷贝,无需线程逐条发起拷贝指令,可完全解放计算单元;同时支持任意维度的张量切片、自动 Padding、格式转换,是大尺寸 Tile、复杂布局拷贝的标准方案。
法则四:用 Warp Shuffle 替代 Shared Memory Reduction
核心思想:Warp 内的 32 个线程可以通过__shfl_*原语直接交换寄存器中的数据,无需经过 Shared Memory 的 load/store 路径。
为什么更快:Shared Memory Reduction 需要"写入 Shared Memory →__syncthreads()同步 → 读取 Shared Memory"三步,同步开销和片上存储访问延迟都比寄存器操作高一个量级;Warp Shuffle 利用 SIMT 锁步执行的特性,让 32 个线程在同一周期内通过专用交叉网络直接交换寄存器值,省去了同步屏障和存储访问。
典型用法:
// Warp 内树形归约求和,无需 Shared Memory,无需 __syncthreadsfor(intoffset=16;offset>0;offset>>=1){val+=__shfl_down_sync(0xFFFFFFFF,val,offset);}// 归约后 lane 0 持有 32 个线程的求和结果⚠️适用边界:仅能在单个 Warp(32 线程)内部交换数据,跨 Warp 的归约仍需 Shared Memory 或 Warp 间同步原语(如__syncwarp配合多级归约:先 Warp 内 Shuffle 归约,再用少量 Shared Memory 做 Warp 间二次归约)。
⚠️隐含要求:__shfl_*系列函数自 CUDA 9.0 起要求显式传入掩码参数(如0xFFFFFFFF)标识参与的线程,Warp Divergence 场景下掩码必须准确反映实际活跃线程,否则会读到未定义值。
法则五:为 Tensor Core 准备正确的数据布局
核心思想:Tensor Core 执行矩阵乘累加(MMA)操作时需要特定的数据分块和内存布局。错误的布局会导致 Shared Memory Bank Conflict 或寄存器 Shuffle 开销。
为什么有特殊要求:Tensor Core 是按"Warp 整体"为单位发起 MMA 指令的(如mma.syncPTX 指令),它要求参与运算的 16x16(或更大)子矩阵分片以特定的线程到寄存器映射关系驻留——这个映射关系由硬件固定,不是程序员可以随意指定的,因此 Shared Memory 中的 Tile 必须按该映射要求的 layout(行主序/列主序、特定的 swizzle 模式)预先排布,否则加载到寄存器时需要额外的 Shuffle 操作搬运数据,或者引发 Bank Conflict。
编程启示:
- 优先使用 CUTLASS、cuBLAS 等已经封装好 Tensor Core 数据搬运逻辑的库,而非手写 PTX
mma.sync; - 若必须手写,需按架构对应的 Warp-Level MMA Shape(如 Ampere 的
m16n8k16)和官方 Swizzle 模式排布 Shared Memory; - FP16/BF16/INT8 等不同精度的 MMA 指令对应的 Tile Shape 不同,混合精度场景需要分别适配。
法则六:AoS → SoA 数据布局转换
问题:结构体数组(Array of Structures, AoS)在 GPU 上导致严重的 Stride-N 非合并访存。
// ❌ AoS:每个线程读 .x 字段时步长 = sizeof(Particle) = 12 bytesstructParticle{floatx,y,z;};Particle particles[N];floatpx=particles[i].x;// Stride-3,带宽效率 ~33%// ✅ SoA:每个字段独立连续存放structParticles{floatx[N],y[N],z[N];};floatpx=particles.x[i];// Stride-1,带宽效率 ~95%性能差距:AoS 的非合并访存可比 SoA 的合并访存慢 10~20 倍。
混合方案:当所有字段总是一起被访问时,使用float4向量化加载:
// 每个线程加载一个 16 字节对齐的 float4 → 完美合并float4 p=reinterpret_cast<float4*>(particles)[i];// p.x, p.y, p.z, p.w 一次性获取法则七:利用 L2 Cache Residency 控制热点数据驻留
核心思想:在 Ampere/Hopper 架构上,手动指定哪些数据应持久驻留在 L2 Cache 中,避免被流式数据冲刷。
为什么需要手动控制:L2 Cache 默认由硬件按 LRU 等策略自动管理,但当 Kernel 同时存在"小块高频复用的热点数据"(如 Attention 的 KV Cache 片段)和"大块只读一次的流式数据"(如逐 Tile 搬入的激活值)时,硬件默认策略可能让流式数据把热点数据"冲刷"出 L2,导致热点数据反复从 HBM 重新加载。
实现方式:CUDA 11+ 提供cudaStreamAttrValue的accessPolicyWindow,可以为指定的全局内存地址区间设置 L2 Persisting 属性,并配置命中比例(hitRatio),让该区间内一定比例的访问优先保留在 L2,而非被普通流式访问替换。
⚠️风险点:
- L2 Persisting 区域会挤占其余 Kernel/Stream 可用的 L2 容量,需要结合实际 L2 总容量(如 A100 的 40MB)和热点数据大小评估收益,过度占用会拖慢其他无关访存;
hitRatio设置不当(过高)可能导致非目标数据被异常驻留,需要配合 Nsight Compute 的 L2 Hit Rate 指标验证实际效果,而非凭直觉设置。
法则八:LLM 推理中的 KV Cache 优化 ——PagedAttention
编程启示:如果你正在构建 LLM 推理引擎,KV Cache 的内存管理策略对吞吐量的影响可能大于算子本身的优化。优先考虑 PagedAttention 式的分块管理。
核心问题:传统实现为每个请求预分配"最大序列长度"的连续 KV Cache 显存,但实际请求长度差异很大,导致大量显存碎片化浪费(类似传统操作系统出现之前的连续内存分配问题)。
PagedAttention 的解法:借鉴操作系统虚拟内存分页的思路,把 KV Cache 切分为固定大小的物理 Block(如每块存 16 个 token 的 KV),每个请求按需申请 Block,通过一张逻辑→物理 Block 的映射表寻址;不同请求甚至可以共享相同内容的 Block(如多个请求共享同一段 System Prompt 的 KV,仅需引用而非复制)。
⚠️代价与局限:
- 分页带来的间接寻址(Block Table 查表)相比连续内存的直接寻址有额外开销,需要专门设计 Kernel(如 vLLM 的 PagedAttention Kernel)来高效处理非连续 Block 的访存合并,否则反而会破坏法则一强调的 Stride-1 合并访存;
- Block 大小是吞吐与碎片率的权衡参数:Block 越小,碎片越少但 Block Table 越大、间接寻址开销越高;Block 越大则相反,需要结合实际请求长度分布调优。
⚠️ PagedAttention 分页 KV 的核心目标是提升多请求并发吞吐量;在空载单请求场景会小幅牺牲推理延迟,但真实线上高并发场景能消除请求排队长尾延迟,整体平均延迟反而更优,工程上该取舍完全划算。
法则九:掌握性能分析工具,用数据驱动优化
不要猜测,要 Profiling。
Roofline 分析:
算术强度 Arithmetic Intensity = FLOPs / Bytes Accessed
如果 Kernel 在 Memory Roofline 之下 → 优先优化访存模式
如果 Kernel 在 Compute Roofline 之下 → 优先优化计算(如使用 Tensor Core)
在实际 Profiling 中,可通过 Nsight Compute 重点关注以下核心访存指标,精准定位优化方向:
带宽类:DRAM Bandwidth Utilization、Memory Throughput,直接反映显存带宽利用水平
缓存类:L2 Cache Hit Rate、Shared Memory Bank Conflict Rate,反映各缓存层级的执行效率
合并效率:理想全局内存事务数 / 实际全局内存事务数,比值低于 80% 说明访存合并存在明显优化空间
寄存器溢出:Local Memory Load/Store 指标不为 0 时,说明存在寄存器溢出到片外显存的性能降级
性能优化需形成完整闭环:先通过 Roofline 分析定位瓶颈类型(访存受限 / 计算受限),再针对性匹配优化法则 —— 访存受限优先优化访存合并、片上复用与数据布局;计算受限优先适配 Tensor Core、减少分支发散与指令开销;优化后通过 Profiling 指标验证收益,动态调整分块粒度、对齐参数等,避免单一维度过度优化导致其他维度性能下降。
法则十:软件流水线 —— 让数据搬运和计算永远并行
核心思想:通过多 Buffer 交替使用,确保任何时刻 GPU 都在同时做两件事:计算当前 Tile + 加载下一个 Tile。
最佳 Buffer 数量:
Double Buffering(2 个 Buffer):当 Load Time ≈ Compute Time 时最优
Triple Buffering(3 个 Buffer):当 Load Time ≠ Compute Time 时消除流水线气泡
N-Stage(N > 3):仅在极特殊场景(如极高延迟的远端内存)下使用
法则十一:缓存感知编程 —— 用加载限定符控制数据在 Cache 中的行为
核心思想:GPU 的 L1/L2 Cache 是硬件自动管理的,但 PTX 提供了加载 / 存储限定符,允许程序员精细控制数据在缓存层级中的驻留行为。
为什么需要这种控制:硬件默认的缓存策略是通用启发式(如 LRU 近似),对于程序员已经明确知道访问模式的场景(比如"这块数据只读一次,读完即弹出"或"这块数据要被多次复用,必须留住"),通用策略往往不是最优选择,PTX 限定符把这部分决策权交还给程序员。
常见限定符(CUDA 中通过__ldg、内联 PTX 或cuda::memory_access等方式暴露):
ca(cache at all levels):默认行为,正常写入 L1/L2;cg(cache global,仅 L2):跳过 L1,适合多个 Warp 共享但单线程内不复用的数据,避免无意义占用 L1;cs(cache streaming):提示硬件该数据"用一次就丢",优先淘汰,避免冲刷掉其他热点数据(与法则七的 L2 Persisting 是互补手段:cs主动"低优先级"标记流式数据,Persisting 是主动"高优先级"标记热点数据);lu(last use):读取后即标记该缓存行可丢弃,适合归约类操作中"读完即不再需要"的中间数据。
⚠️局限:这些限定符是"提示"(hint)而非强制指令,硬件仍可能根据实际负载情况不严格遵守;效果依架构差异较大,⚠️ 待结合具体架构的 Nsight Compute Cache 命中率指标验证,不应假设设置后必然生效。
法则十二:Kernel 融合与 L2 感知分块 —— 最大化缓存时间局部性
核心思想:独立的 Kernel 调用会导致数据在 HBM 和 L2 之间反复搬运。通过 Kernel 融合和 L2 感知的分块策略,可以将数据保留在缓存中多次复用。
为什么独立 Kernel 会浪费带宽:每个 Kernel 启动是一次独立的调度单元,Kernel A 写出的中间结果即使马上要被 Kernel B 读取,两者之间也隔着一次完整的 HBM 写回 + 读取(除非数据小到能稳定留在 L2,但 L2 容量有限且会被其他并发 Kernel/Stream 冲刷)。典型例子是朴素实现的 Attention:QK^T、Softmax、与 V 相乘若拆成三个独立 Kernel,中间的 Attention Score 矩阵(O(N²) 大小)要完整写入再读出 HBM 两次。
融合策略:
- 算子融合(Kernel Fusion):把多个逐元素/局部依赖的算子合并进同一个 Kernel,中间结果直接保留在寄存器或 Shared Memory,不落 HBM,FlashAttention 是这一思路的典型代表(融合 QK^T、Softmax、AV 三步,配合在线 Softmax 算法避免显式物化完整 Score 矩阵);
- L2 感知分块:当多个 Kernel 难以完全融合时,调整分块顺序和大小,让相邻 Kernel 处理同一批 Tile 的时间窗口足够接近,使中间数据有更大概率仍留在 L2 中被复用,而不是被完全冲刷出去。
这正是法则九 Roofline 分析中、把算子从 Memory Roofline 推向 Compute Roofline 的典型手段:融合后总访存量下降,算术强度(FLOPs / Bytes Accessed)随之提升,Kernel 在 Roofline 图上的位置也随之右移。
FlashAttention 系列技术是该法则的典型落地范例,其不同变种的优化思路可泛化至所有高访存算子,具备普适的参考价值:
- 按需物化中间结果:不完整物化大尺寸中间矩阵,仅在片上保留当前计算必需的片段,用增量计算替代全量内存读写,从根源削减全局访存总量;如把 Q、K 切成小片段,每次只拿一小段 Q、一小段 K,只算出这一小块 S,立刻就地在片上缓存算 softmax、乘对应 V 片段;算完直接丢弃这块 S,不保存完整大矩阵;
- 层级化分块复用:采用「全局内存→L2→共享内存→寄存器」的多级 Tiling 策略,匹配各存储层级的容量与延迟特性,让数据在尽可能低的层级完成复用,最大化缓存利用效率;
- 访存路径裁剪:针对动态、稀疏的计算场景,仅加载当前步骤有效数据,过滤无关内存访问,进一步降低无效带宽消耗。如只加载有效 token 对应的数据块,填充占位符、无交互的稀疏位置直接跳过,不分配、不读取这部分显存;
法则补充
除上述 12 条核心法则外,工业界更多优秀实践可提炼为可复用的泛化优化逻辑,无需绑定特定硬件或框架:
- 稀疏性利用:识别算子的数据稀疏性与计算稀疏性,通过硬件适配的稀疏存储格式、稀疏访存指令过滤无效访问,提升有效带宽与算力的利用率,而非单纯追求绝对峰值带宽。如一段句子长度参差不齐,短句子末尾会补一堆无意义的占位 0,通过提前标记哪些是有效文字、哪些是填充 0,读取内存时直接跳过 0 区域,不加载、不运算空白占位符,同样算力只算真实文本。
- 精度 - 带宽权衡:在精度损失可控的前提下,采用低精度数据格式或压缩存储方案,以精度权衡换取访存带宽提升;同时匹配硬件原生精度指令,避免额外的格式转换开销。如图像像素原本用 32 位浮点存储特征图,换成 FP16,全程 FP16 存储 + 卷积计算,硬件本身支持 FP16 运算,不用来回高低精度转换,带宽压力砍半,图片识别准确率几乎不变。但如果硬件不支持 INT8 指令,强行把 FP32 转 INT8,每一步都要额外换算,反而更慢,故强调要匹配硬件原生精度。
- 架构感知调优:针对目标硬件的核心特性(显存带宽、SM 规模、计算单元能力)动态调整分块粒度、访存策略与并行度,让软件设计匹配硬件优势,避免硬件资源浪费。
- 多流异步调度:将算子拆分为访存、计算等独立阶段,通过多流异步调度实现不同阶段的时空重叠,进一步隐藏访存延迟,提升硬件资源的整体利用率。
LLM Decode 阶段天生极度吃显存带宽(Memory Bound),且单 Token 输入的特性,让「稀疏性利用」「精度 - 带宽权衡」两条优化收益远大于训练 / 预填充(Prefill)阶段。
本质与总结
访存优化的本质,是在物理硬件的约束条件下,最大化每一次 ACT/PRE 周期的有效数据产出。所有优化技巧均可归为三大核心逻辑:匹配硬件特性、提升数据复用、隐藏访存延迟,最终指向「**最大化有效带宽利用率」**的核心目标。无论你是在调优一个 GEMM 算子、设计 FlashAttention 的 Tiling 策略,还是优化 LLM 推理的 KV Cache 管理,最终都回到同一个问题 —— 如何让数据在对的时间、以对的粒度、到达对的地方。
理解硬件的真相,是写出高性能代码的第一步。而持续的 Profiling 和迭代,是让它跑满带宽的唯一途径。
参考资料
深入浅出计算机组成原理
LLM访存优化(八):从 Coalescing 到 FlashAttention 的 12 条核心法则
LLM访存优化(四):地址映射、交织策略与请求调度