All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v9 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work
@ 2026-07-31  2:44 Luo Gengkun
  2026-07-31  2:44 ` [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus Luo Gengkun
  2026-07-31  2:44 ` [PATCH v9 2/2] -- DO NOT APPLY!!! -- sched/cache/debug: Add trace event and sched feature to track scan cost Luo Gengkun
  0 siblings, 2 replies; 7+ messages in thread
From: Luo Gengkun @ 2026-07-31  2:44 UTC (permalink / raw)
  To: mingo, peterz, juri.lelli, vincent.guittot, tim.c.chen, yu.c.chen
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel

Hi everyone,
The cache-aware scheduling patches have now been merged into the mainline.
The goal of this series is to reduce the overhead of task_cache_work() by
minimizing the number of scanned CPUs, which yields significant
performance gains in multi-instance scenarios like Redis. To facilitate
testing, a debug patch is appended.

Below are the Redis test results conducted on AMD server.

When NUMA balancing is disabled:

valkey-benchmark rps  |      baseline       |         schedcache              |     schedcache_visit
                      |  (no cache aware)   |    (without this series)        |     (with this series)
----------------------+---------------------+---------------------+-----------+-------------------+--------
                      |   p99 latency(ms)   |    p99 latency(ms)  |   DIFF(%) |   p99 latency(ms) |  DIFF(%)
400000                |      0.436          |        0.554        |   -25.68% |         0.441     |   -1.14% 

Additionally, the output of perf top -e cycles:k highlights the overhead
incurred by task_cache_work():

valkey-benchmark rps  |         schedcache                        |     schedcache_visit
                      |    (without this series)                  |     (with this series)
----------------------+-------------------------------------------+---------------------------------------
400000                |  0.81%  [kernel]  [k] task_cache_work     |  0.02%  [kernel]  [k] task_cache_work

The trace data is shown below:

echo SC_NODE > /sys/kernel/debug/sched/features
echo NO_SC_VISIT > /sys/kernel/debug/sched/features
    redis-server-24660   [195] .....  2916.005989: sched_cache_scan: comm=redis-server pid=24660 scan=384
 valkey-benchmar-48836   [317] .....  2916.005993: sched_cache_scan: comm=valkey-benchmar pid=48836 scan=384
 valkey-benchmar-48487   [310] .....  2916.006002: sched_cache_scan: comm=valkey-benchmar pid=48487 scan=384

echo NO_SC_NODE > /sys/kernel/debug/sched/features
echo SC_VISIT > /sys/kernel/debug/sched/features
    redis-server-29080   [225] .....  2876.257909: sched_cache_scan: comm=redis-server pid=29080 scan=14    
    redis-server-26694   [018] .....  2876.257909: sched_cache_scan: comm=redis-server pid=26694 scan=13    
 valkey-benchmar-48198   [103] .....  2876.257909: sched_cache_scan: comm=valkey-benchmar pid=48198 scan=16 

The above results show that this series significantly reduces the overhead
of task_cache_work() by decreasing the number of CPUs that need to be
scanned, from 384 to 16, when NUMA balancing is disabled.

Even with NUMA balancing enabled, this patch provides better performance
benefits. The following tests were conducted with NUMA balancing enabled:

valkey-benchmark rps  |      baseline       |         schedcache              |     schedcache_visit
                      |  (no cache aware)   |    (without this series)        |     (with this series)
----------------------+---------------------+---------------------+-----------+-------------------+--------
                      |   p99 latency(ms)   |    p99 latency(ms)  |   DIFF(%) |   p99 latency(ms) |  DIFF(%)
400000                |      0.437          |        0.454        |   -3.89%  |         0.442     |   -1.14%

The overhead of task_cache_work() is as follows:

valkey-benchmark rps  |         schedcache                        |     schedcache_visit
                      |    (without this series)                  |     (with this series)
----------------------+-------------------------------------------+---------------------------------------
400000                |  0.13%  [kernel]  [k] task_cache_work     |  0.03%  [kernel]  [k] task_cache_work 


Hackbench testing was also conducted. The results below show that this
series does not affect the accuracy of cache-aware scheduling.

=========================================
Hackbench Comparison: baseline vs schedcache
=========================================
MODE       GROUPS  FDS |     baseline(std)  |  schedcache(std)   |    DIFF(%) | VERDICT
---------- ------ -----+--------------------+--------------------+------------+-----------
threads         1   10 |    113.200 (4.22%) |     67.300 (1.32%) |     40.55% | IMPROVED
threads         1    2 |     16.555 (4.11%) |     11.020 (1.66%) |     33.43% | IMPROVED
threads         1   20 |    250.774 (1.26%) |    265.026 (5.44%) |     -5.68% | REGRESSED
threads         1    4 |     42.117 (1.44%) |     27.758 (1.64%) |     34.09% | IMPROVED
threads         1    6 |     65.140 (4.31%) |     39.182 (1.38%) |     39.85% | IMPROVED
threads         1    8 |     84.286 (1.29%) |     53.721 (1.58%) |     36.26% | IMPROVED
threads         2   10 |    122.592 (0.44%) |    113.365 (4.93%) |      7.53% | IMPROVED
threads         2    2 |     17.702 (4.09%) |     10.473 (0.42%) |     40.84% | IMPROVED
threads         2   20 |    336.457 (1.77%) |    314.108 (1.51%) |      6.64% | IMPROVED
threads         2    4 |     43.989 (0.88%) |     27.067 (3.38%) |     38.47% | IMPROVED
threads         2    6 |     69.322 (0.85%) |     41.707 (4.19%) |     39.84% | IMPROVED
threads         2    8 |    103.767 (1.81%) |     58.518 (3.00%) |     43.61% | IMPROVED
threads         4   10 |    148.882 (3.56%) |    149.449 (1.06%) |     -0.38% | REGRESSED
threads         4    2 |     18.909 (2.96%) |     11.063 (2.08%) |     41.49% | IMPROVED
threads         4   20 |    724.943 (2.14%) |    631.222 (3.92%) |     12.93% | IMPROVED
threads         4    4 |     48.191 (1.91%) |     27.352 (5.35%) |     43.24% | IMPROVED
threads         4    6 |     79.725 (3.84%) |     78.732 (4.10%) |      1.25% | IMPROVED
threads         4    8 |    108.768 (1.36%) |    105.928 (1.65%) |      2.61% | IMPROVED
     
Hackbench Comparison: schedcache vs schedcache_visit
=========================================
MODE       GROUPS  FDS |    schedcache(std) |schedcache_visit(std) | DIFF(%) | VERDICT
---------- ------ -----+--------------------+----------------------+---------+-----------
threads         1   10 |     67.300 (1.32%) |     67.014 (0.96%)   |   0.42% | IMPROVED
threads         1    2 |     11.020 (1.66%) |     10.557 (1.46%)   |   4.20% | IMPROVED
threads         1   20 |    265.026 (5.44%) |   212.366 (16.32%)   |  19.87% | IMPROVED
threads         1    4 |     27.758 (1.64%) |     25.711 (1.32%)   |   7.37% | IMPROVED
threads         1    6 |     39.182 (1.38%) |     38.914 (0.34%)   |   0.68% | IMPROVED
threads         1    8 |     53.721 (1.58%) |     52.889 (0.27%)   |   1.55% | IMPROVED
threads         2   10 |    121.203 (6.99%) |    124.254 (1.38%)   |  -2.52% | REGRESSED
threads         2    2 |     10.473 (0.42%) |     11.206 (5.91%)   |  -7.00% | REGRESSED
threads         2   20 |    314.108 (1.51%) |    301.754 (1.95%)   |   3.93% | IMPROVED
threads         2    4 |     27.067 (3.38%) |     28.028 (2.01%)   |  -3.55% | REGRESSED
threads         2    6 |     41.707 (4.19%) |     42.149 (3.35%)   |  -1.06% | REGRESSED
threads         2    8 |     58.518 (3.00%) |     57.133 (4.39%)   |   2.37% | IMPROVED
threads         4   10 |    149.449 (1.06%) |    141.407 (0.08%)   |   5.38% | IMPROVED
threads         4    2 |     11.063 (2.08%) |     11.360 (5.85%)   |  -2.68% | REGRESSED
threads         4   20 |    631.222 (3.92%) |    622.780 (2.49%)   |   1.34% | IMPROVED
threads         4    4 |     27.352 (5.35%) |     27.947 (5.37%)   |  -2.18% | REGRESSED
threads         4    6 |     78.732 (4.10%) |     73.911 (0.70%)   |   6.12% | IMPROVED
threads         4    8 |    105.928 (1.65%) |    107.535 (3.29%)   |  -1.52% | REGRESSED 

---
Changes history
**v9 Changes:**
1. Using for_each_cpu_and(i, sched_domain_span(sd), cpus) instead of
for_each_cpu_and(i, sched_domain_span(sd), mm->sc_stat.visited_cpus) in
the inner loop to reduce the race window to  read mm->sc_stat.pcpu_sched.
2. Update the comment to reflect that a single scanner is elected per epoch
in task_cache_work().

Link to v8: https://lore.kernel.org/all/20260723040429.630176-1-luogengkun2@huawei.com/

**v8 Changes:**
1. Remove get_scan_cpumasks() since visited_cpus provides the exact CPUs to
scan.

Link to v7: https://lore.kernel.org/all/20260720122214.3977092-1-luogengkun2@huawei.com/

**v7 Changes:**
1. Add code comments in task_cache_work() to clarify the race condition
between setting and reading visited_cpus.

Link to v6: https://lore.kernel.org/all/20260717041231.3324851-1-luogengkun2@huawei.com/

**v6 Changes:**
1. Switched to cpumask_var_t to dynamically allocate visited_cpus inside
mm_struct, preventing per-process memory bloat when NR_CPUS is configured
large.
2. Intersected the LLC scheduling domain span directly with visited_cpus
to prevent skipping CPU is cross-node LLC topolgies.
3. Renamed epoch_timeout to epoch_last_visit to better reflect its purpose.
4. Moved __update_mm_sched to execute before the timeout check in
fraction_mm_sched()

Link to v5: https://lore.kernel.org/all/20260709130053.2749834-1-luogengkun2@huawei.com/

**v5 Changes:**
1. Restore get_scan_cpumasks() to avoid violating NUMA_BALANCING constraints.
2. Use for_each_cpu_and() to filter CPUs within the LLC domain.

Link to v4: https://lore.kernel.org/all/20260618064300.80072-1-luogengkun2@huawei.com/

**v4 Changes:**
1. Rebase to the master.
2. epoch_timeout is introduced to evict expired CPUs instead of relying on
epoch, because epoch is refreshed periodically due to invocations of
fraction_mm_sched().
3. Move the increasement of nr_running before fraction_mm_sched().
4. Remove the redundant 'work->next' reset at the end of task_cache_work().
5. Add a debug patch to show the number of CPUs scanned to show the
benefit of this optimization.

Link to v3: https://lore.kernel.org/all/20260423085414.1389749-1-luogengkun2@huawei.com/

**v3 Changes:**
1. Remove the static key and enable this feature by default.
2. Reuse llc_epoch_affinity_timeout instead of introducing
llc_epoch_visited_timeout.
3. Move the calculation of rq->cpu_epoch - pcpu_sched->epoch into
fraction_mm_sched() to avoid race between task_cache_work() and
__update_mm_sched(). 
4. Reset work->next at the end of task_cache_work() to prevent concurrent
executions by multiple threads within the same process.

Link to v2: https://lore.kernel.org/all/20260414150745.225416-1-luogengkun2@huawei.com/

**v2 Changes:**
1. Added a pre-check before set/clear visited_cpus to avoid C2C overhead.
2. Optimized llc_epoch_visited_timeout by using a static key to minimize overhead.

Link to v1: https://lore.kernel.org/all/f2488085-4b52-491d-84be-d30d43954381@huawei.com/

Luo Gengkun (2):
  sched/cache: Reduce the overhead of task_cache_work by only scan the
    visisted cpus
  -- DO NOT APPLY!!! -- sched/cache/debug: Add trace event and sched
    feature to track scan cost

 include/linux/mm_types.h     |  6 +++
 include/linux/sched.h        |  2 +
 include/trace/events/sched.h | 21 +++++++++++
 kernel/sched/fair.c          | 71 +++++++++++++++++++++++++++++-------
 kernel/sched/features.h      |  2 +
 5 files changed, 89 insertions(+), 13 deletions(-)

-- 
2.34.1


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus
  2026-07-31  2:44 [PATCH v9 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work Luo Gengkun
@ 2026-07-31  2:44 ` Luo Gengkun
  2026-08-11  2:27   ` Luo Gengkun
  2026-07-31  2:44 ` [PATCH v9 2/2] -- DO NOT APPLY!!! -- sched/cache/debug: Add trace event and sched feature to track scan cost Luo Gengkun
  1 sibling, 1 reply; 7+ messages in thread
From: Luo Gengkun @ 2026-07-31  2:44 UTC (permalink / raw)
  To: mingo, peterz, juri.lelli, vincent.guittot, tim.c.chen, yu.c.chen
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel

The overhead of task_cache_work() is high, especially in multi-NUMA systems.
Currently, task_cache_work() tries to find the pref_llc by scanning all CPUs
in the system. However, most of these scans are meaningless, such as those
for CPUs that have never been visited or were accessed a long time ago.

To address this problem, introduce visited_cpus to track the visited CPUs
and evict them once they have not been accessed for a duration exceeding
llc_epoch_affinity_timeout.

Now that we know exactly which CPUs to scan from visited_cpus, we can remove
get_scan_cpumasks().

Tested-by: Chen Yu <yu.c.chen@intel.com>
Reviewed-by: Tim Chen <tim.c.chen@linux.intel.com>
Signed-off-by: Luo Gengkun <luogengkun2@huawei.com>
---
 include/linux/mm_types.h |  6 +++
 include/linux/sched.h    |  2 +
 kernel/sched/fair.c      | 96 ++++++++++++++++------------------------
 3 files changed, 47 insertions(+), 57 deletions(-)

diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index b18c2b2e7d2c..35559079e4d4 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -1620,6 +1620,11 @@ static inline int mm_alloc_sched_noprof(struct mm_struct *mm)
 	if (!pcpu_sched)
 		return -ENOMEM;
 
+	if (!zalloc_cpumask_var(&mm->sc_stat.visited_cpus, GFP_KERNEL)) {
+		free_percpu(pcpu_sched);
+		return -ENOMEM;
+	}
+
 	mm_init_sched(mm, pcpu_sched);
 	return 0;
 }
@@ -1630,6 +1635,7 @@ static inline void mm_destroy_sched(struct mm_struct *mm)
 {
 	free_percpu(mm->sc_stat.pcpu_sched);
 	mm->sc_stat.pcpu_sched = NULL;
+	free_cpumask_var(mm->sc_stat.visited_cpus);
 }
 #else /* !CONFIG_SCHED_CACHE */
 
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 373bcc0598d1..b461a71a65da 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2388,6 +2388,7 @@ static __always_inline int task_mm_cid(struct task_struct *t)
 struct sched_cache_time {
 	u64 runtime;
 	unsigned long epoch;
+	unsigned long epoch_last_visit;
 };
 
 struct sched_cache_stat {
@@ -2398,6 +2399,7 @@ struct sched_cache_stat {
 	unsigned long next_scan;
 	unsigned long footprint;
 	int cpu;
+	cpumask_var_t visited_cpus;
 } ____cacheline_aligned_in_smp;
 
 #else
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index d78467ec6ee1..2bb370b38b79 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1585,6 +1585,7 @@ void mm_init_sched(struct mm_struct *mm,
 		pcpu_sched->runtime = 0;
 		/* a slightly stale cpu epoch is acceptible */
 		pcpu_sched->epoch = rq->cpu_epoch;
+		pcpu_sched->epoch_last_visit = rq->cpu_epoch;
 		epoch = rq->cpu_epoch;
 	}
 
@@ -1635,13 +1636,23 @@ static inline void __update_mm_sched(struct rq *rq,
 	}
 }
 
-static unsigned long fraction_mm_sched(struct rq *rq,
-				       struct sched_cache_time *pcpu_sched)
+static unsigned long fraction_mm_sched(int cpu,
+				       struct mm_struct *mm)
 {
+	struct sched_cache_time *pcpu_sched =
+		per_cpu_ptr(mm->sc_stat.pcpu_sched, cpu);
+	struct rq *rq = cpu_rq(cpu);
+
 	guard(raw_spinlock_irqsave)(&rq->cpu_epoch_lock);
 
 	__update_mm_sched(rq, pcpu_sched);
 
+	/* Skip the rq that has not been hit for a long time */
+	if ((rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout) {
+		cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
+		return 0;
+	}
+
 	/*
 	 * Runtime is a geometric series (r=0.5) and as such will sum to twice
 	 * the accumulation period, this means the multiplcation here should
@@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
 		pcpu_sched->runtime += delta_exec;
 		rq->cpu_runtime += delta_exec;
 		epoch = rq->cpu_epoch;
+		pcpu_sched->epoch_last_visit = epoch;
+		if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
+			cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
 	}
 
 	/*
@@ -1761,51 +1775,6 @@ static void task_tick_cache(struct rq *rq, struct task_struct *p)
 	}
 }
 
-static void get_scan_cpumasks(cpumask_var_t cpus, struct task_struct *p)
-{
-#ifdef CONFIG_NUMA_BALANCING
-	int cpu, curr_cpu, nid, pref_nid;
-
-	if (!static_branch_likely(&sched_numa_balancing))
-		goto out;
-
-	cpu = READ_ONCE(p->mm->sc_stat.cpu);
-	if (cpu != -1)
-		nid = cpu_to_node(cpu);
-	curr_cpu = task_cpu(p);
-
-	/*
-	 * Scanning in the preferred NUMA node is ideal. However, the NUMA
-	 * preferred node is per-task rather than per-process. It is possible
-	 * for different threads of the process to have distinct preferred
-	 * nodes; consequently, the process-wide preferred LLC may bounce
-	 * between different nodes. As a workaround, maintain the scan
-	 * CPU mask to also cover the process's current preferred LLC and the
-	 * current running node to mitigate the bouncing risk.
-	 * TBD: numa_group should be considered during task aggregation.
-	 */
-	pref_nid = p->numa_preferred_nid;
-	/* honor the task's preferred node */
-	if (pref_nid == NUMA_NO_NODE)
-		goto out;
-
-	cpumask_or(cpus, cpus, cpumask_of_node(pref_nid));
-
-	/* honor the task's preferred LLC CPU */
-	if (cpu != -1 && !cpumask_test_cpu(cpu, cpus) && nid != NUMA_NO_NODE)
-		cpumask_or(cpus, cpus, cpumask_of_node(nid));
-
-	/* make sure the task's current running node is included */
-	if (!cpumask_test_cpu(curr_cpu, cpus))
-		cpumask_or(cpus, cpus, cpumask_of_node(cpu_to_node(curr_cpu)));
-
-	return;
-
-out:
-#endif
-	cpumask_copy(cpus, cpu_online_mask);
-}
-
 static inline void update_avg_scale(u64 *avg, u64 sample)
 {
 	int factor = per_cpu(sd_llc_size, raw_smp_processor_id());
@@ -1845,7 +1814,7 @@ static void task_cache_work(struct callback_head *work)
 	if (time_before(now, next_scan))
 		return;
 
-	/* only 1 thread is allowed to scan */
+	/* elect a single scanner per epoch */
 	if (!try_cmpxchg(&mm->sc_stat.next_scan, &next_scan,
 			 now + max_t(unsigned long,
 				     READ_ONCE(llc_epoch_period), 1)))
@@ -1866,7 +1835,18 @@ static void task_cache_work(struct callback_head *work)
 	scoped_guard (cpus_read_lock) {
 		guard(rcu)();
 
-		get_scan_cpumasks(cpus, p);
+		/*
+		 * Data race: While evaluating the visited_cpus without
+		 * a lock, a CPU could be concurrently set by
+		 * account_mm_sched(), meaning the scan might skip the newly
+		 * visited CPU if the bit changes during the scan. This is
+		 * a deliberate trade-off between accuracy and efficiency:
+		 * locking would prevent this race but incur extra overhead.
+		 * The missed runtime contribution is negligible because it
+		 * implies this process hasn't run on that CPU for a long
+		 * time, and will be captured in the next cycle.
+		 */
+		cpumask_and(cpus, cpu_online_mask, mm->sc_stat.visited_cpus);
 
 		for_each_cpu(cpu, cpus) {
 			/* XXX sched_cluster_active */
@@ -1877,19 +1857,21 @@ static void task_cache_work(struct callback_head *work)
 			if (!sd)
 				continue;
 
-			for_each_cpu(i, sched_domain_span(sd)) {
-				occ = fraction_mm_sched(cpu_rq(i),
-							per_cpu_ptr(mm->sc_stat.pcpu_sched, i));
+			for_each_cpu_and(i, sched_domain_span(sd), cpus) {
+				cur = rcu_dereference_all(cpu_rq(i)->curr);
+				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
+				    cur->mm == mm)
+					nr_running++;
+
+				occ = fraction_mm_sched(i, mm);
+				if (occ == 0)
+					continue;
+
 				a_occ += occ;
 				if (occ > m_occ) {
 					m_occ = occ;
 					m_cpu = i;
 				}
-
-				cur = rcu_dereference_all(cpu_rq(i)->curr);
-				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
-				    cur->mm == mm)
-					nr_running++;
 			}
 
 			/*
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v9 2/2] -- DO NOT APPLY!!! -- sched/cache/debug: Add trace event and sched feature to track scan cost
  2026-07-31  2:44 [PATCH v9 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work Luo Gengkun
  2026-07-31  2:44 ` [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus Luo Gengkun
@ 2026-07-31  2:44 ` Luo Gengkun
  1 sibling, 0 replies; 7+ messages in thread
From: Luo Gengkun @ 2026-07-31  2:44 UTC (permalink / raw)
  To: mingo, peterz, juri.lelli, vincent.guittot, tim.c.chen, yu.c.chen
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel

This commit re-adds get_scan_cpumasks() and introduces two sched features
as well as trace events to track the scanning cost for testing purposes.

Signed-off-by: Luo Gengkun <luogengkun2@huawei.com>
---
 include/trace/events/sched.h | 21 +++++++++++
 kernel/sched/fair.c          | 73 +++++++++++++++++++++++++++++++++---
 kernel/sched/features.h      |  2 +
 3 files changed, 91 insertions(+), 5 deletions(-)

diff --git a/include/trace/events/sched.h b/include/trace/events/sched.h
index 535860581f15..aced624f198d 100644
--- a/include/trace/events/sched.h
+++ b/include/trace/events/sched.h
@@ -10,6 +10,27 @@
 #include <linux/tracepoint.h>
 #include <linux/binfmts.h>
 
+TRACE_EVENT(sched_cache_scan,
+
+	TP_PROTO(struct task_struct *t, int scan),
+
+	TP_ARGS(t, scan),
+
+	TP_STRUCT__entry(
+		__string(	comm,	t->comm		)
+		__field(	pid_t,	pid		)
+		__field(	int,	scan		)
+	),
+
+	TP_fast_assign(
+		__assign_str(comm);
+		__entry->pid	= t->pid;
+		__entry->scan	= scan;
+	),
+
+	TP_printk("comm=%s pid=%d scan=%d", __get_str(comm), __entry->pid,
+					__entry->scan)
+);
 /*
  * Tracepoint for calling kthread_stop, performed to end a kthread:
  */
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 2bb370b38b79..7cf341bcd8c8 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1648,7 +1648,8 @@ static unsigned long fraction_mm_sched(int cpu,
 	__update_mm_sched(rq, pcpu_sched);
 
 	/* Skip the rq that has not been hit for a long time */
-	if ((rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout) {
+	if (sched_feat(SC_VISIT) &&
+	    (rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout) {
 		cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
 		return 0;
 	}
@@ -1723,7 +1724,8 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
 		rq->cpu_runtime += delta_exec;
 		epoch = rq->cpu_epoch;
 		pcpu_sched->epoch_last_visit = epoch;
-		if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
+		if (sched_feat(SC_VISIT) &&
+		    !cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
 			cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
 	}
 
@@ -1775,6 +1777,51 @@ static void task_tick_cache(struct rq *rq, struct task_struct *p)
 	}
 }
 
+static void get_scan_cpumasks(cpumask_var_t cpus, struct task_struct *p)
+{
+#ifdef CONFIG_NUMA_BALANCING
+	int cpu, curr_cpu, nid, pref_nid;
+
+	if (!static_branch_likely(&sched_numa_balancing))
+		goto out;
+
+	cpu = READ_ONCE(p->mm->sc_stat.cpu);
+	if (cpu != -1)
+		nid = cpu_to_node(cpu);
+	curr_cpu = task_cpu(p);
+
+	/*
+	 * Scanning in the preferred NUMA node is ideal. However, the NUMA
+	 * preferred node is per-task rather than per-process. It is possible
+	 * for different threads of the process to have distinct preferred
+	 * nodes; consequently, the process-wide preferred LLC may bounce
+	 * between different nodes. As a workaround, maintain the scan
+	 * CPU mask to also cover the process's current preferred LLC and the
+	 * current running node to mitigate the bouncing risk.
+	 * TBD: numa_group should be considered during task aggregation.
+	 */
+	pref_nid = p->numa_preferred_nid;
+	/* honor the task's preferred node */
+	if (pref_nid == NUMA_NO_NODE)
+		goto out;
+
+	cpumask_or(cpus, cpus, cpumask_of_node(pref_nid));
+
+	/* honor the task's preferred LLC CPU */
+	if (cpu != -1 && !cpumask_test_cpu(cpu, cpus) && nid != NUMA_NO_NODE)
+		cpumask_or(cpus, cpus, cpumask_of_node(nid));
+
+	/* make sure the task's current running node is included */
+	if (!cpumask_test_cpu(curr_cpu, cpus))
+		cpumask_or(cpus, cpus, cpumask_of_node(cpu_to_node(curr_cpu)));
+
+	return;
+
+out:
+#endif
+	cpumask_copy(cpus, cpu_online_mask);
+}
+
 static inline void update_avg_scale(u64 *avg, u64 sample)
 {
 	int factor = per_cpu(sd_llc_size, raw_smp_processor_id());
@@ -1801,7 +1848,8 @@ static void task_cache_work(struct callback_head *work)
 	unsigned long curr_m_a_occ = 0;
 	struct mm_struct *mm = p->mm;
 	unsigned long m_a_occ = 0;
-	cpumask_var_t cpus;
+	cpumask_var_t cpus, scan_cpus;
+	int scanned = 0;
 
 	WARN_ON_ONCE(work != &p->cache_work);
 
@@ -1832,6 +1880,11 @@ static void task_cache_work(struct callback_head *work)
 	if (!zalloc_cpumask_var(&cpus, GFP_KERNEL))
 		return;
 
+	if (!zalloc_cpumask_var(&scan_cpus, GFP_KERNEL)) {
+		free_cpumask_var(cpus);
+		return;
+	}
+
 	scoped_guard (cpus_read_lock) {
 		guard(rcu)();
 
@@ -1846,7 +1899,10 @@ static void task_cache_work(struct callback_head *work)
 		 * implies this process hasn't run on that CPU for a long
 		 * time, and will be captured in the next cycle.
 		 */
-		cpumask_and(cpus, cpu_online_mask, mm->sc_stat.visited_cpus);
+		if (sched_feat(SC_NODE))
+			get_scan_cpumasks(cpus, p);
+		else if (sched_feat(SC_VISIT))
+			cpumask_and(cpus, cpu_online_mask, mm->sc_stat.visited_cpus);
 
 		for_each_cpu(cpu, cpus) {
 			/* XXX sched_cluster_active */
@@ -1857,12 +1913,17 @@ static void task_cache_work(struct callback_head *work)
 			if (!sd)
 				continue;
 
-			for_each_cpu_and(i, sched_domain_span(sd), cpus) {
+			cpumask_copy(scan_cpus, sched_domain_span(sd));
+			if (sched_feat(SC_VISIT))
+				cpumask_and(scan_cpus, scan_cpus, cpus);
+
+			for_each_cpu(i, scan_cpus) {
 				cur = rcu_dereference_all(cpu_rq(i)->curr);
 				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
 				    cur->mm == mm)
 					nr_running++;
 
+				scanned++;
 				occ = fraction_mm_sched(i, mm);
 				if (occ == 0)
 					continue;
@@ -1917,6 +1978,8 @@ static void task_cache_work(struct callback_head *work)
 
 	update_avg_scale(&mm->sc_stat.nr_running_avg, nr_running);
 	free_cpumask_var(cpus);
+	free_cpumask_var(scan_cpus);
+	trace_sched_cache_scan(p, scanned);
 }
 
 void init_sched_mm(struct task_struct *p)
diff --git a/kernel/sched/features.h b/kernel/sched/features.h
index 8f0dee8fc475..98701c52b919 100644
--- a/kernel/sched/features.h
+++ b/kernel/sched/features.h
@@ -142,3 +142,5 @@ SCHED_FEAT(LATENCY_WARN, false)
  */
 SCHED_FEAT(NI_RANDOM, true)
 SCHED_FEAT(NI_RATE, true)
+SCHED_FEAT(SC_VISIT, false)
+SCHED_FEAT(SC_NODE, false)
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus
  2026-07-31  2:44 ` [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus Luo Gengkun
@ 2026-08-11  2:27   ` Luo Gengkun
  2026-08-11  7:46     ` Chen, Yu C
  0 siblings, 1 reply; 7+ messages in thread
From: Luo Gengkun @ 2026-08-11  2:27 UTC (permalink / raw)
  To: peterz
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel, juri.lelli, vincent.guittot,
	tim.c.chen, yu.c.chen, mingo



On 2026/7/31 10:44, Luo Gengkun wrote:
> The overhead of task_cache_work() is high, especially in multi-NUMA systems.
> Currently, task_cache_work() tries to find the pref_llc by scanning all CPUs
> in the system. However, most of these scans are meaningless, such as those
> for CPUs that have never been visited or were accessed a long time ago.
> 
> To address this problem, introduce visited_cpus to track the visited CPUs
> and evict them once they have not been accessed for a duration exceeding
> llc_epoch_affinity_timeout.
> 
> Now that we know exactly which CPUs to scan from visited_cpus, we can remove
> get_scan_cpumasks().
> 
> Tested-by: Chen Yu <yu.c.chen@intel.com>
> Reviewed-by: Tim Chen <tim.c.chen@linux.intel.com>
> Signed-off-by: Luo Gengkun <luogengkun2@huawei.com>
Hi Peter,

Friendly ping on this patch.

It has accumulated Reviewed-by and Tested-by tags, and there are no further
comments. Please let me know your thoughts when you have a moment.

Thanks,
Gengkun.
> ---
>   include/linux/mm_types.h |  6 +++
>   include/linux/sched.h    |  2 +
>   kernel/sched/fair.c      | 96 ++++++++++++++++------------------------
>   3 files changed, 47 insertions(+), 57 deletions(-)
> 
> diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
> index b18c2b2e7d2c..35559079e4d4 100644
> --- a/include/linux/mm_types.h
> +++ b/include/linux/mm_types.h
> @@ -1620,6 +1620,11 @@ static inline int mm_alloc_sched_noprof(struct mm_struct *mm)
>   	if (!pcpu_sched)
>   		return -ENOMEM;
>   
> +	if (!zalloc_cpumask_var(&mm->sc_stat.visited_cpus, GFP_KERNEL)) {
> +		free_percpu(pcpu_sched);
> +		return -ENOMEM;
> +	}
> +
>   	mm_init_sched(mm, pcpu_sched);
>   	return 0;
>   }
> @@ -1630,6 +1635,7 @@ static inline void mm_destroy_sched(struct mm_struct *mm)
>   {
>   	free_percpu(mm->sc_stat.pcpu_sched);
>   	mm->sc_stat.pcpu_sched = NULL;
> +	free_cpumask_var(mm->sc_stat.visited_cpus);
>   }
>   #else /* !CONFIG_SCHED_CACHE */
>   
> diff --git a/include/linux/sched.h b/include/linux/sched.h
> index 373bcc0598d1..b461a71a65da 100644
> --- a/include/linux/sched.h
> +++ b/include/linux/sched.h
> @@ -2388,6 +2388,7 @@ static __always_inline int task_mm_cid(struct task_struct *t)
>   struct sched_cache_time {
>   	u64 runtime;
>   	unsigned long epoch;
> +	unsigned long epoch_last_visit;
>   };
>   
>   struct sched_cache_stat {
> @@ -2398,6 +2399,7 @@ struct sched_cache_stat {
>   	unsigned long next_scan;
>   	unsigned long footprint;
>   	int cpu;
> +	cpumask_var_t visited_cpus;
>   } ____cacheline_aligned_in_smp;
>   
>   #else
> diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
> index d78467ec6ee1..2bb370b38b79 100644
> --- a/kernel/sched/fair.c
> +++ b/kernel/sched/fair.c
> @@ -1585,6 +1585,7 @@ void mm_init_sched(struct mm_struct *mm,
>   		pcpu_sched->runtime = 0;
>   		/* a slightly stale cpu epoch is acceptible */
>   		pcpu_sched->epoch = rq->cpu_epoch;
> +		pcpu_sched->epoch_last_visit = rq->cpu_epoch;
>   		epoch = rq->cpu_epoch;
>   	}
>   
> @@ -1635,13 +1636,23 @@ static inline void __update_mm_sched(struct rq *rq,
>   	}
>   }
>   
> -static unsigned long fraction_mm_sched(struct rq *rq,
> -				       struct sched_cache_time *pcpu_sched)
> +static unsigned long fraction_mm_sched(int cpu,
> +				       struct mm_struct *mm)
>   {
> +	struct sched_cache_time *pcpu_sched =
> +		per_cpu_ptr(mm->sc_stat.pcpu_sched, cpu);
> +	struct rq *rq = cpu_rq(cpu);
> +
>   	guard(raw_spinlock_irqsave)(&rq->cpu_epoch_lock);
>   
>   	__update_mm_sched(rq, pcpu_sched);
>   
> +	/* Skip the rq that has not been hit for a long time */
> +	if ((rq->cpu_epoch - pcpu_sched->epoch_last_visit) > llc_epoch_affinity_timeout) {
> +		cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
> +		return 0;
> +	}
> +
>   	/*
>   	 * Runtime is a geometric series (r=0.5) and as such will sum to twice
>   	 * the accumulation period, this means the multiplcation here should
> @@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
>   		pcpu_sched->runtime += delta_exec;
>   		rq->cpu_runtime += delta_exec;
>   		epoch = rq->cpu_epoch;
> +		pcpu_sched->epoch_last_visit = epoch;
> +		if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
> +			cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
>   	}
>   
>   	/*
> @@ -1761,51 +1775,6 @@ static void task_tick_cache(struct rq *rq, struct task_struct *p)
>   	}
>   }
>   
> -static void get_scan_cpumasks(cpumask_var_t cpus, struct task_struct *p)
> -{
> -#ifdef CONFIG_NUMA_BALANCING
> -	int cpu, curr_cpu, nid, pref_nid;
> -
> -	if (!static_branch_likely(&sched_numa_balancing))
> -		goto out;
> -
> -	cpu = READ_ONCE(p->mm->sc_stat.cpu);
> -	if (cpu != -1)
> -		nid = cpu_to_node(cpu);
> -	curr_cpu = task_cpu(p);
> -
> -	/*
> -	 * Scanning in the preferred NUMA node is ideal. However, the NUMA
> -	 * preferred node is per-task rather than per-process. It is possible
> -	 * for different threads of the process to have distinct preferred
> -	 * nodes; consequently, the process-wide preferred LLC may bounce
> -	 * between different nodes. As a workaround, maintain the scan
> -	 * CPU mask to also cover the process's current preferred LLC and the
> -	 * current running node to mitigate the bouncing risk.
> -	 * TBD: numa_group should be considered during task aggregation.
> -	 */
> -	pref_nid = p->numa_preferred_nid;
> -	/* honor the task's preferred node */
> -	if (pref_nid == NUMA_NO_NODE)
> -		goto out;
> -
> -	cpumask_or(cpus, cpus, cpumask_of_node(pref_nid));
> -
> -	/* honor the task's preferred LLC CPU */
> -	if (cpu != -1 && !cpumask_test_cpu(cpu, cpus) && nid != NUMA_NO_NODE)
> -		cpumask_or(cpus, cpus, cpumask_of_node(nid));
> -
> -	/* make sure the task's current running node is included */
> -	if (!cpumask_test_cpu(curr_cpu, cpus))
> -		cpumask_or(cpus, cpus, cpumask_of_node(cpu_to_node(curr_cpu)));
> -
> -	return;
> -
> -out:
> -#endif
> -	cpumask_copy(cpus, cpu_online_mask);
> -}
> -
>   static inline void update_avg_scale(u64 *avg, u64 sample)
>   {
>   	int factor = per_cpu(sd_llc_size, raw_smp_processor_id());
> @@ -1845,7 +1814,7 @@ static void task_cache_work(struct callback_head *work)
>   	if (time_before(now, next_scan))
>   		return;
>   
> -	/* only 1 thread is allowed to scan */
> +	/* elect a single scanner per epoch */
>   	if (!try_cmpxchg(&mm->sc_stat.next_scan, &next_scan,
>   			 now + max_t(unsigned long,
>   				     READ_ONCE(llc_epoch_period), 1)))
> @@ -1866,7 +1835,18 @@ static void task_cache_work(struct callback_head *work)
>   	scoped_guard (cpus_read_lock) {
>   		guard(rcu)();
>   
> -		get_scan_cpumasks(cpus, p);
> +		/*
> +		 * Data race: While evaluating the visited_cpus without
> +		 * a lock, a CPU could be concurrently set by
> +		 * account_mm_sched(), meaning the scan might skip the newly
> +		 * visited CPU if the bit changes during the scan. This is
> +		 * a deliberate trade-off between accuracy and efficiency:
> +		 * locking would prevent this race but incur extra overhead.
> +		 * The missed runtime contribution is negligible because it
> +		 * implies this process hasn't run on that CPU for a long
> +		 * time, and will be captured in the next cycle.
> +		 */
> +		cpumask_and(cpus, cpu_online_mask, mm->sc_stat.visited_cpus);
>   
>   		for_each_cpu(cpu, cpus) {
>   			/* XXX sched_cluster_active */
> @@ -1877,19 +1857,21 @@ static void task_cache_work(struct callback_head *work)
>   			if (!sd)
>   				continue;
>   
> -			for_each_cpu(i, sched_domain_span(sd)) {
> -				occ = fraction_mm_sched(cpu_rq(i),
> -							per_cpu_ptr(mm->sc_stat.pcpu_sched, i));
> +			for_each_cpu_and(i, sched_domain_span(sd), cpus) {
> +				cur = rcu_dereference_all(cpu_rq(i)->curr);
> +				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
> +				    cur->mm == mm)
> +					nr_running++;
> +
> +				occ = fraction_mm_sched(i, mm);
> +				if (occ == 0)
> +					continue;
> +
>   				a_occ += occ;
>   				if (occ > m_occ) {
>   					m_occ = occ;
>   					m_cpu = i;
>   				}
> -
> -				cur = rcu_dereference_all(cpu_rq(i)->curr);
> -				if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
> -				    cur->mm == mm)
> -					nr_running++;
>   			}
>   
>   			/*

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus
  2026-08-11  2:27   ` Luo Gengkun
@ 2026-08-11  7:46     ` Chen, Yu C
  2026-08-12  9:03       ` Luo Gengkun
  0 siblings, 1 reply; 7+ messages in thread
From: Chen, Yu C @ 2026-08-11  7:46 UTC (permalink / raw)
  To: Luo Gengkun
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel, juri.lelli, vincent.guittot,
	tim.c.chen, mingo, peterz, chen.yu@linux.dev, Jianyong Wu

Hi Gengkun,
On 8/11/2026 10:27 AM, Luo Gengkun wrote:

I did a further study on this and have two minor questions:

>> @@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct 
>> task_struct *p, s64 delta_exec)
>>           pcpu_sched->runtime += delta_exec;
>>           rq->cpu_runtime += delta_exec;
>>           epoch = rq->cpu_epoch;
>> +        pcpu_sched->epoch_last_visit = epoch;
>> +        if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
>> +            cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);

visited_cpus bits are only cleared inside fraction_mm_sched(), which is
reachable in task_cache_work() - but that loop is skipped when 
invalid_llc_nr()
returns true for any single-threaded process. As a result, single-threaded
processes keep setting bits in account_mm_sched() without using them.
Maybe a gate would be useful:

if (get_nr_threads(p) > 1 &&
     !cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
	cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);


[ ... ]

>> @@ -1866,7 +1835,18 @@ static void task_cache_work(struct 
>> callback_head *work)
>>       scoped_guard (cpus_read_lock) {
>>           guard(rcu)();
>> -        get_scan_cpumasks(cpus, p);

I'm thinking of if this could bring cross-node bouncing. Is it doable
to honor the result from NUMA preference:
	get_scan_cpumasks(cpus, p);
	cpumask_and(cpus, cpus, mm->sc_stat.visited_cpus);

thanks,
Chenyu

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus
  2026-08-11  7:46     ` Chen, Yu C
@ 2026-08-12  9:03       ` Luo Gengkun
  2026-08-18 14:41         ` Chen, Yu C
  0 siblings, 1 reply; 7+ messages in thread
From: Luo Gengkun @ 2026-08-12  9:03 UTC (permalink / raw)
  To: Chen, Yu C
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel, juri.lelli, vincent.guittot,
	tim.c.chen, mingo, peterz, chen.yu@linux.dev, Jianyong Wu



On 2026/8/11 15:46, Chen, Yu C wrote:
> Hi Gengkun,
> On 8/11/2026 10:27 AM, Luo Gengkun wrote:
> 
> I did a further study on this and have two minor questions:
> 
>>> @@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
>>>           pcpu_sched->runtime += delta_exec;
>>>           rq->cpu_runtime += delta_exec;
>>>           epoch = rq->cpu_epoch;
>>> +        pcpu_sched->epoch_last_visit = epoch;
>>> +        if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
>>> +            cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
> 
> visited_cpus bits are only cleared inside fraction_mm_sched(), which is
> reachable in task_cache_work() - but that loop is skipped when invalid_llc_nr()
> returns true for any single-threaded process. As a result, single-threaded
> processes keep setting bits in account_mm_sched() without using them.
> Maybe a gate would be useful:
 From a technical perspective, adding a gate here is unnecessary.

The overhead is virtually nonexistent, especially since it resides on a path
already burdened by heavier operations like __update_mm_sched(). If we were
to care about performance and optimization, focusing on __update_mm_sched()
would be far more meaningful than adding checks here.

What do you think?
> 
> if (get_nr_threads(p) > 1 &&
>      !cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
>      cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
> 
> 
> [ ... ]
> 
>>> @@ -1866,7 +1835,18 @@ static void task_cache_work(struct callback_head *work)
>>>       scoped_guard (cpus_read_lock) {
>>>           guard(rcu)();
>>> -        get_scan_cpumasks(cpus, p);
> 
> I'm thinking of if this could bring cross-node bouncing. Is it doable
> to honor the result from NUMA preference:
>      get_scan_cpumasks(cpus, p);
>      cpumask_and(cpus, cpus, mm->sc_stat.visited_cpus);

I looked closely at get_scan_cpumasks(). The CPU mask it returns is the
union of node(p->numa_preferred_nid), node(mm->sc_stat.cpu), and node(task_cpu(p)).
Its purpose was only to mitigate sc_stat.cpu bouncing — it did not fully
eliminate it. Relying on visited_cpus alone follows the actual footprint
of where the threads really ran, making it even less prone to bouncing.

Additionally, even if the numa node derived from visited_cpus disagrees with a
given thread's numa_preferred_nid, get_pref_llc() still prevents that
thread from being migrated to that node, so it remains safe either way.

Please let me know if I'm missing something.

thanks,
Gengkun
> 
> thanks,
> Chenyu
> 

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus
  2026-08-12  9:03       ` Luo Gengkun
@ 2026-08-18 14:41         ` Chen, Yu C
  0 siblings, 0 replies; 7+ messages in thread
From: Chen, Yu C @ 2026-08-18 14:41 UTC (permalink / raw)
  To: Luo Gengkun
  Cc: dietmar.eggemann, rostedt, bsegall, mgorman, vschneid,
	kprateek.nayak, linux-kernel, juri.lelli, vincent.guittot,
	tim.c.chen, mingo, peterz, chen.yu@linux.dev, Jianyong Wu

On 8/12/2026 5:03 PM, Luo Gengkun wrote:
> 
> 
> On 2026/8/11 15:46, Chen, Yu C wrote:
>> Hi Gengkun,
>> On 8/11/2026 10:27 AM, Luo Gengkun wrote:
>>
>> I did a further study on this and have two minor questions:
>>
>>>> @@ -1711,6 +1722,9 @@ void account_mm_sched(struct rq *rq, struct 
>>>> task_struct *p, s64 delta_exec)
>>>>           pcpu_sched->runtime += delta_exec;
>>>>           rq->cpu_runtime += delta_exec;
>>>>           epoch = rq->cpu_epoch;
>>>> +        pcpu_sched->epoch_last_visit = epoch;
>>>> +        if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
>>>> +            cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
>>
>> visited_cpus bits are only cleared inside fraction_mm_sched(), which is
>> reachable in task_cache_work() - but that loop is skipped when 
>> invalid_llc_nr()
>> returns true for any single-threaded process. As a result, single- 
>> threaded
>> processes keep setting bits in account_mm_sched() without using them.
>> Maybe a gate would be useful:
>  From a technical perspective, adding a gate here is unnecessary.
> 
> The overhead is virtually nonexistent, especially since it resides on a 
> path
> already burdened by heavier operations like __update_mm_sched(). If we were
> to care about performance and optimization, focusing on __update_mm_sched()
> would be far more meaningful than adding checks here.
> 
> What do you think?

Makes sense. I was previously worried about the multiple accesses to the
shared mm->variable, but since it's only a single thread with no race, it
should be fine.

>>
>> if (get_nr_threads(p) > 1 &&
>>      !cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
>>      cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
>>
>>
>> [ ... ]
>>
>>>> @@ -1866,7 +1835,18 @@ static void task_cache_work(struct 
>>>> callback_head *work)
>>>>       scoped_guard (cpus_read_lock) {
>>>>           guard(rcu)();
>>>> -        get_scan_cpumasks(cpus, p);
>>
>> I'm thinking of if this could bring cross-node bouncing. Is it doable
>> to honor the result from NUMA preference:
>>      get_scan_cpumasks(cpus, p);
>>      cpumask_and(cpus, cpus, mm->sc_stat.visited_cpus);
> 
> I looked closely at get_scan_cpumasks(). The CPU mask it returns is the
> union of node(p->numa_preferred_nid), node(mm->sc_stat.cpu), and 
> node(task_cpu(p)).
> Its purpose was only to mitigate sc_stat.cpu bouncing — it did not fully
> eliminate it. Relying on visited_cpus alone follows the actual footprint
> of where the threads really ran, making it even less prone to bouncing.
> 
> Additionally, even if the numa node derived from visited_cpus disagrees 
> with a
> given thread's numa_preferred_nid, get_pref_llc() still prevents that
> thread from being migrated to that node, so it remains safe either way.
> 
> Please let me know if I'm missing something.
> 

I'm not against removing the node-aware-scan-first strategy, as long
as it doesn't cause any bouncing regression. I also heard from Jianyong
who is working on extending the CAS from a single-preferred LLC to
multiple-preferred LLCs, that removing the NUMA-based scan would benefit
that work. So, your change on this is fine with me.

thanks,
Chenyu

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-08-18 14:42 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-31  2:44 [PATCH v9 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work Luo Gengkun
2026-07-31  2:44 ` [PATCH v9 1/2] sched/cache: Reduce the overhead of task_cache_work by only scan the visisted cpus Luo Gengkun
2026-08-11  2:27   ` Luo Gengkun
2026-08-11  7:46     ` Chen, Yu C
2026-08-12  9:03       ` Luo Gengkun
2026-08-18 14:41         ` Chen, Yu C
2026-07-31  2:44 ` [PATCH v9 2/2] -- DO NOT APPLY!!! -- sched/cache/debug: Add trace event and sched feature to track scan cost Luo Gengkun

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.