欢迎光临
我们一直在努力

第七章 :GPU Scheduler 分析:7.1 为什么需要 GPU 调度器

1. 从一个真实的 GPU 现场开始

想象一台普通 Linux 桌面正在同时发生这些事:

  • 桌面合成器不断提交小而频繁的渲染命令,希望屏幕稳定刷新;
  • 浏览器在用 GPU 合成网页、解码视频、上传纹理;
  • 游戏进程在 GFX ring 上持续提交 command buffer;
  • 另一个进程提交了耗时较长的 compute job;
  • 某些 BO 通过 dma-buf 在进程之间共享,访问顺序依赖 dma_fence;
  • 某个 job 里有错误命令,GPU 执行到一半不再返回完成信号。

这些事情最终都会变成一个问题:谁来决定 GPU 命令什么时候进入硬件队列?

如果驱动只是把用户提交的命令直接写进硬件 ring,看起来路径最短,但马上会遇到几个问题:

  • 桌面合成器的小 job 会不会被游戏的大量提交淹没?
  • 依赖还没完成的 job 如果提前执行,会不会读到旧数据或写坏共享 BO?
  • ring 和硬件流水线容量有限,驱动能不能无限往里塞命令?
  • 如果某个 job hang 住,内核怎样知道是哪一个 job、哪个 entity、哪个进程触发的问题?
  • GPU reset 后,哪些 job 需要丢弃,哪些 job 可以重放?

drm_gpu_scheduler 存在的意义,就在这里。

它把"驱动向硬件提交命令"这件事,变成一个可排队、可同步、可限流、可超时恢复的执行模型。

接下来逐个拆开这些问题。

2. 如果没有 Scheduler,会发生什么

2.1 缺少公平性:谁提交得快,谁就占满 ring

硬件 ring 是有限资源,但用户进程数量不是。

如果每个进程都能通过驱动直接把命令塞进 ring,那么高频提交者天然占优势。一个游戏、一个 compute 程序,或者一个异常活跃的用户 context,都可能让其他进程长时间得不到执行机会。

GPU Scheduler 的做法是引入 entity:每个进程或 context 不再直接面对硬件 ring,而是先把 job 放进自己的软件队列。调度器再从多个 entity 中挑选下一个 job。

这带来了一个关键变化:调度对象从"裸命令"变成了"有归属、有优先级、有顺序的 job"。

没有 scheduler:

process A ─┐
process B ─┼──> driver ───> hardware ring
process C ─┘

谁先调用驱动,谁更容易占住 ring。

有 scheduler:

process A ──> entity A ─┐
process B ──> entity B ─┼──> drm_gpu_scheduler ───> hardware ring
process C ──> entity C ─┘

scheduler 在 entity 之间做选择。

2.2 缺少依赖管理:fence 没完成就执行,数据会错

GPU job 很少是完全独立的。

一个 job 可能依赖:

  • 同一个 entity 里前一个 job 的结果;
  • 另一个进程对共享 BO 的写入完成;
  • dma-buf 导入方提供的外部 fence;
  • 驱动内部 VM update、page table update、buffer migration 等前置操作。

这些依赖通常表现为 dma_fence。如果 fence 还没 signal,job 就被送进硬件,结果不是"快一点",而是可能直接读到未完成的数据,或者覆盖别人还在使用的 BO。

GPU Scheduler 的核心职责之一,就是在 job 真正下发前检查依赖。依赖没就绪,job 留在软件队列里;依赖满足后,才进入 run_job()。

这也是它能和 dma-fence/dma_resv 体系自然衔接的原因:同步系统负责描述"什么时候安全",scheduler 负责把这个安全条件落实到执行顺序上。

值得强调的是,scheduler 在这里不只是 fence 的消费者,它也是 fence 的生产者。每个 job 下发和完成时,scheduler 都会 signal 自己的 fence,让后续依赖者感知到进度。这个双重角色是理解 scheduler 与 dma-fence 体系关系的关键——后面在 drm_sched_fence 部分会详细展开。

2.3 缺少流控:硬件队列不是无限深

硬件 ring 和 GPU 内部流水线都有容量限制。

如果调度器不限制已经发射但尚未完成的 job 数量,驱动可能把过多命令推到硬件侧,导致 ring 空间紧张、延迟不可控,甚至让错误恢复变得更复杂。

drm_gpu_scheduler 用 credit 做流控:

  • 每个 scheduler 有一个 credit_limit,表示这条硬件队列可承受的在途容量;
  • 每个 job 有自己的 credits,表示它会消耗多少容量;
  • job 被发射后,credits 计入 credit_count;
  • job 完成后,credits 被归还;
  • 如果再发射一个 job 会超过上限,scheduler 暂停下发。

可以把它理解成硬件 ring 前面的"闸门":不是所有已经就绪的 job 都马上进入硬件,而是要看当前硬件侧还有没有容量。

2.4 缺少超时恢复:GPU hang 后没人知道卡在哪里

GPU 可能 hang。

它可能是用户提交了非法命令,也可能是驱动 bug、firmware bug、硬件问题,或者复杂同步路径中的边界条件。无论原因是什么,症状都类似:某个已经发射的 job 长时间没有完成,硬件 fence 不再 signal。

如果没有 scheduler,驱动很难统一回答:

  • 当前哪些 job 已经进入硬件但还没完成?
  • 最老的 pending job 是谁?
  • 超时应该归因到哪个 entity?
  • reset 前要停止哪些调度路径?
  • reset 后哪些 job 可以恢复或重放?

GPU Scheduler 用 pending_list 记录已发射、未完成的 job,并用 delayed work 做 TDR(Timeout Detection and Recovery)检测。超时后,它调用驱动提供的 timedout_job() 回调,让具体驱动执行 GPU reset、job replay、entity guilty 标记等恢复逻辑。

换句话说,scheduler 不只是"排队器",它还是 GPU 执行现场的账本。

2.5 小结:四类问题与对应机制

把上面四个子问题压缩成源码视角:

问题没有它的后果Scheduler 的机制关键对象
多进程共享 ring 高频提交者可能长期占用硬件队列 多 entity + 多优先级 rq + entity 选择策略 drm_sched_entity, drm_sched_rq
job 之间有依赖 fence 未完成就执行,可能造成数据损坏 job dependency 检查,未就绪则等待 fence signal drm_sched_job
硬件容量有限 ring 被过度填充,在途 job 失控 credit_limit / credit_count 流控 drm_gpu_scheduler
GPU 可能 hang 整条执行链永久卡住,难以定位责任 pending_list + TDR delayed work + timedout_job() drm_sched_job, drm_gpu_scheduler

所以,drm_gpu_scheduler 的本质不是"给 ring 前面再套一层队列"。它真正提供的是一个通用执行模型:

用户提交
-> 软件队列化
-> 依赖检查
-> 优先级和公平性选择
-> credit 流控
-> run_job() 下发硬件
-> pending_list 跟踪
-> fence 完成通知
-> TDR 超时恢复

这条链路让 DRM 驱动可以把"如何写硬件 ring"留给自己,把"如何调度 job"交给公共框架。

3. 核心对象与执行流程

GPU Scheduler 的核心对象可以按一条执行链理解:

drm_gpu_scheduler
├── drm_sched_rq
│ └── drm_sched_entity
│ └── drm_sched_job
│ └── drm_sched_fence
└── pending_list

下面先看一次完整提交的流程图,然后在流程的关键节点上解释每个对象的角色。

3.1 一次 Job 的完整旅程

在这里插入图片描述

3.2 drm_gpu_scheduler:一条硬件队列的软件调度器

对应流程图中的"调度循环被唤醒"到"run_job()"这一段。

drm_gpu_scheduler 通常对应一条硬件 ring。AMDGPU 里常见的 GFX、SDMA、Compute、VCN 等 ring,都可以有自己的 scheduler。这样每条硬件队列维护自己的运行队列、credit 状态和 TDR 状态,互不混淆。

它负责回答:这条 ring 现在还能不能接收新 job?下一个应该发射哪个 job?已经发射但没完成的 job 有哪些?

3.3 drm_sched_rq:按优先级分组的运行队列

对应流程图中"扫描优先级 rq"这一步。

调度器内部有多个 run queue,常见优先级是:

KERNEL > HIGH > NORMAL > LOW

跨优先级时,scheduler 先看高优先级 rq;同一优先级内部,再按照策略选择 entity。默认策略是 FIFO,也可以通过 drm_sched_policy 切换到 Round Robin。

这一层解决的是:不同重要性的提交不能完全混在一起。

3.4 drm_sched_entity:用户 context 的提交通道

对应流程图中"entity_push_job()“(入口)和"选择 entity”(出口)两个节点。

entity 是 scheduler 里非常关键的抽象。它通常对应一个用户 context、一个进程的某条提交通道,或者驱动内部的一类提交来源。每个 entity 有自己的 job queue,内部保持 FIFO 顺序。

这意味着:

  • 同一个 entity 内的 job 有天然顺序;
  • scheduler 可以在多个 entity 之间做公平选择;
  • hang、flush、finish 等操作可以定位到具体 entity;
  • 一个 entity 还可以绑定多个 scheduler,用于多 ring 负载均衡。

entity 让 GPU 调度从"谁提交命令"变成"哪个提交通道现在应该前进"。

3.5 drm_sched_job:一次 GPU 提交的可调度单元

对应流程图中从"创建驱动私有 job"到"free_job()"的整个生命周期。

job 是一次命令提交在 scheduler 里的表示。驱动通常会定义自己的 job 结构,并把 struct drm_sched_job 嵌进去。例如 amdgpu 会在自己的 job 中保存 IB、VM flush、依赖、调度信息等驱动私有状态。

对 scheduler 来说,一个 job 至少要回答几件事:

  • 它属于哪个 entity?
  • 它依赖哪些 fence?
  • 它消耗多少 credits?
  • 它的 scheduler fence 是什么?
  • 调用 run_job() 后,驱动返回的硬件 fence 是什么?

job 让一次 GPU 提交变成"能等待、能排序、能下发、能追踪、能释放"的对象。

3.6 drm_sched_fence:scheduler 对外发布的两个时间点

对应流程图中"signal scheduled fence"和"signal finished fence"两个节点。

每个 scheduler job 都有一个 drm_sched_fence。它里面不是一个 fence,而是两个 fence:

Fence何时 signal表达的语义
scheduled run_job() 被调用,job 已交给驱动/硬件队列 这个 job 已经被调度,可以让某些后续依赖流水化
finished 硬件 fence signal,job 真正执行完成 这个 job 的结果可见,BO 可以被后续访问者安全使用

这个双 fence 设计很重要:

entity queue
|
| dependency ready + credit available
v
run_job() called
|
|– signal scheduled fence
|
v
hardware execution
|
| hw fence signal
v
signal finished fence

为什么需要 scheduled?因为有些依赖只需要保证前一个 job 已经进入硬件队列,不一定要等它彻底完成。这样后续 job 可以更早进入流水线,减少 CPU 往返和调度空洞。

为什么还需要 finished?因为 BO 的最终可见性、用户空间等待、跨设备共享,都必须以真正完成为准。写入 dma_resv、导出 sync_file、用户等待 syncobj,通常关心的是 finished 语义。

也正因为这里能生产 finished fence,后续它才能写入 BO 的 dma_resv,被 dma-buf、syncobj、sync_file 等机制继续传播。Scheduler 把 GPU 命令执行过程变成 dma-fence 生态能够理解的时间线。

4. 在 DRM 技术栈中的定位与边界

GPU Scheduler 位于 DRM 技术栈的执行层。它不负责分配显存,也不负责解析命令,但它把前面几层的结果串起来。

层次负责的问题Scheduler 的参与
GEM 用户可见的 buffer object 抽象 job 操作的对象通常是 GEM BO
TTM BO 的物理资源、迁移、放置 job 执行前可能需要 BO 已迁移到正确位置
DMA-BUF / PRIME 跨设备、跨进程共享 BO job 可能等待外部设备 fence
dma-fence / dma_resv 描述异步访问完成时间点 scheduler 消费依赖 fence,也生产完成 fence
GPU Scheduler 决定 job 何时执行、如何排队、如何恢复 本章主题
硬件 ring 接收驱动写入的具体命令 scheduler 通过驱动 run_job() 间接下发

同样重要的是明确 scheduler 不做什么:

不负责说明通常由谁负责
分配 VRAM/GTT scheduler 不决定 BO 放在哪里 TTM / 驱动内存管理
建 GPU 页表 scheduler 不处理 VA 到 PA 的映射 GPUVM / 驱动
生成命令包 scheduler 不理解 IB 里的具体 packet 用户态驱动 / 内核驱动
直接写 ring scheduler 不自己操作硬件寄存器 驱动的 run_job() 回调
全局跨设备调度 scheduler 管的是某个 DRM 驱动内部的执行队列 用户态、驱动、设备专有机制

GPU Scheduler 是公共执行框架,不是 GPU 驱动的全部。它抽象的是"job 如何被调度",不是"job 具体怎么让硬件跑起来"。

这也是为什么 scheduler 适合放在 dma-fence 之后学习。dma-fence 解释了"异步完成"如何表达;GPU Scheduler 则解释了"GPU 命令执行"如何变成这些 fence 的生产和消费过程。

5. 源码地图和本章分析顺序

这一篇不做逐行分析,但最好先建立源码地图。

主题关键文件先看什么
核心结构体定义 include/drm/gpu_scheduler.h drm_gpu_scheduler, drm_sched_entity, drm_sched_job, drm_sched_fence
调度主循环 drivers/gpu/drm/scheduler/sched_main.c entity 选择、dependency 检查、credit 检查、run_job() 调用
entity 入队与依赖等待 drivers/gpu/drm/scheduler/sched_entity.c drm_sched_entity_push_job(), drm_sched_entity_pop_job()
scheduler fence drivers/gpu/drm/scheduler/sched_fence.c scheduled / finished fence 的初始化与 signal
AMDGPU 接入 drivers/gpu/drm/amd/amdgpu/ amdgpu_ring, amdgpu_job, amdgpu_sched_ops

阅读顺序可以按这个问题链走:

job 怎么创建?
-> job 怎么进入 entity?
-> entity 怎么进入 rq?
-> scheduler 怎么选 entity?
-> dependency 没好时怎么等待?
-> credit 不够时怎么停住?
-> run_job() 返回的 hw fence 怎么接到 sched fence?
-> timeout 后怎么走 TDR?

后续章节基本就是沿着这条链路逐段拆开。

  • 7.2 drm_gpu_scheduler:per-ring 调度器的字段、初始化、credit 和 pending_list;
  • 7.3 drm_sched_entity:用户提交通道、优先级、负载均衡和 entity 生命周期;
  • 7.4 drm_sched_job:一次提交从 init、arm、push、run 到 free 的生命周期;
  • 7.5 drm_sched_fence:scheduled / finished 双 fence 语义;
  • 7.6 sched_main.c:调度主循环、依赖等待、credit 流控;
  • 7.7 TDR:超时检测、guilty job、GPU reset 与 job replay;
  • 7.8 AMDGPU 实战:从 amdgpu_cs_ioctl() 到 run_job() 的完整路径。

开篇内容很多,如果只留下一个结论,那就是:

drm_gpu_scheduler 把 GPU 命令提交从"驱动把命令塞进 ring",提升成"有归属、有依赖、有容量、有完成语义、有恢复路径"的执行模型。

赞(0)
未经允许不得转载:171主机测评 » 第七章 :GPU Scheduler 分析:7.1 为什么需要 GPU 调度器
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址