All of lore.kernel.org
 help / color / mirror / Atom feed
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

  reply	other threads:[~2026-11-06 16:30 UTC|newest]

Thread overview: 67+ 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-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.