【CUDA】store/load普通访存 vs 非临时(Non-Temporal)访存

https://zhuanlan.zhihu.com/p/1940804802721325499

一、背景

测试

测例 1 demo_mpi_peer_pingpong_min_fabric pingpong 过程中经常卡住

测例 2 demo_mpi_ring_ll_latency_switch_ipc_fabric 不会卡住

分析后发现:

两个测例集群同步使用的remote store 和load 是两种不同的方式:

*remote_flag = iter

while (*local_flag < iter)

__builtin_nontemporal_store

__builtin_nontemporal_load

📎demo_mpi_peer_pingpong_min_fabric.cpp

📎demo_mpi_ring_ll_latency_switch_ipc_fabric.cpp

前者使用的是:

  • remote_flag = iter; while (*local_flag < iter):是用普通访存 + 显式 flag 做同步 的 ping-pong,走正常 cache 一致性路径,有缓存、有一致性开销、延迟偏高、吞吐受 cache 限制

后者使用的是:

  • __builtin_nontemporal_store/load:是绕过 cache 的 "流式 / 一次性" 访存 ,直接写到远端显存、不污染本地 cache,延迟更低、吞吐更高、无一致性开销 ,但不保证顺序、不做缓存一致性,只适合 ping-pong 这种 "写完就走、读一次就扔" 的场景。

二、普通访存 vs 非临时(Non-Temporal)访存

1. 访存本质:普通访存 vs 非临时(Non-Temporal)访存

方式 A:while (*local_flag < iter)

  • 用的是普通全局 load/store(默认带 cache、参与一致性)。
  • 流程(以 GPU A 写 → GPU B 读为例):
    1. A:remote_flag = iter → 写远端(B)显存,先过 A 的 L1/L2 cache,再通过 NVLink/PCIe 刷到 B 显存。
    2. B:while (*local_flag < iter) → 轮询读自己的显存,读入 B 的 L1/L2 cache,反复命中 cache。
  • 特点:
    • 有缓存一致性 :GPU 间会通过 L2/PCIe/NVLink 做一致性同步,保证可见性
    • 延迟高、吞吐低:每次写要刷 cache 一致性,读一直命中 cache 但轮询本身开销大。
    • 适合需要强同步、数据会复用的场景

方式 B:__builtin_nontemporal_store / load

  • 这是编译器内建的 "非临时访存" 指令 (AMD GPU/Clang 常用,NVIDIA 也有类似ld.nontemporal/st.nontemporal)。
  • 核心语义:数据只使用一次,不要放进 cache,直接走内存总线
    • __builtin_nontemporal_store(val, ptr)跳过 L1/L2 cache,直接写显存 (远端或本地),不写回、不分配 cache 行、不触发一致性
    • __builtin_nontemporal_load(ptr)读数据但不缓存 (或仅放 L1、不入 L2,且标记为 "一次性使用"),读完即弃,不污染 cache
  • 流程(GPU A 写 → GPU B 读):
    1. A:__builtin_nontemporal_store(iter, remote_flag) → 直接通过 NVLink/PCIe 写 B 显存,不进 A 的 cache、不做一致性
    2. B:while (__builtin_nontemporal_load(local_flag) < iter) → 每次都直接读 B 显存,不进 cache,轮询但无 cache 开销。
  • 特点:
    • 无 cache、无一致性 :写不缓存、读不缓存,不保证顺序、不触发一致性协议,靠用户保证 "写完再读"。
    • 延迟低、吞吐高 :绕过 cache,直接走物理链路,带宽接近 NVLink/PCIe 理论值。
    • 适合 ping-pong 这种 "一次性读写、无复用" 场景

2. 关键差异对比表(GPU 跨卡 ping-pong)

表格

|--------------|-------------------------------------------|------------------------------------------------|
| 维度 | while (local_flag < iter) (普通访存) | __builtin_nontemporal_store/load (非临时) |
| Cache 行为 | 读写都走 L1/L2,缓存命中、一致性同步 | 完全绕过 L1/L2,不缓存、不污染 |
| 一致性保证 | 强一致,自动同步可见性 | 无一致性,用户自己保证顺序 |
| 延迟 | 高(cache + 一致性开销) | 低(直接访存,无 cache) |
| 吞吐 | 低(cache 带宽瓶颈) | 高(接近物理链路带宽) |
| 适用场景 | 数据复用、强同步、通用通信 | ping-pong、一次性读写、高带宽测试 |
| VMM 跨卡 | 支持,但
一致性开销大* | 完美匹配 VMM 直接映射,零拷贝、低延迟 |


3. 为什么在 VMM+ping-pong 里常用非临时访存?

GPU VMM(Virtual Memory Management)核心是直接映射远端 GPU 显存到本地虚拟地址,无需 CPU 中转。

  • ping-pong 本质是极小消息(仅一个 int)的高频来回:写一个数 → 对方读到 → 写回 → 循环。
  • 普通访存:每次 flag 读写都触发跨卡 cache 一致性 ,NVLink/PCIe 带宽被一致性流量占满,测不出真实链路带宽
  • 非临时访存:直接读写远端显存,无 cache、无一致性 ,流量就是纯数据流量,能测出 NVLink/PCIe 极限带宽和最低延迟

4. 风险与注意事项(非临时访存)

  • 不保证顺序 :多个 nontemporal store 可能被重排,ping-pong 里只能单 flag、单线程用
  • 无一致性 :写完必须显式屏障(如 __sync_synchronize()****) 依赖硬件自动刷写,否则对方可能读到旧值。
  • 硬件依赖 :AMD GPU(ROCm/Clang)原生支持;NVIDIA 需用 PTX 指令(ld.nontemporal/st.nontemporal),编译器内建不一定通用。

一句话总结

  • 普通 flag 循环:走 cache、强一致、延迟高,适合通用通信。
  • 非临时访存 :绕 cache、无一致、低延迟高吞吐,专为 VMM 跨卡 ping-pong 极限性能测试设计

三、两种 GPU 跨卡 PingPong 访存架构示意图

一、方式 1:普通 load/store 轮询 while (*local_flag < iter)

硬件层级通路

复制代码
GPU-A核线程
     ↓
L1 Cache → L2 Cache
     ↓
NVLink/PCIe
     ↓
GPU-B 显存
     ↓
L2 Cache → L1 Cache
     ↓
GPU-B核线程 轮询读

核心行为

  • 读写都进 L1/L2 缓存
  • 触发GPU 间 Cache 一致性协议
  • 轮询时一直命中本地 Cache,持续产生一致性广播流量
  • 多一层 Cache 转发 + 一致性握手,延迟高、有额外开销

二、方式 2:nontemporal 非临时读写

__builtin_nontemporal_store / load

硬件层级通路

复制代码
GPU-A核线程
     ↓
【跳过 L1、跳过 L2 缓存】
     ↓
直接走 内存控制器 → NVLink/PCIe
     ↓
GPU-B 显存
     ↓
【跳过 L2、跳过 L1 缓存】
     ↓
GPU-B核线程 直接读原始显存数据

核心行为

  • 完全绕过 L1/L2,不分配 Cache 行、不污染缓存
  • 不触发 Cache 一致性
  • 纯数据通路,无额外握手流量
  • 直达显存,延迟更低、能跑满物理链路带宽

三、关键差异极简对比图

复制代码
普通访存: 核 → L1 → L2 → 链路 → 显存 → L2 → L1 → 核
非临时:  核  ---------------------直达------------------→ 显存 ---------------------直达------------------→ 核
          (无Cache中间层、无一致性开销)

极简大白话

普通 load/store:数据必走 L1→L2,会缓存驻留,还要维护多 GPU 缓存一致性,有额外开销、容易有缓存可见性问题。

nontemporal 内置:强制不走各级 Cache ,线程 <--> 内存 / 远端显存 直连通路,不缓存、不驻留、不参与一致性协议

四、硬件结构

L1/L2 和内存,不是硬串联必走通路 不是:CPU/GPU → L1 → L2 → 内存 必须逐级过

真实架构是:CPU/GPU 核心,同时直连 L1、也可以直连内存控制器 Cache 只是旁路缓存,不是必经独木桥。

  • L1、L2 是挂在核心边上的高速缓存
  • 内存控制器也是直接连核心
  • 不是排队串联,是并行双通路

两种通路(重点)

通路 1:常规普通 load/store(走 Cache)

核心 → L1 Cache → L2 Cache → 内存控制器 → 显存 / 内存默认走缓存,数据进 Cache、驻留、维护一致性

通路 2:Non-temporal 非临时访存(绕开 Cache)

核心 直接 发请求给内存控制器完全跳过 L1、L2,不进缓存、不占缓存行、不维护一致性

__builtin_nontemporal_store/load就是强制编译器生成「走第二条直达路」的硬件指令。

复制代码
          ┌───────── L1 Cache ─────────┐
          │                            │
CPU/GPU核心┤                            ├─ L2 Cache
          │                            │
          └─────内存控制器/链路───────┘
                       ↓
                  内存/远端显存

下面这张图是「层级」,但不是「硬串联的单行道」 这张图里,Core → L1 → L2 → L3 → 内存,画的是数据访问的层级关系 ,但不是说数据必须按这个顺序、一条路走到黑

真实的硬件互联是这样的:

  • 每个 Core 都和自己的 L1/L2 直连(这是最快的通路)
  • 所有 Core 共享的 L3,是挂在片上的最后一级缓存,同时也连接内存控制器
  • Core 可以不经过 L1/L2,直接发起访问内存的请求(通过内存控制器),这就是非临时访存的底层通路。

五、数据读写路径

一、 CPU/GPU 默认的缓存一致性流程:

  1. 数据先写进 L1/L2 缓存
  2. 当缓存满了,或者遇到缓存一致性 / 内存屏障指令时,再按规则刷回 L3,最终写入主存
  3. 读数据时,也是先从 L1/L2 找,找不到再往下找 L3 / 内存

这是通用程序里的默认行为,目的是用缓存来提升性能。

二、 非临时访存流程:

  • 非临时访存指令(比如__builtin_nontemporal_store)的本质是:
  • 写:不把数据加载进 L1/L2 缓存,直接发给内存控制器,写入主存
  • 读:不从 L1/L2 缓存里取,直接从主存读,读完也不放进缓存里它不是说 "不经过缓存硬件",而是不把数据留在缓存里,从源头避免缓存污染和一致性开销。

L3 的情况要区分 CPU/GPU

  • CPU:很多架构里,非临时访存也会走 L3,但不会分配缓存行,只是作为通路
  • GPU:像你做的跨卡 VMM 场景,L2 是跨 GPU 一致性的关键节点,非临时访存会直接绕开 L1/L2,直达 NVLink/PCIe 链路,所以能测出裸性能。

https://docs.hpc.kaust.edu.sa/tech_blogs/comp_arch/gpu_basics.html

三、总结

复制代码
Core
 ├───默认路径:Core → L1 → L2 → L3 → 主存(带缓存、带一致性)
 └───非临时路径:Core → 内存控制器 → 主存(绕开L1/L2,不驻留)

https://dl.acm.org/doi/fullHtml/10.1145/3408060

六、例子

测试代码

可编译、可运行、带注释、带普通 / 非临时双版本GPU VMM 跨卡 PingPong 测试代码(ROCm / AMD GPU 环境,做 VMM 测试最常用)。

完整可运行代码:VMM PingPong(普通 + Non-Temporal 双版本)

复制代码
#include <hip/hip_runtime.h>
#include <stdio.h>
#include <stdint.h>

// 测试配置
#define ITERATIONS 100000
#define GPU0 0
#define GPU1 1

// ---------------------------
// 方式 1:普通访存(带cache、一致性)
// ---------------------------
__global__ void pingpong_normal(
    int* remote_flag,    // 映射到对端GPU的VMM地址
    int* local_flag,     // 本GPU地址
    int iter_total
) {
    int iter = 0;
    while (iter < iter_total) {
        // 写对端flag(普通store,走cache)
        *remote_flag = iter;

        // 轮询本端flag(普通load,走cache)
        while (*local_flag <= iter);

        iter++;
    }
}

// ---------------------------
// 方式 2:Non-Temporal 非临时访存(绕cache)
// ---------------------------
__global__ void pingpong_nontemporal(
    int* remote_flag,    // VMM映射的对端GPU地址
    int* local_flag,     // 本端GPU地址
    int iter_total
) {
    int iter = 0;
    while (iter < iter_total) {
        // 非临时写:直接写远端,不入cache
        __builtin_nontemporal_store(iter, remote_flag);

        // 非临时读:直接读显存,不缓存
        int val;
        do {
            val = __builtin_nontemporal_load(local_flag);
        } while (val <= iter);

        iter++;
    }
}

// ---------------------------
// 主机端:VMM 映射 + 启动 kernel
// ---------------------------
int main() {
    hipSetDevice(GPU0);
    int *d0_flag, *d1_flag;

    // 分配本端显存
    hipMalloc(&d0_flag, sizeof(int));
    hipSetDevice(GPU1);
    hipMalloc(&d1_flag, sizeof(int));

    // ----------------------
    // VMM 关键:映射对方显存到本GPU虚拟地址空间
    // ----------------------
    hipDeviceptr_t remote_d1_on_gpu0;
    hipDeviceptr_t remote_d0_on_gpu1;

    // GPU0 可以直接访问 GPU1 的 d1_flag
    hipMemAddressMapRemote(
        &remote_d1_on_gpu0, (uint64_t)d1_flag,
        sizeof(int), GPU1, GPU0
    );

    // GPU1 可以直接访问 GPU0 的 d0_flag
    hipMemAddressMapRemote(
        &remote_d0_on_gpu1, (uint64_t)d0_flag,
        sizeof(int), GPU0, GPU1
    );

    printf("=== 测试 1:普通访存 pingpong ===\n");
    hipSetDevice(GPU0);
    hipMemset(d0_flag, 0, sizeof(int));
    hipSetDevice(GPU1);
    hipMemset(d1_flag, 0, sizeof(int));

    // 启动双GPU pingpong
    hipLaunchKernelGGL(pingpong_normal, 1,1,0,0,
        (int*)remote_d1_on_gpu0, d0_flag, ITERATIONS);
    hipLaunchKernelGGL(pingpong_normal, 1,1,0,0,
        (int*)remote_d0_on_gpu1, d1_flag, ITERATIONS);

    hipDeviceSynchronize();
    printf("普通访存完成\n\n");

    printf("=== 测试 2:Non-Temporal 非临时访存 pingpong ===\n");
    hipSetDevice(GPU0);
    hipMemset(d0_flag, 0, sizeof(int));
    hipSetDevice(GPU1);
    hipMemset(d1_flag, 0, sizeof(int));

    hipLaunchKernelGGL(pingpong_nontemporal, 1,1,0,0,
        (int*)remote_d1_on_gpu0, d0_flag, ITERATIONS);
    hipLaunchKernelGGL(pingpong_nontemporal, 1,1,0,0,
        (int*)remote_d0_on_gpu1, d1_flag, ITERATIONS);

    hipDeviceSynchronize();
    printf("非临时访存完成\n");

    return 0;
}

编译命令(直接复制用)

复制代码
hipcc pingpong_vmm.cpp -o pingpong_vmm

运行

复制代码
./pingpong_vmm

代码里最关键的两行

普通访存:

复制代码
*remote_flag = iter;
while (*local_flag <= iter);

走 cache、一致性、跨卡同步慢。

Non-Temporal 非临时访存:

复制代码
__builtin_nontemporal_store(iter, remote_flag);
val = __builtin_nontemporal_load(local_flag);

绕开 L1/L2、不驻留、不触发一致性、延迟更低、带宽更高这就是你 VMM 测试要测的 "裸链路性能"


路径对比(结合代码)

普通访存路径

复制代码
SM核心 → L1 → L2 → NVLink → 对端 L2 → L1 → SM
(带一致性、开销大)

Non-Temporal 访存路径

复制代码
SM核心 ------------------直接------------→ NVLink ------------------直接------------→ SM
(不走L1/L2、不驻留、无一致性)

https://strikefreedom.top/archives/cpu-caches-theory-and-application

相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第53题】【JVM篇】第13题:JVM采用什么算法判断一个对象是否需要被回收?
java·jvm·算法·面试
逍遥德1 小时前
常见的任务调度框架介绍
java·spring boot·中间件
jiayong231 小时前
Memory 写入、检索与纠错机制:让 Agent 记住,也让它忘对
java·服务器·网络·hermes
小赵不会秃头1 小时前
数据结构Day 06:线性结构、库操作及 Makefile 完整学习笔记
java·linux·数据结构·算法·面试
xqqxqxxq1 小时前
Maven 完整配置与使用技术笔记
java·笔记·maven
砍材农夫1 小时前
物联网 基于netty理解粘包/拆包
java·物联网·struts
Counter-Strike大牛1 小时前
Nacos源码修改tomcat版本方法
java·tomcat
天天进步20151 小时前
透视 Mission Control 源码:如何构建高性能的 Agent 实时监控架构?
开发语言
apple爱加油1 小时前
线程相关_线程池原理
开发语言·python