Re: [RFC] mm: restrict zero-page remapping to underused THP splits
From: Nico Pache
Date: Mon May 11 2026 - 14:44:27 EST
On Fri, May 8, 2026 at 9:22 PM Lance Yang <lance.yang@xxxxxxxxx> wrote:
>
>
> On Fri, May 08, 2026 at 11:05:09AM -0600, Nico Pache wrote:
> >Since commit b1f202060afe ("mm: remap unused subpages to shared zeropage
> >when splitting isolated thp"), splitting an anonymous THP remaps all
> >zero-filled subpages to the shared zeropage via TTU_USE_SHARED_ZEROPAGE.
> >This flag is set unconditionally for every anonymous folio split,
> >including splits triggered by KSM.
> >
> >When KSM is enabled with THP=always, this causes two regressions:
> >
> >1. use_zero_pages=1: KSM calls try_to_merge_one_page() which triggers
> > split_huge_page(). The split remaps all 512 zero-filled subpages to
> > the shared zeropage at once, freeing the entire 2MB THP when KSM only
> > intended to process a single 4KB page. This bypasses KSM's
> > pages_to_scan rate limiting, causing ~1GB to be freed almost
> > instantly.
> >
> >2. use_zero_pages=0: The same split side-effect occurs through the
> > stable/unstable tree merge paths. Each pages_to_scan iteration
> > triggers an expensive split_huge_page() that silently frees 2MB,
> > while the scanner wastes cycles on tree searches for zero-filled
> > pages that were already freed as a side-effect.
> >
> >Fix this by restricting TTU_USE_SHARED_ZEROPAGE to only the deferred
> >split shrinker path (deferred_split_scan), which is the only caller that
> >intentionally splits underused THPs to reclaim zero-filled subpages.
> >Introduce folio_split_underused() as a dedicated entry point that
> >passes is_underused_thp=true through __folio_split(), and use it from
> >deferred_split_scan(). All other split callers (KSM, compaction, etc.)
> >no longer get the zero-page remapping side-effect.
> >
> >Reviewers notes: this patch is one of two potential approaches. This patch
> >turns off the zero-page freeing that has been done since the noted commit,
> >in all the other callers, only leaving the underused shrinker to do such
> >behavior. We can also take the opposite approach of with something like
> >split_huge_page_no_zeropage() and call this within KSM.
> >
> >Fixes: b1f202060afe ("mm: remap unused subpages to shared zeropage when splitting isolated thp")
> >Signed-off-by: Nico Pache <npache@xxxxxxxxxx>
> >---
> > include/linux/huge_mm.h | 2 +-
> > mm/huge_memory.c | 17 ++++++++++++-----
> > 2 files changed, 13 insertions(+), 6 deletions(-)
> >
> >diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
> >index 2949e5acff35..4ae1b52d7411 100644
> >--- a/include/linux/huge_mm.h
> >+++ b/include/linux/huge_mm.h
> >@@ -378,7 +378,7 @@ int folio_check_splittable(struct folio *folio, unsigned int new_order,
> > enum split_type split_type);
> > int folio_split(struct folio *folio, unsigned int new_order, struct page *page,
> > struct list_head *list);
> >-
> >+int folio_split_underused(struct folio *folio);
> > static inline int split_huge_page_to_list_to_order(struct page *page, struct list_head *list,
> > unsigned int new_order)
> > {
> >diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> >index 970e077019b7..91f7fad72c8a 100644
> >--- a/mm/huge_memory.c
> >+++ b/mm/huge_memory.c
> >@@ -4045,7 +4045,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,
> >+ bool is_underused_thp)
> > {
> > XA_STATE(xas, &folio->mapping->i_pages, folio->index);
> > struct folio *end_folio = folio_next(folio);
> >@@ -4174,7 +4175,7 @@ static int __folio_split(struct folio *folio, unsigned int new_order,
> > if (nr_shmem_dropped)
> > shmem_uncharge(mapping->host, nr_shmem_dropped);
> >
> >- if (!ret && is_anon && !folio_is_device_private(folio))
> >+ if (!ret && is_anon && !folio_is_device_private(folio) && is_underused_thp)
> > ttu_flags = TTU_USE_SHARED_ZEROPAGE;
> >
> > remap_page(folio, 1 << old_order, ttu_flags);
> >@@ -4309,7 +4310,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, false);
> > }
> >
> > /**
> >@@ -4340,7 +4341,13 @@ 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, false);
> >+}
> >+
> >+int folio_split_underused(struct folio *folio)
> >+{
> >+ return __folio_split(folio, 0, &folio->page, &folio->page,
> >+ NULL, SPLIT_TYPE_NON_UNIFORM, true);
>
> IIUC, it should be SPLIT_TYPE_UNIFORM, not SPLIT_TYPE_NON_UNIFORM ...
Thats what i had originally then convinced myself otherwise. Ill
reverify before submitting again. Unless we just end up on a different
solution like David suggested.
Thanks!
-- Nico
>
> deferred_split_scan() used split_folio(), so for the underused case it
> split the whole THP uniformly down to order-0 pages. The shared zeropage
> remapping happens later, via remove_migration_ptes(), after the split.
>
> With SPLIT_TYPE_NON_UNIFORM and split_at == &folio->page, most of an
> order-9 THP can stays as larger folios.
>
> Then try_to_map_unused_to_zeropage() rejects those folios:
>
> if (PageCompound(page) || PageHWPoison(page))
> return false;
>
> So the underused shrinker would no longer remap/free many zero-filled
> subpages ...
>
> > }
> >
> > /**
> >@@ -4559,7 +4566,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
> > }
> > if (!folio_trylock(folio))
> > goto requeue;
> >- if (!split_folio(folio)) {
> >+ if (!folio_split_underused(folio)) {
> > did_split = true;
> > if (underused)
> > count_vm_event(THP_UNDERUSED_SPLIT_PAGE);
> >--
> >2.54.0
> >
> >
>