Skip to contentSkip to conversation

mm: enable lru cache for smaller large folios

RFC PATCH In review Barry Song (Xiaomi) 4 participants 17 messages Aug 18 – Sep 12

Subsystems (3)
lore
Share & tools
lore

Barry Song (Xiaomi)

author reviewer
#0 lore raw

v3 cover mm: enable lru cache for smaller large folios

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

* Use the following microbenchmark:

#include <pthread.h>
#include <sys/mman.h>
#include <string.h>

 #define NUM_THREADS     20
 #define MEM_SIZE        (16 * 1024 * 1024)
 #define LOOP_COUNT      1000
 void* thread_worker(void* arg) {
        void *addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

        for (int i = 0; i < LOOP_COUNT; i++) {
                for(int j = 0; j < MEM_SIZE; j += 4096)
                        *(unsigned char *)(addr + j) = 0x55;
                madvise(addr, MEM_SIZE, MADV_DONTNEED);
        }

        munmap(addr, MEM_SIZE);
        pthread_exit(NULL);

}

 int main() {
        pthread_t threads[NUM_THREADS];

        for (long t = 0; t < NUM_THREADS; t++) {
                pthread_create(&threads[t], NULL, thread_worker, (void*)t);
        }

        for (int t = 0; t < NUM_THREADS; t++) {
                pthread_join(threads[t], NULL);
        }

        return 0;

}

w/o patch:

$ time ./a.out 

real 0m17.760s
user 0m8.207s
sys 5m46.067s

perf lock report:

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                       21414857   21414857     30.07 us     10.73 m     101.94 us       880 ns 
                          42890      42890     11.29 us    484.21 ms    123.08 us       923 ns 
                           1678       1678      1.57 us      2.63 ms      5.77 us       985 ns 
                             52         52      1.46 us     75.98 us      2.69 us      1.07 us 
                             18         18     17.82 us    320.69 us     58.92 us      1.62 us 
                             18         18      2.36 ms     42.52 ms      5.31 ms      1.47 us 
           rcu_state         12         12      1.98 us     23.78 us      2.62 us      1.47 us 
           rcu_state          9          9      1.72 us     15.49 us      2.27 us      1.34 us 
                              2          2      2.42 us      4.83 us      2.48 us      2.36 us 

w/ patch:

$ time ./a.out 

real 0m16.292s
user 0m8.587s
sys 5m13.787s

perf lock report

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                        2641286    2641286     46.87 us      2.06 m     107.92 us      1.01 us 
                         235275     235275     10.57 us      2.49 s     115.32 us      1.02 us 
           rcu_state       1982       1982      7.58 us     15.02 ms     31.72 us      1.26 us 
           rcu_state       1929       1929      7.42 us     14.32 ms     30.50 us      1.43 us 
                             86         86      3.61 us    310.19 us     13.89 us      1.15 us 
                             20         20     19.70 us    394.03 us    108.07 us      2.15 us 
       tasklist_lock          1          1      2.13 us      2.13 us      2.13 us      2.13 us 

* Build the kernel in a 1 GiB memcg by -j20 with zRAM configured as swap:

w/o patch:

Perf lock report:

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                         782337     782337     17.61 us     13.78 s     402.22 us       896 ns 
                          55459      55459     19.48 us      1.08 s     117.59 us      1.01 us 
                           7826       7826      8.01 us     62.68 ms     19.03 us       887 ns 
                           5324       5324      7.83 us     41.68 ms     37.59 us      1.11 us 
           rcu_state       5144       5144      6.27 us     32.23 ms     25.17 us      1.58 us 
           rcu_state       5142       5142      6.35 us     32.67 ms     30.55 us      1.48 us 
                           3855       3855      8.74 us     33.68 ms     42.32 us       996 ns 
                           2770       2770      9.22 us     25.55 ms     27.83 us       914 ns 
                           2342       2342      5.88 us     13.77 ms    318.75 us      1.00 us 

time:

*** Executing round 0 ***

real    1m46.847s
user    25m10.848s
sys     2m57.282s

*** Executing round 1 ***

real    1m46.423s
user    25m10.072s
sys     2m54.348s

*** Executing round 2 ***

real    1m46.308s
user    25m13.800s
sys     2m58.963s

*** Executing round 3 ***

real    1m46.155s
user    25m18.079s
sys     2m59.721s

*** Executing round 4 ***

real    1m45.980s
user    25m15.493s
sys     2m56.959s

w/ patch:

perf lock report

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                         202647     202647     34.27 us      6.94 s     467.82 us      1.18 us 
                          51819      51819     16.26 us    842.46 ms    245.55 us       885 ns 
     inode_hash_lock      15169      15169      8.58 us    130.21 ms     23.23 us      1.04 us 
                           5306       5306      7.54 us     40.03 ms     31.53 us      1.17 us 
           rcu_state       4945       4945      6.97 us     34.47 ms     30.00 us      1.76 us 
           rcu_state       4899       4899      7.03 us     34.42 ms     24.65 us      1.42 us 
                           3923       3923      8.56 us     33.57 ms     27.51 us      1.08 us 
                           2212       2212      5.23 us     11.56 ms    222.79 us       965 ns 
                           1412       1412      6.35 us      8.97 ms     23.09 us      1.77 us 

time:

*** Executing round 0 ***

real 1m46.463s
user 25m17.448s
sys 2m49.524s

*** Executing round 1 ***

real 1m46.274s
user 25m12.178s
sys 2m53.522s

*** Executing round 2 ***

real 1m46.362s
user 25m13.115s
sys 2m53.005s

*** Executing round 3 ***

real 1m46.036s
user 25m17.627s
sys 2m53.477s

*** Executing round 4 ***

real 1m46.329s
user 25m15.130s
sys 2m51.508s

-RFC v3:
* Rather than hard-coding < COSTLY_ORDER to enable the lru_cache, allow the lru_cache for larger orders as long as the folio contains fewer than FOLIO_BATCH_LRU pages. Also limit the total number of pages held in the lru_cache. Hugh may prefer this approach as well.
* Clean up the comments and if conditions in "mm: improve large folio reuse for LRU-cached folios" based on David's feedback. Thanks!
* Properly drain the lru_cache when splitting folios. Thanks to Sashiko and David!

-RFC v2:
* Make __wp_can_reuse_large_anon_folio() aware of LRU-cached large folios. As David pointed out, it currently does not account for large folios residing in the per-CPU LRU cache.
https://lore.kernel.org/all/[email protected]/

Barry Song (Xiaomi) (4):
mm: allow smaller large folios to use lru_cache
mm: improve large folio reuse for LRU-cached folios
mm: drain LRU cache if necessary for splitting large folios
mm: batch lru_cache draining in deferred_split_scan

 include/linux/folio_batch.h | 25 +++++++++++++++++++++++++
 mm/folio.c                  | 10 +++++++++-
 mm/huge_memory.c            | 23 +++++++++++++++++++----
 mm/internal.h               |  4 ++--
 mm/memory.c                 |  6 ++++++
 5 files changed, 61 insertions(+), 7 deletions(-)
Signature · 1 line
2.34.1
author reviewer
#1 lore raw

RFC PATCH v3 1/4 mm: allow smaller large folios to use lru_cache

For systems that primarily use smaller-order large folios, enabling the lru_cache can help reduce lock contention.

For higher-order large folios, the number of folios involved is likely to be smaller, making lock contention less significant.

This patch enables the lru_cache for large folios whose nr_pages is smaller than FOLIO_BATCH_SIZE. To avoid holding too many pages in the lru_cache, which could affect accounting and reclamation, we also limit the total number of pages in the cache to FOLIO_BATCH_SIZE.

To track the number of pages, this patch adds an unsigned short nr_pages field to struct folio_batch. It cannot overflow because the batch contains at most FOLIO_BATCH_SIZE folios, each of which has fewer than FOLIO_BATCH_SIZE pages.

For non-LRU caches, folio_batch only needs to track the number of folios, so nr_pages is left at zero.

---
 include/linux/folio_batch.h | 25 +++++++++++++++++++++++++
 mm/folio.c                  | 10 +++++++++-
 mm/internal.h               |  4 ++--
 3 files changed, 36 insertions(+), 3 deletions(-)
include/linux/folio_batch.h +25 -0
index b45946adc50b..ffc7de091fa3 100644
@@ -10,6 +10,7 @@
 #define _LINUX_FOLIO_BATCH_H
 
 #include <linux/types.h>
+#include <linux/mm.h>
 
 /* 31 pointers + header align the folio_batch structure to a power of two */
 #define FOLIO_BATCH_SIZE	31
@@ -28,6 +29,7 @@ struct folio;
 struct folio_batch {
 	unsigned char nr;
 	unsigned char i;
+	unsigned short nr_pages;
 	bool percpu_pvec_drained;
 	struct folio *folios[FOLIO_BATCH_SIZE];
 };
@@ -42,6 +44,7 @@ static inline void folio_batch_init(struct folio_batch *fbatch)
 {
 	fbatch->nr = 0;
 	fbatch->i = 0;
+	fbatch->nr_pages = 0;
 	fbatch->percpu_pvec_drained = false;
 }
 
@@ -49,6 +52,7 @@ static inline void folio_batch_reinit(struct folio_batch *fbatch)
 {
 	fbatch->nr = 0;
 	fbatch->i = 0;
+	fbatch->nr_pages = 0;
 }
 
 static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
@@ -78,6 +82,27 @@ static inline unsigned folio_batch_add(struct folio_batch *fbatch,
 	return folio_batch_space(fbatch);
 }
 
+/**
+ * folio_batch_add_lru_cache() - Add a folio to a batch of lru_cache
+ * @fbatch: The folio batch.
+ * @folio: The folio to add.
+ *
+ * The folio is added to the end of the batch.
+ * The batch must have previously been initialised using folio_batch_init().
+ *
+ * Return: 0 if the lru_cache is filled with more than FOLIO_BATCH_SIZE
+ *         pages; otherwise, the number of available slots.
+ */
+static inline unsigned folio_batch_add_lru_cache(struct folio_batch *fbatch,
+		struct folio *folio)
+{
+	fbatch->folios[fbatch->nr++] = folio;
+	fbatch->nr_pages += (unsigned short)folio_nr_pages(folio);
+	if (fbatch->nr_pages > FOLIO_BATCH_SIZE)
+		return 0;
+	return folio_batch_space(fbatch);
+}
+
 /**
  * folio_batch_next - Return the next folio to process.
  * @fbatch: The folio batch being processed.
mm/folio.c +9 -1
index 59c477120b9a..e5820d7263e8 100644
@@ -219,7 +219,7 @@ static void __folio_batch_add_and_move(struct folio_batch __percpu *fbatch,
 	else
 		local_lock(&cpu_fbatches.lock);
 
-	if (!folio_batch_add(this_cpu_ptr(fbatch), folio) ||
+	if (!folio_batch_add_lru_cache(this_cpu_ptr(fbatch), folio) ||
 			!folio_may_be_lru_cached(folio) || lru_cache_disabled())
 		folio_batch_move_lru(this_cpu_ptr(fbatch), move_fn);
 
@@ -981,6 +981,7 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
 	int i, j;
 	struct lruvec *lruvec = NULL;
 	unsigned long flags = 0;
+	unsigned long nr_pages = 0;
 
 	for (i = 0, j = 0; i < folios->nr; i++) {
 		struct folio *folio = folios->folios[i];
@@ -1020,6 +1021,7 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
 
 		if (j != i)
 			folios->folios[j] = folio;
+		nr_pages += folio_nr_pages(folio);
 		j++;
 	}
 	if (lruvec)
@@ -1030,6 +1032,12 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
 	}
 
 	folios->nr = j;
+	/*
+	 * For lru_cache, track the number of pages; for non-LRU caches,
+	 * folio_batch->nr_pages is always 0.
+	 */
+	if (folios->nr_pages > 0)
+		folios->nr_pages = nr_pages;
 	mem_cgroup_uncharge_folios(folios);
 	free_unref_folios(folios);
 }
mm/internal.h +2 -2
index 38b1165212c9..06adf78e13a2 100644
@@ -48,9 +48,9 @@ static inline bool folio_may_be_lru_cached(const struct folio *folio)
 	/*
 	 * Holding PMD-sized folios in per-CPU LRU cache unbalances accounting.
 	 * Holding small numbers of low-order mTHP folios in per-CPU LRU cache
-	 * will be sensible, but nobody has implemented and tested that yet.
+	 * will be sensible.
 	 */
-	return !folio_test_large(folio);
+	return folio_nr_pages(folio) < FOLIO_BATCH_SIZE;
 }
 
 static inline void lru_cache_enable(void)
Signature · 1 line
2.34.1
author reviewer
#2 lore raw

Re: [RFC PATCH v3 1/4] mm: allow smaller large folios to use lru_cache

Quoted 40 lines · Barry Song (Xiaomi)

For systems that primarily use smaller-order large folios, enabling the lru_cache can help reduce lock contention.

For higher-order large folios, the number of folios involved is likely to be smaller, making lock contention less significant.

This patch enables the lru_cache for large folios whose nr_pages is smaller than FOLIO_BATCH_SIZE. To avoid holding too many pages in the lru_cache, which could affect accounting and reclamation, we also limit the total number of pages in the cache to FOLIO_BATCH_SIZE.

To track the number of pages, this patch adds an unsigned short nr_pages field to struct folio_batch. It cannot overflow because the batch contains at most FOLIO_BATCH_SIZE folios, each of which has fewer than FOLIO_BATCH_SIZE pages.

For non-LRU caches, folio_batch only needs to track the number of folios, so nr_pages is left at zero.

---
 include/linux/folio_batch.h | 25 +++++++++++++++++++++++++
 mm/folio.c                  | 10 +++++++++-
 mm/internal.h               |  4 ++--
 3 files changed, 36 insertions(+), 3 deletions(-)
include/linux/folio_batch.h +1 -0
index b45946adc50b..ffc7de091fa3 100644
@@ -10,6 +10,7 @@
 #define _LINUX_FOLIO_BATCH_H
 
 #include <linux/types.h>
+#include <linux/mm.h>
 
 /* 31 pointers + header align the folio_batch structure to a power of two */
 #define FOLIO_BATCH_SIZE       31

include/linux/folio_batch.h struct folio; · line 32

Quoted 4 lines
include/linux/folio_batch.h +1 -0
@@ -28,6 +29,7 @@ struct folio;
 struct folio_batch {
        unsigned char nr;
        unsigned char i;
+       unsigned short nr_pages;

Sashiko says
"
Does adding this field break the power-of-two alignment on 32-bit architectures?
The comment right above this struct says "31 pointers + header align the folio_batch structure to a power of two". On 32-bit, the 31 pointers take 124 bytes. Originally, nr, i, and percpu_pvec_drained fit into 3 bytes, allowing the struct to be exactly 128 bytes.
By adding an unsigned short here, the header expands, making the entire struct 132 bytes and breaking the 128-byte cache alignment. Since the maximum nr_pages is limited to slightly above FOLIO_BATCH_SIZE (which is 31), could an unsigned char be used instead to preserve the layout?"

sashiko is right, sorry for my poor math. "unsigned char" is really enough.

Quoted 77 lines
include/linux/folio_batch.h +23 -0
        bool percpu_pvec_drained;
        struct folio *folios[FOLIO_BATCH_SIZE];
 };
@@ -42,6 +44,7 @@ static inline void folio_batch_init(struct folio_batch *fbatch)
 {
        fbatch->nr = 0;
        fbatch->i = 0;
+       fbatch->nr_pages = 0;
        fbatch->percpu_pvec_drained = false;
 }
 
@@ -49,6 +52,7 @@ static inline void folio_batch_reinit(struct folio_batch *fbatch)
 {
        fbatch->nr = 0;
        fbatch->i = 0;
+       fbatch->nr_pages = 0;
 }
 
 static inline unsigned int folio_batch_count(const struct folio_batch *fbatch)
@@ -78,6 +82,27 @@ static inline unsigned folio_batch_add(struct folio_batch *fbatch,
        return folio_batch_space(fbatch);
 }
 
+/**
+ * folio_batch_add_lru_cache() - Add a folio to a batch of lru_cache
+ * @fbatch: The folio batch.
+ * @folio: The folio to add.
+ *
+ * The folio is added to the end of the batch.
+ * The batch must have previously been initialised using folio_batch_init().
+ *
+ * Return: 0 if the lru_cache is filled with more than FOLIO_BATCH_SIZE
+ *         pages; otherwise, the number of available slots.
+ */
+static inline unsigned folio_batch_add_lru_cache(struct folio_batch *fbatch,
+               struct folio *folio)
+{
+       fbatch->folios[fbatch->nr++] = folio;
+       fbatch->nr_pages += (unsigned short)folio_nr_pages(folio);
+       if (fbatch->nr_pages > FOLIO_BATCH_SIZE)
+               return 0;
+       return folio_batch_space(fbatch);
+}
+
 /**
  * folio_batch_next - Return the next folio to process.
  * @fbatch: The folio batch being processed.
mm/folio.c +3 -1
index 59c477120b9a..e5820d7263e8 100644
@@ -219,7 +219,7 @@ static void __folio_batch_add_and_move(struct folio_batch __percpu *fbatch,
        else
                local_lock(&cpu_fbatches.lock);
 
-       if (!folio_batch_add(this_cpu_ptr(fbatch), folio) ||
+       if (!folio_batch_add_lru_cache(this_cpu_ptr(fbatch), folio) ||
                        !folio_may_be_lru_cached(folio) || lru_cache_disabled())
                folio_batch_move_lru(this_cpu_ptr(fbatch), move_fn);
 
@@ -981,6 +981,7 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
        int i, j;
        struct lruvec *lruvec = NULL;
        unsigned long flags = 0;
+       unsigned long nr_pages = 0;
 
        for (i = 0, j = 0; i < folios->nr; i++) {
                struct folio *folio = folios->folios[i];
@@ -1020,6 +1021,7 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
 
                if (j != i)
                        folios->folios[j] = folio;
+               nr_pages += folio_nr_pages(folio);
                j++;
        }
        if (lruvec)

mm/folio.c void folios_put_refs(struct folio_batch *folios, unsigned int *refs) · line 1040

Quoted 9 lines
mm/folio.c +6 -0
@@ -1030,6 +1032,12 @@ void folios_put_refs(struct folio_batch *folios, unsigned int *refs)
        }
 
        folios->nr = j;
+       /*
+        * For lru_cache, track the number of pages; for non-LRU caches,
+        * folio_batch->nr_pages is always 0.
+        */
+       if (folios->nr_pages > 0)
+               folios->nr_pages = nr_pages;

sashiko says:

"It looks like folios->nr_pages is assigned here, but is never actually read afterwards.
The function immediately calls mem_cgroup_uncharge_folios() and free_unref_folios(), neither of which access folios->nr_pages. In fact, free_unref_folios() resets the entire batch by calling folio_batch_reinit() before returning.
Can this dead store and the related tracking be removed to avoid overhead in this hot path?"

I understand that folios->nr_pages is read by folio_batch_add_lru_cache(), but not by folios_put_refs():

+static inline unsigned folio_batch_add_lru_cache(struct folio_batch *fbatch,
+ struct folio *folio)
+{

+     fbatch->folios[fbatch->nr++] = folio;
+     fbatch->nr_pages += (unsigned short)folio_nr_pages(folio);
+     if (fbatch->nr_pages > FOLIO_BATCH_SIZE)

+ return 0;
+ return folio_batch_space(fbatch);
+}

Thanks
Barry

author reviewer
#3 lore raw

RFC PATCH v3 2/4 mm: improve large folio reuse for LRU-cached folios

In reply to Barry Song (Xiaomi)

Large folios may now reside in the per-CPU LRU cache. Before attempting to reuse them, drain the local LRU cache, which can still be beneficial in cases where the folios are likely to remain in this CPU's LRU cache:

int main(int argc, char *argv[])

{
    int i;
    while (1) {
        volatile int *p = mmap(0, SIZE, PROT_READ | PROT_WRITE,
                               MAP_PRIVATE | MAP_ANONYMOUS,
                               -1, 0);
        for (int i = 0; i < SIZE / sizeof(int); i++)
            p[i] = i;
        madvise((void *)p, SIZE, MADV_PAGEOUT);
        if (!fork())
            _exit(0);
        for (int i = 0; i < SIZE / sizeof(int); i++)
            p[i] = i;
        munmap((void *)p, SIZE);
    }
    return 0;
}
---
 mm/memory.c | 6 ++++++
 1 file changed, 6 insertions(+)
mm/memory.c +6 -0
index 4134ac607ee0..efdf82b3c418 100644
@@ -4275,6 +4275,12 @@ static bool __wp_can_reuse_large_anon_folio(struct folio *folio,
 		folio_unlock(folio);
 	}
 
+	if (folio_may_be_lru_cached(folio) && !folio_test_lru(folio)) {
+		if (folio_ref_count(folio) != folio_large_mapcount(folio) + 1)
+			return false;
+		lru_add_drain();
+	}
+
 	if (folio_large_mapcount(folio) != folio_ref_count(folio))
 		return false;
 
Signature · 1 line
2.34.1
author reviewer
#4 lore raw

RFC PATCH v3 3/4 mm: drain LRU cache if necessary for splitting large folios

In reply to Barry Song (Xiaomi)

Smaller large folios might now be present in the LRU cache. Use David's new lru_cache_drain_for_folio() helper to drain the LRU cache before splitting a folio, ensuring that the folio can be split successfully.

Also, we only perform the drain when it may actually help, assuming that the lru_cache holds an extra reference.

---
 mm/huge_memory.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
mm/huge_memory.c +10 -0
index ced400f72d43..263ef9b6949d 100644
@@ -4201,6 +4201,9 @@ static int __folio_split(struct folio *folio, unsigned int new_order,
 		if (shmem_mapping(mapping))
 			end = shmem_fallocend(mapping->host, end);
 	}
+	if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
+	    folio_may_be_lru_cached(folio))
+		lru_cache_drain_for_folio(folio, 1, NULL);
 
 	/*
 	 * Racy check if we can split the page, before unmap_folio() will
@@ -4325,6 +4328,9 @@ int folio_split_unmapped(struct folio *folio, unsigned int new_order)
 	VM_WARN_ON_ONCE_FOLIO(!folio_test_large(folio), folio);
 	VM_WARN_ON_ONCE_FOLIO(!folio_test_anon(folio), folio);
 
+	if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
+	    folio_may_be_lru_cached(folio))
+		lru_cache_drain_for_folio(folio, 1, NULL);
 	if (folio_expected_ref_count(folio) != folio_ref_count(folio) - 1)
 		return -EAGAIN;
 
@@ -4805,6 +4811,10 @@ static int split_huge_pages_pid(int pid, unsigned long vaddr_start,
 			goto next;
 
 		total++;
+
+		if (folio_ref_count(folio) == folio_expected_ref_count(folio) +
+		    folio_may_be_lru_cached(folio))
+			lru_cache_drain_for_folio(folio, 0, NULL);
 		/*
 		 * For folios with private, split_huge_page_to_list_to_order()
 		 * will try to drop it before split and then check if the folio
Signature · 1 line
2.34.1
author reviewer
#5 lore raw

Re: [RFC PATCH v3 3/4] mm: drain LRU cache if necessary for splitting large folios

Quoted 38 lines · Barry Song (Xiaomi)

Smaller large folios might now be present in the LRU cache. Use David's new lru_cache_drain_for_folio() helper to drain the LRU cache before splitting a folio, ensuring that the folio can be split successfully.

Also, we only perform the drain when it may actually help, assuming that the lru_cache holds an extra reference.

---
 mm/huge_memory.c | 10 ++++++++++
 1 file changed, 10 insertions(+)
mm/huge_memory.c +6 -0
index ced400f72d43..263ef9b6949d 100644
@@ -4201,6 +4201,9 @@ static int __folio_split(struct folio *folio, unsigned int new_order,
                if (shmem_mapping(mapping))
                        end = shmem_fallocend(mapping->host, end);
        }
+       if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
+           folio_may_be_lru_cached(folio))
+               lru_cache_drain_for_folio(folio, 1, NULL);
 
        /*
         * Racy check if we can split the page, before unmap_folio() will
@@ -4325,6 +4328,9 @@ int folio_split_unmapped(struct folio *folio, unsigned int new_order)
        VM_WARN_ON_ONCE_FOLIO(!folio_test_large(folio), folio);
        VM_WARN_ON_ONCE_FOLIO(!folio_test_anon(folio), folio);
 
+       if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
+           folio_may_be_lru_cached(folio))
+               lru_cache_drain_for_folio(folio, 1, NULL);
        if (folio_expected_ref_count(folio) != folio_ref_count(folio) - 1)
                return -EAGAIN;
 

mm/huge_memory.c static int split_huge_pages_pid(int pid, unsigned long vaddr_start, · line 4817

Quoted 7 lines
mm/huge_memory.c +4 -0
@@ -4805,6 +4811,10 @@ static int split_huge_pages_pid(int pid, unsigned long vaddr_start,
                        goto next;
 
                total++;
+
+               if (folio_ref_count(folio) == folio_expected_ref_count(folio) +
+                   folio_may_be_lru_cached(folio))
+                       lru_cache_drain_for_folio(folio, 0, NULL);

sashiko says:

"Does calling lru_cache_drain_for_folio() here sleep inside an atomic context? In split_huge_pages_pid(), this code executes between folio_walk_start() and folio_walk_end() where the page table spinlock is held and preemption is disabled.
If the condition is met, lru_cache_drain_for_folio() can call lru_add_drain_all(), which acquires a mutex and flushes work, both of which are blocking operations.
Can this result in a "scheduling while atomic" panic when writing to /sys/kernel/debug/split_huge_pages?"

I guess I can either drop it since this is a debug interface, and failing to split folios in lru_cache shouldn't cause any serious issues, or move it before folio_walk_start().

Quoted 6 lines
mm/huge_memory.c +0 -1
                /*
                 * For folios with private, split_huge_page_to_list_to_order()
                 * will try to drop it before split and then check if the folio
--

2.34.1

Best Regards
Barry

author reviewer
#6 lore raw

RFC PATCH v3 4/4 mm: batch lru_cache draining in deferred_split_scan

In reply to Barry Song (Xiaomi)

deferred_split_scan() splits a batch of folios, so we only need to drain the lru_cache once for the entire batch.

---
 mm/huge_memory.c | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)
mm/huge_memory.c +10 -5
index 263ef9b6949d..e797f6d1837e 100644
@@ -4085,6 +4085,8 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
  * @lock_at: a page within @folio to be left locked to caller
  * @list: after-split folios will be put on it if non NULL
  * @split_type: perform uniform split or not (non-uniform split)
+ * @lru_cache_drained: whether lru_cache has been drained locally
+ *  or on all CPUs for a batch of folios
  *
  * It calls __split_unmapped_folio() to perform uniform and non-uniform split.
  * It is in charge of checking whether the split is supported or not and
@@ -4100,7 +4102,8 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
  */
 static int __folio_split(struct folio *folio, unsigned int new_order,
 		struct page *split_at, struct page *lock_at,
-		struct list_head *list, enum split_type split_type)
+		struct list_head *list, enum split_type split_type,
+		enum lru_cache_drained *lru_cache_drained)
 {
 	XA_STATE(xas, &folio->mapping->i_pages, folio->index);
 	struct folio *end_folio = folio_next(folio);
@@ -4203,7 +4206,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,
 	}
 	if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
 	    folio_may_be_lru_cached(folio))
-		lru_cache_drain_for_folio(folio, 1, NULL);
+		lru_cache_drain_for_folio(folio, 1, lru_cache_drained);
 
 	/*
 	 * Racy check if we can split the page, before unmap_folio() will
@@ -4395,7 +4398,7 @@ int __split_huge_page_to_list_to_order(struct page *page, struct list_head *list
 	struct folio *folio = page_folio(page);
 
 	return __folio_split(folio, new_order, &folio->page, page, list,
-			     SPLIT_TYPE_UNIFORM);
+			     SPLIT_TYPE_UNIFORM, NULL);
 }
 
 /**
@@ -4426,7 +4429,7 @@ int folio_split(struct folio *folio, unsigned int new_order,
 		struct page *split_at, struct list_head *list)
 {
 	return __folio_split(folio, new_order, split_at, &folio->page, list,
-			     SPLIT_TYPE_NON_UNIFORM);
+			     SPLIT_TYPE_NON_UNIFORM, NULL);
 }
 
 /**
@@ -4622,6 +4625,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 	struct folio *folio, *next;
 	int split = 0;
 	unsigned long isolated;
+	enum lru_cache_drained drained = LRU_CACHE_NOT_DRAINED;
 
 	isolated = list_lru_shrink_walk_irq(&deferred_split_lru, sc,
 					    deferred_split_isolate, &dispose);
@@ -4646,7 +4650,8 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 		}
 		if (!folio_trylock(folio))
 			goto requeue;
-		if (!split_folio(folio)) {
+		if (!__folio_split(folio, 0, &folio->page, &folio->page, NULL,
+		    SPLIT_TYPE_UNIFORM, &drained)) {
 			did_split = true;
 			if (underused)
 				count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
Signature · 1 line
2.34.1
reviewer
#7 lore raw
In reply to Barry Song (Xiaomi)

On Wed, Aug 19, 2026 at 06:59:00AM +0800, Barry Song (Xiaomi) wrote:
[...]

Quoted 5 lines

-RFC v3:
* Rather than hard-coding < COSTLY_ORDER to enable the lru_cache, allow the lru_cache for larger orders as long as the folio contains fewer than FOLIO_BATCH_LRU pages. Also limit the total number of

Tiny typo ... should FOLIO_BATCH_LRU be FOLIO_BATCH_SIZE here?

author reviewer
#8 lore raw
Quoted 11 lines · Lance Yang

On Wed, Aug 19, 2026 at 06:59:00AM +0800, Barry Song (Xiaomi) wrote:
[...]

Quoted 5 lines

-RFC v3:
* Rather than hard-coding < COSTLY_ORDER to enable the lru_cache, allow the lru_cache for larger orders as long as the folio contains fewer than FOLIO_BATCH_LRU pages. Also limit the total number of

Tiny typo ... should FOLIO_BATCH_LRU be FOLIO_BATCH_SIZE here?

Lance, thanks for the review. You’re right—it should be FOLIO_BATCH_SIZE.

Best Regards
Barry

#9 lore raw
In reply to Barry Song (Xiaomi)
Quoted 8 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

As raised, there is this problem with collect_longterm_unpinnable_folios()

(see
https://lore.kernel.org/r/[email protected]
)

whereby we don't know how many refs we actually hold. Certainly not 1.

We might have to wait for Hugh's cleanup to handle that cleanly (and avoid all the other LRU cache draining).

Signature · 3 lines
Cheers,

David
author reviewer
#10 lore raw
Quoted 13 lines · David Hildenbrand (Arm)
Quoted 8 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

As raised, there is this problem with collect_longterm_unpinnable_folios()

Quoted 8 lines

(see
https://lore.kernel.org/r/[email protected]
)

whereby we don't know how many refs we actually hold. Certainly not 1.

We might have to wait for Hugh's cleanup to handle that cleanly (and avoid all the other LRU cache draining).

Hi David,
Thanks for raising this.
Yes, I saw your comment and took a closer look at it. I think the best approach for now is to leave that part untouched until Hugh's patch lands? We might end up doing some extra draining in that case, but that's safe—any value greater than 1 might not be. We may just drain more than necessary?

Best Regards
Barry

author reviewer
#11 lore raw
Quoted 25 lines · Barry Song
Quoted 13 lines · David Hildenbrand (Arm)
Quoted 8 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

As raised, there is this problem with collect_longterm_unpinnable_folios()

Quoted 8 lines

(see
https://lore.kernel.org/r/[email protected]
)

whereby we don't know how many refs we actually hold. Certainly not 1.

We might have to wait for Hugh's cleanup to handle that cleanly (and avoid all the other LRU cache draining).

Quoted 7 lines

Hi David,
Thanks for raising this.
Yes, I saw your comment and took a closer look at it. I think the best approach for now is to leave that part untouched until Hugh's patch lands? We might end up doing some extra draining in that case, but that's safe—any value greater than 1 might not be. We may just drain more than necessary?

BTW, David. This is also why I didn't move the below checks in patch 3/4[1] to lru_cache_drain_for_folio() as we are not safe to do it for that "1" in collect_longterm_unpinnable_folios():

+ if (folio_ref_count(folio) == folio_expected_ref_count(folio) + 1 +
+ folio_may_be_lru_cached(folio))
+ lru_cache_drain_for_folio(folio, 1, NULL);

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

Quoted 3 lines

Best Regards
Barry

#12 lore raw
In reply to Barry Song
Quoted 21 lines · Barry Song
Quoted 19 lines · David Hildenbrand (Arm)
Quoted 8 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

As raised, there is this problem with collect_longterm_unpinnable_folios()

(see
https://lore.kernel.org/r/[email protected]
)

whereby we don't know how many refs we actually hold. Certainly not 1.

Quoted 8 lines
Quoted 2 lines

We might have to wait for Hugh's cleanup to handle that cleanly (and avoid all the other LRU cache draining).

Hi David,
Thanks for raising this.
Yes, I saw your comment and took a closer look at it. I think the best approach for now is to leave that part untouched until Hugh's patch lands?

I think we should not consider your patch set until Hugh either resolved it or we have a better way to handle draining. Building on top of this for large folios now just gets painful.

Quoted 2 lines

We might end up doing some extra draining in that case, but that's safe—any value greater than 1 might not be.

We'll unconditionally drain all LRU caches, which is precisely *not* what we want to do in the first place. :)

Signature · 3 lines
Cheers,

David
author reviewer
#13 lore raw
Quoted 32 lines · David Hildenbrand (Arm)
Quoted 21 lines · Barry Song
Quoted 19 lines · David Hildenbrand (Arm)
Quoted 8 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

As raised, there is this problem with collect_longterm_unpinnable_folios()

(see
https://lore.kernel.org/r/[email protected]
)

whereby we don't know how many refs we actually hold. Certainly not 1.

Quoted 8 lines
Quoted 2 lines

We might have to wait for Hugh's cleanup to handle that cleanly (and avoid all the other LRU cache draining).

Hi David,
Thanks for raising this.
Yes, I saw your comment and took a closer look at it. I think the best approach for now is to leave that part untouched until Hugh's patch lands?

Quoted 3 lines

I think we should not consider your patch set until Hugh either resolved it or we have a better way to handle draining. Building on top of this for large folios now just gets painful.

Sure, I’m perfectly fine with this. I swear I asked Hugh whether I should suspend this job and wait for him, but he didn’t seem to suggest suspending it. :-) However, I agree that it makes more sense to prioritize Hugh’s job before this one.

Quoted 6 lines
Quoted 3 lines

We might end up doing some extra draining in that case, but that's safe—any value greater than 1 might not be.

We'll unconditionally drain all LRU caches, which is precisely *not* what we want to do in the first place. :)

Sure. Maybe because the test case is a kernel build, the collect_longterm_unpinnable_folios() path is not really a hot path. But I agree we can probably find some workloads where it is hot.

I’m perfectly fine with putting this large folio lru_cache work on hold and working with Hugh on optimizing those drain paths first. :-)

Best Regards
Barry

Tested-by on the v3 series
#14 lore raw
In reply to Barry Song (Xiaomi)
Quoted 207 lines · Barry Song (Xiaomi)

This patchset enables the per-CPU LRU cache for large folios with fewer than FOLIO_BATCH_SIZE (31) pages. It also limits each per-CPU LRU cache to at most FOLIO_BATCH_SIZE pages to avoid negatively affecting accounting and memory reclamation pressure.

This is particularly beneficial on systems that use relatively small large folios. For larger folios, the benefit is likely to be smaller because far fewer folios are expected to contend for the LRU cache.

* Use the following microbenchmark:

#include <pthread.h>
#include <sys/mman.h>
#include <string.h>

 #define NUM_THREADS     20
 #define MEM_SIZE        (16 * 1024 * 1024)
 #define LOOP_COUNT      1000
 void* thread_worker(void* arg) {
        void *addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

        for (int i = 0; i < LOOP_COUNT; i++) {
                for(int j = 0; j < MEM_SIZE; j += 4096)
                        *(unsigned char *)(addr + j) = 0x55;
                madvise(addr, MEM_SIZE, MADV_DONTNEED);
        }

        munmap(addr, MEM_SIZE);
        pthread_exit(NULL);

}

 int main() {
        pthread_t threads[NUM_THREADS];

        for (long t = 0; t < NUM_THREADS; t++) {
                pthread_create(&threads[t], NULL, thread_worker, (void*)t);
        }

        for (int t = 0; t < NUM_THREADS; t++) {
                pthread_join(threads[t], NULL);
        }

        return 0;

}

w/o patch:

$ time ./a.out 

real 0m17.760s
user 0m8.207s
sys 5m46.067s

perf lock report:

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                       21414857   21414857     30.07 us     10.73 m     101.94 us       880 ns 
                          42890      42890     11.29 us    484.21 ms    123.08 us       923 ns 
                           1678       1678      1.57 us      2.63 ms      5.77 us       985 ns 
                             52         52      1.46 us     75.98 us      2.69 us      1.07 us 
                             18         18     17.82 us    320.69 us     58.92 us      1.62 us 
                             18         18      2.36 ms     42.52 ms      5.31 ms      1.47 us 
           rcu_state         12         12      1.98 us     23.78 us      2.62 us      1.47 us 
           rcu_state          9          9      1.72 us     15.49 us      2.27 us      1.34 us 
                              2          2      2.42 us      4.83 us      2.48 us      2.36 us 

w/ patch:

$ time ./a.out 

real 0m16.292s
user 0m8.587s
sys 5m13.787s

perf lock report

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                        2641286    2641286     46.87 us      2.06 m     107.92 us      1.01 us 
                         235275     235275     10.57 us      2.49 s     115.32 us      1.02 us 
           rcu_state       1982       1982      7.58 us     15.02 ms     31.72 us      1.26 us 
           rcu_state       1929       1929      7.42 us     14.32 ms     30.50 us      1.43 us 
                             86         86      3.61 us    310.19 us     13.89 us      1.15 us 
                             20         20     19.70 us    394.03 us    108.07 us      2.15 us 
       tasklist_lock          1          1      2.13 us      2.13 us      2.13 us      2.13 us 

* Build the kernel in a 1 GiB memcg by -j20 with zRAM configured as swap:

w/o patch:

Perf lock report:

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                         782337     782337     17.61 us     13.78 s     402.22 us       896 ns 
                          55459      55459     19.48 us      1.08 s     117.59 us      1.01 us 
                           7826       7826      8.01 us     62.68 ms     19.03 us       887 ns 
                           5324       5324      7.83 us     41.68 ms     37.59 us      1.11 us 
           rcu_state       5144       5144      6.27 us     32.23 ms     25.17 us      1.58 us 
           rcu_state       5142       5142      6.35 us     32.67 ms     30.55 us      1.48 us 
                           3855       3855      8.74 us     33.68 ms     42.32 us       996 ns 
                           2770       2770      9.22 us     25.55 ms     27.83 us       914 ns 
                           2342       2342      5.88 us     13.77 ms    318.75 us      1.00 us 

time:

*** Executing round 0 ***

real    1m46.847s
user    25m10.848s
sys     2m57.282s

*** Executing round 1 ***

real    1m46.423s
user    25m10.072s
sys     2m54.348s

*** Executing round 2 ***

real    1m46.308s
user    25m13.800s
sys     2m58.963s

*** Executing round 3 ***

real    1m46.155s
user    25m18.079s
sys     2m59.721s

*** Executing round 4 ***

real    1m45.980s
user    25m15.493s
sys     2m56.959s

w/ patch:

perf lock report

                Name   acquired  contended     avg wait   total wait     max wait     min wait 

                         202647     202647     34.27 us      6.94 s     467.82 us      1.18 us 
                          51819      51819     16.26 us    842.46 ms    245.55 us       885 ns 
     inode_hash_lock      15169      15169      8.58 us    130.21 ms     23.23 us      1.04 us 
                           5306       5306      7.54 us     40.03 ms     31.53 us      1.17 us 
           rcu_state       4945       4945      6.97 us     34.47 ms     30.00 us      1.76 us 
           rcu_state       4899       4899      7.03 us     34.42 ms     24.65 us      1.42 us 
                           3923       3923      8.56 us     33.57 ms     27.51 us      1.08 us 
                           2212       2212      5.23 us     11.56 ms    222.79 us       965 ns 
                           1412       1412      6.35 us      8.97 ms     23.09 us      1.77 us 

time:

*** Executing round 0 ***

real 1m46.463s
user 25m17.448s
sys 2m49.524s

*** Executing round 1 ***

real 1m46.274s
user 25m12.178s
sys 2m53.522s

*** Executing round 2 ***

real 1m46.362s
user 25m13.115s
sys 2m53.005s

*** Executing round 3 ***

real 1m46.036s
user 25m17.627s
sys 2m53.477s

*** Executing round 4 ***

real 1m46.329s
user 25m15.130s
sys 2m51.508s

(quoted diff) +0 -2
-RFC v3:
  * Rather than hard-coding `< COSTLY_ORDER` to enable the lru_cache,
    allow the lru_cache for larger orders as long as the folio contains
    fewer than `FOLIO_BATCH_LRU` pages. Also limit the total number of
    pages held in the lru_cache. Hugh may prefer this approach as well.
  * Clean up the comments and `if` conditions in "mm: improve large folio
    reuse for LRU-cached folios" based on David's feedback. Thanks!
  * Properly drain the lru_cache when splitting folios. Thanks to Sashiko
    and David!
 
-RFC v2:
 * Make __wp_can_reuse_large_anon_folio() aware of LRU-cached large
   folios. As David pointed out, it currently does not account for
   large folios residing in the per-CPU LRU cache.
   https://lore.kernel.org/all/[email protected]/

Barry Song (Xiaomi) (4):
mm: allow smaller large folios to use lru_cache
mm: improve large folio reuse for LRU-cached folios
mm: drain LRU cache if necessary for splitting large folios
mm: batch lru_cache draining in deferred_split_scan

Quoted 6 lines
 include/linux/folio_batch.h | 25 +++++++++++++++++++++++++
 mm/folio.c                  | 10 +++++++++-
 mm/huge_memory.c            | 23 +++++++++++++++++++----
 mm/internal.h               |  4 ++--
 mm/memory.c                 |  6 ++++++
 5 files changed, 61 insertions(+), 7 deletions(-)

Thanks for this series.
I tested this against a Graph500 regression with 16K mTHP.
The series removes the regression and reduces folio_lruvec_lock_irqsave contention to approximately the 4K rate.

System and Workload
===================

AMD EPYC Zen 5 System, 2 sockets, 160 cores/socket, SMT on -> 320 cores / 640 threads
2 NUMA nodes, ~512 GB/node
node 0 CPUs: 0-159,320-479

Base: v7.3-rc2+ (50d05c7c76c9)
Runtime: performance governor, preempt=full (lazy)

Workload: Graph500, Scale 27, edgefactor 16
Metric: median_time in ms (lower is better)

Bind to node 0 with numactl -C 0-159,320-479 -m 0

Performance
===========

                  Base       Patched    delta
4K                109.60     109.11     -0.4%

mTHP-16K 144.10 99.00 -31.3%
mTHP-64K 104.75 104.25 -0.5%

mTHP-16K was ~31% slower than THP-never, due to lock contentions. With your series, mTHP-16 performs ~9% faster than THP-never.

The dominant folio_lruvec_lock_irqsave entries were:

Base Patched
contended wait contended wait

4K               834603   2.41 m         844193   2.61 m
mTHP-16K        7008834   28.88 m        840695   4.87 m
mTHP-64K         188144   752 ms         40874    93.46 ms

Thanks,
Shivank

author reviewer
#15 lore raw

On Fri, Sep 11, 2026 at 11:39 PM Garg, Shivank <[email protected]> wrote:
[...]

Quoted 8 lines

Thanks for this series.
I tested this against a Graph500 regression with 16K mTHP.
The series removes the regression and reduces folio_lruvec_lock_irqsave contention to approximately the 4K rate.

Thanks very much, Shivank. I’m very happy to see that mTHP LRUCACHE not only resolves your regression but also improves the performance of your case.

As discussed with David, this series will be respun after Hugh’s work[1] is merged.

In the meantime, I’d really appreciate it if you could test an updated version that I haven’t sent out before:

https://git.kernel.org/pub/scm/linux/kernel/git/baohua/linux.git/log/?h=mthp_lrucache

It includes some modifications based on Sashiko’s review comments on the version you tested. I’d like to know whether it still provides the same performance gains you saw with that version.

[1] https://lore.kernel.org/linux-mm/[email protected]/

Quoted 30 lines

System and Workload
===================

AMD EPYC Zen 5 System, 2 sockets, 160 cores/socket, SMT on -> 320 cores / 640 threads
2 NUMA nodes, ~512 GB/node
node 0 CPUs: 0-159,320-479

Base: v7.3-rc2+ (50d05c7c76c9)
Runtime: performance governor, preempt=full (lazy)

Workload: Graph500, Scale 27, edgefactor 16
Metric: median_time in ms (lower is better)

Bind to node 0 with numactl -C 0-159,320-479 -m 0

Performance
===========

                  Base       Patched    delta
4K                109.60     109.11     -0.4%

mTHP-16K 144.10 99.00 -31.3%
mTHP-64K 104.75 104.25 -0.5%

mTHP-16K was ~31% slower than THP-never, due to lock contentions. With your series, mTHP-16 performs ~9% faster than THP-never.

Quoted 9 lines

The dominant folio_lruvec_lock_irqsave entries were:

Base Patched
contended wait contended wait

4K               834603   2.41 m         844193   2.61 m
mTHP-16K        7008834   28.88 m        840695   4.87 m
mTHP-64K         188144   752 ms         40874    93.46 ms

Best Regards
Barry

#16 lore raw
Quoted 21 lines · Barry Song

On Fri, Sep 11, 2026 at 11:39 PM Garg, Shivank <[email protected]> wrote:
[...]

Quoted 8 lines

Thanks for this series.
I tested this against a Graph500 regression with 16K mTHP.
The series removes the regression and reduces folio_lruvec_lock_irqsave contention to approximately the 4K rate.

Thanks very much, Shivank. I’m very happy to see that mTHP LRUCACHE not only resolves your regression but also improves the performance of your case.

As discussed with David, this series will be respun after Hugh’s work[1] is merged.

In the meantime, I’d really appreciate it if you could test an updated version that I haven’t sent out before:

Quoted 8 lines

https://git.kernel.org/pub/scm/linux/kernel/git/baohua/linux.git/log/?h=mthp_lrucache

It includes some modifications based on Sashiko’s review comments on the version you tested. I’d like to know whether it still provides the same performance gains you saw with that version.

[1] https://lore.kernel.org/linux-mm/[email protected]/

I tested this branch.

Base: 4b65683fd ("mm/swap, PM: hibernate: atomically replace
hibernation pin")
Patched (Base+series): 5dfdf87ee (branch tip, the 4 lru_cache patches)

                       Base              Patched           reduction
    4K            839281   2.30 m      842435   2.91 m    
    mTHP-16K     7032797  28.79 m      839301   4.53 m    8.4x / 6.4x
    mTHP-64K      267948   1.19 s       36073  93.48 ms   7.4x / 12.7x

Numbers are almost unchanged, except some run-to-run variations.

Thanks
Shivank

Quoted 40 lines
Quoted 39 lines

System and Workload
===================

AMD EPYC Zen 5 System, 2 sockets, 160 cores/socket, SMT on -> 320 cores / 640 threads
2 NUMA nodes, ~512 GB/node
node 0 CPUs: 0-159,320-479

Base: v7.3-rc2+ (50d05c7c76c9)
Runtime: performance governor, preempt=full (lazy)

Workload: Graph500, Scale 27, edgefactor 16
Metric: median_time in ms (lower is better)

Bind to node 0 with numactl -C 0-159,320-479 -m 0

Performance
===========

                  Base       Patched    delta
4K                109.60     109.11     -0.4%

mTHP-16K 144.10 99.00 -31.3%
mTHP-64K 104.75 104.25 -0.5%

mTHP-16K was ~31% slower than THP-never, due to lock contentions. With your series, mTHP-16 performs ~9% faster than THP-never.

The dominant folio_lruvec_lock_irqsave entries were:

Base Patched
contended wait contended wait

4K               834603   2.41 m         844193   2.61 m
mTHP-16K        7008834   28.88 m        840695   4.87 m
mTHP-64K         188144   752 ms         40874    93.46 ms