摘要
XNACK(eXception on Negative ACKnowledgement)是 AMD GPU 架构中支持"可恢复内存访问缺页"(Recoverable Memory Page Fault)的核心机制,是实现统一内存(Unified Memory / SVM,Shared Virtual Memory)的关键基础设施。它使得 GPU wavefront 在访问尚未驻留的虚拟地址时能够挂起等待、由驱动完成缺页处理后重放(replay)访存,而非直接触发致命错误。本文从硬件流水线、内存管理单元设计、内核驱动(amdgpu/KFD)到用户态运行时(ROCm/HSA)四个层次,系统梳理 XNACK 的软硬件协同架构与实现路径。
1. 背景与动机
1.1 传统 GPU 内存模型的局限
传统离散 GPU 编程模型要求应用显式管理设备内存:hipMalloc/cudaMalloc 分配的内存必须提前 pin 并建立页表映射,访问未映射地址直接导致 VM_FAULT(致命异常),kernel 被杀死。这种模型对以下场景不友好:
- • 大规模不规则数据结构(图计算、稀疏矩阵)的按需迁移
- • CPU/GPU 之间细粒度共享同一虚拟地址空间(如
hipMallocManaged) - • Oversubscription:working set 超过显存容量时的透明分页
1.2 XNACK 的角色定位
XNACK 本质上是 GPU 硬件对"缺页可恢复"的协议级支持:当访存单元(TA/TC,或更底层的 L1/L2 cache 控制器)检测到地址转换失败时,硬件不是立即 fault-kill,而是:
- 1. 向发起该访存的 wave 返回一个 NACK(negative acknowledgement)
- 2. wave 因此进入 stall/sleep 状态,指令不退休
- 3. 硬件生成一次 retry-able page fault 上报给内核驱动
- 4. 驱动完成缺页处理(分配物理页、建立映射、必要时页迁移)后更新页表
- 5. 硬件将该访存请求 replay(重放),wave 恢复执行
这与 x86 CPU 缺页中断的语义高度相似,区别在于 GPU 是大规模 SIMT 并行执行,需要在 wavefront 粒度上处理"部分 lane 命中、部分 lane 缺页"的复杂情形,且缺页处理延迟(微秒到毫秒级)远高于 CPU(因为常伴随跨 PCIe/xGMI 的页迁移)。
2. 硬件架构
2.1 地址转换路径
GPU 的虚拟地址转换涉及多级结构:
关键结构:
- • GPUVM(GPU Virtual Memory):多级 radix 页表,以 VMID 区分不同进程/上下文的地址空间,与 amdgpu 的
amdgpu_vm软件对象一一对应。 - • ATC(Address Translation Cache):位于 XGMI/IOMMU 路径上,用于缓存 GPU 侧对系统内存(CPU 侧 page table,通过 IOMMUv2/ATS 获取)的翻译结果。
- • VM Fault FIFO / Interrupt Handler Ring:硬件将 fault 信息(faulting VMID、地址、读写属性、client id)打包写入中断队列,供驱动异步消费。
2.2 XNACK 使能与 wave 的行为差异
XNACK 是否使能,直接决定硬件对缺页的处理策略,这是理解该机制的核心:
| 状态 | 硬件行为 | 软件语义 |
|---|---|---|
| XNACK OFF | 缺页立即视为致命错误,触发 VM_FAULT 中断并可能导致该 VMID 下所有 wave 被杀死 | 传统模型:内存必须提前完全映射(如 hipMalloc/pinned memory) |
| XNACK ON | 缺页时硬件向发起 wave 返回 NACK,wave 挂起等待重放;同时上报 retry fault 给驱动 | SVM/托管内存模型:允许按需分页、迁移、hipMallocManaged |
在 CDNA(MI100/MI200/MI300 系列)架构上,XNACK 通过 GPU firmware/固件配置位控制,且同一进程运行期间不可切换——这是因为 wave 的缺页恢复路径(compute unit 内部的 replay 缓冲)在两种模式下微架构行为不同,切换需要重新加载 kernel context。这也是 ROCm 中 HSA_XNACK 环境变量必须在进程启动前设置的硬件根因。
2.3 Retry Fault 的硬件字段
一次典型的 GPU retry page fault 中断携带的信息包括:
- • Faulting page address(触发缺页的虚拟地址,按 page 粒度对齐)
- • VMID:标识发起访问的进程上下文
- • Client ID / Engine ID:区分是 Compute Unit(CU)发起,还是 SDMA、显示引擎、CP 等其他硬件模块
- • 访存类型:Read / Write / Execute / Atomic(Atomic 缺页处理更复杂,因为需要保证 read-modify-write 的原子语义在 replay 后仍成立)
- • Timestamp
多个 CU 并发触发的 fault 会在 fault FIFO 中排队,硬件保证同一地址的多次 fault 不会无限重复上报(有去重/合并机制,通常以 fault 地址+VMID 为 key)。
2.4 与 IOMMU/ATS/PRI 的协同(APU 场景)
在 APU(如 MI300A)或启用了 HSA 完全一致性内存模型的平台上,GPU 缺页处理会与 CPU IOMMU 的 ATS(Address Translation Service) 和 PRI(Page Request Interface,PCIe 定义的标准协议) 协同:
这一路径使得 GPU 可以直接缺页式访问 CPU 侧 malloc 分配的匿名内存,而无需显式 hipHostRegister。

3. 软件架构
3.1 分层视图
3.2 关键内核数据结构
svm_range:KFD SVM 子系统中管理一段可能被缺页/迁移的虚拟地址区间的核心结构(drivers/gpu/drm/amd/amdkfd/kfd_svm.h),记录:
- • 起止虚拟地址、所属
mm_struct - • 当前 actual location(驻留在系统内存还是某块 GPU 显存,
prange->actual_loc) - • prefetch/迁移策略提示
- • 关联的
mmu_interval_notifier,用于响应 CPU 侧地址空间变化(如munmap、fork 后的 COW)
svm_range_list:每个 kfd_process 一份,以 interval tree 组织所有 svm_range,支持按地址快速查找覆盖范围。
Retry Fault 处理入口(简化后的逻辑路径,对应 svm_range_restore_pages):
// 伪代码,示意 KFD 处理 GPU retry fault 的核心流程
int svm_range_restore_pages(struct kfd_node *node,
unsigned int pasid,
uint64_t addr, bool write_fault)
{
struct kfd_process *p = kfd_lookup_process_by_pasid(pasid);
struct svm_range *prange = svm_range_from_addr(&p->svms, addr);
/* 1. 依据 prange 决定迁移策略:
* - 若目标应驻留 GPU 显存 -> migrate_vma_* 迁移
* - 若应驻留系统内存 -> hmm_range_fault 直接取得 PFN */
if (prange->preferred_loc == node->id)
svm_migrate_to_vram(prange, node, addr);
else
hmm_range_fault(&hmm_range); /* 走 HMM 通用路径 */
/* 2. 更新 GPUVM 页表 (PTE),指向新物理页 */
svm_range_map_to_gpu(node, prange, addr);
/* 3. 通知硬件 replay:写 fault-done 到 GMC/IH */
amdgpu_gmc_fault_recovery(node->adev, pasid, addr);
return 0;
}3.3 与 Linux HMM 框架的集成
XNACK/SVM 软件栈重度依赖内核的 HMM(Heterogeneous Memory Management) 基础设施:
- •
hmm_range_fault():驱动通过该接口,以非阻塞方式取得给定虚拟地址范围当前对应的 PFN(若尚未在 CPU 侧建立映射,会触发标准 CPU 缺页处理链路)。 - •
mmu_interval_notifier:注册在目标地址区间上,一旦 CPU 侧发生 TLB 失效(如页面被换出、迁移、COW),驱动会收到回调并使 GPU 侧对应的 PTE 失效,保证一致性。 - •
migrate_vma_setup/migrate_vma_pages/migrate_vma_finalize:这一组接口用于把系统内存页迁移为设备私有页(Device Private Memory,MEMORY_DEVICE_PRIVATE),是 XNACK 场景下"数据靠近计算"(migrate-on-fault)策略的底层实现,GPU 显存以 ZONE_DEVICE 的形式纳入内核内存管理框架。
3.4 与用户态 ROCm 栈的接口
- • HSA Runtime(ROCr):
hsa_amd_memory_pool_allocate配合HSA_AMD_MEMORY_POOL_STANDARD_FLAG或直接使用系统malloc+hsa_amd_svm_attributes_set,向内核声明该内存区域参与 SVM。 - •
hipMallocManaged/hipMemAdvise/hipMemPrefetchAsync:HIP 层面对 CUDA UM API 的对齐实现,底层最终转化为对 KFDioctl(AMDKFD_IOC_SVM)的调用,设置prange的 preferred location、prefetch 提示等属性,直接影响 3.3 节中迁移决策的选择。 - • 环境变量
HSA_XNACK=1:这是用户态触发内核为该进程建立"可恢复缺页"上下文的开关,本质上是在进程创建 KFD context 时向硬件请求以 XNACK-enabled 模式初始化该 VMID 的 fault 处理策略。
4. 端到端流程整合
需要强调几个实现细节上的复杂性:
- 1. Wavefront 部分缺页:一个 wave 的 64 个 lane 可能只有部分 lane 命中缺页地址,硬件需要按 lane mask 记录哪些 lane 需要 replay,避免已完成的 lane 被重复执行(这对访存有副作用的场景,如 atomic,尤为关键)。
- 2. Fault 风暴与 QoS:大规模并发缺页(如冷启动大数据集的图算法)可能产生海量 fault 中断,驱动需要合并/限流处理,否则中断处理开销会反噬性能——这也是为何
hipMemPrefetchAsync这类"主动预取"API 在实践中比纯粹依赖缺页驱动的迁移性能更优。 - 3. Livelock 防护:迁移过程中若目标页再次被其他 wave 或 CPU 线程并发访问导致状态变化,驱动需要保证 fault 处理的最终收敛,避免"迁移-替换-再缺页"的无限循环。
5. 性能与工程考量
- • 延迟量级:GPU retry fault 从触发到 replay 完成,典型延迟在数微秒(命中系统内存、无需迁移)到数百微秒甚至毫秒级(需要跨 xGMI/PCIe 迁移大页)不等,相比 L2 TLB miss 的几十纳秒有数量级差距,因此 XNACK 模式下的 kernel 若 working set 局部性差,性能会显著劣化。
- • XNACK 对寄存器分配的影响:在 CDNA 架构上,开启 XNACK 会占用额外的硬件资源用于保存 replay 所需的上下文(部分文献/AMD 文档提及会影响可用 VGPR 数量或 occupancy),这是"按需选择是否开启 XNACK"而非默认全局开启的现实工程原因之一。
- • 与显式内存管理的权衡:生产环境的高性能 HPC/AI workload 通常仍偏好显式
hipMalloc+ 手动hipMemcpyAsync,将 XNACK/SVM 用于原型开发、不规则数据结构或内存超订阅场景,二者并非互斥,可在同一应用中混合使用。
6. 总结
XNACK 机制体现了现代 GPU 从"纯加速器"向"一等公民的可编程处理器"演进过程中,在硬件缺页语义、内核异构内存管理(HMM)与用户态运行时(ROCm/HSA)三层之间构建的协同设计。其硬件基础是 NACK-replay 式的缺页恢复流水线与 GPUVM/ATC 地址转换体系,其软件核心是 KFD SVM 子系统对 Linux HMM 框架的深度集成,最终对上层暴露为 hipMallocManaged 等统一内存编程模型。理解这一机制,对于诊断 ROCm 应用中的缺页性能问题、设计跨 CPU/GPU 的内存迁移策略,以及评估 UM/SVM 模型与显式内存管理的取舍,都具有直接的工程意义。




