本文档涵盖 AMD GPU KFD SVM (Shared Virtual Memory) 子系统的完整执行逻辑,包括核心数据结构、三大执行路径、异步工作队列、迁移机制,以及调试过程中遇到的典型问题解析。
难度: 🔴 高级
预计学习时间: 3小时
前置知识: 第1-10章,特别是第9、10章
目录
- [1. 概述](#1. 概述)
- [2. 核心数据结构](#2. 核心数据结构)
- [3. KFD SVM 三大执行路径](#3. KFD SVM 三大执行路径)
- [3.1 用户态 ioctl 路径](#3.1 用户态 ioctl 路径)
- [3.2 MMU Notifier 路径](#3.2 MMU Notifier 路径)
- [3.3 GPU Retry Fault 路径](#3.3 GPU Retry Fault 路径)
- [4. 异步工作队列](#4. 异步工作队列)
- [4.1 Deferred List Work(延迟工作)](#4.1 Deferred List Work(延迟工作))
- [4.2 Restore Work(恢复工作)](#4.2 Restore Work(恢复工作))
- [5. 迁移机制](#5. 迁移机制)
- [5.1 RAM → VRAM 迁移](#5.1 RAM → VRAM 迁移)
- [5.2 VRAM → RAM 迁移](#5.2 VRAM → RAM 迁移)
- [5.3 migrate_to_ram 回调](#5.3 migrate_to_ram 回调)
- [6. XNACK On/Off 差异](#6. XNACK On/Off 差异)
- [7. 锁层级](#7. 锁层级)
- [8. 调试案例分析](#8. 调试案例分析)
- [8.1 GDB 调试时意外触发 migrate_to_ram](#8.1 GDB 调试时意外触发 migrate_to_ram)
- [8.2 部分 Prefetch 后属性查询返回 0xFFFFFFFF](#8.2 部分 Prefetch 后属性查询返回 0xFFFFFFFF)
- [9. 关键源文件索引](#9. 关键源文件索引)
前面的章节对KFD SVM的实现进行了详细的分析,本章进行一个总结,并给出调试SVM中可能遇到的问题进行说明。
1. 概述
AMD GPU KFD SVM 子系统实现了 CPU 和 GPU 之间的统一虚拟地址(Shared Virtual Memory),允许 GPU 直接访问 CPU 进程的虚拟地址空间,并支持页面在 VRAM 和系统内存之间透明迁移。
KFD SVM 通过 KFD ioctl (/dev/kfd) 提供用户态接口,核心实现位于 kfd_svm.c 和 kfd_migrate.c,使用自定义 interval tree 管理 svm_range,通过 amdgpu_hmm_range_get_pages() 获取页面,使用 migrate_vma_* + SDMA 进行页面迁移。
2. 核心数据结构
kfd_process
└── kfd_process_device (per GPU)
└── svm_range_list (svms)
├── interval_tree (所有 svm_range 的红黑树)
├── deferred_range_list (延迟处理队列)
├── criu_svm_metadata_list
└── deferred_list_work (工作队列)
svm_range
├── start, last (页对齐的虚拟地址范围)
├── prefetch_loc, actual_loc (迁移位置)
├── flags (CoW, GPU exec, RO 等)
├── granularity (迁移粒度)
├── bitmap_access[] (GPU 访问位图)
├── bitmap_aip[] (GPU AIP 位图)
├── dma_addr[][] (per-GPU DMA 地址数组)
├── ttm_res (VRAM BO 的 TTM 资源)
├── migrate_mutex (迁移互斥锁)
├── lock (范围读写锁)
├── notifier (MMU interval notifier)
├── work_item (延迟工作项)
└── child_list (子范围列表,用于分裂)
3. KFD SVM 三大执行路径
3.1 用户态 ioctl 路径
入口: svm_range_set_attr() --- 通过 KFD ioctl AMDKFD_IOC_SVM 触发
用户态 hsaKmtSVMSetAttr()
└── ioctl(KFD_IOC_SVM, SET_ATTR)
└── svm_range_set_attr()
├── 1. svm_range_check_attr() --- 参数校验
├── 2. svm_range_debug_dump() --- 调试输出当前状态
├── 3. svm_range_add() --- 创建/分裂/合并 svm_range
│ ├── 在 [start, last] 区间查找所有重叠 range
│ ├── 如果没有 → 创建新 range
│ ├── 如果部分重叠 → 分裂(split)现有 range
│ └── 新的 range 加入 update_list
├── 4. 遍历 update_list:
│ ├── svm_range_apply_attrs() --- 应用新属性到每个 range
│ └── 累积 update_mapping / flush_tlb 标志
├── 5. 如果 trigger_migration:
│ ├── prefetch_loc 指向 VRAM:
│ │ └── svm_range_trigger_migration()
│ │ └── svm_migrate_ram_to_vram()
│ └── prefetch_loc 指向 SYSMEM:
│ └── svm_migrate_vram_to_ram()
└── 6. 如果 update_mapping:
└── svm_range_validate_and_map()
--- 更新 GPU 页表映射
关键流程 --- svm_range_validate_and_map():
svm_range_validate_and_map()
├── svm_range_reserve_bos() --- 预留 BO/VM 资源
├── for each GPU that has access:
│ ├── amdgpu_hmm_range_get_pages() --- HMM 获取页面
│ │ └── hmm_range_fault() --- 触发缺页,填充 pfn 数组
│ ├── svm_range_dma_map() --- DMA 映射
│ └── svm_range_map_to_gpus() --- 更新 GPU 页表 (PTE)
│ └── svm_range_map_to_gpu()
│ └── amdgpu_vm_update_range() --- 写入 PDE/PTE
└── svm_range_unreserve_bos() --- 释放预留
3.2 MMU Notifier 路径
入口: svm_range_cpu_invalidate_pagetables() --- Linux MMU notifier 回调
当 CPU 页表发生变化时(如 munmap、页面迁移、CoW 等),内核通过 MMU notifier 通知 SVM 子系统。
CPU 页表变化 (munmap / migrate / CoW / swap)
└── mmu_interval_notifier_ops.invalidate
└── svm_range_cpu_invalidate_pagetables()
├── 检查事件类型:
│ ├── MMU_NOTIFY_MIGRATE:
│ │ └── 如果 owner == 我们自己 → 跳过(自触发迁移)
│ ├── MMU_NOTIFY_RELEASE:
│ │ └── 直接返回
│ └── 其他事件:
│ └── 继续处理
├── notifier_seq 递增(使 HMM pages 无效)
├── 如果需要 GPU unmap:
│ └── svm_range_unmap_from_gpus()
│ └── amdgpu_vm_update_range(clear PTE)
│ └── amdgpu_vm_update_pdes()
└── 如果需要恢复:
├── svm_range_add_list_work() --- 加入延迟工作队列
└── schedule_deferred_list_work() --- 调度异步工作
3.3 GPU Retry Fault 路径
入口: svm_range_restore_pages() --- GPU 页面错误中断处理
当 GPU 访问未映射的虚拟地址时,硬件产生 retry fault,由 interrupt handler 调用此函数。
GPU 访问未映射地址
└── 硬件产生 retry fault
└── amdgpu_vm_handle_fault() / kfd_svm_page_fault()
└── svm_range_restore_pages()
├── 1. 查找/创建 svm_range
│ ├── svm_range_from_addr() --- 在 interval tree 中查找
│ └── 如果不存在:
│ ├── find_vma() --- 检查 VMA 是否存在
│ ├── svm_range_create() --- 创建新 range
│ └── svm_range_add() --- 加入 interval tree
├── 2. 策略驱动迁移:
│ ├── svm_range_best_restore_location() --- 决定最佳位置
│ │ ├── 检查 actual_loc vs preferred_loc
│ │ ├── 检查 GPU 访问权限
│ │ └── 返回目标 node ID
│ ├── 如果需要迁移到 VRAM:
│ │ └── svm_migrate_ram_to_vram()
│ └── 如果需要迁移到 RAM:
│ └── svm_migrate_vram_to_ram()
└── 3. svm_range_validate_and_map() --- 建立 GPU 映射
4. 异步工作队列
4.1 Deferred List Work(延迟工作)
函数: svm_range_deferred_list_work()
svm_range_deferred_list_work()
└── 遍历 svms->deferred_range_list:
├── 取出 work_item (包含 mm, start, last, op)
├── 根据 op 分类:
│ ├── SVM_OP_UNMAP_RANGE:
│ │ └── svm_range_unmap_split()
│ │ --- 处理 munmap:清除 PTE,分裂/删除 range
│ ├── SVM_OP_UPDATE_RANGE_NOTIFIER:
│ │ └── svm_range_update_notifier_and_interval_tree()
│ │ --- 更新 notifier 注册范围
│ ├── SVM_OP_UPDATE_RANGE_NOTIFIER_AND_MAP:
│ │ └── 更新 notifier + validate_and_map()
│ │ --- 用于 range 分裂后重建映射
│ └── SVM_OP_ADD_RANGE_AND_MAP:
│ └── 添加新 range + validate_and_map()
└── svm_range_drain_retry_fault() --- 等待 GPU fault 处理完成
4.2 Restore Work(恢复工作)
函数: svm_range_restore_work()
仅在 XNACK Off 模式下使用。当 MMU notifier 清除了 GPU PTE 后,需要通过此 worker 重新建立映射。
svm_range_restore_work()
├── kfd_process_evict_queues() --- 暂停所有 GPU 队列
├── 遍历所有 svm_range:
│ ├── 如果 range 不需要更新 → 跳过
│ └── svm_range_validate_and_map() --- 重建 GPU 映射
│ ├── hmm_range_fault() --- 重新获取页面
│ └── map_to_gpus() --- 更新 PTE
└── kfd_process_restore_queues() --- 恢复 GPU 队列
XNACK Off 下的 Queue Eviction 机制:
MMU notifier 触发
→ 通知 SVM 清除 GPU PTE
→ kfd_process_evict_queues() --- 暂停队列,防止 GPU 访问无效地址
→ restore_work 重建所有 PTE
→ kfd_process_restore_queues() --- 恢复队列
5. 迁移机制
5.1 RAM → VRAM 迁移
svm_migrate_ram_to_vram(prange, best_loc)
├── svm_range_vram_node_new() --- 分配 VRAM BO (TTM)
├── 分段迁移 (每段 256 页):
│ ├── migrate_vma_setup() --- 初始化迁移上下文
│ ├── svm_migrate_copy_to_vram()
│ │ ├── 分配 device private pages (ZONE_DEVICE)
│ │ ├── amdgpu_copy_buffer() --- SDMA DMA 拷贝
│ │ │ ├── src: 系统内存 DMA 地址
│ │ │ └── dst: VRAM 偏移
│ │ └── 设置 page->pgmap = svm_pgmap
│ ├── migrate_vma_pages() --- 将页面所有权转移给 device
│ └── migrate_vma_finalize() --- 完成迁移,更新 CPU PTE
└── 成功后: actual_loc = best_loc
5.2 VRAM → RAM 迁移
svm_migrate_vram_to_ram(prange, mm, ...)
├── 分段迁移 (每段 256 页):
│ ├── migrate_vma_setup() --- 标记 MIGRATE_VMA_SELECT_DEVICE_PRIVATE
│ ├── svm_migrate_copy_to_ram()
│ │ ├── 分配系统页面 (alloc_page_vma)
│ │ ├── amdgpu_copy_buffer() --- SDMA DMA 拷贝
│ │ │ ├── src: VRAM 偏移
│ │ │ └── dst: 系统内存 DMA 地址
│ │ └── dma_fence_wait() --- 等待 SDMA 完成
│ ├── migrate_vma_pages()
│ └── migrate_vma_finalize()
└── 成功后: actual_loc = 0 (系统内存)
5.3 migrate_to_ram 回调
当 CPU 访问 device private page 时,内核自动触发:
CPU 访问 device private page
└── do_swap_page() → migrate_to_ram()
└── svm_migrate_to_ram() (注册为 pgmap ops)
├── 查找对应的 svm_range
├── svm_migrate_vram_to_ram() --- 将页面搬回系统内存
└── 返回,CPU 重新访问现在在 RAM 中的页面
6. XNACK On/Off 差异
| 方面 | XNACK On | XNACK Off |
|---|---|---|
| GPU 缺页处理 | 硬件自动 retry,等待 SVM 建立映射 | 产生错误中断 |
| MMU notifier | 标记 range 需要更新,GPU 下次访问时 retry | 清除 GPU PTE + 暂停队列 + restore_work 重建 |
| Queue 管理 | 不需要暂停/恢复队列 | 需要 evict/restore 队列 |
| 性能 | 更灵活,按需映射 | 需要主动维护所有映射 |
| validate_and_map | 惰性:GPU fault 时触发 | 主动:restore_work 批量重建 |
当 XNACK Off 时,每次 MMU notifier 回调都需要:
- 清除受影响的 GPU PTE
- 暂停 GPU 队列(防止访问无效映射)
- 异步通过 restore_work 重建所有映射
- 恢复 GPU 队列
7. 锁层级
process_info->lock (进程级互斥锁)
└── mmap_write_lock(mm) (进程 VMA 锁)
└── svms->lock (SVM range list 锁)
└── prange->migrate_mutex (迁移互斥锁)
└── prange->lock (范围读写锁)
8. 调试案例分析
8.1 GDB 调试时意外触发 migrate_to_ram
问题现象:
在 GDB 单步调试 KFDSVMRangeTest::PrefetchTest 时,执行 SVMRangePrefetchToNode(pBuf, BufSize/2, gpuNode) 后,dmesg 中意外出现 svm_migrate_to_ram 日志,但用户代码并没有主动触发回迁。
根因分析:
GDB 通过 ptrace 读取被调试进程的内存来显示变量值、评估表达式。当 GDB 尝试读取已经被 prefetch 到 VRAM 的 device private page 时:
GDB 读取变量/内存
└── ptrace(PEEK) → access_process_vm()
└── CPU 页表查找 → 发现 device private page
└── do_swap_page() → migrate_to_ram()
└── svm_migrate_to_ram()
└── svm_migrate_vram_to_ram() ← 出现在 dmesg!
解决方案: 这是正常行为。GDB 调试会影响 SVM 页面位置,在分析迁移行为时需要考虑调试器的干扰。
8.2 部分 Prefetch 后属性查询返回 0xFFFFFFFF
该测试用用例的理解,需要深度理解SVM的range管理机制,特别是range分裂策略。
问题现象:
cpp
// 16KB buffer, 仅 prefetch 后半部分到 GPU
SVMRangePrefetchToNode(pBuf + BufSize/2, BufSize/2, gpuNode);
// 查询整个 buffer 的 prefetch 位置 → 返回 0xFFFFFFFF!
SVMRangeGetPrefetchNode(pBuf, BufSize, &node_id);
// node_id == 0xFFFFFFFF (INVALID_NODEID)
根因分析:
属性树中,buffer 被分成了两个区段:
[pBuf, pBuf+BufSize/2) → prefetch_loc = UNDEFINED (未 prefetch)
[pBuf+BufSize/2, pBuf+BufSize) → prefetch_loc = gpuNode (已 prefetch)
查询整个 buffer 时,amdgpu_svm_attr_get() 遍历所有子区段并合并属性:
c
// amdgpu_svm_attr.c: attr_get_ctx_add()
static void attr_get_ctx_add(struct attr_get_ctx *ctx,
const struct amdgpu_svm_attrs *seg_attrs, ...)
{
if (ctx->count == 0) {
ctx->merged = *seg_attrs; // 第一个区段: prefetch_loc = UNDEFINED
} else {
// 第二个区段: prefetch_loc = gpuNode
if (ctx->merged.prefetch_loc != seg_attrs->prefetch_loc)
ctx->merged.prefetch_loc = AMDGPU_SVM_LOCATION_UNDEFINED;
// ↑ 不一致 → 设为 UNDEFINED (0xFFFFFFFF)
}
ctx->count++;
}
9. 关键源文件索引
内核态
| 文件 | 路径 | 描述 |
|---|---|---|
kfd_svm.c |
drivers/gpu/drm/amd/amdkfd/ |
KFD SVM 核心实现 (~4339 行) |
kfd_svm.h |
drivers/gpu/drm/amd/amdkfd/ |
KFD SVM 头文件 (~276 行) |
kfd_migrate.c |
drivers/gpu/drm/amd/amdkfd/ |
KFD 迁移实现 (~1086 行) |
用户态
| 文件 | 路径 | 描述 |
|---|---|---|
svm.c |
libhsakmt/src/ |
Thunk 层 SVM 接口(KFD/DRM 切换) |
KFDSVMRangeTest.cpp |
libhsakmt/tests/kfdtest/src/ |
SVM 测试套件 |
KFDTestUtil.cpp |
libhsakmt/tests/kfdtest/src/ |
测试工具函数 |