AMD 正在使用 drm svm框架重构SVM的实现,看来drm svm框架要进入大范围应用了。下面是在kernel社区上由AMD的开发人员提交的POC 验证版本的patches的技术方案实现。这里快速总结了实现,以飨读者。
因是POC版本,设计可能会变动,读者们慎重使用。本文仅用来跟踪前沿驱动技术的迭代发展现状。
1. 结论
amdgpu_svm_attr_range(属性层)与 amdgpu_svm_range(映射层)不是一一对应的 ,而是 1:N 关系 ------一个 attr_range 对应多个 svm_range。两者有不同的拆分标准、不同的粒度、不同的生命周期,通过 amdgpu_svm_range_apply_attr_change() 桥接。
反方向(一个 svm_range 跨多个 attr_range)在设计上不允许 ,因为 amdgpu_svm_range_update_gpu_range() 对整个 svm_range 施加统一的 PTE flags,如果跨了不同属性的 attr_range,映射会出错。
2. 两层架构概览
┌─────────────────────────────────────────────────────────────────┐
│ 用户空间 ioctl │
│ amdgpu_svm_attr_set(start, size, attrs) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 属性层 (Attribute Layer) │
│ │
│ amdgpu_svm_attr_range: 存储在 attr_tree->tree (interval tree) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ range A │ │ range B │ │ range C │ ← 按属性边界拆分 │
│ │flags=RW │ │flags=RO │ │flags=RW │ │
│ │access=EN │ │access=EN │ │access=NO │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ 职责:记录用户设置的属性元数据(纯数据,无 GPU 映射状态) │
└────────────────────────────┬────────────────────────────────────┘
│ attr_change → trigger
▼
┌─────────────────────────────────────────────────────────────────┐
│ 映射层 (Mapping Layer) │
│ │
│ amdgpu_svm_range (wraps drm_gpusvm_range): │
│ 存储在 drm_gpusvm_notifier 内的 interval tree │
│ ┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐ │
│ │ r0 ││ r1 ││ r2 ││ r3 ││ r4 ││ r5 ││ r6 ││ r7 │ ← chunk对齐 │
│ └────┘└────┘└────┘└────┘└────┘└────┘└────┘└────┘ │
│ 职责:持有实际 GPU 页表映射、DMA 状态、pages │
└─────────────────────────────────────────────────────────────────┘
3. 两层对比
| 维度 | amdgpu_svm_attr_range (属性层) |
amdgpu_svm_range / drm_gpusvm_range (映射层) |
|---|---|---|
| 存储位置 | attr_tree->tree (interval tree) |
drm_gpusvm_notifier 内的 interval tree |
| 职责 | 记录用户设置的属性元数据 | 持有实际 GPU 页表映射、DMA 状态、pages |
| 拆分依据 | 用户 ioctl 设置的属性边界 | chunk_size 对齐、VMA 边界、notifier 边界 |
| 创建时机 | amdgpu_svm_attr_set_range() --- ioctl 驱动 |
drm_gpusvm_range_find_or_insert() --- fault/map 驱动 |
| 持有内容 | preferred_loc, flags, access, granularity |
gpu_mapped, pte_flags, pages, DMA mapping |
| 粒度 | 任意大(如用户对 1GB 区间设置属性 → 一个 attr_range) | 较小,受 chunk_sizes (4K/64K/2M)、VMA、notifier 约束 |
| 生命周期 | 由用户 ioctl 创建,attr_clear_pages 或进程退出时销毁 |
由 fault/map 创建,mmu_notifier invalidate 时可回收 |
4. 拆分标准的差异
4.1 attr_range 的拆分:属性边界驱动
amdgpu_svm_attr_set_existing() 在用户对一个已有 attr_range 的子区间设置新属性时,会 split 出 left/right 残留段:
初始状态:
attr_range: [========== A (flags=RW, access=ENABLE) ==========]
0x1000 0x5000
用户 ioctl: 设置 [0x2000, 0x3000] flags=RO
拆分后:
attr_range: [== A (RW) ==][== B (RO) ==][====== A (RW) ======]
0x1000 0x1FFF 0x2000 0x3000 0x3001 0x5000
关键代码(amdgpu_svm_attr_set_existing):
c
/* split head */
if (start > range_start) {
left = attr_alloc_range(range_start, start - 1, &old_attrs);
}
/* split tail */
if (last < range_last) {
right = attr_alloc_range(last + 1, range_last, &old_attrs);
}
4.2 svm_range 的拆分:硬件约束驱动
drm_gpusvm_range_chunk_size() 根据以下约束确定每个 svm_range 的大小:
c
for (; i < gpusvm->num_chunks; ++i) {
start = ALIGN_DOWN(fault_addr, gpusvm->chunk_sizes[i]);
end = ALIGN(fault_addr + 1, gpusvm->chunk_sizes[i]);
if (start >= vas->vm_start && end <= vas->vm_end && // VMA 边界
start >= drm_gpusvm_notifier_start(notifier) && // notifier 边界
end <= drm_gpusvm_notifier_end(notifier) &&
start >= gpuva_start && end <= gpuva_end) // GPUVA 边界
break;
}
约束包括:
- chunk_sizes 对齐:预定义的 2 的幂数组(如 2M, 64K, 4K),从大到小尝试
- VMA 边界:不能跨越 CPU VMA
- notifier 边界:不能跨越 mmu_interval_notifier
- 已有 range 边界:不能与已存在的 svm_range 重叠
5. 1:N 对应关系详解
5.1 一个 attr_range 对应多个 svm_range(1:N)
这是正常的对应关系。用户通过 ioctl 设置大范围属性,产生一个大的 attr_range;而 GPU 映射时按 chunk_size 对齐创建多个小的 svm_range:
attr_range: [================ 0x1000 - 0x10000 (access=enable) ================]
svm_ranges: [--64K--][--64K--][--64K--][--64K--][--64K--]... (chunk-aligned)
由 drm_gpusvm_range_find_or_insert() 逐个创建
调用路径:
amdgpu_svm_attr_apply_change()
→ amdgpu_svm_range_map_interval()
→ amdgpu_svm_range_map()
→ while (addr < end):
drm_gpusvm_range_find_or_insert() ← 每次创建/复用一个 chunk 对齐的 svm_range
drm_gpusvm_range_get_pages()
amdgpu_svm_range_update_gpu_range()
5.2 为什么一个 svm_range 不能跨多个 attr_range
amdgpu_svm_range_update_gpu_range() 对整个 svm_range 施加统一的 PTE flags:
c
drm_gpusvm_range_for_each_page(page, range, iter, num_dma_mapped_pages) {
amdgpu_vm_update_range(svm->adev, svm->vm, ...,
start_page, last_page, seg_pte_flags, ...);
}
如果一个 svm_range 跨了两个不同属性的 attr_range(比如前半 RW、后半 RO),remap 时整个 svm_range 会被刷成同一套 flags,另一半的映射就错了。因此设计上不允许 M:1。
代码通过以下机制保证 svm_range 不会跨 attr 边界:
(1)创建时受 gpuva_end 约束
amdgpu_svm_range_map() 传入 end = (attr_last + 1) << PAGE_SHIFT 作为 gpuva_end,drm_gpusvm_range_chunk_size() 检查对齐后的 range 不超出此边界:
c
// drm_gpusvm_range_chunk_size() 中
end = ALIGN(fault_addr + 1, gpusvm->chunk_sizes[i]);
if (... && end <= gpuva_end) // ← svm_range 不会超出 attr 段
break;
(2)attr 拆分后通过 rebuild/remap 维护一致性
如果用户后来对已有 attr_range 的子区间设置新属性,导致 attr 被 split,此时可能有旧的 svm_range 跨越了新的 attr 边界。amdgpu_svm_range_apply_attr_change() 会处理这种情况:
- LOCATION_CHANGE :
rebuild_locked()销毁所有重叠的 svm_range,然后按新的 attr 边界重建 - PTE_FLAG_CHANGE / MAPPING_FLAG_CHANGE :
map_interval()对变更区间 remap,找到的旧 svm_range 整体用新 attrs 重映射(旧 attrs 被覆盖,保持整个 svm_range 属性统一)
5.3 attr_range 空洞(默认属性区域)
在 attr_tree 中不存在 attr_range 的区域使用默认属性 。amdgpu_svm_attr_lookup_page_locked() 在查询空洞时返回默认属性:
c
// 查询命中空洞时
attr_set_default(attr_tree->svm, attrs);
*range_last = ULONG_MAX;
// 查找下一个 attr_range 的起始位置来确定空洞的结束
node = interval_tree_iter_first(&attr_tree->tree, page + 1, ULONG_MAX);
if (node) {
range = container_of(node, struct amdgpu_svm_attr_range, it_node);
if (range->it_node.start > page)
*range_last = range->it_node.start - 1;
}
6. 桥接机制:属性变更如何传递到映射层
6.1 完整调用链
amdgpu_svm_attr_set() ← 用户 ioctl 入口
│
├─ 验证 + VMA 检查
│
└→ amdgpu_svm_attr_set_range() ← 遍历 [start, last],按 attr 段处理
│
│ while (cursor <= last):
│ ┌─ cursor 命中已有 attr_range?
│ │ YES → amdgpu_svm_attr_set_existing() ← 可能 split left/right
│ │ NO → amdgpu_svm_attr_set_hole() ← 空洞区域创建新 range
│ │
│ └→ 产生 attr_set_ctx { start, last, trigger, prev_attrs, new_attrs }
│
└→ amdgpu_svm_attr_apply_change() ← 桥接:属性层 → 映射层
│
│ 根据 trigger 类型决定操作:
│
├─ ACCESS_CHANGE + new_access:
│ → amdgpu_svm_range_map_interval() ← remap GPU 页表
│
├─ PTE_FLAG_CHANGE / MAPPING_FLAG_CHANGE:
│ → amdgpu_svm_range_map_interval() ← remap GPU 页表
│
└─ LOCATION_CHANGE:
→ amdgpu_svm_range_rebuild_locked() ← destroy + recreate svm_ranges
│
├─ amdgpu_svm_range_remove_overlaps() ← 删除旧 svm_ranges
└─ amdgpu_svm_range_map_attr_ranges() ← 按 attr 边界重建
6.2 trigger 类型与映射层操作的对应
| trigger 类型 | 映射层操作 | 原因 |
|---|---|---|
ATTR_TRIGGER_ACCESS_CHANGE (enable) |
map_interval → remap |
需要建立新的 GPU 映射 |
ATTR_TRIGGER_ACCESS_CHANGE (disable) |
无操作 (align with KFD) | TODO: 应 unmap |
ATTR_TRIGGER_PTE_FLAG_CHANGE |
map_interval → remap |
PTE flags 变了需要更新页表 |
ATTR_TRIGGER_MAPPING_FLAG_CHANGE |
map_interval → remap |
映射策略变了需要更新 |
ATTR_TRIGGER_LOCATION_CHANGE |
rebuild_locked → destroy + recreate |
migrate_devmem flag 是不可变的,必须重建 range |
ATTR_TRIGGER_GRANULARITY_CHANGE |
无操作 | 粒度变化只影响后续 range 创建 |
ATTR_TRIGGER_ATTR_ONLY |
无操作 | 无实质变化 |
6.3 LOCATION_CHANGE 需要 rebuild 的原因
drm_gpusvm_range 的 migrate_devmem flag 在创建时就确定了(来自 gpusvm_ctx.devmem_possible),之后不可变。如果用户改变了 preferred_loc/prefetch_loc,旧的 svm_range 仍然带着过时的 flag,migration 不会发生。因此必须:
- 删除旧的 svm_ranges(
amdgpu_svm_range_remove_overlaps) - 重新创建(
amdgpu_svm_range_map_attr_ranges),新 range 会从当前 attr 读取最新的devmem_possible
7. svm_range 不直接存储属性
amdgpu_svm_range 只缓存属性的结果,不存储属性本身:
c
struct amdgpu_svm_range {
struct drm_gpusvm_range base;
bool gpu_mapped; // 是否已映射到 GPU
uint64_t pte_flags; // 当前 GPU PTE flags(属性的结果)
uint32_t attr_flags; // 当前属性 flags 的快照
// ... 没有 preferred_loc, access 等属性字段
};
当 svm_range 需要 restore(如 eviction 后恢复映射)时,会回查 attr_tree 获取最新属性:
c
// amdgpu_svm_range_map_attr_ranges() --- restore 路径
amdgpu_svm_attr_lookup_page_locked(attr_tree, cursor, &attrs, &seg_last);
// 用查到的 attrs 重新映射
amdgpu_svm_range_map_interval(svm, cursor, seg_last, &attrs);
这确保了即使属性在 eviction 期间被修改,restore 时也能使用最新的属性。
8. 锁的协调
两层使用不同的锁,且有特定的获取顺序:
amdgpu_svm_attr_set_range() 中的锁序:
mutex_lock(&attr_tree->lock); ← 属性层锁
// 修改 attr_tree
mutex_unlock(&attr_tree->lock);
down_write(&svm->svm_lock); ← 映射层锁
amdgpu_svm_attr_apply_change(); // 操作 svm_ranges
up_write(&svm->svm_lock);
不能同时持有两把锁 ,因为 amdgpu_svm_range_map_interval() 内部需要获取 mmap_read_lock,如果持有 attr_tree->lock 可能导致死锁。因此设计上先释放 attr 锁,再获取 svm 锁。
9. 图示总结
用户视角 (ioctl): [============= 设置 access=ENABLE, flags=RO ===============]
start_page last_page
属性层 (attr_tree): [hole][= attr_A =][ hole ][=== attr_B ===][hole][attr_C]
↑ default attrs ↑ default ↑ default
各段按属性边界划分,与 ioctl 历史相关
映射层 (gpusvm): [r0][r1] [r2][r3][r4] [r5][r6][r7][r8] [r9][r10]
↑ 按 chunk_size 对齐,受 VMA/notifier 边界约束
各 svm_range 不跨越 attr 边界,统一 PTE flags
对应关系: 1 : N (一个 attr_range 对应多个 svm_range,反向不允许)
| 概念 | 说明 |
|---|---|
attr_range = "这段 VA 有什么属性" |
面向用户,ioctl 驱动,纯元数据 |
svm_range = "这段 VA 有什么 GPU 映射" |
面向硬件,fault/map 驱动,持有页表状态 |
| 对应关系 = 1:N | 一个 attr_range 包含多个 chunk 对齐的 svm_range |
| 约束 = svm_range 不跨 attr 边界 | update_gpu_range 对整个 svm_range 施加统一 PTE flags |
| 桥接 = trigger → range remap/rebuild | attr 层通知 range 层"属性变了,请更新映射" |
这种解耦设计的好处是:属性管理(用户策略)和 GPU 映射管理(硬件约束)各自独立演进,互不干扰。
10. 复杂性代价分析
N:M 解耦设计引入了明显的复杂性,需要客观评估其代价与收益。
10.1 引入的具体复杂性
(1) 锁序窗口期与 retry 机制
因为两层各有独立的锁,amdgpu_svm_attr_set_range() 不得不采用"释放 attr 锁 → 获取 svm 锁"的模式:
c
mutex_lock(&attr_tree->lock);
// 修改 attr_tree(属性已变)
mutex_unlock(&attr_tree->lock);
← 窗口期:属性已改,映射未更新
down_write(&svm->svm_lock);
amdgpu_svm_attr_apply_change(); // 更新映射
up_write(&svm->svm_lock);
在这个窗口期 内,如果另一个线程读取 attr 并触发 restore,可能看到属性与映射不一致的状态。代码中的 -EAGAIN retry 机制正是为了应对这种竞争:
c
// amdgpu_svm_attr_set() 中
retry:
r = amdgpu_svm_attr_set_range(...);
if (r == -EAGAIN) {
amdgpu_svm_range_flush(svm);
cond_resched();
goto retry;
}
如果是单层设计,只需一把锁保护原子操作,不需要 retry。
(2) restore 路径必须逐段查属性
因为 svm_range 不存储完整属性,每次 restore 都要调用 amdgpu_svm_attr_lookup_page_locked() 逐段查询 attr_tree:
c
// restore 路径 --- 每个 svm_range 都要回查 attr_tree
while (cursor <= last_page) {
mutex_lock(&attr_tree->lock);
amdgpu_svm_attr_lookup_page_locked(attr_tree, cursor, &attrs, &seg_last);
mutex_unlock(&attr_tree->lock);
amdgpu_svm_range_map_interval(svm, cursor, seg_last, &attrs);
cursor = seg_last + 1;
}
如果是 1:1 对应,svm_range 直接内嵌属性即可,restore 零查询开销。
(3) LOCATION_CHANGE 的 destroy + recreate 开销
因为 drm_gpusvm_range 的 migrate_devmem flag 在创建时不可变,location 属性变更时必须销毁再重建所有重叠的 svm_range。这意味着丢弃已有的 GPU 映射和 DMA 状态,重新走一遍完整的 map 流程。
如果属性直接存在 svm_range 上且 flag 可变,只需修改 flag + 重新 migrate,无需 destroy(但这受限于 drm_gpusvm 框架设计)。
(4) 两棵独立 interval tree 的维护负担
- 内存开销:两棵树各自维护节点
- 代码复杂度:边界不对齐时的交叉查询逻辑容易出错
- 调试难度:排查问题需要同时 dump 两棵树的状态才能理清全貌
10.2 为什么仍然采用这种设计
核心原因是两层受不同的外部约束,无法统一:
| 约束来源 | attr_range | svm_range |
|---|---|---|
| 边界由谁决定 | 用户 ioctl(任意地址范围) | drm_gpusvm 框架(chunk 对齐 + VMA + notifier) |
| 生命周期 | 持久存在直到用户清除或进程退出 | 可能被 mmu_notifier 随时回收再重建 |
| 可控性 | amdgpu 驱动完全控制 | drm_gpusvm 是 DRM 公共框架(xe、amdgpu 共用),不能改其 range 结构 |
关键限制:drm_gpusvm_range 是 DRM 子系统的公共框架,它的 range 拆分逻辑和生命周期不能为 amdgpu 的属性需求定制。所以 amdgpu 只能在外面再加一层属性管理。
10.3 如果要简化的方向
理论上如果 drm_gpusvm_range 能支持以下能力,就可以去掉 attr_tree,只维护一棵树:
| 需要的框架改动 | 当前限制 | 简化效果 |
|---|---|---|
| range 内嵌驱动私有属性 | drm_gpusvm_range 无属性字段 |
无需外部 attr_tree |
migrate_devmem 可变 / 延迟决定 |
创建时确定,不可变 | 无需 destroy + recreate |
| range 拆分时感知属性边界 | chunk_size 纯硬件对齐 | 减少 N:M 不对齐情况 |
但这需要修改 DRM 公共框架,影响面涉及 xe 等其他驱动,短期内不现实。
10.4 小结
复杂性代价 设计收益
───────────── ─────────
锁序窗口 + retry 机制 属性层与映射层独立演进
restore 逐段查属性开销 svm_range 被回收后属性仍然保留
LOCATION_CHANGE destroy+recreate 不侵入 drm_gpusvm 公共框架
两棵 interval tree 维护成本 适配 KFD 属性语义,兼容用户空间 API
总结:复杂性是真实的代价,根本原因是 amdgpu 的属性管理需求与 drm_gpusvm 公共框架的 range 管理模型不匹配,被迫用两层解耦来适配。如果未来 drm_gpusvm 框架能扩展属性支持能力,这层复杂性可以消除。