cuda编程笔记(34)-- 内存访问控制与缓存提示
在cuda编程笔记(23)-- __threadfence()和__ldg()-CSDN博客,提到了__ldg,本文将cuda里内存访问的相关api都介绍一下
__ldg
功能:从 global memory 通过只读缓存(read-only cache)加载数据。
template <typename T>
__device__ __forceinline__ T __ldg(const T* ptr);
注意这并不是函数原型,但是你可以这么理解,T并不是所有类型都能适配的。
-
在老架构(Maxwell/Kepler)上,它会使用 LDG 指令,利用只读缓存(texture cache / L1 read-only)。
-
在现代架构(Pascal+)仍然有效,但新架构 L2 的自动缓存更智能。
-
应用场景:当你的 global memory 数据只读且多线程共享,使用
__ldg可以减少对 L1 的压力,提高带宽利用。
__global__ void kernel(const float* data, float* out) {int idx = threadIdx.x + blockIdx.x * blockDim.x;float val = __ldg(&data[idx]);out[idx] = val * 2.0f;
}
| 架构代号 | Compute Capability | Read-Only Cache 状态 | __ldg() 行为 |
|---|---|---|---|
| Kepler (GK110) | 3.5 | ✅ 独立的只读缓存 | 有效,大幅优化 |
| Maxwell / Pascal | 5.x / 6.x | ✅ 仍保留,只读缓存共享与 L1 | 有效,但作用减弱 |
| Volta / Turing / Ampere | 7.x / 8.x | ⚠️ 与 L1 Cache 合并 | __ldg() 存在,但与普通 *ptr 效果几乎一样 |
| Hopper (H100) | 9.0 | ❌ 不再单独实现 | __ldg() 仍编译,但仅作普通 load |
在 Ampere 及以后:
-
Read-only cache 不再是独立单元;
-
所有 load 都经过一个统一的 L1 Cache(Unified Data Cache);
-
__ldg()仍然存在,编译器不会报错; -
但 PTX 层会退化为普通的
LDG.E或LDG.G指令,与*ptr无区别。
__prefetch_global
功能:提前将 global memory 数据加载到 cache,隐藏访问延迟。
template <typename T>
__device__ void __prefetch_global(const T* ptr);
说明:
-
Ampere 架构开始支持。
-
只是 hint,不保证立即访问到数据,只是告诉硬件“接下来可能用到”。
-
对延迟敏感的循环访问场景非常有用。
__global__ void kernel(float* data) {int idx = threadIdx.x + blockIdx.x * blockDim.x;__prefetch_global(&data[idx]);float val = data[idx]; // 实际访问时可能已经在 L1/L2
}
但是普通的游戏显卡,比如3090,4090可能不支持
| 架构 | GPU类型 | SM版本 | 是否支持 __prefetch_global() | 说明 |
|---|---|---|---|---|
| A100 | 数据中心 | sm_80 | ✅ 支持 | pipeline copy / prefetch 全支持 |
| H100 | 数据中心 | sm_90 | ✅ 支持 | 支持更多 pipeline 指令 |
| RTX 3090 / 3080 | 消费级 | sm_86 | ❌ 不支持 | 指令未开放,prefetch 会报错 |
| RTX 4090 (Ada) | 消费级 | sm_89 | ❌ 不支持 | 同样未公开 prefetch API |
__ldcs() / __ldcg() / __ldca()
功能:控制加载时的缓存策略。
| API | 含义 |
|---|---|
__ldcs() | load via streaming cache(只 L2) |
__ldcg() | load global memory (cached) |
__ldca() | load via L1 cache |
template <typename T>
__device__ T __ldcs(const T* ptr);
template <typename T>
__device__ T __ldcg(const T* ptr);
template <typename T>
__device__ T __ldca(const T* ptr);
说明:
-
Ampere/Hopper 架构提供,允许程序员控制 L1/L2 的命中策略。
-
对优化多线程数据共享、减少 L1 冲突有帮助。
-
对大多数通用算法不是必需,更多是高性能调优手段。
__stcs() / __stcg() / __stwb()
功能:控制 store(写)时缓存策略。
| API | 含义 |
|---|---|
__stcs() | streaming store (L2 only) |
__stcg() | cached store |
__stwb() | write-back store |
说明:
-
可以降低 L1 负担或者控制写合并策略。
-
对性能优化高并发写场景有时有效。
__pipeline_memcpy_async
功能:异步拷贝数据到 shared memory / register / L2 pipeline。
-
原型(示意):
template <typename T>
__device__ void __pipeline_memcpy_async(T* dst, const T* src, size_t n);
说明:
-
Ampere+ GPU 支持。
-
类似
cp.async指令,但 API 级别,更安全。 -
允许 overlap memory copy 与 compute,隐藏 global memory 访问延迟。
-
必须配合
__pipeline_commit()和__pipeline_wait_prior()控制阶段。
__pipeline_commit() / __pipeline_wait_prior()
功能:控制 pipeline 的异步拷贝阶段。
-
用法:
-
__pipeline_commit():提交之前的 async copy 指令到 pipeline。 -
__pipeline_wait_prior():等待 pipeline 中之前提交的 copy 完成。
-
-
说明:
-
通常配合循环或双缓冲使用,实现 compute 与 copy 的 overlap。
-
相当于 Ampere+ 的“软件可控 cp.async”。
-
for(int i=0;i<N;i+=tile){__pipeline_memcpy_async(sm_tile, &gmem[i], tile);__pipeline_commit();__pipeline_wait_prior(); // 等待上一次拷贝完成compute(sm_tile);
}
GPU缓存结构
CUDA GPU 的存储层级(从慢到快)大致是:
Global Memory (DRAM)│▼L2 Cache ← 所有 SM 共享│┌──────┴──────────┐│ │▼ ▼
L1 Data Cache Read-Only Cache (texture)│▼
Registers / Shared Memory
-
Global Memory (DRAM):显存,全局可访问,但延迟高(几百个 cycles)。
-
L2 Cache:芯片级共享缓存,所有 SM 都能访问,延迟较低(几十个 cycles)。
-
L1 Cache:每个 SM 独立的一级缓存,延迟更低(10-20 cycles)。
-
Read-Only Cache:专门优化只读访问(只在老架构中单独存在,现在常与 L1 合并或共享)。
-
Registers / Shared Memory:线程或线程块级的高速存储,延迟极低。
我们来对照看下不同指令的缓存行为。
| 指令 / API | 访问路径 | 缓存层级 | 特点 | 典型用途 |
|---|---|---|---|---|
__ldg | Global → Read-only cache → Register | L2 + Read-only(L1T) | 只读缓存,不污染普通 L1 | 共享常量、查表数据 |
__ldcg | Global → L2 → L1 → Reg | L2 + L1 | 默认路径(全缓存) | 一般数据加载 |
__ldca | Global → L1 only → Reg | L1 only(不走 L2) | 强制优先用 L1,L2可不命中 | 临时数据,多次局部访问 |
__ldcs | Global → L2 only → Reg | L2 only(跳过 L1) | 不污染 L1 cache | 大流量读取(streaming read) |
但实际上,这些全是“建议式控制”,不是显式管理。
L1/L2/只读缓存的对比总结
| 缓存层 | 作用范围 | 容量 | 一致性 | 可写 | 典型用途 |
|---|---|---|---|---|---|
| L1 Data Cache | 每个 SM 独立 | 小(128KB~192KB) | 不全局一致 | 可写 | 局部数据缓存 |
| Read-only Cache | 每个 SM 独立 | 小(48KB~128KB) | 无需一致性 | 只读 | 常量、查表 |
| L2 Cache | 全 SM 共享 | 大(几 MB) | 全局一致 | 可写 | 跨 SM 通信、共享数据 |
实际调优经验
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 常量查表 / 只读共享 | __ldg | 走 read-only cache,不污染 L1 |
| 大数据流式读 | __ldcs | 避免 L1 被大量数据污染 |
| 局部复用强(tile/block 内) | __ldca | 强制走 L1,本地复用效率高 |
| 一般数据访问 | 默认 / __ldcg | 默认行为即可 |
| Ampere+ 异步拷贝 | __pipeline_memcpy_async | 取代手动 prefetch,性能更高 |
SM 与缓存的关系
SM(Streaming Multiprocessor) 是 GPU 的计算核心单元,就像 CPU 里的“核(core)”。
一张 GPU(比如 A100、RTX 4090)内部通常有 几十到上百个 SM。
CUDA 的层级关系:
Grid → Block → Warp → Thread
而 SM 是执行的硬件实体:
| 概念 | 含义 | 对应的硬件 |
|---|---|---|
| Grid | 一次 kernel 启动的所有线程集合 | GPU 整体 |
| Block | 一组线程(可通信/同步) | 分配给一个 SM 执行 |
| Warp | 32 个线程组成的调度单元 | SM 内的调度器发射 |
| Thread | 单个执行流 | SM 内的计算核心(ALU) |
每个 block 在运行时都会被分配到某个 SM 上执行,
block 内的所有线程都在那个 SM 的共享资源(如 shared memory、L1 cache)中运行。
每个 SM 有自己独立的 L1 Cache 和 Shared Memory,所以:
-
一个 SM 里的线程块(block)可以在 shared memory 中高速共享数据;
-
不同 SM 之间的数据共享必须通过 L2 Cache 或 Global Memory;
-
L1 cache 不同步(每个 SM 自己的 L1 可能不一致);
-
L2 cache 在所有 SM 间是 一致的(coherent)。
这也就是为什么有下面这种访问策略:
| 缓存类型 | 是否 SM 共享 | 一致性 | 示例 API |
|---|---|---|---|
| L1 Cache | ❌ 独立 | 不一致 | __ldca() |
| Read-only Cache | ❌ 独立 | 不一致 | __ldg() |
| L2 Cache | ✅ 所有 SM 共享 | 一致 | __ldcs() |
| Global Memory | ✅ 所有 SM 共享 | 一致 | — |
SM的小知识
可以用下面的代码查看自己 GPU 的 SM 数量:
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
std::cout << "SM数量: " << prop.multiProcessorCount << std::endl;
比如3090输出82
“核函数一次最多启动的 block 数不能超过 SM 数”
这是 不对的。
block 数可以远远多于 SM 数,只不过 同时在 GPU 上运行的 block 数受到 SM 数量和资源限制。
正确理解
核函数启动时你写的:
myKernel<<<numBlocks, threadsPerBlock>>>(...);
里指定的 numBlocks 是整个 Grid 的大小。
GPU 会把这些 block 分批调度到 SM 上执行。
举个具体例子(比如 A100):
-
每个 SM 最多同时运行 2048 线程
-
每个 block 有 256 线程
-
那么每个 SM 同时最多能运行 8 个 block
-
若总共有 108 个 SM,则同时最多有
108 × 8 = 864个 block 在执行
但如果你启动了 10,000 个 block,其他的会排队等前面的执行完再上。
| 层次 | 存储位置 | 是否共享 | 说明 |
|---|---|---|---|
| 寄存器 (Registers) | 每个线程私有 | ❌ | 每个线程自己的最快速存储空间(延迟 < 10 cycles)。编译器自动分配。 |
| 共享内存 (Shared Memory) | 每个 Block 独占 | ✅(同 Block 内共享) | 位于 SM 内部的片上 SRAM。延迟 ~100 cycles,比 global memory 快很多。 |
| L1 Cache | 每个 SM 独占 | ✅(同 SM 上的所有 Block 共享) | 用于缓存 global memory 加载的结果,可配置与 Shared Memory 共用物理空间。 |
| L2 Cache | GPU 所有 SM 共享 | ✅(全局共享) | 位于芯片片上(on-chip),负责不同 SM 之间的数据共享与一致性。 |
| Global Memory (DRAM) | 芯片外显存 | ✅(全局共享) | 访问延迟几百到上千 cycles。 |
