[PATCH v6 00/17] mm/huge_memory: clean up and decouple the anon and file split helpers

Kairui Song via B4 Relay posted 17 patches 16 hours ago
mm/huge_memory.c | 584 +++++++++++++++++++++++++++++--------------------------
mm/swap_state.c  |   3 +-
2 files changed, 306 insertions(+), 281 deletions(-)
[PATCH v6 00/17] mm/huge_memory: clean up and decouple the anon and file split helpers
Posted by Kairui Song via B4 Relay 16 hours ago
The folio split path handles anon, page cache and swap cache folios in
one routine.  That mixing is what makes the swap cache split
restrictions hard to lift and to review. We now support uniform split to
order-0 only, and no mappingless swap cache folios. And it has left a fair
number of dead or redundant checks behind.

This series prepares for lifting those restrictions by cleaning up the
code first: split the routine into an anon and a file helper, and keep
all swap cache handling in the anon helper.  The file helper never sees
a swap cache folio, folio_check_splittable() rejects them up front.

Apart from two bug fixes (patch 1 and 2) and a slight adjustment of anon
splitting (patch 12), this is a pure cleanup.

Testing:

The in-tree split_huge_page_test selftest (uniform, non-uniform and
in-folio-offset splits of anon and pagecache folios) passes 62/62 over
600 runs on the patched kernel.

ftrace function_graph tracing filtered on __folio_split() was used to
compare per-call durations between the base and the patched kernel on
the same x86-64 box (interleaved runs across alternating reboots.
135 split calls per run, 600 test runs):

Before: 68.52 us, stddev: 1.58
After:  67.38 us, stddev: 1.33

The patched kernel is slightly faster. The stack usage and object size
change as the config and compiler change, but in general the stack
usage is reduced and object size is basically unchanged.

Signed-off-by: Kairui Song <kasong@tencent.com>
---
Changes in v6:
- Comments updated and make a few variables const as suggested by
  [David Hildenbrand]
- Link to v5: https://patch.msgid.link/20260915-swap-thp-cleanup-v5-0-39878b37dfb0@tencent.com

Changes in v5:
- Rebase onto mm-new.
- Rename remap_folio() to remap_anon_folio(), and reword the changelog
  to explain why only anon folios are remapped. [David Hildenbrand,
  Kiryl Shutsemau]
- Expand the folio_mapped() comment in the anon helper, and drop the
  folio_lock_anon_vma_read() reference from the changelog.
  [Zi Yan, Kiryl Shutsemau]
- Kept the old test result and the code change is identical, and initial
  test matches previous results.
- Clarify the after-split folio freeing changelog.
- Collect Reviewed-by/Acked-by tags.
- Link to v4: https://patch.msgid.link/20260908-swap-thp-cleanup-v4-0-b532a3f20e71@tencent.com

Changes in v4:
- Rebase on top of mm-unstable, also mergable on mm-new now.
- Patch 12 might need some attention as we can skip anon_vma locking for
  unmapped anon folio now due to further cleanup, including unmapped anon
  swap cache (not mappingless swap cache).
- Drop the two swap cache behavior changes, will be sent separately once
  this lands, so it gets its own attention. Also drop related doc
  updates. [ David Hildenbrand ]
- Rename the split helpers to __folio_freeze_split_anon() and
  __folio_freeze_split_file(), frozen implies unmapped.
  [ Kiryl Shutsemau, David Hildenbrand ]
- Keep zone_device_private_split_cb() in the anon helper only, and add
  VM_WARN_ON_ONCE_FOLIO(folio_is_device_private()) to the file helper:
  device private folios can only back anonymous memory.
  [ Kiryl Shutsemau ]
- Replace the "bool unmap" parameter with a folio_mapped(), rename
  remap_page() to remap_folio() and document it. [ David Hildenbrand ]
- Move the racy folio_expected_ref_count() check into unmap_folio()
  instead of duplicating it.
- Make more locals const.
- Reword the mm/swap off-by-one fix: non-uniform swap cache split is not
  enabled yet, so the bogus sanity check is currently dead code.
- Collect Reviewed-by/Acked-by tags.
- Link to v3: https://patch.msgid.link/20260821-swap-thp-cleanup-v3-0-9b43f5163238@tencent.com

Changes in v3:
- Get rid of for_each_folio_safe and open code it.
- Check if the folio is mapped before freeing it swap cache to avoid
  potential performance lose.
- Initial test and binary analyze showed everything is very similiar to
  previously series.
- Drop the redundant mapping argument of __split_frozen_folio
- Link to v2: https://patch.msgid.link/20260813-swap-thp-cleanup-v2-0-d2ee48c6aa49@tencent.com

Changes in v2:
- Return -EBUSY instead of -EINVAL for swap cache & shmem folio split
  attempt.
- Introduce a for_each_folio_safe macro to dedupliate the code and
  hightlight the reason we need to keep the iterate safe from folio
  freeing. [ Zi Yan ]
- Rename __split_unmapped_folio() to __split_frozen_folio [ Zi Yan ]
- Rename __folio_freeze_split_unmap_anon. [ Zi Yan ]
- Several comment improments [ Zi Yan ]
- Drop an unused do_lru argument.
- Previouse test results are basically unchanged, stack usage reduced,
  object very slightly larger.
- Link to v1: https://patch.msgid.link/20260808-swap-thp-cleanup-v1-0-689939a7ccc3@tencent.com

---
Kairui Song (17):
      mm/swap: fix off-by-one in swap cache replace sanity check
      mm/huge_memory: fix rejection of swap cache folios with a mapping
      mm/huge_memory: invert folio_ref_freeze() check to reduce indentation
      mm/huge_memory: split the routine for splitting anon and file folio
      mm/huge_memory: rename __split_unmapped_folio() to __split_frozen_folio()
      mm/huge_memory: consolidate irq and locking for folio split
      mm/huge_memory: move EOF trimming into the file split helper
      mm/huge_memory: move unmap and remap into the split helpers
      mm/huge_memory: rename remap_page() to remap_anon_folio()
      mm/huge_memory: move the racy refcount check into unmap_folio()
      mm/huge_memory: move filemap management into the file split helper
      mm/huge_memory: move anon_vma handling into the anon split helper
      mm/huge_memory: move memcg switch into the file split helper
      mm/huge_memory: drop the unused do_lru argument of the file split helper
      mm/huge_memory: clean up after-split folio freeing in __folio_split
      mm/huge_memory: count only swap cache refs in anon folio split
      mm/huge_memory: drop the redundant mapping argument of __split_frozen_folio

 mm/huge_memory.c | 584 +++++++++++++++++++++++++++++--------------------------
 mm/swap_state.c  |   3 +-
 2 files changed, 306 insertions(+), 281 deletions(-)
---
base-commit: 55f19451cabacdf97712a9c26a3def6d3ff8ff56
change-id: 20260804-swap-thp-cleanup-6ce2be6cf3b8

Best regards,
--  
Kairui Song <kasong@tencent.com>
Re: [PATCH v6 00/17] mm/huge_memory: clean up and decouple the anon and file split helpers
Posted by Andrew Morton 15 hours ago
On Wed, 23 Sep 2026 23:32:31 +0200 Kairui Song via B4 Relay <devnull+kasong.tencent.com@kernel.org> wrote:

> The folio split path handles anon, page cache and swap cache folios in
> one routine.  That mixing is what makes the swap cache split
> restrictions hard to lift and to review. We now support uniform split to
> order-0 only, and no mappingless swap cache folios. And it has left a fair
> number of dead or redundant checks behind.
> 
> This series prepares for lifting those restrictions by cleaning up the
> code first: split the routine into an anon and a file helper, and keep
> all swap cache handling in the anon helper.  The file helper never sees
> a swap cache folio, folio_check_splittable() rejects them up front.
> 
> Apart from two bug fixes (patch 1 and 2) and a slight adjustment of anon
> splitting (patch 12), this is a pure cleanup.

Thanks, I've updated mm.git's mm-unstable branch to this version.

> Changes in v6:
> - Comments updated and make a few variables const as suggested by
>   [David Hildenbrand]
> - Link to v5: https://patch.msgid.link/20260915-swap-thp-cleanup-v5-0-39878b37dfb0@tencent.com

Here's how v6 altered mm.git:


 mm/huge_memory.c |   19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

--- a/mm/huge_memory.c~b
+++ a/mm/huge_memory.c
@@ -3538,6 +3538,11 @@ void vma_adjust_trans_huge(struct vm_are
 		split_huge_pmd_if_needed(next, end);
 }
 
+/*
+ * A return value of 0 does not mean that unmapping succeeded. It might
+ * still have failed, but remap_anon_folio() must be called afterwards,
+ * for anon folios.
+ */
 static int unmap_folio(struct folio *folio)
 {
 	enum ttu_flags ttu_flags = TTU_RMAP_LOCKED | TTU_SYNC |
@@ -3555,7 +3560,6 @@ static int unmap_folio(struct folio *fol
 	/*
 	 * Anon pages need migration entries to preserve them, but file
 	 * pages can simply be left unmapped, then faulted back on demand.
-	 * If that is ever changed (perhaps for mlock), update remap_anon_folio().
 	 */
 	if (folio_test_anon(folio))
 		try_to_migrate(folio, ttu_flags);
@@ -4025,7 +4029,7 @@ static int __folio_freeze_split_anon(str
 {
 	struct folio *end_folio = folio_next(folio);
 	struct swap_cluster_info *ci = NULL;
-	int old_order = folio_order(folio);
+	const int old_order = folio_order(folio);
 	struct folio *new_folio, *next;
 	struct anon_vma *anon_vma = NULL;
 	enum ttu_flags ttu_flags = 0;
@@ -4048,8 +4052,7 @@ static int __folio_freeze_split_anon(str
 	 * the reference nor the lock. Anything else needs a reference
 	 * first and folio_ref_freeze() below catches it.
 	 *
-	 * Note a swapped-out THP counts as unmapped here as swap PTEs do
-	 * not contribute mapcount, and they are splittable.
+	 * Note that entirely swapped-out THPs are unmapped but can be split.
 	 */
 	if (folio_mapped(folio)) {
 		anon_vma = folio_get_anon_vma(folio);
@@ -4133,10 +4136,10 @@ static int __folio_freeze_split_file(str
 		unsigned int new_order, struct page *split_at,
 		struct list_head *list, enum split_type split_type)
 {
+	const long old_nr_pages = folio_nr_pages(folio);
 	struct address_space *mapping = folio->mapping;
 	XA_STATE(xas, &mapping->i_pages, folio->index);
 	struct folio *end_folio = folio_next(folio);
-	long old_nr_pages = folio_nr_pages(folio);
 	struct mem_cgroup *memcg, *old_memcg;
 	struct folio *new_folio, *next;
 	int nr_shmem_dropped = 0;
@@ -4166,7 +4169,7 @@ static int __folio_freeze_split_file(str
 	mapping_set_update(&xas, mapping);
 
 	if (split_type == SPLIT_TYPE_UNIFORM) {
-		int old_order = folio_order(folio);
+		const int old_order = folio_order(folio);
 
 		xas_set_order(&xas, folio->index, new_order);
 		xas_split_alloc(&xas, folio, old_order, gfp);
@@ -4269,10 +4272,6 @@ static int __folio_freeze_split_file(str
 	folio_ref_unfreeze(folio, folio_nr_pages(folio) + 1);
 	lruvec_unlock(lruvec);
 fail:
-	/*
-	 * If we want to use try_to_migrate() on file in unmap_folio,
-	 * remember to add remap_anon_folio() and adapt it.
-	 */
 	xas_unlock_irq(&xas);
 fail_mmap_unlock:
 	if (nr_shmem_dropped)
_