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() 的开销包括:
-
三把锁串行获取 :
process_info->lock→mmap_write_lock→svms->lock,持锁期间阻塞该进程所有其他内存操作
-
全量遍历 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 应用),遍历开销线性增长。
- 逐 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 拆分为两阶段:
- 锁外阶段:准备 page 数据(
get_user_pages等 IO 密集操作) - 短暂持锁:更新 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 路径的关键区别:
- 不调用
kgd2kfd_quiesce_mm()------queues 保持运行 - 不调度
svm_range_restore_work()------不需要全量 restore - 只做
svm_range_unmap_from_gpus()------invalidate 受影响的 GPU page table entries - 如果 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,是终极解决方案 |