AMDGPU 基于DRM SVM框架的新SVM功能实现 :attr_range 与 svm_range 的对应关系分析

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_enddrm_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_CHANGErebuild_locked() 销毁所有重叠的 svm_range,然后按新的 attr 边界重建
  • PTE_FLAG_CHANGE / MAPPING_FLAG_CHANGEmap_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_rangemigrate_devmem flag 在创建时就确定了(来自 gpusvm_ctx.devmem_possible),之后不可变。如果用户改变了 preferred_loc/prefetch_loc,旧的 svm_range 仍然带着过时的 flag,migration 不会发生。因此必须:

  1. 删除旧的 svm_ranges(amdgpu_svm_range_remove_overlaps
  2. 重新创建(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_rangemigrate_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 框架能扩展属性支持能力,这层复杂性可以消除。

相关推荐
DeeplyMind1 天前
AMDGPU 基于DRM SVM框架的新SVM功能实现 :属性子系统结构体关系解析
amdgpu svm·drm_gpusvm
DeeplyMind6 天前
drm_gpusvm_pages — svm range物理页面映射状态管理者的实现详细分析
drm_gpusvm·gpusvm_pages