mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: K Prateek Nayak <kprateek.nayak@amd.com>
To: Xukai Wang <kingxukai@zohomail.com>,
	John Stultz <jstultz@google.com>, Ingo Molnar <mingo@redhat.com>,
	Peter Zijlstra <peterz@infradead.org>,
	Juri Lelli <juri.lelli@redhat.com>,
	Vincent Guittot <vincent.guittot@linaro.org>,
	Dietmar Eggemann <dietmar.eggemann@arm.com>,
	Steven Rostedt <rostedt@goodmis.org>,
	Ben Segall <bsegall@google.com>, Mel Gorman <mgorman@suse.de>,
	Valentin Schneider <vschneid@redhat.com>
Cc: <linux-kernel@vger.kernel.org>
Subject: Re: [PATCH RFC] sched/proxy: Defer donor commit until after proxy resolution
Date: Mon, 13 Jul 2026 09:52:13 +0530	[thread overview]
Message-ID: <30ac52e7-3058-42f4-8f20-ff466716e9a5@amd.com> (raw)
In-Reply-To: <47c5e555-92ce-4ab1-8adf-c10639753b49@zohomail.com>

(+ John)

Hello Xukai,

On 7/10/2026 8:44 PM, Xukai Wang wrote:
>> On 7/7/2026 9:16 PM, Xukai Wang wrote:
>>> pick_next_task() currently commits the selected task into the scheduling
>>> class state before proxy execution has resolved whether that task should
>>> really become the committed donor.
>>>
>>> With proxy execution enabled, the task returned by pick_next_task() may
>>> be blocked. __schedule() then calls find_proxy_task() to resolve the
>>> proxy chain. If find_proxy_task() returns NULL, __schedule() retries
>>> through pick_again, but the picked donor has already gone through
>>> put_prev_set_next_task(). In that case the class current state was
>>> speculatively switched to a donor which is not the final scheduling
>>> decision.
>> As long as rq_lock is held, the pick will always select the same
>> donor since there cannot be a wakeup / migration until the rq_lock
>> is released.
>>
>> If there is a change in the proxy state, and the lock needs to be
>> dropped, we do a proxy_resched_idle() and transiently switch to the
>> idle task's accounting context.
>>
>> If we don't need to drop the rq_lock, we just retry the pick after
>> dropping the blocked_lock and the pick will converge to the same
>> donor unless ->balance() manages to pull something.
>>
>> Very often, it is the same donor task pick converges on. I don't
>> see how this transient state is a big enough problem to see any
>> noticeable overhead.
> 
> I agree that, if the rq state does not materially change while rq_lock is
> held, the retry can converge back to the same donor. I did not intend to
> make a performance claim with the RFC. The counters were meant to
> characterize what happens in the proxy path after a blocked donor has
> already been committed.

I've added John on this thread in case he knows of any dependency
in the find_proxy_task() bits that rely on rq->donor being updated
with the recent pick (I, personally am not aware of any but there
might be some dependency that eventually gets added with the full
proxy series that I haven't yet considered).

> 
> I added some temporary instrumentation on the baseline kernel to check the
> retry behavior after find_proxy_task() returns NULL. The test was run on
> top of tip/sched/core at commit 19b7bdc3a1550ab2550427c33395bec7caeaf72d
> with only temporary debug instrumentation added.
> 
> The test uses a small misc driver which repeatedly takes a kernel struct
> mutex and busy-waits while holding it. A userspace stress program creates
> one low-priority holder and multiple higher-priority waiters pinned to
> selected CPUs. The specific run below used:
> holder CPU: 1
> 
> waiter CPU
> 
> base: 0
> 
> waiter CPU span: 4
> 
> waiters: 8
> 
> duration: 60s
> 
> holder hold time: 3000 us
> 
> waiter hold time: 1 us
> 
> waiter policy: SCHED_RR prio 50
> 
> The temporary counters mean:
> spec_commit_blocked:pick_next_task() selected a blocked donor, and in
> the baseline code that donor had already gone through
> put_prev_set_next_task() before proxy-chain resolution.
> 
> spec_commit_then_null:the blocked donor had already been committed, but
> find_proxy_task() returned NULL and __schedule() retried.
> 
> spec_commit_then_idle:the blocked donor had already been committed, but
> find_proxy_task() returned rq->idle.
> 
> spec_commit_then_success:the blocked donor had already been committed,
> and find_proxy_task() successfully found a task to run.
> 
> null_retry_same_donor:
> after find_proxy_task() returned NULL, the next pick selected the same
> donor again.
> 
> null_retry_diff_donor:
> after find_proxy_task() returned NULL, the next pick selected a
> different non-idle donor.
> 
> null_retry_to_idle:
> after find_proxy_task() returned NULL, the next pick selected idle.
> 
> In that run, I got:
> find_call                         5407
> find_success                       523
> find_idle                         1660
> find_null                         3224
> 
> spec_commit_blocked               5407
> spec_commit_then_null             3224
> spec_commit_then_idle             1660
> spec_commit_then_success           523
> 
> null_deactivate_no_mutex             6
> null_deactivate_no_owner           125
> null_deactivate_owner_not_runnable 962
> null_migrate                      2131

I think the chain-migration bits can bring down some of these
null_migrate calls substantially.

> 
> null_retry_same_donor                0
> null_retry_diff_donor             2258
> null_retry_to_idle                 966

Seems like you mostly have one task that keeps getting migrated to the
owner's CPU.

> 
> So in this workload, the selected blocked donor had already been committed
> before proxy-chain resolution, but most resolutions ended in NULL or idle.
> For the NULL cases, the retry did not pick the same donor again. Most of
> those NULL cases came from deactivate/migrate paths, so this appears to be
> the kind of case where proxy state changes before the retry.
> 
>>> Make pick_next_task() return only the selected candidate task. Move the
>>> put_prev_set_next_task() call into __schedule(), next to rq_set_donor(),
>>> so the class commit happens at the schedule level.
>>>
>>> For proxy execution, keep the picked donor separate from the actual task
>>> to run:
>>>
>>>   donor = task selected by pick_next_task()
>>>   next  = task returned by find_proxy_task(), possibly the mutex owner
>>>
>>> Only commit the donor after proxy-chain resolution succeeds:
>>>
>>>   put_prev_set_next_task(rq, prev_donor, donor);
>>>   rq_set_donor(rq, donor);
>>>
>>> If find_proxy_task() returns NULL, the candidate donor has not been
>>> committed and __schedule() can retry without first undoing a speculative
>>> set_next() on that donor.
>>>
>>> A proxy candidate is not necessarily the committed rq->donor anymore.
>>> Update proxy_deactivate() accordingly. If the task being blocked is
>>> still the committed donor, proxy_resched_idle() is needed to drop
>>> rq/class current references before block_task() clears ->on_rq. If
>>> it is only an uncommitted proxy candidate, it is still queued and can
>>> be blocked directly.
>>>
>>> proxy_migrate_task() still switches the rq to idle before dropping the
>>> rq lock. Migrating a task found in the proxy chain abandons the current
>>> proxy pick attempt, and switching the committed donor to idle leaves
>>> rq->donor and the class current state in a defined state while the chain
>>> is modified and the task is attached elsewhere.
>>>
>>> This keeps the proxy donor accounting semantics intact: the scheduling
>>> class state is committed to the donor, while the actual context switch
>>> may still be to the proxy owner returned by find_proxy_task().
>>>
>>> Signed-off-by: Xukai Wang <kingxukai@zohomail.com>
>>> ---
>>>  kernel/sched/core.c | 70 ++++++++++++++++++++++++++++++-----------------------
>>>  1 file changed, 40 insertions(+), 30 deletions(-)
>>>
> ...
>>>
>>> @@ -7147,11 +7153,11 @@ static void __sched notrace __schedule(int sched_mode)
>>>         rq->next_class = next->sched_class;
>>>         if (sched_proxy_exec()) {
>>>                 struct task_struct *prev_donor = rq->donor;
>>> +               struct task_struct *donor = next;
>>>
>>> -               rq_set_donor(rq, next);
>>> -               next->blocked_donor = NULL;
>>> -               if (unlikely(next->is_blocked)) {
>>> -                       next = find_proxy_task(rq, next, &rf);
>>> +               donor->blocked_donor = NULL;
>>> +               if (unlikely(donor->is_blocked)) {
>>> +                       next = find_proxy_task(rq, donor, &rf);
>>>                         if (!next) {
>>>                                 zap_balance_callbacks(rq);
>> You don't need to zap any balance callbacks if you don't do
>> a put_prev_set_next_task(). It should always be empty here.
>>
>>>                                 goto pick_again;
>>> @@ -7161,8 +7167,11 @@ static void __sched notrace __schedule(int sched_mode)
>>>                                 goto keep_resched;
>>>                         }
>>>                 }
>>> -               if (rq->donor == prev_donor && prev != next) {
>>> -                       struct task_struct *donor = rq->donor;
>>> +
>>> +               put_prev_set_next_task(rq, prev_donor, donor);
>>> +               rq_set_donor(rq, donor);
>> Since the next condition only cares for donor == prev_donor, I
>> think you can do this after the next block and unify the
>> !sched_proxy_exec() case too like:
>>
>> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
>> index f13f53bd6d9d7..c6e37005a73c4 100644
>> --- a/kernel/sched/core.c
>> +++ b/kernel/sched/core.c
>> @@ -7171,9 +7171,6 @@ static void __sched notrace __schedule(int sched_mode)
>>                       }
>>               }
>>
>> -             put_prev_set_next_task(rq, prev_donor, donor);
>> -             rq_set_donor(rq, donor);
>> -
>>               if (donor == prev_donor && prev != next) {
>>                       /*
>>                        * When transitioning like:
>> @@ -7190,10 +7187,9 @@ static void __sched notrace __schedule(int sched_mode)
>>                       donor->sched_class->put_prev_task(rq, donor, donor);
>>                       donor->sched_class->set_next_task(rq, donor, true);
>>               }
>> -     } else {
>> -             put_prev_set_next_task(rq, rq->donor, next);
>> -             rq_set_donor(rq, next);
>>       }
>> +     put_prev_set_next_task(rq, rq->donor, next);
>> +     rq_set_donor(rq, next);
>>
>>  picked:
>>       clear_tsk_need_resched(prev);
>> ---
>>
> Based on your comments, I am thinking of restructuring the patch like this.
> 
> First, move the final put_prev_set_next_task()/rq_set_donor() out of the
> proxy/non-proxy branches. I think the common commit target still needs to
> be the donor rather than next, because after find_proxy_task(), next may be
> the proxy owner that will actually run, while donor is still the selected
> scheduling donor/accounting context:

You are right! I had not booted into a kernel with my suggested changes
to have realized that mistake :-)

> __schuedle()
> 
> struct task_struct *donor;
> ...
> 
> next = pick_next_task(rq, &rf);
> rq->next_class = next->sched_class;
> donor = next;
> if (sched_proxy_exec()) {
>   struct task_struct *prev_donor = rq->donor;
> 
>   donor->blocked_donor = NULL;
>   if (unlikely(donor->is_blocked)) {
>     next = find_proxy_task(rq, donor, &rf);
>     if (!next)
>       goto pick_again;
>     if (next == rq->idle)
>       goto keep_resched;
>   }
> 
>   if (donor == prev_donor && prev != next) {
>     donor->sched_class->put_prev_task(rq, donor, donor);
>     donor->sched_class->set_next_task(rq, donor, true);
>   }
> }
> put_prev_set_next_task(rq, rq->donor, donor);
> rq_set_donor(rq, donor);
> 
> Second, for zap_balance_callbacks(), I noticed that some NULL/idle paths
> can still go through proxy_resched_idle(), which performs the idle put/set
> transition before returning. So instead of keeping zap_balance_callbacks()
> at the outer !next / next == rq->idle sites, I am thinking of moving it
> into proxy_resched_idle(), next to the idle commit itself:
> static inline struct task_struct *proxy_resched_idle(struct rq *rq)
> {
>   put_prev_set_next_task(rq, rq->donor, rq->idle);
>   rq->next_class = &idle_sched_class;
>   rq_set_donor(rq, rq->idle);
>   set_tsk_need_resched(rq->idle);
>   zap_balance_callbacks(rq);
>   return rq->idle;
> }
> 
> Then the outer NULL/idle paths no longer need to zap unconditionally. If no
> put/set happened, there should be no callbacks to clear; if the path went
> through proxy_resched_idle(), the callbacks are cleared at the place that
> did the idle commit.
> 
> There may be paths such as proxy_migrate_task() where proxy_resched_idle()
> is followed later by proxy_release_rq_lock(), which also zaps callbacks
> before dropping the rq lock. My current thinking is that this is still OK:
> proxy_resched_idle() clears callbacks from the idle commit itself, while
> proxy_release_rq_lock() remains the final cleanup point after later
> dequeue/migration changes. But I would like to know if you would prefer a
> different placement.
> 
> Does this look like the right direction?

Your suggestions look correct to me. Perhaps you can send a v2 including
John and we can take a look.

> 
> If useful, I can also share the temporary instrumentation and the exact
> proxy_mutex_test setup used to reproduce the numbers above. I can further
> look into whether this has a measurable performance effect, but I would
> keep that separate from the correctness/commit-boundary question unless
> you think it is necessary for this change.

Just the patches should be fine. I can crunch the performance numbers
from my side with schedstats and update on the threads.

-- 
Thanks and Regards,
Prateek


  reply	other threads:[~2026-07-13  4:22 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-07 15:46 [PATCH RFC] sched/proxy: Defer donor commit until " Xukai Wang
2026-07-07 15:46 ` [PATCH RFC] sched/proxy: Defer donor commit until after " Xukai Wang
2026-07-10  7:23   ` K Prateek Nayak
2026-07-10 15:00     ` Xukai Wang
2026-07-10 15:14     ` Xukai Wang
2026-07-13  4:22       ` K Prateek Nayak [this message]
2026-07-13  5:01         ` Xukai Wang

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=30ac52e7-3058-42f4-8f20-ff466716e9a5@amd.com \
    --to=kprateek.nayak@amd.com \
    --cc=bsegall@google.com \
    --cc=dietmar.eggemann@arm.com \
    --cc=jstultz@google.com \
    --cc=juri.lelli@redhat.com \
    --cc=kingxukai@zohomail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=mgorman@suse.de \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=vincent.guittot@linaro.org \
    --cc=vschneid@redhat.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

all inboxes | Powered by JetHome®