From: sashiko-bot@kernel.org
To: "Vincent Guittot" <vincent.guittot@linaro.org>,
sched-ext@lists.linux.dev
Subject: Re: [PATCH 17/20 v3] sched/fair: Rework feec() to use cost instead of spare capacity
Date: Fri, 06 Nov 2026 16:30:57 +0000 [thread overview]
Message-ID: <sashiko-outbox-201823@kernel.org> (raw)
In-Reply-To: <20261106151947.3991524-18-vincent.guittot@linaro.org>
Thank you for your contribution! Sashiko AI review found 8 potential
issues to consider:
- [High] sched/fair: missing task argument in update_best_cpu() call
breaks build
- [High] sched/fair: underestimated energy delta in
find_energy_efficient_cpu()
- [Medium] sched/fair: min_stat not reset on better fits in
find_energy_efficient_cpu()
- [Medium] sched/fair: find_energy_efficient_cpu() picks smallest PD
when fits is -1
- [Medium] sched/fair: find_energy_efficient_cpu() migrates 0-util tasks
to first PD
- [Medium] sched/fair: off-by-one OPP boundary checks in
find_energy_efficient_cpu()
- [Low] sched/fair: unreachable prev_cpu nr_running adjustment in
update_best_cpu()
- [Low] sched/fair: wrong base max_cost in find_energy_efficient_cpu()
when uclamp_max is 0
--
commit d227a3eb8d24b8112f9074f06edbce339ce827bc
Author: Vincent Guittot <vincent.guittot@linaro.org>
sched/fair: Rework feec() to use cost instead of spare capacity
This commit reworks find_energy_efficient_cpu() to select the lowest-cost
CPU within each performance domain using OPP cost and performance tie-breakers
rather than maximum spare capacity.
> diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
> index 894bee51b99ad..ea117c3d0dd71 100644
> --- a/kernel/sched/fair.c
> +++ b/kernel/sched/fair.c
[ ... ]
> @@ -9762,124 +9768,161 @@ static inline void eenv_task_busy_time(struct energy_env *eenv,
[ ... ]
> +/* Find the cost of a performance domain for the estimated utilization */
> +static inline void find_pd_cost(struct em_perf_domain *pd,
> + unsigned long max_util,
> + struct energy_cpu_stat *stat)
> +{
> + struct em_perf_table *em_table;
> + struct em_perf_state *ps;
> + int i;
>
> - /*
> - * If there is no active max uclamp constraint,
> - * directly use task's one, otherwise keep max.
> - */
> - if (uclamp_rq_is_idle(cpu_rq(cpu)))
> - max = uclamp_eff_value(p, UCLAMP_MAX);
> - else
> - max = max(max, uclamp_eff_value(p, UCLAMP_MAX));
> - }
> + /*
> + * Find the lowest performance state of the Energy Model above the
> + * requested performance.
> + */
> + em_table = rcu_dereference_all(pd->em_table);
> + i = em_pd_get_efficient_state(em_table->state, pd, max_util);
> + ps = &em_table->state[i];
>
> - eff_util = sugov_effective_cpu_perf(cpu, eff_util, min, max);
> - max_util = max(max_util, eff_util);
> + /* Save the cost and performance range of the OPP */
> + stat->max_perf = ps->performance;
> + stat->cost = ps->cost;
> + i = em_pd_get_previous_state(em_table->state, pd, i);
> + if (i < 0) {
> + stat->min_perf = 0;
> + } else {
> + ps = &em_table->state[i];
> + stat->min_perf = ps->performance;
> }
> +}
[ ... ]
> +static bool update_best_cpu(struct energy_cpu_stat *target,
> + struct energy_cpu_stat *min,
> + int prev, struct sched_domain *sd,
> + struct task_struct *p)
> {
> - unsigned long max_util = eenv_pd_max_util(eenv, pd_cpus, p, dst_cpu);
> - unsigned long busy_time = eenv->pd_busy_time;
> - unsigned long energy;
> + bool target_idle = choose_idle_cpu(target->cpu, p);
> + bool min_idle = choose_idle_cpu(min->cpu, p);
> +
> + /* Select the one which is idle */
> + if (target_idle != min_idle)
> + return target_idle;
>
> - if (dst_cpu >= 0)
> - busy_time = min(eenv->pd_cap, busy_time + eenv->task_busy_time);
> + /* Favor previous CPU */
> + if (target->cpu == prev)
> + return true;
> + if (min->cpu == prev)
> + return false;
>
> - energy = em_cpu_energy(pd->em_pd, max_util, busy_time, eenv->cpu_cap);
> + /* Select the one with the least number of running tasks otherwise */
> + if (target->nr_running < min->nr_running)
> + return true;
> + if (target->nr_running > min->nr_running)
> + return false;
[ ... ]
> @@ -9945,19 +9985,19 @@ static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
> target = prev_cpu;
>
> sync_task_load_avg(p);
> - if (!task_util_est(p) && p_util_min == 0)
> - return target;
> -
> - eenv_task_busy_time(&eenv, p, prev_cpu);
> + task_util = task_busy_time(p, prev_cpu);
[Severity: Medium]
Does removing the !task_util_est(p) && p_util_min == 0 early return cause a
regression where zero-utilization tasks are migrated to the first
performance domain in rd->pd instead of staying on prev_cpu?
When task_util_est(p) is 0 and p_util_min is 0, task_util is 0 and
min_stat.cost does not exceed max_cost, so delta_nrg evaluates to 0 for
every performance domain. The first fitting domain sets best_nrg = 0 and
every subsequent domain is skipped by delta_nrg >= best_nrg, placing the
task on the head of rd->pd (typically the highest-capacity domain built by
build_perf_domains()).
[ ... ]
> @@ -9980,113 +10016,115 @@ static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
> if (!cpumask_test_cpu(cpu, p->cpus_ptr))
> continue;
>
> - util = cpu_util(cpu, p, cpu, 0);
> - cpu_cap = capacity_of(cpu);
> + target_stat.fits = check_cpu_with_task(p, cpu);
>
> - /*
> - * Skip CPUs that cannot satisfy the capacity request.
> - * IOW, placing the task there would make the CPU
> - * overutilized. Take uclamp into account to see how
> - * much capacity we can get out of the CPU; this is
> - * aligned with sched_cpu_util().
> - */
> - if (uclamp_is_used() && !uclamp_rq_is_idle(rq)) {
> - /*
> - * Open code uclamp_rq_util_with() except for
> - * the clamp() part. I.e.: apply max aggregation
> - * only. util_fits_cpu() logic requires to
> - * operate on non clamped util but must use the
> - * max-aggregated uclamp_{min, max}.
> - */
> - rq_util_min = uclamp_rq_get(rq, UCLAMP_MIN);
> - rq_util_max = uclamp_rq_get(rq, UCLAMP_MAX);
> + if (!target_stat.fits)
> + continue;
>
> - util_min = max(rq_util_min, p_util_min);
> - util_max = max(rq_util_max, p_util_max);
> - }
> + /* 1st select the CPU that fits best */
> + if (target_stat.fits < min_stat.fits)
> + continue;
> +
> + /* Then select the CPU with lowest cost */
> +
> + /* Get the performance of the CPU w/ the waking task */
> + target_perf = find_cpu_max_util(cpu, p, cpu);
> + target_perf = min(target_perf, cpu_actual_cap);
>
> - fits = util_fits_cpu(util, util_min, util_max, cpu);
> - if (!fits)
> + /* Needing a higher OPP means a higher cost */
> + if (target_perf > min_stat.max_perf)
> continue;
[Severity: Medium]
Can a CPU with target_stat.fits == 1 be rejected in favor of an earlier CPU
in the same performance domain that only had min_stat.fits == -1?
Because check_cpu_with_task() aggregates per-rq UCLAMP_MIN on non-idle
runqueues and get_actual_cpu_capacity() accounts for per-CPU hw_load_avg,
an earlier CPU in the domain can record min_stat.fits = -1 while a later
CPU returns target_stat.fits = 1. When target_stat.fits > min_stat.fits,
min_stat is not reset, so the better-fitting CPU can be skipped here if
target_perf > min_stat.max_perf or rejected below by update_best_cpu().
> - lsub_positive(&cpu_cap, util);
> + /*
> + * At this point, target's cost can be either equal or
> + * lower than the current minimum cost.
> + */
>
> - if (cpu == prev_cpu) {
> - /* Always use prev_cpu as a candidate. */
> - prev_spare_cap = cpu_cap;
> - prev_fits = fits;
> - } else if ((fits > max_fits) ||
> - ((fits == max_fits) && ((long)cpu_cap > max_spare_cap))) {
> - /*
> - * Find the CPU with the maximum spare capacity
> - * among the remaining CPUs in the performance
> - * domain.
> - */
> - max_spare_cap = cpu_cap;
> - max_spare_cap_cpu = cpu;
> - max_fits = fits;
> - }
> + /* Gather more statistics */
> + target_stat.cpu = cpu;
> + target_stat.runnable = cpu_runnable(cpu_rq(cpu));
> + target_stat.capa = capacity_of(cpu);
> + target_stat.nr_running = cpu_rq(cpu)->cfs.h_nr_runnable;
> + if (p->on_rq && !p->se.sched_delayed && cpu == prev_cpu)
> + target_stat.nr_running--;
[Severity: Low]
Is this target_stat.nr_running decrement for prev_cpu reachable when
update_best_cpu() compares nr_running?
update_best_cpu() returns early whenever target->cpu == prev or
min->cpu == prev before reaching the nr_running comparison, so this
adjustment is dead code and a non-idle prev_cpu is always chosen over
another non-idle CPU at the same OPP cost regardless of how many other
tasks are runnable on prev_cpu.
> +
> + /* If the target needs a lower OPP, then look up for
> + * the corresponding OPP and its associated cost.
> + * Otherwise at same cost level, select the CPU which
> + * provides best performance.
> + */
> + if (target_perf < min_stat.min_perf)
> + find_pd_cost(pd->em_pd, target_perf, &target_stat);
[Severity: Medium]
Should this check handle target_perf == min_stat.min_perf when
min_stat.min_perf > 0?
In find_pd_cost(), em_pd_get_efficient_state() selects the lowest state
with ps->performance >= max_util, and min_perf is set to the previous
state's performance. A candidate with target_perf == min_stat.min_perf
therefore fits in the previous lower-cost OPP, but strict < skips
find_pd_cost() and treats it as having the higher OPP's cost. In addition,
if em_update_performance_limits() caps pd->max_perf_state below
cpu_actual_cap, find_pd_cost() sets min_stat.max_perf to the capped state's
performance, causing later CPUs with target_perf > min_stat.max_perf to be
skipped even though they would clamp to the same maximum OPP.
> + else if (!update_best_cpu(&target_stat, &min_stat, prev_cpu, sd))
> + continue;
[Severity: High]
Does this call to update_best_cpu() fail to compile because the 5th argument
p is missing?
update_best_cpu() is defined above with five parameters, including struct
task_struct *p, so omitting p here breaks compilation until a later patch
in the series, commit "sched/fair: Take into account slice in EAS", passes
p.
> +
> + /* Save the new most efficient CPU of the PD */
> + min_stat = target_stat;
> }
>
> - if (max_spare_cap_cpu < 0 && prev_spare_cap < 0)
> + if (min_stat.cpu == -1)
> continue;
>
> - eenv_pd_busy_time(&eenv, cpus, p);
> - /* Compute the 'base' energy of the pd, without @p */
> - base_energy = compute_energy(&eenv, pd, cpus, p, -1);
> -
> - /* Evaluate the energy impact of using prev_cpu. */
> - if (prev_spare_cap > -1) {
> - prev_delta = compute_energy(&eenv, pd, cpus, p,
> - prev_cpu);
> - /* CPU utilization has changed */
> - if (prev_delta < base_energy)
> - return target;
> - prev_delta -= base_energy;
> - prev_actual_cap = cpu_actual_cap;
> - best_delta = min(best_delta, prev_delta);
> - }
> + if (min_stat.fits < best_fits)
> + continue;
>
> - /* Evaluate the energy impact of using max_spare_cap_cpu. */
> - if (max_spare_cap_cpu >= 0 && max_spare_cap > prev_spare_cap) {
> - /* Current best energy cpu fits better */
> - if (max_fits < best_fits)
> - continue;
> + /* Idle system costs nothing */
> + target_stat.max_perf = 0;
> + target_stat.cost = 0;
>
> - /*
> - * Both don't fit performance hint (i.e. uclamp_min)
> - * but best energy cpu has better capacity.
> - */
> - if ((max_fits < 0) &&
> - (cpu_actual_cap <= best_actual_cap))
> - continue;
> + /* Estimate utilization and cost without p */
> + for_each_cpu(cpu, cpus) {
> + unsigned long target_util;
>
> - cur_delta = compute_energy(&eenv, pd, cpus, p,
> - max_spare_cap_cpu);
> - /* CPU utilization has changed */
> - if (cur_delta < base_energy)
> - return target;
> - cur_delta -= base_energy;
> + /* Accumulate actual utilization w/o task p */
> + pd_actual_util += find_cpu_actual_util(cpu, p);
>
> - /*
> - * Both fit for the task but best energy cpu has lower
> - * energy impact.
> - */
> - if ((max_fits > 0) && (best_fits > 0) &&
> - (cur_delta >= best_delta))
> + /* Get the max utilization of the CPU w/o task p */
> + target_util = find_cpu_max_util(cpu, p, -1);
> + target_util = min(target_util, cpu_actual_cap);
> +
> + /* Current OPP is enough */
> + if (target_util <= target_stat.max_perf)
> continue;
>
> - best_delta = cur_delta;
> - best_energy_cpu = max_spare_cap_cpu;
> - best_fits = max_fits;
> - best_actual_cap = cpu_actual_cap;
> + /* Compute and save the cost of the OPP */
> + find_pd_cost(pd->em_pd, target_util, &target_stat);
> + max_cost = target_stat.cost;
> }
[Severity: Low]
Does target_util <= target_stat.max_perf leave max_cost at 0 when a busy
performance domain is capped with uclamp_max = 0?
When pd_actual_util > 0 but active tasks on the domain have UCLAMP_MAX = 0
(and UCLAMP_MIN = 0 with no IRQ or DL pressure), find_cpu_max_util()
returns 0 for every CPU. Because target_stat.max_perf is initialized to 0,
find_pd_cost() is never called and max_cost stays 0 instead of the minimum
OPP cost, inflating delta_nrg by pd_actual_util * min_stat.cost below.
> +
> + /* Add the energy cost of p */
> + delta_nrg = task_util * min_stat.cost;
> +
> + /*
> + * Compute the energy cost of others running at higher OPP
> + * because of p.
> + */
> + if (min_stat.cost > max_cost)
> + delta_nrg += pd_actual_util * (min_stat.cost - max_cost);
> +
> + /* Delta energy with p */
> + trace_sched_compute_energy_tp(p, min_stat.cpu, delta_nrg,
> + min_stat.max_perf,
> + pd_actual_util + task_util);
[Severity: High]
Does computing delta_nrg = task_util * min_stat.cost underestimate the
energy cost when another CPU in the performance domain is already running
at a higher OPP (max_cost > min_stat.cost)?
min_stat.cost is derived from min_stat.cpu in isolation, whereas all CPUs
in the performance domain share the same frequency and run at
max(min_stat.cost, max_cost). When a sibling CPU in a big domain is busy
at a high OPP and min_stat.cpu is idle, task p will execute at max_cost,
not min_stat.cost, so using min_stat.cost causes find_energy_efficient_cpu()
to underestimate the big domain's energy delta and misplace tasks onto it.
In addition, candidate CPUs in the first loop whose target_perf exceeds
min_stat.max_perf but remains at or below the domain's base OPP are skipped
even though they would not raise the domain's OPP cost above max_cost.
> +
> + /*
> + * The probability that delta energies are equals is almost
> + * null. PDs being sorted by max capacity, keep the one with
> + * highest max capacity if this happens.
> + * TODO: add a margin in energy cost and take into account
> + * other stats.
> + */
> + if (min_stat.fits == best_fits &&
> + delta_nrg >= best_nrg)
> + continue;
> +
> + best_fits = min_stat.fits;
> + best_nrg = delta_nrg;
> + best_cpu = min_stat.cpu;
> }
[Severity: Medium]
When no performance domain satisfies uclamp_min (min_stat.fits == -1 and
best_fits == -1), does comparing delta_nrg >= best_nrg pick the smallest
domain instead of the highest-capacity one?
Previously, when fits < 0 across candidate domains,
find_energy_efficient_cpu() compared cpu_actual_cap against best_actual_cap
and prev_actual_cap to come as close as possible to the uclamp_min request.
Because target_perf is clamped to cpu_actual_cap, a little domain clamps a
boosted task to a lower OPP and smaller delta_nrg than a big domain with
slight thermal pressure, so comparing delta_nrg places the boosted task on
the lowest-capacity domain.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261106151947.3991524-2-vincent.guittot@linaro.org?part=17
next prev parent reply other threads:[~2026-11-06 16:30 UTC|newest]
Thread overview: 68+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 15:19 [PATCH 00/18 v3] Improving latency of short slice tasks Vincent Guittot
2026-11-06 15:19 ` [PATCH 01/20 v3] sched/eevdf: Decay positive lag of sleeping entities Vincent Guittot
2026-11-06 15:43 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 02/20 v3] sched/eevdf: Reset lag at wakeup when prev CPU has been idle Vincent Guittot
2026-11-06 16:22 ` Kayra Cizmeci
2026-11-09 15:27 ` Vincent Guittot
2026-11-09 15:45 ` Peter Zijlstra
2026-11-09 16:27 ` Vincent Guittot
2026-11-11 9:03 ` Peter Zijlstra
2026-11-12 7:02 ` Vincent Guittot
2026-11-11 10:04 ` K Prateek Nayak
2026-11-12 7:33 ` Vincent Guittot
2026-11-06 15:19 ` [PATCH 03/20 v3] sched/eevdf: Reset lag at wakeup when migrating and prev " Vincent Guittot
2026-11-06 18:05 ` Kayra Cizmeci
2026-11-06 15:19 ` [PATCH 04/20 v3] sched/eevdf: Reset lag when waking up on idle cpu Vincent Guittot
2026-11-06 18:39 ` Kayra Cizmeci
2026-11-06 15:19 ` [PATCH 05/20 v3] sched/eevdf: Add per cpu cached min_slice Vincent Guittot
2026-11-07 6:53 ` Kayra Cizmeci
2026-11-09 15:27 ` Vincent Guittot
2026-11-06 15:19 ` [PATCH 06/20 v3] sched/eevdf: Add min slice check when selecting CPU Vincent Guittot
2026-11-06 15:40 ` sashiko-bot
2026-11-07 8:57 ` Kayra Cizmeci
2026-11-09 13:55 ` Peter Zijlstra
2026-11-09 14:15 ` Kayra Cizmeci
2026-11-09 15:37 ` Vincent Guittot
2026-11-09 15:34 ` Vincent Guittot
2026-11-09 15:55 ` Vincent Guittot
2026-11-09 19:56 ` Kayra Cizmeci
2026-11-10 13:32 ` Vincent Guittot
2026-11-06 15:19 ` [PATCH 07/20 v3] sched/fair: Prepare select_task_rq_fair() to be called for new cases Vincent Guittot
2026-11-06 15:39 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 08/20 v3] sched/fair: Add push task mechanism for fair Vincent Guittot
2026-11-06 16:15 ` sashiko-bot
2026-11-09 14:43 ` Peter Zijlstra
2026-11-09 16:07 ` Vincent Guittot
2026-11-11 8:59 ` Peter Zijlstra
2026-11-12 7:01 ` Vincent Guittot
2026-11-09 15:54 ` Peter Zijlstra
2026-11-09 16:14 ` Vincent Guittot
2026-11-11 9:01 ` Peter Zijlstra
2026-11-12 7:01 ` Vincent Guittot
2026-11-12 7:36 ` Vincent Guittot
2026-11-12 9:49 ` Peter Zijlstra
2026-11-12 11:29 ` Vincent Guittot
2026-11-12 12:19 ` Peter Zijlstra
2026-11-06 15:19 ` [PATCH 09/20 v3] sched/fair: Optimize " Vincent Guittot
2026-11-06 16:00 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 10/20 v3] sched/core: Add rq flag to tick parameters Vincent Guittot
2026-11-06 15:19 ` [PATCH 11/20 v3] sched/fair: Add force push task mechanism for fair Vincent Guittot
2026-11-06 16:16 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 12/20 v3] sched/fair: Support not wakeup case in select_idle_sibling Vincent Guittot
2026-11-06 15:51 ` sashiko-bot
2026-11-13 5:31 ` kernel test robot
2026-11-06 15:19 ` [PATCH 13/20] sched/eevdf: Try to push short slice task on a better CPU Vincent Guittot
2026-11-06 16:12 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 14/20 v3] sched/eevdf: Push short slice task that are not picked Vincent Guittot
2026-11-06 16:14 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 15/20 v3] sched/fair: Enable push task for preempt short Vincent Guittot
2026-11-06 16:27 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 16/20 v3] energy model: Add a get previous state function Vincent Guittot
2026-11-10 19:55 ` Rafael J. Wysocki (Intel)
2026-11-06 15:19 ` [PATCH 17/20 v3] sched/fair: Rework feec() to use cost instead of spare capacity Vincent Guittot
2026-11-06 16:30 ` sashiko-bot [this message]
2026-11-06 15:19 ` [PATCH 18/20 v3] energy model: Remove unused em_cpu_energy() Vincent Guittot
2026-11-06 15:19 ` [PATCH 19/20 v3] sched/fair: Take into account slice in EAS Vincent Guittot
2026-11-06 16:26 ` sashiko-bot
2026-11-06 15:19 ` [PATCH 20/20 v3] sched/fair: Fix tasks stacking on same idle CPU Vincent Guittot
2026-11-06 16:53 ` sashiko-bot
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=sashiko-outbox-201823@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=sched-ext@lists.linux.dev \
--cc=vincent.guittot@linaro.org \
/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