AMDGPU SVM 属性设置流程:从用户态 ioctl 到 attr_set_ctx 的完整信息收集

AMD 正在使用 drm svm框架重构SVM的实现,看来drm svm框架要进入大范围应用了。下面是在kernel社区上由AMD的开发人员提交的POC 验证版本的patches的技术方案实现。这里快速总结了实现,以飨读者。

因是POC版本,设计可能会变动,读者们慎重使用。本文仅用来跟踪前沿驱动技术的迭代发展现状。


1. 概述

本文档描述 AMDGPU SVM(Shared Virtual Memory)子系统中,用户态通过 DRM ioctl 设置 SVM 属性的完整内核态处理流程。重点分析从 ioctl 入口到属性变更上下文 struct attr_set_ctx 收集完毕的全过程。

核心设计思想 :attr 层作为「属性管理器」,负责维护虚拟地址空间上的属性区间树(interval tree),并在属性变更时收集差异信息,封装为 attr_set_ctx 传递给 range 层执行实际的 GPU 映射/迁移操作。

2. 数据结构定义

2.1 UAPI 层(用户态/内核态接口)

c 复制代码
/* include/uapi/drm/amdgpu_drm.h */

struct drm_amdgpu_gem_svm {
    __u64 start_addr;   /* 目标虚拟地址(页对齐) */
    __u64 size;          /* 区间大小(字节,页对齐) */
    __u32 operation;     /* 操作码:AMDGPU_SVM_OP_SET_ATTR (0) / GET_ATTR (1) */
    __u32 nattr;         /* 属性数组长度 */
    __u64 attrs_ptr;     /* 指向用户态 drm_amdgpu_svm_attribute[] 的指针 */
};

struct drm_amdgpu_svm_attribute {
    __u32 type;          /* 属性类型 */
    __u32 value;         /* 属性值 */
};

属性类型枚举

类型常量 含义
AMDGPU_SVM_ATTR_PREFERRED_LOC 0 首选内存位置
AMDGPU_SVM_ATTR_PREFETCH_LOC 1 预取目标位置
AMDGPU_SVM_ATTR_ACCESS 2 启用 GPU 访问
AMDGPU_SVM_ATTR_ACCESS_IN_PLACE 3 启用 GPU 原地访问
AMDGPU_SVM_ATTR_NO_ACCESS 4 禁用 GPU 访问
AMDGPU_SVM_ATTR_SET_FLAGS 5 按位设置标志
AMDGPU_SVM_ATTR_CLR_FLAGS 6 按位清除标志
AMDGPU_SVM_ATTR_GRANULARITY 7 页粒度

标志位定义

标志 分类
AMDGPU_SVM_FLAG_HOST_ACCESS 0x01 MAPPING
AMDGPU_SVM_FLAG_COHERENT 0x02 PTE
AMDGPU_SVM_FLAG_HIVE_LOCAL 0x04 MAPPING
AMDGPU_SVM_FLAG_GPU_RO 0x08 PTE
AMDGPU_SVM_FLAG_GPU_EXEC 0x10 PTE
AMDGPU_SVM_FLAG_GPU_READ_MOSTLY 0x20 MAPPING
AMDGPU_SVM_FLAG_GPU_ALWAYS_MAPPED 0x40 MAPPING
AMDGPU_SVM_FLAG_EXT_COHERENT 0x80 PTE

标志位被分为两组掩码,对应不同的触发类型:

c 复制代码
/* amdgpu_svm_attr.h */
#define AMDGPU_SVM_PTE_FLAG_MASK \
    (AMDGPU_SVM_FLAG_COHERENT | AMDGPU_SVM_FLAG_EXT_COHERENT | \
     AMDGPU_SVM_FLAG_GPU_RO | AMDGPU_SVM_FLAG_GPU_EXEC)

#define AMDGPU_SVM_MAPPING_FLAG_MASK \
    (AMDGPU_SVM_FLAG_HOST_ACCESS | AMDGPU_SVM_FLAG_HIVE_LOCAL | \
     AMDGPU_SVM_FLAG_GPU_READ_MOSTLY | AMDGPU_SVM_FLAG_GPU_ALWAYS_MAPPED)

2.2 内核态属性结构

c 复制代码
/* amdgpu_svm_attr.h */

enum amdgpu_svm_attr_access {
    AMDGPU_SVM_ACCESS_NONE     = 0,  /* GPU 无访问权限 */
    AMDGPU_SVM_ACCESS_ENABLE   = 1,  /* GPU 可访问(允许迁移) */
    AMDGPU_SVM_ACCESS_IN_PLACE = 2,  /* GPU 原地访问(不迁移) */
};

struct amdgpu_svm_attrs {
    int32_t  preferred_loc;  /* 首选内存位置 */
    int32_t  prefetch_loc;   /* 预取目标位置 */
    uint32_t flags;          /* 标志位集合 */
    uint32_t granularity;    /* 页粒度 */
    enum amdgpu_svm_attr_access access;  /* 访问模式 */
};

2.3 属性区间树

c 复制代码
struct amdgpu_svm_attr_range {
    struct interval_tree_node it_node;  /* 区间树节点 [start_page, last_page] */
    struct list_head list;               /* 链表节点(按地址有序) */
    struct amdgpu_svm_attrs attrs;       /* 该区间的属性值 */
};

struct amdgpu_svm_attr_tree {
    struct mutex lock;              /* 保护区间树的互斥锁 */
    struct rb_root_cached tree;     /* 红黑树根(区间树底层实现) */
    struct list_head range_list;    /* 所有 attr_range 的有序链表 */
    struct amdgpu_svm *svm;         /* 回指 SVM 上下文 */
};

2.4 变更上下文(attr 层与 range 层的桥梁)

c 复制代码
/* amdgpu_svm_attr.c - 文件作用域,仅在 attr 层内部使用 */

struct attr_set_ctx {
    unsigned long start;                    /* 受影响的起始页 */
    unsigned long last;                     /* 受影响的结束页 */
    uint32_t trigger;                       /* 变更触发位掩码 */
    struct amdgpu_svm_attrs prev_attrs;     /* 变更前属性 */
    struct amdgpu_svm_attrs new_attrs;      /* 变更后属性 */
};

触发位定义

c 复制代码
enum amdgpu_svm_attr_change_trigger {
    AMDGPU_SVM_ATTR_TRIGGER_ACCESS_CHANGE       = (1U << 0),  /* 访问权限变更 */
    AMDGPU_SVM_ATTR_TRIGGER_PTE_FLAG_CHANGE      = (1U << 1),  /* PTE 级标志变更 */
    AMDGPU_SVM_ATTR_TRIGGER_MAPPING_FLAG_CHANGE  = (1U << 2),  /* 映射策略标志变更 */
    AMDGPU_SVM_ATTR_TRIGGER_LOCATION_CHANGE      = (1U << 3),  /* 预取位置变更 */
    AMDGPU_SVM_ATTR_TRIGGER_GRANULARITY_CHANGE   = (1U << 4),  /* 页粒度变更 */
    AMDGPU_SVM_ATTR_TRIGGER_ATTR_ONLY            = (1U << 5),  /* 仅属性记录变更,无需 range 层操作 */
};

3. 完整调用链

复制代码
用户态: ioctl(fd, DRM_IOCTL_AMDGPU_GEM_SVM, &args)
    │
    ▼
┌─────────────────────────────────────────────────────┐
│  DRM 子系统分发                                      │
│  amdgpu_drv.c: amdgpu_ioctls_kms[] 注册表            │
│  DRM_IOCTL_DEF_DRV(AMDGPU_GEM_SVM,                  │
│                    amdgpu_gem_svm_ioctl,            │
│                    DRM_AUTH|DRM_RENDER_ALLOW)       │
└─────────────────────┬───────────────────────────────┘
                      │ DRM 框架自动 copy_from_user
                      │ 将 drm_amdgpu_gem_svm 复制到内核栈
                      ▼
         amdgpu_gem_svm_ioctl()          [amdgpu_svm.c]
                      │
                      ├── ① 参数校验
                      ├── ② amdgpu_svm_copy_attrs()   → 从用户态复制属性数组
                      └── ③ switch(operation)
                              │
                          SET_ATTR
                              │
                              ▼
                  amdgpu_svm_set_attr()              [amdgpu_svm.c]
                              │
                              ├── amdgpu_svm_range_sync_work()  → 刷新 range 工作队列
                              └── amdgpu_svm_attr_set()          → 进入 attr 层
                                          │
                                          ├── ④ 逐属性校验
                                          ├── ⑤ VMA 范围校验
                                          ├── ⑥ 初始化默认属性
                                          └── ⑦ amdgpu_svm_attr_set_range()  → 按段迭代
                                             ┌────────┴────────┐
                                     命中已有区间            落入空洞
                                             │                  │
                                             ▼                  ▼
                                   amdgpu_svm_attr_       amdgpu_svm_attr_
                                   set_existing()         set_hole()
                                             │                  │
                                             └────────┬─────────┘
                                                      ▼
                                             attr_set_ctx 收集完毕
                                                      ▼
                                        amdgpu_svm_attr_apply_change()
                                                      ▼
                                    amdgpu_svm_range_apply_attr_change()
                                               (range 层消费)

4. 各阶段详细分析

4.1 阶段一:DRM ioctl 入口与参数复制

函数amdgpu_gem_svm_ioctl()

DRM 框架在分发 ioctl 之前,已经根据 DRM_IOWR 宏的定义,将 drm_amdgpu_gem_svm 结构体从用户空间 copy_from_user 到内核栈上的 data 指针。ioctl handler 直接将 data 强转为 struct drm_amdgpu_gem_svm *

校验逻辑

复制代码
1. SVM 是否已启用(vm->svm != NULL)         → -EOPNOTSUPP
2. start_addr 和 size 是否页对齐              → -EINVAL
3. start_addr 和 size 是否非零               → -EINVAL

4.2 阶段二:用户态属性数组复制

函数amdgpu_svm_copy_attrs()

c 复制代码
static int amdgpu_svm_copy_attrs(const struct drm_amdgpu_gem_svm *args,
                                 struct drm_amdgpu_svm_attribute **attrs,
                                 size_t *size)
{
    if (!args->nattr || args->nattr > AMDGPU_SVM_MAX_ATTRS)  /* 上限 64 */
        return -EINVAL;
    if (!args->attrs_ptr)
        return -EINVAL;

    *size = args->nattr * sizeof(**attrs);
    *attrs = memdup_user(u64_to_user_ptr(args->attrs_ptr), *size);
    return PTR_ERR_OR_ZERO(*attrs);
}

关键点:

  • 属性数量上限为 64AMDGPU_SVM_MAX_ATTRS),防止用户态传入过大数组导致内存耗尽。
  • memdup_user() 原子地完成内存分配 + copy_from_user,返回内核堆上的副本。
  • u64_to_user_ptr()__u64 安全转换为用户态指针(处理 32/64 位兼容性)。

至此,内核拥有用户请求的完整副本:目标地址范围 + 属性数组

4.3 阶段三:中间分发层

函数amdgpu_svm_set_attr()

c 复制代码
static int amdgpu_svm_set_attr(struct amdgpu_vm *vm, ...)
{
    struct amdgpu_svm *svm = vm->svm;
    amdgpu_svm_range_sync_work(svm);    /* 刷新 range 工作队列 */
    return amdgpu_svm_attr_set(svm->attr_tree, start, size, nattr, attrs);
}
  • amdgpu_svm_range_sync_work() 在进入 attr 层之前刷新 range 层的异步工作队列,减少后续操作因 mmap 锁竞争而失败的概率。
  • vmsvm->attr_tree 的转换完成了从 DRM/VM 抽象到 SVM 子系统内部结构的过渡。

4.4 阶段四:属性校验

函数amdgpu_svm_attr_set()amdgpu_svm_attr_set_validate()

逐条遍历用户传入的属性数组,按类型执行合法性检查:

属性类型 校验规则
PREFERRED_LOC 接受 SYSMEMUNDEFINED,GPU ID > 0 隐式接受(单 GPU 架构)
PREFETCH_LOC 接受 SYSMEM 和 GPU ID > 0,拒绝 UNDEFINED
ACCESS / ACCESS_IN_PLACE / NO_ACCESS value 不能为 0 或 UNDEFINED
SET_FLAGS / CLR_FLAGS value 不能包含 AMDGPU_SVM_VALID_FLAG_MASK 之外的位
GRANULARITY 无限制(由 attr_apply 时截断到 0x3f)

任何一条属性校验失败,整个 ioctl 立即返回 -EINVAL

4.5 阶段五:VMA 范围校验

函数amdgpu_svm_attr_validate_range_vma()

在持有 mmap_read_lock 的情况下,遍历目标页范围 [start_page, last_page] 对应的所有 VMA,确保:

  • 每一页都被有效 VMA 覆盖(不存在空洞)。
  • VMA 不带有 VM_IO | VM_PFNMAP | VM_MIXEDMAP 标志(排除设备映射区域)。

不满足条件则返回 -EFAULT。这保证了后续操作不会作用在不合法的虚拟地址范围上。

4.6 阶段六:默认属性初始化

函数attr_set_default()

c 复制代码
static void attr_set_default(struct amdgpu_svm *svm,
                             struct amdgpu_svm_attrs *attrs)
{
    attrs->preferred_loc = AMDGPU_SVM_LOCATION_UNDEFINED;
    attrs->prefetch_loc  = AMDGPU_SVM_LOCATION_UNDEFINED;
    attrs->granularity   = svm->default_granularity;
    attrs->flags         = AMDGPU_SVM_FLAG_HOST_ACCESS | AMDGPU_SVM_FLAG_COHERENT;
    attrs->access        = svm->xnack_enabled ?
                           AMDGPU_SVM_ACCESS_ENABLE : AMDGPU_SVM_ACCESS_NONE;
}

默认属性用于 interval tree 中不存在属性区间(空洞)的地址段。这意味着 attr 层采用稀疏存储 策略:只有与默认值不同的区间才会分配 amdgpu_svm_attr_range 节点。

4.7 阶段七:按段迭代 ------ amdgpu_svm_attr_set_range()

这是信息收集的主循环。用户请求的地址范围 [start_page, last_page] 可能跨越多个已有属性区间和空洞,因此需要逐段处理。

复制代码
用户请求范围:  [========================================]
区间树现状:         [range_A]     [range_B]    [range_C]
                ↑                ↑           ↑         ↑
              空洞            空洞         空洞       空洞

主循环以 cursor 指针从 start_page 开始,每次处理一个段:

c 复制代码
while (cursor <= last) {
    mutex_lock(&attr_tree->lock);

    node = interval_tree_iter_first(&attr_tree->tree, cursor, cursor);

    if (node) {
        /* 命中已有区间 → amdgpu_svm_attr_set_existing() */
        seg_last = min(last, attr_last_page(range));
    } else {
        /* 落入空洞 → amdgpu_svm_attr_set_hole() */
        next = interval_tree_iter_first(..., cursor + 1, ULONG_MAX);
        seg_last = min(last, attr_start_page(next_range) - 1);
    }

    /* 填充 attr_set_ctx ... */

    mutex_unlock(&attr_tree->lock);

    /* 持 svm_lock 消费 change ... */

    cursor = seg_last + 1;
}

锁序 :每段处理中,先持 attr_tree->lock(mutex)操作区间树,释放后再持 svm->svm_lock(rwsem write)传递给 range 层。两把锁不嵌套。

4.8 路径 A:空洞处理 ------ amdgpu_svm_attr_set_hole()

cursor 所在位置没有任何属性区间时,该段地址继承默认属性。

复制代码
输入: default_attrs + 用户属性数组
处理:
  1. new_attrs = default_attrs
  2. amdgpu_svm_attr_apply(&new_attrs, nattr, attrs)    // 叠加用户请求
  3. if (new_attrs == default_attrs) → return 0          // 无变化,跳过
  4. 分配新的 amdgpu_svm_attr_range,插入区间树
  5. trigger = attr_change_ctx_trigger(default_attrs, &new_attrs)
  6. 填充 attr_set_ctx{start, last, trigger, default_attrs, new_attrs}

优化:如果叠加后属性仍等于默认值,则不分配节点、不产生 change,保持稀疏存储。

4.9 路径 B:已有区间处理 ------ amdgpu_svm_attr_set_existing()

cursor 命中已有的 amdgpu_svm_attr_range 时,流程更为复杂。

4.9.1 属性叠加
c 复制代码
old_attrs = range->attrs;
new_attrs = old_attrs;
amdgpu_svm_attr_apply(&new_attrs, nattr, attrs);

amdgpu_svm_attr_apply() 遍历用户属性数组,按类型修改 new_attrs

操作 语义
PREFERRED_LOC new.preferred_loc = value
PREFETCH_LOC new.prefetch_loc = value
ACCESS new.access = ENABLE
ACCESS_IN_PLACE new.access = IN_PLACE
NO_ACCESS new.access = NONE
SET_FLAGS `new.flags
CLR_FLAGS new.flags &= ~value
GRANULARITY new.granularity = min(value, 0x3f)

注意:用户可以在一次 ioctl 中传入多条属性,它们按数组顺序依次叠加。后出现的同类型属性覆盖先前的。

4.9.2 无变化快速路径
c 复制代码
if (attr_same_attrs(range, nattr, attrs)) {
    if (!force_trigger) return 0;        // 完全无变化,跳过
    // force_trigger 场景: 见 4.9.3
}
4.9.3 Force Trigger 机制

两种情况下,即使属性值未变也必须产生触发:

  1. xnack 关闭时的 ACCESS 设置 :attr 层不知道 range 层的 gpu_mapped 状态,range 层必须重新检查并按需建立映射。
  2. PREFETCH_LOC 重复设置 :预取是一次性命令(one-shot),不是持久状态。即使 prefetch_loc 值相同,页可能已被驱逐回 RAM,必须重新触发迁移。
c 复制代码
force_trigger = (!attr_tree->svm->xnack_enabled && attr_has_access(nattr, attrs)) ||
                 attr_has_prefetch_loc(nattr, attrs);
4.9.4 触发类型计算
c 复制代码
static uint32_t attr_change_ctx_trigger(const struct amdgpu_svm_attrs *prev,
                                        const struct amdgpu_svm_attrs *new)
{
    uint32_t trigger = 0;
    uint32_t changed_flags = prev->flags ^ new->flags;

    if (prev->access != new->access)
        trigger |= TRIGGER_ACCESS_CHANGE;

    if (changed_flags & AMDGPU_SVM_PTE_FLAG_MASK)
        trigger |= TRIGGER_PTE_FLAG_CHANGE;        // COHERENT/EXT_COHERENT/GPU_RO/GPU_EXEC

    if (changed_flags & AMDGPU_SVM_MAPPING_FLAG_MASK)
        trigger |= TRIGGER_MAPPING_FLAG_CHANGE;     // HOST_ACCESS/HIVE_LOCAL/READ_MOSTLY/ALWAYS_MAPPED

    if (prev->prefetch_loc != new->prefetch_loc)
        trigger |= TRIGGER_LOCATION_CHANGE;

    if (prev->granularity != new->granularity)
        trigger |= TRIGGER_GRANULARITY_CHANGE;

    if (!trigger)
        trigger = TRIGGER_ATTR_ONLY;                // 属性值没变或变化不影响硬件

    return trigger;
}

trigger 位掩码的设计使得 range 层可以精确知道需要执行哪些操作:

  • ACCESS_CHANGE → 重新评估 GPU 映射/解映射
  • PTE_FLAG_CHANGE → 需要更新 GPU 页表项的标志位
  • MAPPING_FLAG_CHANGE → 需要重新评估映射策略
  • LOCATION_CHANGE → 触发数据迁移
  • GRANULARITY_CHANGE → 影响后续 range 分割粒度
  • ATTR_ONLY → 仅属性变更,range 层无需操作
4.9.5 区间树分裂

当用户请求范围不完整覆盖已有区间时,需要将其分裂:

复制代码
已有区间:  [----------- range -----------]
用户范围:        [=======]
分裂后:    [left] [updated] [   right    ]
c 复制代码
if (start > range_start)
    left = attr_alloc_range(range_start, start - 1, &old_attrs);   // 保留旧属性
if (last < range_last)
    right = attr_alloc_range(last + 1, range_last, &old_attrs);    // 保留旧属性

attr_remove_range_locked(attr_tree, range, false);  // 从树中移除原区间(不释放)
if (left)  attr_insert_range_locked(attr_tree, left);
attr_set_interval(range, start, last);               // 复用原节点,调整区间
range->attrs = new_attrs;                             // 更新为新属性
attr_insert_range_locked(attr_tree, range);
if (right) attr_insert_range_locked(attr_tree, right);

注意:中间段复用原 range 对象(避免额外分配),两端新分配节点继承旧属性。

4.9.6 打包 attr_set_ctx

无论走哪条路径,最终都调用:

c 复制代码
amdgpu_svm_attr_change_ctx_set(change, start, last, trigger,
                               &prev_attrs, &new_attrs);

将本段的变更信息完整封装到栈上的 attr_set_ctx 变量中。

5. attr_set_ctx 消费

主循环中,每段 attr_set_ctx 收集完毕后,释放 attr_tree->lock,随即消费:

c 复制代码
mutex_unlock(&attr_tree->lock);

down_write(&svm->svm_lock);
ret = amdgpu_svm_attr_apply_change(svm, &change);
up_write(&svm->svm_lock);

amdgpu_svm_attr_apply_change() 是消费端的入口:

c 复制代码
static int amdgpu_svm_attr_apply_change(struct amdgpu_svm *svm,
                                        const struct attr_set_ctx *change)
{
    if (!change->trigger || change->trigger == AMDGPU_SVM_ATTR_TRIGGER_ATTR_ONLY)
        return 0;    /* 无硬件影响,直接跳过 */

    return amdgpu_svm_range_apply_attr_change(svm,
        change->start, change->last,
        change->trigger,
        &change->prev_attrs, &change->new_attrs);
}

过滤条件:trigger == 0(不可能,但防御性编程)和 ATTR_ONLY(属性记录变了但不影响硬件)直接跳过,不进入 range 层。

若 range 层返回 -EAGAIN,主循环将标记 need_retry,在整个范围处理完后由上层 amdgpu_svm_attr_set() 的 retry 循环重试。

6. 关键设计要点

  1. 稀疏存储 :attr 层只为与默认值不同的地址段创建 attr_range 节点,空洞隐式继承默认属性。

  2. 段式处理 :用户请求范围被区间树自然地分割为多个段,每段独立收集 attr_set_ctx、独立消费。这意味着一次 ioctl 可能产生多个到 range 层的调用。

  3. 锁分离attr_tree->lock(mutex, attr 层)和 svm->svm_lock(rwsem, range 层)不嵌套,每段处理先释放 attr 锁再获取 range 锁,减少锁持有时间。

  4. Force Trigger :attr 层引入 force_trigger 机制弥补自身信息不足------它不知道 range 层的 gpu_mapped 状态,也无法判断页面是否已被驱逐,因此在特定场景下即使属性值未变也生成触发。

  5. 重试机制 :range 层可能因无法获取 mmap 锁而返回 -EAGAIN,上层通过 flush + cond_resched + goto retry 实现退让重试。

相关推荐
DeeplyMind13 天前
linux中的HMM vs drm_pagemap 对比分析
hmm·drm_gpusvm·drm_pagemap·dev_pagemap·hmm_range
DeeplyMind17 天前
AMDGPU 基于DRM SVM框架的新SVM功能实现 :attr_range 与 svm_range 的对应关系分析
drm_gpusvm·drm_pagemap
DeeplyMind18 天前
AMDGPU 基于DRM SVM框架的新SVM功能实现 :属性子系统结构体关系解析
amdgpu svm·drm_gpusvm
DeeplyMind23 天前
drm_gpusvm_pages — svm range物理页面映射状态管理者的实现详细分析
drm_gpusvm·gpusvm_pages
DeeplyMind2 个月前
附录A:AMDGPU SVM 属性类型
kfd·amdgpu svm
DeeplyMind2 个月前
02 - SVM相关的Linux内核基础
hmm·rocm·kfd·共享虚拟内存·amdgpu svm