cuda编程笔记(34)-- 内存访问控制与缓存提示

cuda编程笔记(23)-- __threadfence()和__ldg()-CSDN博客,提到了__ldg,本文将cuda里内存访问的相关api都介绍一下

__ldg

功能:从 global memory 通过只读缓存(read-only cache)加载数据。

cpp 复制代码
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 的压力,提高带宽利用。

cpp 复制代码
__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.ELDG.G 指令,与 *ptr 无区别。

__prefetch_global

功能:提前将 global memory 数据加载到 cache,隐藏访问延迟。

cpp 复制代码
template <typename T>
__device__ void __prefetch_global(const T* ptr);

说明

  • Ampere 架构开始支持。

  • 只是 hint,不保证立即访问到数据,只是告诉硬件"接下来可能用到"。

  • 对延迟敏感的循环访问场景非常有用。

cpp 复制代码
__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
cpp 复制代码
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。

  • 原型(示意)
cpp 复制代码
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"。

cpp 复制代码
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 的存储层级(从慢到快)大致是:

cpp 复制代码
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 CacheGlobal 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 数量:

cpp 复制代码
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
std::cout << "SM数量: " << prop.multiProcessorCount << std::endl;

比如3090输出82

"核函数一次最多启动的 block 数不能超过 SM 数"

这是 不对的
block 数可以远远多于 SM 数 ,只不过 同时在 GPU 上运行的 block 数受到 SM 数量和资源限制

正确理解

核函数启动时你写的:

cpp 复制代码
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。
相关推荐
报错小能手4 小时前
C++笔记(面向对象)深赋值 浅赋值
c++·笔记·学习
superlls4 小时前
(场景题)怎么实现数据的批量插入?
笔记·mybatis
繁花与尘埃4 小时前
CSS引入方式(本文为个人学习笔记,内容整理自哔哩哔哩UP主【非学者勿扰】的公开课程。 > 所有知识点归属原作者,仅作非商业用途分享)
css·笔记·学习
_落纸5 小时前
《自动控制原理》第 3 章 线性控制系统的运动分析:3.6、3.7
笔记·自动化
colus_SEU6 小时前
【计算机网络笔记】第一章 计算机网络导论
笔记·计算机网络·1024程序员节
koo3647 小时前
李宏毅机器学习笔记33
人工智能·笔记·机器学习
IT伟哥7 小时前
软件设计师重点笔记-3
笔记·软考·软件设计师·软考备考
航Hang*7 小时前
第1章:初识Linux系统——第8节:查看/修改权限控制和ACL
linux·运维·服务器·笔记·操作系统
Peace & Love4878 小时前
C++初阶 -- 模拟实现list
开发语言·c++·笔记