[PATCH] sched/cache: Keep nr_pref_llc_running in the runnable doma=

From: Tim Chen

Date: Thu Aug 27 2026 - 14:20:31 EST


alb_break_llc() decides whether to break LLC preference during active
load balance. It does so by testing that every runnable fair task on the
source rq prefers its LLC:

env->src_rq->nr_pref_llc_running =3D=3D env->src_rq->cfs.h_nr_runnable

But the two counters cover different sets. nr_pref_llc_running is updated
in account_llc_enqueue()/account_llc_dequeue(), next to cfs_rq->nr_queued,
so it follows queued tasks. h_nr_runnable is updated in set_delayed()/
clear_delayed() and drops delay-dequeued tasks.

Say we start off with all running tasks preferring source LLC.
So under DELAY_DEQUEUE, a preferring task that goes to sleep stays counted
in nr_pref_llc_running while h_nr_runnable falls. The equality then breaks,
alb_break_llc() returns false, and active balance is free to pull a task
off its preferred LLC. Active balance only moves runnable tasks, and this
is the only LLC check it consults: once the stopper runs, LBF_ACTIVE_LB
skips the per-task test in can_migrate_task().

Fix it on the counter side. Adjust nr_pref_llc_running in set_delayed()
and clear_delayed(), the same places that adjust h_nr_runnable, so both
exclude delayed tasks.

This needs care to not count a task twice. set_delayed() removes the task
from nr_pref_llc_running when it sleeps. Later, when the task is really
dequeued, dequeue_entity() calls account_llc_dequeue() and then
clear_delayed(). Left as is, account_llc_dequeue() would remove the task a
second time and clear_delayed() would add it back. So account_llc_dequeue()
skips its decrement while the task is still delayed, and clears
pref_llc_queued so clear_delayed() also leaves the count alone.
nr_llc_running and sd->llc_counts are not touched and stay on queued
semantics.

Reported-by: Zhan Xusheng <zhanxusheng@xxxxxxxxxx>
Assisted-By: Claude Opus 4.8 <noreply@xxxxxxxxxxxxx>
Signed-off-by: Tim Chen <tim.c.chen@xxxxxxxxxxxxxxx>
---
kernel/sched/fair.c | 45 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index d78467ec6ee1..1673b17273c5 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1544,7 +1544,14 @@ static void account_llc_dequeue(struct rq *rq, struc=
t task_struct *p)
=20
rq->nr_llc_running--;
if (p->pref_llc_queued) {
- rq->nr_pref_llc_running--;
+ /*
+ * If the task is being finally dequeued while still delayed,
+ * set_delayed() already removed it from nr_pref_llc_running;
+ * skip here to avoid underflow. Clearing pref_llc_queued also
+ * stops the subsequent clear_delayed() from re-adding it.
+ */
+ if (!p->se.sched_delayed)
+ rq->nr_pref_llc_running--;
/*
* Update the status in case
* other logic might query
@@ -1572,6 +1579,24 @@ static void account_llc_dequeue(struct rq *rq, struc=
t task_struct *p)
}
}
=20
+/*
+ * A task becoming delay-dequeued leaves the runnable set while staying
+ * queued. Keep nr_pref_llc_running in the runnable domain (like
+ * h_nr_runnable) so alb_break_llc() can compare the two directly.
+ */
+static void account_llc_delayed(struct rq *rq, struct task_struct *p)
+{
+ if (p->pref_llc_queued)
+ rq->nr_pref_llc_running--;
+}
+
+/* A delay-dequeued task becoming runnable again rejoins the count. */
+static void account_llc_requeue_delayed(struct rq *rq, struct task_struct =
*p)
+{
+ if (p->pref_llc_queued)
+ rq->nr_pref_llc_running++;
+}
+
void mm_init_sched(struct mm_struct *mm,
struct sched_cache_time __percpu *_pcpu_sched)
{
@@ -1969,6 +1994,10 @@ static void account_llc_enqueue(struct rq *rq, struc=
t task_struct *p) {}
=20
static void account_llc_dequeue(struct rq *rq, struct task_struct *p) {}
=20
+static void account_llc_delayed(struct rq *rq, struct task_struct *p) {}
+
+static void account_llc_requeue_delayed(struct rq *rq, struct task_struct =
*p) {}
+
#endif /* CONFIG_SCHED_CACHE */
=20
/*
@@ -6203,6 +6232,13 @@ static void set_delayed(struct sched_entity *se)
if (!entity_is_task(se))
return;
=20
+ /*
+ * A delayed task is queued but no longer runnable. Drop it from
+ * nr_pref_llc_running so that counter keeps runnable semantics and
+ * stays comparable with h_nr_runnable in alb_break_llc().
+ */
+ account_llc_delayed(rq_of(cfs_rq_of(se)), task_of(se));
+
for_each_sched_entity(se) {
struct cfs_rq *cfs_rq =3D cfs_rq_of(se);
=20
@@ -6223,6 +6259,13 @@ static void clear_delayed(struct sched_entity *se)
if (!entity_is_task(se))
return;
=20
+ /*
+ * Re-add on wake (requeue_delayed_entity). On the final delayed
+ * dequeue, account_llc_dequeue() has already cleared pref_llc_queued,
+ * so this correctly does nothing.
+ */
+ account_llc_requeue_delayed(rq_of(cfs_rq_of(se)), task_of(se));
+
for_each_sched_entity(se) {
struct cfs_rq *cfs_rq =3D cfs_rq_of(se);
=20

base-commit: a13c140cc289c0b7b3770bce5b3ad42ab35074aa
--=20
2.53.0