drm_gpu_scheduler
flowchart TD
subgraph GPU
slot-0[HW Run Queue]
slot-1[HW Run Queue]
slot-2[HW Run Queue]
end
sched0[drm_gpu_scheduler]
sched1[drm_gpu_scheduler]
sched2[drm_gpu_scheduler]
runq00[drm_sched_rq<br>KERNEL]
runq01[drm_sched_rq<br>HIGH]
runq02[drm_sched_rq<br>NORMAL]
runq03[drm_sched_rq<br>LOW]
runq10[drm_sched_rq<br>KERNEL]
runq11[drm_sched_rq<br>HIGH]
runq20[drm_sched_rq<br>KERNEL]
runq21[drm_sched_rq<br>HIGH]
runq22[drm_sched_rq<br>NORMAL]
entity00@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity01@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity02@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity03@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity10@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity11@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity12@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity20@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity21@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity22@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity00 --> entity01 --> entity02 --> entity03 --> runq00
entity10 --> entity11 --> entity12 --> runq10
entity20 --> entity21 --> entity22 --> runq02
runq00 --> sched0
runq01 --> sched0
runq02 --> sched0
runq03 --> sched0
runq10 --> sched1
runq11 --> sched1
runq20 --> sched2
runq21 --> sched2
runq22 --> sched2
sched0 --> slot-0
sched1 --> slot-1
sched2 --> slot-2
Notes:
- 每个 hw run queue (hw ring) 对应一个 drm_gpu_scheduler
DRIVER | MAX_HW_RINGS | VALUE |
---|---|---|
amdgpu | AMDGPU_MAX_RINGS | 124 |
etnaviv | ETNA_MAX_PIPES | 4 |
powervr | 4 | |
panfrost | NUM_JOB_SLOTS | 3 |
v3d | V3D_MAX_QUEUES | 6 |
- 每个 scheduler 对应多个不同优先级(
enum drm_sched_priority
)的 scheduler run queue (sw run queue)
但似乎对于未来的硬件,尤其那些通过 FW/HW 调度 job 的 GPU, 更希望的拓扑结构是 scheduler : run queue : entity 是 1:1:1. 这样的 drm_gpu_scheduler
已经退化成一个 dependency tracker, 没有了实质的调度的作用。
flowchart TD
subgraph GPU
GuC[FW/HW scheduler]
slot-0[HW Run Queue]
slot-1[HW Run Queue]
slot-2[HW Run Queue]
end
sched0[drm_gpu_scheduler]
sched1[drm_gpu_scheduler]
sched2[drm_gpu_scheduler]
runq00[drm_sched_rq<br>KERNEL]
runq10[drm_sched_rq<br>KERNEL]
runq20[drm_sched_rq<br>KERNEL]
entity00@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity10@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity20@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity00 --> runq00 --> sched0 --> GuC --> slot-0
entity10 --> runq10 --> sched1 --> GuC --> slot-1
entity20 --> runq20 --> sched2 --> GuC --> slot-2
- 每个 scheduler run queue 是一个将被调度的 entity 队列
drm_sched_entity.sched_list
, 一个 entity 上的 jobs 可以被调度到这个 sched_list 上的任意一个 scheduler 上, 实现这个过程是通过drm_sched_entity_select_rq()
完成的。
flowchart TD
subgraph GPU
slot-0[HW Run Queue]
slot-1[HW Run Queue]
slot-2[HW Run Queue]
end
sched0[drm_gpu_scheduler]
sched1[drm_gpu_scheduler]
sched2[drm_gpu_scheduler]
runq00[drm_sched_rq<br>KERNEL]
runq10[drm_sched_rq<br>KERNEL]
runq20[drm_sched_rq<br>KERNEL]
entity00@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity10@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity20@{shape: docs, label: "drm_sched_entity<br>job chain"}
entity00 --> runq00 --> sched0 --> slot-0
entity00 -.-> runq10
entity10 --> runq10 --> sched1 --> slot-1
entity10 -.-> runq00
entity20 --> runq20 --> sched2 --> slot-2
- 每个 entity 由包含若干个 gpu job 的链表组成
数据结构
Linux DRM 子系统的 drm_gpu_scheduler
负责提交和调度 GPU job,以一个单独的内核模块(gpu-sched
) 的形式存在。
drm_gpu_scheduler
调度器实例 (instance),运行时实际上是一个内核线程 (kthread), 这个线程启动是在 drm_sched_init()
。
实际上,自从内核 v6.8-rc1 a6149f039369 (“drm/sched: Convert drm scheduler to use a work queue rather than kthread”) drm_gpu_scheduler
的实现已经从 kthread 变成 work queue 了。 这个修改与 Intel Gen9+ 引入的 microcontrollers (μC) 之一 GuC 有关。
drm_sched_rq
若干个 drm_sched_entity
(list) 的封装。一个 scheduler 实例最多可以有 DRM_SCHED_PRIORITY_COUNT
个 drm_sched_rq
。调度器调度的其实就是一个个 entity。 这么多个 entity 按什么顺序提交给 GPU 由具体的 调度策略 (Scheduling Policy) 决定,而调度优先级 (Scheduling Priority) 由 drm_sched_rq
实现,有多少个优先级,一个 drm_gpu_scheduler
里就有多少个 drm_sched_rq
,每个优先级对应一个 drm_sched_rq
。
drm_sched_entity
若干个 drm_sched_job
(list) 的封装
drm_sched_job
被 entity 运行的一个 job, 一个 job 总是属于某一个 entity
drm_sched_fence
1 | struct drm_sched_fence { |
初始化 Scheduler 实例
v6.8
1 | int drm_sched_init( |
1 | int drm_sched_entity_init( |
Note:
- 5.4 没有让驱动提供一个 timeout_wq, 而是固定使用 delayable workqueue 去执行 drm_sched_job_timedout()
- 参数中的
timeout
是以 jiffies 计算的,如果设置成MAX_SCHEDULE_TIMEOUT
, 表示由驱动自己处理超时
Entity - Jobs 的容器
sequenceDiagram
participant Driver
participant Scheduler
participant Kworker
note right of Driver : Assign an entity to job
Driver ->> Scheduler : drm_sched_job_init()
Driver ->> Scheduler : drm_sched_job_arm(job)
note right of Scheduler : Given a priority, rq chosen,<br>then scheduler chosen,<br>then HW ring chosen
Scheduler ->> Scheduler : drm_sched_entity_select_rq(entity)
Driver ->> Scheduler : drm_sched_entity_push_job(job)
Scheduler ->> Scheduler : drm_sched_rq_add_entity(rq, entity)
opt DRM_SCHED_POLICY_FIFO
Scheduler ->> Scheduler : drm_sched_rq_update_fifo_locked(entity, rq, submit_timestamp)
end
note right of Scheduler : wake up the scheduler<br>(queue_work(submit_wq, &work_run_job))
Scheduler ->> Kworker : drm_sched_wakup()
rect rgb(200, 150, 255)
note left of Kworker : drm_sched_run_job_work()
Kworker ->> Scheduler : drm_sched_select_entity(sched)
Kworker ->> Scheduler : drm_sched_entity_pop_job(entity)
Kworker ->> Driver : sched->ops->run_job()
Kworker ->> Kworker : complete_all(entity->idle)
Kworker ->> Scheduler : drm_sched_fence_scheduled(s_fence, fence)
Kworker ->> Scheduler : drm_sched_job_done(job, result)
Kworker ->> Kworker : wake_up(&sched->job_scheduled)
note right of Kworker : again queue_work(submit_wq, &work_run_job)
Kworker ->> Kworker : drm_sched_run_job_queue()
end
Scheduler 如何工作
Job 提交一般由用户驱动通过 IOCTL_SUBMIT 命令触发,将 job 下发给 hw, 所谓下发就是将 64 位的 job(chain) 的起始地址写入 MMIO 寄存器或 ringbuffer, 然后再触发 doorbell, hw 就开始执行
sequenceDiagram
participant UMD
participant KMD
participant Kworker
KMD ->> KMD : drm_sched_init()
note right of KMD : 创建一个 work item
KMD ->> KMD : INIT_WORK(&sched->work_run_job,<br>drm_sched_run_job_work)
UMD ->> KMD : ioctl(SUBMIT)
KMD ->> KMD : drm_sched_job_init()
KMD ->> KMD : drm_sched_job_arm()
KMD ->> KMD : drm_sched_entity_push_job()
KMD ->> KMD : drm_sched_waitup()
note right of KMD : 将 work item 扔到 submit_wq 上去
KMD ->> KMD : drm_sched_run_job_queue()
note right of KMD : 一旦 workqueue 上有了 work,<br>空闲的 kworker 就会执行 work item
KMD ->> Kworker : wakeup
Kworker ->> Kworker : drm_sched_run_job_work()
Kworker ->> Kworker : drm_sched_entity_pop_job()
Kworker ->> Kworker : sched->ops->run_job()
note left of Kworker : writel(jc, dev->iomem + reg)<br>Go!
Note:
drm_sched_free_job_work()
和drm_sched_run_job_work()
是分开的两个 work item, 但它俩都会被扔到同一个 workqueue 上submit_wq
(workqueue 的实现很有意思,异步执行的单位是函数 (work_struct),而这些函数会被加入一个队列 (workqueue) 里推迟执行 (deferred),只要队列不为空,后台线程们就把它们拿出来并发地执行 (CMWQ). 后台线程是一个由内核自动管理的线程池,唤醒和睡眠不用驱动管,驱动只需要queue_work_on()
)- gpu scheduler 里的
submit_wq
是一个 Ordered Workqueue, 意思就是加到这个 wq 上的函数保证是顺序执行的,这就天然地解决了 run_job 和 free_job 的依赖问题 (mutual exclusive)
参考资料
- drm/scheduler: improve GPU scheduler documentation v2
- linux DRM GPU scheduler 笔记
- drm/panfrost: Add initial panfrost driver
- drivers/gpu 下的
drm_sched_backend_ops
- PowerVR Rogue Command Stream format
- Linux kernel workqueue 机制分析
- 异步处理的强力助手:Linux Workqueue 机制详解
- Concurrency Managed Workqueue之(一):workqueue的基本概念