8.2 把用户递交的 chunk 翻译成了作业骨架,8.3 又为其引用的所有 BO 完成了加锁与驻留。此刻 job 已经「有形有料」,但还不能贸然交给调度器——因为一次提交往往并非孤立执行:它可能必须等待其他提交先完成(例如上一帧的渲染结果、另一个队列写入的纹理),也可能被别人等待(后续提交要读它的输出)。本节聚焦第 ③ 阶段中依赖类 chunk 的解析与第 ⑦ 阶段amdgpu_cs_sync_rings,讲清内核如何收集本次提交的入方向依赖(in-fence)、又如何为其准备出方向信号(out-fence)。
1. 两个方向、两种同步
围绕一次提交的同步关系可以拆成两个方向:
- in-fence(前向依赖):本次提交的 job 开始执行前必须等待的一组 fence。只有它们全部 signal,job 才被允许上 ring。
- out-fence(完成信号):本次提交完成时对外发出的 fence,供后续提交或 CPU 侧等待。
而 in-fence 又按「谁来指定」分为两类,这正对应第六章讲过的两种同步策略:
| 类别 | 来源 | 对应章节 |
|---|---|---|
| 显式依赖 | 用户在 chunk 中明确列出要等待的 fence / syncobj | 6.4 显式同步 |
| 隐式依赖 | 从本次引用的 BO 的dma_resv上自动读取已挂载的 fence | 6.3 隐式同步 dma_resv |
内核用 parser 上的两个字段分别承接这两个方向:
p->sync(amdgpu_sync容器):汇集所有 in-fence,无论显式还是隐式,最终统一压入 job 作为调度依赖。p->post_deps:登记那些需要用本次 out-fence 去「点亮」的 syncobj,待提交成功后再逐一 signal。
其中「显式依赖的解析」与「out-fence 目标的登记」发生在第 ③ 阶段的pass2;「隐式依赖的收集与依赖入队」发生在第 ⑦ 阶段的sync_rings;「out-fence 的真正生成与分发」发生在第 ⑧ 阶段的submit。下面按此顺序展开。
2. 显式 in-fence:pass2 解析依赖类 chunk(③)
回顾 8.2:pass2遍历所有 chunk,IB 分支已在 8.2 讲过;本节接手它的依赖类分支。这些分支的共同归宿都是把解析出的 fence 通过amdgpu_sync_fence(&p->sync, fence, ...)塞进p->sync。
2.1 fence-based 依赖:DEPENDENCIES
amdgpu_cs_p2_dependencies处理AMDGPU_CHUNK_ID_DEPENDENCIES。用户以「(ctx_id, ip_type, ip_instance, ring, handle)」五元组指明「我要等某个上下文某条 ring 上序号为 handle 的那次提交」:
ctx=amdgpu_ctx_get(fpriv,deps[i].ctx_id);amdgpu_ctx_get_entity(ctx,deps[i].ip_type,deps[i].ip_instance,deps[i].ring,&entity);fence=amdgpu_ctx_get_fence(ctx,entity,deps[i].handle);/* 按 seq 取出目标 fence */...if(chunk->chunk_id==AMDGPU_CHUNK_ID_SCHEDULED_DEPENDENCIES){/* 取 scheduled 而非 finished,允许流水线重叠 */s_fence=to_drm_sched_fence(fence);fence=dma_fence_get(&s_fence->scheduled);...}r=amdgpu_sync_fence(&p->sync,fence,GFP_KERNEL);这里的handle正是被依赖的那次提交返回给用户的 seq(见第 5 节 out-fence 的cs->out.handle)——依赖链就此闭合。值得注意的是SCHEDULED_DEPENDENCIES变体:它取目标 fence 的scheduled(已被调度器排上但未必执行完)而非finished。这是一种流水线优化——后继作业只需等前驱「排上队」即可开始准备,无需苦等其彻底完成,前提是二者在同一条 ring 上天然保序。
2.2 syncobj 依赖:SYNCOBJ_IN 与 TIMELINE_WAIT
两者共用辅助函数amdgpu_syncobj_lookup_and_add,区别只在于「取哪个点的 fence」:
r=drm_syncobj_find_fence(p->filp,handle,point,flags,&fence);...r=amdgpu_sync_fence(&p->sync,fence,GFP_KERNEL);amdgpu_cs_p2_syncobj_in(SYNCOBJ_IN):二元 syncobj,固定point = 0——取该 syncobj 当前持有的 fence。amdgpu_cs_p2_syncobj_timeline_wait(SYNCOBJ_TIMELINE_WAIT):timeline syncobj,按用户给定的point取对应时间点的 fence。
关于 syncobj 与 timeline 的内核实现,可回看 6.4.2 drm_syncobj。
综上,pass2 中所有显式 in-fence 的解析可归纳为一张表——终点都是p->sync:
| chunk 类型 | 处理函数 | 依赖来源 |
|---|---|---|
DEPENDENCIES/SCHEDULED_DEPENDENCIES | p2_dependencies | 另一提交的 fence(按 ctx+ring+seq 取,后者取 scheduled) |
SYNCOBJ_IN | p2_syncobj_in | 二元 syncobj 当前 fence |
SYNCOBJ_TIMELINE_WAIT | p2_syncobj_timeline_wait | timeline syncobj 指定 point 的 fence |
3. 显式 out-fence 目标:登记待 signal 的 syncobj(③)
对称地,用户也可要求「本次提交完成后去点亮某些 syncobj」。这类 out chunk 在 pass2 里只做登记,把目标 syncobj 存进p->post_deps,真正 signal 留到第 ⑧ 阶段:
staticintamdgpu_cs_p2_syncobj_out(structamdgpu_cs_parser*p,...){...p->post_deps=kmalloc_array(num_deps,sizeof(*p->post_deps),GFP_KERNEL);for(i=0;i<num_deps;++i){p->post_deps[i].syncobj=drm_syncobj_find(p->filp,deps[i].handle);p->post_deps[i].chain=NULL;/* 二元:无需 chain */p->post_deps[i].point=0;p->num_post_deps++;}}amdgpu_cs_p2_syncobj_out(SYNCOBJ_OUT):二元 syncobj,chain = NULL、point = 0。amdgpu_cs_p2_syncobj_timeline_signal(SYNCOBJ_TIMELINE_SIGNAL):timeline syncobj,若point != 0则预先dma_fence_chain_alloc()备好一个 chain 节点(timeline 语义要求把新 fence 以「点」的形式串上时间线,参见 6.1.5)。
可跳过的实现细节:第 2、3 节的多个
p2_*函数形态高度相似,本质都是「遍历数组 → 查出 fence/syncobj → 存入p->sync或p->post_deps」。若只关心主干,记住「显式 in-fence 汇入p->sync,显式 out 目标登记进p->post_deps」即可,各 chunk 子类型的差异可在需要时再回看。
4. 隐式 in-fence 与依赖入队:sync_rings(⑦)
BO 加锁完成后(8.3),第 ⑦ 阶段amdgpu_cs_sync_rings补齐隐式依赖,并把p->sync里累积的全部 fence 正式压入 job。它依次做三件事:
4.1 等待本 entity 的上一次提交
r=amdgpu_ctx_wait_prev_fence(p->ctx,p->entities[p->gang_leader_idx]);这一步保证同一调度实体上的提交按序推进,兼作对 ctx fence 环的流控——避免尚有在途提交时就覆盖其记录。
4.2 从每个 BO 的 dma_resv 收集隐式依赖
drm_exec_for_each_locked_object(&p->exec,index,obj){structdma_resv*resv=gem_to_amdgpu_bo(obj)->tbo.base.resv;sync_mode=amdgpu_bo_explicit_sync(bo)?AMDGPU_SYNC_EXPLICIT:AMDGPU_SYNC_NE_OWNER;r=amdgpu_sync_resv(p->adev,&p->sync,resv,sync_mode,&fpriv->vm);}遍历 8.3 锁定的每个 BO,把其dma_resv上已有的 fence 纳入p->sync。sync_mode是关键:
AMDGPU_SYNC_NE_OWNER(默认):只同步不属于本 VM的 fence(Not-Equal-OWNER)。同一进程在同一 VM 上对某 BO 的先前写入,本就由 ring 顺序保证可见,无需再插入依赖——借此避免自我串行化,是重要的性能优化。AMDGPU_SYNC_EXPLICIT:当 BO 被标记为「显式同步」时,隐式路径不再自动为它建立依赖,一切交由用户显式指定。
dma_resv上 fence 的读/写用途分层(DMA_RESV_USAGE_*)决定了哪些 fence 需要被等待,细节见 6.3.1 dma_resv_usage。
4.3 把依赖压入 job,并为同 ring 依赖插入流水线同步
for(i=0;i<p->gang_size;++i)amdgpu_sync_push_to_job(&p->sync,p->jobs[i]);/* 全部 in-fence → 各 job 的调度依赖 */sched=p->gang_leader->base.entity->rq->sched;while((fence=amdgpu_sync_get_fence(&p->sync))){s_fence=to_drm_sched_fence(fence);if(!s_fence||s_fence->sched!=sched){/* 非同一调度器 ring:跳过 */dma_fence_put(fence);continue;}/* 同一 ring 上的依赖:额外挂到 gang_leader->explicit_sync */amdgpu_sync_fence(&p->gang_leader->explicit_sync,fence,GFP_KERNEL);}amdgpu_sync_push_to_job把p->sync中的每个 fence 登记为 job 的调度依赖——调度器要等这些 fence 全部 signal 才会真正发射该 job。随后那段while循环单独处理「依赖恰好落在与本提交同一条 ring 上」的情况:把它们额外收进gang_leader->explicit_sync,以便在执行前插入一次流水线同步(pipeline sync),确保上一作业的缓存刷新、结果对下一作业可见——同一 ring 上先后执行的两个作业之间,硬件并不天然保证这种可见性。
5. out-fence 的生成与分发(⑧ 的依赖部分)
依赖备齐后,真正的 out-fence 在第 ⑧ 阶段amdgpu_cs_submit中诞生。submit 的完整临界区(drm_sched_job_arm、dma_resv回写、userptr 失效重试等)是 8.5 的主题,这里只截取与「out-fence」直接相关的三步:
p->fence=dma_fence_get(&leader->base.s_fence->finished);/* ① out-fence = 领队的 finished fence */...seq=amdgpu_ctx_add_fence(p->ctx,p->entities[p->gang_leader_idx],p->fence);/* ② 登记进 ctx,得 seq */amdgpu_cs_post_dependencies(p);/* ③ 用 out-fence 点亮 post_deps 中的 syncobj */...cs->out.handle=seq;/* seq 返回用户态 */三步含义:
- out-fence 的本体是 gang 领队 job 的调度器
finishedfence——它在整个 gang 执行完毕时 signal。 amdgpu_ctx_add_fence把该 fence 登记进 ctx 的 fence 环并返回一个序号seq;seq经cs->out.handle回传用户态。日后用户可凭此 seq 通过amdgpu_cs_wait_ioctl等待,或经amdgpu_cs_fence_to_handle_ioctl转成 sync_file fd——这也正是第 2.1 节里DEPENDENCIES依赖所引用的那个handle。amdgpu_cs_post_dependencies遍历第 3 节登记的p->post_deps,用刚诞生的p->fence去点亮每个目标 syncobj:
if(post_deps[i].chain&&post_deps[i].point)drm_syncobj_add_point(syncobj,chain,p->fence,point);/* timeline:串上时间点 */elsedrm_syncobj_replace_fence(syncobj,p->fence);/* 二元:替换当前 fence */至此,入方向(等待谁)与出方向(谁来等我)双双闭合:本次提交的 job 已挂好全部依赖,其完成信号也已既能被 seq 查询、又能点亮用户指定的 syncobj。
6. 形象小结:一次「排班与交接」
若把每次提交看作车间里的一道工序,本阶段做的正是排班与交接:
| 环节 | 阶段 | 动作 |
|---|---|---|
| 列出「我要等哪些前道工序」——点名指定 | ③ pass2 | 解析DEPENDENCIES/SYNCOBJ_IN/TIMELINE_WAIT→p->sync |
| 登记「我完工后要通知谁」 | ③ pass2 | SYNCOBJ_OUT/TIMELINE_SIGNAL→p->post_deps |
| 自动补上「我碰过的料还有谁在用」 | ⑦ sync_rings | 从各 BOdma_resv收集隐式依赖 →p->sync |
| 把等待清单钉到工单上 | ⑦ sync_rings | push_to_job+ 同 ring 流水线同步 |
| 发出完工凭据、通知下游 | ⑧ submit | 领队 finished fence →seq/cs->out.handle+ 点亮 post_deps |
设计上的两条主线值得记住:其一,所有 in-fence(显式 + 隐式)汇于一处p->sync,再统一压入 job,让调度器只面对一份干净的依赖清单;其二,out-fence 一体两面——对内是可按 seq 查询的 ctx fence,对外是可点亮 syncobj 的信号源,二者同为领队的finishedfence。
可跳过的实现细节:只关心主干的读者记住三句话即可——(1) 显式依赖来自 chunk、隐式依赖来自 BO 的
dma_resv,两者都进p->sync并压给 job;(2)NE_OWNER模式跳过本 VM 自己的 fence,避免自我串行;(3) out-fence 就是领队 job 的 finished fence,既登记为 seq 返回用户、又用来点亮 out-syncobj。
依赖与信号均已就位,job 只差最后一步——被真正「武装」并推入调度器。下一节 8.5 将进入第 ⑧ 阶段amdgpu_cs_submit,剖析drm_sched_job_arm、notifier_lock保护下的dma_resvfence 回写、userptr 失效导致的-EAGAIN重试,以及最终的drm_sched_entity_push_job。