* [RFC v2 0/2] Realtime workqueues and panthor realtime submission
@ 2026-07-17 7:28 Tvrtko Ursulin
2026-07-17 7:28 ` [RFC v2 1/2] workqueue: Add support for real-time workers Tvrtko Ursulin
2026-07-17 7:28 ` [RFC v2 2/2] drm/panthor: Create per queue priority workqueues Tvrtko Ursulin
0 siblings, 2 replies; 6+ messages in thread
From: Tvrtko Ursulin @ 2026-07-17 7:28 UTC (permalink / raw)
To: dri-devel
Cc: Boris Brezillon, Steven Price, Liviu Dudau, Chia-I Wu,
Matthew Brost, kernel-dev, linux-kernel, Tvrtko Ursulin,
Chia-I Wu, Tejun Heo
This is a continuation of the previous discussion which was here:
https://lore.kernel.org/dri-devel/20260702143745.79293-1-tvrtko.ursulin@igalia.com/
Work is now converted to a much simpler approach by adding real-time scheduling
workqueues based on Tejun's feedback
To re-cap, when an userspace thread submits GPU work, due how the DRM scheduler
uses workqueues to feed the GPU, and regardless of the GPU rendering context
priority, or the CPU scheduling priority of the userspace thread itself, the
use of workqueues adds (a lot of) latency to the submit path.
When CPU is busy with enough backround load this translates to severe latency
spikes measured as time between userspace submitting work and GPU actually being
given that work to execute.
With the panthor workqueue upgraded to use WQ_HIGHPRI and varying the CPU
priority of the submit thread, the test program from
https://gitlab.freedesktop.org/panfrost/linux/-/work_items/49 reproduces these
kind of latencies:
. N RT
M 27 28 32
95% 163 246 809
98% 924 991 1882
Legend:
M = Median submit latency in us
95% = Percentile latency in us
. = Userspace submit thread SCHED_OTHER
N = -||= nice -1
RT = -||- FIFO 1
Upgrading the panthor workqueues so that the realtime GPU priority queue uses
WQ_RTPRI, submit latency becomes completely controlled with the median of 14us
and 95 and 98-th percentiles at 23us and 25us respectively.
Important to note is that VK_QUEUE_GLOBAL_PRIORITY_REALTIME already required
the userspace to have CAP_SYS_NICE, meaning access to real-time workqueues is
effectively also guared behind this capability. The implementation of WQ_RTPRI
itself also adds some limites on the maximum number of worker threads in order
to prevent scheduling starvation.
v2:
* See patch 1 changelog.
Cc: Boris Brezillon <boris.brezillon@collabora.com>
Cc: Chia-I Wu <olv@google.com>
Cc: Liviu Dudau <liviu.dudau@arm.com>
Cc: Matthew Brost <matthew.brost@intel.com>
Cc: Steven Price <steven.price@arm.com>
Cc: Tejun Heo <tj@kernel.org>
Tvrtko Ursulin (2):
workqueue: Add support for real-time workers
drm/panthor: Create per queue priority workqueues
drivers/gpu/drm/panthor/panthor_sched.c | 38 +++++-
include/linux/workqueue.h | 23 +++-
kernel/workqueue.c | 149 ++++++++++++++++++------
3 files changed, 166 insertions(+), 44 deletions(-)
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC v2 1/2] workqueue: Add support for real-time workers
2026-07-17 7:28 [RFC v2 0/2] Realtime workqueues and panthor realtime submission Tvrtko Ursulin
@ 2026-07-17 7:28 ` Tvrtko Ursulin
2026-07-17 11:25 ` Bradley Morgan
2026-07-17 7:28 ` [RFC v2 2/2] drm/panthor: Create per queue priority workqueues Tvrtko Ursulin
1 sibling, 1 reply; 6+ messages in thread
From: Tvrtko Ursulin @ 2026-07-17 7:28 UTC (permalink / raw)
To: dri-devel
Cc: Boris Brezillon, Steven Price, Liviu Dudau, Chia-I Wu,
Matthew Brost, kernel-dev, linux-kernel, Tvrtko Ursulin,
Chia-I Wu, Tejun Heo
For use cases such as the DRM scheduler submitting work to the GPU on
behalf of low latency userspace applications, where latter have sufficient
privileges to have had successfully obtained realtime Vulkan global
priority, competing with random background CPU load can create large
latency spikes which gets in the way of a smooth user experience.
For these situations the existing WQ_HIGHPRI does not bring a noticeable
improvement and a stronger hint is needed.
Lets add WQ_RTPRI which creates workers with a SCHED_FIFO scheduling class
to improve this.
We use a minimum priority level since we only care about winning the
contest against normal background CPU load. We also limit the number of
instantiated threads, both per workqueue to a maximum of two, or one on a
dual processor machine, and system-wide to one less than the number of
CPUs. The idea of that is to prevent system wide starvation caused by
potentially misbehaving work items.
Signed-off-by: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>
Cc: Boris Brezillon <boris.brezillon@collabora.com>
Cc: Chia-I Wu <olv@google.com>
Cc: Liviu Dudau <liviu.dudau@arm.com>
Cc: Matthew Brost <matthew.brost@intel.com>
Cc: Steven Price <steven.price@arm.com>
Cc: Tejun Heo <tj@kernel.org>
---
v2:
* Limit WQ_RTPRI to unbound workqueues and make it have strict CPU
affinitity. (Tejun)
* Fixed commit message typos. (AI)
* Fixed sysfs handling, max_active setting and user modified nice
application. (AI)
---
include/linux/workqueue.h | 23 ++++--
kernel/workqueue.c | 149 +++++++++++++++++++++++++++++---------
2 files changed, 133 insertions(+), 39 deletions(-)
diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index 6177624539b3..bf92271501f0 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -140,6 +140,13 @@ enum wq_affn_scope {
WQ_AFFN_NR_TYPES,
};
+enum wq_priority {
+ WQ_PRIO_NORMAL = 0,
+ WQ_PRIO_HIGH = 1,
+ WQ_PRIO_RT = 2,
+ NUM_WQ_PRIO, /* Keep last */
+};
+
/**
* struct workqueue_attrs - A struct for workqueue attributes.
*
@@ -147,7 +154,12 @@ enum wq_affn_scope {
*/
struct workqueue_attrs {
/**
- * @nice: nice level
+ * @prio: priority level
+ */
+ enum wq_priority prio;
+
+ /**
+ * @nice: nice level for WQ_PRIO_HIGH
*/
int nice;
@@ -374,8 +386,9 @@ enum wq_flags {
WQ_FREEZABLE = 1 << 2, /* freeze during suspend */
WQ_MEM_RECLAIM = 1 << 3, /* may be used for memory reclaim */
WQ_HIGHPRI = 1 << 4, /* high priority */
- WQ_CPU_INTENSIVE = 1 << 5, /* cpu intensive workqueue */
- WQ_SYSFS = 1 << 6, /* visible in sysfs, see workqueue_sysfs_register() */
+ WQ_RTPRI = 1 << 5, /* real-time priority, valid only with WQ_UNBOUND */
+ WQ_CPU_INTENSIVE = 1 << 6, /* cpu intensive workqueue */
+ WQ_SYSFS = 1 << 7, /* visible in sysfs, see workqueue_sysfs_register() */
/*
* Per-cpu workqueues are generally preferred because they tend to
@@ -402,8 +415,8 @@ enum wq_flags {
*
* http://thread.gmane.org/gmane.linux.kernel/1480396
*/
- WQ_POWER_EFFICIENT = 1 << 7,
- WQ_PERCPU = 1 << 8, /* bound to a specific cpu */
+ WQ_POWER_EFFICIENT = 1 << 8,
+ WQ_PERCPU = 1 << 9, /* bound to a specific cpu */
__WQ_DESTROYING = 1 << 15, /* internal: workqueue is destroying */
__WQ_DRAINING = 1 << 16, /* internal: workqueue is draining */
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 33b721a9af02..f15aa9d0f2d3 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -509,10 +509,10 @@ static DEFINE_IDR(worker_pool_idr); /* PR: idr of all pools */
static DEFINE_HASHTABLE(unbound_pool_hash, UNBOUND_POOL_HASH_ORDER);
/* I: attributes used when instantiating standard unbound pools on demand */
-static struct workqueue_attrs *unbound_std_wq_attrs[NR_STD_WORKER_POOLS];
+static struct workqueue_attrs *unbound_std_wq_attrs[NR_STD_WORKER_POOLS + 1];
/* I: attributes used when instantiating ordered pools on demand */
-static struct workqueue_attrs *ordered_wq_attrs[NR_STD_WORKER_POOLS];
+static struct workqueue_attrs *ordered_wq_attrs[NR_STD_WORKER_POOLS + 1];
/*
* I: kthread_worker to release pwq's. pwq release needs to be bounced to a
@@ -546,6 +546,8 @@ EXPORT_SYMBOL_GPL(system_bh_highpri_wq);
struct workqueue_struct *system_dfl_long_wq __ro_after_init;
EXPORT_SYMBOL_GPL(system_dfl_long_wq);
+static atomic_t total_rtpri_workers = ATOMIC_INIT(0);
+
static int worker_thread(void *__worker);
static void workqueue_sysfs_unregister(struct workqueue_struct *wq);
static void show_pwq(struct pool_workqueue *pwq);
@@ -1236,9 +1238,7 @@ static bool assign_work(struct work_struct *work, struct worker *worker,
static struct irq_work *bh_pool_irq_work(struct worker_pool *pool)
{
- int high = pool->attrs->nice == HIGHPRI_NICE_LEVEL ? 1 : 0;
-
- return &per_cpu(bh_pool_irq_works, pool->cpu)[high];
+ return &per_cpu(bh_pool_irq_works, pool->cpu)[pool->attrs->prio];
}
static void kick_bh_pool(struct worker_pool *pool)
@@ -1251,7 +1251,7 @@ static void kick_bh_pool(struct worker_pool *pool)
return;
}
#endif
- if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
+ if (pool->attrs->prio >= WQ_PRIO_HIGH)
raise_softirq_irqoff(HI_SOFTIRQ);
else
raise_softirq_irqoff(TASKLET_SOFTIRQ);
@@ -2801,13 +2801,16 @@ static int format_worker_id(char *buf, size_t size, struct worker *worker,
worker->rescue_wq->name);
if (pool) {
- if (pool->cpu >= 0)
+ if (pool->cpu >= 0) {
+ const char *suffix[NUM_WQ_PRIO] = { "", "H", "R" };
+
return scnprintf(buf, size, "kworker/%d:%d%s",
pool->cpu, worker->id,
- pool->attrs->nice < 0 ? "H" : "");
- else
+ suffix[pool->attrs->prio]);
+ } else {
return scnprintf(buf, size, "kworker/u%d:%d",
pool->id, worker->id);
+ }
} else {
return scnprintf(buf, size, "kworker/dying");
}
@@ -2830,12 +2833,30 @@ static struct worker *create_worker(struct worker_pool *pool)
struct worker *worker;
int id;
+ /*
+ * Do not consume all CPUs with RT workers to avoid scheduler
+ * starvation.
+ */
+ if (pool->attrs->prio == WQ_PRIO_RT) {
+ unsigned int max = num_online_cpus();
+
+ if (max > 2)
+ max = max - 1;
+ else
+ max = 1;
+
+ if (atomic_inc_return(&total_rtpri_workers) > max) {
+ atomic_dec(&total_rtpri_workers);
+ return NULL;
+ }
+ }
+
/* ID is needed to determine kthread name */
id = ida_alloc(&pool->worker_ida, GFP_KERNEL);
if (id < 0) {
pr_err_once("workqueue: Failed to allocate a worker ID: %pe\n",
ERR_PTR(id));
- return NULL;
+ goto fail_ida;
}
worker = alloc_worker(pool->node);
@@ -2863,7 +2884,11 @@ static struct worker *create_worker(struct worker_pool *pool)
goto fail;
}
- set_user_nice(worker->task, pool->attrs->nice);
+ if (pool->attrs->prio == WQ_PRIO_RT)
+ sched_set_fifo_low(worker->task);
+ else
+ set_user_nice(worker->task, pool->attrs->nice);
+
kthread_bind_mask(worker->task, pool_allowed_cpus(pool));
}
@@ -2888,6 +2913,9 @@ static struct worker *create_worker(struct worker_pool *pool)
return worker;
+fail_ida:
+ if (pool->attrs->prio == WQ_PRIO_RT)
+ atomic_dec(&total_rtpri_workers);
fail:
ida_free(&pool->worker_ida, id);
kfree(worker);
@@ -2909,6 +2937,8 @@ static void reap_dying_workers(struct list_head *cull_list)
list_for_each_entry_safe(worker, tmp, cull_list, entry) {
list_del_init(&worker->entry);
kthread_stop_put(worker->task);
+ if (worker->pool->attrs->prio == WQ_PRIO_RT)
+ atomic_dec(&total_rtpri_workers);
kfree(worker);
}
}
@@ -3765,7 +3795,7 @@ static void drain_dead_softirq_workfn(struct work_struct *work)
* don't hog this CPU's BH.
*/
if (repeat) {
- if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
+ if (pool->attrs->prio >= WQ_PRIO_HIGH)
queue_work(system_bh_highpri_wq, work);
else
queue_work(system_bh_wq, work);
@@ -3797,7 +3827,7 @@ void workqueue_softirq_dead(unsigned int cpu)
dead_work.pool = pool;
init_completion(&dead_work.done);
- if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
+ if (pool->attrs->prio >= WQ_PRIO_HIGH)
queue_work(system_bh_highpri_wq, &dead_work.work);
else
queue_work(system_bh_wq, &dead_work.work);
@@ -4772,6 +4802,7 @@ struct workqueue_attrs *alloc_workqueue_attrs_noprof(void)
static void copy_workqueue_attrs(struct workqueue_attrs *to,
const struct workqueue_attrs *from)
{
+ to->prio = from->prio;
to->nice = from->nice;
cpumask_copy(to->cpumask, from->cpumask);
cpumask_copy(to->__pod_cpumask, from->__pod_cpumask);
@@ -4803,6 +4834,7 @@ static u32 wqattrs_hash(const struct workqueue_attrs *attrs)
{
u32 hash = 0;
+ hash = jhash_1word(attrs->prio, hash);
hash = jhash_1word(attrs->nice, hash);
hash = jhash_1word(attrs->affn_strict, hash);
hash = jhash(cpumask_bits(attrs->__pod_cpumask),
@@ -4817,6 +4849,8 @@ static u32 wqattrs_hash(const struct workqueue_attrs *attrs)
static bool wqattrs_equal(const struct workqueue_attrs *a,
const struct workqueue_attrs *b)
{
+ if (a->prio != b->prio)
+ return false;
if (a->nice != b->nice)
return false;
if (a->affn_strict != b->affn_strict)
@@ -5603,11 +5637,17 @@ static void unbound_wq_update_pwq(struct workqueue_struct *wq, int cpu)
static int alloc_and_link_pwqs(struct workqueue_struct *wq)
{
- bool highpri = wq->flags & WQ_HIGHPRI;
- int cpu, ret;
+ int prio, cpu, ret;
lockdep_assert_held(&wq_pool_mutex);
+ if (wq->flags & WQ_RTPRI)
+ prio = WQ_PRIO_RT;
+ else if (wq->flags & WQ_HIGHPRI)
+ prio = WQ_PRIO_HIGH;
+ else
+ prio = WQ_PRIO_NORMAL;
+
wq->cpu_pwq = alloc_percpu(struct pool_workqueue *);
if (!wq->cpu_pwq)
goto enomem;
@@ -5624,7 +5664,7 @@ static int alloc_and_link_pwqs(struct workqueue_struct *wq)
struct pool_workqueue **pwq_p;
struct worker_pool *pool;
- pool = &(per_cpu_ptr(pools, cpu)[highpri]);
+ pool = &(per_cpu_ptr(pools, cpu)[prio]);
pwq_p = per_cpu_ptr(wq->cpu_pwq, cpu);
*pwq_p = kmem_cache_alloc_node(pwq_cache, GFP_KERNEL,
@@ -5644,14 +5684,14 @@ static int alloc_and_link_pwqs(struct workqueue_struct *wq)
if (wq->flags & __WQ_ORDERED) {
struct pool_workqueue *dfl_pwq;
- ret = apply_workqueue_attrs_locked(wq, ordered_wq_attrs[highpri]);
+ ret = apply_workqueue_attrs_locked(wq, ordered_wq_attrs[prio]);
/* there should only be single pwq for ordering guarantee */
dfl_pwq = rcu_access_pointer(wq->dfl_pwq);
WARN(!ret && (wq->pwqs.next != &dfl_pwq->pwqs_node ||
wq->pwqs.prev != &dfl_pwq->pwqs_node),
"ordering guarantee broken for workqueue %s\n", wq->name);
} else {
- ret = apply_workqueue_attrs_locked(wq, unbound_std_wq_attrs[highpri]);
+ ret = apply_workqueue_attrs_locked(wq, unbound_std_wq_attrs[prio]);
}
if (ret)
@@ -5816,6 +5856,12 @@ static struct workqueue_struct *__alloc_workqueue(const char *fmt,
return NULL;
}
+ if (flags & WQ_RTPRI) {
+ if (WARN_ON_ONCE((flags & (WQ_HIGHPRI | WQ_UNBOUND)) !=
+ WQ_UNBOUND))
+ return NULL;
+ }
+
/* see the comment above the definition of WQ_POWER_EFFICIENT */
if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
flags |= WQ_UNBOUND;
@@ -5842,7 +5888,17 @@ static struct workqueue_struct *__alloc_workqueue(const char *fmt,
pr_warn_once("workqueue: name exceeds WQ_NAME_LEN. Truncating to: %s\n",
wq->name);
- if (flags & WQ_BH) {
+ if (flags & WQ_RTPRI) {
+ /*
+ * RT workqueues are limited to max half of possible CPUs to
+ * avoid scheduling starvation and have strict CPU affinity for
+ * low latency execution.
+ */
+ max_active = min_t(int, max_active,
+ DIV_ROUND_UP(num_possible_cpus(), 2));
+ wq->unbound_attrs->affn_scope = WQ_AFFN_CPU;
+ wq->unbound_attrs->affn_strict = true;
+ } else if (flags & WQ_BH) {
/*
* BH workqueues always share a single execution context per CPU
* and don't impose any max_active limit.
@@ -6344,17 +6400,26 @@ void print_worker_info(const char *log_lvl, struct task_struct *task)
}
}
+static void pr_cont_bh_suffix(enum wq_priority prio)
+{
+ if (prio == WQ_PRIO_RT)
+ pr_cont("bh-rt");
+ else if (prio == WQ_PRIO_HIGH)
+ pr_cont("bh-hi");
+ else
+ pr_cont("bh");
+}
+
static void pr_cont_pool_info(struct worker_pool *pool)
{
pr_cont(" cpus=%*pbl", nr_cpumask_bits, pool->attrs->cpumask);
if (pool->node != NUMA_NO_NODE)
pr_cont(" node=%d", pool->node);
- pr_cont(" flags=0x%x", pool->flags);
+ pr_cont(" flags=0x%x ", pool->flags);
if (pool->flags & POOL_BH)
- pr_cont(" bh%s",
- pool->attrs->nice == HIGHPRI_NICE_LEVEL ? "-hi" : "");
+ pr_cont_bh_suffix(pool->attrs->prio);
else
- pr_cont(" nice=%d", pool->attrs->nice);
+ pr_cont("prio=%d", pool->attrs->prio);
}
static void pr_cont_worker_id(struct worker *worker)
@@ -6362,8 +6427,7 @@ static void pr_cont_worker_id(struct worker *worker)
struct worker_pool *pool = worker->pool;
if (pool->flags & POOL_BH)
- pr_cont("bh%s",
- pool->attrs->nice == HIGHPRI_NICE_LEVEL ? "-hi" : "");
+ pr_cont_bh_suffix(pool->attrs->prio);
else
pr_cont("%d%s", task_pid_nr(worker->task),
worker->rescue_wq ? "(RESCUER)" : "");
@@ -7287,7 +7351,11 @@ static ssize_t wq_nice_show(struct device *dev, struct device_attribute *attr,
int written;
mutex_lock(&wq->mutex);
- written = scnprintf(buf, PAGE_SIZE, "%d\n", wq->unbound_attrs->nice);
+ if (wq->unbound_attrs->prio != WQ_PRIO_RT)
+ written = scnprintf(buf, PAGE_SIZE, "%d\n",
+ wq->unbound_attrs->nice);
+ else
+ written = -EINVAL;
mutex_unlock(&wq->mutex);
return written;
@@ -7313,13 +7381,20 @@ static ssize_t wq_nice_store(struct device *dev, struct device_attribute *attr,
{
struct workqueue_struct *wq = dev_to_wq(dev);
struct workqueue_attrs *attrs;
- int ret = -ENOMEM;
+ int ret;
apply_wqattrs_lock();
attrs = wq_sysfs_prep_attrs(wq);
- if (!attrs)
+ if (!attrs) {
+ ret = -ENOMEM;
goto out_unlock;
+ }
+
+ if (attrs->prio == WQ_PRIO_RT) {
+ ret = -EINVAL;
+ goto out_unlock;
+ }
if (sscanf(buf, "%d", &attrs->nice) == 1 &&
attrs->nice >= MIN_NICE && attrs->nice <= MAX_NICE)
@@ -7927,12 +8002,13 @@ static void __init restrict_unbound_cpumask(const char *name, const struct cpuma
cpumask_and(wq_unbound_cpumask, wq_unbound_cpumask, mask);
}
-static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, int nice)
+static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, enum wq_priority prio, int nice)
{
BUG_ON(init_worker_pool(pool));
pool->cpu = cpu;
cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu));
cpumask_copy(pool->attrs->__pod_cpumask, cpumask_of(cpu));
+ pool->attrs->prio = prio;
pool->attrs->nice = nice;
pool->attrs->affn_strict = true;
pool->node = cpu_to_node(cpu);
@@ -7956,7 +8032,8 @@ static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, int n
void __init workqueue_init_early(void)
{
struct wq_pod_type *pt = &wq_pod_types[WQ_AFFN_SYSTEM];
- int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
+ int std_prio[NR_STD_WORKER_POOLS + 1] = { 0, WQ_PRIO_HIGH, WQ_PRIO_RT };
+ int std_nice[NR_STD_WORKER_POOLS + 1] = { 0, HIGHPRI_NICE_LEVEL, 0 };
void (*irq_work_fns[NR_STD_WORKER_POOLS])(struct irq_work *) =
{ bh_pool_kick_normal, bh_pool_kick_highpri };
int i, cpu;
@@ -8008,22 +8085,25 @@ void __init workqueue_init_early(void)
i = 0;
for_each_bh_worker_pool(pool, cpu) {
- init_cpu_worker_pool(pool, cpu, std_nice[i]);
+ init_cpu_worker_pool(pool, cpu, std_prio[i], std_nice[i]);
pool->flags |= POOL_BH;
init_irq_work(bh_pool_irq_work(pool), irq_work_fns[i]);
i++;
}
i = 0;
- for_each_cpu_worker_pool(pool, cpu)
- init_cpu_worker_pool(pool, cpu, std_nice[i++]);
+ for_each_cpu_worker_pool(pool, cpu) {
+ init_cpu_worker_pool(pool, cpu, std_prio[i], std_nice[i]);
+ i++;
+ }
}
/* create default unbound and ordered wq attrs */
- for (i = 0; i < NR_STD_WORKER_POOLS; i++) {
+ for (i = 0; i < NR_STD_WORKER_POOLS + 1; i++) {
struct workqueue_attrs *attrs;
BUG_ON(!(attrs = alloc_workqueue_attrs()));
+ attrs->prio = std_prio[i];
attrs->nice = std_nice[i];
unbound_std_wq_attrs[i] = attrs;
@@ -8032,6 +8112,7 @@ void __init workqueue_init_early(void)
* guaranteed by max_active which is enforced by pwqs.
*/
BUG_ON(!(attrs = alloc_workqueue_attrs()));
+ attrs->prio = std_prio[i];
attrs->nice = std_nice[i];
attrs->ordered = true;
ordered_wq_attrs[i] = attrs;
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC v2 2/2] drm/panthor: Create per queue priority workqueues
2026-07-17 7:28 [RFC v2 0/2] Realtime workqueues and panthor realtime submission Tvrtko Ursulin
2026-07-17 7:28 ` [RFC v2 1/2] workqueue: Add support for real-time workers Tvrtko Ursulin
@ 2026-07-17 7:28 ` Tvrtko Ursulin
1 sibling, 0 replies; 6+ messages in thread
From: Tvrtko Ursulin @ 2026-07-17 7:28 UTC (permalink / raw)
To: dri-devel
Cc: Boris Brezillon, Steven Price, Liviu Dudau, Chia-I Wu,
Matthew Brost, kernel-dev, linux-kernel, Tvrtko Ursulin,
Chia-I Wu, Tejun Heo
Split the single workqueue shared between the driver internal logic and
DRM scheduler use into separate ones, where the DRM scheduler one is
created per GPU priority level using the appropriate mapping to
workqueue priorities.
Low and medium GPU priority are served by a normal workqueue,
high is server by a WQ_HIGHPRI instance, while realtime GPU priority is
using the newly added WQ_RTPRI flag for lowest possible latency.
These workqueues are device global and for all three we set the maximum
concurrency to two in order to keep the GPU optimally fed with work.
Signed-off-by: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>
Cc: Boris Brezillon <boris.brezillon@collabora.com>
Cc: Chia-I Wu <olv@google.com>
Cc: Liviu Dudau <liviu.dudau@arm.com>
Cc: Matthew Brost <matthew.brost@intel.com>
Cc: Steven Price <steven.price@arm.com>
Cc: Tejun Heo <tj@kernel.org>
---
drivers/gpu/drm/panthor/panthor_sched.c | 38 +++++++++++++++++++++----
1 file changed, 33 insertions(+), 5 deletions(-)
diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
index 2bee1c92fb9e..f01adc8c024c 100644
--- a/drivers/gpu/drm/panthor/panthor_sched.c
+++ b/drivers/gpu/drm/panthor/panthor_sched.c
@@ -152,11 +152,18 @@ struct panthor_scheduler {
*
* Used for the scheduler tick, group update or other kind of FW
* event processing that can't be handled in the threaded interrupt
- * path. Also passed to the drm_gpu_scheduler instances embedded
- * in panthor_queue.
+ * path.
*/
struct workqueue_struct *wq;
+ /**
+ * @submit_wq: Per priority workqueues for the DRM scheduler
+ *
+ * Passed to the drm_gpu_scheduler instances embedded
+ * in panthor_queue based on the queue priority.
+ */
+ struct workqueue_struct *submit_wq[PANTHOR_CSG_PRIORITY_COUNT];
+
/**
* @heap_alloc_wq: Workqueue used to schedule tiler_oom works.
*
@@ -3488,7 +3495,6 @@ group_create_queue(struct panthor_group *group,
{
struct drm_sched_init_args sched_args = {
.ops = &panthor_queue_sched_ops,
- .submit_wq = group->ptdev->scheduler->wq,
/*
* The credit limit argument tells us the total number of
* instructions across all CS slots in the ringbuffer, with
@@ -3581,8 +3587,14 @@ group_create_queue(struct panthor_group *group,
goto err_free_queue;
}
+ if (group->priority >= ARRAY_SIZE(group->ptdev->scheduler->submit_wq) ||
+ !group->ptdev->scheduler->submit_wq[group->priority]) {
+ ret = -EINVAL;
+ goto err_free_queue;
+ }
+
sched_args.name = queue->name;
-
+ sched_args.submit_wq = group->ptdev->scheduler->submit_wq[group->priority];
ret = drm_sched_init(&queue->scheduler, &sched_args);
if (ret)
goto err_free_queue;
@@ -4072,6 +4084,15 @@ static void panthor_sched_fini(struct drm_device *ddev, void *res)
if (!sched || !sched->csg_slot_count)
return;
+ if (sched->submit_wq[PANTHOR_CSG_PRIORITY_MEDIUM])
+ destroy_workqueue(sched->submit_wq[PANTHOR_CSG_PRIORITY_MEDIUM]);
+
+ if (sched->submit_wq[PANTHOR_CSG_PRIORITY_HIGH])
+ destroy_workqueue(sched->submit_wq[PANTHOR_CSG_PRIORITY_HIGH]);
+
+ if (sched->submit_wq[PANTHOR_CSG_PRIORITY_RT])
+ destroy_workqueue(sched->submit_wq[PANTHOR_CSG_PRIORITY_RT]);
+
if (sched->wq)
destroy_workqueue(sched->wq);
@@ -4173,7 +4194,14 @@ int panthor_sched_init(struct panthor_device *ptdev)
*/
sched->heap_alloc_wq = alloc_workqueue("panthor-heap-alloc", WQ_UNBOUND, 0);
sched->wq = alloc_workqueue("panthor-csf-sched", WQ_MEM_RECLAIM | WQ_UNBOUND, 0);
- if (!sched->wq || !sched->heap_alloc_wq) {
+ sched->submit_wq[PANTHOR_CSG_PRIORITY_MEDIUM] = alloc_workqueue("panthor-drm", WQ_MEM_RECLAIM | WQ_UNBOUND, 2);
+ sched->submit_wq[PANTHOR_CSG_PRIORITY_LOW] = sched->submit_wq[PANTHOR_CSG_PRIORITY_MEDIUM];
+ sched->submit_wq[PANTHOR_CSG_PRIORITY_HIGH] = alloc_workqueue("panthor-drm-high", WQ_HIGHPRI | WQ_MEM_RECLAIM | WQ_UNBOUND, 2);
+ sched->submit_wq[PANTHOR_CSG_PRIORITY_RT] = alloc_workqueue("panthor-drm-rt", WQ_RTPRI | WQ_MEM_RECLAIM | WQ_UNBOUND, 2);
+ if (!sched->wq || !sched->heap_alloc_wq ||
+ !sched->submit_wq[PANTHOR_CSG_PRIORITY_MEDIUM] ||
+ !sched->submit_wq[PANTHOR_CSG_PRIORITY_HIGH] ||
+ !sched->submit_wq[PANTHOR_CSG_PRIORITY_RT]) {
panthor_sched_fini(&ptdev->base, sched);
drm_err(&ptdev->base, "Failed to allocate the workqueues");
return -ENOMEM;
--
2.54.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC v2 1/2] workqueue: Add support for real-time workers
2026-07-17 7:28 ` [RFC v2 1/2] workqueue: Add support for real-time workers Tvrtko Ursulin
@ 2026-07-17 11:25 ` Bradley Morgan
2026-07-17 11:51 ` Tvrtko Ursulin
0 siblings, 1 reply; 6+ messages in thread
From: Bradley Morgan @ 2026-07-17 11:25 UTC (permalink / raw)
To: tvrtko.ursulin
Cc: boris.brezillon, dri-devel, kernel-dev, linux-kernel,
liviu.dudau, matthew.brost, olv, olvaffe, steven.price, tj
Hi Tvrtko,
Applies to master. On linux-next one hunk rejects, the WQ_RTPRI block
in __alloc_workqueue(), the rest is clean. Rebase v3 onto the wq tree,
please..
Things to fix:
> +fail_ida:
> + if (pool->attrs->prio == WQ_PRIO_RT)
> + atomic_dec(&total_rtpri_workers);
> fail:
> ida_free(&pool->worker_ida, id);
> kfree(worker);
Labels swapped. The ida_alloc() failure falls into fail:, so ida_free()
gets a negative id and kfree() an uninitialized pointer, and the
later failure paths leak the counter. The fix is something like,
id = ida_alloc(&pool->worker_ida, GFP_KERNEL);
if (id < 0)
goto fail_dec;
...
fail:
ida_free(&pool->worker_ida, id);
kfree(worker);
fail_dec:
if (pool->attrs->prio == WQ_PRIO_RT)
atomic_dec(&total_rtpri_workers);
return NULL;
> + if (worker->pool->attrs->prio == WQ_PRIO_RT)
> + atomic_dec(&total_rtpri_workers);
worker->pool is NULL here, the WORKER_DIE path clears it before
kthread_stop_put() returns, so every cull oopses.
Flag the worker at creation, a bool rtpri in struct worker:
worker->rtpri = pool->attrs->prio == WQ_PRIO_RT;
and in reap_dying_workers():
- if (worker->pool->attrs->prio == WQ_PRIO_RT)
+ if (worker->rtpri)
atomic_dec(&total_rtpri_workers);
> + wq->unbound_attrs->affn_scope = WQ_AFFN_CPU;
> + wq->unbound_attrs->affn_strict = true;
Its a dead store, apply_wqattrs_commit() overwrites both, so the headline
v2
change never engages. Drop these two lines and mark the RT entries in
the workqueue_init_early() attrs loops instead, for both
unbound_std_wq_attrs and ordered_wq_attrs:
attrs->prio = std_prio[i];
attrs->nice = std_nice[i];
+ if (attrs->prio == WQ_PRIO_RT) {
+ attrs->affn_scope = WQ_AFFN_CPU;
+ attrs->affn_strict = true;
+ }
Questions: the changelog says two workers per workqueue, but the code clamps max_active, which caps work items, not threads. At the global cap create_worker() fails silently and maybe_create_worker() retries forever, so the pool stalls; with strict pods the last pod on an N CPU
box may never get a worker. pr_warn() or fall back to a normal worker?
Also the rescuer is not FIFO, so WQ_MEM_RECLAIM plus WQ_RTPRI loses
the latency claim exactly under memory pressure.
Nits: the "R" suffix branch is dead code, RT is unbound only. The
affinity sysfs files can still undo the forced affinity.
Id prefer NUM_WQ_PRIO over NR_STD_WORKER_POOLS + 1. workqueue.rst needs
WQ_RTPRI.
With the rebase and fixes, please add:
Reviewed-by: Bradley Morgan <include@grrlz.net> # kernel/
Thanks for the patch!
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC v2 1/2] workqueue: Add support for real-time workers
2026-07-17 11:25 ` Bradley Morgan
@ 2026-07-17 11:51 ` Tvrtko Ursulin
2026-07-17 11:55 ` Bradley Morgan
0 siblings, 1 reply; 6+ messages in thread
From: Tvrtko Ursulin @ 2026-07-17 11:51 UTC (permalink / raw)
To: Bradley Morgan
Cc: boris.brezillon, dri-devel, kernel-dev, linux-kernel,
liviu.dudau, matthew.brost, olv, olvaffe, steven.price, tj
Hi,
Is this a human review, or AI review, or combined?
On 17/07/2026 12:25, Bradley Morgan wrote:
> Hi Tvrtko,
>
> Applies to master. On linux-next one hunk rejects, the WQ_RTPRI block
> in __alloc_workqueue(), the rest is clean. Rebase v3 onto the wq tree,
> please..
Fixed in v3 already.
>
> Things to fix:
>
>> +fail_ida:
>> + if (pool->attrs->prio == WQ_PRIO_RT)
>> + atomic_dec(&total_rtpri_workers);
>> fail:
>> ida_free(&pool->worker_ida, id);
>> kfree(worker);
>
> Labels swapped. The ida_alloc() failure falls into fail:, so ida_free()
> gets a negative id and kfree() an uninitialized pointer, and the
> later failure paths leak the counter. The fix is something like,
>
> id = ida_alloc(&pool->worker_ida, GFP_KERNEL);
> if (id < 0)
> goto fail_dec;
> ...
> fail:
> ida_free(&pool->worker_ida, id);
> kfree(worker);
> fail_dec:
> if (pool->attrs->prio == WQ_PRIO_RT)
> atomic_dec(&total_rtpri_workers);
> return NULL;
Already fixed locally after Sashiko flagged the onion unwind fail.
>
>> + if (worker->pool->attrs->prio == WQ_PRIO_RT)
>> + atomic_dec(&total_rtpri_workers);
>
> worker->pool is NULL here, the WORKER_DIE path clears it before
> kthread_stop_put() returns, so every cull oopses.
>
> Flag the worker at creation, a bool rtpri in struct worker:
>
> worker->rtpri = pool->attrs->prio == WQ_PRIO_RT;
>
> and in reap_dying_workers():
>
> - if (worker->pool->attrs->prio == WQ_PRIO_RT)
> + if (worker->rtpri)
> atomic_dec(&total_rtpri_workers);
Ditto but differently.
>> + wq->unbound_attrs->affn_scope = WQ_AFFN_CPU;
>> + wq->unbound_attrs->affn_strict = true;
>
> Its a dead store, apply_wqattrs_commit() overwrites both, so the headline
> v2
> change never engages. Drop these two lines and mark the RT entries in
> the workqueue_init_early() attrs loops instead, for both
> unbound_std_wq_attrs and ordered_wq_attrs:
>
> attrs->prio = std_prio[i];
> attrs->nice = std_nice[i];
>
> + if (attrs->prio == WQ_PRIO_RT) {
> + attrs->affn_scope = WQ_AFFN_CPU;
> + attrs->affn_strict = true;
> + }
Already done locally exactly like this.
> Questions: the changelog says two workers per workqueue, but the code clamps max_active, which caps work items, not threads. At the global cap create_worker() fails silently and maybe_create_worker() retries forever, so the pool stalls; with strict pods the last pod on an N CPU
> box may never get a worker. pr_warn() or fall back to a normal worker?
Yeah it's a design open. I will probably just drop the global limits if
Tejun agrees.
> Also the rescuer is not FIFO, so WQ_MEM_RECLAIM plus WQ_RTPRI loses
> the latency claim exactly under memory pressure.
Sashiko commented on this already. But I don't think latency is super
relevant under memory pressure.
> Nits: the "R" suffix branch is dead code, RT is unbound only. The
Curiosly lkml Sashiko missed this but I am aware of it. It is leftover
from v1. And it's not a branch but an extra array element.
> affinity sysfs files can still undo the forced affinity.
I am opting letting the admins shoot themselves in the foot.
> Id prefer NUM_WQ_PRIO over NR_STD_WORKER_POOLS + 1. workqueue.rst needs
> WQ_RTPRI.
I was on the fence. Either way is not ideal.
RST noted.
> With the rebase and fixes, please add:
>
> Reviewed-by: Bradley Morgan <include@grrlz.net> # kernel/
>
> Thanks for the patch!
Thank you for reading through it!
Regards,
Tvrtko
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC v2 1/2] workqueue: Add support for real-time workers
2026-07-17 11:51 ` Tvrtko Ursulin
@ 2026-07-17 11:55 ` Bradley Morgan
0 siblings, 0 replies; 6+ messages in thread
From: Bradley Morgan @ 2026-07-17 11:55 UTC (permalink / raw)
To: Tvrtko Ursulin
Cc: boris.brezillon, dri-devel, kernel-dev, linux-kernel,
liviu.dudau, matthew.brost, olv, olvaffe, steven.price, tj
On July 17, 2026 12:51:06 PM GMT+01:00, Tvrtko Ursulin
<tvrtko.ursulin@igalia.com> wrote:
>
>Hi,
>
>Is this a human review, or AI review, or combined?
Human :)
>On 17/07/2026 12:25, Bradley Morgan wrote:
>> Hi Tvrtko,
>>
>> Applies to master. On linux-next one hunk rejects, the WQ_RTPRI block
>> in __alloc_workqueue(), the rest is clean. Rebase v3 onto the wq tree,
>> please..
>
>Fixed in v3 already.
>
>>
>> Things to fix:
>>
>>> +fail_ida:
>>> + if (pool->attrs->prio == WQ_PRIO_RT)
>>> + atomic_dec(&total_rtpri_workers);
>>> fail:
>>> ida_free(&pool->worker_ida, id);
>>> kfree(worker);
>>
>> Labels swapped. The ida_alloc() failure falls into fail:, so ida_free()
>> gets a negative id and kfree() an uninitialized pointer, and the
>> later failure paths leak the counter. The fix is something like,
>>
>> id = ida_alloc(&pool->worker_ida, GFP_KERNEL);
>> if (id < 0)
>> goto fail_dec;
>> ...
>> fail:
>> ida_free(&pool->worker_ida, id);
>> kfree(worker);
>> fail_dec:
>> if (pool->attrs->prio == WQ_PRIO_RT)
>> atomic_dec(&total_rtpri_workers);
>> return NULL;
>
>Already fixed locally after Sashiko flagged the onion unwind fail.
Thanks.
>>
>>> + if (worker->pool->attrs->prio == WQ_PRIO_RT)
>>> + atomic_dec(&total_rtpri_workers);
>>
>> worker->pool is NULL here, the WORKER_DIE path clears it before
>> kthread_stop_put() returns, so every cull oopses.
>>
>> Flag the worker at creation, a bool rtpri in struct worker:
>>
>> worker->rtpri = pool->attrs->prio == WQ_PRIO_RT;
>>
>> and in reap_dying_workers():
>>
>> - if (worker->pool->attrs->prio == WQ_PRIO_RT)
>> + if (worker->rtpri)
>> atomic_dec(&total_rtpri_workers);
>
>Ditto but differently.
Thanks.
>>> + wq->unbound_attrs->affn_scope = WQ_AFFN_CPU;
>>> + wq->unbound_attrs->affn_strict = true;
>>
>> Its a dead store, apply_wqattrs_commit() overwrites both, so the
>headline
>> v2
>> change never engages. Drop these two lines and mark the RT entries in
>> the workqueue_init_early() attrs loops instead, for both
>> unbound_std_wq_attrs and ordered_wq_attrs:
>>
>> attrs->prio = std_prio[i];
>> attrs->nice = std_nice[i];
>>
>> + if (attrs->prio == WQ_PRIO_RT) {
>> + attrs->affn_scope = WQ_AFFN_CPU;
>> + attrs->affn_strict = true;
>> + }
>
>Already done locally exactly like this.
Thanks.
>> Questions: the changelog says two workers per workqueue, but the code
>clamps max_active, which caps work items, not threads. At the global cap
>create_worker() fails silently and maybe_create_worker() retries forever,
>so the pool stalls; with strict pods the last pod on an N CPU
>> box may never get a worker. pr_warn() or fall back to a normal worker?
>
>Yeah it's a design open. I will probably just drop the global limits if
>Tejun agrees.
Ok.
>> Also the rescuer is not FIFO, so WQ_MEM_RECLAIM plus WQ_RTPRI loses
>> the latency claim exactly under memory pressure.
>
>Sashiko commented on this already. But I don't think latency is super
>relevant under memory pressure.
>
>> Nits: the "R" suffix branch is dead code, RT is unbound only. The
>
>Curiosly lkml Sashiko missed this but I am aware of it. It is leftover
>from v1. And it's not a branch but an extra array element.
>
okay.
>> affinity sysfs files can still undo the forced affinity.
>
>I am opting letting the admins shoot themselves in the foot.
Heh. Fair enough
>> Id prefer NUM_WQ_PRIO over NR_STD_WORKER_POOLS + 1. workqueue.rst needs
>> WQ_RTPRI.
>
>I was on the fence. Either way is not ideal.
>
>RST noted.
Yeah. Agreed.
>> With the rebase and fixes, please add:
>>
>> Reviewed-by: Bradley Morgan <include@grrlz.net> # kernel/
>>
>> Thanks for the patch!
>
>Thank you for reading through it!
>
>Regards,
>
>Tvrtko
>
>
>
Thanks!
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-07-17 11:55 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-17 7:28 [RFC v2 0/2] Realtime workqueues and panthor realtime submission Tvrtko Ursulin
2026-07-17 7:28 ` [RFC v2 1/2] workqueue: Add support for real-time workers Tvrtko Ursulin
2026-07-17 11:25 ` Bradley Morgan
2026-07-17 11:51 ` Tvrtko Ursulin
2026-07-17 11:55 ` Bradley Morgan
2026-07-17 7:28 ` [RFC v2 2/2] drm/panthor: Create per queue priority workqueues Tvrtko Ursulin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox