linux/drivers/gpu/drm/scheduler/sched_rq.c
Tvrtko Ursulin 16e7698bc0 drm/sched: Embed run queue singleton into the scheduler
Now that the run queue to scheduler relationship is always 1:1 we can
embed it (the run queue) directly in the scheduler struct and save on
some allocation error handling code and such.

Signed-off-by: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>
Cc: Christian König <christian.koenig@amd.com>
Cc: Danilo Krummrich <dakr@kernel.org>
Cc: Matthew Brost <matthew.brost@intel.com>
Cc: Philipp Stanner <phasta@kernel.org>
Acked-by: Danilo Krummrich <dakr@kernel.org>
Tested-by: Vitaly Prosyak <vitaly.prosyak@amd.com>
Signed-off-by: Philipp Stanner <phasta@kernel.org>
Link: https://patch.msgid.link/20260417103744.76020-15-tvrtko.ursulin@igalia.com
2026-04-17 14:43:29 +02:00

384 lines
10 KiB
C

// SPDX-License-Identifier: MIT
/* Copyright 2015 Advanced Micro Devices, Inc. */
/* Copyright (c) 2025 Valve Corporation */
#include <linux/rbtree.h>
#include <drm/drm_print.h>
#include <drm/gpu_scheduler.h>
#include "sched_internal.h"
static __always_inline bool
drm_sched_entity_compare_before(struct rb_node *a, const struct rb_node *b)
{
struct drm_sched_entity *ea =
rb_entry((a), struct drm_sched_entity, rb_tree_node);
struct drm_sched_entity *eb =
rb_entry((b), struct drm_sched_entity, rb_tree_node);
return ktime_before(ea->oldest_job_waiting, eb->oldest_job_waiting);
}
static void drm_sched_rq_update_prio(struct drm_sched_rq *rq)
{
enum drm_sched_priority prio = DRM_SCHED_PRIORITY_INVALID;
struct rb_node *rb;
lockdep_assert_held(&rq->lock);
rb = rb_first_cached(&rq->rb_tree_root);
if (rb) {
struct drm_sched_entity *entity =
rb_entry(rb, typeof(*entity), rb_tree_node);
/*
* The normal locking order is entity then run-queue so taking
* the entity lock here would be a locking inversion for the
* case when the current head of the run-queue is different from
* the one we already have locked. The unlocked read is fine
* though, because if the priority had just changed it is no big
* deal for our algorithm, but just a transient reachable only
* by drivers with userspace dynamic priority changes API. Equal
* in effect to the priority change becoming visible a few
* instructions later.
*/
prio = READ_ONCE(entity->priority);
}
rq->head_prio = prio;
}
static void drm_sched_rq_remove_tree_locked(struct drm_sched_entity *entity,
struct drm_sched_rq *rq)
{
lockdep_assert_held(&entity->lock);
lockdep_assert_held(&rq->lock);
if (!RB_EMPTY_NODE(&entity->rb_tree_node)) {
rb_erase_cached(&entity->rb_tree_node, &rq->rb_tree_root);
RB_CLEAR_NODE(&entity->rb_tree_node);
drm_sched_rq_update_prio(rq);
}
}
static void drm_sched_rq_update_tree_locked(struct drm_sched_entity *entity,
struct drm_sched_rq *rq,
ktime_t ts)
{
/*
* Both locks need to be grabbed, one to protect from entity->rq change
* for entity from within concurrent drm_sched_entity_select_rq and the
* other to update the rb tree structure.
*/
lockdep_assert_held(&entity->lock);
lockdep_assert_held(&rq->lock);
drm_sched_rq_remove_tree_locked(entity, rq);
entity->oldest_job_waiting = ts;
rb_add_cached(&entity->rb_tree_node, &rq->rb_tree_root,
drm_sched_entity_compare_before);
drm_sched_rq_update_prio(rq);
}
/**
* drm_sched_rq_init - initialize a given run queue struct
* @rq: scheduler run queue
*
* Initializes a scheduler runqueue.
*/
void drm_sched_rq_init(struct drm_sched_rq *rq)
{
spin_lock_init(&rq->lock);
INIT_LIST_HEAD(&rq->entities);
rq->rb_tree_root = RB_ROOT_CACHED;
rq->head_prio = DRM_SCHED_PRIORITY_INVALID;
}
/*
* Core part of the CFS-like algorithm is that the virtual runtime of lower
* priority tasks should grow quicker than the higher priority ones, so that
* when we then schedule entities with the aim of keeping their accumulated
* virtual time balanced, we can approach fair distribution of GPU time.
*
* For converting the real GPU time into virtual we pick some multipliers with
* the idea to achieve the following GPU time distribution:
*
* - Kernel priority gets roughly 2x GPU time compared to high.
* - High gets ~4x relative to normal.
* - Normal gets ~8x relative to low.
*/
static const unsigned int vruntime_shift[] = {
[DRM_SCHED_PRIORITY_KERNEL] = 1,
[DRM_SCHED_PRIORITY_HIGH] = 2,
[DRM_SCHED_PRIORITY_NORMAL] = 4,
[DRM_SCHED_PRIORITY_LOW] = 7,
};
static ktime_t
drm_sched_rq_get_min_vruntime(struct drm_sched_rq *rq)
{
ktime_t vruntime = 0;
struct rb_node *rb;
lockdep_assert_held(&rq->lock);
rb = rb_first_cached(&rq->rb_tree_root);
if (rb) {
struct drm_sched_entity *entity =
rb_entry(rb, typeof(*entity), rb_tree_node);
struct drm_sched_entity_stats *stats = entity->stats;
spin_lock(&stats->lock);
vruntime = stats->vruntime;
spin_unlock(&stats->lock);
}
return vruntime;
}
static void
drm_sched_entity_save_vruntime(struct drm_sched_entity *entity,
ktime_t min_vruntime)
{
struct drm_sched_entity_stats *stats = entity->stats;
ktime_t vruntime;
spin_lock(&stats->lock);
vruntime = stats->vruntime;
if (min_vruntime && vruntime > min_vruntime)
vruntime = ktime_sub(vruntime, min_vruntime);
else
vruntime = 0;
stats->vruntime = vruntime;
spin_unlock(&stats->lock);
}
static ktime_t
drm_sched_entity_restore_vruntime(struct drm_sched_entity *entity,
ktime_t min_vruntime,
enum drm_sched_priority rq_prio)
{
struct drm_sched_entity_stats *stats = entity->stats;
struct drm_gpu_scheduler *sched =
container_of(entity->rq, typeof(*sched), rq);
enum drm_sched_priority prio = entity->priority;
unsigned long avg_us, sched_avg_us;
ktime_t vruntime;
BUILD_BUG_ON(DRM_SCHED_PRIORITY_NORMAL < DRM_SCHED_PRIORITY_HIGH);
spin_lock(&stats->lock);
vruntime = stats->vruntime;
avg_us = ewma_drm_sched_avgtime_read(&stats->avg_job_us);
/*
* Unlocked read of the scheduler average is fine since it is just
* heuristics and data type is a natural word size.
*/
sched_avg_us = ewma_drm_sched_avgtime_read(&sched->avg_job_us);
/*
* Special handling for entities which were picked from the top of the
* queue and are now re-joining the top with another one already there.
*/
if (!vruntime && rq_prio != DRM_SCHED_PRIORITY_INVALID) {
if (prio > rq_prio) {
/*
* Lower priority should not overtake higher when re-
* joining at the top of the queue so push it back
* somewhere behind the "middle" of the run-queue,
* proportional to the scheduler and entity average job
* durations.
*/
vruntime = us_to_ktime((1 + avg_us + sched_avg_us) <<
vruntime_shift[prio]);
} else if (prio < rq_prio) {
/*
* Higher priority can go first.
*/
vruntime = -ns_to_ktime(rq_prio - prio);
} else {
/* Favour entity with shorter jobs (interactivity). */
if (avg_us <= sched_avg_us)
vruntime = -ns_to_ktime(1);
else
vruntime = ns_to_ktime(1);
}
}
/*
* Restore saved relative position in the queue.
*/
vruntime = ktime_add(min_vruntime, vruntime);
stats->vruntime = vruntime;
spin_unlock(&stats->lock);
return vruntime;
}
static ktime_t drm_sched_entity_update_vruntime(struct drm_sched_entity *entity)
{
struct drm_sched_entity_stats *stats = entity->stats;
ktime_t runtime, prev;
spin_lock(&stats->lock);
prev = stats->prev_runtime;
runtime = stats->runtime;
stats->prev_runtime = runtime;
runtime = ktime_add_ns(stats->vruntime,
ktime_to_ns(ktime_sub(runtime, prev)) <<
vruntime_shift[entity->priority]);
stats->vruntime = runtime;
spin_unlock(&stats->lock);
return runtime;
}
/**
* drm_sched_rq_add_entity - add an entity
* @entity: scheduler entity
*
* Adds a scheduler entity to the run queue.
*
* Return: DRM scheduler selected to handle this entity or NULL if entity has
* been stopped and cannot be submitted to.
*/
struct drm_gpu_scheduler *
drm_sched_rq_add_entity(struct drm_sched_entity *entity)
{
struct drm_gpu_scheduler *sched;
struct drm_sched_rq *rq;
ktime_t ts;
/* Add the entity to the run queue */
spin_lock(&entity->lock);
if (entity->stopped) {
spin_unlock(&entity->lock);
DRM_ERROR("Trying to push to a killed entity\n");
return NULL;
}
rq = entity->rq;
sched = container_of(rq, typeof(*sched), rq);
spin_lock(&rq->lock);
if (list_empty(&entity->list)) {
atomic_inc(sched->score);
list_add_tail(&entity->list, &rq->entities);
}
ts = drm_sched_rq_get_min_vruntime(rq);
ts = drm_sched_entity_restore_vruntime(entity, ts, rq->head_prio);
drm_sched_rq_update_tree_locked(entity, rq, ts);
spin_unlock(&rq->lock);
spin_unlock(&entity->lock);
return sched;
}
/**
* drm_sched_rq_remove_entity - remove an entity
* @rq: scheduler run queue
* @entity: scheduler entity
*
* Removes a scheduler entity from the run queue.
*/
void drm_sched_rq_remove_entity(struct drm_sched_rq *rq,
struct drm_sched_entity *entity)
{
struct drm_gpu_scheduler *sched = container_of(rq, typeof(*sched), rq);
lockdep_assert_held(&entity->lock);
if (list_empty(&entity->list))
return;
spin_lock(&rq->lock);
atomic_dec(sched->score);
list_del_init(&entity->list);
drm_sched_rq_remove_tree_locked(entity, rq);
spin_unlock(&rq->lock);
}
/**
* drm_sched_rq_pop_entity - pops an entity
* @entity: scheduler entity
*
* To be called every time after a job is popped from the entity.
*/
void drm_sched_rq_pop_entity(struct drm_sched_entity *entity)
{
struct drm_sched_job *next_job;
struct drm_sched_rq *rq;
/*
* Update the entity's location in the min heap according to
* the timestamp of the next job, if any.
*/
spin_lock(&entity->lock);
rq = entity->rq;
spin_lock(&rq->lock);
next_job = drm_sched_entity_queue_peek(entity);
if (next_job) {
ktime_t ts;
ts = drm_sched_entity_update_vruntime(entity);
drm_sched_rq_update_tree_locked(entity, rq, ts);
} else {
ktime_t min_vruntime;
drm_sched_rq_remove_tree_locked(entity, rq);
min_vruntime = drm_sched_rq_get_min_vruntime(rq);
drm_sched_entity_save_vruntime(entity, min_vruntime);
}
spin_unlock(&rq->lock);
spin_unlock(&entity->lock);
}
/**
* drm_sched_select_entity - Select an entity which provides a job to run
* @sched: the gpu scheduler
*
* Find oldest waiting ready entity.
*
* Return an entity if one is found; return an error-pointer (!NULL) if an
* entity was ready, but the scheduler had insufficient credits to accommodate
* its job; return NULL, if no ready entity was found.
*/
struct drm_sched_entity *
drm_sched_select_entity(struct drm_gpu_scheduler *sched)
{
struct drm_sched_rq *rq = &sched->rq;
struct rb_node *rb;
spin_lock(&rq->lock);
for (rb = rb_first_cached(&rq->rb_tree_root); rb; rb = rb_next(rb)) {
struct drm_sched_entity *entity;
entity = rb_entry(rb, struct drm_sched_entity, rb_tree_node);
if (drm_sched_entity_is_ready(entity)) {
/* If we can't queue yet, preserve the current entity in
* terms of fairness.
*/
if (!drm_sched_can_queue(sched, entity)) {
spin_unlock(&rq->lock);
return ERR_PTR(-ENOSPC);
}
reinit_completion(&entity->entity_idle);
break;
}
}
spin_unlock(&rq->lock);
return rb ? rb_entry(rb, struct drm_sched_entity, rb_tree_node) : NULL;
}