第7篇:GPU 命令提交调度:DRM Scheduler
源码:drivers/gpu/drm/scheduler/sched_main.c + sched_entity.c | 头文件:include/drm/gpu_scheduler.h 系列目录:NVIDIA AI Infra 内核源码深度解析
1. 问题背景:GPU 作业调度为何复杂 GPU 不像 CPU——它不是简单的”取指令→执行→写回”。GPU 同时运行数百个线程、数千个 wavefront,命令是异步提交 的,执行延迟不可预测。内核需要:
流控 :防止用户态无限提交作业耗尽内核资源
优先级 :KERNEL 级命令优先于 USER 级命令
超时检测 :GPU 挂死时踢出 offending job 恢复可用性
依赖管理 :作业之间有依赖关系(fence)
多引擎调度 :GFX(渲染)、Compute、DMA、Video 各自有独立调度器
DRM Scheduler 是 Linux 内核中解决这些问题的通用框架。它不绑定特定硬件——amdgpu、nouveau、xe、panthor 以及所有 accel 子系统的驱动都在用它。
2. 架构总览 drivers/gpu/drm/scheduler/sched_main.c:25-49(DOC 注释):
1 2 3 4 1. 每个硬件运行队列 → 一个 drm_gpu_scheduler 2. 每个 scheduler → 多个 run queue(HIGH_HW, HIGH_SW, KERNEL, NORMAL) 3. 每个 run queue → 多个 drm_sched_entity 排队 4. 每个 entity → SPSC 队列存储 drm_sched_job
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ┌─────────────────────────────────────────────────────────┐ │ drm_gpu_scheduler │ │ ops: backend_ops (prepare_job, run_job, timedout_job) │ │ credit_limit: 64 credit_count: atomic_t │ │ │ │ ┌─────────────────────────────────────────────────────┐│ │ │ submit_wq: ordered_workqueue("amdgpu-submit" ) ││ │ │ ││ │ │ sched_rq[DRM_SCHED_PRIORITY_HIGH_HW] ← 最高优先 ││ │ │ sched_rq[DRM_SCHED_PRIORITY_HIGH_SW] ││ │ │ sched_rq[DRM_SCHED_PRIORITY_KERNEL] ← 内核命令 ││ │ │ sched_rq[DRM_SCHED_PRIORITY_NORMAL] ← 用户命令 ││ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ │ │ entity A │ │ entity B │ │ entity C │ ... ││ │ │ │ SPSC Q │ │ SPSC Q │ │ SPSC Q │ ││ │ │ │ [job1][ │ │ [job3][ │ │ [job5] │ ││ │ │ │ job2][ │ │ job4] │ │ │ ││ │ │ └──────────┘ └──────────┘ └──────────┘ ││ │ └─────────────────────────────────────────────────────┘│ │ │ │ delayed_work → timeout handler (TDR) │ └─────────────────────────────────────────────────────────┘
3. 核心数据结构 3.1 drm_sched_job — GPU 作业 每个提交到硬件的 GPU 命令被打包成 drm_sched_job:
1 2 3 4 5 6 7 8 9 struct drm_sched_job { struct dma_fence *s_fence ; struct dma_fence *hw_fence ; u32 credits; struct drm_sched_entity *entity ; struct list_head list ; };
关键字段:
s_fence:scheduled fence——作业被调度到硬件 时 signal
hw_fence:hardware fence——硬件执行完成 时 signal(由驱动的 run_job 回调返回)
credits:占用多少信用。复杂作业(大 buffer)可以占 2+ 信用点,简单作业占 1
3.2 drm_sched_entity — 用户上下文 drm_sched_entity 代表一个用户态上下文 (比如一个 OpenGL context 或 CUDA stream):
1 2 3 4 5 6 7 struct drm_sched_entity { struct drm_sched_rq *rq ; struct spsc_queue job_queue ; u32 priority; struct rb_node rb_tree_node ; };
SPSC 队列 (Single-Producer-Single-Consumer):用户态只有一个 writer(push_job),内核调度器只有一个 reader(pop_job)。锁开销为零。
3.3 drm_sched_backend_ops — 驱动回调 include/drm/gpu_scheduler.h:
1 2 3 4 5 6 7 struct drm_sched_backend_ops { struct dma_fence *(*prepare_job )(struct drm_sched_job *job , struct drm_sched_entity *entity ); struct dma_fence *(*run_job )(struct drm_sched_job *job ); void (*timedout_job)(struct drm_sched_job *job); void (*free_job)(struct drm_sched_job *job); };
回调
调用时机
含义
prepare_job
job 从 entity 取出后
准备依赖,做最后的 job 处理
run_job
硬件可以接纳新作业时
真正提交到 GPU 硬件队列
timedout_job
job 超时
该作业已挂死,驱动负责恢复
free_job
job 生命周期结束
释放驱动私有数据
3.4 drm_sched_init_args — 初始化参数 include/drm/gpu_scheduler.h:
1 2 3 4 5 6 7 8 9 10 11 12 struct drm_sched_init_args { const struct drm_sched_backend_ops *ops ; u32 credit_limit; const char *name; unsigned num_rqs; u32 hang_limit; long timeout; struct workqueue_struct *timeout_wq ; struct workqueue_struct *submit_wq ; struct device *dev ; };
4. 信用制流控 这是调度器最核心的反压机制 。想象用户态疯狂提交作业——如果没有流控,内核会耗尽内存。
4.1 drm_sched_available_credits sched_main.c:96-105
1 2 3 4 5 6 7 8 static u32 drm_sched_available_credits (struct drm_gpu_scheduler *sched) { u32 credits; WARN_ON(check_sub_overflow(sched->credit_limit, atomic_read (&sched->credit_count), &credits)); return credits; }
credit_limit - credit_count = available_credits。初始 credit_count=0,最大 64。
4.2 drm_sched_can_queue sched_main.c:115-134
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static bool drm_sched_can_queue (struct drm_gpu_scheduler *sched, struct drm_sched_entity *entity) { struct drm_sched_job *s_job ; s_job = drm_sched_entity_queue_peek(entity); if (!s_job) return false ; if (s_job->credits > sched->credit_limit) { dev_WARN(sched->dev, "Jobs may not exceed the credit limit, truncate.\n" ); s_job->credits = sched->credit_limit; } return drm_sched_available_credits(sched) >= s_job->credits; }
关键保护 :即使某个 job 声称 credits 超过 credit_limit,调度器也将其截断为 limit。保证永远有前向进展 ——不会出现”需要 65 个 credit 但最多只有 64”的死锁。
4.3 信用流动 1 2 3 4 5 6 7 8 9 用户 push_job → entity SPSC 队列 ↓ 调度器 wake_up → 查看队首 job 的 credits ↓ available_credits >= credits? ├─ YES → 从 entity 弹出 job ,credit_count += credits │ 调用 prepare_job → run_job → 提交给硬件 │ 硬件完成后 → credit_count -= credits(回收) └─ NO → 等待(调度器休眠,某个 job 完成时唤醒)
1 2 3 4 5 6 7 8 9 信用水位: credit_limit (64) ┌────────────────────────────────────────┐ │████████████████████████░░░░░░░░░░░░░░░░│ ← 已用 30 信用 └────────────────────────────────────────┘ credit_count = 30, available = 34 新 job 需要 10 credits → OK 新 job 需要 50 credits → 等待
5. 优先级调度 5.1 优先级定义 DRM scheduler 定义了多个优先级级别:
1 2 3 4 5 6 7 8 enum drm_sched_priority { DRM_SCHED_PRIORITY_MIN, DRM_SCHED_PRIORITY_NORMAL = DRM_SCHED_PRIORITY_MIN, DRM_SCHED_PRIORITY_HIGH_SW, DRM_SCHED_PRIORITY_KERNEL, DRM_SCHED_PRIORITY_HIGH_HW, DRM_SCHED_PRIORITY_COUNT };
数字越大优先级越高:HIGH_HW > KERNEL > HIGH_SW > NORMAL。
5.2 Round-Robin 同一优先级内调度 sched_main.c:136-139:
1 2 3 4 5 static __always_inline bool drm_sched_entity_compare_before ( struct rb_node *a, const struct rb_node *b) { struct drm_sched_entity *ent_a = rb_entry((a), struct drm_sched_entity, rb_tree_node);
当多个 entity 在同一优先级 run queue 中时,调度器使用 RB 树实现 Round-Robin:每次从当前 entity 取一个 job,然后移动到下一个 entity。防止某个 entity 饿死其他 entity。
5.3 调度循环 1 2 3 4 5 6 7 for priority in [HIGH_HW, KERNEL, HIGH_SW, NORMAL]: if sched_rq[priority] 有 entity 且 can_queue: 从当前 entity 取 job 调用 run_job() return 如果所有优先级都无可提交的 job → 调度器睡眠
6. Job 生命周期 6.1 drm_sched_job_init — 初始化 sched_main.c:800-829
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int drm_sched_job_init (struct drm_sched_job *job, struct drm_sched_entity *entity, u32 credits, void *owner, uint64_t drm_client_id) { if (!entity->rq) { dev_err(job->sched->dev, "%s: entity has no rq!\n" , __func__); return -ENOENT; } if (unlikely(!credits)) { pr_err("*ERROR* %s: credits cannot be 0!\n" , __func__); return -EINVAL; } memset (job, 0 , sizeof (*job)); job->entity = entity; job->credits = credits; job->s_fence = drm_sched_fence_alloc(entity, owner, drm_client_id);
初始化流程:
检查 entity 是否有有效 run queue
检查 credits 不为 0
memset 清零 整个 job 结构体——这是防御性编程
分配 s_fence(scheduled fence)和 hw_fence
6.2 drm_sched_entity_push_job — 提交 sched_entity.c:576
1 2 3 4 5 6 7 8 9 10 void drm_sched_entity_push_job (struct drm_sched_job *sched_job) { struct drm_sched_entity *entity = sched_job->entity; bool first; ktime_t submit_ts; trace_drm_sched_job_queue(sched_job, entity); atomic_inc (entity->rq->sched->score); WRITE_ONCE(entity->last_user, current->group_leader);
关键 : push_job 只是把 job 放入 entity 的 SPSC 队列。实际的 run_job 发生在调度器 workqueue 线程中。
6.3 run_job — 硬件提交 驱动实现的 run_job 回调将 job 写入 GPU 寄存器/命令环缓冲区(ring buffer)。返回一个 dma_fence,表示硬件完成信号。
1 2 3 4 5 6 7 8 9 10 11 12 用户态 内核态 GPU 硬件 │ │ │ ├─ ioctl (提交job) ──→│ │ │ ├─ push_job → SPSC queue │ │ ├─ wake_up scheduler │ │ ├─ prepare_job │ │ ├─ run_job ──────────────────────→│ │ │ (写寄存器/ring buffer) │ │ │ ├─ 执行命令 │ │ ├─ 完成! │ ← fence signal ─┤ ← hw_fence signal ───────────┤ │ ├─ free_job(释放资源) │
7. 超时处理与 TDR 7.1 Timeout Detection and Recovery sched_main.c:1317 — drm_sched_init 设置 sched->timeout(jiffies),然后启动一个延迟 workqueue:
每个 job 提交后,记录 job->submit_ts
调度器定期检查 pending_list 中最老的 job
如果 now - submit_ts > timeout → 调用 timedout_job 回调
驱动在 timedout_job 中重置 GPU 引擎或踢出 offending job
7.2 hang_limit sched_main.c:1325:
1 sched->hang_limit = args->hang_limit;
如果连续多次挂死(超过 hang_limit),调度器认为硬件不可恢复,标记整个 scheduler 不可用。
8. drm_sched_init 和 drm_sched_fini 8.1 初始化 sched_main.c:1317-1359
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int drm_sched_init (struct drm_gpu_scheduler *sched, const struct drm_sched_init_args *args) { int i; sched->ops = args->ops; sched->credit_limit = args->credit_limit; sched->name = args->name; sched->timeout = args->timeout; sched->hang_limit = args->hang_limit; sched->timeout_wq = args->timeout_wq ? args->timeout_wq : system_percpu_wq; sched->score = args->score ? args->score : &sched->_score; sched->dev = args->dev; if (args->num_rqs > DRM_SCHED_PRIORITY_COUNT) { dev_err(sched->dev, "%s: num_rqs cannot be greater than DRM_SCHED_PRIORITY_COUNT\n" , __func__); return -EINVAL; } sched->sched_rq = kmalloc_objs(*sched->sched_rq, args->num_rqs, GFP_KERNEL | __GFP_ZERO);
关键步骤:
从 args 复制所有参数
校验 num_rqs 不超过 DRM_SCHED_PRIORITY_COUNT
分配 sched_rq[] 数组,每个 run queue 用 RB 树管理 entity
分配 submit_wq:alloc_ordered_workqueue(行 1349),有序保证 FIFO
启动 timeout delayed work
8.2 销毁 sched_main.c:1420-1448
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void drm_sched_fini (struct drm_gpu_scheduler *sched) { int i; drm_sched_wqueue_stop(sched); for (i = DRM_SCHED_PRIORITY_KERNEL; i < sched->num_rqs; i++) kfree(sched->sched_rq[i]); wake_up_all(&sched->job_scheduled); cancel_delayed_work_sync(&sched->work_tdr); if (sched->ops->cancel_job) drm_sched_cancel_remaining_jobs(sched); if (sched->own_submit_wq) destroy_workqueue(sched->submit_wq); sched->ready = false ; if (!list_empty(&sched->pending_list)) dev_warn(sched->dev, "Tearing down scheduler while jobs are pending!\n" ); }
防御性警告 (行 1445-1446):如果销毁时 pending_list 非空,说明有 job 未完成——这通常是 bug。
9. NVIDIA AI Infra 中的角色 在 NVIDIA AI Infra 场景中,DRM Scheduler 直接管理 CUDA kernel launch 和 GPU 计算命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 用户: cudaLaunchKernel(myKernel , grid , block ) ↓ libcuda.so → ioctl 到内核 ↓ drm_sched_job_init(job , entity , credits =1, ... ) ↓ drm_sched_entity_push_job(job ) ← 放入 SPSC 队列 ↓ 调度器 workqueue: prepare_job → 检查 fence 依赖 run_job → 写入 GPU 命令环缓冲区 ↓ GPU 执行 kernel 计算 ↓ hw_fence signal → job 释放 → credit 回收
AI 训练中,数千个 kernel 在多个 CUDA streams 上并发。DRM Scheduler 确保:
Compute 优先级 高于 GFX 优先级
大 kernel (长运行时间)不会阻塞小 kernel
超时 kernel (死循环)被 TDR 踢出,不影响同一 GPU 上的其他进程
下一篇文章 第8篇:GPU→RDMA 零拷贝桥梁:umem_dmabuf.c
简介:ib_umem_dmabuf_get_pinned 如何接收 GPU 的 dmabuf fd 并转换为 RDMA MR 所需的 SG table。