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

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.

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(). The runnable set is the one
we want.

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>
Suggested-by: Chen Yu <yu.c.chen@xxxxxxxxx>
Assisted-By: claude-opus-4.8
Signed-off-by: Tim Chen <tim.c.chen@xxxxxxxxxxxxxxx>
---
Changes in v2:
- Prevent delay dequeued task from counted as running task in enqueue
operation.
---
kernel/sched/fair.c | 53 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 51 insertions(+), 2 deletions(-)

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 8dff37059faf..84c068f1deec 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1549,7 +1549,13 @@ static void account_llc_enqueue(struct rq *rq, struc=
t task_struct *p)
=20
pref_llc_queued =3D (pref_llc =3D=3D task_llc(p));
rq->nr_llc_running++;
- rq->nr_pref_llc_running +=3D pref_llc_queued;
+ /*
+ * If the task is being migrated while delay dequeued, keep it out
+ * of the runnable-domain nr_pref_llc_running; clear_delayed() ->
+ * account_llc_requeue_delayed() re-adds it when it wakes.
+ */
+ if (!p->se.sched_delayed)
+ rq->nr_pref_llc_running +=3D pref_llc_queued;
=20
/*
* Record whether p is enqueued on its preferred
@@ -1583,7 +1589,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
@@ -1611,6 +1624,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)
{
@@ -2008,6 +2039,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
/*
@@ -6392,6 +6427,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
@@ -6412,6 +6454,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
--=20
2.32.0