* [RFC PATCH 1/3] vmscan: Support multiple kswapd threads per node
2026-09-08 22:10 [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Ehab Ababneh
@ 2026-09-08 22:10 ` Ehab Ababneh
2026-09-08 22:10 ` [RFC PATCH 2/3] mm/vmscan: handle racing max_seq advancement Ehab Ababneh
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Ehab Ababneh @ 2026-09-08 22:10 UTC (permalink / raw)
To: linux-mm, linux-kernel
Cc: Buddy Lumpkin, Andrew Morton, Kairui Song, Qi Zheng,
Shakeel Butt, Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Brendan Jackman, Johannes Weiner, Zi Yan
From: Buddy Lumpkin <buddy.lumpkin@oracle.com>
Page replacement is handled in the Linux Kernel in one of two ways:
1) Asynchronously via kswapd
2) Synchronously, via direct reclaim
At page allocation time the allocating task is immediately given a page
from the zone free list allowing it to go right back to work doing
whatever it was doing; Probably directly or indirectly executing business
logic.
Just prior to satisfying the allocation, free pages is checked to see if
it has reached the zone low watermark and if so, kswapd is awakened.
Kswapd will start scanning pages looking for inactive pages to evict to
make room for new page allocations. The work of kswapd allows tasks to
continue allocating memory from their respective zone free list without
incurring any delay.
When the demand for free pages exceeds the rate that kswapd tasks can
supply them, page allocation works differently. Once the allocating task
finds that the number of free pages is at or below the zone min watermark,
the task will no longer pull pages from the free list. Instead, the task
will run the same CPU-bound routines as kswapd to satisfy its own
allocation by scanning and evicting pages. This is called a direct reclaim.
The time spent performing a direct reclaim can be substantial, often
taking tens to hundreds of milliseconds for small order0 allocations to
half a second or more for order9 huge-page allocations. In fact, kswapd is
not actually required on a linux system. It exists for the sole purpose of
optimizing performance by preventing direct reclaims.
When memory shortfall is sufficient to trigger direct reclaims, they can
occur in any task that is running on the system. A single aggressive
memory allocating task can set the stage for collateral damage to occur in
small tasks that rarely allocate additional memory. Consider the impact of
injecting an additional 100ms of latency when nscd allocates memory to
facilitate caching of a DNS query.
The presence of direct reclaims 10 years ago was a fairly reliable
indicator that too much was being asked of a Linux system. Kswapd was
likely wasting time scanning pages that were ineligible for eviction.
Adding RAM or reducing the working set size would usually make the problem
go away. Since then hardware has evolved to bring a new struggle for
kswapd. Storage speeds have increased by orders of magnitude while CPU
clock speeds stayed the same or even slowed down in exchange for more
cores per package. This presents a throughput problem for a single
threaded kswapd that will get worse with each generation of new hardware.
Test Details
NOTE: The tests below were run with shadow entries disabled. See the
associated patch and cover letter for details
The tests below were designed with the assumption that a kswapd bottleneck
is best demonstrated using filesystem reads. This way, the inactive list
will be full of clean pages, simplifying the analysis and allowing kswapd
to achieve the highest possible steal rate. Maximum steal rates for kswapd
are likely to be the same or lower for any other mix of page types on the
system.
Tests were run on a 2U Oracle X7-2L with 52 Intel Xeon Skylake 2GHz cores,
756GB of RAM and 8 x 3.6 TB NVMe Solid State Disk drives. Each drive has
an XFS file system mounted separately as /d0 through /d7. SSD drives
require multiple concurrent streams to show their potential, so I created
eleven 250GB zero-filled files on each drive so that I could test with
parallel reads.
The test script runs in multiple stages. At each stage, the number of dd
tasks run concurrently is increased by 2. I did not include all of the
test output for brevity.
During each stage dd tasks are launched to read from each drive in a round
robin fashion until the specified number of tasks for the stage has been
reached. Then iostat, vmstat and top are started in the background with 10
second intervals. After five minutes, all of the dd tasks are killed and
the iostat, vmstat and top output is parsed in order to report the
following:
CPU consumption
- sy - aggregate kernel mode CPU consumption from vmstat output. The value
doesn't tend to fluctuate much so I just grab the highest value.
Each sample is averaged over 10 seconds
- dd_cpu - for all of the dd tasks averaged across the top samples since
there is a lot of variation.
Throughput
- in Kbytes
- Command is iostat -x -d 10 -g total
This first test performs reads using O_DIRECT in order to show the maximum
throughput that can be obtained using these drives. It also demonstrates
how rapidly throughput scales as the number of dd tasks are increased.
The dd command for this test looks like this:
Command Used: dd iflag=direct if=/d${i}/$n of=/dev/null bs=4M
Test #1: Direct IO
dd sy dd_cpu throughput
6 0 2.33 14726026.40
10 1 2.95 19954974.80
16 1 2.63 24419689.30
22 1 2.63 25430303.20
28 1 2.91 26026513.20
34 1 2.53 26178618.00
40 1 2.18 26239229.20
46 1 1.91 26250550.40
52 1 1.69 26251845.60
58 1 1.54 26253205.60
64 1 1.43 26253780.80
70 1 1.31 26254154.80
76 1 1.21 26253660.80
82 1 1.12 26254214.80
88 1 1.07 26253770.00
90 1 1.04 26252406.40
Throughput was close to peak with only 22 dd tasks. Very little system CPU
was consumed as expected as the drives DMA directly into the user address
space when using direct IO.
In this next test, the iflag=direct option is removed and we only run the
test until the pgscan_kswapd from /proc/vmstat starts to increment. At
that point metrics are parsed and reported and the pagecache contents are
dropped prior to the next test. Lather, rinse, repeat.
Test #2: standard file system IO, no page replacement
dd sy dd_cpu throughput
6 2 28.78 5134316.40
10 3 31.40 8051218.40
16 5 34.73 11438106.80
22 7 33.65 14140596.40
28 8 31.24 16393455.20
34 10 29.88 18219463.60
40 11 28.33 19644159.60
46 11 25.05 20802497.60
52 13 26.92 22092370.00
58 13 23.29 22884881.20
64 14 23.12 23452248.80
70 15 22.40 23916468.00
76 16 22.06 24328737.20
82 17 20.97 24718693.20
88 16 18.57 25149404.40
90 16 18.31 25245565.60
Each read has to pause after the buffer in kernel space is populated while
those pages are added to the pagecache and copied into the user address
space. For this reason, more parallel streams are required to achieve peak
throughput. The copy operation consumes substantially more CPU than direct
IO as expected.
The next test measures throughput after kswapd starts running. This is the
same test only we wait for kswapd to wake up before we start collecting
metrics. The script actually keeps track of a few things that were not
mentioned earlier. It tracks direct reclaims and page scans by watching
the metrics in /proc/vmstat. CPU consumption for kswapd is tracked the
same way it is tracked for dd.
Since the test is 100% reads, you can assume that the page steal rate for
kswapd and direct reclaims is almost identical to the scan rate.
Test #3: 1 kswapd thread per node
dd sy dd_cpu kswapd0 kswapd1 throughput dr pgscan_kswapd pgscan_direct
10 4 26.07 28.56 27.03 7355924.40 0 459316976 0
16 7 34.94 69.33 69.66 10867895.20 0 872661643 0
22 10 36.03 93.99 99.33 13130613.60 489 1037654473 11268334
28 10 30.34 95.90 98.60 14601509.60 671 1182591373 15429142
34 14 34.77 97.50 99.23 16468012.00 10850 1069005644 249839515
40 17 36.32 91.49 97.11 17335987.60 18903 975417728 434467710
46 19 38.40 90.54 91.61 17705394.40 25369 855737040 582427973
52 22 40.88 83.97 83.70 17607680.40 31250 709532935 724282458
58 25 40.89 82.19 80.14 17976905.60 35060 657796473 804117540
64 28 41.77 73.49 75.20 18001910.00 39073 561813658 895289337
70 33 45.51 63.78 64.39 17061897.20 44523 379465571 1020726436
76 36 46.95 57.96 60.32 16964459.60 47717 291299464 1093172384
82 39 47.16 55.43 56.16 16949956.00 49479 247071062 1134163008
88 42 47.41 53.75 47.62 16930911.20 51521 195449924 1180442208
90 43 47.18 51.40 50.59 16864428.00 51618 190758156 1183203901
In the previous test where kswapd was not involved, the system-wide kernel
mode CPU consumption with 90 dd tasks was 16%. In this test CPU consumption
with 90 tasks is at 43%. With 52 cores, and two kswapd tasks (one per NUMA
node), kswapd can only be responsible for a little over 4% of the increase.
The rest is likely caused by 51,618 direct reclaims that scanned 1.2
billion pages over the five minute time period of the test.
Same test, more kswapd tasks:
Test #4: 4 kswapd threads per node
dd sy dd_cpu kswapd0 kswapd1 throughput dr pgscan_kswapd pgscan_direct
10 5 27.09 16.65 14.17 7842605.60 0 459105291 0
16 10 37.12 26.02 24.85 11352920.40 15 920527796 358515
22 11 36.94 37.13 35.82 13771869.60 0 1132169011 0
28 13 35.23 48.43 46.86 16089746.00 0 1312902070 0
34 15 33.37 53.02 55.69 18314856.40 0 1476169080 0
40 19 35.90 69.60 64.41 19836126.80 0 1629999149 0
46 22 36.82 88.55 57.20 20740216.40 0 1708478106 0
52 24 34.38 93.76 68.34 21758352.00 0 1794055559 0
58 24 30.51 79.20 82.33 22735594.00 0 1872794397 0
64 26 30.21 97.12 76.73 23302203.60 176 1916593721 4206821
70 33 32.92 92.91 92.87 23776588.00 3575 1817685086 85574159
76 37 31.62 91.20 89.83 24308196.80 4752 1812262569 113981763
82 29 25.53 93.23 92.33 24802791.20 306 2032093122 7350704
88 43 37.12 76.18 77.01 25145694.40 20310 1253204719 487048202
90 42 38.56 73.90 74.57 22516787.60 22774 1193637495 545463615
By increasing the number of kswapd threads, throughput increased by ~50%
while kernel mode CPU utilization decreased or stayed the same, likely due
to a decrease in the number of parallel tasks at any given time doing page
replacement.
Signed-off-by: Buddy Lumpkin <buddy.lumpkin@oracle.com>
Signed-off-by: Ehab Ababneh <ehab.ababneh@intel.com>
---
include/linux/mmzone.h | 5 +-
mm/compaction.c | 8 +++-
mm/internal.h | 3 ++
mm/page_alloc.c | 26 +++++++++++
mm/vmscan.c | 104 +++++++++++++++++++++++++++++++++++------
5 files changed, 128 insertions(+), 18 deletions(-)
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index ca2712187147..5b06a6bc9c82 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -62,6 +62,8 @@
*/
#define PAGE_ALLOC_COSTLY_ORDER 3
+#define MAX_KSWAPD_THREADS 16
+
#if !defined(CONFIG_HAVE_GIGANTIC_FOLIOS)
/*
* We don't expect any folios that exceed buddy sizes (and consequently
@@ -1516,7 +1518,8 @@ typedef struct pglist_data {
#ifdef CONFIG_MEMORY_HOTPLUG
struct mutex kswapd_lock;
#endif
- struct task_struct *kswapd; /* Protected by kswapd_lock */
+ /* Protected by kswapd_lock */
+ struct task_struct *kswapd[MAX_KSWAPD_THREADS];
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;
diff --git a/mm/compaction.c b/mm/compaction.c
index f08765ade014..e72112e2716e 100644
--- a/mm/compaction.c
+++ b/mm/compaction.c
@@ -2168,9 +2168,13 @@ static isolate_migrate_t isolate_migratepages(struct compact_control *cc)
static bool kswapd_is_running(pg_data_t *pgdat)
{
bool running;
-
+ int i;
pgdat_kswapd_lock(pgdat);
- running = pgdat->kswapd && task_is_running(pgdat->kswapd);
+ for (i = 0; i < MAX_KSWAPD_THREADS; ++i) {
+ running = pgdat->kswapd[i] && task_is_running(pgdat->kswapd[i]);
+ if (running)
+ break;
+ }
pgdat_kswapd_unlock(pgdat);
return running;
diff --git a/mm/internal.h b/mm/internal.h
index 181e79f1d6a2..1c4cc3b7492b 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -645,10 +645,13 @@ extern char * const zone_names[MAX_NR_ZONES];
/* perform sanity checks on struct pages being allocated or freed */
DECLARE_STATIC_KEY_MAYBE(CONFIG_DEBUG_VM, check_pages_enabled);
+extern int kswapd_threads;
+extern int max_kswapd_threads;
extern int min_free_kbytes;
extern int defrag_mode;
void setup_per_zone_wmarks(void);
+void update_kswapd_threads(void);
void calculate_min_free_kbytes(void);
int __meminit init_per_zone_wmark_min(void);
void page_alloc_sysctl_init(void);
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index ee902a468c2f..abecf96a599b 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -273,6 +273,7 @@ int user_min_free_kbytes = -1;
static int watermark_boost_factor __read_mostly = 15000;
static int watermark_scale_factor = 10;
int defrag_mode;
+int max_kswapd_threads = MAX_KSWAPD_THREADS;
/* movable_zone is the "real" zone pages in ZONE_MOVABLE are taken from */
int movable_zone;
@@ -6614,6 +6615,22 @@ static int min_free_kbytes_sysctl_handler(const struct ctl_table *table, int wri
return 0;
}
+static int kswapd_threads_sysctl_handler(const struct ctl_table *table,
+ int write, void __user *buffer,
+ size_t *length, loff_t *ppos)
+{
+ int rc;
+
+ rc = proc_dointvec_minmax(table, write, buffer, length, ppos);
+ if (rc)
+ return rc;
+
+ if (write)
+ update_kswapd_threads();
+
+ return 0;
+}
+
static int watermark_scale_factor_sysctl_handler(const struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
@@ -6776,6 +6793,15 @@ static const struct ctl_table page_alloc_sysctl_table[] = {
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO,
},
+ {
+ .procname = "kswapd_threads",
+ .data = &kswapd_threads,
+ .maxlen = sizeof(kswapd_threads),
+ .mode = 0644,
+ .proc_handler = kswapd_threads_sysctl_handler,
+ .extra1 = SYSCTL_ONE,
+ .extra2 = &max_kswapd_threads,
+ },
{
.procname = "watermark_scale_factor",
.data = &watermark_scale_factor,
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 35c3bb15ae96..fbb04848f59b 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -179,6 +179,13 @@ struct scan_control {
struct reclaim_state reclaim_state;
};
+/*
+ * Number of active kswapd threads
+ */
+#define DEF_KSWAPD_THREADS_PER_NODE 1
+int kswapd_threads = DEF_KSWAPD_THREADS_PER_NODE;
+int kswapd_threads_current = DEF_KSWAPD_THREADS_PER_NODE;
+
#ifdef ARCH_HAS_PREFETCHW
#define prefetchw_prev_lru_folio(_folio, _base, _field) \
do { \
@@ -7587,26 +7594,91 @@ unsigned long shrink_all_memory(unsigned long nr_to_reclaim)
}
#endif /* CONFIG_HIBERNATION */
+static void update_kswapd_threads_node(int nid)
+{
+ pg_data_t *pgdat;
+ int drop, increase;
+ int last_idx, start_idx, hid;
+ int nr_threads = kswapd_threads_current;
+
+ pgdat = NODE_DATA(nid);
+ pgdat_kswapd_lock(pgdat);
+ last_idx = nr_threads - 1;
+ if (kswapd_threads < nr_threads) {
+ drop = nr_threads - kswapd_threads;
+ for (hid = last_idx; hid > (last_idx - drop); hid--) {
+ if (pgdat->kswapd[hid]) {
+ kthread_stop(pgdat->kswapd[hid]);
+ pgdat->kswapd[hid] = NULL;
+ }
+ }
+ } else {
+ increase = kswapd_threads - nr_threads;
+ start_idx = last_idx + 1;
+ for (hid = start_idx; hid < (start_idx + increase); hid++) {
+ pgdat->kswapd[hid] = kthread_run(kswapd, pgdat, "kswapd%d:%d",
+ nid, hid);
+ if (IS_ERR(pgdat->kswapd[hid])) {
+ pr_err("Failed to start kswapd%d on node %d\n", hid, nid);
+ pgdat->kswapd[hid] = NULL;
+ /*
+ * We are out of resources. Do not start any
+ * more threads.
+ */
+ break;
+ }
+ }
+ }
+ pgdat_kswapd_unlock(pgdat);
+}
+
+void update_kswapd_threads(void)
+{
+ int nid;
+
+ if (kswapd_threads_current == kswapd_threads)
+ return;
+
+ /*
+ * Hold the memory hotplug lock to avoid racing with memory
+ * hotplug initiated updates
+ */
+ mem_hotplug_begin();
+ for_each_node_state(nid, N_MEMORY)
+ update_kswapd_threads_node(nid);
+
+ pr_info("kswapd_thread count changed, old:%d new:%d\n",
+ kswapd_threads_current, kswapd_threads);
+ kswapd_threads_current = kswapd_threads;
+ mem_hotplug_done();
+}
+
/*
* This kswapd start function will be called by init and node-hot-add.
*/
void __meminit kswapd_run(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
+ int hid, nr_threads;
pgdat_kswapd_lock(pgdat);
- if (!pgdat->kswapd) {
- pgdat->kswapd = kthread_create_on_node(kswapd, pgdat, nid, "kswapd%d", nid);
- if (IS_ERR(pgdat->kswapd)) {
- /* failure at boot is fatal */
- pr_err("Failed to start kswapd on node %d, ret=%pe\n",
- nid, pgdat->kswapd);
- BUG_ON(system_state < SYSTEM_RUNNING);
- pgdat->kswapd = NULL;
- } else {
- wake_up_process(pgdat->kswapd);
+ nr_threads = kswapd_threads;
+ for (hid = 0; hid < nr_threads; hid++) {
+ if (!pgdat->kswapd[hid]) {
+ pgdat->kswapd[hid] =
+ kthread_create_on_node(kswapd, pgdat, nid,
+ "kswapd%d:%d", nid, hid);
+ if (IS_ERR(pgdat->kswapd[hid])) {
+ /* failure at boot is fatal */
+ pr_err("Failed to start kswapd%d on node %d, ret=%pe\n", hid,
+ nid, pgdat->kswapd[hid]);
+ pgdat->kswapd[hid] = NULL;
+ } else {
+ wake_up_process(pgdat->kswapd[hid]);
+ }
}
}
+ kswapd_threads_current = nr_threads;
pgdat_kswapd_unlock(pgdat);
}
@@ -7617,13 +7689,15 @@ void __meminit kswapd_run(int nid)
void __meminit kswapd_stop(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
- struct task_struct *kswapd;
+ int hid;
+ int nr_threads = kswapd_threads_current;
pgdat_kswapd_lock(pgdat);
- kswapd = pgdat->kswapd;
- if (kswapd) {
- kthread_stop(kswapd);
- pgdat->kswapd = NULL;
+ for (hid = 0; hid < nr_threads; hid++) {
+ if (pgdat->kswapd[hid]) {
+ kthread_stop(pgdat->kswapd[hid]);
+ pgdat->kswapd[hid] = NULL;
+ }
}
pgdat_kswapd_unlock(pgdat);
}
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [RFC PATCH 2/3] mm/vmscan: handle racing max_seq advancement
2026-09-08 22:10 [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Ehab Ababneh
2026-09-08 22:10 ` [RFC PATCH 1/3] vmscan: Support multiple kswapd threads per node Ehab Ababneh
@ 2026-09-08 22:10 ` Ehab Ababneh
2026-09-08 22:10 ` [RFC PATCH 3/3] mm/vmscan: make kswapd wakeups NUMA load-aware Ehab Ababneh
2026-09-09 2:16 ` [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Zi Yan
3 siblings, 0 replies; 5+ messages in thread
From: Ehab Ababneh @ 2026-09-08 22:10 UTC (permalink / raw)
To: linux-mm, linux-kernel
Cc: Andrew Morton, Johannes Weiner, David Hildenbrand, Michal Hocko,
Qi Zheng, Shakeel Butt, Lorenzo Stoakes, Kairui Song, Barry Song,
Axel Rasmussen, Yuanchu Xie, Wei Xu
When kswapd support is expanded to multiple threads per node, concurrent
aging becomes more likely and try_to_inc_max_seq() can lose a race to
another aging pass that has already advanced max_seq.
Treat that case as success instead of warning, since the desired state has
already been reached by a concurrent updater.
Signed-off-by: Ehab Ababneh <ehab.ababneh@intel.com>
---
mm/vmscan.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/mm/vmscan.c b/mm/vmscan.c
index fbb04848f59b..ad2b398a68f9 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -3942,15 +3942,18 @@ static bool inc_max_seq(struct lruvec *lruvec, unsigned long seq, int swappiness
struct lru_gen_folio *lrugen = &lruvec->lrugen;
restart:
if (seq < READ_ONCE(lrugen->max_seq))
- return false;
+ return true;
lruvec_lock_irq(lruvec);
VM_WARN_ON_ONCE(!seq_is_valid(lruvec));
success = seq == lrugen->max_seq;
- if (!success)
+ if (!success) {
+ /* Concurrent aging advanced max_seq first; goal achieved. */
+ success = true;
goto unlock;
+ }
for (type = 0; type < ANON_AND_FILE; type++) {
if (get_nr_gens(lruvec, type) != MAX_NR_GENS)
@@ -4045,10 +4048,8 @@ static bool try_to_inc_max_seq(struct lruvec *lruvec, unsigned long seq,
walk_mm(mm, walk);
} while (mm);
done:
- if (success) {
+ if (success)
success = inc_max_seq(lruvec, seq, swappiness);
- WARN_ON_ONCE(!success);
- }
return success;
}
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [RFC PATCH 3/3] mm/vmscan: make kswapd wakeups NUMA load-aware
2026-09-08 22:10 [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Ehab Ababneh
2026-09-08 22:10 ` [RFC PATCH 1/3] vmscan: Support multiple kswapd threads per node Ehab Ababneh
2026-09-08 22:10 ` [RFC PATCH 2/3] mm/vmscan: handle racing max_seq advancement Ehab Ababneh
@ 2026-09-08 22:10 ` Ehab Ababneh
2026-09-09 2:16 ` [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Zi Yan
3 siblings, 0 replies; 5+ messages in thread
From: Ehab Ababneh @ 2026-09-08 22:10 UTC (permalink / raw)
To: linux-mm, linux-kernel
Cc: Steven Rostedt, Masami Hiramatsu, Mathieu Desnoyers,
Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, Kairui Song, Qi Zheng, Shakeel Butt,
Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu, Thomas Ballasi,
Jiayuan Chen, Bunyod Suvonov, open list:TRACING
Track a per-node 10-second rolling average of idle CPU cores and use
it in wakeup_kswapd() to decide how many kswapd threads to wake.
A background worker samples the number of idle cores on each NUMA node
once per second (node_cores - nr_running, clamped to zero) and maintains
a rolling average over a 10-second window. When memory pressure triggers
a wakeup, the number of kswapd threads woken is capped to that average
so reclaim scales with available CPU capacity.
On a lightly loaded node most cores are idle, so more threads wake and
reclaim can proceed quickly. On a saturated node few cores are idle, so
fewer threads wake, avoiding unnecessary scheduling contention that
would compete with the workload already running there.
Signed-off-by: Ehab Ababneh <ehab.ababneh@intel.com>
---
include/trace/events/vmscan.h | 28 +++
mm/internal.h | 4 +-
mm/page_alloc.c | 16 +-
mm/vmscan.c | 340 ++++++++++++++++++++++++++++++++--
4 files changed, 359 insertions(+), 29 deletions(-)
diff --git a/include/trace/events/vmscan.h b/include/trace/events/vmscan.h
index b4bf7b8def1f..e4b10417fbb6 100644
--- a/include/trace/events/vmscan.h
+++ b/include/trace/events/vmscan.h
@@ -174,6 +174,34 @@ TRACE_EVENT(mm_vmscan_wakeup_kswapd,
show_gfp_flags(__entry->gfp_flags))
);
+TRACE_EVENT(mm_vmscan_kswapd_threads_to_wake,
+
+ TP_PROTO(int nid, int threads_to_wake, int node_cpus,
+ int avg_idle_cores),
+
+ TP_ARGS(nid, threads_to_wake, node_cpus, avg_idle_cores),
+
+ TP_STRUCT__entry(
+ __field(int, nid)
+ __field(int, threads_to_wake)
+ __field(int, node_cpus)
+ __field(int, avg_idle_cores)
+ ),
+
+ TP_fast_assign(
+ __entry->nid = nid;
+ __entry->threads_to_wake = threads_to_wake;
+ __entry->node_cpus = node_cpus;
+ __entry->avg_idle_cores = avg_idle_cores;
+ ),
+
+ TP_printk("nid=%d threads_to_wake=%d node_cpus=%d avg_idle_cores=%d",
+ __entry->nid,
+ __entry->threads_to_wake,
+ __entry->node_cpus,
+ __entry->avg_idle_cores)
+);
+
DECLARE_EVENT_CLASS(mm_vmscan_direct_reclaim_begin_template,
TP_PROTO(gfp_t gfp_flags, int order, struct mem_cgroup *memcg),
diff --git a/mm/internal.h b/mm/internal.h
index 1c4cc3b7492b..94cebde4db05 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -645,13 +645,13 @@ extern char * const zone_names[MAX_NR_ZONES];
/* perform sanity checks on struct pages being allocated or freed */
DECLARE_STATIC_KEY_MAYBE(CONFIG_DEBUG_VM, check_pages_enabled);
-extern int kswapd_threads;
+extern int max_kswapds_per_node;
extern int max_kswapd_threads;
extern int min_free_kbytes;
extern int defrag_mode;
void setup_per_zone_wmarks(void);
-void update_kswapd_threads(void);
+void update_max_kswapds_per_node(void);
void calculate_min_free_kbytes(void);
int __meminit init_per_zone_wmark_min(void);
void page_alloc_sysctl_init(void);
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index abecf96a599b..64178fcef1d9 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -6615,9 +6615,9 @@ static int min_free_kbytes_sysctl_handler(const struct ctl_table *table, int wri
return 0;
}
-static int kswapd_threads_sysctl_handler(const struct ctl_table *table,
- int write, void __user *buffer,
- size_t *length, loff_t *ppos)
+static int max_kswapds_per_node_sysctl_handler(const struct ctl_table *table,
+ int write, void __user *buffer,
+ size_t *length, loff_t *ppos)
{
int rc;
@@ -6626,7 +6626,7 @@ static int kswapd_threads_sysctl_handler(const struct ctl_table *table,
return rc;
if (write)
- update_kswapd_threads();
+ update_max_kswapds_per_node();
return 0;
}
@@ -6794,11 +6794,11 @@ static const struct ctl_table page_alloc_sysctl_table[] = {
.extra1 = SYSCTL_ZERO,
},
{
- .procname = "kswapd_threads",
- .data = &kswapd_threads,
- .maxlen = sizeof(kswapd_threads),
+ .procname = "max_kswapds_per_node",
+ .data = &max_kswapds_per_node,
+ .maxlen = sizeof(max_kswapds_per_node),
.mode = 0644,
- .proc_handler = kswapd_threads_sysctl_handler,
+ .proc_handler = max_kswapds_per_node_sysctl_handler,
.extra1 = SYSCTL_ONE,
.extra2 = &max_kswapd_threads,
},
diff --git a/mm/vmscan.c b/mm/vmscan.c
index ad2b398a68f9..1a53ebd9ca22 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -32,6 +32,7 @@
#include <linux/rmap.h>
#include <linux/topology.h>
#include <linux/cpu.h>
+#include <linux/cpuhotplug.h>
#include <linux/cpuset.h>
#include <linux/compaction.h>
#include <linux/notifier.h>
@@ -58,12 +59,14 @@
#include <linux/random.h>
#include <linux/mmu_notifier.h>
#include <linux/parser.h>
+#include <linux/smp.h>
#include <asm/tlbflush.h>
#include <asm/div64.h>
#include <linux/swapops.h>
#include <linux/sched/sysctl.h>
+#include <linux/slab.h>
#include "internal.h"
#include "swap.h"
@@ -180,11 +183,283 @@ struct scan_control {
};
/*
- * Number of active kswapd threads
+ * Max and current number of kswapd threads per node.
*/
-#define DEF_KSWAPD_THREADS_PER_NODE 1
-int kswapd_threads = DEF_KSWAPD_THREADS_PER_NODE;
-int kswapd_threads_current = DEF_KSWAPD_THREADS_PER_NODE;
+#define DEF_MAX_KSWAPDS_PER_NODE 1
+int max_kswapds_per_node = DEF_MAX_KSWAPDS_PER_NODE;
+int current_kswapds_per_node = DEF_MAX_KSWAPDS_PER_NODE;
+
+#ifdef CONFIG_NUMA
+/*
+ * Per-node 10-second rolling average of idle CPU cores.
+ * Sampled once per second; used by wakeup_kswapd() to decide how many
+ * kswapd threads to wake without adding contention on a loaded node.
+ */
+#define KSWAPD_NUMA_AVG_WINDOW 10
+
+struct kswapd_node_idle_avg {
+ u32 samples[KSWAPD_NUMA_AVG_WINDOW];
+ u32 sum;
+ u8 idx;
+ u8 count;
+ u32 avg_idle_cores; /* rolling average, written only by the work item */
+ int nid;
+ int node_cpus; /* updated by cpuhp callbacks */
+ bool started;
+ u64 last_sample_ns;
+ struct delayed_work work;
+};
+
+static struct kswapd_node_idle_avg *kswapd_idle_avg;
+static u64 *kswapd_prev_idle;
+static bool *kswapd_prev_idle_valid;
+
+static int kswapd_node_first_online_cpu(int nid)
+{
+ return cpumask_first_and(cpumask_of_node(nid), cpu_online_mask);
+}
+
+static u64 kswapd_read_idle_cpu(int cpu)
+{
+ if (!cpu_online(cpu))
+ return 0;
+
+ return kcpustat_field_idle(cpu);
+}
+
+static void kswapd_idle_avg_queue_work(struct kswapd_node_idle_avg *avg)
+{
+ int cpu;
+
+ cpu = kswapd_node_first_online_cpu(avg->nid);
+ if (cpu < nr_cpu_ids)
+ queue_delayed_work_on(cpu, system_wq, &avg->work, HZ);
+}
+
+static void kswapd_idle_avg_sample_workfn(struct work_struct *work)
+{
+ struct kswapd_node_idle_avg *avg;
+ u64 now_ns, elapsed_ns;
+ u64 idle_cores_milli = 0;
+ u32 idle_cores;
+ int cpu, node_cpus;
+
+ if (!kswapd_idle_avg || !kswapd_prev_idle || !kswapd_prev_idle_valid)
+ return;
+
+ avg = container_of(to_delayed_work(work), struct kswapd_node_idle_avg,
+ work);
+ node_cpus = 0;
+ now_ns = ktime_get_ns();
+
+ if (!avg->last_sample_ns) {
+ avg->last_sample_ns = now_ns;
+ for_each_cpu(cpu, cpumask_of_node(avg->nid)) {
+ if (!cpu_online(cpu))
+ continue;
+ kswapd_prev_idle[cpu] = kswapd_read_idle_cpu(cpu);
+ kswapd_prev_idle_valid[cpu] = true;
+ }
+ kswapd_idle_avg_queue_work(avg);
+ return;
+ }
+
+ elapsed_ns = now_ns - avg->last_sample_ns;
+ avg->last_sample_ns = now_ns;
+
+ if (!elapsed_ns) {
+ kswapd_idle_avg_queue_work(avg);
+ return;
+ }
+
+ for_each_cpu(cpu, cpumask_of_node(avg->nid)) {
+ u64 idle_now, idle_delta;
+
+ if (!cpu_online(cpu))
+ continue;
+
+ node_cpus++;
+ idle_now = kswapd_read_idle_cpu(cpu);
+
+ if (!kswapd_prev_idle_valid[cpu]) {
+ kswapd_prev_idle_valid[cpu] = true;
+ kswapd_prev_idle[cpu] = idle_now;
+ continue;
+ }
+
+ idle_delta = idle_now - kswapd_prev_idle[cpu];
+ kswapd_prev_idle[cpu] = idle_now;
+
+ idle_cores_milli += div_u64(min(idle_delta, elapsed_ns) * 1000,
+ elapsed_ns);
+ }
+
+ idle_cores = min_t(u32, DIV_ROUND_CLOSEST_ULL(idle_cores_milli, 1000),
+ node_cpus);
+
+ if (avg->count == KSWAPD_NUMA_AVG_WINDOW)
+ avg->sum -= avg->samples[avg->idx];
+ else
+ avg->count++;
+
+ avg->samples[avg->idx] = idle_cores;
+ avg->sum += idle_cores;
+ avg->idx = (avg->idx + 1) % KSWAPD_NUMA_AVG_WINDOW;
+ WRITE_ONCE(avg->avg_idle_cores, DIV_ROUND_CLOSEST(avg->sum, avg->count));
+
+ if (READ_ONCE(avg->started))
+ kswapd_idle_avg_queue_work(avg);
+}
+
+static void kswapd_idle_avg_start_node(int nid)
+{
+ struct kswapd_node_idle_avg *avg;
+
+ if (!kswapd_idle_avg || nid < 0 || nid >= nr_node_ids)
+ return;
+
+ avg = &kswapd_idle_avg[nid];
+ if (READ_ONCE(avg->started))
+ return;
+
+ WRITE_ONCE(avg->started, true);
+ avg->last_sample_ns = 0;
+ kswapd_idle_avg_queue_work(avg);
+}
+
+static void kswapd_idle_avg_stop_node(int nid)
+{
+ struct kswapd_node_idle_avg *avg;
+ int cpu;
+
+ if (!kswapd_idle_avg || nid < 0 || nid >= nr_node_ids)
+ return;
+
+ avg = &kswapd_idle_avg[nid];
+ if (!READ_ONCE(avg->started))
+ return;
+
+ WRITE_ONCE(avg->started, false);
+ cancel_delayed_work_sync(&avg->work);
+
+ for_each_cpu(cpu, cpumask_of_node(nid))
+ kswapd_prev_idle_valid[cpu] = false;
+}
+
+static int kswapd_idle_avg_cpu_online(unsigned int cpu)
+{
+ if (kswapd_prev_idle_valid)
+ kswapd_prev_idle_valid[cpu] = false;
+
+ if (kswapd_idle_avg) {
+ int nid = cpu_to_node(cpu);
+
+ WRITE_ONCE(kswapd_idle_avg[nid].node_cpus,
+ cpumask_weight_and(cpumask_of_node(nid), cpu_online_mask));
+ }
+
+ return 0;
+}
+
+static int kswapd_idle_avg_cpu_offline(unsigned int cpu)
+{
+ if (kswapd_prev_idle_valid)
+ kswapd_prev_idle_valid[cpu] = false;
+
+ if (kswapd_idle_avg) {
+ int nid = cpu_to_node(cpu);
+
+ WRITE_ONCE(kswapd_idle_avg[nid].node_cpus,
+ cpumask_weight_and(cpumask_of_node(nid), cpu_online_mask));
+ }
+
+ return 0;
+}
+
+static u32 kswapd_node_avg_idle_cores(int nid)
+{
+ if (!kswapd_idle_avg || nid < 0 || nid >= nr_node_ids)
+ return 0;
+
+ return READ_ONCE(kswapd_idle_avg[nid].avg_idle_cores);
+}
+
+static int kswapd_node_cpu_count(int nid)
+{
+ if (!kswapd_idle_avg || nid < 0 || nid >= nr_node_ids)
+ return cpumask_weight_and(cpumask_of_node(nid), cpu_online_mask);
+
+ return READ_ONCE(kswapd_idle_avg[nid].node_cpus);
+}
+
+static void kswapd_idle_avg_init(void)
+{
+ int cpu;
+ int nid;
+ int ret;
+
+ kswapd_idle_avg = kcalloc(nr_node_ids, sizeof(*kswapd_idle_avg),
+ GFP_KERNEL);
+ if (!kswapd_idle_avg)
+ return;
+
+ kswapd_prev_idle = kcalloc(nr_cpu_ids, sizeof(*kswapd_prev_idle),
+ GFP_KERNEL);
+ if (!kswapd_prev_idle)
+ goto free_idle_avg;
+
+ kswapd_prev_idle_valid = kcalloc(nr_cpu_ids,
+ sizeof(*kswapd_prev_idle_valid),
+ GFP_KERNEL);
+ if (!kswapd_prev_idle_valid)
+ goto free_prev_idle;
+
+ for (nid = 0; nid < nr_node_ids; nid++) {
+ struct kswapd_node_idle_avg *avg = &kswapd_idle_avg[nid];
+
+ avg->nid = nid;
+ INIT_DELAYED_WORK(&avg->work, kswapd_idle_avg_sample_workfn);
+ }
+
+ ret = cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,
+ "mm/vmscan:online",
+ kswapd_idle_avg_cpu_online,
+ kswapd_idle_avg_cpu_offline);
+ if (ret < 0)
+ goto free_prev_idle_valid;
+
+ cpus_read_lock();
+ for_each_online_cpu(cpu)
+ kswapd_idle_avg_cpu_online(cpu);
+ cpus_read_unlock();
+
+ return;
+
+free_prev_idle_valid:
+ kfree(kswapd_prev_idle_valid);
+ kswapd_prev_idle_valid = NULL;
+free_prev_idle:
+ kfree(kswapd_prev_idle);
+ kswapd_prev_idle = NULL;
+free_idle_avg:
+ kfree(kswapd_idle_avg);
+ kswapd_idle_avg = NULL;
+}
+#else
+static inline u32 kswapd_node_avg_idle_cores(int nid)
+{
+ return 0;
+}
+
+static inline int kswapd_node_cpu_count(int nid)
+{
+ return cpumask_weight_and(cpumask_of_node(nid), cpu_online_mask);
+}
+
+static inline void kswapd_idle_avg_init(void) {}
+static inline void kswapd_idle_avg_start_node(int nid) {}
+static inline void kswapd_idle_avg_stop_node(int nid) {}
+#endif /* CONFIG_NUMA */
#ifdef ARCH_HAS_PREFETCHW
#define prefetchw_prev_lru_folio(_folio, _base, _field) \
@@ -7490,6 +7765,8 @@ void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
{
pg_data_t *pgdat;
enum zone_type curr_idx;
+ int nid, threads_to_wake;
+ int node_cpus, avg_idle_cores;
if (!managed_zone(zone))
return;
@@ -7527,7 +7804,28 @@ void wakeup_kswapd(struct zone *zone, gfp_t gfp_flags, int order,
trace_mm_vmscan_wakeup_kswapd(pgdat->node_id, highest_zoneidx, order,
gfp_flags);
- wake_up_interruptible(&pgdat->kswapd_wait);
+
+ /* Determine how many kswapd threads to wake based on the 10-second
+ * rolling average of idle cores on this NUMA node. More idle cores
+ * means more threads can run without adding contention; fewer idle
+ * cores means we keep the wakeup conservative.
+ */
+ nid = pgdat->node_id;
+ avg_idle_cores = kswapd_node_avg_idle_cores(nid);
+ node_cpus = kswapd_node_cpu_count(nid);
+
+ /*
+ * If no average is available yet (early boot), fall back to waking
+ * a single thread to avoid stalling reclaim.
+ */
+ threads_to_wake = max(1,
+ min3(avg_idle_cores, node_cpus,
+ READ_ONCE(current_kswapds_per_node)));
+
+ wake_up_nr(&pgdat->kswapd_wait, threads_to_wake);
+
+ trace_mm_vmscan_kswapd_threads_to_wake(nid, threads_to_wake, node_cpus,
+ avg_idle_cores);
}
void kswapd_clear_hopeless(pg_data_t *pgdat, enum kswapd_clear_hopeless_reason reason)
@@ -7595,18 +7893,18 @@ unsigned long shrink_all_memory(unsigned long nr_to_reclaim)
}
#endif /* CONFIG_HIBERNATION */
-static void update_kswapd_threads_node(int nid)
+static void update_kswapds_per_node_node(int nid)
{
pg_data_t *pgdat;
int drop, increase;
int last_idx, start_idx, hid;
- int nr_threads = kswapd_threads_current;
+ int nr_threads = current_kswapds_per_node;
pgdat = NODE_DATA(nid);
pgdat_kswapd_lock(pgdat);
last_idx = nr_threads - 1;
- if (kswapd_threads < nr_threads) {
- drop = nr_threads - kswapd_threads;
+ if (max_kswapds_per_node < nr_threads) {
+ drop = nr_threads - max_kswapds_per_node;
for (hid = last_idx; hid > (last_idx - drop); hid--) {
if (pgdat->kswapd[hid]) {
kthread_stop(pgdat->kswapd[hid]);
@@ -7614,7 +7912,7 @@ static void update_kswapd_threads_node(int nid)
}
}
} else {
- increase = kswapd_threads - nr_threads;
+ increase = max_kswapds_per_node - nr_threads;
start_idx = last_idx + 1;
for (hid = start_idx; hid < (start_idx + increase); hid++) {
pgdat->kswapd[hid] = kthread_run(kswapd, pgdat, "kswapd%d:%d",
@@ -7633,11 +7931,11 @@ static void update_kswapd_threads_node(int nid)
pgdat_kswapd_unlock(pgdat);
}
-void update_kswapd_threads(void)
+void update_max_kswapds_per_node(void)
{
int nid;
- if (kswapd_threads_current == kswapd_threads)
+ if (current_kswapds_per_node == max_kswapds_per_node)
return;
/*
@@ -7646,11 +7944,11 @@ void update_kswapd_threads(void)
*/
mem_hotplug_begin();
for_each_node_state(nid, N_MEMORY)
- update_kswapd_threads_node(nid);
+ update_kswapds_per_node_node(nid);
- pr_info("kswapd_thread count changed, old:%d new:%d\n",
- kswapd_threads_current, kswapd_threads);
- kswapd_threads_current = kswapd_threads;
+ pr_info("max_kswapds_per_node changed, old:%d new:%d\n",
+ current_kswapds_per_node, max_kswapds_per_node);
+ current_kswapds_per_node = max_kswapds_per_node;
mem_hotplug_done();
}
@@ -7663,7 +7961,7 @@ void __meminit kswapd_run(int nid)
int hid, nr_threads;
pgdat_kswapd_lock(pgdat);
- nr_threads = kswapd_threads;
+ nr_threads = max_kswapds_per_node;
for (hid = 0; hid < nr_threads; hid++) {
if (!pgdat->kswapd[hid]) {
pgdat->kswapd[hid] =
@@ -7679,8 +7977,9 @@ void __meminit kswapd_run(int nid)
}
}
}
- kswapd_threads_current = nr_threads;
+ current_kswapds_per_node = nr_threads;
pgdat_kswapd_unlock(pgdat);
+ kswapd_idle_avg_start_node(nid);
}
/*
@@ -7691,7 +7990,9 @@ void __meminit kswapd_stop(int nid)
{
pg_data_t *pgdat = NODE_DATA(nid);
int hid;
- int nr_threads = kswapd_threads_current;
+ int nr_threads = current_kswapds_per_node;
+
+ kswapd_idle_avg_stop_node(nid);
pgdat_kswapd_lock(pgdat);
for (hid = 0; hid < nr_threads; hid++) {
@@ -7730,6 +8031,7 @@ static int __init kswapd_init(void)
int nid;
swap_setup();
+ kswapd_idle_avg_init();
for_each_node_state(nid, N_MEMORY)
kswapd_run(nid);
register_sysctl_init("vm", vmscan_sysctl_table);
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* Re: [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim
2026-09-08 22:10 [RFC PATCH 0/3] mm/vmscan: adaptive multi-threaded kswapd for NUMA-aware reclaim Ehab Ababneh
` (2 preceding siblings ...)
2026-09-08 22:10 ` [RFC PATCH 3/3] mm/vmscan: make kswapd wakeups NUMA load-aware Ehab Ababneh
@ 2026-09-09 2:16 ` Zi Yan
3 siblings, 0 replies; 5+ messages in thread
From: Zi Yan @ 2026-09-09 2:16 UTC (permalink / raw)
To: Ehab Ababneh, linux-mm, linux-kernel
Cc: Buddy Lumpkin, Andrew Morton, Kairui Song, Qi Zheng,
Shakeel Butt, Barry Song, Axel Rasmussen, Yuanchu Xie, Wei Xu,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Brendan Jackman, Johannes Weiner
On Tue Sep 8, 2026 at 6:10 PM EDT, Ehab Ababneh wrote:
> This series revives Buddy Lumpkin's earlier multi-kswapd proposal:
> https://lkml.iu.edu/hypermail/linux/kernel/1804.0/00342.html
+others
Please Cc everyone in the cover letter, so people can know the
motivation of the patchset.
>
> The motivation is stronger now than when the patch was first discussed.
> Many current systems have hundreds of cores per NUMA node, not the
> single-digit or low-tens core counts that were more common at the time.
> When reclaim does not keep up, direct reclaim can still push allocation
> latency into application paths and leave substantial CPU capacity waiting
> for memory to be freed.
>
> This patchset adds adaptive multi-threaded kswapd. The wakeup policy uses
> node load to decide how many kswapd workers to run, so reclaim can scale
> when it helps and stay conservative on already busy nodes.
>
> Series summary:
>
> 1. Allow multiple kswapd threads per node and add control plumbing.
> 2. Wake an appropriate number of kswapd threads from per-node
> runnable load.
>
> Concerns from the original discussion and how this series addresses some
> of them:
>
> - Concern: Direct reclaim is intended to slow a memory-hogging thread.
> Response: That can be acceptable on lower-core systems. On high-core
> systems, idling many cores while reclaim catches up can cost more than
> allowing reclaim parallelism to scale. It can also block higher-priority
> tasks in direct reclaim while they perform reclaim work on behalf of
> lower-priority memory-hogging tasks.
>
> - Concern: More kswapd threads may hide deeper reclaim issues.
> Response: This series is additive to ongoing reclaim improvements. In
> our testing, multi-threaded kswapd was able to improve performance on
> top of what multi-gen LRU already provides.
>
> - Concern: Existing knobs (such as swappiness and watermarks) should be
> preferred.
> Response: In our testing, those knobs alone did not reliably hit
> performance targets and could increase CPU cost for the same workload
> objective.
>
> - Concern: Need evidence from real workloads.
> Response: This cover letter includes Cassandra results showing higher
> throughput and lower response latency.
>
> - Concern: More reclaim threads may increase pressure on well-behaved
> tasks.
> Response: Adaptive wakeup addresses this by choosing thread count from
> node load.
>
> - Concern: Additional configuration can increase operational complexity.
> Response: The user-facing interface is intentionally minimal:
> max_kswapds_per_node.
>
> - Concern: Lock contention may serialize workers.
> Response: The Cassandra runs below still show net gains, indicating
> contention did not erase the benefit for this workload. The wakeup path
> now uses wake_up_nr() against the existing kswapd_wait queue, avoiding
> pgdat->kswapd_lock (a sleeping mutex) entirely on the allocation hot
> path.
>
> Real-world workload results (Cassandra):
>
> Tests were performed on 7.0.0-rc1.
>
> - max_kswapds_per_node=1
> - throughput sample: 171146
> - reference latency value: 6.375
> - op rates: 43256, 42731, 42341, 42818 ops/s
> - p99 latency: 6.3, 6.4, 6.4, 6.4 ms
>
> - max_kswapds_per_node=8
> - throughput sample: 183639
> - reference latency value: 6.0
> - op rates: 45791, 45253, 46534, 46061 ops/s
> - p99 latency: 6.0, 6.1, 5.9, 6.0 ms
>
> Observed improvement in these runs was about +7.3% throughput and about
> -5.9% response latency, which shows practical benefit for production-style
> database workloads.
But the result suggested we should not have multi-threaded kswapd,
because 7 extra threads give only 7.3% throughput. That is not a good
use of CPU cores. I suspect these threads are competing for LRU locks.
We might want to partition each LRU list into N first, before N kswapds
can work truely in parallel.
>
> In our runs, performance numbers were essentially unchanged with and
> without the adaptive multi-threaded kswapd wakeup policy. In both cases,
> they outperformed the single-kswapd-thread baseline. This indicates the
> adaptive method preserved the multi-threaded performance improvement.
>
> Addendum: alternative approaches evaluated
>
> - PSI per NUMA node.
> I prototyped PSI-based node pressure ranges to drive wakeup count.
> This became cumbersome because robust PSI-to-thread mappings were not
> straightforward across workload types.
>
> - CPU mask snapshot policy.
> I also tested a simple CPU mask snapshot approach.
> While functional, it reflects a moment-in-time view and does not capture
> pressure trends over a broader sampling window.
>
> Buddy Lumpkin (1):
> vmscan: Support multiple kswapd threads per node
>
> Ehab Ababneh (2):
> mm/vmscan: handle racing max_seq advancement
> mm/vmscan: make kswapd wakeups NUMA load-aware
>
> include/linux/mmzone.h | 5 +-
> include/trace/events/vmscan.h | 28 +++
> mm/compaction.c | 8 +-
> mm/internal.h | 3 +
> mm/page_alloc.c | 26 +++
> mm/vmscan.c | 419 +++++++++++++++++++++++++++++++++++++++---
> 6 files changed, 465 insertions(+), 24 deletions(-)
--
Best Regards,
Yan, Zi
^ permalink raw reply [flat|nested] 5+ messages in thread