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);
}
关键点:
- 属性数量上限为 64 (
AMDGPU_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 锁竞争而失败的概率。- 从
vm到svm->attr_tree的转换完成了从 DRM/VM 抽象到 SVM 子系统内部结构的过渡。
4.4 阶段四:属性校验
函数 :amdgpu_svm_attr_set() → amdgpu_svm_attr_set_validate()
逐条遍历用户传入的属性数组,按类型执行合法性检查:
| 属性类型 | 校验规则 |
|---|---|
PREFERRED_LOC |
接受 SYSMEM 和 UNDEFINED,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 机制
两种情况下,即使属性值未变也必须产生触发:
- xnack 关闭时的 ACCESS 设置 :attr 层不知道 range 层的
gpu_mapped状态,range 层必须重新检查并按需建立映射。 - 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. 关键设计要点
-
稀疏存储 :attr 层只为与默认值不同的地址段创建
attr_range节点,空洞隐式继承默认属性。 -
段式处理 :用户请求范围被区间树自然地分割为多个段,每段独立收集
attr_set_ctx、独立消费。这意味着一次 ioctl 可能产生多个到 range 层的调用。 -
锁分离 :
attr_tree->lock(mutex, attr 层)和svm->svm_lock(rwsem, range 层)不嵌套,每段处理先释放 attr 锁再获取 range 锁,减少锁持有时间。 -
Force Trigger :attr 层引入
force_trigger机制弥补自身信息不足------它不知道 range 层的gpu_mapped状态,也无法判断页面是否已被驱逐,因此在特定场景下即使属性值未变也生成触发。 -
重试机制 :range 层可能因无法获取 mmap 锁而返回
-EAGAIN,上层通过flush + cond_resched + goto retry实现退让重试。