AMDGPU驱动性能实战: KFD Queue Quiesce/Restore 机制分析与优化方案探讨

1. 问题提出:Per-Process 粒度的 Queue Quiesce/Restore

1.1 核心问题

在 AMDGPU KFD 驱动中,当某个 BO或 SVM range 需要被 evict 或 invalidate 时,驱动会 quiesce(停止)该进程的所有 user queues,而不仅仅是引用了该 BO 的 queues。

amdgpu_amdkfd_fence.c 中的注释所述:

Big Idea - Since KFD submissions are done by user queues, a BO cannot be

evicted unless all the user queues for that process are evicted.

这意味着:即使某个 queue 完全不访问被 evict 的 BO,它也会被一起停掉。这是一个 per-process、all-or-nothing 的操作,粒度较粗。

1.2 所有触发 Quiesce/Restore 的场景

在当前内核代码中,共有 9 个场景 会触发进程级别的 queue quiesce 或 restore,最终都通过 kfd_process_evict_queues() / kfd_process_restore_queues() 执行。

SVM 相关(3 个场景)

场景 1:MMU Notifier Invalidation(XNACK Off)

  • 入口:svm_range_evict()kgd2kfd_quiesce_mm()kfd_svm.c
  • 触发条件:CPU page table 发生变更(如 MMU_NOTIFY_CLEAR、migration),且 XNACK 关闭
  • 行为:第一个 range 被 evict 时(evicted_ranges == 1)quiesce 所有 queues,
    后续 range 只递增计数器不重复 quiesce
  • 恢复:调度 svm_range_restore_work,延迟 1ms 后尝试 restore
c 复制代码
/* kfd_svm.c - svm_range_evict() */
evicted_ranges = atomic_inc_return(&svms->evicted_ranges);
if (evicted_ranges != 1)
    return r;
/* First eviction, stop the queues */
r = kgd2kfd_quiesce_mm(mm, KFD_QUEUE_EVICTION_TRIGGER_SVM);

场景 2:Queue-Vital Buffer 被 Unmap

  • 入口:svm_range_unmap_from_cpu()kgd2kfd_quiesce_mm()kfd_svm.c
  • 触发条件:MMU_NOTIFY_UNMAP 事件中,被 unmap 的 range 有非零 queue_refcount
    (即该 range 是 doorbell、MQD、wptr 等 queue 关键内存的 backing store)
  • 行为:立即 quiesce 所有 queues
  • 恢复:无对应 restore 路径(queue 赖以运行的内存已被释放)
c 复制代码
/* kfd_svm.c - svm_range_unmap_from_cpu() */
if (atomic_read(&prange->queue_refcount)) {
    pr_warn("Freeing queue vital buffer 0x%lx, queue evicted\n", ...);
    r = kgd2kfd_quiesce_mm(mm, KFD_QUEUE_EVICTION_TRIGGER_SVM);
}

场景 3:SVM Range Restore Work

  • 入口:svm_range_restore_work()kgd2kfd_resume_mm()kfd_svm.c
  • 触发条件:场景 1 的延迟恢复
  • 行为:遍历所有 evicted ranges,逐一调用 svm_range_validate_and_map() 重建 GPU 映射,
    全部成功后调用 kgd2kfd_resume_mm() 恢复 queues
  • 失败处理:重调度 restore work,期间 queues 持续处于 quiesced 状态
TTM BO Eviction 相关(2 个场景)

场景 4:VRAM 压力 Eviction

  • 入口:evict_process_worker()kfd_process_evict_queues()kfd_process.c
  • 触发条件:TTM 内存管理器需要腾出 VRAM 空间,通过 eviction fence 触发
  • 行为:evict 所有 queues,signal eviction fence 允许 BO move
  • 恢复:调度 restore_process_worker

场景 5:TTM Restore

  • 入口:restore_process_worker()kfd_process_restore_queues()kfd_process.c
  • 触发条件:场景 4 的恢复
  • 行为:先调用 amdgpu_amdkfd_gpuvm_restore_process_bos() 将 BO 恢复到 VRAM,
    再 restore 所有 queues
  • 失败处理:以 PROCESS_BACK_OFF_TIME_MS 间隔重调度
系统级(2 个场景)

场景 6:System Suspend

  • 入口:kfd_suspend_all_processes()kfd_process_evict_queues()kfd_process.c
  • 触发条件:系统挂起或 GPU reset
  • 行为:遍历 kfd_processes_table,evict 每个进程的所有 queues 并 signal eviction fence

场景 7:System Resume

  • 入口:kfd_resume_all_processes()kfd_process.c
  • 触发条件:系统恢复
  • 行为:为每个进程调度 restore_process_worker
CRIU 相关(2 个场景)

场景 8:CRIU Checkpoint

  • 入口:criu_checkpoint()kfd_process_evict_queues()kfd_chardev.c
  • 触发条件:CRIU 对进程做快照
  • 行为:暂停所有 queues 以保证进程状态一致性

场景 9:CRIU Restore

  • 入口:criu_restore()kfd_process_evict_queues()kfd_chardev.c
  • 触发条件:CRIU 恢复进程
  • 行为:先 evict queues 防止在 BO/mapping 完全就绪前运行,后续恢复

1.3 场景分类总结

类别 场景 Quiesce 入口 Restore 入口 触发频率
SVM MMU notifier invalidation svm_range_evict() svm_range_restore_work() 中-高
SVM Queue-vital buffer unmap svm_range_unmap_from_cpu() 极低
TTM VRAM 压力 eviction evict_process_worker() restore_process_worker() 低-中
系统 Suspend / Resume kfd_suspend_all_processes() kfd_resume_all_processes() 极低
CRIU Checkpoint / Restore criu_checkpoint/restore() 后续流程 极低

所有路径的共同特征 :quiesce/restore 都是 per-process 粒度,没有 per-queue 或 per-BO 的细粒度控制。

其中 SVM MMU notifier 路径(场景 1)是运行时触发频率最高的路径,对性能影响最大。


2. 架构原因分析:为什么选择 Per-Process 粒度

2.1 Per-Process VM(Page Table)共享

KFD 的 GPU 虚拟内存管理采用 per-process 的 VM 模型:同一进程的所有 user queues 共享同一个 GPU page table(即同一个 VMID 和 page directory base)。

在 queue 创建时,page table base 取自进程级的 qcm_process_device,而非queue 自身:

c 复制代码
/* kfd_device_queue_manager.c - add_queue_mes() */
queue_input.process_id = pdd->pasid;
queue_input.page_table_base_addr = qpd->page_table_base;  // 进程级 page table
queue_input.process_va_start = 0;
queue_input.process_va_end = adev->vm_manager.max_pfn - 1;

qpd->page_table_base 是在 register_process() 时设置的进程级 page directory,所有 queue 共享同一地址空间范围 [0, max_pfn)

直接后果 :当某个 BO 或 SVM range 的 GPU page table entry 被 invalidate 后,该进程的 任意 queue 的 shader 都可能访问到这个已失效的 VA。驱动无法判断哪些 queues "安全"、哪些 queues 会触发 fault,因此只能保守地停掉所有 queues。

2.2 为什么不做 Per-Queue / Per-BO 的细粒度 Eviction

从架构上看,细粒度 eviction 面临根本性障碍------User Queue 模式下内核无法得知每个 queue 访问了哪些 BO

(1)User Queue 与图形 Job 提交模型的本质区别

在传统图形提交(如 amdgpu CS ioctl)路径中,内核负责构建 command buffer 并提交 job,因此内核清楚知道每个 job 引用了哪些 BO(通过 bo_list 参数显式传入)。TTM 可以基于这些信息做 per-job 粒度的 eviction 和 fence 管理。

但 KFD 采用的是 user queue 模型:queue 创建后,用户态直接向 queue 提交 dispatch,内核完全不参与提交过程,也无法拦截或解析用户态的提交内容。因此,内核无法建立 BO → Queue 的映射关系------它根本不知道某个 queue 的 shader 会访问哪些地址。

复制代码
图形提交模型:  App → ioctl(CS, bo_list) → Kernel(知道 BO 集合) → GPU
User Queue 模型:App → 直接写 doorbell → GPU(内核不可见)

(2)BO → Queue 映射关系不存在且无法建立

当前内核中没有维护"哪个 queue 引用了哪些 BO"的映射关系。KFD 的内存分配(kfd_ioctl_alloc_memory_of_gpu)和 queue 创建(kfd_ioctl_create_queue)是独立的 ioctl,它们之间没有建立关联。用户态通过 SVM 或 map_memory_to_gpu 建立的映射是进程级的,任何 queue 都可以访问。

即使引入 BO → Queue 的 tracking,这个映射也是高度动态的:

  • 用户态随时可以 map / unmap 内存
  • SVM range 的 GPU mapping 可以因 fault、migration、eviction 等原因变化
  • 需要在每次 mapping 变更时更新 tracking 数据结构,引入额外的锁和同步开销
  • 更关键的是,用户态的 shader 可以通过指针运算访问任意已映射的 VA, 这些访问模式内核完全不可见

2.3 小结

Per-process 粒度的 quiesce/restore 不是设计疏忽,而是以下架构约束的必然结果:

约束 影响
User Queue 模型,内核不参与提交 无法得知每个 queue 访问了哪些 BO
无 BO → Queue 映射关系且无法有效建立 无法做选择性 eviction

这是一个典型的 correctness vs. performance 的 trade-off:以粗粒度保证正确性,用简单的实现换取可维护性。


3. 性能影响分析与优化方向

3.1 各场景的性能影响评估

在第一章列举的 9 个场景中,系统级(suspend/resume)和 CRIU 场景属于低频操作,quiesce/restore 的开销可以忽略。真正影响运行时性能的是 SVM 路径TTM 路径

SVM 路径(场景 1)------ 运行时主要瓶颈

SVM MMU notifier invalidation 是触发频率最高的路径,其性能影响体现在三个阶段:

Quiesce 阶段:开销本身较小,但有去重机制控制频率

c 复制代码
/* 只在第一个 range 被 evict 时 quiesce,后续只递增计数器 */
evicted_ranges = atomic_inc_return(&svms->evicted_ranges);
if (evicted_ranges != 1)
    return r;

此外,mapped_to_gpu 检查避免了未映射 range 的无效 quiesce。这两个优化确保了单次 burst invalidation 只触发一次 quiesce。

Restore 阶段:这是主要的性能瓶颈

svm_range_restore_work() 的开销包括:

  1. 三把锁串行获取process_info->lockmmap_write_locksvms->lock

    持锁期间阻塞该进程所有其他内存操作

  2. 全量遍历 range list

c 复制代码
list_for_each_entry(prange, &svms->list, list) {
    invalid = atomic_read(&prange->invalid);
    if (!invalid)
        continue;   // 大量 range 在这里跳过
    svm_range_validate_and_map(...);
}

即使只有 1 个 range 被 invalidate,也要遍历整个 list。当进程有大量 SVM ranges 时(如大 working set 的 HPC 应用),遍历开销线性增长。

  1. 逐 range validate_and_map :每个 invalid range 需要重新获取 CPU 页面、更新 GPU page table,涉及 get_user_pages 和 GPU page table 写操作

Thrashing 风险 :restore 过程中如果又有新的 invalidation 发生,atomic_cmpxchg 会失败,导致整个 restore 重来:

c 复制代码
if (atomic_cmpxchg(&svms->evicted_ranges, evicted_ranges, 0) !=
    evicted_ranges)
    goto out_reschedule;   // 全部重来

在 CPU 内存活动频繁的场景下(如大量 mmap/munmap/migration),可能出现 quiesce → restore → 又被 quiesce 的反复抖动,GPU 在此期间完全空转。

TTM 路径(场景 4)------ VRAM 压力下的影响

TTM eviction 的触发频率低于 SVM 路径,但单次开销更大:

  • Quiesce 后需要等待 GPU 上所有进行中的工作完成
  • Restore 需要将所有 BO 重新 validate 到 VRAM(amdgpu_amdkfd_gpuvm_restore_process_bos),
    涉及 VRAM 分配和可能的 BO migration
  • Restore 延迟为 PROCESS_RESTORE_TIME_MS = 100ms,远大于 SVM 路径的 1ms
  • 失败时 back off 也是 100ms(PROCESS_BACK_OFF_TIME_MS

在多进程共享 GPU 且 VRAM 紧张的场景下,进程间可能互相 evict,导致大量时间消耗在 quiesce/restore 循环中。

3.2 性能影响程度对比

因素 SVM 路径 TTM 路径
触发频率 中-高(CPU 内存活动驱动) 低-中(VRAM 压力驱动)
Quiesce 开销 低(有去重)
Restore 延迟 1ms 起步 + validate 时间 100ms 起步 + BO restore 时间
主要瓶颈 全量遍历 range list + 锁竞争 BO validate + VRAM 分配
Thrashing 风险 高(CAS 失败重调度) 中(进程间互相 evict)
GPU 空转时间 与 invalid range 数量成正比 与 BO 总量成正比

3.3 XNACK Off 模式下的优化方向

在不改变 XNACK off 前提下,以下优化可以缓解性能问题:

优化 1:Evicted List 替代全量遍历(推荐,低成本高收益)

当前 restore work 遍历 svms->list 上的所有 range,逐一检查 invalid 标记。

可以引入独立的 evicted list,只追踪被 invalidate 的 ranges:

c 复制代码
/* 当前实现:O(total_ranges) */
list_for_each_entry(prange, &svms->list, list) {
    if (!atomic_read(&prange->invalid))
        continue;
    svm_range_validate_and_map(...);
}

/* 优化后:O(evicted_ranges) */
list_for_each_entry_safe(prange, next, &svms->evicted_list, evict_link) {
    svm_range_validate_and_map(...);
    list_del(&prange->evict_link);
}

收益:restore 时间从 O(total_ranges) 降为 O(evicted_ranges),对大 working set 场景改善明显。改动局部,风险可控。

优化 2:Batch Coalescing 合并多次 Eviction(不可行)

当前第一次 eviction 立刻 quiesce,restore delay 仅 1ms。在短时间内发生大量 MMU notifier 事件时(如批量 munmap),可能触发多次 quiesce → restore → 再 quiesce 的抖动。

此优化不可行 。MMU notifier 的 .invalidate 回调(即svm_range_cpu_invalidate_pagetables

invalidate_range_start 路径中被调用,其语义要求:回调返回前,设备侧必须不再访问被 invalidate 的页面

如果引入延迟窗口来 batch invalidation,在窗口期内 CPU page table 已经 invalidate,但 GPU queues 仍在运行并可能访问旧映射,这直接违反了 MMU notifier 的正确性 contract。因此 quiesce 必须在 notifier 回调返回前完成,不能延迟。

优化 3:锁粒度优化(值得探索)

当前 restore work 持有三把锁(process_info->lock + mmap_write_lock + svms->lock

贯穿整个 validate_and_map 过程,阻塞了其他操作。

可以将 restore 拆分为两阶段:

  1. 锁外阶段:准备 page 数据(get_user_pages 等 IO 密集操作)
  2. 短暂持锁:更新 GPU page table

收益:减少持锁时间,降低对 SVM fault handling 等并发路径的阻塞。

复杂度:中等,需要仔细处理并发 invalidation 与 restore 的竞争。

不推荐的方向
方向 原因
Per-queue eviction User queue 模型下无法得知 queue 访问了哪些 BO(见第二章分析)
延迟 quiesce + GPU fault recovery XNACK off 下 VM fault 是 fatal 的,等于重新实现 XNACK

3.4 小结

Per-process quiesce/restore 的性能影响主要集中在 SVM restore 路径:全量遍历 range list、重锁竞争、以及 CAS 失败导致的 thrashing。

在 XNACK off 模式下,evicted list 和锁粒度优化是可探索的优化方向,可以降低 restore 开销。但这些都是渐进式改良,无法从根本上消除 quiesce/restore 的开销------这需要 XNACK on。


4. 终极方案:XNACK On 模式

4.1 XNACK On 如何从根本上消除 SVM 路径的 Quiesce/Restore

在 XNACK on 模式下,GPU MMU 支持 retry fault :当 GPU 访问到无效的 page table entry 时,不会产生 fatal VM fault,而是暂停发出访问的 wavefront,产生一个 retry fault 中断,等待内核处理完成并更新 page table 后,GPU 自动 retry 该访问。

这从根本上改变了 SVM invalidation 的处理方式。对比 svm_range_evict() 中的两个分支:

c 复制代码
/* kfd_svm.c - svm_range_evict() */
if (!p->xnack_enabled || (prange->flags & KFD_IOCTL_SVM_FLAG_GPU_ALWAYS_MAPPED)) {
    /* XNACK Off 路径:quiesce 所有 queues,schedule restore work */
    evicted_ranges = atomic_inc_return(&svms->evicted_ranges);
    if (evicted_ranges != 1)
        return r;
    r = kgd2kfd_quiesce_mm(mm, KFD_QUEUE_EVICTION_TRIGGER_SVM);
    queue_delayed_work(..., &svms->restore_work, ...);
} else {
    /* XNACK On 路径:只 unmap GPU page table,不 quiesce queues */
    svm_range_unmap_from_gpus(prange, s, l, trigger);
}

XNACK on 路径的关键区别

  1. 不调用 kgd2kfd_quiesce_mm()------queues 保持运行
  2. 不调度 svm_range_restore_work()------不需要全量 restore
  3. 只做 svm_range_unmap_from_gpus()------invalidate 受影响的 GPU page table entries
  4. 如果 queue 的 shader 后续访问了被 unmap 的地址,GPU 硬件自动产生 retry fault,由内核 fault handler 按需恢复映射

4.2 GPU Retry Fault 处理流程

当 XNACK on 模式下 GPU 遇到 page fault 时,处理流程如下:

复制代码
GPU shader 访问无效 VA → GPU MMU retry fault → 中断 → KFD interrupt handler
→ svm_range_restore_pages()
    → 查找对应的 svm_range
    → svm_range_best_restore_location() 决定最佳位置
    → 如果需要,执行 migration(svm_migrate_to_vram / svm_migrate_vram_to_ram)
    → svm_range_validate_and_map() 重建单个 range 的 GPU mapping
→ GPU 自动 retry 访问,wavefront 恢复执行

关键代码在 svm_range_restore_pages() 中,它只处理 fault 涉及的那一个 range

c 复制代码
/* kfd_svm.c - svm_range_restore_pages() */
prange = svm_range_from_addr(svms, addr, NULL);
...
r = svm_range_validate_and_map(mm, start, last, prange, gpuidx, false, false, false);

4.3 XNACK On 消除的三个核心问题

问题 XNACK Off XNACK On
Quiesce 所有 queues 每次 MMU invalidation 都要停掉所有 queues 不需要 quiesce,queues 保持运行
全量遍历 range list restore work 遍历整个 svms->list 按需处理,只 validate fault 涉及的 range
GPU 空转等待 restore quiesce 到 restore 完成期间 GPU 完全空闲 只有 fault 的 wavefront 暂停,其他 wavefront 继续执行

这三个优势叠加,使得 XNACK on 在 SVM 场景下的性能显著优于 XNACK off。

4.4 XNACK On 未能消除的场景

需要注意的是,XNACK on 只消除了 SVM MMU notifier 路径(场景 1)的 quiesce/restore

以下场景仍然需要 per-process quiesce:

  • TTM BO eviction(场景 4):VRAM 压力下的 BO eviction 仍走 eviction fence 路径
  • System suspend/resume(场景 6、7):系统级操作不受 XNACK 影响
  • CRIU(场景 8、9):进程状态序列化仍需要 quiesce
  • Queue-vital buffer unmap(场景 2):queue 关键内存释放仍需要 quiesce

但这些场景触发频率远低于 SVM 路径,实际影响有限。

4.5 XNACK On 的 Trade-off

XNACK on 并非没有代价:

(1)Retry fault 的延迟开销

每次 retry fault 需要经过中断处理、内核 fault handler、page table 更新、GPU retry 的完整路径。如果 fault 频率高(如首次访问大量新映射),累积的 fault handling 开销可能显著。不过对比 XNACK off 下停掉所有 queues + 全量 restore 的方案,单次 fault 的代价远小于全量 quiesce。

(2)TLB 管理开销

XNACK on 要求 GPU TLB invalidation 更频繁、更精确,以确保 retry fault 能正确触发。这增加了 TLB miss rate 和 invalidation 的开销。

(3)硬件兼容性限制

不是所有 GPU 都支持 XNACK on:

c 复制代码
/* kfd_process.c - kfd_process_xnack_mode() */
/* Aldebaran (MI200) 支持 per-process XNACK 模式选择 */
if (supported && KFD_SUPPORT_XNACK_PER_PROCESS(dev))
    continue;

/* GFXv10+ 不支持 page fault 期间的 shader preemption,可能导致死锁 */
if (KFD_GC_VERSION(dev) >= IP_VERSION(10, 1, 1))
    return false;

/* 如果硬件设置了 noretry,不支持 XNACK */
if (dev->kfd->noretry)
    return false;
  • GFX9(Vega 系列):支持 XNACK,但 SQ retry 模式必须在 boot 时全局设定,混合 XNACK on/off 进程可能 hang GPU
  • Aldebaran(MI200/MI210/MI250) :支持 per-process XNACK 模式选择, 可以每个进程独立配置,是 XNACK on 的最佳平台
  • GFX10+(RDNA 系列)不支持 page fault 期间的 shader preemption,可能导致 QoS 问题或死锁,因此代码直接返回 false
  • MI300 系列:延续 Aldebaran 的 per-process XNACK 支持

(4)XNACK on 对 SVM memory accounting 的影响

XNACK on 时不预留 system memory limit(因为映射是按需建立的),XNACK off 时会预留:

c 复制代码
/* kfd_svm.c - svm_range_new() */
if (!p->xnack_enabled && update_mem_usage &&
    amdgpu_amdkfd_reserve_mem_limit(NULL, size << PAGE_SHIFT,
                KFD_IOC_ALLOC_MEM_FLAGS_USERPTR, 0)) {
    ...
}

4.6 小结

XNACK on 通过 GPU 硬件 retry fault 机制,从根本上消除了 SVM 路径(场景 1)中per-process quiesce/restore 的需求:

复制代码
XNACK Off: MMU invalidation → quiesce 所有 queues → restore work 全量遍历恢复 → resume
XNACK On:  MMU invalidation → unmap GPU PTE → GPU retry fault → 按需恢复单个 range

核心收益:

  • 零 quiesce:queues 永不因 SVM invalidation 被停止
  • 按需恢复:只处理 GPU 实际访问到的 range,而非全量 restore
  • 最小粒度暂停:只有触发 fault 的 wavefront 暂停,其余继续执行

在支持 per-process XNACK 的硬件(MI200/MI300)上,XNACK on 应作为 SVM 场景的默认模式。


五、总结

章节 关键结论
问题 KFD 的 quiesce/restore 是 per-process 粒度的 all-or-nothing 操作,共 9 个触发场景
根因 User queue 模型下内核无法得知 queue 访问了哪些 BO,无法做细粒度 eviction
性能 SVM restore 路径是主要瓶颈:全量遍历、重锁竞争、thrashing 风险
XNACK off 优化 Evicted list(可行)、锁粒度优化(值得探索),但都是渐进式改良
XNACK on 从根本上消除 SVM 路径的 quiesce/restore,是终极解决方案