7-15《GMC 层:PTE/PDE 的二进制编码与 ASIC 差异》 分析了 addr + flags 编码为 64 位表项的过程,其中多次出现的 MTYPE 位段与 override_vm_pte_flags 留到本篇展开。MTYPE(Memory Type)是 PTE 中一段编码内存缓存类型的标志位,它决定 GPU 访问某一页时采用何种缓存策略------是否使用一致性缓存(CC)、非一致缓存(NC)、还是完全不缓存(UC)。缓存类型的选择直接关系到 GPU 与 CPU、本地显存与远端显存之间的数据一致性:选错会导致读到陈旧数据,选得过于保守则牺牲性能。
MTYPE 的取值不是孤立设定的,而是一条从用户分配意图(coherent / uncached)出发,经 KFD、GEM flags,最终在 GMC 层结合 ASIC 型号、内存位置、NUMA 拓扑综合决策的链路。本篇分析这条链路:MTYPE 的取值语义、用户意图的来源、gmc_v9_0_get_coherence_flags() 的分型决策、NUMA 逐页覆盖,以及 snoop 与 PTE_SNOOPED 的关系。
目录
- [1 概述:一致性问题与 MTYPE 的角色](#1 概述:一致性问题与 MTYPE 的角色)
- [2 MTYPE 的取值与语义](#2 MTYPE 的取值与语义)
- [3 用户意图的来源:coherent / uncached](#3 用户意图的来源:coherent / uncached)
- [4 一致性决策:get_coherence_flags 的分型](#4 一致性决策:get_coherence_flags 的分型)
- [5 NUMA 逐页覆盖:override_vm_pte_flags](#5 NUMA 逐页覆盖:override_vm_pte_flags)
- [6 snoop 与 PTE_SNOOPED](#6 snoop 与 PTE_SNOOPED)
- [7 小结](#7 小结)
1 概述:一致性问题与 MTYPE 的角色
GPU 与 CPU 共享数据时存在缓存一致性问题:GPU 有自己的缓存层级,CPU 也有;当一方写入内存、另一方读取时,若缓存未同步,读到的可能是陈旧值。传统 dGPU 通过显式的缓存刷新/失效来维护一致性,而现代 APU(CPU 与 GPU 集成、共享物理内存)以及支持 XGMI 一致互联的多 GPU 系统,则要求在页表层面就标注每一页的缓存语义。
MTYPE 正是这个标注。它编码在 PTE 的特定位段(GFX9 为 bit 57--58),常见取值有:
- NC(Non-Coherent):非一致缓存。GPU 缓存该页但不保证与 CPU 缓存一致,适合 GPU 私有数据,性能最好。
- CC(Coherent Cached):一致缓存。硬件维护该页在 GPU 与 CPU 之间的缓存一致性,适合细粒度共享。
- UC(UnCached):不缓存。每次访问直达内存,一致性最强、性能最低,适合远端或需强一致的场景。
- RW(Read-Write):GFX9 上用于本地显存的默认可缓存类型。
- WC(Write-Combine):写合并。
不同 ASIC、不同内存位置(本地显存 / 远端显存 / 系统内存)、不同用户请求(一致 / 非一致 / 不缓存)会选择不同的 MTYPE。决策逻辑集中在 GMC 层的 get_coherence_flags。下面先看取值定义,再看意图来源与决策。
2 MTYPE 的取值与语义
MTYPE 的硬件枚举值来自各架构的寄存器定义。以 GFX9(vega10)为例:
c
//drivers/gpu/drm/amd/include/vega10_enum.h
MTYPE_NC = 0x00000000,
MTYPE_WC = 0x00000001,
MTYPE_RW = 0x00000001,
MTYPE_CC = 0x00000002,
MTYPE_UC = 0x00000003,
这些值经 AMDGPU_PTE_MTYPE_VG10(flags, mtype) 宏移位到 bit 57--58 写入 PTE(见 7-14)。值得注意的是 NC = 0:由于 MTYPE_NC 对应位段全零,它是"默认不写入"的类型------get_coherence_flags 末尾对 mtype == MTYPE_NC 的情况不显式写标志,保持位段为零即等价于 NC。
本地显存的默认可缓存类型(GFX9 记为 mtype_local)可由模块参数 mtype_local 调整:
c
//drivers/gpu/drm/amd/amdgpu/amdgpu_drv.c
MODULE_PARM_DESC(mtype_local, "MTYPE for local memory (default: ASIC dependent, 0 = MTYPE_RW, 1 = MTYPE_NC, 2 = MTYPE_CC)");
默认(-1)下 mtype_local 取 MTYPE_RW,即本地显存默认走可读写缓存;用户可强制为 NC 或 CC 以调优或排障。
3 用户意图的来源:coherent / uncached
MTYPE 决策的输入之一是用户对这块内存的一致性意图。该意图通过 GEM 创建标志传入,由 KFD 分配路径从 Thunk/ROCr 的 CoarseGrain/ExtendedCoherent 等语义翻译而来(这条上层链路属于 ROCr/KFD 范畴,此处只看 GMC 侧的入口)。get_coherence_flags 从 BO 的 flags 读出三个布尔量:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
bool coherent = bo->flags & (AMDGPU_GEM_CREATE_COHERENT |
AMDGPU_GEM_CREATE_EXT_COHERENT);
bool ext_coherent = bo->flags & AMDGPU_GEM_CREATE_EXT_COHERENT;
bool uncached = bo->flags & AMDGPU_GEM_CREATE_UNCACHED;
三者的语义层级递进:
uncached:显式要求不缓存,优先级最高,直接对应 UC。coherent:要求一致性(细粒度一致),包含普通 coherent 与扩展 coherent 两种。ext_coherent:扩展一致性,在跨设备/系统内存范围内维持一致,比普通 coherent 更强。
粗粒度(coarse-grain)分配不置这些位,落到各分型的默认值(通常是 NC 或本地的 RW/CC)。这三个布尔量与"内存在本地还是远端"、"是显存还是系统内存"、"ASIC 是 APU 还是 dGPU"共同决定最终 MTYPE。
4 一致性决策:get_coherence_flags 的分型
gmc_v9_0_get_coherence_flags() 按 GC IP 版本分型决策。先看它对内存位置的判定与两代 MI 加速卡的处理。
MI100/MI200(IP 9.4.1 / 9.4.2)区分本地显存、远端显存与系统内存:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
case IP_VERSION(9, 4, 1):
case IP_VERSION(9, 4, 2):
if (is_vram) {
if (bo_adev == adev) {
if (uncached)
mtype = MTYPE_UC;
else if (coherent)
mtype = MTYPE_CC;
else
mtype = MTYPE_RW;
...
} else {
if (uncached || coherent)
mtype = MTYPE_UC;
else
mtype = MTYPE_NC;
if (amdgpu_xgmi_same_hive(adev, bo_adev))
snoop = true;
}
} else {
if (uncached || coherent)
mtype = MTYPE_UC;
else
mtype = MTYPE_NC;
snoop = true;
}
break;
规则可归纳为:本地显存(bo_adev == adev)默认 RW、coherent 用 CC、uncached 用 UC;远端显存(同一 XGMI hive 的另一颗 GPU)默认 NC、一致或不缓存都退化为 UC,并置 snoop;系统内存默认 NC、一致或不缓存用 UC,恒置 snoop。核心取向是"越远越保守"------本地可放心用可缓存类型,远端与系统内存倾向 UC 以避免一致性风险。
MI300 系列(IP 9.4.3 / 9.4.4 / 9.5.0)引入 APU 与 dGPU 的进一步区分,并先计算 is_local:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
is_local = (!is_vram && (adev->flags & AMD_IS_APU) &&
num_possible_nodes() <= 1) ||
(is_vram && adev == bo_adev &&
KFD_XCP_MEM_ID(adev, bo->xcp_id) == vm->mem_id);
snoop = true;
if (uncached) {
mtype = MTYPE_UC;
} else if (ext_coherent) {
mtype = is_local ? MTYPE_CC : MTYPE_UC;
} else if (adev->flags & AMD_IS_APU) {
mtype = is_local ? mtype_local : MTYPE_NC;
} else {
/* dGPU */
if (is_local)
mtype = mtype_local;
else if (gc_ip_version < IP_VERSION(9, 5, 0) && !is_vram)
mtype = MTYPE_UC;
else
mtype = MTYPE_NC;
}
break;
is_local 的定义是"整块可判定为本地":非 NUMA APU 上的系统内存,或本设备本 XCP 分区的显存。决策链是:uncached 恒 UC;ext_coherent 本地用 CC、远端用 UC;APU 非扩展一致时本地用 mtype_local、远端用 NC;dGPU 本地用 mtype_local,远端在旧版本且系统内存时用 UC、否则 NC。这套分型体现了 MI300 统一内存架构下更细的本地/远端与一致性组合。
决策末尾统一写入 MTYPE 与 snoop:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
if (mtype != MTYPE_NC)
*flags = AMDGPU_PTE_MTYPE_VG10(*flags, mtype);
*flags |= snoop ? AMDGPU_PTE_SNOOPED : 0;
再次印证 NC 的"零即默认"语义------仅当 MTYPE 非 NC 时才显式写入位段。
5 NUMA 逐页覆盖:override_vm_pte_flags
第 4 节的决策针对整个 BO,但在 NUMA APU 上,一块系统内存 BO 的不同页可能分布在不同 NUMA 节点上------有的页与发起访问的 GPU 同节点(本地),有的跨节点(远端)。整块用 NC 是安全但保守的兜底;若能逐页判断本地性,本地页就可以升级为更快的可缓存类型。这正是 gmc_v9_0_override_vm_pte_flags() 的职责,它在页表写入路径(7-15 第 5 节的 override_pte 开关)被逐页调用。
它只覆盖 MTYPE 为 NC 或 UC(扩展一致) 的表项:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
if ((*flags & AMDGPU_PTE_MTYPE_VG10_MASK) !=
AMDGPU_PTE_MTYPE_VG10(0ULL, MTYPE_NC) &&
(*flags & AMDGPU_PTE_MTYPE_VG10_MASK) !=
AMDGPU_PTE_MTYPE_VG10(0ULL, MTYPE_UC)) {
dev_dbg_ratelimited(adev->dev, "MTYPE is not NC or UC\n");
return;
}
随后取 VM 所在的本地 NUMA 节点,用 pfn_to_nid() 查出该物理页所属节点,若二者相同(页为本地),则升级 MTYPE:
c
//drivers/gpu/drm/amd/amdgpu/gmc_v9_0.c
nid = pfn_to_nid(addr >> PAGE_SHIFT);
if (nid == local_node) {
if ((*flags & AMDGPU_PTE_MTYPE_VG10_MASK) ==
AMDGPU_PTE_MTYPE_VG10(0ULL, MTYPE_NC)) {
unsigned int mtype_local = MTYPE_RW;
if (amdgpu_mtype_local == 1)
mtype_local = MTYPE_NC;
else if (amdgpu_mtype_local == 2)
mtype_local = MTYPE_CC;
*flags = AMDGPU_PTE_MTYPE_VG10(*flags, mtype_local);
} else {
/* MTYPE_UC case */
*flags = AMDGPU_PTE_MTYPE_VG10(*flags, MTYPE_CC);
}
}
本地页把 NC 升级为 mtype_local(默认 RW)、把扩展一致的 UC 升级为 CC;跨节点页保持原状。函数还会跳过非 RAM 页(!page_is_ram,如 PCIe 资源无 struct page/NUMA 节点)。这样,整块保守设 NC/UC 的 BO 在写页表时被逐页精化,本地页得到可缓存的性能收益,远端页维持保守语义。
6 snoop 与 PTE_SNOOPED
MTYPE 之外,一致性还涉及 PTE_SNOOPED(bit 2,见 7-14)。snoop 表示 GPU 访问该页时需要对 CPU 缓存做窥探,以保证与 CPU 缓存的一致。第 4 节的决策中,snoop 布尔量在多处被置真:MI200 的远端 XGMI 显存与系统内存、MI300 全部分型,都设 snoop;最终统一 *flags |= snoop ? AMDGPU_PTE_SNOOPED : 0。
snoop 与 MTYPE 是两个正交的维度:MTYPE 决定 GPU 侧的缓存策略(缓存/不缓存/一致缓存),snoop 决定访问是否窥探 CPU 缓存。二者配合覆盖 GPU-CPU 一致性的不同侧面。此外 amdgpu_ttm_tt_pde_flags()(7-14 第 3 节)在系统内存/显存为 cached 时也会置 SNOOPED,与此处的一致性决策共同构成最终的 snoop 标志。
7 小结
本篇分析了缓存类型 MTYPE 如何从一致性需求一路决策到 PTE:
- MTYPE 编码 GPU 访问某页的缓存策略:NC(非一致缓存,值 0,默认不写位段)、CC(一致缓存)、UC(不缓存)、RW(本地可缓存默认)、WC(写合并);
mtype_local模块参数可调本地显存默认。 - 用户意图经 GEM flags 传入:
uncached/coherent/ext_coherent三个布尔量,粗粒度分配不置位走默认。 gmc_v9_0_get_coherence_flags()按 IP 版本分型:MI200 区分本地显存(RW/CC/UC)、远端显存与系统内存(NC/UC + snoop);MI300 先算is_local,再按 uncached/ext_coherent/APU/dGPU 组合选型,整体取向"越远越保守"。- NUMA APU 上
override_vm_pte_flags()逐页覆盖:本地页把 NC 升级为mtype_local、UC 升级为 CC,跨节点页保持,跳过非 RAM 页。 PTE_SNOOPED与 MTYPE 正交,标注是否窥探 CPU 缓存,在远端/系统内存场景普遍置位。
至此,页表项内容侧的三篇(7-14 标志语义、7-15 二进制编码、7-16 缓存一致性)已完整覆盖一个 PTE 从标志到编码、从权限到缓存类型的全部维度。下一篇 7-17《PRT / 稀疏驻留(Sparse Binding)》 将转向一类特殊映射:稀疏纹理与稀疏 buffer 如何借助 AMDGPU_PTE_PRT 实现部分驻留,以及 prt_lock、num_prt_users 对这类映射的管理。