难度 : 🔴🔴 高级
预计学习时间 : 2.5-3小时
前置知识: 第6-8章、GPU架构、页面异常处理
📋 概述
缺页处理(Page Fault Handling)是SVM最核心的功能之一。当GPU或CPU访问未映射的页面时,会触发页面异常,驱动需要快速恢复访问。想象一下:
- 🚨 页面异常: GPU访问地址时发现页表无映射
- ⏸️ XNACK暂停: GPU暂停执行,等待驱动修复
- 🔧 驱动修复: 查找范围、迁移数据、建立映射
- ▶️ 重试成功: GPU恢复执行,访问成功
本章深入页面异常的检测、处理和恢复机制。
9.1 XNACK机制
什么是XNACK?
XNACK(Execute No Acknowledge)是AMD GPU的页面错误重试机制:
传统GPU(无XNACK):
访问未映射地址 → VM Fault → GPU挂起 → 无法恢复
XNACK GPU:
访问未映射地址 → VM Fault → 暂停执行 → 通知CPU
↓
驱动修复映射
↓
GPU重试访问 → 成功!
XNACK硬件支持:
- GFX9及以上架构(Vega, RDNA, CDNA)
- 需要在进程创建时启用
- 检查方式:
/sys/class/kfd/kfd/nodes/0/capabilities
XNACK工作流程
步骤1: GPU shader执行
┌────────────────────────────┐
│ GPU: load [0x12345000] │ ← 访问虚拟地址
└────────────────────────────┘
↓
步骤2: 页表查询
┌────────────────────────────┐
│ GPU MMU: 查找PTE │
│ 结果: PTE无效或不存在 │
└────────────────────────────┘
↓
步骤3: 触发VM Fault
┌────────────────────────────┐
│ 硬件记录: │
│ - 故障地址: 0x12345000 │
│ - PASID: 0x8001 │
│ - VMID: 7 │
│ - 写故障?: false │
│ - 时间戳: TS │
└────────────────────────────┘
↓
步骤4: 中断CPU
┌────────────────────────────┐
│ GPU → PCIe → CPU IRQ │
│ 调用: amdgpu_irq_handler() │
└────────────────────────────┘
↓
步骤5: 查找进程和范围
┌────────────────────────────┐
│ svm_range_restore_pages() │
│ - 根据PASID找进程 │
│ - 根据地址找svm_range │
└────────────────────────────┘
↓
步骤6: 修复映射
┌────────────────────────────┐
│ - 迁移页面(如需要) │
│ - 更新GPU页表 │
└────────────────────────────┘
↓
步骤7: GPU重试
┌────────────────────────────┐
│ GPU: 重新执行load指令 │
│ 结果: 成功! │
└────────────────────────────┘
XNACK启用检查
c
// 进程创建时
struct kfd_process *p = kfd_create_process(...);
// 检查GPU是否支持XNACK
if (node->adev->gfx.xnack_enabled) {
p->xnack_enabled = true;
}
// 后续使用
if (!p->xnack_enabled) {
pr_debug("XNACK not enabled for pasid 0x%x\n", pasid);
return -EFAULT; // 不支持页面错误恢复
}
9.2 GPU页面异常处理
主函数:svm_range_restore_pages
这是GPU页面异常的入口函数:
c
int svm_range_restore_pages(struct amdgpu_device *adev,
unsigned int pasid, // 进程ID
uint32_t vmid, // 虚拟机ID
uint32_t node_id, // GPU节点
uint64_t addr, // 故障地址(页号)
uint64_t ts, // 时间戳
bool write_fault) // 写故障?
参数来源:
- 从GPU中断处理程序传递
addr: 触发故障的虚拟地址(页号)ts: 硬件时间戳,用于检测过时故障write_fault: 是否因写操作触发
处理流程详解
┌─────────────────────────────────────────┐
│ 1. 查找进程和GPU节点 │
│ kfd_lookup_process_by_pasid() │
│ kfd_node_by_irq_ids() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. 检查XNACK是否启用 │
│ if (!p->xnack_enabled) return │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. 获取进程mm_struct │
│ mm = get_task_mm(p->lead_thread) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. 检查时间戳(draining检查) │
│ if (ts < checkpoint_ts) → 过时故障 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 5. 查找或创建svm_range │
│ svm_range_from_addr() │
│ 或 svm_range_create_unregistered() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 6. 检查故障有效性 │
│ - 检查VMA是否存在 │
│ - 检查访问权限 │
│ - 跳过重复故障 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 7. 确定最佳恢复位置 │
│ svm_range_best_restore_location() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 8. 迁移页面(如需要) │
│ svm_migrate_to_vram() 或 │
│ svm_migrate_vram_to_ram() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 9. 验证并映射 │
│ svm_range_validate_and_map() │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 10. 更新统计信息 │
│ pdd->faults++ │
└─────────────────────────────────────────┘
代码分析
c
int svm_range_restore_pages(struct amdgpu_device *adev,
unsigned int pasid,
uint32_t vmid, uint32_t node_id,
uint64_t addr, uint64_t ts, bool write_fault)
{
struct svm_range_list *svms;
struct svm_range *prange;
struct kfd_process *p;
struct kfd_node *node;
struct mm_struct *mm;
int32_t best_loc, gpuid, gpuidx;
bool write_locked = false;
int r = 0;
// 1. 设备检查
if (!KFD_IS_SVM_API_SUPPORTED(adev))
return -EFAULT;
// 2. 查找进程
p = kfd_lookup_process_by_pasid(pasid, NULL);
svms = &p->svms;
// 3. 检查是否正在draining(排空故障)
if (atomic_read(&svms->drain_pagefaults)) {
pr_debug("page fault handling disabled\n");
r = 0;
goto out;
}
// 4. 查找GPU节点
node = kfd_node_by_irq_ids(adev, node_id, vmid);
// 5. 获取GPU ID
if (kfd_process_gpuid_from_node(p, node, &gpuid, &gpuidx)) {
r = -EFAULT;
goto out;
}
// 6. XNACK检查
if (!p->xnack_enabled) {
pr_debug("XNACK not enabled for pasid 0x%x\n", pasid);
r = -EFAULT;
goto out;
}
// 7. 获取mm_struct
mm = get_task_mm(p->lead_thread);
// 8. 加锁(初始为读锁)
mmap_read_lock(mm);
retry_write_locked:
mutex_lock(&svms->lock);
// 9. 检查时间戳(排空检查)
if (svms->checkpoint_ts[gpuidx] != 0) {
if (amdgpu_ih_ts_after_or_equal(ts, svms->checkpoint_ts[gpuidx])) {
pr_debug("draining retry fault, drop fault\n");
r = -EAGAIN;
goto out_unlock_svms;
} else {
// 时间戳已经包装,重置checkpoint
svms->checkpoint_ts[gpuidx] = 0;
}
}
// 10. 查找范围
prange = svm_range_from_addr(svms, addr, NULL);
if (!prange) {
// 范围不存在,需要创建
if (!write_locked) {
// 需要写锁来创建范围和MMU notifier
mutex_unlock(&svms->lock);
mmap_read_unlock(mm);
mmap_write_lock(mm);
write_locked = true;
goto retry_write_locked;
}
// 创建未注册的范围
prange = svm_range_create_unregistered_range(node, p, mm, addr);
if (!prange) {
r = -EFAULT;
goto out_unlock_svms;
}
}
// 降级锁(如果之前升级了)
if (write_locked)
mmap_write_downgrade(mm);
// 11. 锁定范围
mutex_lock(&prange->migrate_mutex);
// 12. 检查是否应跳过恢复
if (svm_range_skip_recover(prange)) {
// 范围正在被删除或修改
amdgpu_gmc_filter_faults_remove(node->adev, addr, pasid);
r = 0;
goto out_unlock_range;
}
// 13. 跳过重复故障(相同范围的多次故障)
ktime_t timestamp = ktime_get_boottime();
if (ktime_before(timestamp,
ktime_add_ns(prange->validate_timestamp,
AMDGPU_SVM_RANGE_RETRY_FAULT_PENDING))) {
pr_debug("already restored\n");
r = 0;
goto out_unlock_range;
}
// 14. 检查VMA是否存在
struct vm_area_struct *vma = vma_lookup(mm, addr << PAGE_SHIFT);
if (!vma) {
// VMA已被删除,这是过时故障
pr_debug("VMA is removed\n");
r = 0;
goto out_unlock_range;
}
// 15. 检查访问权限
if (!svm_fault_allowed(vma, write_fault)) {
pr_debug("fault addr 0x%llx no %s permission\n",
addr, write_fault ? "write" : "read");
r = -EPERM;
goto out_unlock_range;
}
// 16. 确定最佳恢复位置
best_loc = svm_range_best_restore_location(prange, node, &gpuidx);
if (best_loc == -1) {
// GPU没有访问权限
pr_debug("failed get best restore loc\n");
r = -EACCES;
goto out_unlock_range;
}
pr_debug("best restore 0x%x, actual loc 0x%x\n",
best_loc, prange->actual_loc);
// 17. SMI事件:页面故障开始
kfd_smi_event_page_fault_start(node, p->lead_thread->pid,
addr, write_fault, timestamp);
// 18. 对齐迁移范围
unsigned long size = 1UL << prange->granularity;
unsigned long start = max_t(unsigned long,
ALIGN_DOWN(addr, size), prange->start);
unsigned long last = min_t(unsigned long,
ALIGN(addr + 1, size) - 1, prange->last);
// 19. 迁移(如需要)
bool migration = false;
if (prange->actual_loc != 0 || best_loc != 0) {
if (best_loc) {
// 迁移到VRAM
r = svm_migrate_to_vram(prange, best_loc, start, last, mm,
KFD_MIGRATE_TRIGGER_PAGEFAULT_GPU);
if (r) {
pr_debug("migrate to vram failed, fallback to RAM\n");
// 回退到系统内存
if (prange->actual_loc && prange->actual_loc != best_loc)
r = svm_migrate_vram_to_ram(prange, mm, start, last,
KFD_MIGRATE_TRIGGER_PAGEFAULT_GPU, NULL);
else
r = 0;
}
} else {
// 迁移到系统RAM
r = svm_migrate_vram_to_ram(prange, mm, start, last,
KFD_MIGRATE_TRIGGER_PAGEFAULT_GPU, NULL);
}
if (r) {
pr_debug("failed %d to migrate\n", r);
goto out_migrate_fail;
} else {
migration = true;
}
}
// 20. 验证并映射
r = svm_range_validate_and_map(mm, start, last, prange, gpuidx,
false, false, false);
if (r)
pr_debug("failed %d to map to gpus\n", r);
out_migrate_fail:
// SMI事件:页面故障结束
kfd_smi_event_page_fault_end(node, p->lead_thread->pid,
addr, migration);
out_unlock_range:
mutex_unlock(&prange->migrate_mutex);
out_unlock_svms:
mutex_unlock(&svms->lock);
mmap_read_unlock(mm);
// 更新统计
if (r != -EAGAIN)
svm_range_count_fault(node, p, gpuidx);
mmput(mm);
out:
kfd_unref_process(p);
// 清理重试故障过滤器
if (r == -EAGAIN) {
amdgpu_gmc_filter_faults_remove(node->adev, addr, pasid);
r = 0;
}
return r;
}
9.3 自动范围创建
未注册范围的处理
当GPU访问从未注册过的地址时,驱动会自动创建SVM范围:
c
static struct svm_range *
svm_range_create_unregistered_range(struct kfd_node *node,
struct kfd_process *p,
struct mm_struct *mm,
int64_t addr)
创建流程:
1. 查找VMA边界
svm_range_get_range_boundaries()
↓
2. 检查冲突
- 检查是否与VM BO重叠
- 检查是否与userptr重叠
↓
3. 如果重叠,缩小范围到单页
start = addr
last = addr
↓
4. 创建范围
prange = svm_range_new(&p->svms, start, last, true)
↓
5. 设置属性
if (is_heap_stack)
prange->preferred_loc = SYSMEM
↓
6. 添加到SVMS
svm_range_add_to_svms(prange)
svm_range_add_notifier_locked(mm, prange)
边界计算
c
static int svm_range_get_range_boundaries(struct kfd_process *p,
int64_t addr,
unsigned long *start,
unsigned long *last,
bool *is_heap_stack)
{
struct vm_area_struct *vma;
unsigned long start_limit, end_limit;
// 1. 查找VMA
vma = vma_lookup(p->mm, addr << PAGE_SHIFT);
if (!vma)
return -EFAULT;
// 2. 检查是否为堆或栈
*is_heap_stack = vma_is_initial_heap(vma) || vma_is_initial_stack(vma);
// 3. 按粒度对齐
start_limit = max(vma->vm_start >> PAGE_SHIFT,
ALIGN_DOWN(addr, 1UL << p->svms.default_granularity));
end_limit = min(vma->vm_end >> PAGE_SHIFT,
ALIGN(addr + 1, 1UL << p->svms.default_granularity));
// 4. 查找相邻范围,避免重叠
// 查找addr之后的第一个范围
node = interval_tree_iter_first(&p->svms.objects, addr + 1, ULONG_MAX);
if (node)
end_limit = min(end_limit, node->start);
// 查找addr之前的最后一个范围
rb_node = rb_prev(&node->rb);
if (rb_node) {
node = container_of(rb_node, struct interval_tree_node, rb);
start_limit = max(start_limit, node->last + 1);
}
*start = start_limit;
*last = end_limit - 1;
return 0;
}
示例:
VMA: [0x1000-0x5000]
粒度: 2MB (0x200页)
故障地址: 0x1234
计算:
对齐起始: ALIGN_DOWN(0x1234, 0x200) = 0x1200
对齐结束: ALIGN(0x1235, 0x200) = 0x1400
检查相邻范围:
前面有范围 [0x1000-0x11FF]
后面有范围 [0x1500-0x17FF]
最终范围: [0x1200-0x14FF]
9.4 最佳恢复位置选择
svm_range_best_restore_location
决定将页面恢复到哪里(系统RAM还是GPU VRAM):
c
static int32_t svm_range_best_restore_location(struct svm_range *prange,
struct kfd_node *node,
int32_t *gpuidx)
决策流程:
┌─────────────────────────────────────────┐
│ APU且prefer_gtt? │
│ → 返回0 (系统内存) │
└─────────────────────────────────────────┘
↓ 否
┌─────────────────────────────────────────┐
│ preferred_loc == 故障GPU │
│ 或 preferred_loc == SYSMEM? │
│ → 返回preferred_loc │
└─────────────────────────────────────────┘
↓ 否
┌─────────────────────────────────────────┐
│ preferred_loc是其他GPU │
│ 且与故障GPU在同一XGMI hive? │
│ → 返回preferred_loc │
└─────────────────────────────────────────┘
↓ 否
┌─────────────────────────────────────────┐
│ 故障GPU在ACCESS位图中? │
│ → 返回故障GPU ID │
└─────────────────────────────────────────┘
↓ 否
┌─────────────────────────────────────────┐
│ 故障GPU在ACCESS_IN_PLACE位图中? │
│ → actual_loc==0? 返回0 │
│ → actual_loc GPU与故障GPU同hive? │
│ 返回actual_loc │
│ → 否则返回0 │
└─────────────────────────────────────────┘
↓ 否
┌─────────────────────────────────────────┐
│ 无访问权限 │
│ → 返回-1 │
└─────────────────────────────────────────┘
代码实现:
c
static int32_t svm_range_best_restore_location(struct svm_range *prange,
struct kfd_node *node,
int32_t *gpuidx)
{
struct kfd_node *bo_node, *preferred_node;
struct kfd_process *p;
uint32_t gpuid;
p = container_of(prange->svms, struct kfd_process, svms);
// 获取故障GPU的ID
r = kfd_process_gpuid_from_node(p, node, &gpuid, gpuidx);
// 1. APU且prefer_gtt
if (node->adev->apu_prefer_gtt)
return 0;
// 2. preferred_loc检查
if (prange->preferred_loc == gpuid ||
prange->preferred_loc == KFD_IOCTL_SVM_LOCATION_SYSMEM) {
return prange->preferred_loc;
}
// 3. preferred_loc是其他GPU,检查XGMI
if (prange->preferred_loc != KFD_IOCTL_SVM_LOCATION_UNDEFINED) {
preferred_node = svm_range_get_node_by_id(prange,
prange->preferred_loc);
if (preferred_node && svm_nodes_in_same_hive(node, preferred_node))
return prange->preferred_loc;
}
// 4. ACCESS权限
if (test_bit(*gpuidx, prange->bitmap_access))
return gpuid;
// 5. ACCESS_IN_PLACE权限
if (test_bit(*gpuidx, prange->bitmap_aip)) {
if (!prange->actual_loc)
return 0; // 系统内存
bo_node = svm_range_get_node_by_id(prange, prange->actual_loc);
if (bo_node && svm_nodes_in_same_hive(node, bo_node))
return prange->actual_loc;
else
return 0;
}
// 6. 无访问权限
return -1;
}
示例场景:
场景1: 单GPU,有ACCESS权限
preferred_loc: UNDEFINED
bitmap_access: GPU0=1
故障GPU: GPU0
→ 返回: GPU0 (迁移到GPU0 VRAM)
场景2: 多GPU XGMI,preferred_loc设置
preferred_loc: GPU1
actual_loc: GPU1
故障GPU: GPU0
GPU0和GPU1在同一hive
→ 返回: GPU1 (无需迁移,直接通过XGMI访问)
场景3: ACCESS_IN_PLACE,数据在系统RAM
bitmap_aip: GPU0=1
actual_loc: 0 (系统RAM)
故障GPU: GPU0
→ 返回: 0 (保持在系统RAM,远程访问)
场景4: 无权限
bitmap_access: 空
bitmap_aip: 空
故障GPU: GPU0
→ 返回: -1 (无访问权限,返回错误)
9.5 故障过滤与优化
重复故障检测
避免对同一范围的多次故障进行重复处理:
c
// 检查validate_timestamp
ktime_t timestamp = ktime_get_boottime();
if (ktime_before(timestamp,
ktime_add_ns(prange->validate_timestamp,
AMDGPU_SVM_RANGE_RETRY_FAULT_PENDING))) {
// 这个范围最近刚恢复过,跳过
pr_debug("already restored\n");
return 0;
}
AMDGPU_SVM_RANGE_RETRY_FAULT_PENDING:
- 默认值:2ms (2,000,000 ns)
- 意义:2ms内的重复故障被视为同一次故障
示例:
时间轴:
T0: GPU访问prange的页0 → 故障1 → 驱动恢复 → validate_timestamp=T0
T0+1ms: GPU访问prange的页1 → 故障2
检查: T0+1ms < T0+2ms → 跳过(认为是故障1的延续)
T0+3ms: GPU访问prange的页2 → 故障3
检查: T0+3ms > T0+2ms → 处理(新的故障)
Draining机制
在某些操作期间禁用故障处理:
c
// 设置draining标志
atomic_set(&svms->drain_pagefaults, 1);
// 记录checkpoint时间戳
svms->checkpoint_ts[gpuidx] = amdgpu_ih_get_wptr_ts(adev);
// 等待所有排队的故障处理完成
// ...
// 清除draining标志
atomic_set(&svms->drain_pagefaults, 0);
使用场景:
- 进程退出时
- 大规模内存操作前
- 驱动重置时
时间戳检查:
c
if (svms->checkpoint_ts[gpuidx] != 0) {
if (amdgpu_ih_ts_after_or_equal(ts, svms->checkpoint_ts[gpuidx])) {
// 故障是在checkpoint之后发生的,排空中
return -EAGAIN;
} else {
// 故障是在checkpoint之前的,已经wrap around
svms->checkpoint_ts[gpuidx] = 0;
}
}
硬件故障过滤
GPU硬件维护一个故障过滤器,避免重复中断:
c
// 从过滤器中移除故障
amdgpu_gmc_filter_faults_remove(node->adev, addr, pasid);
工作原理:
GPU发现故障 → 添加到过滤器
↓
同样故障再次发生 → 被过滤器阻止 → 不产生中断
↓
驱动恢复完成 → 移除过滤器 → 允许重试
9.6 故障统计
统计信息
c
struct kfd_process_device {
// ...
atomic64_t faults; // 总故障次数
atomic64_t page_in; // 迁移到VRAM的页数
atomic64_t page_out; // 迁移到RAM的页数
};
统计更新
c
static void svm_range_count_fault(struct kfd_node *node,
struct kfd_process *p,
int32_t gpuidx)
{
struct kfd_process_device *pdd;
pdd = kfd_process_device_from_gpuidx(p, gpuidx);
if (pdd)
WRITE_ONCE(pdd->faults, pdd->faults + 1);
}
查看统计
bash
# 查看进程的SVM统计
cat /sys/kernel/debug/kfd/proc/<pid>/svm_ranges
# 输出示例:
# GPU 0:
# Faults: 1234
# Pages in: 5678
# Pages out: 234
💡 重点提示
-
XNACK是必需的:没有XNACK,SVM无法处理GPU页面异常。
-
自动范围创建:首次访问时自动创建范围,简化用户使用。
-
智能恢复位置:根据preferred_loc和访问模式选择最佳位置。
-
重复故障优化:2ms窗口内的重复故障被合并。
-
Draining保护:关键操作期间禁用故障处理,防止竞争。
⚠️ 常见陷阱
❌ 陷阱1:"禁用XNACK运行SVM程序"
- ✅ 正确:确保GPU支持并启用XNACK。
❌ 陷阱2:"大量小范围导致频繁故障"
- ✅ 正确:使用更大的粒度(granularity)减少故障。
❌ 陷阱3:"忽略权限检查"
- ✅ 正确:确保VMA有正确的读写权限。
❌ 陷阱4:"未处理-EACCES错误"
- ✅ 正确:GPU无访问权限时,应用需要修改访问策略。
📝 实践练习
-
追踪页面故障:
bash# 启用页面故障调试 echo 'file kfd_svm.c line 2972 +p' > /sys/kernel/debug/dynamic_debug/control # 运行GPU程序 ./my_svm_app # 查看故障日志 dmesg | grep "svm_range_restore_pages" -
查看故障统计:
bash# 实时监控故障 watch -n 1 'cat /sys/kernel/debug/kfd/proc/*/svm_ranges | grep Faults' -
思考题:
- 为什么需要2ms的重复故障窗口?
- Draining机制如何防止竞争?
- XGMI如何影响最佳恢复位置?
- 自动范围创建可能带来什么问题?
-
性能分析:
bash# 使用perf追踪页面故障 perf record -e amdgpu:amdgpu_vm_fault -a perf report
📚 本章小结
- XNACK机制:GPU页面异常重试的硬件支持
- restore_pages:故障处理的核心函数
- 自动创建范围:首次访问时自动注册
- 最佳位置选择:根据策略选择RAM或VRAM
- 故障优化:重复检测、Draining、硬件过滤
- 统计监控:跟踪故障和迁移情况
缺页处理是SVM的核心,理解这一机制对优化性能至关重要。
➡️ 下一步
掌握了缺页处理后,下一章我们将学习MMU Notifier集成------如何同步CPU页表变化。
🔗 导航
- 上一章:08 - 页面映射与GPU页表
- 下一章:审核中...
- 返回目录: AMD ROCm-SVM技术的实现与应用深度分析目录