Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,12 @@
CONFIG_KUNIT=y
CONFIG_DRM=y
CONFIG_DRM_SCHED_KUNIT_TEST=y
CONFIG_EXPERT=y
CONFIG_DEBUG_SPINLOCK=y
CONFIG_DEBUG_MUTEXES=y
CONFIG_DEBUG_ATOMIC_SLEEP=y
CONFIG_LOCK_DEBUGGING_SUPPORT=y
CONFIG_PROVE_LOCKING=y
CONFIG_LOCKDEP=y
CONFIG_DEBUG_LOCKDEP=y
CONFIG_DEBUG_LIST=y
@@ -0,0 +1,27 @@
#
# Copyright 2017 Advanced Micro Devices, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
#
gpu-sched-y := sched_main.o sched_fence.o sched_entity.o sched_rq.o
obj-$(CONFIG_DRM_SCHED) += gpu-sched.o
obj-$(CONFIG_DRM_SCHED_KUNIT_TEST) += tests/
@@ -0,0 +1,159 @@
/*
* Copyright 2017 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
#if !defined(_GPU_SCHED_TRACE_H_) || defined(TRACE_HEADER_MULTI_READ)
#define _GPU_SCHED_TRACE_H_
#include <linux/stringify.h>
#include <linux/types.h>
#include <linux/tracepoint.h>
#undef TRACE_SYSTEM
#define TRACE_SYSTEM gpu_scheduler
#define TRACE_INCLUDE_FILE gpu_scheduler_trace
/**
* DOC: uAPI trace events
*
* ``drm_sched_job_queue``, ``drm_sched_job_run``, ``drm_sched_job_add_dep``,
* ``drm_sched_job_done`` and ``drm_sched_job_unschedulable`` are considered
* stable uAPI.
*
* Common trace events attributes:
*
* * ``dev`` - the dev_name() of the device running the job.
*
* * ``ring`` - the hardware ring running the job. Together with ``dev`` it
* uniquely identifies where the job is going to be executed.
*
* * ``fence`` - the &struct dma_fence.context and the &struct dma_fence.seqno of
* &struct drm_sched_fence.finished
*
* All the events depends on drm_sched_job_arm() having been called already for
* the job because they use &struct drm_sched_job.sched or
* &struct drm_sched_job.s_fence.
*/
DECLARE_EVENT_CLASS(drm_sched_job,
TP_PROTO(struct drm_sched_job *sched_job, struct drm_sched_entity *entity),
TP_ARGS(sched_job, entity),
TP_STRUCT__entry(
__string(name, sched_job->sched->name)
__field(u32, job_count)
__field(int, hw_job_count)
__string(dev, dev_name(sched_job->sched->dev))
__field(u64, fence_context)
__field(u64, fence_seqno)
__field(u64, client_id)
),
TP_fast_assign(
__assign_str(name);
__entry->job_count = spsc_queue_count(&entity->job_queue);
__entry->hw_job_count = atomic_read(
&sched_job->sched->credit_count);
__assign_str(dev);
__entry->fence_context = sched_job->s_fence->finished.context;
__entry->fence_seqno = sched_job->s_fence->finished.seqno;
__entry->client_id = sched_job->s_fence->drm_client_id;
),
TP_printk("dev=%s, fence=%llu:%llu, ring=%s, job count:%u, hw job count:%d, client_id:%llu",
__get_str(dev),
__entry->fence_context, __entry->fence_seqno, __get_str(name),
__entry->job_count, __entry->hw_job_count, __entry->client_id)
);
DEFINE_EVENT(drm_sched_job, drm_sched_job_queue,
TP_PROTO(struct drm_sched_job *sched_job, struct drm_sched_entity *entity),
TP_ARGS(sched_job, entity)
);
DEFINE_EVENT(drm_sched_job, drm_sched_job_run,
TP_PROTO(struct drm_sched_job *sched_job, struct drm_sched_entity *entity),
TP_ARGS(sched_job, entity)
);
TRACE_EVENT(drm_sched_job_done,
TP_PROTO(struct drm_sched_fence *fence),
TP_ARGS(fence),
TP_STRUCT__entry(
__field(u64, fence_context)
__field(u64, fence_seqno)
),
TP_fast_assign(
__entry->fence_context = fence->finished.context;
__entry->fence_seqno = fence->finished.seqno;
),
TP_printk("fence=%llu:%llu signaled",
__entry->fence_context, __entry->fence_seqno)
);
TRACE_EVENT(drm_sched_job_add_dep,
TP_PROTO(struct drm_sched_job *sched_job, struct dma_fence *fence),
TP_ARGS(sched_job, fence),
TP_STRUCT__entry(
__field(u64, fence_context)
__field(u64, fence_seqno)
__field(u64, ctx)
__field(u64, seqno)
),
TP_fast_assign(
__entry->fence_context = sched_job->s_fence->finished.context;
__entry->fence_seqno = sched_job->s_fence->finished.seqno;
__entry->ctx = fence->context;
__entry->seqno = fence->seqno;
),
TP_printk("fence=%llu:%llu depends on fence=%llu:%llu",
__entry->fence_context, __entry->fence_seqno,
__entry->ctx, __entry->seqno)
);
TRACE_EVENT(drm_sched_job_unschedulable,
TP_PROTO(struct drm_sched_job *sched_job, struct dma_fence *fence),
TP_ARGS(sched_job, fence),
TP_STRUCT__entry(
__field(u64, fence_context)
__field(u64, fence_seqno)
__field(u64, ctx)
__field(u64, seqno)
),
TP_fast_assign(
__entry->fence_context = sched_job->s_fence->finished.context;
__entry->fence_seqno = sched_job->s_fence->finished.seqno;
__entry->ctx = fence->context;
__entry->seqno = fence->seqno;
),
TP_printk("fence=%llu:%llu depends on unsignalled fence=%llu:%llu",
__entry->fence_context, __entry->fence_seqno,
__entry->ctx, __entry->seqno)
);
#endif /* _GPU_SCHED_TRACE_H_ */
/* This part must be outside protection */
#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH ../../drivers/gpu/drm/scheduler
#include <trace/define_trace.h>
@@ -0,0 +1,668 @@
/*
* Copyright 2015 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
#include <linux/export.h>
#include <linux/slab.h>
#include <linux/completion.h>
#include <drm/drm_print.h>
#include <drm/gpu_scheduler.h>
#include "sched_internal.h"
#include "gpu_scheduler_trace.h"
/**
* drm_sched_entity_stats_release - Entity stats kref release function
* @kref: Entity stats embedded kref pointer
*/
void drm_sched_entity_stats_release(struct kref *kref)
{
struct drm_sched_entity_stats *stats =
container_of(kref, typeof(*stats), kref);
kfree(stats);
}
/**
* drm_sched_entity_stats_new - Allocate a new struct drm_sched_entity_stats object
*
* Return: Pointer to newly allocated struct drm_sched_entity_stats object.
*/
static struct drm_sched_entity_stats *drm_sched_entity_stats_new(void)
{
struct drm_sched_entity_stats *stats;
stats = kzalloc_obj(*stats);
if (!stats)
return NULL;
kref_init(&stats->kref);
spin_lock_init(&stats->lock);
ewma_drm_sched_avgtime_init(&stats->avg_job_us);
return stats;
}
/**
* drm_sched_entity_stats_job_add_gpu_time - Account job execution time to entity
* @job: Scheduler job to account.
*
* Accounts the execution time of @job to its respective entity stats object.
*
* Return: Job's real duration in micro seconds.
*/
ktime_t drm_sched_entity_stats_job_add_gpu_time(struct drm_sched_job *job)
{
struct drm_sched_entity_stats *stats = job->entity_stats;
struct drm_sched_fence *s_fence = job->s_fence;
ktime_t start, end, duration;
start = dma_fence_timestamp(&s_fence->scheduled);
end = dma_fence_timestamp(&s_fence->finished);
duration = ktime_sub(end, start);
spin_lock(&stats->lock);
stats->runtime = ktime_add(stats->runtime, duration);
ewma_drm_sched_avgtime_add(&stats->avg_job_us, ktime_to_us(duration));
spin_unlock(&stats->lock);
return duration;
}
/**
* drm_sched_entity_init - Init a context entity used by scheduler when
* submit to HW ring.
*
* @entity: scheduler entity to init
* @priority: priority of the entity
* @sched_list: the list of drm scheds on which jobs from this
* entity can be submitted
* @num_sched_list: number of drm sched in sched_list
* @guilty: atomic_t set to 1 when a job on this queue
* is found to be guilty causing a timeout
*
* Note that the &sched_list must have at least one element to schedule the entity.
*
* For changing @priority later on at runtime see
* drm_sched_entity_set_priority(). For changing the set of schedulers
* @sched_list at runtime see drm_sched_entity_modify_sched().
*
* An entity is cleaned up by calling drm_sched_entity_fini(). See also
* drm_sched_entity_destroy().
*
* Returns 0 on success or a negative error code on failure.
*/
int drm_sched_entity_init(struct drm_sched_entity *entity,
enum drm_sched_priority priority,
struct drm_gpu_scheduler **sched_list,
unsigned int num_sched_list,
atomic_t *guilty)
{
if (!entity || !sched_list || !num_sched_list || !sched_list[0])
return -EINVAL;
memset(entity, 0, sizeof(struct drm_sched_entity));
entity->stats = drm_sched_entity_stats_new();
if (!entity->stats)
return -ENOMEM;
INIT_LIST_HEAD(&entity->list);
entity->rq = NULL;
entity->guilty = guilty;
entity->priority = priority;
entity->last_user = current->group_leader;
entity->rq_priority = drm_sched_policy == DRM_SCHED_POLICY_FAIR ?
DRM_SCHED_PRIORITY_KERNEL : priority;
entity->num_sched_list = num_sched_list;
entity->sched_list = num_sched_list > 1 ? sched_list : NULL;
RCU_INIT_POINTER(entity->last_scheduled, NULL);
RB_CLEAR_NODE(&entity->rb_tree_node);
if (!sched_list[0]->sched_rq) {
/* Since every entry covered by num_sched_list
* should be non-NULL and therefore we warn drivers
* not to do this and to fix their DRM calling order.
*/
pr_warn("%s: called with uninitialized scheduler\n", __func__);
} else {
enum drm_sched_priority p = entity->priority;
/*
* The "priority" of an entity cannot exceed the number of
* run-queues of a scheduler. Protect against num_rqs being 0,
* by converting to signed. Choose the lowest priority
* available.
*/
if (p >= sched_list[0]->num_user_rqs) {
dev_err(sched_list[0]->dev, "entity with out-of-bounds priority:%u num_user_rqs:%u\n",
p, sched_list[0]->num_user_rqs);
p = max_t(s32,
(s32)sched_list[0]->num_user_rqs - 1,
(s32)DRM_SCHED_PRIORITY_KERNEL);
entity->priority = p;
}
entity->rq = sched_list[0]->sched_rq[entity->rq_priority];
}
init_completion(&entity->entity_idle);
/* We start in an idle state. */
complete_all(&entity->entity_idle);
spin_lock_init(&entity->lock);
spsc_queue_init(&entity->job_queue);
atomic_set(&entity->fence_seq, 0);
entity->fence_context = dma_fence_context_alloc(2);
return 0;
}
EXPORT_SYMBOL(drm_sched_entity_init);
/**
* drm_sched_entity_modify_sched - Modify sched of an entity
* @entity: scheduler entity to init
* @sched_list: the list of new drm scheds which will replace
* existing entity->sched_list
* @num_sched_list: number of drm sched in sched_list
*
* Note that this must be called under the same common lock for @entity as
* drm_sched_job_arm() and drm_sched_entity_push_job(), or the driver needs to
* guarantee through some other means that this is never called while new jobs
* can be pushed to @entity.
*/
void drm_sched_entity_modify_sched(struct drm_sched_entity *entity,
struct drm_gpu_scheduler **sched_list,
unsigned int num_sched_list)
{
WARN_ON(!num_sched_list || !sched_list);
spin_lock(&entity->lock);
entity->sched_list = sched_list;
entity->num_sched_list = num_sched_list;
spin_unlock(&entity->lock);
}
EXPORT_SYMBOL(drm_sched_entity_modify_sched);
static bool drm_sched_entity_is_idle(struct drm_sched_entity *entity)
{
rmb(); /* for list_empty to work without lock */
if (list_empty(&entity->list) ||
spsc_queue_count(&entity->job_queue) == 0 ||
entity->stopped)
return true;
return false;
}
/**
* drm_sched_entity_error - return error of last scheduled job
* @entity: scheduler entity to check
*
* Opportunistically return the error of the last scheduled job. Result can
* change any time when new jobs are pushed to the hw.
*/
int drm_sched_entity_error(struct drm_sched_entity *entity)
{
struct dma_fence *fence;
int r;
rcu_read_lock();
fence = rcu_dereference(entity->last_scheduled);
r = fence ? fence->error : 0;
rcu_read_unlock();
return r;
}
EXPORT_SYMBOL(drm_sched_entity_error);
static void drm_sched_entity_kill_jobs_cb(struct dma_fence *f,
struct dma_fence_cb *cb);
static void drm_sched_entity_kill_jobs_work(struct work_struct *wrk)
{
struct drm_sched_job *job = container_of(wrk, typeof(*job), work);
struct dma_fence *f;
unsigned long index;
/* Wait for all dependencies to avoid data corruptions */
xa_for_each(&job->dependencies, index, f) {
struct drm_sched_fence *s_fence = to_drm_sched_fence(f);
if (s_fence && f == &s_fence->scheduled) {
/* The dependencies array had a reference on the scheduled
* fence, and the finished fence refcount might have
* dropped to zero. Use dma_fence_get_rcu() so we get
* a NULL fence in that case.
*/
f = dma_fence_get_rcu(&s_fence->finished);
/* Now that we have a reference on the finished fence,
* we can release the reference the dependencies array
* had on the scheduled fence.
*/
dma_fence_put(&s_fence->scheduled);
}
xa_erase(&job->dependencies, index);
if (f && !dma_fence_add_callback(f, &job->finish_cb,
drm_sched_entity_kill_jobs_cb))
return;
dma_fence_put(f);
}
drm_sched_fence_scheduled(job->s_fence, NULL);
drm_sched_fence_finished(job->s_fence, -ESRCH);
WARN_ON(job->s_fence->parent);
job->sched->ops->free_job(job);
}
/* Signal the scheduler finished fence when the entity in question is killed. */
static void drm_sched_entity_kill_jobs_cb(struct dma_fence *f,
struct dma_fence_cb *cb)
{
struct drm_sched_job *job = container_of(cb, struct drm_sched_job,
finish_cb);
dma_fence_put(f);
INIT_WORK(&job->work, drm_sched_entity_kill_jobs_work);
schedule_work(&job->work);
}
/**
* drm_sched_entity_kill - kill an entity's pending jobs and remove it
* @entity: the entity to remove
*
* Removes the entity from the scheduler's run queue and kills all pending jobs.
*
* This function should be used over drm_sched_entity_flush() if it is not
* desired to actually wait for all pending jobs to finish.
*/
void drm_sched_entity_kill(struct drm_sched_entity *entity)
{
struct drm_sched_job *job;
struct dma_fence *prev;
if (!entity->rq)
return;
spin_lock(&entity->lock);
entity->stopped = true;
drm_sched_rq_remove_entity(entity->rq, entity);
spin_unlock(&entity->lock);
/* Make sure this entity is not used by the scheduler at the moment */
wait_for_completion(&entity->entity_idle);
/* The entity is guaranteed to not be used by the scheduler */
prev = rcu_dereference_check(entity->last_scheduled, true);
dma_fence_get(prev);
while ((job = drm_sched_entity_queue_pop(entity))) {
struct drm_sched_fence *s_fence = job->s_fence;
dma_fence_get(&s_fence->finished);
if (!prev ||
dma_fence_add_callback(prev, &job->finish_cb,
drm_sched_entity_kill_jobs_cb)) {
/*
* Adding callback above failed.
* dma_fence_put() checks for NULL.
*/
dma_fence_put(prev);
drm_sched_entity_kill_jobs_cb(NULL, &job->finish_cb);
}
prev = &s_fence->finished;
}
dma_fence_put(prev);
}
EXPORT_SYMBOL(drm_sched_entity_kill);
/**
* drm_sched_entity_flush - Flush a context entity
*
* @entity: scheduler entity
* @timeout: time to wait in for Q to become empty in jiffies.
*
* Splitting drm_sched_entity_fini() into two functions, The first one does the
* waiting, removes the entity from the runqueue and returns an error when the
* process was killed.
*
* Returns the remaining time in jiffies left from the input timeout
*/
long drm_sched_entity_flush(struct drm_sched_entity *entity, long timeout)
{
struct drm_gpu_scheduler *sched;
struct task_struct *last_user;
long ret = timeout;
if (!entity->rq)
return 0;
sched = entity->rq->sched;
/*
* The client will not queue more jobs during this fini - consume
* existing queued ones, or discard them on SIGKILL.
*/
if (current->flags & PF_EXITING) {
if (timeout)
ret = wait_event_timeout(
sched->job_scheduled,
drm_sched_entity_is_idle(entity),
timeout);
} else {
wait_event_killable(sched->job_scheduled,
drm_sched_entity_is_idle(entity));
}
/* For a killed process disallow further enqueueing of jobs. */
last_user = cmpxchg(&entity->last_user, current->group_leader, NULL);
if (last_user == current->group_leader &&
(current->flags & PF_EXITING) && (current->exit_code == SIGKILL))
drm_sched_entity_kill(entity);
return ret;
}
EXPORT_SYMBOL(drm_sched_entity_flush);
/**
* drm_sched_entity_fini - Destroy a context entity
*
* @entity: scheduler entity
*
* Cleanups up @entity which has been initialized by drm_sched_entity_init().
*
* If there are potentially job still in flight or getting newly queued
* drm_sched_entity_flush() must be called first. This function then goes over
* the entity and signals all jobs with an error code if the process was killed.
*/
void drm_sched_entity_fini(struct drm_sched_entity *entity)
{
/*
* If consumption of existing jobs wasn't completed forcefully remove
* them. Also makes sure that the scheduler won't touch this entity any
* more.
*/
drm_sched_entity_kill(entity);
if (entity->dependency) {
dma_fence_remove_callback(entity->dependency, &entity->cb);
dma_fence_put(entity->dependency);
entity->dependency = NULL;
}
dma_fence_put(rcu_dereference_check(entity->last_scheduled, true));
RCU_INIT_POINTER(entity->last_scheduled, NULL);
drm_sched_entity_stats_put(entity->stats);
}
EXPORT_SYMBOL(drm_sched_entity_fini);
/**
* drm_sched_entity_destroy - Destroy a context entity
* @entity: scheduler entity
*
* Calls drm_sched_entity_flush() and drm_sched_entity_fini() as a
* convenience wrapper.
*/
void drm_sched_entity_destroy(struct drm_sched_entity *entity)
{
drm_sched_entity_flush(entity, MAX_WAIT_SCHED_ENTITY_Q_EMPTY);
drm_sched_entity_fini(entity);
}
EXPORT_SYMBOL(drm_sched_entity_destroy);
/*
* drm_sched_entity_wakeup - callback to clear the entity's dependency and
* wake up the scheduler
*/
static void drm_sched_entity_wakeup(struct dma_fence *f,
struct dma_fence_cb *cb)
{
struct drm_sched_entity *entity =
container_of(cb, struct drm_sched_entity, cb);
entity->dependency = NULL;
dma_fence_put(f);
drm_sched_wakeup(entity->rq->sched);
}
/**
* drm_sched_entity_set_priority - Sets priority of the entity
*
* @entity: scheduler entity
* @priority: scheduler priority
*
* Update the priority of runqueues used for the entity.
*/
void drm_sched_entity_set_priority(struct drm_sched_entity *entity,
enum drm_sched_priority priority)
{
spin_lock(&entity->lock);
entity->priority = priority;
spin_unlock(&entity->lock);
}
EXPORT_SYMBOL(drm_sched_entity_set_priority);
/*
* Add a callback to the current dependency of the entity to wake up the
* scheduler when the entity becomes available.
*/
static bool drm_sched_entity_add_dependency_cb(struct drm_sched_entity *entity,
struct drm_sched_job *sched_job)
{
struct drm_gpu_scheduler *sched = entity->rq->sched;
struct dma_fence *fence = entity->dependency;
struct drm_sched_fence *s_fence;
if (fence->context == entity->fence_context ||
fence->context == entity->fence_context + 1) {
/*
* Fence is a scheduled/finished fence from a job
* which belongs to the same entity, we can ignore
* fences from ourself
*/
dma_fence_put(entity->dependency);
return false;
}
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)) {
/*
* Fence is from the same scheduler, only need to wait for
* it to be scheduled
*/
fence = dma_fence_get(&s_fence->scheduled);
dma_fence_put(entity->dependency);
entity->dependency = fence;
}
if (trace_drm_sched_job_unschedulable_enabled() &&
!test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &entity->dependency->flags))
trace_drm_sched_job_unschedulable(sched_job, entity->dependency);
if (!dma_fence_add_callback(entity->dependency, &entity->cb,
drm_sched_entity_wakeup))
return true;
dma_fence_put(entity->dependency);
return false;
}
static struct dma_fence *
drm_sched_job_dependency(struct drm_sched_job *job,
struct drm_sched_entity *entity)
{
struct dma_fence *f;
/* We keep the fence around, so we can iterate over all dependencies
* in drm_sched_entity_kill_jobs_cb() to ensure all deps are signaled
* before killing the job.
*/
f = xa_load(&job->dependencies, job->last_dependency);
if (f) {
job->last_dependency++;
return dma_fence_get(f);
}
if (job->sched->ops->prepare_job)
return job->sched->ops->prepare_job(job, entity);
return NULL;
}
struct drm_sched_job *drm_sched_entity_pop_job(struct drm_sched_entity *entity)
{
struct drm_sched_job *sched_job;
sched_job = drm_sched_entity_queue_peek(entity);
if (!sched_job)
return NULL;
while ((entity->dependency =
drm_sched_job_dependency(sched_job, entity))) {
if (drm_sched_entity_add_dependency_cb(entity, sched_job))
return NULL;
}
/* skip jobs from entity that marked guilty */
if (entity->guilty && atomic_read(entity->guilty))
dma_fence_set_error(&sched_job->s_fence->finished, -ECANCELED);
dma_fence_put(rcu_dereference_check(entity->last_scheduled, true));
rcu_assign_pointer(entity->last_scheduled,
dma_fence_get(&sched_job->s_fence->finished));
/*
* If the queue is empty we allow drm_sched_entity_select_rq() to
* locklessly access ->last_scheduled. This only works if we set the
* pointer before we dequeue and if we a write barrier here.
*/
smp_wmb();
spsc_queue_pop(&entity->job_queue);
drm_sched_rq_pop_entity(entity);
/* Jobs and entities might have different lifecycles. Since we're
* removing the job from the entities queue, set the jobs entity pointer
* to NULL to prevent any future access of the entity through this job.
*/
sched_job->entity = NULL;
return sched_job;
}
void drm_sched_entity_select_rq(struct drm_sched_entity *entity)
{
struct dma_fence *fence;
struct drm_gpu_scheduler *sched;
struct drm_sched_rq *rq;
/* single possible engine and already selected */
if (!entity->sched_list)
return;
/* queue non-empty, stay on the same engine */
if (spsc_queue_count(&entity->job_queue))
return;
/*
* Only when the queue is empty are we guaranteed that
* drm_sched_run_job_work() cannot change entity->last_scheduled. To
* enforce ordering we need a read barrier here. See
* drm_sched_entity_pop_job() for the other side.
*/
smp_rmb();
fence = rcu_dereference_check(entity->last_scheduled, true);
/* stay on the same engine if the previous job hasn't finished */
if (fence && !dma_fence_is_signaled(fence))
return;
spin_lock(&entity->lock);
sched = drm_sched_pick_best(entity->sched_list, entity->num_sched_list);
rq = sched ? sched->sched_rq[entity->rq_priority] : NULL;
if (rq != entity->rq) {
drm_sched_rq_remove_entity(entity->rq, entity);
entity->rq = rq;
}
if (entity->num_sched_list == 1)
entity->sched_list = NULL;
spin_unlock(&entity->lock);
}
/**
* drm_sched_entity_push_job - Submit a job to the entity's job queue
* @sched_job: job to submit
*
* Note: To guarantee that the order of insertion to queue matches the job's
* fence sequence number this function should be called with drm_sched_job_arm()
* under common lock for the struct drm_sched_entity that was set up for
* @sched_job in drm_sched_job_init().
*/
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);
if (trace_drm_sched_job_add_dep_enabled()) {
struct dma_fence *entry;
unsigned long index;
xa_for_each(&sched_job->dependencies, index, entry)
trace_drm_sched_job_add_dep(sched_job, entry);
}
atomic_inc(entity->rq->sched->score);
WRITE_ONCE(entity->last_user, current->group_leader);
/*
* After the sched_job is pushed into the entity queue, it may be
* completed and freed up at any time. We can no longer access it.
* Make sure to set the submit_ts first, to avoid a race.
*/
sched_job->submit_ts = submit_ts = ktime_get();
first = spsc_queue_push(&entity->job_queue, &sched_job->queue_node);
/* first job wakes up scheduler */
if (first) {
struct drm_gpu_scheduler *sched;
sched = drm_sched_rq_add_entity(entity, submit_ts);
if (sched)
drm_sched_wakeup(sched);
}
}
EXPORT_SYMBOL(drm_sched_entity_push_job);
@@ -0,0 +1,242 @@
/*
* Copyright 2015 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
#include <linux/export.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/wait.h>
#include <drm/gpu_scheduler.h>
#include "sched_internal.h"
static struct kmem_cache *sched_fence_slab;
static int __init drm_sched_fence_slab_init(void)
{
sched_fence_slab = KMEM_CACHE(drm_sched_fence, SLAB_HWCACHE_ALIGN);
if (!sched_fence_slab)
return -ENOMEM;
return 0;
}
static void __exit drm_sched_fence_slab_fini(void)
{
rcu_barrier();
kmem_cache_destroy(sched_fence_slab);
}
static void drm_sched_fence_set_parent(struct drm_sched_fence *s_fence,
struct dma_fence *fence)
{
/*
* smp_store_release() to ensure another thread racing us
* in drm_sched_fence_set_deadline_finished() sees the
* fence's parent set before test_bit()
*/
smp_store_release(&s_fence->parent, dma_fence_get(fence));
if (test_bit(DRM_SCHED_FENCE_FLAG_HAS_DEADLINE_BIT,
&s_fence->finished.flags))
dma_fence_set_deadline(fence, s_fence->deadline);
}
void drm_sched_fence_scheduled(struct drm_sched_fence *fence,
struct dma_fence *parent)
{
/* Set the parent before signaling the scheduled fence, such that,
* any waiter expecting the parent to be filled after the job has
* been scheduled (which is the case for drivers delegating waits
* to some firmware) doesn't have to busy wait for parent to show
* up.
*/
if (!IS_ERR_OR_NULL(parent))
drm_sched_fence_set_parent(fence, parent);
dma_fence_signal(&fence->scheduled);
}
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);
}
static const char *drm_sched_fence_get_driver_name(struct dma_fence *fence)
{
return "drm_sched";
}
static const char *drm_sched_fence_get_timeline_name(struct dma_fence *f)
{
struct drm_sched_fence *fence = to_drm_sched_fence(f);
return (const char *)fence->sched->name;
}
static void drm_sched_fence_free_rcu(struct rcu_head *rcu)
{
struct dma_fence *f = container_of(rcu, struct dma_fence, rcu);
struct drm_sched_fence *fence = to_drm_sched_fence(f);
if (!WARN_ON_ONCE(!fence))
kmem_cache_free(sched_fence_slab, fence);
}
/**
* drm_sched_fence_free - free up an uninitialized fence
*
* @fence: fence to free
*
* Free up the fence memory. Should only be used if drm_sched_fence_init()
* has not been called yet.
*/
void drm_sched_fence_free(struct drm_sched_fence *fence)
{
/* This function should not be called if the fence has been initialized. */
if (!WARN_ON_ONCE(fence->sched))
kmem_cache_free(sched_fence_slab, fence);
}
/**
* drm_sched_fence_release_scheduled - callback that fence can be freed
*
* @f: fence
*
* This function is called when the reference count becomes zero.
* It just RCU schedules freeing up the fence.
*/
static void drm_sched_fence_release_scheduled(struct dma_fence *f)
{
struct drm_sched_fence *fence = to_drm_sched_fence(f);
dma_fence_put(fence->parent);
call_rcu(&fence->finished.rcu, drm_sched_fence_free_rcu);
}
/**
* drm_sched_fence_release_finished - drop extra reference
*
* @f: fence
*
* Drop the extra reference from the scheduled fence to the base fence.
*/
static void drm_sched_fence_release_finished(struct dma_fence *f)
{
struct drm_sched_fence *fence = to_drm_sched_fence(f);
dma_fence_put(&fence->scheduled);
}
static void drm_sched_fence_set_deadline_finished(struct dma_fence *f,
ktime_t deadline)
{
struct drm_sched_fence *fence = to_drm_sched_fence(f);
struct dma_fence *parent;
unsigned long flags;
dma_fence_lock_irqsave(f, flags);
/* If we already have an earlier deadline, keep it: */
if (test_bit(DRM_SCHED_FENCE_FLAG_HAS_DEADLINE_BIT, &f->flags) &&
ktime_before(fence->deadline, deadline)) {
dma_fence_unlock_irqrestore(f, flags);
return;
}
fence->deadline = deadline;
set_bit(DRM_SCHED_FENCE_FLAG_HAS_DEADLINE_BIT, &f->flags);
dma_fence_unlock_irqrestore(f, flags);
/*
* smp_load_aquire() to ensure that if we are racing another
* thread calling drm_sched_fence_set_parent(), that we see
* the parent set before it calls test_bit(HAS_DEADLINE_BIT)
*/
parent = smp_load_acquire(&fence->parent);
if (parent)
dma_fence_set_deadline(parent, deadline);
}
static const struct dma_fence_ops drm_sched_fence_ops_scheduled = {
.get_driver_name = drm_sched_fence_get_driver_name,
.get_timeline_name = drm_sched_fence_get_timeline_name,
.release = drm_sched_fence_release_scheduled,
};
static const struct dma_fence_ops drm_sched_fence_ops_finished = {
.get_driver_name = drm_sched_fence_get_driver_name,
.get_timeline_name = drm_sched_fence_get_timeline_name,
.release = drm_sched_fence_release_finished,
.set_deadline = drm_sched_fence_set_deadline_finished,
};
struct drm_sched_fence *to_drm_sched_fence(struct dma_fence *f)
{
if (rcu_access_pointer(f->ops) == &drm_sched_fence_ops_scheduled)
return container_of(f, struct drm_sched_fence, scheduled);
if (rcu_access_pointer(f->ops) == &drm_sched_fence_ops_finished)
return container_of(f, struct drm_sched_fence, finished);
return NULL;
}
EXPORT_SYMBOL(to_drm_sched_fence);
struct drm_sched_fence *drm_sched_fence_alloc(struct drm_sched_entity *entity,
void *owner,
u64 drm_client_id)
{
struct drm_sched_fence *fence = NULL;
fence = kmem_cache_zalloc(sched_fence_slab, GFP_KERNEL);
if (fence == NULL)
return NULL;
fence->owner = owner;
fence->drm_client_id = drm_client_id;
spin_lock_init(&fence->lock);
return fence;
}
void drm_sched_fence_init(struct drm_sched_fence *fence,
struct drm_sched_entity *entity)
{
unsigned seq;
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, seq);
dma_fence_init(&fence->finished, &drm_sched_fence_ops_finished,
&fence->lock, entity->fence_context + 1, seq);
}
module_init(drm_sched_fence_slab_init);
module_exit(drm_sched_fence_slab_fini);
MODULE_DESCRIPTION("DRM GPU scheduler");
MODULE_LICENSE("GPL and additional rights");
@@ -0,0 +1,154 @@
/* SPDX-License-Identifier: MIT */
#ifndef _DRM_GPU_SCHEDULER_INTERNAL_H_
#define _DRM_GPU_SCHEDULER_INTERNAL_H_
#include <linux/ktime.h>
#include <linux/kref.h>
#include <linux/spinlock.h>
/**
* struct drm_sched_entity_stats - execution stats for an entity.
* @kref: reference count for the object.
* @lock: lock guarding the @runtime updates.
* @runtime: time entity spent on the GPU.
* @prev_runtime: previous @runtime used to get the runtime delta.
* @vruntime: virtual runtime as accumulated by the fair algorithm.
* @avg_job_us: average job duration.
*
* Because jobs and entities have decoupled lifetimes, ie. we cannot access the
* entity once the job has been de-queued, and we do need know how much GPU time
* each entity has spent, we need to track this in a separate object which is
* reference counted by both entities and jobs.
*/
struct drm_sched_entity_stats {
struct kref kref;
spinlock_t lock; /* Protects the below fields. */
ktime_t runtime;
ktime_t prev_runtime;
ktime_t vruntime;
struct ewma_drm_sched_avgtime avg_job_us;
};
/* Used to choose between FIFO and RR job-scheduling */
extern int drm_sched_policy;
#define DRM_SCHED_POLICY_RR 0
#define DRM_SCHED_POLICY_FIFO 1
#define DRM_SCHED_POLICY_FAIR 2
bool drm_sched_can_queue(struct drm_gpu_scheduler *sched,
struct drm_sched_entity *entity);
void drm_sched_wakeup(struct drm_gpu_scheduler *sched);
void drm_sched_rq_init(struct drm_gpu_scheduler *sched,
struct drm_sched_rq *rq);
struct drm_gpu_scheduler *
drm_sched_rq_add_entity(struct drm_sched_entity *entity, ktime_t ts);
void drm_sched_rq_remove_entity(struct drm_sched_rq *rq,
struct drm_sched_entity *entity);
void drm_sched_rq_pop_entity(struct drm_sched_entity *entity);
struct drm_sched_entity *
drm_sched_rq_select_entity(struct drm_gpu_scheduler *sched,
struct drm_sched_rq *rq);
void drm_sched_entity_select_rq(struct drm_sched_entity *entity);
struct drm_sched_job *drm_sched_entity_pop_job(struct drm_sched_entity *entity);
struct drm_sched_fence *drm_sched_fence_alloc(struct drm_sched_entity *s_entity,
void *owner, u64 drm_client_id);
void drm_sched_fence_init(struct drm_sched_fence *fence,
struct drm_sched_entity *entity);
void drm_sched_fence_free(struct drm_sched_fence *fence);
void drm_sched_fence_scheduled(struct drm_sched_fence *fence,
struct dma_fence *parent);
void drm_sched_fence_finished(struct drm_sched_fence *fence, int result);
/**
* drm_sched_entity_queue_pop - Low level helper for popping queued jobs
*
* @entity: scheduler entity
*
* Low level helper for popping queued jobs.
*
* Returns: The job dequeued or NULL.
*/
static inline struct drm_sched_job *
drm_sched_entity_queue_pop(struct drm_sched_entity *entity)
{
struct spsc_node *node;
node = spsc_queue_pop(&entity->job_queue);
if (!node)
return NULL;
return container_of(node, struct drm_sched_job, queue_node);
}
/**
* drm_sched_entity_queue_peek - Low level helper for peeking at the job queue
*
* @entity: scheduler entity
*
* Low level helper for peeking at the job queue
*
* Returns: The job at the head of the queue or NULL.
*/
static inline struct drm_sched_job *
drm_sched_entity_queue_peek(struct drm_sched_entity *entity)
{
struct spsc_node *node;
node = spsc_queue_peek(&entity->job_queue);
if (!node)
return NULL;
return container_of(node, struct drm_sched_job, queue_node);
}
/* Return true if entity could provide a job. */
static inline bool
drm_sched_entity_is_ready(struct drm_sched_entity *entity)
{
if (!spsc_queue_count(&entity->job_queue))
return false;
if (READ_ONCE(entity->dependency))
return false;
return true;
}
void drm_sched_entity_stats_release(struct kref *kref);
/**
* drm_sched_entity_stats_get - Obtain a reference count on &struct drm_sched_entity_stats object
* @stats: struct drm_sched_entity_stats pointer
*
* Return: struct drm_sched_entity_stats pointer
*/
static inline struct drm_sched_entity_stats *
drm_sched_entity_stats_get(struct drm_sched_entity_stats *stats)
{
kref_get(&stats->kref);
return stats;
}
/**
* drm_sched_entity_stats_put - Release a reference count on &struct drm_sched_entity_stats object
* @stats: struct drm_sched_entity_stats pointer
*/
static inline void
drm_sched_entity_stats_put(struct drm_sched_entity_stats *stats)
{
kref_put(&stats->kref, drm_sched_entity_stats_release);
}
ktime_t drm_sched_entity_stats_job_add_gpu_time(struct drm_sched_job *job);
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,420 @@
// 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_fifo_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_fifo_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_fifo_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
* @sched: scheduler instance to associate with this run queue
* @rq: scheduler run queue
*
* Initializes a scheduler runqueue.
*/
void drm_sched_rq_init(struct drm_gpu_scheduler *sched,
struct drm_sched_rq *rq)
{
spin_lock_init(&rq->lock);
INIT_LIST_HEAD(&rq->entities);
rq->rb_tree_root = RB_ROOT_CACHED;
rq->sched = sched;
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 = entity->rq->sched;
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;
}
static ktime_t drm_sched_entity_get_job_ts(struct drm_sched_entity *entity)
{
return drm_sched_entity_update_vruntime(entity);
}
/**
* drm_sched_rq_add_entity - add an entity
* @entity: scheduler entity
* @ts: submission timestamp
*
* 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, ktime_t ts)
{
struct drm_gpu_scheduler *sched;
struct drm_sched_rq *rq;
/* 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;
spin_lock(&rq->lock);
sched = rq->sched;
if (list_empty(&entity->list)) {
atomic_inc(sched->score);
list_add_tail(&entity->list, &rq->entities);
}
if (drm_sched_policy == DRM_SCHED_POLICY_FAIR) {
ts = drm_sched_rq_get_min_vruntime(rq);
ts = drm_sched_entity_restore_vruntime(entity, ts,
rq->head_prio);
} else if (drm_sched_policy == DRM_SCHED_POLICY_RR) {
ts = entity->rr_ts;
}
drm_sched_rq_update_fifo_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)
{
lockdep_assert_held(&entity->lock);
if (list_empty(&entity->list))
return;
spin_lock(&rq->lock);
atomic_dec(rq->sched->score);
list_del_init(&entity->list);
drm_sched_rq_remove_fifo_locked(entity, rq);
spin_unlock(&rq->lock);
}
static ktime_t
drm_sched_rq_next_rr_ts(struct drm_sched_rq *rq,
struct drm_sched_entity *entity)
{
ktime_t ts;
lockdep_assert_held(&entity->lock);
lockdep_assert_held(&rq->lock);
ts = ktime_add_ns(rq->rr_ts, 1);
entity->rr_ts = ts;
rq->rr_ts = ts;
return ts;
}
/**
* 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;
if (drm_sched_policy == DRM_SCHED_POLICY_FAIR)
ts = drm_sched_entity_get_job_ts(entity);
else if (drm_sched_policy == DRM_SCHED_POLICY_FIFO)
ts = next_job->submit_ts;
else
ts = drm_sched_rq_next_rr_ts(rq, entity);
drm_sched_rq_update_fifo_locked(entity, rq, ts);
} else {
drm_sched_rq_remove_fifo_locked(entity, rq);
if (drm_sched_policy == DRM_SCHED_POLICY_FAIR) {
ktime_t min_vruntime;
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_rq_select_entity - Select an entity which provides a job to run
* @sched: the gpu scheduler
* @rq: scheduler run queue to check.
*
* 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_rq_select_entity(struct drm_gpu_scheduler *sched,
struct drm_sched_rq *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;
}
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: GPL-2.0
drm-sched-tests-y := \
mock_scheduler.o \
tests_basic.o \
tests_scheduler.o
obj-$(CONFIG_DRM_SCHED_KUNIT_TEST) += drm-sched-tests.o
@@ -0,0 +1,370 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */
#include "sched_tests.h"
/*
* Here we implement the mock "GPU" (or the scheduler backend) which is used by
* the DRM scheduler unit tests in order to exercise the core functionality.
*
* Test cases are implemented in a separate file.
*/
/**
* drm_mock_sched_entity_new - Create a new mock scheduler entity
*
* @test: KUnit test owning the entity
* @priority: Scheduling priority
* @sched: Mock scheduler on which the entity can be scheduled
*
* Returns: New mock scheduler entity with allocation managed by the test
*/
struct drm_mock_sched_entity *
drm_mock_sched_entity_new(struct kunit *test,
enum drm_sched_priority priority,
struct drm_mock_scheduler *sched)
{
struct drm_mock_sched_entity *entity;
struct drm_gpu_scheduler *drm_sched;
int ret;
entity = kunit_kzalloc(test, sizeof(*entity), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, entity);
drm_sched = &sched->base;
ret = drm_sched_entity_init(&entity->base,
priority,
&drm_sched, 1,
NULL);
KUNIT_ASSERT_EQ(test, ret, 0);
entity->test = test;
return entity;
}
/**
* drm_mock_sched_entity_free - Destroys a mock scheduler entity
*
* @entity: Entity to destroy
*
* To be used from the test cases once done with the entity.
*/
void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity)
{
drm_sched_entity_destroy(&entity->base);
}
static void drm_mock_sched_job_complete(struct drm_mock_sched_job *job)
{
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(job->base.sched);
lockdep_assert_held(&sched->lock);
job->flags |= DRM_MOCK_SCHED_JOB_DONE;
list_del(&job->link);
dma_fence_signal_locked(&job->hw_fence);
complete(&job->done);
}
static enum hrtimer_restart
drm_mock_sched_job_signal_timer(struct hrtimer *hrtimer)
{
struct drm_mock_sched_job *job =
container_of(hrtimer, typeof(*job), timer);
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(job->base.sched);
struct drm_mock_sched_job *next;
ktime_t now = ktime_get();
unsigned long flags;
LIST_HEAD(signal);
spin_lock_irqsave(&sched->lock, flags);
list_for_each_entry_safe(job, next, &sched->job_list, link) {
if (!job->duration_us)
break;
if (ktime_before(now, job->finish_at))
break;
sched->hw_timeline.cur_seqno = job->hw_fence.seqno;
drm_mock_sched_job_complete(job);
}
spin_unlock_irqrestore(&sched->lock, flags);
return HRTIMER_NORESTART;
}
/**
* drm_mock_sched_job_new - Create a new mock scheduler job
*
* @test: KUnit test owning the job
* @entity: Scheduler entity of the job
*
* Returns: New mock scheduler job with allocation managed by the test
*/
struct drm_mock_sched_job *
drm_mock_sched_job_new(struct kunit *test,
struct drm_mock_sched_entity *entity)
{
struct drm_mock_sched_job *job;
int ret;
job = kunit_kzalloc(test, sizeof(*job), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, job);
ret = drm_sched_job_init(&job->base,
&entity->base,
1,
NULL,
1);
KUNIT_ASSERT_EQ(test, ret, 0);
job->test = test;
init_completion(&job->done);
INIT_LIST_HEAD(&job->link);
hrtimer_setup(&job->timer, drm_mock_sched_job_signal_timer,
CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
return job;
}
static const char *drm_mock_sched_hw_fence_driver_name(struct dma_fence *fence)
{
return "drm_mock_sched";
}
static const char *
drm_mock_sched_hw_fence_timeline_name(struct dma_fence *fence)
{
struct drm_mock_sched_job *job =
container_of(fence, typeof(*job), hw_fence);
return (const char *)job->base.sched->name;
}
static void drm_mock_sched_hw_fence_release(struct dma_fence *fence)
{
struct drm_mock_sched_job *job =
container_of(fence, typeof(*job), hw_fence);
hrtimer_cancel(&job->timer);
/* Containing job is freed by the kunit framework */
}
static const struct dma_fence_ops drm_mock_sched_hw_fence_ops = {
.get_driver_name = drm_mock_sched_hw_fence_driver_name,
.get_timeline_name = drm_mock_sched_hw_fence_timeline_name,
.release = drm_mock_sched_hw_fence_release,
};
static struct dma_fence *mock_sched_run_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
dma_fence_init(&job->hw_fence,
&drm_mock_sched_hw_fence_ops,
&sched->lock,
sched->hw_timeline.context,
atomic_inc_return(&sched->hw_timeline.next_seqno));
dma_fence_get(&job->hw_fence); /* Reference for the job_list */
spin_lock_irq(&sched->lock);
if (job->duration_us) {
ktime_t prev_finish_at = 0;
if (!list_empty(&sched->job_list)) {
struct drm_mock_sched_job *prev =
list_last_entry(&sched->job_list, typeof(*prev),
link);
prev_finish_at = prev->finish_at;
}
if (!prev_finish_at)
prev_finish_at = ktime_get();
job->finish_at = ktime_add_us(prev_finish_at, job->duration_us);
}
list_add_tail(&job->link, &sched->job_list);
if (job->finish_at)
hrtimer_start(&job->timer, job->finish_at, HRTIMER_MODE_ABS);
spin_unlock_irq(&sched->lock);
return &job->hw_fence;
}
/*
* Normally, drivers would take appropriate measures in this callback, such as
* killing the entity the faulty job is associated with, resetting the hardware
* and / or resubmitting non-faulty jobs.
*
* For the mock scheduler, there are no hardware rings to be resetted nor jobs
* to be resubmitted. Thus, this function merely ensures that
* a) timedout fences get signaled properly and removed from the pending list
* b) the mock scheduler framework gets informed about the timeout via a flag
* c) The drm_sched_job, not longer needed, gets freed
*/
static enum drm_gpu_sched_stat
mock_sched_timedout_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
unsigned long flags;
if (job->flags & DRM_MOCK_SCHED_JOB_DONT_RESET) {
job->flags |= DRM_MOCK_SCHED_JOB_RESET_SKIPPED;
return DRM_GPU_SCHED_STAT_NO_HANG;
}
spin_lock_irqsave(&sched->lock, flags);
if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
list_del(&job->link);
job->flags |= DRM_MOCK_SCHED_JOB_TIMEDOUT;
dma_fence_set_error(&job->hw_fence, -ETIMEDOUT);
dma_fence_signal_locked(&job->hw_fence);
}
spin_unlock_irqrestore(&sched->lock, flags);
dma_fence_put(&job->hw_fence);
drm_sched_job_cleanup(sched_job);
/* Mock job itself is freed by the kunit framework. */
return DRM_GPU_SCHED_STAT_RESET;
}
static void mock_sched_free_job(struct drm_sched_job *sched_job)
{
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
dma_fence_put(&job->hw_fence);
drm_sched_job_cleanup(sched_job);
/* Mock job itself is freed by the kunit framework. */
}
static void mock_sched_cancel_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
unsigned long flags;
hrtimer_cancel(&job->timer);
spin_lock_irqsave(&sched->lock, flags);
if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
list_del(&job->link);
dma_fence_set_error(&job->hw_fence, -ECANCELED);
dma_fence_signal_locked(&job->hw_fence);
}
spin_unlock_irqrestore(&sched->lock, flags);
/*
* The GPU Scheduler will call drm_sched_backend_ops.free_job(), still.
* Mock job itself is freed by the kunit framework.
*/
}
static const struct drm_sched_backend_ops drm_mock_scheduler_ops = {
.run_job = mock_sched_run_job,
.timedout_job = mock_sched_timedout_job,
.free_job = mock_sched_free_job,
.cancel_job = mock_sched_cancel_job,
};
/**
* drm_mock_sched_new - Create a new mock scheduler
*
* @test: KUnit test owning the job
* @timeout: Job timeout to set
*
* Returns: New mock scheduler with allocation managed by the test
*/
struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test, long timeout)
{
struct drm_sched_init_args args = {
.ops = &drm_mock_scheduler_ops,
.num_rqs = DRM_SCHED_PRIORITY_COUNT,
.credit_limit = U32_MAX,
.hang_limit = 1,
.timeout = timeout,
.name = "drm-mock-scheduler",
};
struct drm_mock_scheduler *sched;
int ret;
sched = kunit_kzalloc(test, sizeof(*sched), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, sched);
ret = drm_sched_init(&sched->base, &args);
KUNIT_ASSERT_EQ(test, ret, 0);
sched->test = test;
sched->hw_timeline.context = dma_fence_context_alloc(1);
atomic_set(&sched->hw_timeline.next_seqno, 0);
INIT_LIST_HEAD(&sched->job_list);
spin_lock_init(&sched->lock);
return sched;
}
/**
* drm_mock_sched_fini - Destroys a mock scheduler
*
* @sched: Scheduler to destroy
*
* To be used from the test cases once done with the scheduler.
*/
void drm_mock_sched_fini(struct drm_mock_scheduler *sched)
{
drm_sched_fini(&sched->base);
}
/**
* drm_mock_sched_advance - Advances the mock scheduler timeline
*
* @sched: Scheduler timeline to advance
* @num: By how many jobs to advance
*
* Advancing the scheduler timeline by a number of seqnos will trigger
* signalling of the hardware fences and unlinking the jobs from the internal
* scheduler tracking.
*
* This can be used from test cases which want complete control of the simulated
* job execution timing. For example submitting one job with no set duration
* would never complete it before test cases advances the timeline by one.
*/
unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
unsigned int num)
{
struct drm_mock_sched_job *job, *next;
unsigned int found = 0;
unsigned long flags;
LIST_HEAD(signal);
spin_lock_irqsave(&sched->lock, flags);
if (WARN_ON_ONCE(sched->hw_timeline.cur_seqno + num <
sched->hw_timeline.cur_seqno))
goto unlock;
sched->hw_timeline.cur_seqno += num;
list_for_each_entry_safe(job, next, &sched->job_list, link) {
if (sched->hw_timeline.cur_seqno < job->hw_fence.seqno)
break;
drm_mock_sched_job_complete(job);
found++;
}
unlock:
spin_unlock_irqrestore(&sched->lock, flags);
return found;
}
MODULE_DESCRIPTION("DRM mock scheduler and tests");
MODULE_LICENSE("GPL");
@@ -0,0 +1,224 @@
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright (c) 2025 Valve Corporation */
#ifndef _SCHED_TESTS_H_
#define _SCHED_TESTS_H_
#include <kunit/test.h>
#include <linux/atomic.h>
#include <linux/completion.h>
#include <linux/dma-fence.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/types.h>
#include <drm/gpu_scheduler.h>
/*
* DOC: Mock DRM scheduler data structures
*
* drm_mock_* data structures are used to implement a mock "GPU".
*
* They subclass the core DRM scheduler objects and add their data on top, which
* enables tracking the submitted jobs and simulating their execution with the
* attributes as specified by the test case.
*/
/**
* struct drm_mock_scheduler - implements a trivial mock GPU execution engine
*
* @base: DRM scheduler base class
* @test: Backpointer to owning the kunit test case
* @lock: Lock to protect the simulated @hw_timeline and @job_list
* @job_list: List of jobs submitted to the mock GPU
* @hw_timeline: Simulated hardware timeline has a @context, @next_seqno and
* @cur_seqno for implementing a struct dma_fence signaling the
* simulated job completion.
*
* Trivial mock GPU execution engine tracks submitted jobs and enables
* completing them strictly in submission order.
*/
struct drm_mock_scheduler {
struct drm_gpu_scheduler base;
struct kunit *test;
spinlock_t lock;
struct list_head job_list;
struct {
u64 context;
atomic_t next_seqno;
unsigned int cur_seqno;
} hw_timeline;
};
/**
* struct drm_mock_sched_entity - implements a mock GPU sched entity
*
* @base: DRM scheduler entity base class
* @test: Backpointer to owning the kunit test case
*
* Mock GPU sched entity is used by the test cases to submit jobs to the mock
* scheduler.
*/
struct drm_mock_sched_entity {
struct drm_sched_entity base;
struct kunit *test;
};
/**
* struct drm_mock_sched_job - implements a mock GPU job
*
* @base: DRM sched job base class
* @done: Completion signaling job completion.
* @flags: Flags designating job state.
* @link: List head element used by job tracking by the drm_mock_scheduler
* @timer: Timer used for simulating job execution duration
* @duration_us: Simulated job duration in micro seconds, or zero if in manual
* timeline advance mode
* @finish_at: Absolute time when the jobs with set duration will complete
* @lock: Lock used for @hw_fence
* @hw_fence: Fence returned to DRM scheduler as the hardware fence
* @test: Backpointer to owning the kunit test case
*
* Mock GPU sched job is used by the test cases to submit jobs to the mock
* scheduler.
*/
struct drm_mock_sched_job {
struct drm_sched_job base;
struct completion done;
#define DRM_MOCK_SCHED_JOB_DONE 0x1
#define DRM_MOCK_SCHED_JOB_TIMEDOUT 0x2
#define DRM_MOCK_SCHED_JOB_DONT_RESET 0x4
#define DRM_MOCK_SCHED_JOB_RESET_SKIPPED 0x8
unsigned long flags;
struct list_head link;
struct hrtimer timer;
unsigned int duration_us;
ktime_t finish_at;
struct dma_fence hw_fence;
struct kunit *test;
};
static inline struct drm_mock_scheduler *
drm_sched_to_mock_sched(struct drm_gpu_scheduler *sched)
{
return container_of(sched, struct drm_mock_scheduler, base);
};
static inline struct drm_mock_sched_entity *
drm_sched_entity_to_mock_entity(struct drm_sched_entity *sched_entity)
{
return container_of(sched_entity, struct drm_mock_sched_entity, base);
};
static inline struct drm_mock_sched_job *
drm_sched_job_to_mock_job(struct drm_sched_job *sched_job)
{
return container_of(sched_job, struct drm_mock_sched_job, base);
};
struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test,
long timeout);
void drm_mock_sched_fini(struct drm_mock_scheduler *sched);
unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
unsigned int num);
struct drm_mock_sched_entity *
drm_mock_sched_entity_new(struct kunit *test,
enum drm_sched_priority priority,
struct drm_mock_scheduler *sched);
void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity);
struct drm_mock_sched_job *
drm_mock_sched_job_new(struct kunit *test,
struct drm_mock_sched_entity *entity);
/**
* drm_mock_sched_job_submit - Arm and submit a job in one go
*
* @job: Job to arm and submit
*/
static inline void drm_mock_sched_job_submit(struct drm_mock_sched_job *job)
{
drm_sched_job_arm(&job->base);
drm_sched_entity_push_job(&job->base);
}
/**
* drm_mock_sched_job_set_duration_us - Set a job duration
*
* @job: Job to set the duration for
* @duration_us: Duration in micro seconds
*
* Jobs with duration set will be automatically completed by the mock scheduler
* as the timeline progresses, unless a job without a set duration is
* encountered in the timelime in which case calling drm_mock_sched_advance()
* will be required to bump the timeline.
*/
static inline void
drm_mock_sched_job_set_duration_us(struct drm_mock_sched_job *job,
unsigned int duration_us)
{
job->duration_us = duration_us;
}
/**
* drm_mock_sched_job_is_finished - Check if a job is finished
*
* @job: Job to check
*
* Returns: true if finished
*/
static inline bool
drm_mock_sched_job_is_finished(struct drm_mock_sched_job *job)
{
return job->flags & DRM_MOCK_SCHED_JOB_DONE;
}
/**
* drm_mock_sched_job_wait_finished - Wait until a job is finished
*
* @job: Job to wait for
* @timeout: Wait time in jiffies
*
* Returns: true if finished within the timeout provided, otherwise false
*/
static inline bool
drm_mock_sched_job_wait_finished(struct drm_mock_sched_job *job, long timeout)
{
if (job->flags & DRM_MOCK_SCHED_JOB_DONE)
return true;
return wait_for_completion_timeout(&job->done, timeout) != 0;
}
/**
* drm_mock_sched_job_wait_scheduled - Wait until a job is scheduled
*
* @job: Job to wait for
* @timeout: Wait time in jiffies
*
* Returns: true if scheduled within the timeout provided, otherwise false
*/
static inline bool
drm_mock_sched_job_wait_scheduled(struct drm_mock_sched_job *job, long timeout)
{
KUNIT_ASSERT_EQ(job->test, job->flags & DRM_MOCK_SCHED_JOB_DONE, 0);
return dma_fence_wait_timeout(&job->base.s_fence->scheduled,
false,
timeout) != 0;
}
#endif
@@ -0,0 +1,563 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */
#include <linux/delay.h>
#include "sched_tests.h"
#define MOCK_TIMEOUT (HZ / 5)
/*
* DRM scheduler basic tests should check the basic functional correctness of
* the scheduler, including some very light smoke testing. More targeted tests,
* for example focusing on testing specific bugs and other more complicated test
* scenarios, should be implemented in separate source units.
*/
static int drm_sched_basic_init(struct kunit *test)
{
test->priv = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
return 0;
}
static void drm_sched_basic_exit(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
drm_mock_sched_fini(sched);
}
static int drm_sched_timeout_init(struct kunit *test)
{
test->priv = drm_mock_sched_new(test, MOCK_TIMEOUT);
return 0;
}
static void drm_sched_basic_submit(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
unsigned int i;
bool done;
/*
* Submit one job to the scheduler and verify that it gets scheduled
* and completed only when the mock hw backend processes it.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, HZ / 2);
KUNIT_ASSERT_FALSE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
}
struct drm_sched_basic_params {
const char *description;
unsigned int queue_depth;
unsigned int num_entities;
unsigned int job_us;
bool dep_chain;
};
static const struct drm_sched_basic_params drm_sched_basic_cases[] = {
{
.description = "A queue of jobs in a single entity",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 1,
},
{
.description = "A chain of dependent jobs across multiple entities",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 1,
.dep_chain = true,
},
{
.description = "Multiple independent job queues",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 4,
},
{
.description = "Multiple inter-dependent job queues",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 4,
.dep_chain = true,
},
};
static void
drm_sched_basic_desc(const struct drm_sched_basic_params *params, char *desc)
{
strscpy(desc, params->description, KUNIT_PARAM_DESC_SIZE);
}
KUNIT_ARRAY_PARAM(drm_sched_basic, drm_sched_basic_cases, drm_sched_basic_desc);
static void drm_sched_basic_test(struct kunit *test)
{
const struct drm_sched_basic_params *params = test->param_value;
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job, *prev = NULL;
struct drm_mock_sched_entity **entity;
unsigned int i, cur_ent = 0;
bool done;
entity = kunit_kcalloc(test, params->num_entities, sizeof(*entity),
GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, entity);
for (i = 0; i < params->num_entities; i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i < params->queue_depth; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= params->num_entities;
drm_mock_sched_job_set_duration_us(job, params->job_us);
if (params->dep_chain && prev)
drm_sched_job_add_dependency(&job->base,
dma_fence_get(&prev->base.s_fence->finished));
drm_mock_sched_job_submit(job);
prev = job;
}
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
for (i = 0; i < params->num_entities; i++)
drm_mock_sched_entity_free(entity[i]);
}
static void drm_sched_basic_entity_cleanup(struct kunit *test)
{
struct drm_mock_sched_job *job, *mid, *prev = NULL;
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity[4];
const unsigned int qd = 100;
unsigned int i, cur_ent = 0;
bool done;
/*
* Submit a queue of jobs across different entities with an explicit
* chain of dependencies between them and trigger entity cleanup while
* the queue is still being processed.
*/
for (i = 0; i < ARRAY_SIZE(entity); i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
if (prev)
drm_sched_job_add_dependency(&job->base,
dma_fence_get(&prev->base.s_fence->finished));
drm_mock_sched_job_submit(job);
if (i == qd / 2)
mid = job;
prev = job;
}
done = drm_mock_sched_job_wait_finished(mid, HZ);
KUNIT_ASSERT_TRUE(test, done);
/* Exit with half of the queue still pending to be executed. */
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static struct kunit_case drm_sched_basic_tests[] = {
KUNIT_CASE(drm_sched_basic_submit),
KUNIT_CASE_PARAM(drm_sched_basic_test, drm_sched_basic_gen_params),
KUNIT_CASE(drm_sched_basic_entity_cleanup),
{}
};
static struct kunit_suite drm_sched_basic = {
.name = "drm_sched_basic_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_basic_tests,
};
static void drm_sched_basic_cancel(struct kunit *test)
{
struct drm_mock_sched_entity *entity;
struct drm_mock_scheduler *sched;
struct drm_mock_sched_job *job;
bool done;
/*
* Check that drm_sched_fini() uses the cancel_job() callback to cancel
* jobs that are still pending.
*/
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
entity = drm_mock_sched_entity_new(test, DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
drm_mock_sched_fini(sched);
KUNIT_ASSERT_EQ(test, job->hw_fence.error, -ECANCELED);
}
static struct kunit_case drm_sched_cancel_tests[] = {
KUNIT_CASE(drm_sched_basic_cancel),
{}
};
static struct kunit_suite drm_sched_cancel = {
.name = "drm_sched_basic_cancel_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_cancel_tests,
};
static void drm_sched_basic_timeout(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
bool done;
/*
* Submit a single job against a scheduler with the timeout configured
* and verify that the timeout handling will run if the backend fails
* to complete it in time.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, MOCK_TIMEOUT / 2);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_TIMEDOUT,
0);
done = drm_mock_sched_job_wait_finished(job, MOCK_TIMEOUT);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_TIMEDOUT,
DRM_MOCK_SCHED_JOB_TIMEDOUT);
drm_mock_sched_entity_free(entity);
}
static void drm_sched_skip_reset(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
unsigned int i;
bool done;
/*
* Submit a single job against a scheduler with the timeout configured
* and verify that if the job is still running, the timeout handler
* will skip the reset and allow the job to complete.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
job->flags = DRM_MOCK_SCHED_JOB_DONT_RESET;
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, 2 * MOCK_TIMEOUT);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_RESET_SKIPPED,
DRM_MOCK_SCHED_JOB_RESET_SKIPPED);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
}
static struct kunit_case drm_sched_timeout_tests[] = {
KUNIT_CASE(drm_sched_basic_timeout),
KUNIT_CASE(drm_sched_skip_reset),
{}
};
static struct kunit_suite drm_sched_timeout = {
.name = "drm_sched_basic_timeout_tests",
.init = drm_sched_timeout_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_timeout_tests,
};
static void drm_sched_priorities(struct kunit *test)
{
struct drm_mock_sched_entity *entity[DRM_SCHED_PRIORITY_COUNT];
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job;
const unsigned int qd = 100;
unsigned int i, cur_ent = 0;
enum drm_sched_priority p;
bool done;
/*
* Submit a bunch of jobs against entities configured with different
* priorities.
*/
BUILD_BUG_ON(DRM_SCHED_PRIORITY_KERNEL > DRM_SCHED_PRIORITY_LOW);
BUILD_BUG_ON(ARRAY_SIZE(entity) != DRM_SCHED_PRIORITY_COUNT);
for (p = DRM_SCHED_PRIORITY_KERNEL; p <= DRM_SCHED_PRIORITY_LOW; p++)
entity[p] = drm_mock_sched_entity_new(test, p, sched);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static void drm_sched_change_priority(struct kunit *test)
{
struct drm_mock_sched_entity *entity[DRM_SCHED_PRIORITY_COUNT];
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job;
const unsigned int qd = 1000;
unsigned int i, cur_ent = 0;
enum drm_sched_priority p;
/*
* Submit a bunch of jobs against entities configured with different
* priorities and while waiting for them to complete, periodically keep
* changing their priorities.
*
* We set up the queue-depth (qd) and job duration so the priority
* changing loop has some time to interact with submissions to the
* backend and job completions as they progress.
*/
for (p = DRM_SCHED_PRIORITY_KERNEL; p <= DRM_SCHED_PRIORITY_LOW; p++)
entity[p] = drm_mock_sched_entity_new(test, p, sched);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
do {
drm_sched_entity_set_priority(&entity[cur_ent]->base,
(entity[cur_ent]->base.priority + 1) %
DRM_SCHED_PRIORITY_COUNT);
cur_ent++;
cur_ent %= ARRAY_SIZE(entity);
usleep_range(200, 500);
} while (!drm_mock_sched_job_is_finished(job));
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static struct kunit_case drm_sched_priority_tests[] = {
KUNIT_CASE(drm_sched_priorities),
KUNIT_CASE_SLOW(drm_sched_change_priority),
{}
};
static struct kunit_suite drm_sched_priority = {
.name = "drm_sched_basic_priority_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_priority_tests,
};
static void drm_sched_test_modify_sched(struct kunit *test)
{
unsigned int i, cur_ent = 0, cur_sched = 0;
struct drm_mock_sched_entity *entity[13];
struct drm_mock_scheduler *sched[3];
struct drm_mock_sched_job *job;
const unsigned int qd = 1000;
/*
* Submit a bunch of jobs against entities configured with different
* schedulers and while waiting for them to complete, periodically keep
* changing schedulers associated with each entity.
*
* We set up the queue-depth (qd) and job duration so the sched modify
* loop has some time to interact with submissions to the backend and
* job completions as they progress.
*
* For the number of schedulers and entities we use primes in order to
* perturb the entity->sched assignments with less of a regular pattern.
*/
for (i = 0; i < ARRAY_SIZE(sched); i++)
sched[i] = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
for (i = 0; i < ARRAY_SIZE(entity); i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched[i % ARRAY_SIZE(sched)]);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
do {
struct drm_gpu_scheduler *modify;
usleep_range(200, 500);
cur_ent++;
cur_ent %= ARRAY_SIZE(entity);
cur_sched++;
cur_sched %= ARRAY_SIZE(sched);
modify = &sched[cur_sched]->base;
drm_sched_entity_modify_sched(&entity[cur_ent]->base, &modify,
1);
} while (!drm_mock_sched_job_is_finished(job));
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
for (i = 0; i < ARRAY_SIZE(sched); i++)
drm_mock_sched_fini(sched[i]);
}
static struct kunit_case drm_sched_modify_sched_tests[] = {
KUNIT_CASE(drm_sched_test_modify_sched),
{}
};
static struct kunit_suite drm_sched_modify_sched = {
.name = "drm_sched_basic_modify_sched_tests",
.test_cases = drm_sched_modify_sched_tests,
};
static void drm_sched_test_credits(struct kunit *test)
{
struct drm_mock_sched_entity *entity;
struct drm_mock_scheduler *sched;
struct drm_mock_sched_job *job[2];
bool done;
int i;
/*
* Check that the configured credit limit is respected.
*/
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
sched->base.credit_limit = 1;
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job[0] = drm_mock_sched_job_new(test, entity);
job[1] = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job[0]);
drm_mock_sched_job_submit(job[1]);
done = drm_mock_sched_job_wait_scheduled(job[0], HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_scheduled(job[1], HZ);
KUNIT_ASSERT_FALSE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_scheduled(job[1], HZ);
KUNIT_ASSERT_TRUE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job[1], HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
drm_mock_sched_fini(sched);
}
static struct kunit_case drm_sched_credits_tests[] = {
KUNIT_CASE_SLOW(drm_sched_test_credits),
{}
};
static struct kunit_suite drm_sched_credits = {
.name = "drm_sched_basic_credits_tests",
.test_cases = drm_sched_credits_tests,
};
kunit_test_suites(&drm_sched_basic,
&drm_sched_timeout,
&drm_sched_cancel,
&drm_sched_priority,
&drm_sched_modify_sched,
&drm_sched_credits);
@@ -0,0 +1,874 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/ktime.h>
#include <linux/math64.h>
#include "sched_tests.h"
/*
* DRM scheduler tests exercise load balancing decisions ie. entity selection
* logic.
*/
static int drm_sched_scheduler_init(struct kunit *test)
{
struct drm_mock_scheduler *sched;
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
sched->base.credit_limit = 1;
test->priv = sched;
return 0;
}
static int drm_sched_scheduler_init2(struct kunit *test)
{
struct drm_mock_scheduler *sched;
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
sched->base.credit_limit = 2;
test->priv = sched;
return 0;
}
static void drm_sched_scheduler_exit(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
drm_mock_sched_fini(sched);
}
static void drm_sched_scheduler_queue_overhead(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
const unsigned int job_us = 1000;
const unsigned int jobs = 1000;
const unsigned int total_us = jobs * job_us;
struct drm_mock_sched_job *job, *first;
ktime_t start, end;
bool done;
int i;
/*
* Deep queue job at a time processing (single credit).
*
* This measures the overhead of picking and processing a job at a time
* by comparing the ideal total "GPU" time of all submitted jobs versus
* the time actually taken.
*/
KUNIT_ASSERT_EQ(test, sched->base.credit_limit, 1);
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i <= jobs; i++) {
job = drm_mock_sched_job_new(test, entity);
if (i == 0)
first = job; /* Extra first job blocks the queue */
else
drm_mock_sched_job_set_duration_us(job, job_us);
drm_mock_sched_job_submit(job);
}
done = drm_mock_sched_job_wait_scheduled(first, HZ);
KUNIT_ASSERT_TRUE(test, done);
start = ktime_get();
i = drm_mock_sched_advance(sched, 1); /* Release the queue */
KUNIT_ASSERT_EQ(test, i, 1);
/* Wait with a safe margin to avoid every failing. */
done = drm_mock_sched_job_wait_finished(job,
usecs_to_jiffies(total_us) * 5);
end = ktime_get();
KUNIT_ASSERT_TRUE(test, done);
pr_info("Expected %uus, actual %lldus\n",
total_us,
ktime_to_us(ktime_sub(end, start)));
drm_mock_sched_entity_free(entity);
}
static void drm_sched_scheduler_ping_pong(struct kunit *test)
{
struct drm_mock_sched_job *job, *first, *prev = NULL;
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity[2];
const unsigned int job_us = 1000;
const unsigned int jobs = 1000;
const unsigned int total_us = jobs * job_us;
ktime_t start, end;
bool done;
int i;
/*
* Two entitites in inter-dependency chain.
*
* This measures the overhead of picking and processing a job at a time,
* where each job depends on the previous one from the diffferent
* entity, by comparing the ideal total "GPU" time of all submitted jobs
* versus the time actually taken.
*/
KUNIT_ASSERT_EQ(test, sched->base.credit_limit, 1);
for (i = 0; i < ARRAY_SIZE(entity); i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i <= jobs; i++) {
job = drm_mock_sched_job_new(test, entity[i & 1]);
if (i == 0)
first = job; /* Extra first job blocks the queue */
else
drm_mock_sched_job_set_duration_us(job, job_us);
if (prev)
drm_sched_job_add_dependency(&job->base,
dma_fence_get(&prev->base.s_fence->finished));
drm_mock_sched_job_submit(job);
prev = job;
}
done = drm_mock_sched_job_wait_scheduled(first, HZ);
KUNIT_ASSERT_TRUE(test, done);
start = ktime_get();
i = drm_mock_sched_advance(sched, 1); /* Release the queue */
KUNIT_ASSERT_EQ(test, i, 1);
/* Wait with a safe margin to avoid every failing. */
done = drm_mock_sched_job_wait_finished(job,
usecs_to_jiffies(total_us) * 5);
end = ktime_get();
KUNIT_ASSERT_TRUE(test, done);
pr_info("Expected %uus, actual %lldus\n",
total_us,
ktime_to_us(ktime_sub(end, start)));
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static struct kunit_case drm_sched_scheduler_overhead_tests[] = {
KUNIT_CASE_SLOW(drm_sched_scheduler_queue_overhead),
KUNIT_CASE_SLOW(drm_sched_scheduler_ping_pong),
{}
};
static struct kunit_suite drm_sched_scheduler_overhead = {
.name = "drm_sched_scheduler_overhead_tests",
.init = drm_sched_scheduler_init,
.exit = drm_sched_scheduler_exit,
.test_cases = drm_sched_scheduler_overhead_tests,
};
/*
* struct drm_sched_client_params - describe a workload emitted from a client
*
* A simulated client will create an entity with a scheduling @priority and emit
* jobs in a loop where each iteration will consist of:
*
* 1. Submit @job_cnt jobs, each with a set duration of @job_us.
* 2. If @sync is true wait for last submitted job to finish.
* 3. Sleep for @wait_us micro-seconds.
* 4. Repeat.
*/
struct drm_sched_client_params {
enum drm_sched_priority priority;
unsigned int job_cnt;
unsigned int job_us;
bool sync;
unsigned int wait_us;
};
struct drm_sched_test_params {
const char *description;
unsigned int num_clients;
struct drm_sched_client_params client[2];
};
static const struct drm_sched_test_params drm_sched_cases[] = {
{
.description = "Normal priority and normal priority",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
},
{
.description = "Normal priority and low priority",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_LOW,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
},
{
.description = "High priority and normal priority",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_HIGH,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
},
{
.description = "High priority and low priority",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_HIGH,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_LOW,
.job_cnt = 1,
.job_us = 8000,
.wait_us = 0,
.sync = false,
},
},
{
.description = "50% and 50%",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 1500,
.wait_us = 1500,
.sync = true,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 2500,
.wait_us = 2500,
.sync = true,
},
},
{
.description = "50% and 50% low priority",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 1500,
.wait_us = 1500,
.sync = true,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_LOW,
.job_cnt = 1,
.job_us = 2500,
.wait_us = 2500,
.sync = true,
},
},
{
.description = "50% high priority and 50%",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_HIGH,
.job_cnt = 1,
.job_us = 1500,
.wait_us = 1500,
.sync = true,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 2500,
.wait_us = 2500,
.sync = true,
},
},
{
.description = "Low priority hog and interactive client",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_LOW,
.job_cnt = 3,
.job_us = 2500,
.wait_us = 500,
.sync = false,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 500,
.wait_us = 10000,
.sync = true,
},
},
{
.description = "Heavy rendering and interactive client",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 3,
.job_us = 2500,
.wait_us = 2500,
.sync = true,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 1000,
.wait_us = 9000,
.sync = true,
},
},
{
.description = "Very heavy rendering and interactive client",
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 4,
.job_us = 50000,
.wait_us = 1,
.sync = true,
},
.client[1] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 1,
.job_us = 1000,
.wait_us = 9000,
.sync = true,
},
},
};
static void
drm_sched_desc(const struct drm_sched_test_params *params, char *desc)
{
strscpy(desc, params->description, KUNIT_PARAM_DESC_SIZE);
}
KUNIT_ARRAY_PARAM(drm_sched_scheduler_two_clients,
drm_sched_cases,
drm_sched_desc);
/*
* struct test_client_stats - track client stats
*
* For each client executing a simulated workload we track some timings for
* which we are interested in the minimum of all iterations (@min_us), maximum
* (@max_us) and the overall total for all iterations (@tot_us).
*/
struct test_client_stats {
unsigned int min_us;
unsigned int max_us;
unsigned long tot_us;
};
/*
* struct test_client - a simulated userspace client submitting scheduler work
*
* Each client executing a simulated workload is represented by one of these.
*
* Each of them instantiates a scheduling @entity and executes a workloads as
* defined in @params. Based on those @params the theoretical execution time of
* the client is calculated as @ideal_duration, while the actual wall time is
* tracked in @duration (calculated based on the @start and @end client time-
* stamps).
*
* Numerical @id is assigned to each for logging purposes.
*
* @worker and @work are used to provide an independent execution context from
* which scheduler jobs are submitted.
*
* During execution statistics on how long it took to submit and execute one
* iteration (whether or not synchronous) is kept in @cycle_time, while
* @latency_time tracks the @cycle_time minus the ideal duration of the one
* cycle.
*
* Once the client has completed the set number of iterations it will write the
* completion status into @done.
*/
struct test_client {
struct kunit *test; /* Backpointer to the kunit test. */
struct drm_mock_sched_entity *entity;
struct kthread_worker *worker;
struct kthread_work work;
struct drm_sched_client_params params;
unsigned int id;
ktime_t duration;
ktime_t ideal_duration;
unsigned int cycles;
unsigned int cycle;
ktime_t start;
ktime_t end;
bool done;
struct test_client_stats cycle_time;
struct test_client_stats latency_time;
};
static void
update_stats(struct test_client_stats *stats, unsigned int us)
{
if (us > stats->max_us)
stats->max_us = us;
if (us < stats->min_us)
stats->min_us = us;
stats->tot_us += us;
}
static unsigned int
get_stats_avg(struct test_client_stats *stats, unsigned int cycles)
{
return div_u64(stats->tot_us, cycles);
}
static void drm_sched_client_work(struct kthread_work *work)
{
struct test_client *client = container_of(work, typeof(*client), work);
const long sync_wait = MAX_SCHEDULE_TIMEOUT;
unsigned int cycle, work_us, period_us;
struct drm_mock_sched_job *job = NULL;
work_us = client->params.job_cnt * client->params.job_us;
period_us = work_us + client->params.wait_us;
client->cycles =
DIV_ROUND_UP((unsigned int)ktime_to_us(client->duration),
period_us);
client->ideal_duration = us_to_ktime(client->cycles * period_us);
client->start = ktime_get();
for (cycle = 0; cycle < client->cycles; cycle++) {
ktime_t cycle_time;
unsigned int batch;
unsigned long us;
if (READ_ONCE(client->done))
break;
cycle_time = ktime_get();
for (batch = 0; batch < client->params.job_cnt; batch++) {
job = drm_mock_sched_job_new(client->test,
client->entity);
drm_mock_sched_job_set_duration_us(job,
client->params.job_us);
drm_mock_sched_job_submit(job);
}
if (client->params.sync)
drm_mock_sched_job_wait_finished(job, sync_wait);
cycle_time = ktime_sub(ktime_get(), cycle_time);
us = ktime_to_us(cycle_time);
update_stats(&client->cycle_time, us);
if (ktime_to_us(cycle_time) >= (long)work_us)
us = ktime_to_us(cycle_time) - work_us;
else if (WARN_ON_ONCE(client->params.sync)) /* GPU job took less than expected. */
us = 0;
update_stats(&client->latency_time, us);
WRITE_ONCE(client->cycle, cycle);
if (READ_ONCE(client->done))
break;
if (client->params.wait_us)
fsleep(client->params.wait_us);
else if (!client->params.sync)
cond_resched(); /* Do not hog the CPU if fully async. */
}
client->done = drm_mock_sched_job_wait_finished(job, sync_wait);
client->end = ktime_get();
}
static const char *prio_str(enum drm_sched_priority prio)
{
switch (prio) {
case DRM_SCHED_PRIORITY_KERNEL:
return "kernel";
case DRM_SCHED_PRIORITY_LOW:
return "low";
case DRM_SCHED_PRIORITY_NORMAL:
return "normal";
case DRM_SCHED_PRIORITY_HIGH:
return "high";
default:
return "???";
}
}
static bool client_done(struct test_client *client)
{
return READ_ONCE(client->done); /* READ_ONCE to document lockless read from a loop. */
}
static void drm_sched_scheduler_two_clients_test(struct kunit *test)
{
const struct drm_sched_test_params *params = test->param_value;
struct drm_mock_scheduler *sched = test->priv;
struct test_client client[2] = { };
unsigned int prev_cycle[2] = { };
unsigned int i, j;
ktime_t start;
/*
* Same job stream from two clients.
*/
for (i = 0; i < ARRAY_SIZE(client); i++)
client[i].entity =
drm_mock_sched_entity_new(test,
params->client[i].priority,
sched);
for (i = 0; i < ARRAY_SIZE(client); i++) {
client[i].test = test;
client[i].id = i;
client[i].duration = ms_to_ktime(1000);
client[i].params = params->client[i];
client[i].cycle_time.min_us = ~0U;
client[i].latency_time.min_us = ~0U;
client[i].worker =
kthread_create_worker(0, "%s-%u", __func__, i);
if (IS_ERR(client[i].worker)) {
for (j = 0; j < i; j++)
kthread_destroy_worker(client[j].worker);
KUNIT_FAIL(test, "Failed to create worker!\n");
}
kthread_init_work(&client[i].work, drm_sched_client_work);
}
for (i = 0; i < ARRAY_SIZE(client); i++)
kthread_queue_work(client[i].worker, &client[i].work);
/*
* The clients (workers) can be a mix of async (deep submission queue),
* sync (one job at a time), or something in between. Therefore it is
* difficult to display a single metric representing their progress.
*
* Each struct drm_sched_client_params describes the actual submission
* pattern which happens in the following steps:
* 1. Submit N jobs
* 2. Wait for last submitted job to finish
* 3. Sleep for U micro-seconds
* 4. Goto 1. for C cycles
*
* Where number of cycles is calculated to match the target client
* duration from the respective struct drm_sched_test_params.
*
* To asses scheduling behaviour what we output for both clients is:
* - pct: Percentage progress of the jobs submitted
* - cps: "Cycles" per second (where one cycle is one complete
* iteration from the above)
* - qd: Number of outstanding jobs in the client/entity
*/
pr_info(" [pct] - Job sumission progress\n"
" [cps] - Cycles per second\n"
" [qd] - Number of outstanding jobs in the client/entity\n");
pr_info("%s:\n\t pct1 cps1 qd1; pct2 cps2 qd2\n",
params->description);
start = ktime_get();
while (!client_done(&client[0]) || !client_done(&client[1])) {
const unsigned int period_ms = 100;
const unsigned int frequency = 1000 / period_ms;
unsigned int pct[2], qd[2], cycle[2], cps[2];
for (i = 0; i < ARRAY_SIZE(client); i++) {
qd[i] = spsc_queue_count(&client[i].entity->base.job_queue);
cycle[i] = READ_ONCE(client[i].cycle);
cps[i] = DIV_ROUND_UP(100 * frequency *
(cycle[i] - prev_cycle[i]),
100);
if (client[i].cycles)
pct[i] = DIV_ROUND_UP(100 * (1 + cycle[i]),
client[i].cycles);
else
pct[i] = 0;
prev_cycle[i] = cycle[i];
}
if (client_done(&client[0]))
pr_info("\t+%6lldms: ; %3u %5u %4u\n",
ktime_to_ms(ktime_sub(ktime_get(), start)),
pct[1], cps[1], qd[1]);
else if (client_done(&client[1]))
pr_info("\t+%6lldms: %3u %5u %4u;\n",
ktime_to_ms(ktime_sub(ktime_get(), start)),
pct[0], cps[0], qd[0]);
else
pr_info("\t+%6lldms: %3u %5u %4u; %3u %5u %4u\n",
ktime_to_ms(ktime_sub(ktime_get(), start)),
pct[0], cps[0], qd[0],
pct[1], cps[1], qd[1]);
msleep(period_ms);
}
for (i = 0; i < ARRAY_SIZE(client); i++) {
kthread_flush_work(&client[i].work);
kthread_destroy_worker(client[i].worker);
}
for (i = 0; i < ARRAY_SIZE(client); i++)
KUNIT_ASSERT_TRUE(test, client[i].done);
for (i = 0; i < ARRAY_SIZE(client); i++) {
pr_info(" %u: prio=%s sync=%u elapsed_ms=%lldms (ideal_ms=%lldms) cycle_time(min,avg,max)=%u,%u,%u us latency_time(min,avg,max)=%u,%u,%u us",
i,
prio_str(params->client[i].priority),
params->client[i].sync,
ktime_to_ms(ktime_sub(client[i].end, client[i].start)),
ktime_to_ms(client[i].ideal_duration),
client[i].cycle_time.min_us,
get_stats_avg(&client[i].cycle_time, client[i].cycles),
client[i].cycle_time.max_us,
client[i].latency_time.min_us,
get_stats_avg(&client[i].latency_time, client[i].cycles),
client[i].latency_time.max_us);
drm_mock_sched_entity_free(client[i].entity);
}
}
static struct kunit_case drm_sched_scheduler_two_clients_tests[] = {
KUNIT_CASE_PARAM_ATTR(drm_sched_scheduler_two_clients_test,
drm_sched_scheduler_two_clients_gen_params,
{ .speed = KUNIT_SPEED_SLOW }),
{}
};
static struct kunit_suite drm_sched_scheduler_two_clients1 = {
.name = "drm_sched_scheduler_two_clients_one_credit_tests",
.init = drm_sched_scheduler_init,
.exit = drm_sched_scheduler_exit,
.test_cases = drm_sched_scheduler_two_clients_tests,
};
static struct kunit_suite drm_sched_scheduler_two_clients2 = {
.name = "drm_sched_scheduler_two_clients_two_credits_tests",
.init = drm_sched_scheduler_init2,
.exit = drm_sched_scheduler_exit,
.test_cases = drm_sched_scheduler_two_clients_tests,
};
static const struct drm_sched_test_params drm_sched_many_cases[] = {
{
.description = "2 clients",
.num_clients = 2,
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 4,
.job_us = 1000,
.wait_us = 0,
.sync = true,
},
},
{
.description = "3 clients",
.num_clients = 3,
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 4,
.job_us = 1000,
.wait_us = 0,
.sync = true,
},
},
{
.description = "7 clients",
.num_clients = 7,
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 4,
.job_us = 1000,
.wait_us = 0,
.sync = true,
},
},
{
.description = "13 clients",
.num_clients = 13,
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 4,
.job_us = 1000,
.wait_us = 0,
.sync = true,
},
},
{
.description = "31 clients",
.num_clients = 31,
.client[0] = {
.priority = DRM_SCHED_PRIORITY_NORMAL,
.job_cnt = 2,
.job_us = 1000,
.wait_us = 0,
.sync = true,
},
},
};
KUNIT_ARRAY_PARAM(drm_sched_scheduler_many_clients,
drm_sched_many_cases,
drm_sched_desc);
static void drm_sched_scheduler_many_clients_test(struct kunit *test)
{
const struct drm_sched_test_params *params = test->param_value;
struct drm_mock_scheduler *sched = test->priv;
const unsigned int clients = params->num_clients;
unsigned int i, j, delta_total = 0, loops = 0;
struct test_client *client;
unsigned int *prev_cycle;
ktime_t start;
char *buf;
/*
* Many clients with deep-ish async queues.
*/
buf = kunit_kmalloc(test, PAGE_SIZE, GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, buf);
client = kunit_kcalloc(test, clients, sizeof(*client), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, client);
prev_cycle = kunit_kcalloc(test, clients, sizeof(*prev_cycle),
GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, prev_cycle);
for (i = 0; i < clients; i++)
client[i].entity =
drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i < clients; i++) {
client[i].test = test;
client[i].id = i;
client[i].params = params->client[0];
client[i].duration = ms_to_ktime(1000 / clients);
client[i].cycle_time.min_us = ~0U;
client[i].latency_time.min_us = ~0U;
client[i].worker =
kthread_create_worker(0, "%s-%u", __func__, i);
if (IS_ERR(client[i].worker)) {
for (j = 0; j < i; j++)
kthread_destroy_worker(client[j].worker);
KUNIT_FAIL(test, "Failed to create worker!\n");
}
kthread_init_work(&client[i].work, drm_sched_client_work);
}
for (i = 0; i < clients; i++)
kthread_queue_work(client[i].worker, &client[i].work);
start = ktime_get();
pr_info("%u clients:\n\tt\t\tcycle:\t min avg max : ...\n", clients);
for (;;) {
unsigned int min = ~0;
unsigned int max = 0;
unsigned int total = 0;
bool done = true;
char pbuf[16];
memset(buf, 0, PAGE_SIZE);
for (i = 0; i < clients; i++) {
unsigned int cycle, cycles;
/* Read current progress from the threaded worker. */
cycle = READ_ONCE(client[i].cycle);
cycles = READ_ONCE(client[i].cycles);
snprintf(pbuf, sizeof(pbuf), " %3d", cycle);
strncat(buf, pbuf, PAGE_SIZE);
total += cycle;
if (cycle < min)
min = cycle;
if (cycle > max)
max = cycle;
if (!min || (cycle + 1) < cycles)
done = false;
}
loops++;
delta_total += max - min;
pr_info("\t+%6lldms\t\t %3u %3u %3u :%s\n",
ktime_to_ms(ktime_sub(ktime_get(), start)),
min, DIV_ROUND_UP(total, clients), max, buf);
if (done)
break;
msleep(100);
}
pr_info(" avg_max_min_delta(x100)=%u\n",
loops ? DIV_ROUND_UP(delta_total * 100, loops) : 0);
for (i = 0; i < clients; i++) {
kthread_flush_work(&client[i].work);
kthread_destroy_worker(client[i].worker);
}
for (i = 0; i < clients; i++)
drm_mock_sched_entity_free(client[i].entity);
}
static struct kunit_case drm_sched_scheduler_many_clients_tests[] = {
KUNIT_CASE_PARAM_ATTR(drm_sched_scheduler_many_clients_test,
drm_sched_scheduler_many_clients_gen_params,
{ .speed = KUNIT_SPEED_SLOW }),
{}
};
static struct kunit_suite drm_sched_scheduler_many_clients = {
.name = "drm_sched_scheduler_many_clients_tests",
.init = drm_sched_scheduler_init2,
.exit = drm_sched_scheduler_exit,
.test_cases = drm_sched_scheduler_many_clients_tests,
};
kunit_test_suites(&drm_sched_scheduler_overhead,
&drm_sched_scheduler_two_clients1,
&drm_sched_scheduler_two_clients2,
&drm_sched_scheduler_many_clients);