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 层(用户态/内核态接口)
/* include/uapi/drm/amdgpu_drm.h */structdrm_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[] 的指针 */};structdrm_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 |
标志位被分为两组掩码,对应不同的触发类型:
/* amdgpu_svm_attr.h */#defineAMDGPU_SVM_PTE_FLAG_MASK\(AMDGPU_SVM_FLAG_COHERENT|AMDGPU_SVM_FLAG_EXT_COHERENT|\AMDGPU_SVM_FLAG_GPU_RO|AMDGPU_SVM_FLAG_GPU_EXEC)#defineAMDGPU_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 内核态属性结构
/* amdgpu_svm_attr.h */enumamdgpu_svm_attr_access{AMDGPU_SVM_ACCESS_NONE=0,/* GPU 无访问权限 */AMDGPU_SVM_ACCESS_ENABLE=1,/* GPU 可访问(允许迁移) */AMDGPU_SVM_ACCESS_IN_PLACE=2,/* GPU 原地访问(不迁移) */};structamdgpu_svm_attrs{int32_tpreferred_loc;/* 首选内存位置 */int32_tprefetch_loc;/* 预取目标位置 */uint32_tflags;/* 标志位集合 */uint32_tgranularity;/* 页粒度 */enumamdgpu_svm_attr_accessaccess;/* 访问模式 */};2.3 属性区间树
structamdgpu_svm_attr_range{structinterval_tree_nodeit_node;/* 区间树节点 [start_page, last_page] */structlist_headlist;/* 链表节点(按地址有序) */structamdgpu_svm_attrsattrs;/* 该区间的属性值 */};structamdgpu_svm_attr_tree{structmutexlock;/* 保护区间树的互斥锁 */structrb_root_cachedtree;/* 红黑树根(区间树底层实现) */structlist_headrange_list;/* 所有 attr_range 的有序链表 */structamdgpu_svm*svm;/* 回指 SVM 上下文 */};2.4 变更上下文(attr 层与 range 层的桥梁)
/* amdgpu_svm_attr.c - 文件作用域,仅在 attr 层内部使用 */structattr_set_ctx{unsignedlongstart;/* 受影响的起始页 */unsignedlonglast;/* 受影响的结束页 */uint32_ttrigger;/* 变更触发位掩码 */structamdgpu_svm_attrsprev_attrs;/* 变更前属性 */structamdgpu_svm_attrsnew_attrs;/* 变更后属性 */};触发位定义:
enumamdgpu_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 是否非零 → -EINVAL4.2 阶段二:用户态属性数组复制
函数:amdgpu_svm_copy_attrs()
staticintamdgpu_svm_copy_attrs(conststructdrm_amdgpu_gem_svm*args,structdrm_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);returnPTR_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()
staticintamdgpu_svm_set_attr(structamdgpu_vm*vm,...){structamdgpu_svm*svm=vm->svm;amdgpu_svm_range_sync_work(svm);/* 刷新 range 工作队列 */returnamdgpu_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()
staticvoidattr_set_default(structamdgpu_svm*svm,structamdgpu_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开始,每次处理一个段:
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 属性叠加
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 无变化快速路径
if(attr_same_attrs(range,nattr,attrs)){if(!force_trigger)return0;// 完全无变化,跳过// force_trigger 场景: 见 4.9.3}4.9.3 Force Trigger 机制
两种情况下,即使属性值未变也必须产生触发:
- xnack 关闭时的 ACCESS 设置:attr 层不知道 range 层的
gpu_mapped状态,range 层必须重新检查并按需建立映射。 - PREFETCH_LOC 重复设置:预取是一次性命令(one-shot),不是持久状态。即使
prefetch_loc值相同,页可能已被驱逐回 RAM,必须重新触发迁移。
force_trigger=(!attr_tree->svm->xnack_enabled&&attr_has_access(nattr,attrs))||attr_has_prefetch_loc(nattr,attrs);4.9.4 触发类型计算
staticuint32_tattr_change_ctx_trigger(conststructamdgpu_svm_attrs*prev,conststructamdgpu_svm_attrs*new){uint32_ttrigger=0;uint32_tchanged_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_EXECif(changed_flags&AMDGPU_SVM_MAPPING_FLAG_MASK)trigger|=TRIGGER_MAPPING_FLAG_CHANGE;// HOST_ACCESS/HIVE_LOCAL/READ_MOSTLY/ALWAYS_MAPPEDif(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;// 属性值没变或变化不影响硬件returntrigger;}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 ]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
无论走哪条路径,最终都调用:
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,随即消费:
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()是消费端的入口:
staticintamdgpu_svm_attr_apply_change(structamdgpu_svm*svm,conststructattr_set_ctx*change){if(!change->trigger||change->trigger==AMDGPU_SVM_ATTR_TRIGGER_ATTR_ONLY)return0;/* 无硬件影响,直接跳过 */returnamdgpu_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实现退让重试。