16- KFD SVM概念与逻辑的高度概括与用例分析

本文档涵盖 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.ckfd_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 回调都需要:

  1. 清除受影响的 GPU PTE
  2. 暂停 GPU 队列(防止访问无效映射)
  3. 异步通过 restore_work 重建所有映射
  4. 恢复 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/ 测试工具函数