The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: Tejun Heo <tj@kernel.org>
To: David Vernet <void@manifault.com>,
	Andrea Righi <arighi@nvidia.com>,
	Changwoo Min <changwoo@igalia.com>
Cc: sched-ext@lists.linux.dev, Emil Tsalapatis <emil@etsalapatis.com>,
	linux-kernel@vger.kernel.org, Tejun Heo <tj@kernel.org>
Subject: [PATCH 12/12] sched_ext: scx_qmap - Add rescue support
Date: Fri, 31 Jul 2026 22:51:50 -1000	[thread overview]
Message-ID: <20260801085150.2697653-13-tj@kernel.org> (raw)
In-Reply-To: <20260801085150.2697653-1-tj@kernel.org>

A sched holds only the cids its parent granted and nothing guarantees that
they cover its tasks' affinities. A task that can run on none of them has
nowhere to go and qmap stalls out: it force-inserts the task onto its first
allowed cid, but the kernel bounces the insert back and the task parks in
SHARED_DSQ, which is drained only on self cids it can't run on.

Set SCX_ENQ_RESCUE on these inserts so the kernel diverts such tasks to its
rescue path instead of bouncing them. The force-insert covers scheds with
and without children and fires on re-enqueues, and the SHARED_DSQ scan on
every dispatch rescues tasks stranded there - the enqueue-time check misses
a task whose cids were lost while it was already queued. The wrong-cid fault
injection carries the flag too and doubles as a deterministic rescue-traffic
generator.

-B and -q set the root-only rescue bandwidth and quantum ops knobs. -B 0
maps to SCX_RESCUE_DISABLE and turns rescue off kernel-side. Rescue inserts
are counted and reported in the hier stats line.

Signed-off-by: Tejun Heo <tj@kernel.org>
---
 tools/sched_ext/scx_qmap.bpf.c | 85 +++++++++++++++++++++++-----------
 tools/sched_ext/scx_qmap.c     | 23 +++++++--
 tools/sched_ext/scx_qmap.h     |  1 +
 3 files changed, 79 insertions(+), 30 deletions(-)

diff --git a/tools/sched_ext/scx_qmap.bpf.c b/tools/sched_ext/scx_qmap.bpf.c
index 9a0321e84e88..b6e7b004611c 100644
--- a/tools/sched_ext/scx_qmap.bpf.c
+++ b/tools/sched_ext/scx_qmap.bpf.c
@@ -449,31 +449,33 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags)
 	taskc->core_sched_seq = qa.core_sched_tail_seqs[idx]++;
 
 	/*
-	 * A node with children delegates most cids. A task of ours that can run
-	 * on none of our self cids (e.g. a per-NUMA kthread pinned to delegated
-	 * cids) would starve in SHARED/FIFO since we never pull those on a
-	 * delegated cid. Force it onto its first allowed cid's local DSQ with
-	 * needs_immed(): if we hold access there it runs, else the kernel
-	 * rejects and bounces it back via REENQ_CAP. Best-effort
-	 * anti-starvation nudge.
+	 * A task of ours that can run on none of our self cids - the parent
+	 * didn't grant them or we delegated them to children - would starve in
+	 * SHARED/FIFO since we only pull from those on self cids.
+	 *
+	 * Force it onto its first allowed cid's local DSQ. If we hold that cid
+	 * it runs. Otherwise the insert carries SCX_ENQ_RESCUE and the kernel
+	 * diverts the task to its rescue path.
 	 */
-	if (qa.nr_sub_scheds && !(enq_flags & SCX_ENQ_REENQ) &&
-	    !cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) {
+	if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) {
 		s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
 
 		if (c >= 0 && c < scx_bpf_nr_cids()) {
 			taskc->force_local = false;
+			__sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
 			scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | c, slice_ns,
-					   enq_flags | needs_immed(c));
+					   enq_flags | needs_immed(c) | SCX_ENQ_RESCUE);
 			return;
 		}
 	}
 
 	/*
 	 * Fault injection: deliberately dispatch one of our own tasks to a cid
-	 * we don't hold. The kernel cap check must reject it and re-enqueue
-	 * with SCX_TASK_REENQ_CAP, so nr_inject_attempts tracks nr_reenq_cap
-	 * and proves delivery-time enforcement. Throttled.
+	 * we don't hold. The inserts carry SCX_ENQ_RESCUE and divert to the
+	 * kernel rescue path, a deterministic rescue-traffic generator. Under
+	 * -B 0 the kernel cap check rejects and re-enqueues them instead, so
+	 * nr_inject_attempts tracks nr_reenq_cap 1:1 and proves delivery-time
+	 * enforcement. Throttled.
 	 */
 	if (qa.inject_mode == QMAP_INJ_WRONG_CID && p->nr_cpus_allowed > 1 &&
 	    !(enq_flags & SCX_ENQ_REENQ)) {
@@ -484,8 +486,9 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags)
 
 			if (bad >= 0 && cmask_test(bad, &taskc->cpus_allowed)) {
 				__sync_fetch_and_add(&qa.nr_inject_attempts, 1);
-				scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | bad,
-						   slice_ns, enq_flags);
+				__sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+				scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | bad, slice_ns,
+						   enq_flags | SCX_ENQ_RESCUE);
 				return;
 			}
 		}
@@ -588,29 +591,47 @@ static void update_core_sched_head_seq(struct task_struct *p)
 }
 
 /*
+ * One pass over SHARED_DSQ: rescue stranded tasks and boost highpri ones. A
+ * task whose cids were lost while it was queued in the fifos would strand on
+ * SHARED_DSQ, which is consumed only on self cids it can't run on - move it to
+ * the kernel rescue path. One whose cids were lost after the highpri cull is
+ * likewise rescued out of HIGHPRI_DSQ below.
+ *
  * To demonstrate the use of scx_bpf_dsq_move(), implement silly selective
- * priority boosting mechanism by scanning SHARED_DSQ looking for highpri tasks,
- * moving them to HIGHPRI_DSQ and then consuming them first. This makes minor
- * difference only when dsp_batch is larger than 1.
+ * priority boosting mechanism by moving highpri tasks to HIGHPRI_DSQ and then
+ * consuming them first. This makes minor difference only when dsp_batch is
+ * larger than 1.
  *
  * scx_bpf_dsq_move[_vtime]() are allowed both from ops.dispatch() and
  * non-rq-lock holding BPF programs. As demonstration, this function is called
  * from qmap_dispatch() and monitor_timerfn().
  */
-static bool dispatch_highpri(bool from_timer)
+static bool scan_shared_dsq(bool from_timer)
 {
 	struct task_struct *p;
 	s32 this_cid = scx_bpf_this_cid();
 	u32 nr_cids = scx_bpf_nr_cids();
 
-	/* scan SHARED_DSQ and move highpri tasks to HIGHPRI_DSQ */
+	/* rescue strands and move highpri tasks to HIGHPRI_DSQ */
 	bpf_for_each(scx_dsq, p, SHARED_DSQ, 0) {
 		static u64 highpri_seq;
 		task_ctx_t *taskc;
+		s32 c;
 
 		if (!(taskc = lookup_task_ctx(p)))
 			return false;
 
+		/* stranded? rescue - it can't be dispatched here either way */
+		if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) {
+			c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
+			if (c >= 0 && c < scx_bpf_nr_cids()) {
+				__sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+				scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c,
+						 needs_immed(c) | SCX_ENQ_RESCUE);
+			}
+			continue;
+		}
+
 		if (taskc->highpri) {
 			/* exercise the set_*() and vtime interface too */
 			scx_bpf_dsq_move_set_slice(BPF_FOR_EACH_ITER, slice_ns * 2);
@@ -640,8 +661,17 @@ static bool dispatch_highpri(bool from_timer)
 			cid = cmask_next_and_set_wrap(&taskc->cpus_allowed,
 						      &qa.self_cids.mask,
 						      this_cid + 1);
-		if (cid >= nr_cids)
+		if (cid >= nr_cids) {
+			/* stranded after the cull - rescue it from here */
+			s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
+
+			if (c >= 0 && c < nr_cids) {
+				__sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+				scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c,
+						 needs_immed(c) | SCX_ENQ_RESCUE);
+			}
 			continue;
+		}
 
 		if (scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | cid,
 				     SCX_ENQ_PREEMPT | needs_immed(cid))) {
@@ -670,10 +700,9 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev)
 	struct cpu_ctx __arena *cpuc;
 	task_ctx_t *taskc;
 	u32 batch = dsp_batch ?: 1;
-	s32 owner;
-	s32 i;
+	s32 owner, i;
 
-	if (dispatch_highpri(false))
+	if (scan_shared_dsq(false))
 		return;
 
 	/*
@@ -785,11 +814,10 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev)
 			 */
 			if (!cmask_test(cid, &taskc->cpus_allowed))
 				scx_bpf_kick_cid(scx_bpf_task_cid(p), 0);
-
 			batch--;
 			cpuc->dsp_cnt--;
 			if (!batch || !scx_bpf_dispatch_nr_slots()) {
-				if (dispatch_highpri(false))
+				if (scan_shared_dsq(false))
 					return;
 				scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid));
 				return;
@@ -801,6 +829,9 @@ void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev)
 		cpuc->dsp_cnt = 0;
 	}
 
+	if (scan_shared_dsq(false))
+		return;
+
 	/*
 	 * No other tasks. @prev will keep running. Update its core_sched_seq as
 	 * if the task were enqueued and dispatched immediately.
@@ -1185,7 +1216,7 @@ static void dump_shared_dsq(void)
 static int monitor_timerfn(void *map, int *key, struct bpf_timer *timer)
 {
 	bpf_rcu_read_lock();
-	dispatch_highpri(true);
+	scan_shared_dsq(true);
 	bpf_rcu_read_unlock();
 
 	monitor_cpuperf();
diff --git a/tools/sched_ext/scx_qmap.c b/tools/sched_ext/scx_qmap.c
index 3f54796e48be..189c8fed2f68 100644
--- a/tools/sched_ext/scx_qmap.c
+++ b/tools/sched_ext/scx_qmap.c
@@ -72,6 +72,8 @@ const char help_fmt[] =
 "  -J MODE       Fault injection (wrong-cid: dispatch to a cid not held,\n"
 "                init-fail/cgrp-init-fail: fail init_task/cpuctl_init for\n"
 "                \"qmfail*\" comms/cgroups)\n"
+"  -B PPT        Rescue bandwidth in parts per thousand, 0 disables (root only, default 20)\n"
+"  -q US         Rescue batch quantum in microseconds (root only, default 5000)\n"
 "  -v            Print libbpf debug messages\n"
 "  -h            Display this help and exit\n";
 
@@ -106,6 +108,7 @@ struct hier_prev {
 	u64 nr_reenq_cap;
 	u64 nr_reenq_immed;
 	u64 nr_inject_attempts;
+	u64 nr_rescue_dsp;
 };
 
 /* current wall-clock time as "HH:MM:SS" for the startup and interval headers */
@@ -187,14 +190,16 @@ static void print_hier(struct qmap_arena *qa, struct hier_prev *prev, u64 own_cg
 	}
 
 	format_cid_ranges(qa, CID_SHARED, ranges, sizeof(ranges));
-	printf("hier   : nsub=%llu excl=%u shared=%s rr=%s reenq cap/immed +%llu/+%llu inj=+%llu\n",
+	printf("hier   : nsub=%llu excl=%u shared=%s rr=%s reenq cap/immed +%llu/+%llu inj=+%llu rescue=+%llu\n",
 	       (unsigned long long)qa->nr_sub_scheds, qa->part.nr_excl, ranges, rr,
 	       (unsigned long long)(qa->nr_reenq_cap - prev->nr_reenq_cap),
 	       (unsigned long long)(qa->nr_reenq_immed - prev->nr_reenq_immed),
-	       (unsigned long long)(qa->nr_inject_attempts - prev->nr_inject_attempts));
+	       (unsigned long long)(qa->nr_inject_attempts - prev->nr_inject_attempts),
+	       (unsigned long long)(qa->nr_rescue_dsp - prev->nr_rescue_dsp));
 	prev->nr_reenq_cap = qa->nr_reenq_cap;
 	prev->nr_reenq_immed = qa->nr_reenq_immed;
 	prev->nr_inject_attempts = qa->nr_inject_attempts;
+	prev->nr_rescue_dsp = qa->nr_rescue_dsp;
 
 	printf("hier   : %-4s %10s %4s %6s %8s  %s\n",
 	       "", "cgroup", "w", "alloc", "disp/s", "cids");
@@ -256,7 +261,8 @@ int main(int argc, char **argv)
 	skel->rodata->slice_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
 	skel->rodata->max_tasks = 16384;
 
-	while ((opt = getopt(argc, argv, "s:e:t:T:l:b:N:PMHc:d:D:SpIF:C:i:R:J:vh")) != -1) {
+	while ((opt = getopt(argc, argv,
+			     "s:e:t:T:l:b:N:PMHc:d:D:SpIF:C:i:R:J:B:q:vh")) != -1) {
 		switch (opt) {
 		case 's':
 			skel->rodata->slice_ns = strtoull(optarg, NULL, 0) * 1000;
@@ -400,6 +406,17 @@ int main(int argc, char **argv)
 			else
 				inject_mode = strtoul(optarg, NULL, 0);
 			break;
+		case 'B': {
+			u32 ppt = strtoul(optarg, NULL, 0);
+
+			if (!ppt)
+				ppt = __COMPAT_ENUM_OR_ZERO("scx_consts", "SCX_RESCUE_DISABLE");
+			skel->struct_ops.qmap_ops->rescue_bandwidth_ppt = ppt;
+			break;
+		}
+		case 'q':
+			skel->struct_ops.qmap_ops->rescue_quantum_us = strtoul(optarg, NULL, 0);
+			break;
 		case 'v':
 			verbose = true;
 			break;
diff --git a/tools/sched_ext/scx_qmap.h b/tools/sched_ext/scx_qmap.h
index c42f7ef74b89..c8f602d58ca3 100644
--- a/tools/sched_ext/scx_qmap.h
+++ b/tools/sched_ext/scx_qmap.h
@@ -175,6 +175,7 @@ struct qmap_arena {
 	u64 nr_reenq_cap;		/* SCX_TASK_REENQ_CAP bounces */
 	u64 nr_reenq_immed;		/* SCX_TASK_REENQ_IMMED bounces */
 	u64 nr_inject_attempts;		/* fault-injection: dispatches to an unheld cid */
+	u64 nr_rescue_dsp;		/* SCX_ENQ_RESCUE dispatch attempts */
 	u32 inject_mode;		/* fault-injection mode (QMAP_INJ_*) */
 };
 
-- 
2.55.0


      parent reply	other threads:[~2026-08-01  8:52 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-01  8:51 [PATCHSET sched_ext/for-7.3] sched_ext: Bandwidth-limited rescue execution for stranded tasks Tejun Heo
2026-08-01  8:51 ` [PATCH 01/12] sched_ext: Rename scx_local_or_reject_dsq() to scx_resolve_local_dsq() Tejun Heo
2026-08-01  8:51 ` [PATCH 02/12] sched_ext: Make several ext.c helpers available outside ext.c Tejun Heo
2026-08-01  8:51 ` [PATCH 03/12] sched_ext: Factor out __scx_bpf_now() Tejun Heo
2026-08-01  8:51 ` [PATCH 04/12] sched_ext: Reject internal enq_flags in the dsq move kfuncs Tejun Heo
2026-08-01  8:51 ` [PATCH 05/12] sched_ext: Make SCX_ENQ_IGNORE_CAPS waive the preemption cap too Tejun Heo
2026-08-01  8:51 ` [PATCH 06/12] sched_ext: Synchronize slice and dsq_vtime writes Tejun Heo
2026-08-01  8:51 ` [PATCH 07/12] sched_ext: Add SCX_TASK_PROTECTED Tejun Heo
2026-08-01  8:51 ` [PATCH 08/12] sched_ext: Add bandwidth-limited rescue execution for stranded tasks Tejun Heo
2026-08-01  8:51 ` [PATCH 09/12] sched_ext: Eject the top rescue consumer on overload Tejun Heo
2026-08-01  8:51 ` [PATCH 10/12] sched_ext: Sync tools autogen enum headers Tejun Heo
2026-08-01  8:51 ` [PATCH 11/12] sched_ext: scx_qmap - Idle-check pinned tasks before direct dispatch Tejun Heo
2026-08-01  8:51 ` Tejun Heo [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260801085150.2697653-13-tj@kernel.org \
    --to=tj@kernel.org \
    --cc=arighi@nvidia.com \
    --cc=changwoo@igalia.com \
    --cc=emil@etsalapatis.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=sched-ext@lists.linux.dev \
    --cc=void@manifault.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox