Re: [PATCH v2 1/2] sched/numa: Drive NUMA task tick from execution context

From: Hui Su

Date: Sat Sep 05 2026 - 10:00:53 EST


On Fri, Sep 4, 2026 at 1:15 PM, Tim Chen wrote:
> Yes, you have a good point. The code I proposed just look at whether
> we have consumed our allotment in the current slice.
>
> What we should have looked at is whether the donor's total run time has
> exceeded its quota when doing core scheduling. And we may happen to
> hit __entity_slice_used() at the front of the slice after advancing
> the deadline and __entity_slice_used()
> returns false instead of true, even though I have consumed more than
> my fair share when looking at longer time period across multiple slices.
>
> The accumulated run time of the donor since it was picked for running
> should be used for selection time baseline.
>
> So maybe a patch like the following instead.

Thanks for the updated prototype.

I tested the pick-time vruntime snapshot further. With a fixed weight,
it behaves as expected: it fixes the proxy case and continues to match
the existing runtime-based predicate in the non-proxy tests I ran.

I then tested it with CONFIG_FAIR_GROUP_SCHED, using nested cgroups and
changing the hierarchical weights in an observation-only setup. The
kernel computes the existing predicate, the pick-time vruntime predicate,
and a pick-time task-clock predicate on the same tick, while the existing
predicate remains the actual return value.

I think the "same weight scaling" condition in the non-proxy equivalence
is the interesting part here. If the hierarchical weight changes after
core_slice_vruntime is captured, vused represents service accumulated
since the pick-time baseline, while vslice is converted using the current
weight.

Across 11 instrumented runs I observed 5985 non-proxy samples. 29 of
those were explicitly identified as cases where the same task's
sched_entity had been reweighted after the current selection-time
baseline was taken. The existing and task-clock predicates matched in all
5985 non-proxy samples, while the vruntime predicate differed in four of
the 29 reweight-after-pick samples.

I also reproduced the divergence with actual runnable load in the
hierarchy. In one causal trace:

selection=5
pick_hweight=1048576
current_hweight=15138

reweight_vruntime=768327601->768327601

existing:
rtime=7980540
used=1

pick-time vruntime:
vused=7980540
vslice=145462386
used=0

task-clock:
rtime=7980540
used=1

This was the same selection: the pick-time baseline had already been
established, the hierarchical weight then changed, and there was no
new pick before the predicate was evaluated.

In this example reweight_eevdf() did not change se->vruntime, so the
divergence does not depend on a vruntime coordinate adjustment. The
weight change alone is enough for the accumulated vused and the
current-weight vslice to no longer necessarily use the same scale.

I also observed the reverse predicate direction in the observation-only
runs, although I have not yet reduced that case to the same detailed
causal trace.

One way to keep the vruntime approach would be to rebase the accumulated
virtual service whenever the relevant weight changes, preserving the
vused / vslice ratio across the reweight. Simply resetting
core_slice_vruntime at reweight time would lose service already consumed
before the reweight.

A simpler alternative I have been testing is to keep the check in the
same real-time domain as the existing predicate:

set_next_entity():
core_sched_start = se->exec_start;

__entity_slice_used():
rtime = se->exec_start - se->core_sched_start;

return rtime * min_nr_tasks > se->slice;

update_se() advances the donor's exec_start from rq_clock_task() while
it is the scheduling context, including during proxy execution, even
though the task-level sum_exec_runtime is charged to rq->curr. This
keeps the force-idle slice check in the same real-time domain as the
existing sum_exec_runtime - prev_sum_exec_runtime comparison and avoids
converting accumulated service across weight changes.

For reference, the local task-clock variant I am testing is below. It
is not intended as a formal posting yet:

diff --git a/include/linux/sched.h b/include/linux/sched.h
index 8b3d47a325cc..c32d9931129f 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -590,6 +590,9 @@ struct sched_entity {
u64 sum_exec_runtime;
u64 prev_sum_exec_runtime;
u64 vruntime;
+#ifdef CONFIG_SCHED_CORE
+ u64 core_sched_start;
+#endif
/* Approximated virtual lag: */
s64 vlag;
/* 'Protected' deadline, to give out minimum quantums: */
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index f78275192036..22ae5dc57337 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -4580,6 +4580,9 @@ static void __sched_fork(u64 clone_flags, struct task_struct *p)
p->se.prev_sum_exec_runtime = 0;
p->se.nr_migrations = 0;
p->se.vruntime = 0;
+#ifdef CONFIG_SCHED_CORE
+ p->se.core_sched_start = 0;
+#endif
p->se.vlag = 0;
p->se.rel_deadline = 0;
INIT_LIST_HEAD(&p->se.group_node);
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 8dff37059faf..1bd05c906d3a 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -6502,6 +6502,9 @@ set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
}

se->prev_sum_exec_runtime = se->sum_exec_runtime;
+#ifdef CONFIG_SCHED_CORE
+ se->core_sched_start = se->exec_start;
+#endif
}

static bool __dequeue_task(struct rq *rq, struct task_struct *p, int flags);
@@ -14748,10 +14751,9 @@ static void rq_offline_fair(struct rq *rq)
static inline bool
__entity_slice_used(struct sched_entity *se, int min_nr_tasks)
{
- u64 rtime = se->sum_exec_runtime - se->prev_sum_exec_runtime;
- u64 slice = se->slice;
+ u64 rtime = se->exec_start - se->core_sched_start;

- return (rtime * min_nr_tasks > slice);
+ return (rtime * min_nr_tasks > se->slice);
}

#define MIN_NR_TASKS_DURING_FORCEIDLE 2

The task-clock version has matched the existing predicate in the
non-proxy tests so far and fixes the proxy reproducer as well. I am
still validating reselection, migration, and the remaining proxy
boundary cases, so I have not posted either implementation.

Do you think keeping the check in the original real-time/task-clock
domain is a reasonable direction here, or would you prefer preserving
the vruntime approach by carrying the accumulated service across
reweights?

Thanks,
Hui