sched/fair: Add cgroup_mode: max

In order to avoid the average CPU fraction avg(F_g_n) becoming tiny '1/N',
assume each cgroup is maximally concurrent and distrubute 'N*weight', such
that:

	F_g_n' = N * F_g_n

Giving:

	avg(F_g_n') = N*avg(F_g_n) ~ N * 1/N = 1

And while this sounds like it solves things, remember what that ~ meant. There
is the corner case when a cgroup is minimally loaded, eg a single runnable
task, therefore limit the CPU fraction to that of a nice -20 task to avoid
getting too much load.

This last bit is what makes it different from a previous proposal to allow
raising cpu.weight to '100 * N', that would not limit the mininal concurrency
case and results in a very large F_g_n. And just like F_g_n << 1 is
problematic, so is F_g_n >> 1 for the exact same reasons (it would drown the
kthreads, but it also risks overflowing the load values).

So while this might appear to be a better scheme than the current default
scheme, it doesn't really handle less than maximal concurrency nicely -- it
clips and introduces artificially large weights. So where the traditional SMP
mode works well when nr_tasks << nr_cpus, MAX doesn't work well in that regime
and vice-versa.

The meaning of "cpu.weight" would be: weight per allowed CPU.

Included for completeness (and infrastructure).

Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://patch.msgid.link/20260605124051.589618504%40infradead.org
This commit is contained in:
Peter Zijlstra
2026-06-30 10:56:52 +02:00
parent 80ad6d3338
commit 90ac22ffef
4 changed files with 76 additions and 5 deletions
+6
View File
@@ -80,6 +80,7 @@ extern void lockdep_assert_cpuset_lock_held(void);
extern void cpuset_cpus_allowed_locked(struct task_struct *p, struct cpumask *mask);
extern void cpuset_cpus_allowed(struct task_struct *p, struct cpumask *mask);
extern bool cpuset_cpus_allowed_fallback(struct task_struct *p);
extern int cpuset_num_cpus(struct cgroup *cgroup);
extern nodemask_t cpuset_mems_allowed(struct task_struct *p);
#define cpuset_current_mems_allowed (current->mems_allowed)
void cpuset_init_current_mems_allowed(void);
@@ -216,6 +217,11 @@ static inline bool cpuset_cpus_allowed_fallback(struct task_struct *p)
return false;
}
static inline int cpuset_num_cpus(struct cgroup *cgroup)
{
return num_online_cpus();
}
static inline nodemask_t cpuset_mems_allowed(struct task_struct *p)
{
return node_possible_map;
+22
View File
@@ -4124,6 +4124,28 @@ bool cpuset_cpus_allowed_fallback(struct task_struct *tsk)
return changed;
}
/*
* Returns the number of CPUs available for this cgroup.
*
* This only really works for cgroup-v2 where all the controllers are mounted
* in the same hierarchy. If not cgroup-v2 or no cpuset controller is
* configured it reverts to num_online_cpus().
*/
int cpuset_num_cpus(struct cgroup *cgrp)
{
int nr = num_online_cpus();
struct cpuset *cs;
if (is_in_v2_mode()) {
guard(rcu)();
cs = css_cs(cgroup_e_css(cgrp, &cpuset_cgrp_subsys));
if (cs)
nr = cpumask_weight(cs->effective_cpus);
}
return nr;
}
void __init cpuset_init_current_mems_allowed(void)
{
nodes_setall(current->mems_allowed);
+1
View File
@@ -640,6 +640,7 @@ static int cgroup_mode = 1;
static const char *cgroup_mode_str[] = {
"up",
"smp",
"max",
};
static int sched_cgroup_mode(const char *str)
+47 -5
View File
@@ -4801,12 +4801,10 @@ static inline int throttled_hierarchy(struct cfs_rq *cfs_rq);
*
* hence icky!
*/
static long calc_smp_shares(struct cfs_rq *cfs_rq)
static long __calc_smp_shares(struct cfs_rq *cfs_rq, long tg_shares, long shares_max)
{
long tg_weight, tg_shares, load, shares;
struct task_group *tg = cfs_rq->tg;
tg_shares = READ_ONCE(tg->shares);
long tg_weight, load, shares;
load = max(scale_load_down(cfs_rq->load.weight), cfs_rq->avg.load_avg);
@@ -4832,7 +4830,48 @@ static long calc_smp_shares(struct cfs_rq *cfs_rq)
* case no task is runnable on a CPU MIN_SHARES=2 should be returned
* instead of 0.
*/
return clamp_t(long, shares, MIN_SHARES, tg_shares);
return clamp_t(long, shares, MIN_SHARES, shares_max);
}
static int tg_cpus(struct task_group *tg)
{
int nr = num_online_cpus();
if (cpusets_enabled()) {
struct cgroup *cgrp = tg->css.cgroup;
if (cgrp)
nr = cpuset_num_cpus(cgrp);
}
return nr;
}
/*
* Func: min(fraction(nr_cpus * tg->shares), nice -20)
*
* Scale tg->shares by the maximal number of CPUs; but clip the max shares at
* nice -20, otherwise a single spinner on a 512 CPU machine would result in
* 512*NICE_0_LOAD, which is also crazy.
*/
static long calc_max_shares(struct cfs_rq *cfs_rq)
{
struct task_group *tg = cfs_rq->tg;
int nr = tg_cpus(tg);
long tg_shares = READ_ONCE(tg->shares);
long max_shares = scale_load(sched_prio_to_weight[0]);
return __calc_smp_shares(cfs_rq, tg_shares * nr, max_shares);
}
/*
* Func: fraction(tg->shares)
*
* This infamously results in tiny shares when you have many CPUs.
*/
static long calc_smp_shares(struct cfs_rq *cfs_rq)
{
struct task_group *tg = cfs_rq->tg;
long tg_shares = READ_ONCE(tg->shares);
return __calc_smp_shares(cfs_rq, tg_shares, tg_shares);
}
/*
@@ -4857,6 +4896,9 @@ void __sched_cgroup_mode_update(int mode)
default:
func = &calc_smp_shares;
break;
case 2:
func = &calc_max_shares;
break;
}
static_call_update(calc_group_shares, func);
}