mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Matthew Brost <matthew.brost@intel.com>
To: Tejun Heo <tj@kernel.org>
Cc: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>,
	<dri-devel@lists.freedesktop.org>,
	Boris Brezillon <boris.brezillon@collabora.com>,
	Steven Price <steven.price@arm.com>,
	"Liviu Dudau" <liviu.dudau@arm.com>,
	Chia-I Wu <olvaffe@gmail.com>, <kernel-dev@igalia.com>,
	<linux-kernel@vger.kernel.org>, Chia-I Wu <olv@google.com>
Subject: Re: [RFC 1/2] workqueue: Add support for real-time workers
Date: Mon, 13 Jul 2026 13:48:19 -0700	[thread overview]
Message-ID: <alVPE2d7AKrDCgGO@gsse-cloud1.jf.intel.com> (raw)
In-Reply-To: <b3e5bcb21184d13c7c631daac955f465@kernel.org>

On Mon, Jul 13, 2026 at 08:59:58AM -1000, Tejun Heo wrote:
> Hello, Tvrtko.
> 
> On Mon, Jul 13, 2026 at 03:14:35PM +0100, Tvrtko Ursulin wrote:
> > We use a mininum 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, and system-
> > wide to a maximum of either two or one less than the number of online
> > CPUs. The idea of that is to prevent system wide starvation cause by
> > potentially misbehaving work items.
> 
> Do your use cases need concurrency management on per-cpu workqueues?
> Concurrency management is what per-cpu pools do to reduce the number of
> parallel worker contexts when there are multiple pending work items
> competing for the same CPU - the next work item starts executing only
> after the current worker blocks. That doesn't seem applicable to RT
> usage where the point is running each work item as soon as possible.
> 
> If it isn't needed, how about supporting RT only on unbound workqueues?

The DRM cases I can think of are all unbound workqueues afiak.

Matt

> Where per-cpu execution is wanted, an unbound workqueue with the
> affinity scope set to "cpu" and affn_strict on behaves like a per-cpu
> workqueue sans concurrency management. That'd leave the per-cpu worker
> pools alone and confine the changes to the unbound side.
> 
> The following is an AI review of the patch:
> 
> > 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 successfuly 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_HIPRI does not bring a noticeable
> > improvemet 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 mininum 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, and system-
> > wide to a maximum of either two or one less than the number of online
> > CPUs. The idea of that is to prevent system wide starvation cause by
> > potentially misbehaving work items.
> 
> A few typos: "successfuly", "improvemet", "WQ_HIPRI", "mininum",
> "cause by".
> 
> Where does the "per workqueue to a maximum of two" limit come from?
> __alloc_workqueue() clamps max_active to num_online_cpus() / 2, which is
> four on an eight-CPU machine; the two in this series comes from the
> max_active the panthor patch passes in.
> 
> The system-wide limit also doesn't seem to apply to the panthor use case
> at all, see the POOL_RT questions below.
> 
> > diff --git a/kernel/workqueue.c b/kernel/workqueue.c
> > index 33b721a9af02..0473c009690f 100644
> > --- a/kernel/workqueue.c
> > +++ b/kernel/workqueue.c
> 
> [ ... ]
> 
> > @@ -2863,7 +2868,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 if (pool->attrs->prio == WQ_PRIO_HIGH)
> > +			set_user_nice(worker->task, pool->attrs->nice);
> > +
> >  		kthread_bind_mask(worker->task, pool_allowed_cpus(pool));
> >  	}
> 
> Doesn't this stop applying attrs->nice to WQ_PRIO_NORMAL pools?
> 
> Unbound workqueues have a user-settable nice level via the sysfs "nice"
> attribute, and wq_nice_store() still accepts it for non-RT workqueues
> and stores it in the pool attrs.  For example the "writeback" workqueue
> is WQ_UNBOUND | WQ_SYSFS with normal priority.
> 
> After this change, a pool created from attrs with prio == WQ_PRIO_NORMAL
> and nice != 0 spawns workers that never get the nice value applied, so
> the existing sysfs knob silently stops having any effect.
> 
> > @@ -2876,6 +2885,9 @@ static struct worker *create_worker(struct worker_pool *pool)
> >  	worker->pool->nr_workers++;
> >  	worker_enter_idle(worker);
> >
> > +	if (pool->flags & POOL_RT)
> > +		atomic_inc(&total_rtpri_workers);
> > +
> 
> [ ... ]
> 
> > @@ -2909,6 +2921,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->flags & WQ_RTPRI)
> > +			atomic_dec(&total_rtpri_workers);
> >  		kfree(worker);
> >  	}
> >  }
> 
> Is this decrement testing the right flags field?  worker->flags holds
> enum worker_flags values (WORKER_DIE, WORKER_IDLE, ...) and no worker
> flag uses the bit that WQ_RTPRI (an enum wq_flags value) occupies, so
> this condition is never true.
> 
> That means total_rtpri_workers is only ever incremented and can never
> drop when workers of an RT pool are culled or their pool is released.
> The increment above keys off pool->flags & POOL_RT, so the two sides of
> the accounting can't pair up as written.
> 
> > @@ -3110,6 +3124,19 @@ __acquires(&pool->lock)
> >  	mod_timer(&pool->mayday_timer, jiffies + MAYDAY_INITIAL_TIMEOUT);
> >
> >  	while (true) {
> > +		if (pool->flags & POOL_RT) {
> > +			unsigned int max = num_online_cpus();
> > +
> > +			if (max > 2)
> > +				max = max - 1;
> > +			else
> > +				max = 2;
> > +
> > +			/* Global cap on the number of RT workers */
> > +			if (atomic_read(&total_rtpri_workers) >= max)
> > +				break;
> > +		}
> > +
> >  		if (create_worker(pool) || !need_to_create_worker(pool))
> >  			break;
> 
> Three questions about this cap.
> 
> 1) Isn't the cap already fully consumed at boot?  workqueue_init() runs
> create_worker() for every per-CPU standard pool of every online CPU, and
> with NR_STD_WORKER_POOLS now 3 that includes the new POOL_RT pool:
> 
> 	for_each_online_cpu(cpu) {
> 		for_each_cpu_worker_pool(pool, cpu) {
> 			pool->flags &= ~POOL_DISASSOCIATED;
> 			BUG_ON(!create_worker(pool));
> 		}
> 	}
> 
> On an N-CPU machine total_rtpri_workers is N right after boot while the
> cap is max(N - 1, 2), so for any N >= 2 the break above already fires
> before the first WQ_RTPRI user exists, and with the decrement never
> running (see above) it stays that way.  A side effect is that every
> machine now boots one idle SCHED_FIFO kworker per CPU whether or not
> anything uses WQ_RTPRI.
> 
> 2) What happens when this break fires while need_to_create_worker() is
> still true?  The code after the loop is:
> 
> 	timer_delete_sync(&pool->mayday_timer);
> 	raw_spin_lock_irq(&pool->lock);
> 	if (need_to_create_worker(pool))
> 		goto restart;
> 
> so the manager goes back to restart and loops forever.  Nothing in the
> cap path sleeps, and each pass re-arms and then deletes the mayday timer
> before it can expire, so pool_mayday_timeout() never runs and no rescuer
> is engaged either.
> 
> Since worker_thread() only starts processing work once
> may_start_working() holds, the first work item ever queued on a per-CPU
> WQ_RTPRI workqueue would leave its SCHED_FIFO worker spinning in
> worker_thread() -> manage_workers() -> maybe_create_worker() on that CPU
> while the work item is never executed.  Nothing in this series creates a
> per-CPU WQ_RTPRI workqueue, but __alloc_workqueue() only rejects the
> WQ_HIGHPRI combination, so this is reachable as soon as one appears.
> 
> 3) POOL_RT is only set on the per-CPU pools in workqueue_init_early().
> get_unbound_pool() never sets it, so neither the cap nor the accounting
> applies to unbound RT pools, which is the only configuration the panthor
> patch in this series uses (WQ_RTPRI | WQ_MEM_RECLAIM | WQ_UNBOUND).
> 
> That seems to invert the commit message: the system-wide limit doesn't
> restrict the posted consumer at all, while it permanently prevents
> worker creation in the per-CPU pools it does cover.
> 
> [ ... ]
> 
> > @@ -5842,7 +5884,12 @@ 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 the online CPUs.
> > +		 */
> > +		max_active = min_t(int, max_active, num_online_cpus() / 2);
> > +	} else if (flags & WQ_BH) {
> 
> Can max_active end up 0 here?  Two ways:
> 
>   - With one online CPU (nr_cpus=1 or maxcpus=1 boots),
>     num_online_cpus() / 2 is 0, so the panthor workqueue created with
>     max_active == 2 gets 0.
> 
>   - Passing max_active == 0, the documented "use default" value, stays
>     0 because the "max_active ?: WQ_DFL_ACTIVE" fallback and
>     wq_clamp_max_active(), which enforces a minimum of 1, are in the
>     else branch and are skipped for WQ_RTPRI.
> 
> With wq->max_active == 0, wq_adjust_max_active() returns before
> activating anything and pwq_tryinc_nr_active() can never succeed, so
> every work item queued on such a workqueue stays on the inactive list
> forever and flush/destroy hang.
> 
> Also, num_online_cpus() is sampled once at allocation time, so a
> workqueue allocated before secondary CPUs are brought online keeps a
> limit computed from the momentary CPU count.
> 
> While in this area: for WQ_RTPRI | WQ_MEM_RECLAIM, should the rescuer
> get SCHED_FIFO as well?  rescuer_thread() runs at RESCUER_NICE_LEVEL,
> so under a mayday the work this flag is trying to keep low latency
> executes below the promised priority.
> 
> [ ... ]
> 
> > @@ -7284,10 +7339,14 @@ static ssize_t wq_nice_show(struct device *dev, struct device_attribute *attr,
> >  			    char *buf)
> >  {
> >  	struct workqueue_struct *wq = dev_to_wq(dev);
> > -	int written;
> > +	int written, nice;
> >
> >  	mutex_lock(&wq->mutex);
> > -	written = scnprintf(buf, PAGE_SIZE, "%d\n", wq->unbound_attrs->nice);
> > +	if (wq->unbound_attrs->prio == WQ_PRIO_RT)
> > +		nice = INT_MIN;
> > +	else
> > +		nice = wq->unbound_attrs->nice;
> > +	written = scnprintf(buf, PAGE_SIZE, "%d\n", nice);
> 
> This isn't a bug, but is INT_MIN the value userspace should read from
> the nice attribute of an RT workqueue?  This file has so far only ever
> produced values in the [-20, 19] range.
> 
> [ ... ]
> 
> Thanks.
> 
> -- 
> tejun

  reply	other threads:[~2026-07-13 20:48 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-13 14:14 [RFC 0/2] Realtime workqueues and panthor realtime submission Tvrtko Ursulin
2026-07-13 14:14 ` [RFC 1/2] workqueue: Add support for real-time workers Tvrtko Ursulin
2026-07-13 18:59   ` Tejun Heo
2026-07-13 20:48     ` Matthew Brost [this message]
2026-07-14 14:46     ` Tvrtko Ursulin
2026-07-13 14:14 ` [RFC 2/2] drm/panthor: Create per queue priority workqueues Tvrtko Ursulin

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=alVPE2d7AKrDCgGO@gsse-cloud1.jf.intel.com \
    --to=matthew.brost@intel.com \
    --cc=boris.brezillon@collabora.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=kernel-dev@igalia.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=liviu.dudau@arm.com \
    --cc=olv@google.com \
    --cc=olvaffe@gmail.com \
    --cc=steven.price@arm.com \
    --cc=tj@kernel.org \
    --cc=tvrtko.ursulin@igalia.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

Powered by JetHome