Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v4 0/4] mm: split underused anonymous mTHP folios
@ 2026-11-03 21:20 Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 1/4] mm/huge_memory: scale thp_underused() to the folio size Joanne Koong
                   ` (3 more replies)
  0 siblings, 4 replies; 9+ messages in thread
From: Joanne Koong @ 2026-11-03 21:20 UTC (permalink / raw)
  To: akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

PMD-sized THPs that are mostly zero-filled are reclaimed under memory
pressure by the deferred split shrinker, but this is not done for mTHP
folios. At Meta we would like to deploy 2M THP=always on arm64 with 64k
base pages, as 2M provides the contpte benefits while the PMD size there
(512M) is too big to use. However, 2M THP=always causes memory regressions
unless the unused portions of those folios can be broken down and
reclaimed.

This series lets the underused shrinker handle anonymous mTHP folios. It has
four patches:

Patch 1 makes thp_underused() folio-size-aware. It scales
khugepaged/max_ptes_none to the folio size, so every folio tolerates the same
fraction of zero-filled pages.

Patch 2 stops queueing folios for underused splitting when
khugepaged/max_ptes_none is at its default maximum, where the shrinker
would never split them anyway. This applies to PMD-sized folios too,
which are unconditionally queued today.

Patch 3 keeps thp_underused_split_page counting only PMD-sized folios and
adds a per-order split_underused counter, matching how thp_split_page and
thp_deferred_split_page are handled.

Patch 4 queues anonymous mTHP folios on the deferred split list, with a
hard-coded 2M minimum folio size as suggested by Barry. PMD-sized folios are
exempt from the minimum, since on s390 the PMD size is 1M.

At the default khugepaged/max_ptes_none, no folio is queued for underused
splitting. Once it's lowered, mTHP folios of at least 2M are queued along with
PMD-sized folios.

Changelog
---------
v3: https://lore.kernel.org/linux-mm/20261023230006.3759543-1-joannelkoong@gmail.com/
Changes since v3:
* Scale khugepaged/max_ptes_none with truncating division instead of
  rounding up, which is exact given the strict > comparison (Sashiko,
  Lance)
* New patch 3: keep thp_underused_split_page PMD-only and add a
  per-order split_underused counter (Kiryl)
* Fix typo in patch 4's commit message (Kiryl)

v2: https://lore.kernel.org/linux-mm/20260916224437.1164512-1-joannelkoong@gmail.com/
Changes since v2:
* Go back to scaling khugepaged/max_ptes_none per folio size (David)
* Keep the check for the sysctl maximum on the raw, unscaled value
  (Sashiko, from v1). Round the scaled value up so a folio is never
  split when fewer than the configured fraction of its pages are
  zero-filled.
* Only queue mTHP folios of at least 2M, a hard-coded minimum (Barry, David)

v1: https://lore.kernel.org/linux-mm/20260707201735.4113107-1-joannelkoong@gmail.com/
Changes since v1:
* Drop the per-order scaling of khugepaged_max_ptes_none. Keep it
  absolute, matching collapse (David, Johannes)
* New patch 2 (suggested by Johannes): don't queue folios that can never
  be underused, which both bounds what patch 3 adds and stops queuing PMD
  folios under default settings (Johannes, David, Barry)
* Fix the thp_underused() early exit to scale to the folio's own size

Joanne Koong (4):
  mm/huge_memory: scale thp_underused() to the folio size
  mm/huge_memory: don't queue folios when underused splitting is
    inactive
  mm/huge_memory: add a per-order counter for underused splits
  mm: add anonymous mTHP folios to the deferred split list

 Documentation/admin-guide/mm/transhuge.rst |  6 ++
 include/linux/huge_mm.h                    |  1 +
 mm/huge_memory.c                           | 66 ++++++++++++++++++----
 mm/memory.c                                |  2 +
 4 files changed, 65 insertions(+), 10 deletions(-)


base-commit: 74d73ffcffe03804f97e9fb82301dadf75671282
-- 
2.52.0



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

* [PATCH v4 1/4] mm/huge_memory: scale thp_underused() to the folio size
  2026-11-03 21:20 [PATCH v4 0/4] mm: split underused anonymous mTHP folios Joanne Koong
@ 2026-11-03 21:20 ` Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive Joanne Koong
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 9+ messages in thread
From: Joanne Koong @ 2026-11-03 21:20 UTC (permalink / raw)
  To: akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

thp_underused() decides whether a large folio on the deferred split list
is underused and should be split so that its zero-filled subpages can be
reclaimed.

Today it only ever runs on PMD-sized folios, so it assumes that the
folio has HPAGE_PMD_NR pages and compares the zero-filled page count
directly against khugepaged_max_ptes_none. A later patch will queue
anonymous mTHP folios onto the deferred split list as well, at which
point that assumption will no longer hold.

Make thp_underused() folio-size-aware, and scale
khugepaged_max_ptes_none proportionally to the number of pages in the
folio (eg every folio tolerates the same fraction of zero-filled pages
the user configured for a PMD-sized folio).

khugepaged deliberately doesn't scale intermediate values when
collapsing to mTHP orders (to avoid the memory footprint creeping up
through repeated collapses), but if/when collapse later gains support
for intermediate values, a folio that khugepaged collapses won't then be
split right back by the shrinker (and vice versa) since both collapse
and split would use the same threshold for a given folio size.

There is no functional change for PMD-sized folios, as for those,
nr_pages is HPAGE_PMD_NR.

Suggested-by: Usama Arif <usama.arif@linux.dev>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 mm/huge_memory.c | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 14599d0fff01..3720c12bdb0d 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -4625,25 +4625,33 @@ static unsigned long deferred_split_count(struct shrinker *shrink,
 
 static bool thp_underused(struct folio *folio)
 {
-	int num_zero_pages = 0, num_filled_pages = 0;
-	int i;
+	const unsigned long nr_pages = folio_nr_pages(folio);
+	unsigned long num_zero_pages = 0, num_filled_pages = 0;
+	unsigned long max_ptes_none = khugepaged_max_ptes_none;
+	unsigned long i;
 
-	if (khugepaged_max_ptes_none == HPAGE_PMD_NR - 1)
+	if (max_ptes_none == HPAGE_PMD_NR - 1)
 		return false;
 
 	if (folio_contain_hwpoisoned_page(folio))
 		return false;
 
-	for (i = 0; i < folio_nr_pages(folio); i++) {
+	/*
+	 * khugepaged_max_ptes_none is a number of pages out of HPAGE_PMD_NR.
+	 * Scale it to this folio's size.
+	 */
+	max_ptes_none = max_ptes_none * nr_pages / HPAGE_PMD_NR;
+
+	for (i = 0; i < nr_pages; i++) {
 		if (pages_identical(folio_page(folio, i), ZERO_PAGE(0))) {
-			if (++num_zero_pages > khugepaged_max_ptes_none)
+			if (++num_zero_pages > max_ptes_none)
 				return true;
 		} else {
 			/*
 			 * Another path for early exit once the number
 			 * of non-zero filled pages exceeds threshold.
 			 */
-			if (++num_filled_pages >= HPAGE_PMD_NR - khugepaged_max_ptes_none)
+			if (++num_filled_pages >= nr_pages - max_ptes_none)
 				return false;
 		}
 	}
-- 
2.52.0



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

* [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive
  2026-11-03 21:20 [PATCH v4 0/4] mm: split underused anonymous mTHP folios Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 1/4] mm/huge_memory: scale thp_underused() to the folio size Joanne Koong
@ 2026-11-03 21:20 ` Joanne Koong
  2026-11-03 22:15   ` Usama Arif
  2026-11-03 21:20 ` [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list Joanne Koong
  3 siblings, 1 reply; 9+ messages in thread
From: Joanne Koong @ 2026-11-03 21:20 UTC (permalink / raw)
  To: akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

Every anonymous PMD-sized folio is put on the deferred split queue when
it is first mapped, so that the shrinker can find it under memory
pressure and split it if it turns out to be mostly zero-filled.

With the default khugepaged/max_ptes_none this is wasted work. The
default is HPAGE_PMD_NR - 1, which tells thp_underused() that any number
of zero-filled pages is tolerable, which means the shrinker will never
split any of these folios for being underused. They take the list_lru
lock at fault time, inflate the object count the shrinker reports, and
are then walked and dropped when they're first scanned.

Skip the queueing when khugepaged/max_ptes_none is at its maximum, and
factor that check out into thp_underused_never() so that
deferred_split_folio() and thp_underused() share it. Since the default
is the maximum, out of the box no folio is queued for underused
splitting at all, PMD-sized ones included. Partially mapped folios are
still queued as before.

This also matters for a subsequent patch that adds anonymous mTHP folios
to the deferred split list, as it keeps systems running the default from
paying for that on every anonymous fault.

Please note that the queue has always been best-effort. A folio that was
queued before khugepaged/max_ptes_none is lowered gets dropped from the
queue by the first scan that finds it's not underused, so changing the
sysctl has never retroactively applied to folios that were already
scanned. Requeueing eligible folios when the sysctl changes will be
addressed in a separate patch.

Suggested-by: Johannes Weiner <hannes@cmpxchg.org>
Suggested-by: David Hildenbrand <david@kernel.org>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 mm/huge_memory.c | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 3720c12bdb0d..903bbe972011 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -4564,6 +4564,15 @@ bool __folio_unqueue_deferred_split(struct folio *folio)
 	return unqueued;	/* useful for debug warnings */
 }
 
+/*
+ * At the sysctl maximum the user tolerates any number of zero-filled pages,
+ * so no folio is ever underused.
+ */
+static bool thp_underused_never(unsigned long max_ptes_none)
+{
+	return max_ptes_none == HPAGE_PMD_NR - 1;
+}
+
 /* partially_mapped=false won't clear PG_partially_mapped folio flag */
 void deferred_split_folio(struct folio *folio, bool partially_mapped)
 {
@@ -4579,8 +4588,17 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 	if (folio_order(folio) <= 1)
 		return;
 
-	if (!partially_mapped && !split_underused_thp)
-		return;
+	if (!partially_mapped) {
+		if (!split_underused_thp)
+			return;
+		/*
+		 * Nothing will ever split this folio for being underused, so
+		 * keep it off the queue entirely rather than paying for the
+		 * list_lru lock here and a shrinker scan later.
+		 */
+		if (thp_underused_never(khugepaged_max_ptes_none))
+			return;
+	}
 
 	/*
 	 * Exclude swapcache: originally to avoid a corrupt deferred split
@@ -4630,7 +4648,7 @@ static bool thp_underused(struct folio *folio)
 	unsigned long max_ptes_none = khugepaged_max_ptes_none;
 	unsigned long i;
 
-	if (max_ptes_none == HPAGE_PMD_NR - 1)
+	if (thp_underused_never(max_ptes_none))
 		return false;
 
 	if (folio_contain_hwpoisoned_page(folio))
-- 
2.52.0



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

* [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits
  2026-11-03 21:20 [PATCH v4 0/4] mm: split underused anonymous mTHP folios Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 1/4] mm/huge_memory: scale thp_underused() to the folio size Joanne Koong
  2026-11-03 21:20 ` [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive Joanne Koong
@ 2026-11-03 21:20 ` Joanne Koong
  2026-11-03 22:20   ` Usama Arif
  2026-11-03 21:20 ` [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list Joanne Koong
  3 siblings, 1 reply; 9+ messages in thread
From: Joanne Koong @ 2026-11-03 21:20 UTC (permalink / raw)
  To: akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

thp_underused_split_page counts every folio that the deferred split
shrinker splits for being underused. Today that is only ever a
PMD-sized folio, but a later patch queues anonymous mTHP folios for
underused splitting too, at which point this counter would start
counting mTHP splits as well.

thp_underused_split_page could then end up larger than thp_split_page,
and there would be no per-order count of underused splits. That doesn't
match how the other THP split stats work. In /proc/vmstat,
thp_split_page and thp_deferred_split_page only count PMD-sized folios.
Splits of every size, PMD included, are counted per order in the split
and split_deferred stats under hugepages-<size>/stats/.

Do the same for underused splits. Only count PMD-sized folios in
thp_underused_split_page, and add a per-order split_underused stat that
counts underused splits of every size. The folio's order has to be read
before splitting it, since the folio is no longer large once the split
succeeds.

There is no functional change to thp_underused_split_page yet, as only
PMD-sized folios are split for being underused today.

Suggested-by: Kiryl Shutsemau <kas@kernel.org>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 Documentation/admin-guide/mm/transhuge.rst |  6 ++++++
 include/linux/huge_mm.h                    |  1 +
 mm/huge_memory.c                           | 10 ++++++++--
 3 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/Documentation/admin-guide/mm/transhuge.rst b/Documentation/admin-guide/mm/transhuge.rst
index b187d618452f..6f2d92a2a556 100644
--- a/Documentation/admin-guide/mm/transhuge.rst
+++ b/Documentation/admin-guide/mm/transhuge.rst
@@ -719,6 +719,12 @@ split_deferred
         it would free up some memory. Pages on split queue are going to
         be split under memory pressure, if splitting is possible.
 
+split_underused
+	is incremented when a huge page on the split queue was split
+	because it was underused. This happens when the number of zero-filled
+	pages in the huge page is above a certain threshold scaled from
+	/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none.
+
 nr_anon
        the number of anonymous THP we have in the whole system. These THPs
        might be currently entirely mapped or have partially unmapped/unused
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index c745f7ad2298..421217aca4ed 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -142,6 +142,7 @@ enum mthp_stat_item {
 	MTHP_STAT_SPLIT,
 	MTHP_STAT_SPLIT_FAILED,
 	MTHP_STAT_SPLIT_DEFERRED,
+	MTHP_STAT_SPLIT_UNDERUSED,
 	MTHP_STAT_NR_ANON,
 	MTHP_STAT_NR_ANON_PARTIALLY_MAPPED,
 	MTHP_STAT_COLLAPSE_EXCEED_SWAP,
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 903bbe972011..8f90fb609e57 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -822,6 +822,7 @@ DEFINE_MTHP_STAT_ATTR(shmem_fallback_charge, MTHP_STAT_SHMEM_FALLBACK_CHARGE);
 DEFINE_MTHP_STAT_ATTR(split, MTHP_STAT_SPLIT);
 DEFINE_MTHP_STAT_ATTR(split_failed, MTHP_STAT_SPLIT_FAILED);
 DEFINE_MTHP_STAT_ATTR(split_deferred, MTHP_STAT_SPLIT_DEFERRED);
+DEFINE_MTHP_STAT_ATTR(split_underused, MTHP_STAT_SPLIT_UNDERUSED);
 DEFINE_MTHP_STAT_ATTR(nr_anon, MTHP_STAT_NR_ANON);
 DEFINE_MTHP_STAT_ATTR(nr_anon_partially_mapped, MTHP_STAT_NR_ANON_PARTIALLY_MAPPED);
 DEFINE_MTHP_STAT_ATTR(collapse_exceed_swap_pte, MTHP_STAT_COLLAPSE_EXCEED_SWAP);
@@ -842,6 +843,7 @@ static struct attribute *anon_stats_attrs[] = {
 	&swpout_fallback_attr.attr,
 #endif
 	&split_deferred_attr.attr,
+	&split_underused_attr.attr,
 	&nr_anon_attr.attr,
 	&nr_anon_partially_mapped_attr.attr,
 	&collapse_exceed_swap_pte_attr.attr,
@@ -4703,6 +4705,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 					    deferred_split_isolate, &dispose);
 
 	list_for_each_entry_safe(folio, next, &dispose, _deferred_list) {
+		const unsigned int order = folio_order(folio);
 		bool did_split = false;
 		bool underused = false;
 
@@ -4724,8 +4727,11 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
 			goto requeue;
 		if (!split_folio(folio)) {
 			did_split = true;
-			if (underused)
-				count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
+			if (underused) {
+				if (is_pmd_order(order))
+					count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
+				count_mthp_stat(order, MTHP_STAT_SPLIT_UNDERUSED);
+			}
 			split++;
 		}
 		folio_unlock(folio);
-- 
2.52.0



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

* [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list
  2026-11-03 21:20 [PATCH v4 0/4] mm: split underused anonymous mTHP folios Joanne Koong
                   ` (2 preceding siblings ...)
  2026-11-03 21:20 ` [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits Joanne Koong
@ 2026-11-03 21:20 ` Joanne Koong
  2026-11-03 22:20   ` Barry Song
  2026-11-04 21:21   ` sashiko-bot
  3 siblings, 2 replies; 9+ messages in thread
From: Joanne Koong @ 2026-11-03 21:20 UTC (permalink / raw)
  To: akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

Unlike for PMD-sized folios, an anonymous mTHP folio doesn't get added
to the deferred split list at fault or collapse time. As a result, a
fully mapped mTHP folio that is mostly zero-filled doesn't get split by
the deferred split shrinker when the system is under memory pressure.

At Meta we would like to deploy 2M THP=always on arm64 with 64k base
pages, as 2M gives the contpte benefits while the PMD size (512M) is too
big to use. Without underused splitting this causes memory regressions,
as the unused parts of those folios can never be broken down and
reclaimed.

Add anonymous mTHP folios to the deferred split list from
map_anon_folio_pte_nopf(), mirroring what map_anon_folio_pmd_nopf()
already does for PMD-sized folios. This covers both the fault path and
the khugepaged mTHP collapse path. If there is memory pressure, a
zero-filled mTHP can then be split with its zero pages remapped to the
shared zero page and reclaimed.

Only queue mTHP folios of at least 2M for underused splitting. With
thp_underused() scaling khugepaged/max_ptes_none to the folio size, any
folio of order 2 or above could in principle be underused, but for small
folios it is unlikely to be worth it. An order-2 folio on 4k base pages
can free at most four pages and on systems that only enable small mTHP
sizes (eg Android's order-2-only configuration) zero-filled subpages
tend to be few and short-lived.

The minimum is hard-coded rather than a knob, since admins have no good
way of choosing it, and is a size rather than an order since what a
split recovers depends on the folio's size in bytes, and the same order
means very different sizes across different base page sizes. It can be
lowered later if data shows smaller sizes are worth it. PMD-sized folios
are exempt from the minimum in order to maintain backwards compatibility
since on some architectures (s390, with a 1M PMD) the PMD size can be
below 2M.

To minimize overhead on the common order-0 fault path, the
deferred_split_folio() call is guarded by an inline folio_test_large()
check.

Suggested-by: Usama Arif <usama.arif@linux.dev>
Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
---
 mm/huge_memory.c | 14 ++++++++++++++
 mm/memory.c      |  2 ++
 2 files changed, 16 insertions(+)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 8f90fb609e57..cda3c886ec53 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -4575,6 +4575,13 @@ static bool thp_underused_never(unsigned long max_ptes_none)
 	return max_ptes_none == HPAGE_PMD_NR - 1;
 }
 
+/*
+ * Smallest mTHP folio that is queued for underused splitting. Kept
+ * conservative, as small folios are unlikely to free enough memory to be
+ * worth queueing and scanning.
+ */
+#define THP_UNDERUSED_MIN_SIZE	SZ_2M
+
 /* partially_mapped=false won't clear PG_partially_mapped folio flag */
 void deferred_split_folio(struct folio *folio, bool partially_mapped)
 {
@@ -4600,6 +4607,13 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
 		 */
 		if (thp_underused_never(khugepaged_max_ptes_none))
 			return;
+		/*
+		 * Keep queueing PMD-sized folios as before, even where the PMD
+		 * size is below the minimum (eg 1M on s390).
+		 */
+		if (folio_size(folio) < THP_UNDERUSED_MIN_SIZE &&
+		    !is_pmd_order(folio_order(folio)))
+			return;
 	}
 
 	/*
diff --git a/mm/memory.c b/mm/memory.c
index 0c9ed04528d8..44376321180b 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -5477,6 +5477,8 @@ void map_anon_folio_pte_nopf(struct folio *folio, pte_t *pte,
 	folio_add_lru_vma(folio, vma);
 	set_ptes(vma->vm_mm, addr, pte, entry, nr_pages);
 	update_mmu_cache_range(NULL, vma, addr, pte, nr_pages);
+	if (folio_test_large(folio))
+		deferred_split_folio(folio, false);
 }
 
 static void map_anon_folio_pte_pf(struct folio *folio, pte_t *pte,
-- 
2.52.0



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

* Re: [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive
  2026-11-03 21:20 ` [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive Joanne Koong
@ 2026-11-03 22:15   ` Usama Arif
  0 siblings, 0 replies; 9+ messages in thread
From: Usama Arif @ 2026-11-03 22:15 UTC (permalink / raw)
  To: Joanne Koong, akpm, david, ljs
  Cc: baohua, hannes, kas, alex, ziy, baolin.wang, liam, npache,
	ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb, mhocko,
	willy, linux-mm



On 03/11/2026 21:20, Joanne Koong wrote:
> Every anonymous PMD-sized folio is put on the deferred split queue when
> it is first mapped, so that the shrinker can find it under memory
> pressure and split it if it turns out to be mostly zero-filled.
> 
> With the default khugepaged/max_ptes_none this is wasted work. The
> default is HPAGE_PMD_NR - 1, which tells thp_underused() that any number
> of zero-filled pages is tolerable, which means the shrinker will never
> split any of these folios for being underused. They take the list_lru
> lock at fault time, inflate the object count the shrinker reports, and
> are then walked and dropped when they're first scanned.
> 
> Skip the queueing when khugepaged/max_ptes_none is at its maximum, and
> factor that check out into thp_underused_never() so that
> deferred_split_folio() and thp_underused() share it. Since the default
> is the maximum, out of the box no folio is queued for underused
> splitting at all, PMD-sized ones included. Partially mapped folios are
> still queued as before.
> 
> This also matters for a subsequent patch that adds anonymous mTHP folios
> to the deferred split list, as it keeps systems running the default from
> paying for that on every anonymous fault.
> 
> Please note that the queue has always been best-effort. A folio that was
> queued before khugepaged/max_ptes_none is lowered gets dropped from the
> queue by the first scan that finds it's not underused, so changing the
> sysctl has never retroactively applied to folios that were already
> scanned. Requeueing eligible folios when the sysctl changes will be
> addressed in a separate patch.
> 
> Suggested-by: Johannes Weiner <hannes@cmpxchg.org>
> Suggested-by: David Hildenbrand <david@kernel.org>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---

This sounds reasonable. In production, as far as I know from speaking with folks
in conferences, no one changes the value during application runtime
(atleast this is true in meta). Its usually only changed before the workload
starts (or just at boot time).

Acked-by: Usama Arif <usama.arif@linux.dev>

>  mm/huge_memory.c | 24 +++++++++++++++++++++---
>  1 file changed, 21 insertions(+), 3 deletions(-)
> 
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index 3720c12bdb0d..903bbe972011 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -4564,6 +4564,15 @@ bool __folio_unqueue_deferred_split(struct folio *folio)
>  	return unqueued;	/* useful for debug warnings */
>  }
>  
> +/*
> + * At the sysctl maximum the user tolerates any number of zero-filled pages,
> + * so no folio is ever underused.
> + */
> +static bool thp_underused_never(unsigned long max_ptes_none)
> +{
> +	return max_ptes_none == HPAGE_PMD_NR - 1;
> +}
> +
>  /* partially_mapped=false won't clear PG_partially_mapped folio flag */
>  void deferred_split_folio(struct folio *folio, bool partially_mapped)
>  {
> @@ -4579,8 +4588,17 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
>  	if (folio_order(folio) <= 1)
>  		return;
>  
> -	if (!partially_mapped && !split_underused_thp)
> -		return;
> +	if (!partially_mapped) {
> +		if (!split_underused_thp)
> +			return;
> +		/*
> +		 * Nothing will ever split this folio for being underused, so
> +		 * keep it off the queue entirely rather than paying for the
> +		 * list_lru lock here and a shrinker scan later.
> +		 */
> +		if (thp_underused_never(khugepaged_max_ptes_none))
> +			return;
> +	}
>  
>  	/*
>  	 * Exclude swapcache: originally to avoid a corrupt deferred split
> @@ -4630,7 +4648,7 @@ static bool thp_underused(struct folio *folio)
>  	unsigned long max_ptes_none = khugepaged_max_ptes_none;
>  	unsigned long i;
>  
> -	if (max_ptes_none == HPAGE_PMD_NR - 1)
> +	if (thp_underused_never(max_ptes_none))
>  		return false;
>  
>  	if (folio_contain_hwpoisoned_page(folio))



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

* Re: [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list
  2026-11-03 21:20 ` [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list Joanne Koong
@ 2026-11-03 22:20   ` Barry Song
  2026-11-04 21:21   ` sashiko-bot
  1 sibling, 0 replies; 9+ messages in thread
From: Barry Song @ 2026-11-03 22:20 UTC (permalink / raw)
  To: Joanne Koong
  Cc: akpm, david, ljs, usama.arif, hannes, kas, alex, ziy, baolin.wang,
	liam, npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt,
	surenb, mhocko, willy, linux-mm

On Wed, Nov 4, 2026 at 5:20 AM Joanne Koong <joannelkoong@gmail.com> wrote:
>
> Unlike for PMD-sized folios, an anonymous mTHP folio doesn't get added
> to the deferred split list at fault or collapse time. As a result, a
> fully mapped mTHP folio that is mostly zero-filled doesn't get split by
> the deferred split shrinker when the system is under memory pressure.
>
> At Meta we would like to deploy 2M THP=always on arm64 with 64k base
> pages, as 2M gives the contpte benefits while the PMD size (512M) is too
> big to use. Without underused splitting this causes memory regressions,
> as the unused parts of those folios can never be broken down and
> reclaimed.
>
> Add anonymous mTHP folios to the deferred split list from
> map_anon_folio_pte_nopf(), mirroring what map_anon_folio_pmd_nopf()
> already does for PMD-sized folios. This covers both the fault path and
> the khugepaged mTHP collapse path. If there is memory pressure, a
> zero-filled mTHP can then be split with its zero pages remapped to the
> shared zero page and reclaimed.
>
> Only queue mTHP folios of at least 2M for underused splitting. With
> thp_underused() scaling khugepaged/max_ptes_none to the folio size, any
> folio of order 2 or above could in principle be underused, but for small
> folios it is unlikely to be worth it. An order-2 folio on 4k base pages
> can free at most four pages and on systems that only enable small mTHP
> sizes (eg Android's order-2-only configuration) zero-filled subpages
> tend to be few and short-lived.
>
> The minimum is hard-coded rather than a knob, since admins have no good
> way of choosing it, and is a size rather than an order since what a
> split recovers depends on the folio's size in bytes, and the same order
> means very different sizes across different base page sizes. It can be
> lowered later if data shows smaller sizes are worth it. PMD-sized folios
> are exempt from the minimum in order to maintain backwards compatibility
> since on some architectures (s390, with a 1M PMD) the PMD size can be
> below 2M.
>
> To minimize overhead on the common order-0 fault path, the
> deferred_split_folio() call is guarded by an inline folio_test_large()
> check.
>
> Suggested-by: Usama Arif <usama.arif@linux.dev>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---

I believe we discussed this before and agreed that a
hardcoded 2MB would be a better starting point.

Reviewed-by: Barry Song <baohua@kernel.org>


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

* Re: [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits
  2026-11-03 21:20 ` [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits Joanne Koong
@ 2026-11-03 22:20   ` Usama Arif
  0 siblings, 0 replies; 9+ messages in thread
From: Usama Arif @ 2026-11-03 22:20 UTC (permalink / raw)
  To: Joanne Koong, akpm, david, ljs
  Cc: baohua, hannes, kas, alex, ziy, baolin.wang, liam, npache,
	ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb, mhocko,
	willy, linux-mm



On 03/11/2026 21:20, Joanne Koong wrote:
> thp_underused_split_page counts every folio that the deferred split
> shrinker splits for being underused. Today that is only ever a
> PMD-sized folio, but a later patch queues anonymous mTHP folios for
> underused splitting too, at which point this counter would start
> counting mTHP splits as well.
> 
> thp_underused_split_page could then end up larger than thp_split_page,
> and there would be no per-order count of underused splits. That doesn't
> match how the other THP split stats work. In /proc/vmstat,
> thp_split_page and thp_deferred_split_page only count PMD-sized folios.
> Splits of every size, PMD included, are counted per order in the split
> and split_deferred stats under hugepages-<size>/stats/.
> 
> Do the same for underused splits. Only count PMD-sized folios in
> thp_underused_split_page, and add a per-order split_underused stat that
> counts underused splits of every size. The folio's order has to be read
> before splitting it, since the folio is no longer large once the split
> succeeds.
> 
> There is no functional change to thp_underused_split_page yet, as only
> PMD-sized folios are split for being underused today.
> 
> Suggested-by: Kiryl Shutsemau <kas@kernel.org>
> Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
> ---
>  Documentation/admin-guide/mm/transhuge.rst |  6 ++++++
>  include/linux/huge_mm.h                    |  1 +
>  mm/huge_memory.c                           | 10 ++++++++--
>  3 files changed, 15 insertions(+), 2 deletions(-)
> 
> diff --git a/Documentation/admin-guide/mm/transhuge.rst b/Documentation/admin-guide/mm/transhuge.rst
> index b187d618452f..6f2d92a2a556 100644
> --- a/Documentation/admin-guide/mm/transhuge.rst
> +++ b/Documentation/admin-guide/mm/transhuge.rst
> @@ -719,6 +719,12 @@ split_deferred
>          it would free up some memory. Pages on split queue are going to
>          be split under memory pressure, if splitting is possible.
>  

In this same commit, would you mind also doing s/THP/PMD mapped THP/ in
thp_underused_split_page section in /proc/vmstat section?

Apart from that, LGTM

Acked-by: Usama Arif <usama.arif@linux.dev>

> +split_underused
> +	is incremented when a huge page on the split queue was split
> +	because it was underused. This happens when the number of zero-filled
> +	pages in the huge page is above a certain threshold scaled from
> +	/sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none.
> +
>  nr_anon
>         the number of anonymous THP we have in the whole system. These THPs
>         might be currently entirely mapped or have partially unmapped/unused
> diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
> index c745f7ad2298..421217aca4ed 100644
> --- a/include/linux/huge_mm.h
> +++ b/include/linux/huge_mm.h
> @@ -142,6 +142,7 @@ enum mthp_stat_item {
>  	MTHP_STAT_SPLIT,
>  	MTHP_STAT_SPLIT_FAILED,
>  	MTHP_STAT_SPLIT_DEFERRED,
> +	MTHP_STAT_SPLIT_UNDERUSED,
>  	MTHP_STAT_NR_ANON,
>  	MTHP_STAT_NR_ANON_PARTIALLY_MAPPED,
>  	MTHP_STAT_COLLAPSE_EXCEED_SWAP,
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index 903bbe972011..8f90fb609e57 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -822,6 +822,7 @@ DEFINE_MTHP_STAT_ATTR(shmem_fallback_charge, MTHP_STAT_SHMEM_FALLBACK_CHARGE);
>  DEFINE_MTHP_STAT_ATTR(split, MTHP_STAT_SPLIT);
>  DEFINE_MTHP_STAT_ATTR(split_failed, MTHP_STAT_SPLIT_FAILED);
>  DEFINE_MTHP_STAT_ATTR(split_deferred, MTHP_STAT_SPLIT_DEFERRED);
> +DEFINE_MTHP_STAT_ATTR(split_underused, MTHP_STAT_SPLIT_UNDERUSED);
>  DEFINE_MTHP_STAT_ATTR(nr_anon, MTHP_STAT_NR_ANON);
>  DEFINE_MTHP_STAT_ATTR(nr_anon_partially_mapped, MTHP_STAT_NR_ANON_PARTIALLY_MAPPED);
>  DEFINE_MTHP_STAT_ATTR(collapse_exceed_swap_pte, MTHP_STAT_COLLAPSE_EXCEED_SWAP);
> @@ -842,6 +843,7 @@ static struct attribute *anon_stats_attrs[] = {
>  	&swpout_fallback_attr.attr,
>  #endif
>  	&split_deferred_attr.attr,
> +	&split_underused_attr.attr,
>  	&nr_anon_attr.attr,
>  	&nr_anon_partially_mapped_attr.attr,
>  	&collapse_exceed_swap_pte_attr.attr,
> @@ -4703,6 +4705,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
>  					    deferred_split_isolate, &dispose);
>  
>  	list_for_each_entry_safe(folio, next, &dispose, _deferred_list) {
> +		const unsigned int order = folio_order(folio);
>  		bool did_split = false;
>  		bool underused = false;
>  
> @@ -4724,8 +4727,11 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
>  			goto requeue;
>  		if (!split_folio(folio)) {
>  			did_split = true;
> -			if (underused)
> -				count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
> +			if (underused) {
> +				if (is_pmd_order(order))
> +					count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
> +				count_mthp_stat(order, MTHP_STAT_SPLIT_UNDERUSED);
> +			}
>  			split++;
>  		}
>  		folio_unlock(folio);



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

* Re: [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list
  2026-11-03 21:20 ` [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list Joanne Koong
  2026-11-03 22:20   ` Barry Song
@ 2026-11-04 21:21   ` sashiko-bot
  1 sibling, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-11-04 21:21 UTC (permalink / raw)
  To: Joanne Koong, akpm, david, ljs
  Cc: baohua, usama.arif, hannes, kas, alex, ziy, baolin.wang, liam,
	npache, ryan.roberts, dev.jain, lance.yang, vbabka, rppt, surenb,
	mhocko, willy, linux-mm

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] mm: try_to_migrate_one() triggers deferred_split_folio() on underused mTHP
--

commit 82b7b7063b986d802fe52e75e17211e58f3f420c
Author: Joanne Koong <joannelkoong@gmail.com>

mm: add anonymous mTHP folios to the deferred split list

This commit queues newly mapped anonymous mTHP folios of at least 2M onto the
deferred split list in map_anon_folio_pte_nopf() so that underused mTHP folios
can be split by the deferred split shrinker under memory pressure.

> diff --git a/mm/memory.c b/mm/memory.c
> index 0c9ed04528d88..44376321180b5 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -5477,6 +5477,8 @@ void map_anon_folio_pte_nopf(struct folio *folio, pte_t *pte,
>  	folio_add_lru_vma(folio, vma);
>  	set_ptes(vma->vm_mm, addr, pte, entry, nr_pages);
>  	update_mmu_cache_range(NULL, vma, addr, pte, nr_pages);
> +	if (folio_test_large(folio))
> +		deferred_split_folio(folio, false);
>  }

[Severity: High]
When deferred_split_scan() splits an underused PTE-mapped mTHP folio queued
here with PG_partially_mapped left clear, doesn't unmap_folio() trigger a
false-positive deferred_split_folio(folio, true) transition during the split?

In deferred_split_scan(), the folio is first removed from the local dispose
list while PG_partially_mapped is still false:

mm/huge_memory.c:deferred_split_scan()
	list_del_init(&folio->_deferred_list);

	if (!folio_test_partially_mapped(folio)) {
		if (folio_test_mlocked(folio))
			goto next;
		underused = thp_underused(folio);
		if (!underused)
			goto next;
	}
	if (!folio_trylock(folio))
		goto requeue;
	if (!split_folio(folio)) {

split_folio() then calls __folio_split() -> __folio_freeze_split_anon(), which
unmaps the folio before freezing its refcount:

mm/huge_memory.c:__folio_freeze_split_anon()
	if (folio_mapped(folio)) {
		anon_vma = folio_get_anon_vma(folio);
		if (!anon_vma)
			return -EBUSY;
		anon_vma_lock_write(anon_vma);
		ret = unmap_folio(folio);
		if (ret)
			goto out_unlock;
	}

	local_irq_disable();

	if (!folio_ref_freeze(folio, folio_swapcache_ref_count(folio) + 1)) {
		ret = -EAGAIN;
		goto out_no_split;
	}

Unlike a PMD-mapped THP (where split_huge_pmd_locked() with freeze=true
removes the entire PMD rmap in one folio_remove_rmap_pmd() call), an mTHP
folio is PTE-mapped, so unmap_folio() -> try_to_migrate() ->
try_to_migrate_one() removes its PTEs one subpage at a time via
folio_remove_rmap_pte().

On the first subpage unmapped while the remaining subpages are still mapped,
__folio_remove_rmap() sees partially_mapped == true and
!folio_test_partially_mapped(folio) == true:

mm/rmap.c:__folio_remove_rmap()
	if (partially_mapped && folio_test_anon(folio) &&
	    !folio_test_partially_mapped(folio) &&
	    !folio_is_device_private(folio))
		deferred_split_folio(folio, true);

Because folio->_deferred_list was emptied by list_del_init() in
deferred_split_scan() and the folio refcount is not frozen yet,
deferred_split_folio(folio, true) sets PG_partially_mapped, increments
MTHP_STAT_SPLIT_DEFERRED and MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, and re-adds
folio->_deferred_list onto deferred_split_lru.

Could this cause three problems?

1. Even when folio_ref_freeze() succeeds and __folio_freeze_split_anon()
   subsequently unqueues the folio and resets PG_partially_mapped, every
   underused mTHP split will also increment MTHP_STAT_SPLIT_DEFERRED in
   addition to MTHP_STAT_SPLIT_UNDERUSED.

2. If folio_ref_freeze() fails (for example due to a concurrent GUP pin, or
   because a second CPU running deferred_split_scan() isolated the re-queued
   folio via folio_try_get() while the remaining PTEs were still being
   unmapped), __folio_freeze_split_anon() jumps to out_no_split and remaps all
   PTEs via remap_anon_folio() without unqueueing the folio or clearing
   PG_partially_mapped. That leaves a fully mapped folio permanently marked
   PG_partially_mapped with MTHP_STAT_NR_ANON_PARTIALLY_MAPPED leaked.

3. If a second CPU running deferred_split_scan() isolates the re-queued folio
   onto its local dispose list while unmap_folio() is still in progress, that
   extra refcount causes folio_ref_freeze() on the first CPU to fail. When the
   first CPU returns to deferred_split_scan(), it sees !did_split &&
   folio_test_partially_mapped(folio) == true and calls list_lru_add_irq() on
   folio->_deferred_list while the folio is on the second CPU's unlocked
   dispose list, corrupting the list.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261103212004.1356327-1-joannelkoong@gmail.com?part=4


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

end of thread, other threads:[~2026-11-04 21:21 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 21:20 [PATCH v4 0/4] mm: split underused anonymous mTHP folios Joanne Koong
2026-11-03 21:20 ` [PATCH v4 1/4] mm/huge_memory: scale thp_underused() to the folio size Joanne Koong
2026-11-03 21:20 ` [PATCH v4 2/4] mm/huge_memory: don't queue folios when underused splitting is inactive Joanne Koong
2026-11-03 22:15   ` Usama Arif
2026-11-03 21:20 ` [PATCH v4 3/4] mm/huge_memory: add a per-order counter for underused splits Joanne Koong
2026-11-03 22:20   ` Usama Arif
2026-11-03 21:20 ` [PATCH v4 4/4] mm: add anonymous mTHP folios to the deferred split list Joanne Koong
2026-11-03 22:20   ` Barry Song
2026-11-04 21:21   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox