GPU Scheduler 分析:7.5 drm_sched_fence — 双 Fence 语义详解

GPU Scheduler 是 dma-fence 的重要消费者,可以作为一个总要的应用场景来理解。

1. 为什么需要两个 Fence

在第六章(dma-fence)中我们知道,dma_fence 是 GPU 异步操作的完成信号。一个直觉的设计是:每个 job 一个 fence,硬件完成时信号。

但 GPU scheduler 引入了一个中间状态 :job 被调度器选中并发射到硬件(run_job()),与 job 在硬件上真正执行完毕,是两个不同的时间点。

复制代码
时间线: ──push──────────────run_job()──────────────hw完成──────→
                              │                       │
                              ▼                       ▼
                         scheduled fence          finished fence
                         (已进入硬件队列)          (硬件执行完毕)

如果只有一个 fence(finished),那么依赖 job A 的 job B 必须等到 A 在硬件上执行完毕才能被发射。但实际上,一旦 A 已经进入硬件 ring,B 紧随其后进入同一 ring 是完全安全的------硬件 ring 本身保证顺序执行。

双 fence 设计的核心收益 :允许依赖链的流水线化(pipelining)------后续 job 只要等前序 job 进入硬件(scheduled),无需等其执行完毕(finished),从而最大化硬件利用率。

2. drm_sched_fence 结构

struct drm_sched_fence

c 复制代码
struct drm_sched_fence {
    struct dma_fence         scheduled;       // 子 fence 1:已调度
    struct dma_fence         finished;        // 子 fence 2:已完成
    ktime_t                  deadline;        // deadline hint
    struct dma_fence         *parent;         // run_job() 返回的硬件 fence
    struct drm_gpu_scheduler *sched;          // 所属 scheduler
    spinlock_t               lock;            // 两个 fence 共享的锁
    void                     *owner;          // 调试用:job 所有者
    uint64_t                 drm_client_id;   // drm_file 的 client_id
};

关键设计:scheduledfinished 是两个独立的 dma_fence ,各自有独立的 fence_ops、独立的信号时机,但共享同一个 spinlock,且使用连续的 fence context(entity->fence_contextentity->fence_context + 1)。

3. 生命周期与信号时机

3.1 分配(alloc)

drm_sched_fence_alloc():在 drm_sched_job_init() 中调用。

c 复制代码
struct drm_sched_fence *drm_sched_fence_alloc(struct drm_sched_entity *entity,
                                              void *owner, u64 drm_client_id)
{
    fence = kmem_cache_zalloc(sched_fence_slab, GFP_KERNEL);
    fence->owner = owner;
    fence->drm_client_id = drm_client_id;
    spin_lock_init(&fence->lock);
    return fence;
}

此时 fence 只是一块内存,scheduledfinished 尚未初始化为有效的 dma_fence

3.2 初始化(init)

drm_sched_fence_init():在 drm_sched_job_arm() 中调用。

c 复制代码
void drm_sched_fence_init(struct drm_sched_fence *fence,
                           struct drm_sched_entity *entity)
{
    fence->sched = entity->rq->sched;
    seq = atomic_inc_return(&entity->fence_seq);

    dma_fence_init(&fence->scheduled,
                   &drm_sched_fence_ops_scheduled,
                   &fence->lock,
                   entity->fence_context,        // context N
                   seq);

    dma_fence_init(&fence->finished,
                   &drm_sched_fence_ops_finished,
                   &fence->lock,
                   entity->fence_context + 1,    // context N+1
                   seq);                         // 同一 seqno
}

两个 fence 使用相邻的 context、相同的 seqno。这确保:

  • 同 entity 的 fence 严格递增(fence ordering 保证)
  • scheduled 和 finished 属于不同 context(不会被 dma_fence_is_later() 误比较)

3.3 Scheduled 信号

drm_sched_fence_scheduled():在 drm_sched_run_job_work() 中,run_job() 调用后立即触发。

c 复制代码
void drm_sched_fence_scheduled(struct drm_sched_fence *fence,
                               struct dma_fence *parent)
{
    if (!IS_ERR_OR_NULL(parent))
        drm_sched_fence_set_parent(fence, parent);  // 关联硬件 fence

    dma_fence_signal(&fence->scheduled);            // 信号!
}

语义:job 的命令已写入硬件 ring,GPU 即将(或正在)执行。所有等待 scheduled fence 的 waiter 被唤醒。

3.4 Finished 信号

drm_sched_fence_finished():在 drm_sched_job_done() 中触发(由 parent fence 的回调间接调用)。

c 复制代码
void drm_sched_fence_finished(struct drm_sched_fence *fence, int result)
{
    if (result)
        dma_fence_set_error(&fence->finished, result);  // 传播错误码

    dma_fence_signal(&fence->finished);                 // 信号!
}

语义:GPU 已完成该 job 的所有命令执行。数据已写入目标 BO,可以安全读取。

3.5 完整时序图

复制代码
drm_sched_job_init()
  └── drm_sched_fence_alloc()         → s_fence 分配(未初始化)

drm_sched_job_arm()
  └── drm_sched_fence_init()          → scheduled/finished 初始化
                                         fence 可以被外部引用
                                         (写入 dma_resv, 返回给用户态)

drm_sched_run_job_work()
  ├── run_job() → 返回 hw_fence       → parent = hw_fence
  └── drm_sched_fence_scheduled()      → ★ scheduled fence 信号

hw_fence 信号 (GPU 完成)
  → drm_sched_job_done_cb()
    → drm_sched_job_done()
      → drm_sched_fence_finished()     → ★ finished fence 信号

4. Parent Fence:连接软件与硬件

parentrun_job() 回调返回的驱动硬件 fence------它代表 GPU 硬件对命令完成的承诺。

复制代码
关系链:
  s_fence->scheduled  ←─ scheduler 内部信号(run_job 时)
  s_fence->parent     ←─ 硬件 fence(GPU 完成时信号)
  s_fence->finished   ←─ parent 信号后,由回调链触发信号

drm_sched_fence_set_parent()

c 复制代码
static void drm_sched_fence_set_parent(struct drm_sched_fence *s_fence,
                                       struct dma_fence *fence)
{
    smp_store_release(&s_fence->parent, dma_fence_get(fence));

    // 如果 finished fence 已经有 deadline hint,传播给 parent
    if (test_bit(DRM_SCHED_FENCE_FLAG_HAS_DEADLINE_BIT,
                 &s_fence->finished.flags))
        dma_fence_set_deadline(fence, s_fence->deadline);
}

注意 smp_store_release:这是与 drm_sched_fence_set_deadline_finished() 的配对,防止并发设置 deadline 时看到未初始化的 parent。

5. 流水线优化:scheduled fence 的核心价值

在 X.3(entity)中我们看到 drm_sched_entity_add_dependency_cb() 的流水线优化:

c 复制代码
s_fence = to_drm_sched_fence(fence);
if (!fence->error && s_fence && s_fence->sched == sched &&
    !test_bit(DRM_SCHED_FENCE_DONT_PIPELINE, &fence->flags)) {
    // 依赖来自同一 scheduler ------ 降级为等 scheduled fence
    fence = dma_fence_get(&s_fence->scheduled);
    dma_fence_put(entity->dependency);
    entity->dependency = fence;
}

效果

复制代码
无流水线 (传统模式):
  Job A: ──run──────────────[hw执行]──────finished──→
  Job B:                                           ──run──[hw执行]──finished──→
         等 A finished 才能 run B                  串行,硬件利用率低

有流水线 (scheduled fence):
  Job A: ──run──────────────[hw执行]──────finished──→
  Job B:        ──run──────────────[hw执行]──────finished──→
         等 A scheduled 即可 run B                 重叠,硬件利用率高

条件限制:

  1. 同一 schedulers_fence->sched == sched):不同 ring 之间不能假设顺序
  2. 无错误!fence->error):前序 job 失败则不能流水线
  3. 未设 DONT_PIPELINE flag:某些场景需要强制完整等待

6. DRM_SCHED_FENCE_DONT_PIPELINE

c 复制代码
#define DRM_SCHED_FENCE_DONT_PIPELINE  DMA_FENCE_FLAG_USER_BITS

设置此 flag 后,即使依赖来自同一 scheduler,也不降级为等 scheduled fence,强制等 finished fence(即硬件完成后才调度后续 job)。

使用场景:

  • 页表更新:VM page table update job 必须在硬件上完成后,后续 job 才能使用新映射
  • Preemption fence:抢占完成后才能重新提交
  • 跨 ring 语义屏障:某些驱动需要显式的执行屏障

7. Deadline 传播

用户态或内核可以通过 dma_fence_set_deadline() 给 finished fence 设置 deadline hint("希望在这个时间点前完成")。

问题:deadline 设置时 parent(硬件 fence)可能还不存在(job 还没被 run)。

解决方案------两阶段传播:

复制代码
场景 1: deadline 先于 parent 设置
  dma_fence_set_deadline(finished, T)
    → 保存到 s_fence->deadline
    → 设置 HAS_DEADLINE_BIT

  (稍后) drm_sched_fence_set_parent(s_fence, hw_fence)
    → 检查 HAS_DEADLINE_BIT
    → dma_fence_set_deadline(hw_fence, T)    ← 传播!

场景 2: parent 先于 deadline 设置
  drm_sched_fence_set_parent(s_fence, hw_fence)
    → smp_store_release(&parent, hw_fence)

  (稍后) dma_fence_set_deadline(finished, T)
    → drm_sched_fence_set_deadline_finished()
    → 保存 deadline + 设 bit
    → parent = smp_load_acquire(&s_fence->parent)
    → dma_fence_set_deadline(parent, T)      ← 传播!

无论哪种顺序,deadline 最终都会到达硬件 fence,驱动可据此调整 GPU 频率或优先级。smp_store_release / smp_load_acquire 配对保证了无锁的正确性。

8. 引用计数与释放

两个 fence 的释放策略不同:

Fence release 回调 释放逻辑
scheduled drm_sched_fence_release_scheduled 释放 parent 引用 + RCU 释放整个 drm_sched_fence 结构
finished drm_sched_fence_release_finished 释放 scheduled fence 的引用(dma_fence_put(&scheduled)

释放链:

复制代码
finished 引用归零
  → release_finished()
    → dma_fence_put(&scheduled)

scheduled 引用归零
  → release_scheduled()
    → dma_fence_put(parent)       // 释放硬件 fence
    → call_rcu(..., free_rcu)     // RCU 延迟释放整个结构体

RCU grace period 后
  → kmem_cache_free(sched_fence_slab, fence)

为什么用 RCU? 因为 to_drm_sched_fence() 可能在无锁路径中被调用(如 entity 的依赖检查),需要保证结构体在 RCU 读临界区内有效。

为什么 scheduled 持有结构体的生命周期? 因为 drm_sched_fence 结构包含两个 fence,整体释放必须等两者都无人引用。设计上让 finished 引用 scheduled,scheduled 引用结构体。finished 归零 → scheduled 归零 → 结构体释放。

9. 与 dma_resv 的联动

这是双 fence 语义在实际使用中的关键环节:

复制代码
驱动提交流程:
  1. drm_sched_job_init() + add_dependency()
  2. drm_sched_job_arm()                     ← s_fence 有效
  3. dma_resv_add_fence(bo->resv,
                        &job->s_fence->finished,
                        DMA_RESV_USAGE_WRITE)  ← 注册到 BO
  4. drm_sched_entity_push_job()

后续访问者:
  drm_sched_job_add_implicit_dependencies(new_job, bo, true)
    → 从 bo->resv 中取出 s_fence->finished
    → 添加为 new_job 的依赖
    → pop 时检查:同 scheduler? → 降级为等 scheduled fence(流水线)

整个闭环

  1. 提交时将 finished fence 注册到 BO 的 reservation object
  2. 后续 job 通过隐式依赖从 BO 的 resv 中获取这个 fence
  3. 如果后续 job 在同一 scheduler 上,自动降级为等 scheduled fence(流水线优化)
  4. 如果在不同 scheduler/设备上,等待 finished fence(硬件完成后才安全)

这就是 scheduler 如何驱动整个 DRM 同步生态------它既是 fence 的生产者(创建 scheduled/finished),也是消费者(检查 job 的 dependencies)。

10. Fence Ops 对比

属性 scheduled fence finished fence
fence_ops drm_sched_fence_ops_scheduled drm_sched_fence_ops_finished
context entity->fence_context entity->fence_context + 1
seqno entity->fence_seq (相同) entity->fence_seq (相同)
信号时机 run_job() 后立即 硬件完成后
set_deadline 有(传播到 parent)
release 释放 parent + RCU 释放结构体 释放 scheduled 引用
用途 流水线依赖解除 最终完成通知、dma_resv

11. 小结

维度 要点
核心价值 双 fence 实现依赖链流水线化,最大化硬件利用率
scheduled run_job() 时信号,表示"已进入硬件队列"
finished 硬件完成时信号,表示"数据已就绪"
parent 连接 scheduler 软件 fence 与驱动硬件 fence
DONT_PIPELINE 禁止流水线,强制等硬件完成
Deadline 双向传播,无论设置顺序都能到达硬件 fence
引用计数 finished → scheduled → 结构体,RCU 延迟释放
dma_resv 联动 finished 注册到 BO,后续 job 通过隐式依赖自动获取

下一篇 7.6 将把前面所有组件串联起来------分析 sched_main.c 中的调度循环如何选取 entity、pop job、检查依赖和 credit、调用 run_job,以及完成后的 free 流程。