Skip to contentSkip to conversation

sched/fair: cpu.max.runtime and idle refill for CPU credits

PATCH Superseded Fernand Sieber 4 participants 16 messages Nov 10 – Nov 11

Subsystems (6)
lore
Share & tools
lore
#0 lore raw

v2 cover sched/fair: cpu.max.runtime and idle refill for CPU credits

This series adds cpu.max.runtime, a cgroup v2 file to read and set the runtime the CFS bandwidth controller has for a cgroup, lets cpu.max.burst exceed the quota, and makes an idle group keep earning quota up to quota + burst. Patch 3 passes the writes on to BPF schedulers, and patch 4 lets them charge their tasks to the same runtime.

Use case
========

We run virtual machines whose CPU time is governed by CPU credits, as burstable instances are. Each instance has a baseline, which is the share of its vCPUs it may always use. Running below the baseline earns credits, running above it spends them, and the credits an instance holds are capped. When it has none left, the instance is held to its baseline. The starting credits, the baseline and the cap are part of the instance type's guarantee and must hold whatever the host does to the instance. An instance must neither lose credits nor gain credits it did not earn.

Each instance's vCPU threads run in their own cgroup, and the CFS bandwidth controller is that bucket of credits as it stands:

- quota per period is the rate credits are earned at, the baseline;
- quota + burst is the cap, the most credits an instance can hold;
- the runtime is the credit level. The controller refills it by the quota every period, up to quota + burst, and throttles the cgroup when it runs out.

The controller already does the per-CPU charging and the throttling in the scheduler, at a granularity that userspace accounting cannot match. Three things are missing. The cap cannot exceed one quota, because burst is limited to the quota (patch 1 drops that limit). The level cannot be read or set (patch 1). An idle group does not earn (patch 2), because the period timer stops after a period in which the group did not run, and the periods it sleeps through refill nothing. For example, an instance that idles for a minute with a 100ms period earns one quota instead of up to 600.

The level has to be read and set by the host's control software:

- at launch, an instance starts with the credits its type grants, not

    with one quota;

- across a host kernel live update (kexec) or a reboot, the cgroups are created anew, so their levels have to be read before and written back after, or every instance loses or gains credits;
- a live migration moves the level with the instance to the new host;
- the control plane raises or lowers the level, for example when credits move between instances.

Ben pointed out that a cpu.max or cpu.max.burst write already adds a quota to the runtime, and that cpuacct gives the usage. That falls short of the cases above:

- it can only add, and only one quota per write. Setting a level requires writing a temporary quota equal to the difference and then the real quota back. Lowering a level, which a restore or a transfer may need, requires shrinking the burst until the cap cuts the runtime and then widening it again. Each of these leaves the cgroup running with the wrong bandwidth in between, and each write goes through tg_set_cfs_bandwidth(), which visits every CPU's cfs_rq;
- usage is not the level. Deriving the level from the usage requires replaying every period's refill in userspace, including the periods the timer slept through, which duplicates the controller's accounting.

Keeping the credits in a BPF scheduler instead (patch 3 lets sched_ext follow the level) does not remove the need. The credit guarantee has to hold before a BPF scheduler loads, after it exits or errors out, and on hosts that run no BPF scheduler. The tasks are then on CFS, where the bandwidth controller is the only enforcement, and it has no BPF hook to refill or set the runtime.

Patch 2 changes behavior for existing cpu.max.burst users. A group with a burst that becomes runnable after idling has up to quota + burst, as if it had been running below its quota throughout, instead of the runtime it had when the timer stopped. Groups without a burst see no change.

Testing
=======

On tip/sched/core in qemu with lockdep, the cgroup test_cpu selftests pass, including eight new ones for patches 1 and 2, and so does the new sched_ext selftest cgroup_runtime, which covers patches 3 and 4. Every patch builds with W=1 and no new warnings, with and without CONFIG_CFS_BANDWIDTH and on 32-bit.

An AI coding assistant (Claude Code) helped write the selftests and changelogs and ran the build and test matrix; I reviewed all of it.

Changes in v2
=============

- New patch 2: refill the periods the period timer slept through.
- New patch 4: with the new SCX_OPS_CHARGE_CPU_MAX flag, a BPF scheduler's tasks are charged to the fair class's runtime, read with the new kfunc scx_bpf_task_cpu_max_runtime(); selftest cgroup_runtime.
- Patch 1: a write distributes the runtime to throttled cfs_rqs at once (Ben); a write moves runtime_snap so it is not counted as burst usage; a read is capped at quota + burst so it can be written back; a write needs a quota; burst is bounded so quota + burst cannot overflow (Ben); cpu.max.runtime needs CONFIG_CFS_BANDWIDTH (v1 did not build with CONFIG_EXT_GROUP_SCHED=y and CONFIG_CFS_BANDWIDTH=n); documented in cgroup-v2.rst and sched-bwc.rst; four more selftests.
- Patch 3: the callback is invoked on every write, since writing the same value again adds back what the cgroup ran; it is serialized with cpu.max writes and may be sleepable; the cached value is dropped; the cid-form variant, ops.cpuctl_set_runtime(), is added. Both writes hold a new mutex across the fair class's update and the callback, so cpu.max and cpu.max.runtime writes reach the scheduler in the order the fair class applied them.
- Use case described (Tejun). Rebased on tip/sched/core.

v1: https://lore.kernel.org/all/[email protected]/

Fernand Sieber (4):
sched/fair: expose cpu.max.runtime to set bandwidth runtime directly sched/fair: refill bandwidth for the periods the timer slept through
sched_ext: add cgroup_set_runtime ops callback
sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

 Documentation/admin-guide/cgroup-v2.rst       |  27 +-
 Documentation/scheduler/sched-bwc.rst         |  14 +-
 kernel/sched/core.c                           |  61 +-
 kernel/sched/ext/ext.c                        |  53 ++
 kernel/sched/ext/ext.h                        |   2 +
 kernel/sched/ext/internal.h                   |  45 +-
 kernel/sched/fair.c                           | 260 ++++++-
 kernel/sched/sched.h                          |  15 +
 tools/sched_ext/include/scx/common.bpf.h      |   1 +
 tools/sched_ext/include/scx/compat.h          |   1 +
 tools/testing/selftests/cgroup/test_cpu.c     | 519 ++++++++++++++
 tools/testing/selftests/sched_ext/Makefile    |   1 +
 .../selftests/sched_ext/cgroup_runtime.bpf.c  | 195 ++++++
 .../selftests/sched_ext/cgroup_runtime.c      | 652 ++++++++++++++++++
 tools/testing/selftests/sched_ext/config      |   1 +
 15 files changed, 1836 insertions(+), 11 deletions(-)
 create mode 100644 tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c
 create mode 100644 tools/testing/selftests/sched_ext/cgroup_runtime.c

base-commit: d7e670601af6c564ccf6d3b2ddf70ee4f41a6d1c

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#1 lore raw

PATCH v2 1/4 sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

Add cpu.max.runtime, a cgroup v2 file that reads and sets the CFS bandwidth runtime, cfs_b->runtime, in microseconds. Userspace can then grant a cgroup runtime beyond its quota. Refills still add only the quota, capped at quota + burst, so the excess is used up over time and the group returns to its quota per period.

A write is rejected without a quota or above quota + burst, the most the group can accumulate. A read is capped at quota + burst so it can be written back; uncapped slack from __return_cfs_rq_runtime() can exceed that until the next refill. Runtime the CPUs already took, at most a slice each, is not included and a write leaves it in place.

A write distributes the new runtime to throttled cfs_rqs at once, as the slack timer does, rather than waiting for the unsynchronized period timer. It also shifts runtime_snap by the change, so cpu.stat burst accounting neither counts the write nor hides what the group ran since the last refill.

Also drop the burst <= quota limit and require only quota + burst <= max_bw_runtime_us (about 203 days), the quota's own limit, so a group can accumulate many periods of quota. This applies to cgroup v1 cpu.cfs_burst_us too, which shares tg_set_bandwidth().

The file exists with CONFIG_CFS_BANDWIDTH only.

---
 Documentation/admin-guide/cgroup-v2.rst   |  22 +-
 Documentation/scheduler/sched-bwc.rst     |  13 +-
 kernel/sched/core.c                       |  34 ++-
 kernel/sched/fair.c                       |  61 ++++
 kernel/sched/sched.h                      |   2 +
 tools/testing/selftests/cgroup/test_cpu.c | 336 ++++++++++++++++++++++
 6 files changed, 459 insertions(+), 9 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +21 -1
index 9e3c67b02ae2e..c5a02057f0242 100644
@@ -1231,12 +1231,32 @@ will be referred to. All time durations are in microseconds.
 	A read-write single value file which exists on non-root
 	cgroups.  The default is "0".
 
-	The burst in the range [0, $MAX].
+	The burst. The cgroup can accumulate the runtime it does not
+	use, up to $MAX + cpu.max.burst. The burst may exceed $MAX, as
+	long as $MAX + cpu.max.burst is at most 2^44 - 1 (about 203
+	days).
 
 	This file affects only processes under the fair-class scheduler and a BPF
 	scheduler with the ``cgroup_set_bandwidth`` callback depending on what
 	the callback actually does.
 
+  cpu.max.runtime
+	A read-write single value file which exists on non-root
+	cgroups if the kernel is built with CONFIG_CFS_BANDWIDTH.
+
+	The runtime the cgroup has accumulated and not used yet, in
+	microseconds. Each period refills it by $MAX, up to $MAX +
+	cpu.max.burst, and a read returns at most $MAX + cpu.max.burst.
+	A write sets it, up to $MAX + cpu.max.burst, and lets a cgroup
+	that ran out of runtime run again at once. A write fails with
+	EINVAL while $MAX is "max".
+
+	The runtime does not include what the cgroup's CPUs already took
+	from it, at most a slice each (sched_cfs_bandwidth_slice_us).
+	A write leaves that with the CPUs.
+
+	This file affects only processes under the fair-class scheduler.
+
   cpu.pressure
 	A read-write nested-keyed file.
 
Documentation/scheduler/sched-bwc.rst +7 -6
index e881a945c1888..c22b3c8c14e29 100644
@@ -90,8 +90,8 @@ bandwidth restriction in place, such a group is described as an unconstrained
 bandwidth group. This represents the traditional work-conserving behavior for
 CFS.
 
-Writing any (valid) positive value(s) no smaller than cpu.cfs_burst_us will
-enact the specified bandwidth limit. The minimum quota allowed for the quota or
+Writing any (valid) positive value(s) will enact the specified bandwidth
+limit. The minimum quota allowed for the quota or
 period is 1ms. There is also an upper bound on the period length of 1s.
 Additional restrictions exist when bandwidth limits are used in a hierarchical
 fashion, these are explained in more detail below.
@@ -101,9 +101,10 @@ and return the group to an unconstrained state once more.
 
 A value of 0 for cpu.cfs_burst_us indicates that the group can not accumulate
 any unused bandwidth. It makes the traditional bandwidth control behavior for
-CFS unchanged. Writing any (valid) positive value(s) no larger than
-cpu.cfs_quota_us into cpu.cfs_burst_us will enact the cap on unused bandwidth
-accumulation.
+CFS unchanged. Writing any (valid) positive value(s) into cpu.cfs_burst_us
+will enact the cap on unused bandwidth accumulation: the group can accumulate
+up to cpu.cfs_quota_us + cpu.cfs_burst_us. The burst may be larger than the
+quota, as long as their sum is at most 2^44 - 1 microseconds (about 203 days).
 
 Any updates to a group's bandwidth specification will result in it becoming
 unthrottled if it is in a constrained state.
@@ -243,4 +244,4 @@ Examples
 	# echo 50000 > cpu.cfs_period_us /* period = 50ms */
 	# echo 10000 > cpu.cfs_burst_us /* burst = 10ms */
 
-   Larger buffer setting (no larger than quota) allows greater burst capacity.
+   Larger buffer setting allows greater burst capacity.
kernel/sched/core.c +32 -2
index 175b52a7ea793..76fcd70ff207a 100644
@@ -10131,8 +10131,7 @@ static int tg_set_bandwidth(struct task_group *tg,
 	if (quota_us != RUNTIME_INF && quota_us > max_bw_runtime_us)
 		return -EINVAL;
 
-	if (quota_us != RUNTIME_INF && (burst_us > quota_us ||
-					burst_us + quota_us > max_bw_runtime_us))
+	if (quota_us != RUNTIME_INF && burst_us > max_bw_runtime_us - quota_us)
 		return -EINVAL;
 
 #ifdef CONFIG_CFS_BANDWIDTH
@@ -10193,6 +10192,29 @@ static int cpu_burst_write_u64(struct cgroup_subsys_state *css,
 	tg_bandwidth(tg, &period_us, &quota_us, NULL);
 	return tg_set_bandwidth(tg, period_us, quota_us, burst_us);
 }
+
+#ifdef CONFIG_CFS_BANDWIDTH
+static int cpu_runtime_write_u64(struct cgroup_subsys_state *css,
+				 struct cftype *cftype, u64 runtime_us)
+{
+	struct task_group *tg = css_tg(css);
+
+	if (runtime_us > max_bw_runtime_us)
+		return -EINVAL;
+
+	return cfs_bandwidth_set_runtime(&tg->cfs_bandwidth,
+					 runtime_us * NSEC_PER_USEC);
+}
+
+static u64 cpu_runtime_read_u64(struct cgroup_subsys_state *css,
+				struct cftype *cftype)
+{
+	struct task_group *tg = css_tg(css);
+
+	return div_u64(cfs_bandwidth_get_runtime(&tg->cfs_bandwidth),
+		       NSEC_PER_USEC);
+}
+#endif /* CONFIG_CFS_BANDWIDTH */
 #endif /* CONFIG_GROUP_SCHED_BANDWIDTH */
 
 #ifdef CONFIG_RT_GROUP_SCHED
@@ -10544,7 +10566,15 @@ static struct cftype cpu_files[] = {
 		.read_u64 = cpu_burst_read_u64,
 		.write_u64 = cpu_burst_write_u64,
 	},
+#ifdef CONFIG_CFS_BANDWIDTH
+	{
+		.name = "max.runtime",
+		.flags = CFTYPE_NOT_ON_ROOT,
+		.read_u64 = cpu_runtime_read_u64,
+		.write_u64 = cpu_runtime_write_u64,
+	},
 #endif /* CONFIG_CFS_BANDWIDTH */
+#endif /* CONFIG_GROUP_SCHED_BANDWIDTH */
 #ifdef CONFIG_UCLAMP_TASK_GROUP
 	{
 		.name = "uclamp.min",
kernel/sched/fair.c +61 -0

73 lines not on this page · raw message

kernel/sched/sched.h +2 -0
index a4df59c9c9b60..1f60c84e0dc38 100644
@@ -578,6 +578,8 @@ extern void init_cfs_bandwidth(struct cfs_bandwidth *cfs_b, struct cfs_bandwidth
 
 extern void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b);
 extern void start_cfs_bandwidth(struct cfs_bandwidth *cfs_b);
+u64 cfs_bandwidth_get_runtime(struct cfs_bandwidth *cfs_b);
+int cfs_bandwidth_set_runtime(struct cfs_bandwidth *cfs_b, u64 runtime);
 extern void unthrottle_cfs_rq(struct cfs_rq *cfs_rq);
 extern bool cfs_task_bw_constrained(struct task_struct *p);
 
tools/testing/selftests/cgroup/test_cpu.c +336 -0

348 lines not on this page · raw message

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#2 lore raw

Re: [PATCH v2 1/4] sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

Thank you for your contribution! Sashiko AI review found 3 potential issues to consider: - [Medium] sched/fair: distribute_cfs_runtime() IRQ assert fails on PREEMPT_RT - [Medium] selftests/cgroup…

Thank you for your contribution! Sashiko AI review found 3 potential issues to consider:
- [Medium] sched/fair: distribute_cfs_runtime() IRQ assert fails on PREEMPT_RT
- [Medium] selftests/cgroup: hog burns runtime before period start in test_cpu
- [Low] selftests/cgroup: missing CONFIG_CFS_BANDWIDTH skip in test_cpu --

commit bbd8379bd9e3f88e46c648f7388044e945e9df20
Author: Fernand Sieber <[email protected]>

sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

This commit adds the cgroup v2 cpu.max.runtime file to read and set cfs_b->runtime directly, lifts the burst <= quota restriction, and adds cgroup selftests for the new interface.

kernel/sched/fair.c static bool distribute_cfs_runtime(struct cfs_bandwidth *cfs_b) · line 7428

Quoted 13 lines
kernel/sched/fair.c +2 -0
index d4cc0caee6265..58e948ed1792f 100644
@@ -7421,6 +7421,8 @@ static bool distribute_cfs_runtime(struct cfs_bandwidth *cfs_b)
 	struct cfs_rq *cfs_rq;
 	struct rq *rq;
 
+	lockdep_assert_irqs_disabled();
+
 	guard(rcu)();
 
 	list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq,

[Severity: Medium]
Does lockdep_assert_irqs_disabled() trigger a lockdep warning on CONFIG_PREEMPT_RT when distribute_cfs_runtime() is called from do_sched_cfs_period_timer() or do_sched_cfs_slack_timer()?

init_cfs_bandwidth() sets up cfs_b->period_timer and cfs_b->slack_timer without HRTIMER_MODE_HARD, so on CONFIG_PREEMPT_RT their callbacks run in softirq context with hardware interrupts enabled. Both do_sched_cfs_period_timer() and do_sched_cfs_slack_timer() release cfs_b->lock via raw_spin_unlock_irqrestore() before calling distribute_cfs_runtime(cfs_b), leaving hardware interrupts enabled on entry. (distribute_cfs_runtime() itself uses guard(rq_lock_irqsave)(rq) and scoped_guard(irqsave) around __cfsb_csd_unthrottle() for that reason.)

Quoted 4 lines
tools/testing/selftests/cgroup/test_cpu.c +0 -0
index 3456e3e988c68..278d7e16eeb2c 100644

[ ... ]

Quoted 2 lines
tools/testing/selftests/cgroup/test_cpu.c +2 -0
+static int test_cpucg_max_runtime(const char *root)
+{

[ ... ]

tools/testing/selftests/cgroup/test_cpu.c static int test_cpucg_max_nested(const char *root) · line 873

Quoted 10 lines
tools/testing/selftests/cgroup/test_cpu.c +10 -0
+	cpucg = cg_name(root, "cpucg_runtime_test");
+	if (!cpucg)
+		goto cleanup;
+
+	if (cg_create(cpucg))
+		goto cleanup;
+
+	/* Without a quota there is no runtime to set. */
+	if (!cg_write_numeric(cpucg, "cpu.max.runtime", runtime_usec))
+		goto cleanup;

[Severity: Low]
Should the new test_cpucg_max_runtime* tests check whether cpu.max.runtime exists and return KSFT_SKIP when CONFIG_CFS_BANDWIDTH is disabled (or add CONFIG_CFS_BANDWIDTH=y to tools/testing/selftests/cgroup/config)?

cpu.max.runtime is only created under CONFIG_CFS_BANDWIDTH, which defaults to n and is not listed in tools/testing/selftests/cgroup/config, so all five tests currently return KSFT_FAIL on kernels built without it.

[ ... ]

Quoted 40 lines
tools/testing/selftests/cgroup/test_cpu.c +40 -0
+static int test_cpucg_max_runtime_burst_kept(const char *root)
+{
+	int ret = KSFT_FAIL;
+	long burst_before, burst_after, runtime_usec;
+	char *cpucg;
+	int pid;
+
+	cpucg = cg_name(root, "cpucg_runtime_burst_kept_test");
+	if (!cpucg)
+		goto cleanup;
+
+	if (cg_create(cpucg))
+		goto cleanup;
+
+	/* 10ms per 1s period, and a runtime of 1s to run on. */
+	if (cg_write(cpucg, "cpu.max", "10000 1000000"))
+		goto cleanup;
+	if (cg_write_numeric(cpucg, "cpu.max.burst", 990000))
+		goto cleanup;
+	if (cg_write_numeric(cpucg, "cpu.max.runtime", 1000000))
+		goto cleanup;
+
+	struct cpu_hog_func_param param = {
+		.nprocs = 1,
+		.ts = { .tv_sec = 10, .tv_nsec = 0 },
+		.clock_type = CPU_HOG_CLOCK_WALL,
+	};
+	pid = cg_run_nowait(cpucg, hog_cpus_timed, (void *)&param);
+	if (pid < 0)
+		goto cleanup;
+
+	/* Run some 300ms, all in one period, and stop. */
+	if (wait_period_start(cpucg)) {
+		kill(pid, SIGKILL);
+		waitpid(pid, NULL, 0);
+		goto cleanup;
+	}
+	burst_before = cg_read_key_long(cpucg, "cpu.stat", "burst_usec");
+	usleep(300000);
+	kill(pid, SIGKILL);

tools/testing/selftests/cgroup/test_cpu.c static int test_cpucg_max_nested(const char *root) · line 1079

Quoted 10 lines
tools/testing/selftests/cgroup/test_cpu.c +10 -0
+	waitpid(pid, NULL, 0);
+
+	/* Write back what is left: the next refill counts the 300ms run. */
+	runtime_usec = cg_read_long(cpucg, "cpu.max.runtime");
+	if (cg_write_numeric(cpucg, "cpu.max.runtime", runtime_usec))
+		goto cleanup;
+	sleep(1);
+	burst_after = cg_read_key_long(cpucg, "cpu.stat", "burst_usec");
+	if (burst_before < 0 || burst_after - burst_before < 250000)
+		goto cleanup;

[Severity: Medium]
Can hog_cpus_timed burn most of the 1s cpu.max.runtime before wait_period_start() returns?

Because cfs_b->period_timer starts with a random offset, wait_period_start() can wait up to 1s for the first period boundary while the hog is already running. When that wait exceeds ~750ms, the refill at the period boundary only adds the 10ms quota, leaving less than 250ms of runtime for the measured 300ms window and causing the burst_after - burst_before < 250000 check to fail. Writing 1000000 to cpu.max.runtime after wait_period_start() (or starting the hog after it) would avoid draining the runtime early.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#3 lore raw

Re: [PATCH v2 1/4] sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

In reply to Fernand Sieber
TL;DR: In fair.c, cpu.max.runtime may return stale leftover runtime (e.g. 50000) after a group is switched to "max" quota, while writes fail; a behaviour question about returning 0 or failing the…
Quoted 1 line

sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

TL;DR: In fair.c, cpu.max.runtime may return stale leftover runtime (e.g. 50000) after a group is switched to "max" quota, while writes fail; a behaviour question about returning 0 or failing the read.

Quoted 3 lines
kernel/sched/fair.c +0 -0

[ ... ]

kernel/sched/fair.c static void init_cfs_rq_runtime(struct cfs_rq *cfs_rq) · line 7834

Quoted 12 lines
kernel/sched/fair.c +12 -0
+u64 cfs_bandwidth_get_runtime(struct cfs_bandwidth *cfs_b)
+{
+	u64 runtime;
+
+	guard(raw_spinlock_irq)(&cfs_b->lock);
+
+	runtime = cfs_b->runtime;
+	if (cfs_b->quota != RUNTIME_INF)
+		runtime = min(runtime, cfs_b->quota + cfs_b->burst);
+
+	return runtime;
+}

When the group is switched to "max", does cpu.max.runtime return a stale value?

tg_set_cfs_bandwidth() calls __refill_cfs_bandwidth_runtime(), which returns early when quota is RUNTIME_INF:

    if (unlikely(cfs_b->quota == RUNTIME_INF))
            return;

After that nothing updates cfs_b->runtime, so the value left over from the last finite quota is returned uncapped here.

For example, write "50000 100000" to cpu.max, leave the group idle, then write "max 100000". cpu.max.runtime keeps reading 50000 for as long as the group is unlimited, while a group that never had a quota reads 0.

Writes fail with EINVAL in that state, but the read reports runtime the group does not have and that is not being tracked.

Should this return a defined value (for example 0) when quota is RUNTIME_INF, or fail the read the way the write fails?

This is documented as "The runtime the cgroup has accumulated and not used yet", which has no meaning for an unconstrained group.

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42982707042

#4 lore raw

PATCH v2 2/4 sched/fair: refill bandwidth for the periods the timer slept through

In reply to Fernand Sieber

cpu.max.burst lets a group accumulate unused quota up to quota + burst. But the period timer stops after a period in which the group took no runtime, and start_cfs_bandwidth() restarts it with hrtimer_forward_now() and discards the periods that passed. A group that sits idle stops accumulating, and one that wakes after idling for a long time has only what it had when the timer stopped.

Count the missed periods instead. While the timer is stopped, its expiry is the first period boundary it missed, so the overrun at restart is the number of refills owed. Add quota for each, capped at quota + burst as __refill_cfs_bandwidth_runtime() caps it.

Without a burst the cap is quota. The timer stops right after a refill that nothing drew from, so the runtime is at quota then, and groups without a burst see no change. Groups with a burst do see a change. A group that has been idle now has up to quota + burst when it runs again. The documentation of cpu.max.burst, cpu.max.runtime and cpu.cfs_burst_us says so.

A change of the bandwidth (cpu.max, cpu.max.burst) applies from that moment, and the group keeps what it earned before it. The periods owed are first refilled at the bandwidth they were earned under, capped at its quota + burst, and then the new bandwidth applies, with its own cap. A group that had no quota earned nothing, so one given a quota after a long time unconstrained does not start full. A cpu.max.runtime write also refills the periods owed first, so that it replaces them, and only the periods after the write refill.

cpu.max.runtime reports the runtime the group has now, owed refills included, without starting the timer.

The idle periods are not added to cpu.stat's nr_periods, which counts the periods in which the timer ran. A timer that runs late by several periods still refills only once, unlike a stopped one; this patch leaves that as it is.

---
 Documentation/admin-guide/cgroup-v2.rst   |  15 +-
 Documentation/scheduler/sched-bwc.rst     |   5 +-
 kernel/sched/core.c                       |   6 +
 kernel/sched/fair.c                       |  79 +++++++++-
 kernel/sched/sched.h                      |   1 +
 tools/testing/selftests/cgroup/test_cpu.c | 183 ++++++++++++++++++++++
 6 files changed, 278 insertions(+), 11 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +8 -7
index c5a02057f0242..2c220e5998639 100644
@@ -1232,9 +1232,9 @@ will be referred to. All time durations are in microseconds.
 	cgroups.  The default is "0".
 
 	The burst. The cgroup can accumulate the runtime it does not
-	use, up to $MAX + cpu.max.burst. The burst may exceed $MAX, as
-	long as $MAX + cpu.max.burst is at most 2^44 - 1 (about 203
-	days).
+	use, up to $MAX + cpu.max.burst, also in the periods in which it
+	does not run. The burst may exceed $MAX, as long as $MAX +
+	cpu.max.burst is at most 2^44 - 1 (about 203 days).
 
 	This file affects only processes under the fair-class scheduler and a BPF
 	scheduler with the ``cgroup_set_bandwidth`` callback depending on what
@@ -1246,10 +1246,11 @@ will be referred to. All time durations are in microseconds.
 
 	The runtime the cgroup has accumulated and not used yet, in
 	microseconds. Each period refills it by $MAX, up to $MAX +
-	cpu.max.burst, and a read returns at most $MAX + cpu.max.burst.
-	A write sets it, up to $MAX + cpu.max.burst, and lets a cgroup
-	that ran out of runtime run again at once. A write fails with
-	EINVAL while $MAX is "max".
+	cpu.max.burst, whether the cgroup runs in it or not. A read
+	returns at most $MAX + cpu.max.burst. A write sets it, up to
+	$MAX + cpu.max.burst, and lets a cgroup that ran out of runtime
+	run again at once. A write fails with EINVAL while $MAX is
+	"max".
 
 	The runtime does not include what the cgroup's CPUs already took
 	from it, at most a slice each (sched_cfs_bandwidth_slice_us).
Documentation/scheduler/sched-bwc.rst +3 -2
index c22b3c8c14e29..ccf7434ca73f0 100644
@@ -103,8 +103,9 @@ A value of 0 for cpu.cfs_burst_us indicates that the group can not accumulate
 any unused bandwidth. It makes the traditional bandwidth control behavior for
 CFS unchanged. Writing any (valid) positive value(s) into cpu.cfs_burst_us
 will enact the cap on unused bandwidth accumulation: the group can accumulate
-up to cpu.cfs_quota_us + cpu.cfs_burst_us. The burst may be larger than the
-quota, as long as their sum is at most 2^44 - 1 microseconds (about 203 days).
+up to cpu.cfs_quota_us + cpu.cfs_burst_us, also over periods in which it does
+not run. The burst may be larger than the quota, as long as their sum is at
+most 2^44 - 1 microseconds (about 203 days).
 
 Any updates to a group's bandwidth specification will result in it becoming
 unthrottled if it is in a constrained state.
kernel/sched/core.c +6 -0
index 76fcd70ff207a..75a5f5d1b8f54 100644
@@ -9856,6 +9856,12 @@ static int tg_set_cfs_bandwidth(struct task_group *tg,
 		cfs_bandwidth_usage_inc();
 
 	scoped_guard (raw_spinlock_irq, &cfs_b->lock) {
+		/*
+		 * The new bandwidth applies from now on. The group keeps what
+		 * it earned while idle under the old one.
+		 */
+		cfs_bandwidth_refill_idle_periods(cfs_b);
+
 		cfs_b->period = ns_to_ktime(period);
 		cfs_b->quota = quota;
 		cfs_b->burst = burst;
kernel/sched/fair.c +77 -2

104 lines not on this page · raw message

kernel/sched/sched.h +1 -0
index 1f60c84e0dc38..676e6371d0265 100644
@@ -580,6 +580,7 @@ extern void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b);
 extern void start_cfs_bandwidth(struct cfs_bandwidth *cfs_b);
 u64 cfs_bandwidth_get_runtime(struct cfs_bandwidth *cfs_b);
 int cfs_bandwidth_set_runtime(struct cfs_bandwidth *cfs_b, u64 runtime);
+void cfs_bandwidth_refill_idle_periods(struct cfs_bandwidth *cfs_b);
 extern void unthrottle_cfs_rq(struct cfs_rq *cfs_rq);
 extern bool cfs_task_bw_constrained(struct task_struct *p);
 
tools/testing/selftests/cgroup/test_cpu.c +183 -0

195 lines not on this page · raw message

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#5 lore raw

Re: [PATCH v2 2/4] sched/fair: refill bandwidth for the periods the timer slept through

Thank you for your contribution! Sashiko AI review found 1 potential issue to consider: - [Medium] sched/fair: period_timer forwarded with old period in tg_set_cfs_bandwidth() --

Thank you for your contribution! Sashiko AI review found 1 potential issue to consider:
- [Medium] sched/fair: period_timer forwarded with old period in tg_set_cfs_bandwidth()
--

commit 6dcac4d8cedf5c5fafb47bd8f0ac725a2be649bc
Author: Fernand Sieber <[email protected]>

sched/fair: refill bandwidth for the periods the timer slept through

This commit refills a CFS group's accumulated bandwidth for the period boundaries missed while its period timer was stopped, capped at quota + burst, and updates bandwidth configuration and cpu.max.runtime reads and writes to account for those idle periods.

Quoted 7 lines
kernel/sched/core.c +0 -0
index 76fcd70ff207a..75a5f5d1b8f54 100644
@@ -9856,6 +9856,12 @@ static int tg_set_cfs_bandwidth(struct task_group *tg,
 		cfs_bandwidth_usage_inc();
 

kernel/sched/core.c static int tg_set_cfs_bandwidth(struct task_group *tg, · line 9867

Quoted 10 lines
kernel/sched/core.c +6 -0
 	scoped_guard (raw_spinlock_irq, &cfs_b->lock) {
+		/*
+		 * The new bandwidth applies from now on. The group keeps what
+		 * it earned while idle under the old one.
+		 */
+		cfs_bandwidth_refill_idle_periods(cfs_b);
+
 		cfs_b->period = ns_to_ktime(period);
 		cfs_b->quota = quota;
 		cfs_b->burst = burst;

[Severity: Medium]
When cfs_b->period_active is 0, does calling cfs_bandwidth_refill_idle_periods() before updating cfs_b->period arm cfs_b->period_timer with the old period's expiry instead of the new one?

cfs_bandwidth_refill_idle_periods() calls hrtimer_forward_now() with the old cfs_b->period, pushing period_timer's expiry into (now, now + old_period]. When tg_set_cfs_bandwidth() then calls start_cfs_bandwidth() after updating cfs_b->period:

kernel/sched/core.c:tg_set_cfs_bandwidth() {
	...
		__refill_cfs_bandwidth_runtime(cfs_b);

		/*
		 * Restart the period timer (if active) to handle new
		 * period expiry:
		 */
		if (runtime_enabled)
			start_cfs_bandwidth(cfs_b);
	}
}

the second cfs_bandwidth_refill_idle_periods() call inside start_cfs_bandwidth() sees an expiry already in the future, so hrtimer_forward_now() returns 0 without adjusting it to the new period. When a newly created cgroup (initialized with a 100ms period) or an idle cgroup is configured with a shorter period such as 1ms, hrtimer_start_expires() arms period_timer up to old_period in the future, starving the group of refills for that interval after its initial quota is spent.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#6 lore raw

PATCH v2 3/4 sched_ext: add cgroup_set_runtime ops callback

In reply to Fernand Sieber

Add a sched_ext_ops callback, and its cid-form variant, that is invoked when userspace writes cpu.max.runtime. A BPF scheduler that implements ops.cgroup_set_bandwidth() to enforce a cgroup's CPU bandwidth also needs the written runtime, so that its budget for the cgroup matches the fair class's, for example after userspace restores a saved runtime or adds to it.

The callback is invoked on every write, after the fair class has applied it. Unlike cgroup_set_bandwidth(), it does not skip a write of the value last written. The runtime goes down as the cgroup runs, so writing the same value again is a top-up.

A new tg_bandwidth_mutex is held across the fair class's update and the callback, by the cpu.max.runtime write and by tg_set_bandwidth(), which the cpu.max and cgroup v1 cpu.cfs_* writes go through and which calls cgroup_set_bandwidth(). The runtime passed is then at most the quota + burst the scheduler was last given, and cpu.max and cpu.max.runtime writes reach it in the order the fair class applied them. The mutex is taken before, and the callbacks run after, the fair class's cpus_read_lock() and cfs_constraints_mutex sections, so cgroup_set_bandwidth() runs as it did before.

Like cgroup_set_bandwidth(), the callback runs in process context and may be sleepable.

---
 Documentation/admin-guide/cgroup-v2.rst |  4 +++-
 kernel/sched/core.c                     | 25 +++++++++++++++++++++++--
 kernel/sched/ext/ext.c                  | 19 +++++++++++++++++++
 kernel/sched/ext/ext.h                  |  2 ++
 kernel/sched/ext/internal.h             | 21 +++++++++++++++++++++
 5 files changed, 68 insertions(+), 3 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +3 -1
index 2c220e5998639..b77ca6f8ca548 100644
@@ -1256,7 +1256,9 @@ will be referred to. All time durations are in microseconds.
 	from it, at most a slice each (sched_cfs_bandwidth_slice_us).
 	A write leaves that with the CPUs.
 
-	This file affects only processes under the fair-class scheduler.
+	This file affects only processes under the fair-class scheduler and a BPF
+	scheduler with the ``cgroup_set_runtime`` callback depending on what
+	the callback actually does.
 
   cpu.pressure
 	A read-write nested-keyed file.
kernel/sched/core.c +23 -2
index 75a5f5d1b8f54..25a4acb72106c 100644
@@ -10099,6 +10099,14 @@ static u64 cpu_period_read_u64(struct cgroup_subsys_state *css,
 	return period_us;
 }
 
+/*
+ * Held across a bandwidth or runtime write and the sched_ext callback that
+ * follows it, so that the BPF scheduler gets the writes in the order the fair
+ * class applied them. The callback runs after the fair class's update has
+ * dropped cpus_read_lock() and cfs_constraints_mutex.
+ */
+static DEFINE_MUTEX(tg_bandwidth_mutex);
+
 static int tg_set_bandwidth(struct task_group *tg,
 			    u64 period_us, u64 quota_us, u64 burst_us)
 {
@@ -10140,6 +10148,8 @@ static int tg_set_bandwidth(struct task_group *tg,
 	if (quota_us != RUNTIME_INF && burst_us > max_bw_runtime_us - quota_us)
 		return -EINVAL;
 
+	guard(mutex)(&tg_bandwidth_mutex);
+
 #ifdef CONFIG_CFS_BANDWIDTH
 	ret = tg_set_cfs_bandwidth(tg, period_us, quota_us, burst_us);
 #endif /* CONFIG_CFS_BANDWIDTH */
@@ -10204,12 +10214,23 @@ static int cpu_runtime_write_u64(struct cgroup_subsys_state *css,
 				 struct cftype *cftype, u64 runtime_us)
 {
 	struct task_group *tg = css_tg(css);
+	int ret;
 
 	if (runtime_us > max_bw_runtime_us)
 		return -EINVAL;
 
-	return cfs_bandwidth_set_runtime(&tg->cfs_bandwidth,
-					 runtime_us * NSEC_PER_USEC);
+	/*
+	 * Serialize against tg_set_bandwidth(), so that the BPF scheduler gets
+	 * the runtime checked against the current quota + burst, and this and
+	 * cpu.max writes reach it in the order the fair class took them.
+	 */
+	guard(mutex)(&tg_bandwidth_mutex);
+
+	ret = cfs_bandwidth_set_runtime(&tg->cfs_bandwidth,
+					runtime_us * NSEC_PER_USEC);
+	if (!ret)
+		scx_group_set_runtime(tg, runtime_us);
+	return ret;
 }
 
 static u64 cpu_runtime_read_u64(struct cgroup_subsys_state *css,
kernel/sched/ext/ext.c +19 -0
index ebdf7b91f9d37..265a80dd93282 100644
@@ -5817,6 +5817,19 @@ void scx_group_set_bandwidth(struct task_group *tg,
 
 	percpu_up_read(&scx_cgroup_ops_rwsem);
 }
+
+void scx_group_set_runtime(struct task_group *tg, u64 runtime_us)
+{
+	struct scx_sched *sch;
+
+	percpu_down_read(&scx_cgroup_ops_rwsem);
+	sch = scx_tg_knob_sched(tg);
+
+	if (scx_cgroup_enabled && sch && SCX_HAS_OP(sch, cgroup_set_runtime))
+		SCX_CALL_OP(sch, cgroup_set_runtime, NULL, tg_cgrp(tg), runtime_us);
+
+	percpu_up_read(&scx_cgroup_ops_rwsem);
+}
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 
 #if defined(CONFIG_EXT_GROUP_SCHED) || defined(CONFIG_EXT_SUB_SCHED)
@@ -9230,6 +9243,7 @@ static int bpf_scx_check_member(const struct btf_type *t,
 	case offsetof(struct sched_ext_ops, cgroup_exit):
 	case offsetof(struct sched_ext_ops, cgroup_prep_move):
 	case offsetof(struct sched_ext_ops, cgroup_set_bandwidth):
+	case offsetof(struct sched_ext_ops, cgroup_set_runtime):
 #endif
 	case offsetof(struct sched_ext_ops, cpu_online):
 	case offsetof(struct sched_ext_ops, cpu_offline):
@@ -9392,6 +9406,7 @@ static void sched_ext_ops__cgroup_cancel_move(struct task_struct *p, struct cgro
 static void sched_ext_ops__cgroup_set_weight(struct cgroup *cgrp, u32 weight) {}
 static void sched_ext_ops__cgroup_set_bandwidth(struct cgroup *cgrp, u64 period_us, u64 quota_us, u64 burst_us) {}
 static void sched_ext_ops__cgroup_set_idle(struct cgroup *cgrp, bool idle) {}
+static void sched_ext_ops__cgroup_set_runtime(struct cgroup *cgrp, u64 runtime_us) {}
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 static s32 sched_ext_ops__sub_attach(struct scx_sub_attach_args *args) { return -EINVAL; }
 static void sched_ext_ops__sub_detach(struct scx_sub_detach_args *args) {}
@@ -9434,6 +9449,7 @@ static struct sched_ext_ops __bpf_ops_sched_ext_ops = {
 	.cgroup_set_weight	= sched_ext_ops__cgroup_set_weight,
 	.cgroup_set_bandwidth	= sched_ext_ops__cgroup_set_bandwidth,
 	.cgroup_set_idle	= sched_ext_ops__cgroup_set_idle,
+	.cgroup_set_runtime	= sched_ext_ops__cgroup_set_runtime,
 #endif
 	.sub_attach		= sched_ext_ops__sub_attach,
 	.sub_detach		= sched_ext_ops__sub_detach,
@@ -9502,6 +9518,7 @@ static struct sched_ext_ops_cid __bpf_ops_sched_ext_ops_cid = {
 	.cpuctl_set_weight	= sched_ext_ops__cgroup_set_weight,
 	.cpuctl_set_bandwidth	= sched_ext_ops__cgroup_set_bandwidth,
 	.cpuctl_set_idle	= sched_ext_ops__cgroup_set_idle,
+	.cpuctl_set_runtime	= sched_ext_ops__cgroup_set_runtime,
 #endif
 	.sub_attach		= sched_ext_ops__sub_attach,
 	.sub_detach		= sched_ext_ops__sub_detach,
@@ -12226,6 +12243,7 @@ static const u32 scx_kf_allow_flags[] = {
 	[SCX_OP_IDX(cgroup_set_weight)]	= SCX_KF_ALLOW_UNLOCKED,
 	[SCX_OP_IDX(cgroup_set_bandwidth)] = SCX_KF_ALLOW_UNLOCKED,
 	[SCX_OP_IDX(cgroup_set_idle)]	= SCX_KF_ALLOW_UNLOCKED,
+	[SCX_OP_IDX(cgroup_set_runtime)] = SCX_KF_ALLOW_UNLOCKED,
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 	[SCX_OP_IDX(sub_attach)]	= SCX_KF_ALLOW_UNLOCKED,
 	[SCX_OP_IDX(sub_detach)]	= SCX_KF_ALLOW_UNLOCKED,
@@ -12395,6 +12413,7 @@ static int __init scx_init(void)
 	CID_OFFSET_MATCH(cgroup_set_weight, cpuctl_set_weight);
 	CID_OFFSET_MATCH(cgroup_set_bandwidth, cpuctl_set_bandwidth);
 	CID_OFFSET_MATCH(cgroup_set_idle, cpuctl_set_idle);
+	CID_OFFSET_MATCH(cgroup_set_runtime, cpuctl_set_runtime);
 #endif
 	/* @priv tail must align since both share the same data block */
 	CID_OFFSET_MATCH(priv, priv);
kernel/sched/ext/ext.h +2 -0
index d32bc01071b01..6d1ce582bf724 100644
@@ -94,6 +94,7 @@ void scx_cgroup_cancel_attach(struct cgroup_taskset *tset);
 void scx_group_set_weight(struct task_group *tg, unsigned long cgrp_weight);
 void scx_group_set_idle(struct task_group *tg, bool idle);
 void scx_group_set_bandwidth(struct task_group *tg, u64 period_us, u64 quota_us, u64 burst_us);
+void scx_group_set_runtime(struct task_group *tg, u64 runtime_us);
 #else	/* CONFIG_EXT_GROUP_SCHED */
 static inline void scx_tg_init(struct task_group *tg) {}
 static inline int scx_tg_online(struct task_group *tg) { return 0; }
@@ -104,5 +105,6 @@ static inline void scx_cgroup_cancel_attach(struct cgroup_taskset *tset) {}
 static inline void scx_group_set_weight(struct task_group *tg, unsigned long cgrp_weight) {}
 static inline void scx_group_set_idle(struct task_group *tg, bool idle) {}
 static inline void scx_group_set_bandwidth(struct task_group *tg, u64 period_us, u64 quota_us, u64 burst_us) {}
+static inline void scx_group_set_runtime(struct task_group *tg, u64 runtime_us) {}
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 #endif	/* CONFIG_CGROUP_SCHED */
kernel/sched/ext/internal.h +21 -0
index ff0a3d508533f..e794507596d6f 100644
@@ -863,6 +863,26 @@ struct sched_ext_ops {
 	 */
 	void (*cgroup_set_idle)(struct cgroup *cgrp, bool idle);
 
+	/**
+	 * @cgroup_set_runtime: A cgroup's runtime is being set
+	 * @cgrp: cgroup whose runtime is being set
+	 * @runtime_us: runtime in microseconds
+	 *
+	 * Set the runtime @cgrp has accumulated and not used yet to
+	 * @runtime_us. This is from the cpu.max.runtime cgroup interface,
+	 * which exists with CONFIG_CFS_BANDWIDTH only. It is called on every
+	 * write, also of an unchanged value. The runtime goes down as @cgrp
+	 * runs, so a write of the same value is a top-up.
+	 *
+	 * @cgrp has a quota, and @runtime_us is at most the quota + burst the
+	 * scheduler was last given, by @cgroup_init or @cgroup_set_bandwidth:
+	 * cpu.max and cpu.max.runtime writes reach the scheduler in the order
+	 * the fair class applied them.
+	 *
+	 * Delivery follows the same rule as cgroup_set_weight().
+	 */
+	void (*cgroup_set_runtime)(struct cgroup *cgrp, u64 runtime_us);
+
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 
 	/**
@@ -1239,6 +1259,7 @@ struct sched_ext_ops_cid {
 	void (*cpuctl_set_bandwidth)(struct cgroup *cgrp, u64 period_us, u64 quota_us,
 				     u64 burst_us);
 	void (*cpuctl_set_idle)(struct cgroup *cgrp, bool idle);
+	void (*cpuctl_set_runtime)(struct cgroup *cgrp, u64 runtime_us);
 #endif	/* CONFIG_EXT_GROUP_SCHED */
 	s32 (*sub_attach)(struct scx_sub_attach_args *args);
 	void (*sub_detach)(struct scx_sub_detach_args *args);
Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#7 lore raw

Re: [PATCH v2 3/4] sched_ext: add cgroup_set_runtime ops callback

TL;DR: The documented guarantee that cgroup_set_runtime never exceeds the quota the scheduler was last given may not hold for schedulers lacking cgroup_init or cgroup_set_bandwidth; comment and…
Quoted 1 line

sched_ext: add cgroup_set_runtime ops callback

TL;DR: The documented guarantee that cgroup_set_runtime never exceeds the quota the scheduler was last given may not hold for schedulers lacking cgroup_init or cgroup_set_bandwidth; comment and commit message may need qualifying, or validation tightened.

Quoted 3 lines
kernel/sched/ext/internal.h +0 -0

[ ... ]

Quoted 8 lines
kernel/sched/ext/internal.h +4 -0
@@ -863,6 +863,26 @@ struct sched_ext_ops {
 	 */
 	void (*cgroup_set_idle)(struct cgroup *cgrp, bool idle);
 
+	/**
+	 * @cgroup_set_runtime: A cgroup's runtime is being set
+	 * @cgrp: cgroup whose runtime is being set
+	 * @runtime_us: runtime in microseconds

[ ... ]

kernel/sched/ext/internal.h struct sched_ext_ops { · line 884

Quoted 8 lines
kernel/sched/ext/internal.h +8 -0
+	 * @cgrp has a quota, and @runtime_us is at most the quota + burst the
+	 * scheduler was last given, by @cgroup_init or @cgroup_set_bandwidth:
+	 * cpu.max and cpu.max.runtime writes reach the scheduler in the order
+	 * the fair class applied them.
+	 *
+	 * Delivery follows the same rule as cgroup_set_weight().
+	 */
+	void (*cgroup_set_runtime)(struct cgroup *cgrp, u64 runtime_us);

Does this guarantee hold for a scheduler that implements cgroup_set_runtime but not cgroup_init or cgroup_set_bandwidth? The commit message makes the same claim: "The runtime passed is then at most the quota + burst the scheduler was last given".

scx_group_set_bandwidth() still updates tg->scx.bw_* when the op is missing:

    if (scx_cgroup_enabled && sch && SCX_HAS_OP(sch, cgroup_set_bandwidth) && ...)
            SCX_CALL_OP(sch, cgroup_set_bandwidth, ...);
    tg->scx.bw_period_us = period_us;
    tg->scx.bw_quota_us = quota_us;
    tg->scx.bw_burst_us = burst_us;

while scx_group_set_runtime() calls the op whenever SCX_HAS_OP(sch, cgroup_set_runtime) is true, and the fair class only checks the runtime against its own cfs_b->quota + cfs_b->burst in cfs_bandwidth_set_runtime().

For example, take a scheduler with cgroup_init and cgroup_set_runtime but no cgroup_set_bandwidth. Cgroup C has cpu.max "10000 100000" at load, so ops.cgroup_init() sees bw_quota_us=10000. Userspace then writes cpu.max "50000 100000", which the fair class applies but the scheduler is never told about. A following write of 40000 to cpu.max.runtime passes cfs_bandwidth_set_runtime() since 40000 <= 50000, and ops.cgroup_set_runtime(C, 40000) runs, four times the quota the scheduler was last given.

Similarly, a scheduler without cgroup_init that loads after C's quota was set is never given a quota, yet still receives cgroup_set_runtime for that cgroup.

Should the comment (and commit message) make the guarantee conditional on the scheduler implementing @cgroup_init and @cgroup_set_bandwidth, or should validation reject cgroup_set_runtime without cgroup_set_bandwidth? The later commit eb360e17b06e ("sched_ext: charge BPF scheduler tasks to cpu.max bandwidth") does not change this text.

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42982707042

#8 lore raw

PATCH v2 4/4 sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

In reply to Fernand Sieber

A BPF scheduler that enforces cpu.max keeps each cgroup's budget itself, and no runtime moves between it and the fair class. One loaded after a cpu.max.runtime write starts the cgroup on a fresh quota, losing the unspent grant.

Conversely, a read reports the fair class's runtime, which refills to quota + burst meanwhile. On disable, the fair class takes the tasks back with that full runtime, so a cgroup that used up its budget runs over its quota until the period accounting catches up.

Under SCX_OPS_SWITCH_PARTIAL, both budgets apply at once: a cgroup with tasks in both classes runs up to its quota in each, which no handover at load or unload can fix. Keep one budget, the fair class's, and let a BPF scheduler opt in to drawing from it with the new SCX_OPS_CHARGE_CPU_MAX flag.

With SCX_OPS_CHARGE_CPU_MAX, update_curr_scx() charges a task's runtime to its cgroups' cfs_rqs as a fair task's would, without throttling it. A task the scheduler stops late leaves its CPU in debt, which the next periods pay, also once the task has moved; debt past minus a slice moves to cfs_b, whose next refill pays up to a quota of it. The new kfunc scx_bpf_task_cpu_max_runtime() returns the runtime left less that debt, and the scheduler runs the task only while it is positive. Cgroups whose period timer is stopped are skipped, as only a charge restarts it.

Both classes then draw from one budget: under SCX_OPS_SWITCH_PARTIAL a cgroup gets its quota per period however its tasks are split, and nothing is handed over at load or unload. A task returning to the fair class leaves its CPU at most a slice in debt; schedulers without the flag behave as before.

cfs_b->lock is taken at most twice per millisecond of run time per CPU. The cgroup_runtime selftest covers these cases.

---
 Documentation/admin-guide/cgroup-v2.rst       |   4 +-
 kernel/sched/ext/ext.c                        |  34 +
 kernel/sched/ext/internal.h                   |  24 +-
 kernel/sched/fair.c                           | 122 ++++
 kernel/sched/sched.h                          |  12 +
 tools/sched_ext/include/scx/common.bpf.h      |   1 +
 tools/sched_ext/include/scx/compat.h          |   1 +
 tools/testing/selftests/sched_ext/Makefile    |   1 +
 .../selftests/sched_ext/cgroup_runtime.bpf.c  | 195 ++++++
 .../selftests/sched_ext/cgroup_runtime.c      | 652 ++++++++++++++++++
 tools/testing/selftests/sched_ext/config      |   1 +
 11 files changed, 1045 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c
 create mode 100644 tools/testing/selftests/sched_ext/cgroup_runtime.c
Documentation/admin-guide/cgroup-v2.rst +3 -1
index b77ca6f8ca548..db9fa6e6dbd49 100644
@@ -1258,7 +1258,9 @@ will be referred to. All time durations are in microseconds.
 
 	This file affects only processes under the fair-class scheduler and a BPF
 	scheduler with the ``cgroup_set_runtime`` callback depending on what
-	the callback actually does.
+	the callback actually does. A BPF scheduler with the
+	``SCX_OPS_CHARGE_CPU_MAX`` flag draws what its processes run from
+	the same runtime, so the cgroup's processes in both share it.
 
   cpu.pressure
 	A read-write nested-keyed file.
kernel/sched/ext/ext.c +34 -0
index 265a80dd93282..6ffc9ca2c5c9f 100644
@@ -1625,6 +1625,11 @@ static void expire_task_slice(struct task_struct *p)
 		scx_set_task_slice(p, 0);
 }
 
+static bool scx_task_charges_cpu_max(const struct task_struct *p)
+{
+	return scx_task_sched(p)->ops.flags & SCX_OPS_CHARGE_CPU_MAX;
+}
+
 static void update_curr_scx(struct rq *rq)
 {
 	struct task_struct *donor;
@@ -1650,6 +1655,9 @@ static void update_curr_scx(struct rq *rq)
 	if (unlikely(donor == scx_rescuee(rq)))
 		scx_rescue_charge(rq, delta_exec);
 
+	if (scx_task_charges_cpu_max(donor))
+		cfs_bandwidth_charge_task(rq, donor, delta_exec);
+
 	dl_server_update(&rq->ext_server, delta_exec);
 }
 
@@ -2605,6 +2613,13 @@ static bool dequeue_task_scx(struct rq *rq, struct task_struct *p, int core_deq_
 
 	ops_dequeue(rq, p, deq_flags);
 
+	/* charge what @p ran, then return what @rq holds for its cgroups */
+	if (scx_task_charges_cpu_max(p)) {
+		if (task_current_donor(rq, p))
+			update_curr_scx(rq);
+		cfs_bandwidth_return_task(rq, p);
+	}
+
 	/*
 	 * A current scheduling context which is going off @rq first gets
 	 * dequeued and then stops running. As we want running <-> stopping
@@ -11793,6 +11808,24 @@ __bpf_kfunc s32 scx_bpf_task_cpu(const struct task_struct *p)
 	return task_cpu(p);
 }
 
+/**
+ * scx_bpf_task_cpu_max_runtime - Runtime a task has left under cpu.max
+ * @p: task of interest
+ *
+ * Return the runtime in nsecs that @p's cgroup and its ancestors have left
+ * under their cpu.max quotas for @p on its current CPU, the least over those
+ * with a quota: the cgroup's cpu.max.runtime plus what the CPU holds for
+ * the cgroup, less what it owes. U64_MAX if none limits it. Read without
+ * locks.
+ *
+ * With %SCX_OPS_CHARGE_CPU_MAX, the scheduler should run @p only while this
+ * returns more than 0. The runtime grows when a period refills it.
+ */
+__bpf_kfunc u64 scx_bpf_task_cpu_max_runtime(struct task_struct *p)
+{
+	return cfs_bandwidth_task_runtime(p);
+}
+
 /**
  * scx_bpf_task_cid - cid a task is currently associated with
  * @p: task of interest
@@ -12119,6 +12152,7 @@ BTF_ID_FLAGS(func, scx_bpf_get_online_cpumask, KF_ACQUIRE)
 BTF_ID_FLAGS(func, scx_bpf_put_cpumask, KF_RELEASE)
 BTF_ID_FLAGS(func, scx_bpf_task_running, KF_RCU)
 BTF_ID_FLAGS(func, scx_bpf_task_cpu, KF_RCU)
+BTF_ID_FLAGS(func, scx_bpf_task_cpu_max_runtime, KF_RCU)
 BTF_ID_FLAGS(func, scx_bpf_task_cid, KF_RCU)
 BTF_ID_FLAGS(func, scx_bpf_locked_rq, KF_IMPLICIT_ARGS | KF_RET_NULL)
 BTF_ID_FLAGS(func, scx_bpf_cpu_curr, KF_IMPLICIT_ARGS | KF_RET_NULL | KF_RCU_PROTECTED)
kernel/sched/ext/internal.h +23 -1
index e794507596d6f..e8c0b573d0a43 100644
@@ -243,6 +243,27 @@ enum scx_ops_flags {
 	 */
 	SCX_OPS_ENQ_BLOCKED		= 1LLU << 10,
 
+	/*
+	 * If set, what the scheduler's tasks run is charged to the cpu.max
+	 * bandwidth of their cgroups, the runtime the fair class keeps and
+	 * cpu.max.runtime reads, as what fair tasks run is. The tasks of a
+	 * cgroup in both classes then share one quota, also with
+	 * %SCX_OPS_SWITCH_PARTIAL, and nothing is handed over when tasks
+	 * switch class.
+	 *
+	 * The kernel never throttles the scheduler's tasks. The scheduler
+	 * enforces the quota by running a task only while
+	 * scx_bpf_task_cpu_max_runtime() returns more than 0. What a task runs
+	 * beyond that is charged: up to a slice (sched_cfs_bandwidth_slice_us)
+	 * per CPU as a debt the cgroup's fair tasks on that CPU pay back, and up
+	 * to a quota more from the cgroup's next period. The scheduler does not
+	 * need ops.cgroup_set_runtime(), as a write of cpu.max.runtime sets that
+	 * runtime.
+	 *
+	 * Without CONFIG_CFS_BANDWIDTH, this flag has no effect.
+	 */
+	SCX_OPS_CHARGE_CPU_MAX		= 1LLU << 11,
+
 	SCX_OPS_ALL_FLAGS		= SCX_OPS_KEEP_BUILTIN_IDLE |
 					  SCX_OPS_ENQ_LAST |
 					  SCX_OPS_ENQ_EXITING |
@@ -253,7 +274,8 @@ enum scx_ops_flags {
 					  SCX_OPS_ALWAYS_ENQ_IMMED |
 					  SCX_OPS_TID_TO_TASK |
 					  SCX_OPS_LAZY_RESCHED |
-					  SCX_OPS_ENQ_BLOCKED,
+					  SCX_OPS_ENQ_BLOCKED |
+					  SCX_OPS_CHARGE_CPU_MAX,
 
 	/* high 8 bits are internal, don't include in SCX_OPS_ALL_FLAGS */
 	__SCX_OPS_INTERNAL_MASK		= 0xffLLU << 56,
kernel/sched/fair.c +122 -0

134 lines not on this page · raw message

kernel/sched/sched.h +12 -0
index 676e6371d0265..fd98000ffd744 100644
@@ -456,6 +456,7 @@ struct cfs_bandwidth {
 	u64			runtime;
 	u64			burst;
 	u64			runtime_snap;
+	u64			debt;
 	s64			hierarchical_quota;
 
 	u8			idle;
@@ -621,6 +622,17 @@ static inline bool cfs_task_bw_constrained(struct task_struct *p) { return false
 
 #endif /* !CONFIG_CGROUP_SCHED */
 
+#ifdef CONFIG_CFS_BANDWIDTH
+void cfs_bandwidth_charge_task(struct rq *rq, struct task_struct *p, u64 delta_exec);
+void cfs_bandwidth_return_task(struct rq *rq, struct task_struct *p);
+u64 cfs_bandwidth_task_runtime(struct task_struct *p);
+#else
+static inline void cfs_bandwidth_charge_task(struct rq *rq, struct task_struct *p,
+					     u64 delta_exec) { }
+static inline void cfs_bandwidth_return_task(struct rq *rq, struct task_struct *p) { }
+static inline u64 cfs_bandwidth_task_runtime(struct task_struct *p) { return RUNTIME_INF; }
+#endif
+
 /*
  * A weight of 0 or 1 can cause arithmetics problems.
  * A weight of a cfs_rq is the sum of weights of which entities
tools/sched_ext/include/scx/common.bpf.h +1 -0
index 6194484d4d9a9..cfb7f1b70665f 100644
@@ -98,6 +98,7 @@ s32 scx_bpf_pick_any_cpu_node(const cpumask_t *cpus_allowed, int node, u64 flags
 s32 scx_bpf_pick_any_cpu(const cpumask_t *cpus_allowed, u64 flags) __ksym;
 bool scx_bpf_task_running(const struct task_struct *p) __ksym;
 s32 scx_bpf_task_cpu(const struct task_struct *p) __ksym;
+u64 scx_bpf_task_cpu_max_runtime(struct task_struct *p) __ksym __weak;
 struct rq *scx_bpf_cpu_rq(s32 cpu) __ksym __weak;
 struct rq *scx_bpf_locked_rq(void) __ksym;
 struct task_struct *scx_bpf_cpu_curr(s32 cpu) __ksym __weak;
tools/sched_ext/include/scx/compat.h +1 -0
index 07fb55e63c57c..60b9972f1a5dc 100644
@@ -215,6 +215,7 @@ static inline bool __COMPAT_struct_has_field(const char *type, const char *field
 #define SCX_OPS_BUILTIN_IDLE_PER_NODE SCX_OPS_FLAG(SCX_OPS_BUILTIN_IDLE_PER_NODE)
 #define SCX_OPS_ALWAYS_ENQ_IMMED SCX_OPS_FLAG(SCX_OPS_ALWAYS_ENQ_IMMED)
 #define SCX_OPS_ENQ_BLOCKED SCX_OPS_FLAG(SCX_OPS_ENQ_BLOCKED)
+#define SCX_OPS_CHARGE_CPU_MAX SCX_OPS_FLAG(SCX_OPS_CHARGE_CPU_MAX)
 
 #define SCX_PICK_IDLE_FLAG(name) __COMPAT_ENUM_OR_ZERO("scx_pick_idle_cpu_flags", #name)
 
tools/testing/selftests/sched_ext/Makefile +1 -0
index c5d3a2eaea7da..4a727b8dc7aab 100644
@@ -165,6 +165,7 @@ all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubs
 
 auto-test-targets :=			\
 	cgroup_nr_cpus			\
+	cgroup_runtime			\
 	create_dsq			\
 	dequeue				\
 	dequeue_iter			\
tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c new file +195 -0

195 lines not on this page · raw message

tools/testing/selftests/sched_ext/cgroup_runtime.c new file +652 -0

652 lines not on this page · raw message

tools/testing/selftests/sched_ext/config +1 -0
index 8173f9170ffe8..495b140403a18 100644
@@ -9,3 +9,4 @@ CONFIG_DEBUG_INFO=y
 CONFIG_DEBUG_INFO_BTF=y
 CONFIG_EXPERT=y
 CONFIG_SCHED_PROXY_EXEC=y
+CONFIG_CFS_BANDWIDTH=y
Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#9 lore raw

Re: [PATCH v2 4/4] sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

Thank you for your contribution! Sashiko AI review found 8 potential issues to consider: - [High] sched/fair: lost refills and wrong runtime in cfs_bandwidth_task_runtime() - [High] sched/fair…

Thank you for your contribution! Sashiko AI review found 8 potential issues to consider:
- [High] sched/fair: lost refills and wrong runtime in cfs_bandwidth_task_runtime()
- [High] sched/fair: cfs_bandwidth_task_runtime() returns RUNTIME_INF on zero runtime
- [Medium] sched/fair: cfs_bandwidth_return_task() strands unpaid debt on old CPU
- [Medium] sched/fair: stale cfs_b->debt persists across cpu.max reconfiguration
- [Medium] sched_ext: scx_can_stop_tick() stops tick for cpu.max constrained tasks
- [Medium] sched/fair: cfs_bandwidth_charge_task() consumes 1ns async unthrottle grant
- [Medium] selftests/sched_ext: cgroup and memory leaks in setup_ctx() and setup_debt()
- [Low] selftests/sched_ext: head-only peek in cgroup_runtime_dispatch() --

commit 0c5e278d21c97ca5fabb3de9616fe36039f5635b
Author: Fernand Sieber <[email protected]>

sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

This commit adds the SCX_OPS_CHARGE_CPU_MAX flag and the scx_bpf_task_cpu_max_runtime() kfunc so BPF schedulers can charge task execution against a cgroup's CFS cpu.max bandwidth pool and share a single quota with the fair class.

Quoted 17 lines
kernel/sched/ext/ext.c +5 -0
index 265a80dd93282..6ffc9ca2c5c9f 100644
@@ -1625,6 +1625,11 @@ static void expire_task_slice(struct task_struct *p)
 		scx_set_task_slice(p, 0);
 }
 
+static bool scx_task_charges_cpu_max(const struct task_struct *p)
+{
+	return scx_task_sched(p)->ops.flags & SCX_OPS_CHARGE_CPU_MAX;
+}
+
 static void update_curr_scx(struct rq *rq)
 {
 	struct task_struct *donor;

kernel/sched/ext/ext.c static void update_curr_scx(struct rq *rq) · line 1662

Quoted 8 lines
kernel/sched/ext/ext.c +3 -0
@@ -1650,6 +1655,9 @@ static void update_curr_scx(struct rq *rq)
 	if (unlikely(donor == scx_rescuee(rq)))
 		scx_rescue_charge(rq, delta_exec);
 
+	if (scx_task_charges_cpu_max(donor))
+		cfs_bandwidth_charge_task(rq, donor, delta_exec);
+
 	dl_server_update(&rq->ext_server, delta_exec);
 }

[Severity: Medium]
Can scx_can_stop_tick() still stop the scheduler tick on a CONFIG_NO_HZ_FULL CPU when a bandwidth-constrained task runs with SCX_SLICE_INF?

set_next_task_scx() sets SCX_RQ_CAN_STOP_TICK whenever p->scx.slice == SCX_SLICE_INF && !p->is_blocked, and __need_bw_check() in kernel/sched/core.c only checks fair_sched_class tasks. If a BPF scheduler with SCX_OPS_CHARGE_CPU_MAX dispatches a task with SCX_SLICE_INF on a nohz_full CPU, stopping the tick stops update_curr_scx() and ops.tick() from running while the task stays in userspace, so cpu.max is neither charged nor enforced.

[ ... ]

Quoted 5 lines
kernel/sched/fair.c +0 -0
index bc068b3fa4ed1..9ff239f122bda 100644

kernel/sched/fair.c void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b) · line 6884

Quoted 10 lines
kernel/sched/fair.c +5 -0
@@ -6875,6 +6875,11 @@ void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b)
 	}
 
 	cfs_b->runtime = min(cfs_b->runtime, cfs_b->quota + cfs_b->burst);
+
+	/* pay the debt cfs_bandwidth_charge_task() left */
+	runtime = min(cfs_b->runtime, cfs_b->debt);
+	cfs_b->runtime -= runtime;
+	cfs_b->debt -= runtime;
 	cfs_b->runtime_snap = cfs_b->runtime;
 }

[Severity: Medium]
Does cfs_b->debt stay stale across cpu.max and cpu.max.runtime writes?

In tg_set_cfs_bandwidth(), setting cpu.max to "max" sets cfs_b->quota to RUNTIME_INF and resets each CPU's cfs_rq->runtime_remaining to 1, but __refill_cfs_bandwidth_runtime() returns early on RUNTIME_INF without clearing cfs_b->debt. Lowering cfs_b->quota (for example from 100ms to 10ms) also leaves prior cfs_b->debt up to the old quota, starving the group for multiple periods under the new quota. Similarly, cfs_bandwidth_set_runtime() overwrites cfs_b->runtime without clearing cfs_b->debt, and __return_cfs_rq_runtime() adds returned slack to cfs_b->runtime without paying down cfs_b->debt.

Quoted 36 lines
kernel/sched/fair.c +32 -0
@@ -8076,6 +8081,123 @@ bool cfs_task_bw_constrained(struct task_struct *p)
 	return false;
 }
 
+/*
+ * Charge @delta_exec, which @p ran on @rq outside the fair class, to the
+ * bandwidth of its task group and of each ancestor with a quota, as
+ * account_cfs_rq_runtime() charges a fair task. A cfs_rq that runs out takes
+ * another slice from cfs_b, but @p is never throttled: the BPF scheduler that
+ * runs it stops it, and the group's fair tasks are throttled when they run.
+ *
+ * Debt below -slice that cfs_b cannot cover moves to cfs_b, up to a quota,
+ * and the next refill pays it, so that a scheduler which keeps running @p
+ * cannot put the group's fair tasks on @rq in more debt than a slice.
+ */
+void cfs_bandwidth_charge_task(struct rq *rq, struct task_struct *p,
+			       u64 delta_exec)
+{
+	s64 floor = -(s64)sched_cfs_bandwidth_slice();
+	struct task_group *tg;
+
+	lockdep_assert_rq_held(rq);
+
+	if (!cfs_bandwidth_used())
+		return;
+
+	for (tg = task_group(p); tg; tg = tg->parent) {
+		struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(tg);
+		struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));
+
+		if (!cfs_rq->runtime_enabled)
+			continue;
+
+		cfs_rq->runtime_remaining -= delta_exec;
+
+		/*

kernel/sched/fair.c bool cfs_task_bw_constrained(struct task_struct *p) · line 8125

Quoted 10 lines
kernel/sched/fair.c +10 -0
+		 * A throttled cfs_rq gets runtime from distribute_cfs_runtime().
+		 * Without runtime, an active period timer refills it, so skip
+		 * the lock. A stopped one is restarted by the assign.
+		 */
+		if (cfs_rq->runtime_remaining <= 0 && !cfs_rq->throttled &&
+		    (READ_ONCE(cfs_b->runtime) || !READ_ONCE(cfs_b->period_active))) {
+			scoped_guard (raw_spinlock, &cfs_b->lock)
+				__assign_cfs_rq_runtime(cfs_b, cfs_rq,
+							sched_cfs_bandwidth_slice());
+		}

[Severity: Medium]
Can cfs_bandwidth_charge_task() consume the 1ns runtime grant from distribute_cfs_runtime() before the async unthrottle CSD runs, aborting the unthrottle?

CPU0 CPU1
---- ----
/* cfs_rq->throttled == 1,
* SCX task running on CPU0
*/

                                        do_sched_cfs_period_timer()
                                          distribute_cfs_runtime()
                                            rq_lock(CPU0)
                                            cfs_rq->runtime_remaining = 1;
                                            unthrottle_cfs_rq_async(cfs_rq)
                                            rq_unlock(CPU0)
update_curr_scx()
  cfs_bandwidth_charge_task()
    cfs_rq->runtime_remaining -= delta;
    /* <= 0, but cfs_rq->throttled == 1
     * so __assign_cfs_rq_runtime() is
     * skipped
     */

__cfsb_csd_unthrottle()
unthrottle_cfs_rq()
if (cfs_rq->runtime_remaining <= 0)
return; <-- aborts unthrottle

Because cfs_rq->h_curr is NULL while an SCX task is running on CPU0, distribute_cfs_runtime() does not call update_curr(cfs_rq) before setting cfs_rq->runtime_remaining = 1. If update_curr_scx() runs on CPU0 before __cfsb_csd_unthrottle(), cfs_rq->runtime_remaining drops back to <= 0 while cfs_rq->throttled is still 1, so unthrottle_cfs_rq() bails out and leaves the group's fair tasks on CPU0 throttled for the rest of the period while subsequent SCX charges also skip __assign_cfs_rq_runtime().

Quoted 27 lines
kernel/sched/fair.c +27 -0
+
+		if (cfs_rq->runtime_remaining < floor) {
+			scoped_guard (raw_spinlock, &cfs_b->lock)
+				cfs_b->debt = min_t(u64, cfs_b->debt + floor -
+						    cfs_rq->runtime_remaining, cfs_b->quota);
+			cfs_rq->runtime_remaining = floor;
+		}
+	}
+}
+
+/*
+ * @p leaves @rq: give the runtime @rq holds for its groups back, less
+ * min_cfs_rq_runtime, as a fair dequeue does when a cfs_rq empties. Pay
+ * what @rq owes from cfs_b, as @p may not run on @rq again.
+ */
+void cfs_bandwidth_return_task(struct rq *rq, struct task_struct *p)
+{
+	struct task_group *tg;
+
+	lockdep_assert_rq_held(rq);
+
+	if (!cfs_bandwidth_used())
+		return;
+
+	for (tg = task_group(p); tg; tg = tg->parent) {
+		struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(tg);
+		struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu_of(rq));

kernel/sched/fair.c bool cfs_task_bw_constrained(struct task_struct *p) · line 8162

Quoted 10 lines
kernel/sched/fair.c +10 -0
+
+		return_cfs_rq_runtime(cfs_rq);
+
+		if (cfs_rq->runtime_enabled && cfs_rq->runtime_remaining < 0 &&
+		    !cfs_rq->throttled && READ_ONCE(cfs_b->runtime)) {
+			scoped_guard (raw_spinlock, &cfs_b->lock)
+				__assign_cfs_rq_runtime(cfs_b, cfs_rq, 0);
+		}
+	}
+}

[Severity: Medium]
When a task overruns on rq and is dequeued while cfs_b->runtime is 0, does cfs_bandwidth_return_task() leave the negative cfs_rq->runtime_remaining stranded on the old CPU?

cfs_bandwidth_charge_task() already pulls from cfs_b->runtime whenever it is non-zero, so cfs_rq->runtime_remaining is negative here primarily when cfs_b->runtime has been exhausted to 0. If p then migrates, sleeps, or switches cgroups before the next period refill, cfs_bandwidth_return_task() does nothing because cfs_b->runtime is 0 and never transfers the unpaid cfs_rq->runtime_remaining to cfs_b->debt. The task escapes up to a slice of debt on each CPU it moves across, while the old CPU's cfs_rq keeps the debt and later refills of cfs_b only pay cfs_b->debt.

Quoted 18 lines
kernel/sched/fair.c +18 -0
+
+/*
+ * The runtime @p's groups have left for it on its CPU, without locks: the
+ * least, over the groups with a quota, of what the CPU holds or owes and
+ * cfs_b's runtime. Debt counts up to a quota less 1ns, so that a full period
+ * lets @p run and pay the rest. A group whose period timer is stopped does
+ * not count, as the next charge restarts the timer and refills what it owes.
+ * RUNTIME_INF if no group counts.
+ */
+u64 cfs_bandwidth_task_runtime(struct task_struct *p)
+{
+	u64 runtime = RUNTIME_INF;
+	struct task_group *tg;
+	int cpu = task_cpu(p);
+
+	if (!cfs_bandwidth_used())
+		return runtime;
+

kernel/sched/fair.c bool cfs_task_bw_constrained(struct task_struct *p) · line 8190

Quoted 10 lines
kernel/sched/fair.c +10 -0
+	guard(rcu)();
+
+	for (tg = task_group(p); tg; tg = tg->parent) {
+		struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(tg);
+		struct cfs_rq *cfs_rq = tg_cfs_rq(tg, cpu);
+		s64 held;
+
+		if (!READ_ONCE(cfs_rq->runtime_enabled) ||
+		    !READ_ONCE(cfs_b->period_active))
+			continue;

[Severity: High]
Can skipping groups with !cfs_b->period_active make cfs_bandwidth_task_runtime() return RUNTIME_INF when cfs_b->runtime is 0 and no idle period refills are owed?

When only SCX tasks run, cfs_rqs are never throttled, so do_sched_cfs_period_timer() sets cfs_b->idle = 1 at every period boundary. If __refill_cfs_bandwidth_runtime() consumes the refill paying cfs_b->debt (leaving cfs_b->runtime == 0), neither holding tasks in BPF while cfs_b->runtime is 0 nor charging further into cfs_b->debt in cfs_bandwidth_charge_task() clears cfs_b->idle. At the next period boundary, do_sched_cfs_period_timer() sees cfs_b->idle && !throttled and deactivates the period timer (cfs_b->period_active = 0) with period_timer already forwarded to the end of the new period. cfs_bandwidth_task_runtime() then sees !cfs_b->period_active and returns RUNTIME_INF even though cfs_b->runtime is 0 and start_cfs_bandwidth() on the next charge will find 0 elapsed idle periods to refill.

Writing 0 to cpu.max.runtime while cfs_b->period_active is 0 hits the same
state: cfs_bandwidth_set_runtime() forwards period_timer via cfs_bandwidth_refill_idle_periods() and sets cfs_b->runtime = 0 without starting period_timer, so scx_bpf_task_cpu_max_runtime() returns U64_MAX instead of 0.

kernel/sched/fair.c bool cfs_task_bw_constrained(struct task_struct *p) · line 8199

Quoted 9 lines
kernel/sched/fair.c +9 -0
+
+		held = max_t(s64, READ_ONCE(cfs_rq->runtime_remaining),
+			     1 - (s64)READ_ONCE(cfs_b->quota));
+		held += READ_ONCE(cfs_b->runtime);
+		runtime = min_t(u64, runtime, max_t(s64, held, 0));
+	}
+
+	return runtime;
+}

[Severity: High]
Does splitting overrun debt between cfs_rq->runtime_remaining and cfs_b->debt cause cfs_bandwidth_task_runtime() to return 0 across a refill, discarding the unspent refill on the next period?

When a task overruns past -slice and waits in a BPF DSQ on the same CPU, cfs_rq->runtime_remaining stays at -slice while the rest of the overrun goes into cfs_b->debt. At the next period refill, __refill_cfs_bandwidth_runtime() adds cfs_b->quota to cfs_b->runtime and immediately subtracts cfs_b->debt, leaving cfs_b->runtime = quota - debt while cfs_rq->runtime_remaining is still -slice. Whenever
quota - debt <= min(slice, quota - 1) (for example, any non-zerocfs_b->debt when quota <= slice), or whenever another CPU consumes any runtime from cfs_b->runtime while this CPU owes -quota, held is <= 0 and cfs_bandwidth_task_runtime() returns 0 for the whole period. Because the BPF scheduler does not run the task, __assign_cfs_rq_runtime() is never called to apply cfs_b->runtime against cfs_rq->runtime_remaining, and the next period refill caps cfs_b->runtime at quota + burst, losing the previous refill while cfs_rq->runtime_remaining remains at -slice.

Conversely, when cfs_b->quota < sched_cfs_bandwidth_slice() and cfs_b->debt == 0 (for example quota = 1ms, slice = 5ms, and cfs_rq->runtime_remaining = -4ms), clamping cfs_rq->runtime_remaining at 1 - quota makes held = 1ns after a single 1ms refill, letting the task run every period despite owing multiple periods of debt on cfs_rq. Finally, held does not subtract READ_ONCE(cfs_b->debt) when cfs_b->runtime is populated before a refill (e.g. via __return_cfs_rq_runtime() or cfs_bandwidth_set_runtime()).

[ ... ]

Quoted 6 lines
tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c new file +0 -0
new file mode 100644
index 0000000000000..ec98a25d19c29
@@ -0,0 +1,195 @@

[ ... ]

tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c line 82

Quoted 7 lines
tools/testing/selftests/sched_ext/cgroup_runtime.bpf.c new file +7 -0
+void BPF_STRUCT_OPS(cgroup_runtime_dispatch, s32 cpu, struct task_struct *prev)
+{
+	struct task_struct *p = __COMPAT_scx_bpf_dsq_peek(HELD_DSQ);
+
+	if (p && runnable(p))
+		scx_bpf_dsq_move_to_local(HELD_DSQ, 0);
+}

[Severity: Low]
Since the two burner tasks are pinned to different CPUs and share HELD_DSQ, can peeking only the head of HELD_DSQ check the wrong task's per-CPU runtime?

If the head task in HELD_DSQ is pinned to CPU 1 and has no runtime on CPU 1, cgroup_runtime_dispatch() on CPU 2 sees !runnable(p) and does not move the second burner pinned to CPU 2 even if CPU 2 has runtime. Conversely, if the head task on CPU 1 is runnable while the second task on CPU 2 is not, cgroup_runtime_dispatch() on CPU 2 calls scx_bpf_dsq_move_to_local(), which skips the CPU 1 task and dispatches the non-runnable CPU 2 task.

[ ... ]

Quoted 6 lines
tools/testing/selftests/sched_ext/cgroup_runtime.c new file +0 -0
new file mode 100644
index 0000000000000..86d50a63a6e73
@@ -0,0 +1,652 @@

[ ... ]

Quoted 24 lines
tools/testing/selftests/sched_ext/cgroup_runtime.c new file +24 -0
+	if (cg_create(ctx->parent)) {
+		SCX_ERR("Failed to create %s", ctx->parent);
+		return SCX_TEST_FAIL;
+	}
+	ctx->parent_created = true;
+	if (cg_write(ctx->parent, "cgroup.subtree_control", "+cpu")) {
+		SCX_ERR("Failed to enable cpu in %s", ctx->parent);
+		return SCX_TEST_FAIL;
+	}
+	if (cg_create(ctx->cg)) {
+		SCX_ERR("Failed to create %s", ctx->cg);
+		return SCX_TEST_FAIL;
+	}
+	ctx->cg_created = true;
+
+	if (access(cg_control(ctx->cg, "cpu.max.runtime"), F_OK))
+		return SCX_TEST_SKIP;
+
+	if (write_str(ctx->cg, "cpu.max", "%lld %lld", quota, period) ||
+	    write_str(ctx->cg, "cpu.max.burst", "%lld", burst) ||
+	    write_str(ctx->cg, "cpu.max.runtime", "%lld", runtime)) {
+		SCX_ERR("Failed to configure %s", ctx->cg);
+		return SCX_TEST_FAIL;
+	}

tools/testing/selftests/sched_ext/cgroup_runtime.c line 274

Quoted 10 lines
tools/testing/selftests/sched_ext/cgroup_runtime.c new file +10 -0
+
+	ctx->skel->bss->cgid = cgroup_id(ctx->cg);
+	if (!ctx->skel->bss->cgid) {
+		SCX_ERR("Failed to read the cgroup ID of %s", ctx->cg);
+		return SCX_TEST_FAIL;
+	}
+	if (libbpf_num_possible_cpus() < NR_BURNERS + 1)
+		return SCX_TEST_SKIP;
+	return SCX_TEST_PASS;
+}

[Severity: Medium]
Does returning SCX_TEST_SKIP or SCX_TEST_FAIL from setup_ctx() (and setup_debt()) after cg_create() leak the cgroups and cause later tests in the same runner to fail with EEXIST?

run_test() in tools/testing/selftests/sched_ext/runner.c only calls test->cleanup() when test->setup() returns SCX_TEST_PASS. Because all cgroup_runtime_* tests in the runner share getpid() in the cgroup path scx_cgroup_runtime_<pid>, any skip (such as having fewer than 3 CPUs, missing cpu.max.runtime, or failing to write sched_cfs_bandwidth_slice_us in setup_debt()) leaves ctx->parent and ctx->cg behind and makes subsequent tests fail cg_create(ctx->parent). Also, passing cg_control() directly to access() leaks the string allocated by cg_control().

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4
#10 lore raw

Re: [PATCH v2 4/4] sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

In reply to Fernand Sieber
TL;DR: Nit only, in the selftest: join_path(), cgroup_id() and controller_enabled() in cgroup_runtime.c duplicate helpers in cgroup_nr_cpus.c; consider moving them into shared util.c/util.h.
Quoted 1 line

sched_ext: charge BPF scheduler tasks to cpu.max bandwidth

TL;DR: Nit only, in the selftest: join_path(), cgroup_id() and controller_enabled() in cgroup_runtime.c duplicate helpers in cgroup_nr_cpus.c; consider moving them into shared util.c/util.h.

Quoted 4 lines
tools/testing/selftests/sched_ext/cgroup_runtime.c new file +0 -0
new file mode 100644

[ ... ]

tools/testing/selftests/sched_ext/cgroup_runtime.c line 72

Quoted 9 lines
tools/testing/selftests/sched_ext/cgroup_runtime.c new file +9 -0
+static int join_path(char *dst, size_t dst_size, const char *parent, const char *name)
+{
+	int ret;
+
+	ret = snprintf(dst, dst_size, "%s/%s", parent, name);
+	if (ret < 0 || (size_t)ret >= dst_size)
+		return -ENAMETOOLONG;
+	return 0;
+}

[ ... ]

this isn't a bug, but join_path(), cgroup_id() and controller_enabled() look identical to the static helpers of the same names in cgroup_nr_cpus.c.

Could they move into the shared util.c/util.h so both cgroup tests use one copy?

---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/42982707042

maintainer
#11 lore raw
In reply to Fernand Sieber

Hello, Fernand.

Quoted 6 lines · Fernand Sieber

This series adds cpu.max.runtime, a cgroup v2 file to read and set the runtime the CFS bandwidth controller has for a cgroup, lets cpu.max.burst exceed the quota, and makes an idle group keep earning quota up to quota + burst. Patch 3 passes the writes on to BPF schedulers, and patch 4 lets them charge their tasks to the same runtime.

I think this patchset is too maximalist. As described, the use case is specific to selling burstable CPU budget. Nobody else has asked for this. There may be other use cases which haven't been stated yet but the description doesn't surface any, so as it stands this is pretty niche.

On top of that, the series integrates into both fair and sched_ext and then makes the two work together so that the handoff is seamless. That's too much for something which doesn't have a strong use case or an established generic need. As with scx_bpf_cgroup_report_throttled(), patch 4 modifies fair's internal state from sched_ext, which I don't think is a good idea as the cgroup piping is split at the interface level:

https://lore.kernel.org/all/[email protected]/

Seamlessly switching tasks between the classes in partial mode has a lot of other integration problems beyond this one and at least I'm not aiming to achieve it. It'd cost a lot more than it'd be useful.

The sched_ext part can just be a feature of your own BPF scheduler, which you seem to be using already. There, the credit level is just state in your BPF maps which your userspace can read and set, so the maximalist version can wait until the need is proven. If you want a handover between your scheduler and fair, that can be done from userspace.

If a burstable budget is something useful to add to fair, can you do that first and separately? It should be able to stand on its own.

Thanks.

--
tejun

#12 lore raw

Hi Tejun,

Thank you for your prompt feedback.

Quoted 2 lines · Tejun Heo

If a burstable budget is something useful to add to fair, can you do that first and separately? It should be able to stand on its own.

That works for me as a starting point. I will resend just patches 1 and 2 as v3.

Quoted 9 lines

Seamlessly switching tasks between the classes in partial mode has a lot of other integration problems beyond this one
[...]
The sched_ext part can just be a feature of your own BPF scheduler, which you seem to be using already. There, the credit level is just state in your BPF maps which your userspace can read and set, so the maximalist version can wait until the need is proven. If you want a handover between your scheduler and fair, that can be done from userspace.

The minimal viable bar for us is to never lose track of CPU credits. With patches 1 and 2, the userspace handover you suggest covers the planned transitions: on roll out and roll back, userspace reads cpu.max.runtime before the switch and writes it back after. The one case it cannot cover is an unplanned exit, a BPF scheduler erroring out or killed by the watchdog and falling back to fair, where there is no window for userspace to hand the level back.

Thanks,
Fernand

Amazon Development Centre (South Africa) (Proprietary) Limited 29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa Registration Number: 2004 / 034463 / 07

#13 lore raw

v3 cover sched/fair: cpu.max.runtime and idle refill for a burstable budget

In reply to Fernand Sieber

This series makes the CFS bandwidth controller's bucket a burstable CPU budget that userspace can save and restore. It lets cpu.max.burst exceed the quota, so the bucket can hold more than one quota, and adds cpu.max.runtime to read and set what it holds (patch 1). It makes a group that is not running keep earning quota up to quota + burst (patch 2).

v2 had two further patches that passed the level on to a BPF scheduler. Tejun asked for the fair side first and on its own, so they are dropped here and will be proposed separately if this is accepted.

Use case
========

We run virtual machines whose CPU time is governed by CPU credits, as burstable instances are. An instance earns credits while it runs below its baseline, spends them above it, and holds at most a capped amount; with none left it is held to the baseline. The starting credits, the baseline and the cap are part of the instance type's guarantee and must hold whatever the host does to the instance.

Each instance's vCPU threads run in their own cgroup, and the controller already is that bucket of credits: the quota per period is the rate credits are earned at, quota + burst is the cap, and the runtime is the level. It does the per-CPU charging and the throttling in the scheduler, at a granularity userspace accounting cannot match. What is missing is a cap above one quota, a way to read and write the level, and earning while idle. An instance that idles for a minute with a 100ms period earns one quota instead of up to 600.

The level has to be saved and restored by the host's control software:
an instance starts with the credits its type grants, not with one quota; a host kernel live update (kexec) or a reboot creates the cgroups anew, so every level has to be read before and written back after, or every instance loses or gains credits; a live migration moves the level with the instance; and the control plane raises or lowers it when credits move between instances. A container checkpoint and restore has the same problem.

Ben pointed out that a cpu.max or cpu.max.burst write already adds a quota to the runtime, and that cpuacct gives the usage. That falls short. A write can only add, and only one quota, so setting a level means writing a temporary quota equal to the difference and then the real quota back, and lowering one means shrinking the burst until the cap cuts the runtime and widening it again; each leaves the cgroup running with the wrong bandwidth in between, and each write visits every CPU's cfs_rq. And usage is not the level: deriving it means replaying every period's refill in userspace, including the periods the timer slept through, which duplicates the controller's accounting.

Patch 2 changes behavior for existing cpu.max.burst users. A group with a burst that becomes runnable after idling has up to quota + burst, as if it had been running below its quota throughout, instead of the runtime it had when the timer stopped. Groups without a burst see no change.

Testing
=======

On tip/sched/core in qemu with lockdep, the cgroup test_cpu selftests pass, 18 of 18, including the nine new ones for these two patches, with no lockdep report in dmesg. Both patches build with W=1 and no warning in kernel/sched, with and without CONFIG_CFS_BANDWIDTH and on 32-bit, and kernel-doc reports nothing on the files they touch.

An AI coding assistant (Claude Code) helped write the selftests and changelogs and ran the build and test matrix; I reviewed all of it.

Changes in v3
=============

- Patches 3 and 4, the sched_ext side, dropped (Tejun).
- Patch 1: cpu.max.runtime reads 0 while cpu.max is "max" instead of a level the group no longer has, and taking a group's quota away drops its runtime instead of keeping it for the next quota (kernel-patches CI); the lockdep_assert_irqs_disabled() in distribute_cfs_runtime() is gone, as both bandwidth timers run with hardirqs enabled on PREEMPT_RT, and a comment states the real precondition; the burst_kept selftest grants the runtime after a period has started, so a hog cannot burn it while the test waits; CONFIG_CFS_BANDWIDTH added to tools/testing/selftests/cgroup/config.
- Patch 2: a cpu.max write starts the next period from the write, so the first refill under a new period is one new period away and not up to one old period away; selftest for it.

v2: https://lore.kernel.org/all/[email protected]/
v1: https://lore.kernel.org/all/[email protected]/

Fernand Sieber (2):
sched/fair: expose cpu.max.runtime to set bandwidth runtime directly sched/fair: refill bandwidth for the periods the timer slept through

 Documentation/admin-guide/cgroup-v2.rst   |  24 +-
 Documentation/scheduler/sched-bwc.rst     |  14 +-
 kernel/sched/core.c                       |  56 ++-
 kernel/sched/fair.c                       | 163 +++++-
 kernel/sched/sched.h                      |   4 +
 tools/testing/selftests/cgroup/config     |   1 +
 tools/testing/selftests/cgroup/test_cpu.c | 587 ++++++++++++++++++++++
 7 files changed, 839 insertions(+), 10 deletions(-)

base-commit: d7e670601af6c564ccf6d3b2ddf70ee4f41a6d1c

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#14 lore raw

PATCH v3 1/2 sched/fair: expose cpu.max.runtime to set bandwidth runtime directly

Add cpu.max.runtime, a cgroup v2 file that reads and sets the CFS bandwidth runtime, cfs_b->runtime, in microseconds. Userspace can then grant a cgroup runtime beyond its quota. Refills still add only the quota, capped at quota + burst, so the excess is used up over time and the group returns to its quota per period.

A write is rejected without a quota or above quota + burst, the most the group can accumulate. A read is capped at quota + burst so it can be written back; uncapped slack from __return_cfs_rq_runtime() can exceed that until the next refill. Runtime the CPUs already took, at most a slice each, is not included and a write leaves it in place. Without a quota a read reports 0, and taking a group's quota away drops the runtime it had, rather than keeping it for the next time the group is given a quota.

A write distributes the new runtime to throttled cfs_rqs at once, as the slack timer does, rather than waiting for the unsynchronized period timer. It also shifts runtime_snap by the change, so cpu.stat burst accounting neither counts the write nor hides what the group ran since the last refill.

Also drop the burst <= quota limit and require only quota + burst <= max_bw_runtime_us (about 203 days), the quota's own limit, so a group can accumulate many periods of quota. This applies to cgroup v1 cpu.cfs_burst_us too, which shares tg_set_bandwidth().

The file exists with CONFIG_CFS_BANDWIDTH only.

---
 Documentation/admin-guide/cgroup-v2.rst   |  23 +-
 Documentation/scheduler/sched-bwc.rst     |  13 +-
 kernel/sched/core.c                       |  44 ++-
 kernel/sched/fair.c                       |  65 ++++
 kernel/sched/sched.h                      |   2 +
 tools/testing/selftests/cgroup/config     |   1 +
 tools/testing/selftests/cgroup/test_cpu.c | 356 ++++++++++++++++++++++
 7 files changed, 495 insertions(+), 9 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +22 -1
index 9e3c67b02ae2e..eb802f8ba877f 100644
@@ -1231,12 +1231,33 @@ will be referred to. All time durations are in microseconds.
 	A read-write single value file which exists on non-root
 	cgroups.  The default is "0".
 
-	The burst in the range [0, $MAX].
+	The burst. The cgroup can accumulate the runtime it does not
+	use, up to $MAX + cpu.max.burst. The burst may exceed $MAX, as
+	long as $MAX + cpu.max.burst is at most 2^44 - 1 (about 203
+	days).
 
 	This file affects only processes under the fair-class scheduler and a BPF
 	scheduler with the ``cgroup_set_bandwidth`` callback depending on what
 	the callback actually does.
 
+  cpu.max.runtime
+	A read-write single value file which exists on non-root
+	cgroups if the kernel is built with CONFIG_CFS_BANDWIDTH.
+
+	The runtime the cgroup has accumulated and not used yet, in
+	microseconds. Each period refills it by $MAX, up to $MAX +
+	cpu.max.burst, and a read returns at most $MAX + cpu.max.burst.
+	A write sets it, up to $MAX + cpu.max.burst, and lets a cgroup
+	that ran out of runtime run again at once. A write fails with
+	EINVAL while $MAX is "max", a read then returns 0, and setting
+	$MAX to "max" drops the runtime the cgroup had.
+
+	The runtime does not include what the cgroup's CPUs already took
+	from it, at most a slice each (sched_cfs_bandwidth_slice_us).
+	A write leaves that with the CPUs.
+
+	This file affects only processes under the fair-class scheduler.
+
   cpu.pressure
 	A read-write nested-keyed file.
 
Documentation/scheduler/sched-bwc.rst +7 -6
index e881a945c1888..c22b3c8c14e29 100644
@@ -90,8 +90,8 @@ bandwidth restriction in place, such a group is described as an unconstrained
 bandwidth group. This represents the traditional work-conserving behavior for
 CFS.
 
-Writing any (valid) positive value(s) no smaller than cpu.cfs_burst_us will
-enact the specified bandwidth limit. The minimum quota allowed for the quota or
+Writing any (valid) positive value(s) will enact the specified bandwidth
+limit. The minimum quota allowed for the quota or
 period is 1ms. There is also an upper bound on the period length of 1s.
 Additional restrictions exist when bandwidth limits are used in a hierarchical
 fashion, these are explained in more detail below.
@@ -101,9 +101,10 @@ and return the group to an unconstrained state once more.
 
 A value of 0 for cpu.cfs_burst_us indicates that the group can not accumulate
 any unused bandwidth. It makes the traditional bandwidth control behavior for
-CFS unchanged. Writing any (valid) positive value(s) no larger than
-cpu.cfs_quota_us into cpu.cfs_burst_us will enact the cap on unused bandwidth
-accumulation.
+CFS unchanged. Writing any (valid) positive value(s) into cpu.cfs_burst_us
+will enact the cap on unused bandwidth accumulation: the group can accumulate
+up to cpu.cfs_quota_us + cpu.cfs_burst_us. The burst may be larger than the
+quota, as long as their sum is at most 2^44 - 1 microseconds (about 203 days).
 
 Any updates to a group's bandwidth specification will result in it becoming
 unthrottled if it is in a constrained state.
@@ -243,4 +244,4 @@ Examples
 	# echo 50000 > cpu.cfs_period_us /* period = 50ms */
 	# echo 10000 > cpu.cfs_burst_us /* burst = 10ms */
 
-   Larger buffer setting (no larger than quota) allows greater burst capacity.
+   Larger buffer setting allows greater burst capacity.
kernel/sched/core.c +42 -2

69 lines not on this page · raw message

kernel/sched/fair.c +65 -0

77 lines not on this page · raw message

kernel/sched/sched.h +2 -0
index a4df59c9c9b60..1f60c84e0dc38 100644
@@ -578,6 +578,8 @@ extern void init_cfs_bandwidth(struct cfs_bandwidth *cfs_b, struct cfs_bandwidth
 
 extern void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b);
 extern void start_cfs_bandwidth(struct cfs_bandwidth *cfs_b);
+u64 cfs_bandwidth_get_runtime(struct cfs_bandwidth *cfs_b);
+int cfs_bandwidth_set_runtime(struct cfs_bandwidth *cfs_b, u64 runtime);
 extern void unthrottle_cfs_rq(struct cfs_rq *cfs_rq);
 extern bool cfs_task_bw_constrained(struct task_struct *p);
 
tools/testing/selftests/cgroup/config +1 -0
index 39f979690dd3b..aaf5872c71410 100644
@@ -1,3 +1,4 @@
+CONFIG_CFS_BANDWIDTH=y
 CONFIG_CGROUPS=y
 CONFIG_CGROUP_CPUACCT=y
 CONFIG_CGROUP_FREEZER=y
tools/testing/selftests/cgroup/test_cpu.c +356 -0

368 lines not on this page · raw message

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07
#15 lore raw

PATCH v3 2/2 sched/fair: refill bandwidth for the periods the timer slept through

In reply to Fernand Sieber

cpu.max.burst lets a group accumulate unused quota up to quota + burst. But the period timer stops after a period in which the group took no runtime, and start_cfs_bandwidth() restarts it with hrtimer_forward_now() and discards the periods that passed. A group that sits idle stops accumulating, and one that wakes after idling for a long time has only what it had when the timer stopped.

Count the missed periods instead. While the timer is stopped, its expiry is the first period boundary it missed, so the overrun at restart is the number of refills owed. Add quota for each, capped at quota + burst as __refill_cfs_bandwidth_runtime() caps it.

Without a burst the cap is quota. The timer stops right after a refill that nothing drew from, so the runtime is at quota then, and groups without a burst see no change. Groups with a burst do see a change. A group that has been idle now has up to quota + burst when it runs again. The documentation of cpu.max.burst, cpu.max.runtime and cpu.cfs_burst_us says so.

A change of the bandwidth (cpu.max, cpu.max.burst) applies from that moment, and the group keeps what it earned before it. The periods owed are first refilled at the bandwidth they were earned under, capped at its quota + burst, and then the new bandwidth applies, with its own cap. A group that had no quota earned nothing, so one given a quota after a long time unconstrained does not start full. The next period starts from the change too: a stopped timer's expiry is a boundary of the period the group had, so leaving it would put the first refill under the new bandwidth up to one old period away. A cpu.max.runtime write also refills the periods owed first, so that it replaces them, and only the periods after the write refill.

cpu.max.runtime reports the runtime the group has now, owed refills included, without starting the timer.

The idle periods are not added to cpu.stat's nr_periods, which counts the periods in which the timer ran. A timer that runs late by several periods still refills only once, unlike a stopped one; this patch leaves that as it is.

---
 Documentation/admin-guide/cgroup-v2.rst   |  17 +-
 Documentation/scheduler/sched-bwc.rst     |   5 +-
 kernel/sched/core.c                       |  12 ++
 kernel/sched/fair.c                       | 100 +++++++++-
 kernel/sched/sched.h                      |   2 +
 tools/testing/selftests/cgroup/test_cpu.c | 231 ++++++++++++++++++++++
 6 files changed, 355 insertions(+), 12 deletions(-)
Documentation/admin-guide/cgroup-v2.rst +9 -8
index eb802f8ba877f..12d40b55f2dbb 100644
@@ -1232,9 +1232,9 @@ will be referred to. All time durations are in microseconds.
 	cgroups.  The default is "0".
 
 	The burst. The cgroup can accumulate the runtime it does not
-	use, up to $MAX + cpu.max.burst. The burst may exceed $MAX, as
-	long as $MAX + cpu.max.burst is at most 2^44 - 1 (about 203
-	days).
+	use, up to $MAX + cpu.max.burst, also in the periods in which it
+	does not run. The burst may exceed $MAX, as long as $MAX +
+	cpu.max.burst is at most 2^44 - 1 (about 203 days).
 
 	This file affects only processes under the fair-class scheduler and a BPF
 	scheduler with the ``cgroup_set_bandwidth`` callback depending on what
@@ -1246,11 +1246,12 @@ will be referred to. All time durations are in microseconds.
 
 	The runtime the cgroup has accumulated and not used yet, in
 	microseconds. Each period refills it by $MAX, up to $MAX +
-	cpu.max.burst, and a read returns at most $MAX + cpu.max.burst.
-	A write sets it, up to $MAX + cpu.max.burst, and lets a cgroup
-	that ran out of runtime run again at once. A write fails with
-	EINVAL while $MAX is "max", a read then returns 0, and setting
-	$MAX to "max" drops the runtime the cgroup had.
+	cpu.max.burst, whether the cgroup runs in it or not. A read
+	returns at most $MAX + cpu.max.burst. A write sets it, up to
+	$MAX + cpu.max.burst, and lets a cgroup that ran out of runtime
+	run again at once. A write fails with EINVAL while $MAX is
+	"max", a read then returns 0, and setting $MAX to "max" drops
+	the runtime the cgroup had.
 
 	The runtime does not include what the cgroup's CPUs already took
 	from it, at most a slice each (sched_cfs_bandwidth_slice_us).
Documentation/scheduler/sched-bwc.rst +3 -2
index c22b3c8c14e29..ccf7434ca73f0 100644
@@ -103,8 +103,9 @@ A value of 0 for cpu.cfs_burst_us indicates that the group can not accumulate
 any unused bandwidth. It makes the traditional bandwidth control behavior for
 CFS unchanged. Writing any (valid) positive value(s) into cpu.cfs_burst_us
 will enact the cap on unused bandwidth accumulation: the group can accumulate
-up to cpu.cfs_quota_us + cpu.cfs_burst_us. The burst may be larger than the
-quota, as long as their sum is at most 2^44 - 1 microseconds (about 203 days).
+up to cpu.cfs_quota_us + cpu.cfs_burst_us, also over periods in which it does
+not run. The burst may be larger than the quota, as long as their sum is at
+most 2^44 - 1 microseconds (about 203 days).
 
 Any updates to a group's bandwidth specification will result in it becoming
 unthrottled if it is in a constrained state.
kernel/sched/core.c +12 -0
index e068d81e1fc5f..265088af6a561 100644
@@ -9856,10 +9856,22 @@ static int tg_set_cfs_bandwidth(struct task_group *tg,
 		cfs_bandwidth_usage_inc();
 
 	scoped_guard (raw_spinlock_irq, &cfs_b->lock) {
+		/*
+		 * The new bandwidth applies from now on. The group keeps what
+		 * it earned while idle under the old one.
+		 */
+		cfs_bandwidth_refill_idle_periods(cfs_b);
+
 		cfs_b->period = ns_to_ktime(period);
 		cfs_b->quota = quota;
 		cfs_b->burst = burst;
 
+		/*
+		 * The next period starts from here too, rather than at the
+		 * boundary left over from the old one.
+		 */
+		cfs_bandwidth_reanchor_period(cfs_b);
+
 		/*
 		 * A group without a quota accumulates no runtime, so drop what
 		 * it had rather than report it and add to it once it has a
kernel/sched/fair.c +98 -2

130 lines not on this page · raw message

kernel/sched/sched.h +2 -0
index 1f60c84e0dc38..a03ac077a8f49 100644
@@ -580,6 +580,8 @@ extern void __refill_cfs_bandwidth_runtime(struct cfs_bandwidth *cfs_b);
 extern void start_cfs_bandwidth(struct cfs_bandwidth *cfs_b);
 u64 cfs_bandwidth_get_runtime(struct cfs_bandwidth *cfs_b);
 int cfs_bandwidth_set_runtime(struct cfs_bandwidth *cfs_b, u64 runtime);
+void cfs_bandwidth_refill_idle_periods(struct cfs_bandwidth *cfs_b);
+void cfs_bandwidth_reanchor_period(struct cfs_bandwidth *cfs_b);
 extern void unthrottle_cfs_rq(struct cfs_rq *cfs_rq);
 extern bool cfs_task_bw_constrained(struct task_struct *p);
 
tools/testing/selftests/cgroup/test_cpu.c +231 -0

243 lines not on this page · raw message

Signature · 8 lines
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07