Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs
@ 2026-08-18 13:09 Usama Arif
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
                   ` (11 more replies)
  0 siblings, 12 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

When reclaim swaps out a PMD-mapped anonymous THP today, the PMD is
split into HPAGE_PMD_NR PTE-level swap entries via TTU_SPLIT_HUGE_PMD
before unmap.  This series introduces a PMD-level swap entry so the
huge mapping can survive the swap round-trip and do_huge_pmd_swap_page()
can restore the PMD mapping directly on swap-in, without waiting for
khugepaged to collapse the range later.

The PMD swap entry is a compact page-table encoding for HPAGE_PMD_NR
consecutive swap slots.  swap_map accounting remains per-slot and is
unchanged.  Importantly, a PMD swap entry does not promise that the swap
cache always contains one PMD-sized folio.  While the cache is empty or
contains one PMD-sized folio, PMD-level handling can proceed.  Once the
cache has split/per-slot state, users either inspect the individual
slots directly (mincore) or split the PMD swap entry and retry through
the PTE path (fault, swapoff, MADV_WILLNEED, UFFDIO_MOVE).  Likewise,
if any slot is still backed by zswap's per-page store, PMD-order
swap-in consumers split and let the PTE path load the range page by page;
an all-on-disk range can still be read back as one PMD-sized folio.

The series is ordered so every consumer can handle PMD swap entries
before the swap-out producer starts installing them.  The swap-out patch
is the last functional change.

Notes on zswap:

  Native PMD-order zswap load/store is intentionally left for a follow-up.
  Alexandre Ghiti is currently working this.
  This series can still preserve PMD swap entries while zswap is enabled:
  zswap stores the THP as order-0 entries, and PMD-order swap-in
  consumers split any range that has zswap entries before reading it.  If
  zswap has written the whole range back to disk, or the swap cache still
  contains one PMD-sized folio, PMD-level handling can proceed.

Testing:

  The 16 pmd_swap selftests pass on x86_64 with zswap both disabled and
  enabled.  PMD_SWAP_DEVICE was set, so the swapoff test ran in both
  configurations.

v5 -> v6: https://lore.kernel.org/all/20260722152043.2273289-1-usama.arif@linux.dev/
- Add patch 1 to rename pmd_to_softleaf_folio() to
  pmd_softleaf_to_folio().  No functional change. (Dev Jain)
- Patch 2: warn when pmd_softleaf_to_folio() is given a non-PFN
  softleaf rather than silently returning NULL. (Dev Jain)
- Patch 4: bound the fork extend-table fallback to one retry, re-read
  the PMD under its lock, normalize unrecoverable copy_huge_pmd() errors
  to -ENOMEM so copy_pmd_range() cannot clear and leak the source swap
  PMD, and drop a redundant thp_migration_supported() gate.
- Patch 5: check multi-page swap-cache insertions for zswap-backed slots
  in __swap_cache_add_check() under the cluster lock, both before
  allocation and before insertion, and reject mixed zswap/disk state
  with -EBUSY. (Yosry Ahmed, Nhat Pham)
- Patch 6: on a failed non-uptodate PMD-order read, remove the large
  folio from swap cache before splitting so order-0
  fallback retries individual slots rather than poisoning the whole
  2 MiB range; retain hardware-poisoned folios for per-subpage handling.
- Patch 7: make HMM snapshot mode report a PMD swap entry as non-resident,
  matching PTE swap entries, rather than HMM_PFN_ERROR.  Drop redundant
  thp_migration_supported() gates and simplify non-present PMD handling.
- Patch 8: factor PMD MADV_WILLNEED prefetch into
  swapin_pmd_swap_entry(), split and retry through PTEs after any
  PMD-order swapin failure, and replace the racy folio_test_locked()
  plus folio_lock() sequence with folio_trylock().
- Patch 9: guard PMD-swap UFFDIO_MOVE code with CONFIG_THP_SWAP, clarify
  RWP marker propagation, and reject a PMD swap entry at the destination
  with -EEXIST so UFFDIO_MOVE cannot loop forever on -EAGAIN.
- Patch 10: honor current THP/VMA policy before PMD-order swap-in, recheck
  that the PMD is still the original swap entry before splitting for PTE
  fallback, and provide the CONFIG_TRANSPARENT_HUGEPAGE wp_huge_pmd()
  declaration/stub needed by THP=n builds.
- Patch 11: make an invalid set_pmd_swap_entry() walk context warn and
  return -EINVAL instead of falsely reporting success and corrupting the
  MM_ANONPAGES/MM_SWAPENTS accounting, and add an exact PMD-size folio
  precondition check. (Luiz Capitulino)
- Patch 12: use /proc/swaps for prerequisite detection, check
  MADV_HUGEPAGE, and distinguish an environment that cannot allocate a
  PMD THP (SKIP) from a swap-out validation failure (FAIL).  Add
  partial-mprotect and partial-munmap split coverage.  Strengthen
  munmap/MADV_FREE VmSwap accounting, pagemap slot-offset checks, and
  mprotect/mremap swapped-state checks; force mremap to move, check
  munmap()'s return, and mark the UFFDIO_MOVE destination MADV_HUGEPAGE
  before asserting PMD restoration.  Move common setup and cleanup into
  one fixture, merge the swapoff fixture, remove the redundant cycles
  test, and make the data pattern differ between base pages so the split
  tests can detect incorrect slot ordering.  Order fork-COW so the parent
  writes while the child still holds the untouched shared swap entry.
  (Luiz Capitulino)
- Clarify commit messages throughout.  Retain TTU_SPLIT_HUGE_PMD after
  prototyping its removal: removing it here requires an extra rmap walk
  and broadens the series beyond PMD swap entries. (Matthew Wilcox)
- Rebase onto akpm/mm-new from 15 August (4b65683fd25f).

v4 -> v5: https://lore.kernel.org/all/20260713133613.2707815-1-usama.arif@linux.dev/
- Commit message improvements for almost all patches (Yosry for zswap patch)
- Patch 1: make pmd_to_softleaf_folio() reject softleaf entries that do
  not encode a PFN, so a PMD swap offset is never interpreted as one.
  PMD swap entries remain valid softleaf entries for classification.
  (sashiko)
- Patch 2: use the existing pmd_swp_uffd() helper and force freeze=false
  for PMD swap entries, which have no struct page for the migration-entry
  freeze path. (sashiko)
- Patch 3: document that the caller's page-table or swap-cache reference
  pins every source slot while a partial PMD-sized duplication is rolled
  back.  Keep the pre-existing PTE fork retry behavior outside this
  series. (sashiko)
- Patch 5: split to the PTE path rather than mapping a PMD-sized folio
  containing a hardware-poisoned subpage, and restore PAGE_NONE when
  swapoff restores a UFFD marker in an RWP VMA. (sashiko)
- Patch 6: account SwapPss for a PMD swap entry one slot at a time because
  the slots can have different swap reference counts.
- Patch 7: if PMD-order MADV_WILLNEED encounters newly populated per-page
  zswap state, revalidate and remove the failed clean PMD-sized cache
  folio before retrying through PTEs.
- Patch 8: mark a moved PMD swap entry for UFFD when the UFFDIO_MOVE
  destination VMA is RWP-registered. (sashiko)
- Patch 9: restore PAGE_NONE for UFFD RWP swap-in, preserve the original
  write-fault state through swap-slot release and COW handling, remove
  the unnecessary LRU drain, and prevent PTE batching from mapping a
  poisoned subpage. (sashiko)
- Patch 10: add and document thp_swpout_pmd, which counts PMD mappings
  replaced by PMD-level swap entries rather than swapped folios.
- Patch 11: register pmd_swap with the default mm selftest runner, preserve
  errno across UFFDIO_MOVE cleanup, check swapoff residency before the
  first memory access, add a parent-side write and verification to the
  fork+COW test, and add RWP regression coverage for swap-in, UFFDIO_MOVE,
  and swapoff. (sashiko)
- Keep do_huge_pmd_swap_page() in patch 9.  Patches 6 and 7 only add
  consumers; patch 10 remains the first producer, so no PMD swap entry
  can reach those paths before the fault handler is present. (sashiko)
- Rebase onto latest akpm/mm-new from 22 July (5e0603ba185a)

v3 -> v4: https://lore.kernel.org/all/20260703173903.3789516-1-usama.arif@linux.dev/
- Patch 1: guard the new arch-specific pmd_swp_mkexclusive /
  pmd_swp_exclusive / pmd_swp_clear_exclusive helpers on arm64,
  loongarch, powerpc, riscv, s390, and x86 with
  CONFIG_ARCH_HAS_PMD_SOFTLEAVES, matching the pattern already
  used for pmd_swp_soft_dirty.  Also fixes the redefinition-vs-
  generic-fallback build errors kernel test robot reported on
  i386-allnoconfig-bpf and riscv-allnoconfig-bpf, and rewraps
  the patch 1 commit message paragraphs to ~75 columns.
  (sashiko, kernel test robot, Usama Arif)
- Patch 2: switch the trailing folio_remove_rmap_pmd() gate in
  __split_huge_pmd_locked() from *pmd to old_pmd, old_pmd retains
  the original present-or-non-present classification for every
  branch above. (sashiko)
- Patch 3: teach swap_retry_table_alloc() (and the underlying
  swap_extend_table_alloc()) to accept an nr parameter and scan
  every slot in [ci_off, ci_off + nr) before committing an
  extend-table allocation.  (sashiko)
- Patch 4: rename zswap_range_has_entry() to zswap_is_present() so
  the same helper serves both single-slot (nr=1) and range queries,
  and switch the implementation from XA_STATE + xas_find() to
  xa_find(), which handles RCU locking and internal-retry markers
  itself.  Rename the callers in patches 5, 7, 9. (Yosry)
- Patch 9: refuse to map a swap-cache folio in do_huge_pmd_swap_page()
  when folio_contain_hwpoisoned_page() reports a poisoned subpage;
  split the PMD swap entry so do_swap_page() can return
  VM_FAULT_HWPOISON per subpage instead of the PMD handler mapping
  the corrupted memory as one THP.  Mirrors the PageHWPoison check
  the PTE swap-in path already performs. (sashiko)
- Patch 9: note explicitly in the commit message that PMD-order
  swap-in deliberately skips the order-0 readahead paths, order-0
  readahead would populate per-page swap-cache state and force the
  PMD swap entry to split before the fault could finish. (Kairui)
- Patch 10: move mm_prepare_for_swap_entries() into
  set_pmd_swap_entry() between folio_dup_swap() and set_pmd_at()
  so this mm is on init_mm.mmlist before any swap PMD referencing
  slots with a non-zero swap_map becomes visible.  Matches the PTE
  swap-out ordering. (sashiko)
- rebase onto latest akpm/mm-new (61cccb8363fcc282d4ae0555b8739dd227f5ad0b)


v2 -> v3: https://lore.kernel.org/all/20260602142537.198755-1-usama.arif@linux.dev/
- Clarified the PMD swap entry rule: it is a compact encoding for
  HPAGE_PMD_NR swap slots, not a guarantee that swap cache always has
  one PMD-sized folio. (Lance Yang)
- Swapoff, fault, MADV_WILLNEED, and UFFDIO_MOVE now classify the
  whole PMD swap-cache range and split/retry through the PTE path for
  split/per-slot cache state. (Lance Yang)
- mincore handles PMD swap entries without assuming one lookup covers
  a split swap-cache range. (Lance Yang)
- UFFDIO_MOVE rechecks all HPAGE_PMD_NR slots before moving an empty
  PMD swap-cache range, avoiding stale rmap metadata for per-slot
  cached folios.
- Added a standalone zswap prerequisite patch from Alexandre that
  distinguishes all-on-disk large-folio ranges from ranges with
  per-page zswap entries.
- Replaced the global zswap-ever-enabled policy with per-range zswap
  checks: PMD swap entries can still be installed while zswap is
  enabled, and PMD-order swap-in consumers split when the range has
  per-page zswap state.
- Added a mincore selftest and updated MADV_WILLNEED coverage so the
  test checks that the PMD swap entry remains in place until first
  touch.  Total pmd_swap coverage is now 14 tests.


v1 -> v2: https://lore.kernel.org/all/20260427100553.2754667-1-usama.arif@linux.dev/
- Patch 1: convert two additional softleaf_to_pmd() callers that
  landed in mm-unstable since v1 (mm/debug_vm_pgtable.c,
  mm/migrate_device.c) (Dev)
- Patch 2: rename helper ensure_on_mmlist() to
  mm_prepare_for_swap_entries() to better describe its purpose
  (David)
- Patch 3: drop VM_WARN_ON_ONCE(!pmd_is_migration_entry) as
  Dev posted it as a separate patch.
- Patch 5 (new): move softleaf_to_folio() inside the device-private
  branch in migrate_vma_collect_pmd(); same class of fix as patch 4
  but for the migrate-device PMD walker.
- Patch 6 (new): rename CONFIG_ARCH_ENABLE_THP_MIGRATION to
  CONFIG_ARCH_HAS_PMD_SOFTLEAVES so the gate that now drives
  swap-entry support too is named for what it actually controls
  (PMD softleaf entries), not just migration. (Dev)
- Patch 7: add the missing pmd_swp_exclusive / mkexclusive /
  clear_exclusive helpers for powerpc.
- Patches 10 and 14: use upstream swapin_sync() (bundles
  swap_cache_alloc_folio + swap_read_folio + the -EEXIST race
  retry) instead of the bespoke swapin_alloc_pmd_folio() helper
  from v1; do_swap_page and shmem_swapin_folio use the same
  helper (Kairui)
- Patch 10: construct a stack vm_fault for the swapoff swap-in so
  the allocator can resolve a mempolicy, mirroring how the PTE
  swapoff path (unuse_pte_range) already does it.
- Patch 11: extend coverage to check_pmd_state() in khugepaged so a
  swapped-out PMD-mapped THP is treated as SCAN_PMD_MAPPED (matches
  the existing migration-entry handling). Route PMD swap entries in the
  pmd_trans_huge_lock() branch of mincore_pte_range() through
  mincore_pmd_swap() so a swapped-out PMD-mapped THP isn't reported as
  resident.
- Patch 12 (new): handle PMD swap entries in MADV_WILLNEED via
  swapin_sync(BIT(HPAGE_PMD_ORDER)); a naive order-0 read-ahead
  would force the subsequent fault to split.
- Patch 13: refuse UFFDIO_MOVE with -EBUSY if the swap-cache folio
  was split between swap-out and the move, matching
  move_pages_pte()'s rejection of large folios; otherwise only one
  of the 512 anon-rmaps would be re-anchored to dst_vma.
- Patch 16: alloc_fill_swap_thp() now uses the existing
  mmap_pmd_aligned() helper so tests don't flake/skip based on VA
  placement; new MADV_WILLNEED test that watches the PMD-order
  mTHP swpin counter; swapoff test restructured to use the
  kselftest_harness ASSERT cleanup blocks (no double swapoff, no
  verify-after-munmap).
- Collected Acks and Reviews.

[1] https://lore.kernel.org/all/20260630164143.1595669-1-usama.arif@linux.dev/ 
 
Alexandre Ghiti (1):
  mm: zswap: add range lookup for large-folio swapin

Usama Arif (11):
  mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio()
  mm: add PMD swap entry detection support
  mm: add PMD swap entry splitting support
  mm: handle PMD swap entries in fork path
  mm: swap in PMD swap entries as whole THPs during swapoff
  mm: handle PMD swap entries in non-present PMD walkers
  mm: handle PMD swap entries in MADV_WILLNEED
  mm: handle PMD swap entries in UFFDIO_MOVE
  mm: handle PMD swap entry faults on swap-in
  mm: install PMD swap entries on swap-out
  selftests/mm: add PMD swap entry tests

 Documentation/admin-guide/mm/transhuge.rst   |   5 +
 arch/arm64/include/asm/pgtable.h             |   6 +
 arch/loongarch/include/asm/pgtable.h         |  19 +
 arch/powerpc/include/asm/book3s/64/pgtable.h |  17 +
 arch/riscv/include/asm/pgtable.h             |  15 +
 arch/s390/include/asm/pgtable.h              |  17 +
 arch/x86/include/asm/pgtable.h               |  17 +
 fs/proc/task_mmu.c                           |  46 +-
 include/linux/huge_mm.h                      |  16 +
 include/linux/leafops.h                      |  30 +-
 include/linux/pgtable.h                      |  17 +
 include/linux/swap.h                         |   4 +-
 include/linux/vm_event_item.h                |   1 +
 include/linux/zswap.h                        |   6 +
 mm/hmm.c                                     |  11 +-
 mm/huge_memory.c                             | 607 ++++++++++++++-
 mm/internal.h                                |  42 ++
 mm/khugepaged.c                              |   6 +
 mm/madvise.c                                 | 120 ++-
 mm/memory.c                                  |  47 +-
 mm/mincore.c                                 |  45 +-
 mm/rmap.c                                    |  19 +
 mm/swap.h                                    |  22 +-
 mm/swap_state.c                              |  54 ++
 mm/swapfile.c                                | 209 +++++-
 mm/userfaultfd.c                             |  14 +
 mm/vmscan.c                                  |   9 +-
 mm/vmstat.c                                  |   1 +
 mm/zswap.c                                   |  46 +-
 tools/testing/selftests/mm/Makefile          |   2 +
 tools/testing/selftests/mm/ksft_pmd_swap.sh  |   4 +
 tools/testing/selftests/mm/pmd_swap.c        | 742 +++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh    |   4 +
 33 files changed, 2104 insertions(+), 116 deletions(-)
 create mode 100755 tools/testing/selftests/mm/ksft_pmd_swap.sh
 create mode 100644 tools/testing/selftests/mm/pmd_swap.c

-- 
2.53.0-Meta



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

* [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio()
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 14:24   ` David Hildenbrand (Arm)
                     ` (2 more replies)
  2026-08-18 13:09 ` [PATCH v6 02/12] mm: add PMD swap entry detection support Usama Arif
                   ` (10 subsequent siblings)
  11 siblings, 3 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

pmd_to_softleaf_folio() converts the softleaf entry encoded by a PMD
to a folio. Rename it to pmd_softleaf_to_folio() to make the conversion
direction explicit and align it with softleaf_to_folio().

No functional change.

Suggested-by: Dev Jain <dev.jain@arm.com>
Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 include/linux/leafops.h | 4 ++--
 mm/huge_memory.c        | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/include/linux/leafops.h b/include/linux/leafops.h
index 4c1476ae32343..7c13c58a5e218 100644
--- a/include/linux/leafops.h
+++ b/include/linux/leafops.h
@@ -657,7 +657,7 @@ static inline bool pmd_is_valid_softleaf(pmd_t pmd)
 }
 
 /**
- * pmd_to_softleaf_folio() - Convert the PMD entry to a folio.
+ * pmd_softleaf_to_folio() - Convert the PMD softleaf entry to a folio.
  * @pmd: PMD entry.
  *
  * The PMD entry is expected to be a valid PMD softleaf entry.
@@ -665,7 +665,7 @@ static inline bool pmd_is_valid_softleaf(pmd_t pmd)
  * Returns: the folio the softleaf entry references if this is a valid softleaf
  * entry, otherwise NULL.
  */
-static inline struct folio *pmd_to_softleaf_folio(pmd_t pmd)
+static inline struct folio *pmd_softleaf_to_folio(pmd_t pmd)
 {
 	const softleaf_t entry = softleaf_from_pmd(pmd);
 
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index ced400f72d43a..1b6b0aa2baa3b 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2467,7 +2467,7 @@ static struct folio *normal_or_softleaf_folio_pmd(struct vm_area_struct *vma,
 
 	if (!thp_migration_supported())
 		WARN_ONCE(1, "Non present huge pmd without pmd migration enabled!");
-	return pmd_to_softleaf_folio(pmdval);
+	return pmd_softleaf_to_folio(pmdval);
 }
 
 static bool has_deposited_pgtable(struct vm_area_struct *vma, pmd_t pmdval,
-- 
2.53.0-Meta



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

* [PATCH v6 02/12] mm: add PMD swap entry detection support
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 14:40   ` David Hildenbrand (Arm)
  2026-08-18 13:09 ` [PATCH v6 03/12] mm: add PMD swap entry splitting support Usama Arif
                   ` (9 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Currently when a PMD-mapped THP is swapped out, the PMD is always
split into HPAGE_PMD_NR PTE-level swap entries. To preserve huge
page information across swap cycles, later patches will install a
single PMD-level swap entry instead. Add the infrastructure to detect
those entries.

Teach the softleaf layer to recognise PMD swap entries:
pmd_is_swap_entry() detects them and softleaf_is_valid_pmd_entry()
accepts them as a valid non-present type. Because swap entries do not
encode a PFN, make pmd_softleaf_to_folio() warn and return NULL for them
instead of passing the swap offset to softleaf_to_folio(). Clear the
exclusive overlay bit in softleaf_from_pmd() before decoding, matching
how soft_dirty and uffd bits are already stripped.

Add pmd_swp_mkexclusive(), pmd_swp_exclusive(), and
pmd_swp_clear_exclusive() helpers to each architecture that supports
PMD softleaf entries (x86, arm64, s390, riscv, loongarch, powerpc),
mirroring the existing PTE swap exclusive helpers in each arch's
pgtable.h. Provide generic no-op PMD swap exclusive fallbacks for
architectures without PMD softleaf support, matching the generic PMD
swap soft-dirty fallbacks.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 arch/arm64/include/asm/pgtable.h             |  6 +++++
 arch/loongarch/include/asm/pgtable.h         | 19 ++++++++++++++
 arch/powerpc/include/asm/book3s/64/pgtable.h | 17 +++++++++++++
 arch/riscv/include/asm/pgtable.h             | 15 +++++++++++
 arch/s390/include/asm/pgtable.h              | 17 +++++++++++++
 arch/x86/include/asm/pgtable.h               | 17 +++++++++++++
 include/linux/leafops.h                      | 26 ++++++++++++++++----
 include/linux/pgtable.h                      | 17 +++++++++++++
 8 files changed, 129 insertions(+), 5 deletions(-)

diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgtable.h
index a2681d7553584..860f95573d1fb 100644
--- a/arch/arm64/include/asm/pgtable.h
+++ b/arch/arm64/include/asm/pgtable.h
@@ -598,6 +598,12 @@ static inline int pmd_protnone(pmd_t pmd)
 #define pmd_swp_clear_uffd(pmd) \
 				pte_pmd(pte_swp_clear_uffd(pmd_pte(pmd)))
 #endif /* CONFIG_HAVE_ARCH_USERFAULTFD_WP */
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+#define pmd_swp_exclusive(pmd)	pte_swp_exclusive(pmd_pte(pmd))
+#define pmd_swp_mkexclusive(pmd)	pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)))
+#define pmd_swp_clear_exclusive(pmd) \
+				pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)))
+#endif
 
 #define pmd_write(pmd)		pte_write(pmd_pte(pmd))
 
diff --git a/arch/loongarch/include/asm/pgtable.h b/arch/loongarch/include/asm/pgtable.h
index 1952e34bc8ee0..aa8e1223d3973 100644
--- a/arch/loongarch/include/asm/pgtable.h
+++ b/arch/loongarch/include/asm/pgtable.h
@@ -357,6 +357,25 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return pte;
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	pmd_val(pmd) |= _PAGE_SWP_EXCLUSIVE;
+	return pmd;
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	pmd_val(pmd) &= ~_PAGE_SWP_EXCLUSIVE;
+	return pmd;
+}
+#endif
+
 #define pte_none(pte)		(!(pte_val(pte) & ~_PAGE_GLOBAL))
 #define pte_present(pte)	(pte_val(pte) & (_PAGE_PRESENT | _PAGE_PROTNONE))
 #define pte_no_exec(pte)	(pte_val(pte) & _PAGE_NO_EXEC)
diff --git a/arch/powerpc/include/asm/book3s/64/pgtable.h b/arch/powerpc/include/asm/book3s/64/pgtable.h
index f4db7d7fbd5c6..6a899d0793b3b 100644
--- a/arch/powerpc/include/asm/book3s/64/pgtable.h
+++ b/arch/powerpc/include/asm/book3s/64/pgtable.h
@@ -699,6 +699,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return __pte_raw(pte_raw(pte) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return __pmd_raw(pmd_raw(pmd) | cpu_to_be64(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return !!(pmd_raw(pmd) & cpu_to_be64(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return __pmd_raw(pmd_raw(pmd) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE));
+}
+#endif
+
 static inline bool check_pte_access(unsigned long access, unsigned long ptev)
 {
 	/*
diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h
index 1225cf05696a2..65b4181c62d9a 100644
--- a/arch/riscv/include/asm/pgtable.h
+++ b/arch/riscv/include/asm/pgtable.h
@@ -1213,6 +1213,21 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 }
 
 #ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pte_swp_exclusive(pmd_pte(pmd));
+}
+
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd)));
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd)));
+}
+
 #define __pmd_to_swp_entry(pmd) ((swp_entry_t) { pmd_val(pmd) })
 #define __swp_entry_to_pmd(swp) __pmd((swp).val)
 #endif /* CONFIG_ARCH_HAS_PMD_SOFTLEAVES */
diff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtable.h
index e882663a58e77..490e4a3464b19 100644
--- a/arch/s390/include/asm/pgtable.h
+++ b/arch/s390/include/asm/pgtable.h
@@ -870,6 +870,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return clear_pte_bit(pte, __pgprot(_PAGE_SWP_EXCLUSIVE));
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return set_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return clear_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE));
+}
+#endif
+
 static inline int pte_soft_dirty(pte_t pte)
 {
 	return pte_val(pte) & _PAGE_SOFT_DIRTY;
diff --git a/arch/x86/include/asm/pgtable.h b/arch/x86/include/asm/pgtable.h
index 8e0018fadd14e..b5da5447e83d0 100644
--- a/arch/x86/include/asm/pgtable.h
+++ b/arch/x86/include/asm/pgtable.h
@@ -1525,6 +1525,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte)
 	return pte_clear_flags(pte, _PAGE_SWP_EXCLUSIVE);
 }
 
+#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pmd_set_flags(pmd, _PAGE_SWP_EXCLUSIVE);
+}
+
+static inline int pmd_swp_exclusive(pmd_t pmd)
+{
+	return pmd_flags(pmd) & _PAGE_SWP_EXCLUSIVE;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pmd_clear_flags(pmd, _PAGE_SWP_EXCLUSIVE);
+}
+#endif
+
 #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY
 static inline pte_t pte_swp_mksoft_dirty(pte_t pte)
 {
diff --git a/include/linux/leafops.h b/include/linux/leafops.h
index 7c13c58a5e218..4a6c52974b305 100644
--- a/include/linux/leafops.h
+++ b/include/linux/leafops.h
@@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd)
 		pmd = pmd_swp_clear_soft_dirty(pmd);
 	if (pmd_swp_uffd(pmd))
 		pmd = pmd_swp_clear_uffd(pmd);
+	if (pmd_swp_exclusive(pmd))
+		pmd = pmd_swp_clear_exclusive(pmd);
 	arch_entry = __pmd_to_swp_entry(pmd);
 
 	/* Temporary until swp_entry_t eliminated. */
@@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd)
  */
 static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry)
 {
-	/* Only device private, migration entries valid for PMD. */
+	/* Device private, migration, and swap entries valid for PMD. */
 	return softleaf_is_device_private(entry) ||
-		softleaf_is_migration(entry);
+		softleaf_is_migration(entry) ||
+		softleaf_is_swap(entry);
+}
+
+/**
+ * pmd_is_swap_entry() - Does this PMD entry encode an actual swap entry?
+ * @pmd: PMD entry.
+ *
+ * Returns: true if the PMD encodes a swap entry, otherwise false.
+ */
+static inline bool pmd_is_swap_entry(pmd_t pmd)
+{
+	return softleaf_is_swap(softleaf_from_pmd(pmd));
 }
 
 /**
  * pmd_is_valid_softleaf() - Is this PMD entry a valid softleaf entry?
  * @pmd: PMD entry.
  *
- * PMD leaf entries are valid only if they are device private or migration
- * entries. This function asserts that a PMD leaf entry is valid in this
- * respect.
+ * PMD leaf entries are valid only if they are device private, migration,
+ * or swap entries. This function asserts that a PMD leaf entry is valid
+ * in this respect.
  *
  * Returns: true if the PMD entry is a valid leaf entry, otherwise false.
  */
@@ -673,6 +687,8 @@ static inline struct folio *pmd_softleaf_to_folio(pmd_t pmd)
 		VM_WARN_ON_ONCE(true);
 		return NULL;
 	}
+	if (WARN_ON_ONCE(!softleaf_has_pfn(entry)))
+		return NULL;
 	return softleaf_to_folio(entry);
 }
 
diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h
index 8c093c119e5a8..e10a7e91e4260 100644
--- a/include/linux/pgtable.h
+++ b/include/linux/pgtable.h
@@ -1917,6 +1917,23 @@ static inline pmd_t pmd_swp_clear_soft_dirty(pmd_t pmd)
 }
 #endif
 
+#ifndef CONFIG_ARCH_HAS_PMD_SOFTLEAVES
+static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd)
+{
+	return pmd;
+}
+
+static inline bool pmd_swp_exclusive(pmd_t pmd)
+{
+	return false;
+}
+
+static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd)
+{
+	return pmd;
+}
+#endif
+
 #ifndef __HAVE_PFNMAP_TRACKING
 /*
  * Interfaces that can be used by architecture code to keep track of
-- 
2.53.0-Meta



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

* [PATCH v6 03/12] mm: add PMD swap entry splitting support
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
  2026-08-18 13:09 ` [PATCH v6 02/12] mm: add PMD swap entry detection support Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 17:53   ` David Hildenbrand (Arm)
  2026-08-18 13:09 ` [PATCH v6 04/12] mm: handle PMD swap entries in fork path Usama Arif
                   ` (8 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Add a swap branch in __split_huge_pmd_locked() that splits a PMD swap
entry into 512 PTE swap entries. No folio reference is needed because
swap entries point to swap slots rather than pages. Each PTE inherits
the correct sub-slot offset and preserves soft_dirty, uffd_wp, and
exclusive flags.

The folio_remove_rmap_pmd() gate at the end must inspect old_pmd
rather than *pmd: for a present THP split, *pmd has already been
cleared by pmdp_invalidate(), and that invalidated bit pattern can
decode as a plausible swap entry.

This branch is reached from the explicit __split_huge_pmd() callers
that hit a non-present PMD: partial-range mprotect / munmap, the
wp_huge_pmd() PMD-COW fallback, and the swap-in / swapoff fallbacks
added in later patches when the cached folio is no longer PMD-sized.
page_vma_mapped_walk() does not iterate PMD swap entries, so
try_to_unmap_one() and try_to_migrate_one() do not reach this branch
and freeze=true cannot occur in this branch today.  page and folio
are therefore left uninitialized in the swap branch; a
VM_WARN_ON_ONCE(freeze) catches any future caller that breaks this
invariant before the freeze path dereferences page_to_pfn(page + i)
or put_page(page).

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 mm/huge_memory.c | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 1b6b0aa2baa3b..a473e85d30f51 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -3252,6 +3252,14 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 			folio_add_anon_rmap_ptes(folio, page, HPAGE_PMD_NR,
 						 vma, haddr, rmap_flags);
 		}
+	} else if (pmd_is_swap_entry(*pmd)) {
+		VM_WARN_ON_ONCE(freeze);
+		/* Swap entries have no page for the migration freeze path. */
+		freeze = false;
+		old_pmd = *pmd;
+		soft_dirty = pmd_swp_soft_dirty(old_pmd);
+		uffd_wp = pmd_swp_uffd(old_pmd);
+		anon_exclusive = pmd_swp_exclusive(old_pmd);
 	} else {
 		/*
 		 * Up to this point the pmd is present and huge and userland has
@@ -3388,6 +3396,25 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 			VM_WARN_ON(!pte_none(ptep_get(pte + i)));
 			set_pte_at(mm, addr, pte + i, entry);
 		}
+	} else if (pmd_is_swap_entry(old_pmd)) {
+		softleaf_t sl_entry = softleaf_from_pmd(old_pmd);
+		pte_t swp_pte;
+		swp_entry_t sub_entry;
+
+		for (i = 0, addr = haddr; i < HPAGE_PMD_NR;
+		     i++, addr += PAGE_SIZE) {
+			sub_entry = swp_entry(swp_type(sl_entry),
+					      swp_offset(sl_entry) + i);
+			swp_pte = swp_entry_to_pte(sub_entry);
+			if (soft_dirty)
+				swp_pte = pte_swp_mksoft_dirty(swp_pte);
+			if (uffd_wp)
+				swp_pte = pte_swp_mkuffd(swp_pte);
+			if (anon_exclusive)
+				swp_pte = pte_swp_mkexclusive(swp_pte);
+			VM_WARN_ON(!pte_none(ptep_get(pte + i)));
+			set_pte_at(mm, addr, pte + i, swp_pte);
+		}
 	} else {
 		pte_t entry;
 
@@ -3415,7 +3442,7 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
 	}
 	pte_unmap(pte);
 
-	if (!pmd_is_migration_entry(*pmd))
+	if (!pmd_is_migration_entry(old_pmd) && !pmd_is_swap_entry(old_pmd))
 		folio_remove_rmap_pmd(folio, page, vma);
 	if (freeze)
 		put_page(page);
-- 
2.53.0-Meta



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

* [PATCH v6 04/12] mm: handle PMD swap entries in fork path
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (2 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 03/12] mm: add PMD swap entry splitting support Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin Usama Arif
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Teach copy_huge_pmd()/copy_huge_non_present_pmd() about swap entries,
mirroring copy_nonpresent_pte().

swap_dup_entry_direct() gains a nr parameter (and is renamed to
swap_dup_entries_direct()) so it can duplicate a contiguous range of
swap slots in one call, matching the existing
swap_put_entries_direct(entry, nr) API.  Existing callers pass 1.

swap_retry_table_alloc() likewise gains a nr parameter so the outer
retry knows how many slots the caller was trying to duplicate.  The
underlying swap_extend_table_alloc() now scans every slot in
[ci_off, ci_off + nr) to confirm that at least one still needs the
per-cluster extend table before committing an allocation.

copy_huge_non_present_pmd() "copies" PMD swap entries during fork
instead of splitting, preserving the THP.  This mirrors
copy_nonpresent_pte() which duplicates the swap slot refcount,
clears the exclusive bit on the source, and adds the destination
mm to mmlist.  If swap_dup_entries_direct() fails (GFP_ATOMIC table
alloc), copy_huge_pmd() retries once after
swap_retry_table_alloc(entry, HPAGE_PMD_NR, GFP_KERNEL).

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 include/linux/swap.h |  4 +--
 mm/huge_memory.c     | 60 ++++++++++++++++++++++++++++++++++++++------
 mm/memory.c          |  4 +--
 mm/swap.h            |  5 ++--
 mm/swapfile.c        | 40 ++++++++++++++++++-----------
 5 files changed, 86 insertions(+), 27 deletions(-)

diff --git a/include/linux/swap.h b/include/linux/swap.h
index 0f953ed9c8630..e465069361733 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -395,7 +395,7 @@ sector_t swap_folio_sector(struct folio *folio);
  * All entries must be allocated by folio_alloc_swap(). And they must have
  * a swap count > 1. See comments of folio_*_swap helpers for more info.
  */
-int swap_dup_entry_direct(swp_entry_t entry);
+int swap_dup_entries_direct(swp_entry_t entry, int nr);
 void swap_put_entries_direct(swp_entry_t entry, int nr);
 
 /*
@@ -439,7 +439,7 @@ static inline void free_swap_cache(struct folio *folio)
 {
 }
 
-static inline int swap_dup_entry_direct(swp_entry_t ent)
+static inline int swap_dup_entries_direct(swp_entry_t ent, int nr)
 {
 	return 0;
 }
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index a473e85d30f51..2735c3de7029c 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -1849,7 +1849,7 @@ bool touch_pmd(struct vm_area_struct *vma, unsigned long addr,
 	return false;
 }
 
-static void copy_huge_non_present_pmd(
+static int copy_huge_non_present_pmd(
 		struct mm_struct *dst_mm, struct mm_struct *src_mm,
 		pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long addr,
 		struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
@@ -1895,14 +1895,35 @@ static void copy_huge_non_present_pmd(
 		 */
 		folio_try_dup_anon_rmap_pmd(src_folio, &src_folio->page,
 					    dst_vma, src_vma);
+	} else if (softleaf_is_swap(entry)) {
+		int err;
+
+		/*
+		 * PMD swap entry: duplicate swap references and clear
+		 * exclusive on source, matching copy_nonpresent_pte().
+		 */
+		err = swap_dup_entries_direct(entry, HPAGE_PMD_NR);
+		if (err < 0)
+			return err;
+
+		mm_prepare_for_swap_entries(dst_mm);
+
+		if (pmd_swp_exclusive(pmd)) {
+			pmd = pmd_swp_clear_exclusive(pmd);
+			set_pmd_at(src_mm, addr, src_pmd, pmd);
+		}
 	}
 
-	add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	if (softleaf_is_swap(entry))
+		add_mm_counter(dst_mm, MM_SWAPENTS, HPAGE_PMD_NR);
+	else
+		add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR);
 	mm_inc_nr_ptes(dst_mm);
 	pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable);
 	if (!userfaultfd_protected(dst_vma))
 		pmd = pmd_swp_clear_uffd(pmd);
 	set_pmd_at(dst_mm, addr, dst_pmd, pmd);
+	return 0;
 }
 
 int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
@@ -1912,6 +1933,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	spinlock_t *dst_ptl, *src_ptl;
 	struct page *src_page;
 	struct folio *src_folio;
+	bool retried = false;
 	pmd_t pmd;
 	pgtable_t pgtable = NULL;
 	int ret = -ENOMEM;
@@ -1943,6 +1965,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	if (unlikely(!pgtable))
 		goto out;
 
+retry:
 	dst_ptl = pmd_lock(dst_mm, dst_pmd);
 	src_ptl = pmd_lockptr(src_mm, src_pmd);
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
@@ -1950,11 +1973,34 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	ret = -EAGAIN;
 	pmd = *src_pmd;
 
-	if (unlikely(thp_migration_supported() &&
-		     pmd_is_valid_softleaf(pmd))) {
-		copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd, addr,
-					  dst_vma, src_vma, pmd, pgtable);
-		ret = 0;
+	if (unlikely(pmd_is_valid_softleaf(pmd))) {
+		ret = copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd,
+						addr, dst_vma, src_vma, pmd,
+						pgtable);
+		if (ret) {
+			spin_unlock(src_ptl);
+			spin_unlock(dst_ptl);
+			/*
+			 * For PMD swap entries -ENOMEM means the per-cluster
+			 * swap-extend table couldn't be GFP_ATOMIC-allocated.
+			 * Try the GFP_KERNEL fallback once before giving up.
+			 * swap_retry_table_alloc() also returns 0 when it
+			 * decides the table is not needed after all, so bound
+			 * this to a single retry rather than looping on it.
+			 */
+			if (ret == -ENOMEM && !retried) {
+				softleaf_t entry = softleaf_from_pmd(pmd);
+
+				retried = true;
+				if (softleaf_is_swap(entry) &&
+				    !swap_retry_table_alloc(entry, HPAGE_PMD_NR,
+							    GFP_KERNEL))
+					goto retry;
+			}
+			pte_free(dst_mm, pgtable);
+			ret = -ENOMEM;
+			goto out;
+		}
 		goto out_unlock;
 	}
 
diff --git a/mm/memory.c b/mm/memory.c
index 4134ac607ee0f..36ddca806be2f 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -1016,7 +1016,7 @@ copy_nonpresent_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
 	struct page *page;
 
 	if (likely(softleaf_is_swap(entry))) {
-		if (swap_dup_entry_direct(entry) < 0)
+		if (swap_dup_entries_direct(entry, 1) < 0)
 			return -EIO;
 
 		mm_prepare_for_swap_entries(dst_mm);
@@ -1431,7 +1431,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 
 	if (ret == -EIO) {
 		VM_WARN_ON_ONCE(!entry.val);
-		if (swap_retry_table_alloc(entry, GFP_KERNEL) < 0) {
+		if (swap_retry_table_alloc(entry, 1, GFP_KERNEL) < 0) {
 			ret = -ENOMEM;
 			goto out;
 		}
diff --git a/mm/swap.h b/mm/swap.h
index 90a551a88df63..e225527b2eb7a 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -222,7 +222,7 @@ static inline void swap_cluster_unlock_irq(struct swap_cluster_info *ci)
 	spin_unlock_irq(&ci->lock);
 }
 
-extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp);
+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp);
 
 /*
  * Below are the core routines for doing swap for a folio.
@@ -428,7 +428,8 @@ static inline int swap_writeout(struct swap_io_ctx *ctx, struct folio *folio)
 	return 0;
 }
 
-static inline int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
+static inline int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr,
+					 gfp_t gfp)
 {
 	return -EINVAL;
 }
diff --git a/mm/swapfile.c b/mm/swapfile.c
index f5dfc7e59191e..44b9ebbe7229f 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1462,9 +1462,11 @@ static bool swap_sync_discard(void)
 
 static int swap_extend_table_alloc(struct swap_info_struct *si,
 				   struct swap_cluster_info *ci,
-				   unsigned int ci_off, gfp_t gfp)
+				   unsigned int ci_off, unsigned int nr,
+				   gfp_t gfp)
 {
 	int count;
+	unsigned int i;
 	void *table;
 
 	table = kzalloc(sizeof(ci->extend_table[0]) * SWAPFILE_CLUSTER, gfp);
@@ -1480,15 +1482,21 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,
 	 */
 	if (!cluster_table_is_alloced(ci))
 		goto out_free;
-	count = swp_tb_get_count(__swap_table_get(ci, ci_off));
-	if (count < (SWP_TB_COUNT_MAX - 1))
-		goto out_free;
 	if (ci->extend_table)
 		goto out_free;
-
-	ci->extend_table = table;
-	spin_unlock(&ci->lock);
-	return 0;
+	/*
+	 * The caller may not know which slot in [ci_off, ci_off + nr) hit
+	 * SWP_TB_COUNT_MAX - 1. Confirm at least one slot in the range still
+	 * needs the extend table before committing the allocation.
+	 */
+	for (i = 0; i < nr; i++) {
+		count = swp_tb_get_count(__swap_table_get(ci, ci_off + i));
+		if (count >= (SWP_TB_COUNT_MAX - 1)) {
+			ci->extend_table = table;
+			spin_unlock(&ci->lock);
+			return 0;
+		}
+	}
 
 out_free:
 	spin_unlock(&ci->lock);
@@ -1496,7 +1504,7 @@ static int swap_extend_table_alloc(struct swap_info_struct *si,
 	return 0;
 }
 
-int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
+int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp)
 {
 	int ret;
 	struct swap_info_struct *si;
@@ -1508,7 +1516,8 @@ int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp)
 		return 0;
 
 	ci = __swap_offset_to_cluster(si, offset);
-	ret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), gfp);
+	ret = swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), nr,
+				      gfp);
 
 	put_swap_device(si);
 	return ret;
@@ -1709,7 +1718,8 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,
 		if (unlikely(err)) {
 			if (err == -ENOMEM) {
 				spin_unlock(&ci->lock);
-				err = swap_extend_table_alloc(si, ci, ci_off, GFP_ATOMIC);
+				err = swap_extend_table_alloc(si, ci, ci_off, 1,
+							      GFP_ATOMIC);
 				spin_lock(&ci->lock);
 				if (!err)
 					goto restart;
@@ -1720,6 +1730,7 @@ static int swap_dup_entries_cluster(struct swap_info_struct *si,
 	swap_cluster_unlock(ci);
 	return 0;
 failed:
+	/* The caller's page-table or swap-cache reference pins every slot. */
 	while (ci_off-- > ci_start)
 		__swap_cluster_put_entry(ci, ci_off);
 	swap_extend_table_try_free(ci);
@@ -3925,8 +3936,9 @@ void si_swapinfo(struct sysinfo *val)
 }
 
 /*
- * swap_dup_entry_direct() - Increase reference count of a swap entry by one.
+ * swap_dup_entries_direct() - Increase reference count of swap entries by one.
  * @entry: first swap entry from which we want to increase the refcount.
+ * @nr: number of contiguous swap entries to duplicate.
  *
  * Returns 0 for success, or -ENOMEM if the extend table is required
  * but could not be atomically allocated.  Returns -EINVAL if the swap
@@ -3938,7 +3950,7 @@ void si_swapinfo(struct sysinfo *val)
  * Also the swap entry must have a count >= 1. Otherwise folio_dup_swap should
  * be used.
  */
-int swap_dup_entry_direct(swp_entry_t entry)
+int swap_dup_entries_direct(swp_entry_t entry, int nr)
 {
 	struct swap_info_struct *si;
 
@@ -3955,7 +3967,7 @@ int swap_dup_entry_direct(swp_entry_t entry)
 	 */
 	VM_WARN_ON_ONCE(!swap_entry_swapped(si, entry));
 
-	return swap_dup_entries_cluster(si, swp_offset(entry), 1);
+	return swap_dup_entries_cluster(si, swp_offset(entry), nr);
 }
 
 #if defined(CONFIG_MEMCG) && defined(CONFIG_BLK_CGROUP)
-- 
2.53.0-Meta



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

* [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (3 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 04/12] mm: handle PMD swap entries in fork path Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 18:28   ` Yosry Ahmed
  2026-08-18 13:09 ` [PATCH v6 06/12] mm: swap in PMD swap entries as whole THPs during swapoff Usama Arif
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Alexandre Ghiti, Usama Arif

From: Alexandre Ghiti <alexghiti@fb.com>

A large folio reaches zswap_load() only when the caller expects
the whole range to be on disk. Zswap still stores large folios as
independent order-0 entries, so reconstructing a large folio from
zswap entries would risk returning partially initialized data.

Teach zswap_load() to scan the covered range. If no slot is in zswap,
return -ENOENT so swap_read_folio() reads the backing device. If any
slot is still in zswap, fail the large-folio read so the caller can
fall back to per-page swapin.

Return -EIO rather than -EINVAL for that conflict. Large-folio loads
are now valid requests; the error means zswap cannot safely satisfy
the request from partial per-page compressed state, not that the
request is unsupported. Existing callers only distinguish -ENOENT,
so this is a semantic clarification rather than a behavioral change.

Add zswap_is_present() so PMD swap-entry consumers can make the same
range decision before attempting PMD-order swapin. Also use it from
__swap_cache_add_check() for multi-page insertions while holding the
swap cluster lock. That check runs before folio allocation and again
immediately before swap-cache insertion, closing the race with zswap
writeback and rejecting mixed zswap/disk backing with -EBUSY.

Signed-off-by: Alexandre Ghiti <alexghiti@fb.com>
Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 include/linux/zswap.h |  6 ++++++
 mm/swap_state.c       | 10 ++++++++++
 mm/zswap.c            | 46 +++++++++++++++++++++++++++++++------------
 3 files changed, 49 insertions(+), 13 deletions(-)

diff --git a/include/linux/zswap.h b/include/linux/zswap.h
index 30c193a1207e1..cd9efcf9dec94 100644
--- a/include/linux/zswap.h
+++ b/include/linux/zswap.h
@@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
 void zswap_folio_swapin(struct folio *folio);
 bool zswap_is_enabled(void);
 bool zswap_never_enabled(void);
+bool zswap_is_present(swp_entry_t entry, unsigned int nr);
 #else
 
 struct zswap_lruvec_state {};
@@ -69,6 +70,11 @@ static inline bool zswap_never_enabled(void)
 	return true;
 }
 
+static inline bool zswap_is_present(swp_entry_t entry, unsigned int nr)
+{
+	return false;
+}
+
 #endif
 
 #endif /* _LINUX_ZSWAP_H */
diff --git a/mm/swap_state.c b/mm/swap_state.c
index b76eb3d876fd7..15e200d6966b9 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -12,6 +12,7 @@
 #include <linux/kernel_stat.h>
 #include <linux/mempolicy.h>
 #include <linux/swap.h>
+#include <linux/zswap.h>
 #include <linux/leafops.h>
 #include <linux/init.h>
 #include <linux/pagemap.h>
@@ -191,6 +192,15 @@ static int __swap_cache_add_check(struct swap_cluster_info *ci,
 	if (nr == 1)
 		return 0;
 
+	/*
+	 * The cluster lock serializes swap-cache insertion with zswap
+	 * writeback. Reject mixed zswap/disk backing before allocating a
+	 * large folio and recheck it before adding the folio to swap cache.
+	 */
+	if (zswap_is_present(swp_entry(swp_type(targ_entry),
+				       round_down(swp_offset(targ_entry), nr)), nr))
+		return -EBUSY;
+
 	is_zero = __swap_table_test_zero(ci, ci_off);
 	ci_off = round_down(ci_off, nr);
 	ci_end = ci_off + nr;
diff --git a/mm/zswap.c b/mm/zswap.c
index 37f34e406c8e3..32671dc2bf84d 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -1571,6 +1571,23 @@ bool zswap_store(struct folio *folio)
 	return ret;
 }
 
+/**
+ * zswap_is_present() - is any slot in [entry, entry + nr) in zswap?
+ * @entry: base swap entry of the range
+ * @nr: number of contiguous slots to check (pass 1 for a single-slot query)
+ */
+bool zswap_is_present(swp_entry_t entry, unsigned int nr)
+{
+	pgoff_t offset = swp_offset(entry);
+	struct xarray *tree = swap_zswap_tree(entry);
+	unsigned long index = offset;
+
+	if (!nr || zswap_never_enabled())
+		return false;
+
+	return xa_find(tree, &index, offset + nr - 1, XA_PRESENT);
+}
+
 /**
  * zswap_load() - load a folio from zswap
  * @folio: folio to load
@@ -1578,13 +1595,9 @@ bool zswap_store(struct folio *folio)
  * Return: 0 on success, with the folio unlocked and marked up-to-date, or one
  * of the following error codes:
  *
- *  -EIO: if the swapped out content was in zswap, but could not be loaded
- *  into the page due to a decompression failure. The folio is unlocked, but
- *  NOT marked up-to-date, so that an IO error is emitted (e.g. do_swap_page()
- *  will SIGBUS).
- *
- *  -EINVAL: if the swapped out content was in zswap, but the page belongs
- *  to a large folio, which is not supported by zswap. The folio is unlocked,
+ *  -EIO: if the swapped out content was in zswap but could not be handed
+ *  back, either because decompression failed or because a slot in a
+ *  large-folio range is unexpectedly still in zswap. The folio is unlocked,
  *  but NOT marked up-to-date, so that an IO error is emitted (e.g.
  *  do_swap_page() will SIGBUS).
  *
@@ -1605,13 +1618,20 @@ int zswap_load(struct folio *folio)
 		return -ENOENT;
 
 	/*
-	 * Large folios should not be swapped in while zswap is being used, as
-	 * they are not properly handled. Zswap does not properly load large
-	 * folios, and a large folio may only be partially in zswap.
+	 * A large folio reaches zswap_load() only when its whole range is
+	 * expected to be on disk: PMD swap-entry consumers split before
+	 * calling into PMD-order swapin whenever any slot is still in zswap.
+	 * Confirm the range is entirely absent from zswap and return -ENOENT
+	 * so the caller reads it from disk; if a slot is unexpectedly still in
+	 * zswap, fail the read rather than return partially-initialized data.
 	 */
-	if (WARN_ON_ONCE(folio_test_large(folio))) {
-		folio_unlock(folio);
-		return -EINVAL;
+	if (folio_test_large(folio)) {
+		if (WARN_ON_ONCE(zswap_is_present(swp,
+						  folio_nr_pages(folio)))) {
+			folio_unlock(folio);
+			return -EIO;
+		}
+		return -ENOENT;
 	}
 
 	entry = xa_load(tree, offset);
-- 
2.53.0-Meta



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

* [PATCH v6 06/12] mm: swap in PMD swap entries as whole THPs during swapoff
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (4 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 07/12] mm: handle PMD swap entries in non-present PMD walkers Usama Arif
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Add swap_pmd_cache_lookup() to classify the swap cache behind a PMD
swap entry as empty, backed by one PMD-sized folio, or requiring
per-page handling because at least one covered slot has a smaller folio
in the swap cache.  PMD swap entries are handled at PMD granularity only
while the covered cache range is empty or backed by a PMD-sized folio;
a split cache forces the entry to be split and retried through the PTE
path.

Add unuse_pmd() and call it from unuse_pmd_range() to swap in
PMD-level swap entries as whole THPs during swapoff.  This mirrors
the existing unuse_pte_range() but operates at PMD granularity.
Preserve soft-dirty, exclusive, and UFFD state when installing the
present PMD.  If an RWP VMA has a UFFD-marked swap entry, also restore
PAGE_NONE so the first subsequent access still generates a userfault.

If the PMD-order folio cannot be allocated or read, the swap cache
already contains per-page folios in the covered range (e.g. split in
the swap cache by deferred_split_scan() or memory_failure() while the
PMD swap entry was installed), or any subpage is hardware-poisoned,
the PMD swap entry is split into PTE-level entries via
__split_huge_pmd() and a non-zero error is returned so unuse_pmd_range()
falls through to unuse_pte_range(), which handles the individual
entries at order-0.

Remove a failed !uptodate PMD-sized folio from swap cache before
splitting so PTE fallback rereads each slot independently. Keep
hwpoisoned folios cached so PTE fallback can isolate bad subpages.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 mm/swap.h       |  17 +++++
 mm/swap_state.c |  44 +++++++++++++
 mm/swapfile.c   | 169 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 230 insertions(+)

diff --git a/mm/swap.h b/mm/swap.h
index e225527b2eb7a..441e017afc67b 100644
--- a/mm/swap.h
+++ b/mm/swap.h
@@ -311,6 +311,23 @@ static inline bool folio_matches_swap_entry(const struct folio *folio,
 bool swap_cache_has_folio(swp_entry_t entry);
 struct folio *swap_cache_get_folio(swp_entry_t entry);
 void *swap_cache_get_shadow(swp_entry_t entry);
+enum swap_pmd_cache {
+	SWAP_PMD_CACHE_EMPTY,
+	SWAP_PMD_CACHE_HUGE,
+	SWAP_PMD_CACHE_SPLIT,
+};
+
+#ifdef CONFIG_THP_SWAP
+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+					  struct folio **foliop);
+#else
+static inline enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+							struct folio **foliop)
+{
+	*foliop = NULL;
+	return SWAP_PMD_CACHE_EMPTY;
+}
+#endif
 void swap_cache_del_folio(struct folio *folio);
 struct folio *swap_cache_alloc_folio(swp_entry_t target_entry, gfp_t gfp_mask,
 				     unsigned long orders, struct vm_fault *vmf,
diff --git a/mm/swap_state.c b/mm/swap_state.c
index 15e200d6966b9..559dc00b28f95 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -125,6 +125,50 @@ bool swap_cache_has_folio(swp_entry_t entry)
 	return swp_tb_is_folio(swp_tb);
 }
 
+#ifdef CONFIG_THP_SWAP
+/**
+ * swap_pmd_cache_lookup - classify the swap cache behind a PMD swap entry
+ * @entry: first swap slot encoded by the PMD swap entry
+ * @foliop: returned PMD-sized folio, with a reference, if present
+ *
+ * A PMD swap entry is a compact page-table encoding for HPAGE_PMD_NR
+ * consecutive swap slots. The swap cache behind those slots can be empty,
+ * one PMD-sized folio, or per-slot folios after the original folio was split.
+ *
+ * Context: Caller must keep @entry valid using the usual swap cache rules.
+ * Return: SWAP_PMD_CACHE_EMPTY if no slot in the PMD range has a cached folio,
+ * SWAP_PMD_CACHE_HUGE if one PMD-sized folio covers the range, or
+ * SWAP_PMD_CACHE_SPLIT if the range needs per-page handling.
+ */
+enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry,
+					  struct folio **foliop)
+{
+	unsigned int type = swp_type(entry);
+	pgoff_t offset = swp_offset(entry);
+	struct folio *folio;
+	int i;
+
+	*foliop = NULL;
+
+	folio = swap_cache_get_folio(entry);
+	if (folio) {
+		if (folio_nr_pages(folio) == HPAGE_PMD_NR) {
+			*foliop = folio;
+			return SWAP_PMD_CACHE_HUGE;
+		}
+		folio_put(folio);
+		return SWAP_PMD_CACHE_SPLIT;
+	}
+
+	for (i = 1; i < HPAGE_PMD_NR; i++) {
+		if (swap_cache_has_folio(swp_entry(type, offset + i)))
+			return SWAP_PMD_CACHE_SPLIT;
+	}
+
+	return SWAP_PMD_CACHE_EMPTY;
+}
+#endif
+
 /**
  * swap_cache_get_shadow - Looks up a shadow in the swap cache.
  * @entry: swap entry used for the lookup.
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 44b9ebbe7229f..67a7e2053dc12 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -42,6 +42,7 @@
 #include <linux/suspend.h>
 #include <linux/zswap.h>
 #include <linux/plist.h>
+#include <linux/huge_mm.h>
 
 #include <asm/tlbflush.h>
 #include <linux/leafops.h>
@@ -2670,6 +2671,160 @@ static int unuse_pte_range(struct vm_area_struct *vma, pmd_t *pmd,
 	return 0;
 }
 
+#ifdef CONFIG_THP_SWAP
+/*
+ * unuse_pmd - Map a locked folio at PMD granularity during swapoff.
+ *
+ * The caller provides a locked, swapped-in folio.  Returns 0 on success
+ * (PMD was mapped).  Returns -EAGAIN if the swap cache folio no longer
+ * matches the entry or the PMD changed under the lock (try_to_unuse will
+ * rescan). Returns -EIO if the folio is not uptodate or contains a poisoned
+ * subpage; in that case the PMD is split so unuse_pte_range() can handle
+ * individual pages.
+ */
+static int unuse_pmd(struct vm_area_struct *vma, pmd_t *pmd,
+		     unsigned long addr, softleaf_t entry,
+		     struct folio *folio)
+{
+	struct mm_struct *mm = vma->vm_mm;
+	struct page *page;
+	pmd_t new_pmd, old_pmd;
+	spinlock_t *ptl;
+	rmap_t rmap_flags = RMAP_NONE;
+	bool exclusive;
+
+	if (unlikely(!folio_matches_swap_entry(folio, entry)))
+		return -EAGAIN;
+
+	if (unlikely(!folio_test_uptodate(folio))) {
+		/* Let PTE fallback reread each slot independently. */
+		swap_cache_del_folio(folio);
+		__split_huge_pmd(vma, pmd, addr, false);
+		return -EIO;
+	}
+
+	if (unlikely(folio_contain_hwpoisoned_page(folio))) {
+		/* Let PTE fallback isolate the poisoned subpages. */
+		__split_huge_pmd(vma, pmd, addr, false);
+		return -EIO;
+	}
+
+	page = folio_page(folio, 0);
+
+	ptl = pmd_lock(mm, pmd);
+	old_pmd = pmdp_get(pmd);
+
+	if (!pmd_is_swap_entry(old_pmd) ||
+	    softleaf_from_pmd(old_pmd).val != entry.val) {
+		spin_unlock(ptl);
+		return -EAGAIN;
+	}
+
+	exclusive = pmd_swp_exclusive(old_pmd);
+
+	/*
+	 * Some architectures may have to restore extra metadata to the folio
+	 * when reading from swap. This metadata may be indexed by swap entry
+	 * so this must be called before folio_put_swap().
+	 */
+	arch_swap_restore(folio_swap(entry, folio), folio);
+
+	add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+
+	new_pmd = folio_mk_pmd(folio, vma->vm_page_prot);
+	new_pmd = pmd_mkold(new_pmd);
+	if (pmd_swp_soft_dirty(old_pmd))
+		new_pmd = pmd_mksoft_dirty(new_pmd);
+	if (pmd_swp_uffd(old_pmd))
+		new_pmd = pmd_mkuffd(new_pmd);
+	if (pmd_swp_uffd(old_pmd) && userfaultfd_rwp(vma))
+		new_pmd = pmd_modify(new_pmd, PAGE_NONE);
+
+	if (exclusive)
+		rmap_flags |= RMAP_EXCLUSIVE;
+
+	folio_get(folio);
+	if (!folio_test_anon(folio))
+		folio_add_new_anon_rmap(folio, vma, addr, rmap_flags);
+	else
+		folio_add_anon_rmap_pmd(folio, page, vma, addr, rmap_flags);
+
+	set_pmd_at(mm, addr, pmd, new_pmd);
+	folio_put_swap(folio, NULL);
+
+	spin_unlock(ptl);
+
+	folio_free_swap(folio);
+	return 0;
+}
+
+/*
+ * Try to swap in a PMD swap entry as a whole THP. Returns 0 on success.
+ * If the swap cache no longer has one PMD-sized folio, zswap may require
+ * per-page loading, or a PMD-order allocation/read fails, split the PMD so
+ * the caller can fall back to unuse_pte_range(). Otherwise propagates the
+ * error from unuse_pmd().
+ */
+static int unuse_pmd_entry(struct vm_area_struct *vma, pmd_t *pmd,
+			   unsigned long addr, softleaf_t entry)
+{
+	struct folio *folio;
+	enum swap_pmd_cache cache_state;
+	int ret;
+
+	cache_state = swap_pmd_cache_lookup(entry, &folio);
+	if (cache_state == SWAP_PMD_CACHE_SPLIT) {
+		ret = -EAGAIN;
+		goto split_fallback;
+	}
+	if (!folio) {
+		struct vm_fault vmf = {
+			.vma = vma,
+			.address = addr,
+			.real_address = addr,
+			.pmd = pmd,
+		};
+
+		if (zswap_is_present(entry, HPAGE_PMD_NR)) {
+			ret = -EAGAIN;
+			goto split_fallback;
+		}
+
+		folio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,
+				    BIT(HPAGE_PMD_ORDER), &vmf, NULL, 0);
+		if (IS_ERR_OR_NULL(folio)) {
+			ret = folio ? PTR_ERR(folio) : -ENOMEM;
+			goto split_fallback;
+		}
+	}
+
+	folio_lock(folio);
+	folio_wait_writeback(folio);
+	/*
+	 * If the cached folio is no longer PMD-sized (e.g. split in the
+	 * swap cache by deferred_split_scan() or memory_failure() while
+	 * the PMD swap entry was installed), the PMD swap entry no longer
+	 * maps a single contiguous folio.  Split the PMD swap entry so
+	 * unuse_pte_range() can swap the per-slot folios in individually.
+	 */
+	if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+		folio_unlock(folio);
+		folio_put(folio);
+		ret = -EAGAIN;
+		goto split_fallback;
+	}
+	ret = unuse_pmd(vma, pmd, addr, entry, folio);
+	folio_unlock(folio);
+	folio_put(folio);
+	return ret;
+
+split_fallback:
+	__split_huge_pmd(vma, pmd, addr, false);
+	return ret;
+}
+#endif
+
 static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,
 				unsigned long addr, unsigned long end,
 				unsigned int type)
@@ -2682,6 +2837,20 @@ static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud,
 	do {
 		cond_resched();
 		next = pmd_addr_end(addr, end);
+
+#ifdef CONFIG_THP_SWAP
+		pmd_t pmdval = pmdp_get(pmd);
+
+		if (pmd_is_swap_entry(pmdval)) {
+			softleaf_t sl = softleaf_from_pmd(pmdval);
+
+			if (swp_type(sl) == type) {
+				if (!unuse_pmd_entry(vma, pmd, addr, sl))
+					continue;
+			}
+		}
+#endif
+
 		ret = unuse_pte_range(vma, pmd, addr, next, type);
 		if (ret)
 			return ret;
-- 
2.53.0-Meta



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

* [PATCH v6 07/12] mm: handle PMD swap entries in non-present PMD walkers
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (5 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 06/12] mm: swap in PMD swap entries as whole THPs during swapoff Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 08/12] mm: handle PMD swap entries in MADV_WILLNEED Usama Arif
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Teach the remaining non-present PMD walkers about swap entries,
mirroring the PTE-level equivalents.

smaps_pmd_entry() accounts swap and swap_pss via a new shared
smaps_account_swap() helper used by both PTE and PMD paths. For a
PMD range, it accounts each covered slot separately because their
swap reference counts can differ.

move_soft_dirty_pmd(), clear_soft_dirty_pmd(), and make_uffd_wp_pmd(),
pagemap_pmd_range_thp() and change_huge_pmd() handle swap entries
alongside migration entries.

hmm_vma_handle_absent_pmd() records a fault for PMD swap entries via
hmm_record_fault() instead of returning -EFAULT, allowing
hmm_range_fault() to fault them in. The first per-page handle_mm_fault()
call triggers do_huge_pmd_swap_page(), which maps the entire folio;
subsequent calls become harmless huge_pmd_set_accessed() and the walker
retries with a present PMD. When no fault is requested, it reports the
range as non-resident rather than HMM_PFN_ERROR, matching the PTE swap
entry path.

madvise_free_huge_pmd() handles PMD swap entries directly: for a
full-range MADV_FREE it clears the PMD, frees the deposited page
table, and releases the swap slots; for a partial range it splits to
PTE swap entries. Without this, MADV_FREE silently becomes a no-op
on swapped-out THPs and leaves the swap slots allocated, unlike the
PTE path which frees them.

zap_huge_pmd() frees swap slots via swap_put_entries_direct(),
matching zap_nonpresent_ptes().

change_non_present_huge_pmd() skips write-permission changes for swap
entries and only updates uffd_wp, matching change_softleaf_pte().

madvise_cold_or_pageout_pte_range() skips PMD swap entries early.
MADV_COLD and MADV_PAGEOUT operate on resident folios, so a swapped-out
THP has nothing to deactivate or reclaim; skipping also prevents the
walker from descending into or splitting the PMD swap entry. The locked
THP path also treats a racing PMD swap entry as handled before checking
for other non-present PMD types.

mincore_pte_range() routes PMD swap entries through a new
mincore_pmd_swap() helper, matching how the PTE path already calls
mincore_swap() for non-present PTEs. Without this a swapped-out
PMD-mapped THP would be reported as resident, because pmd_is_huge()
(and therefore pmd_trans_huge_lock()) accepts any non-present non-none
PMD and the old branch unconditionally did memset(vec, 1, nr).
mincore_pmd_swap() checks the PMD-sized swap-cache folio, or the
individual slots if it was split. Migration and device-private PMDs
keep their previous behavior of reporting 1.

check_pmd_state() in khugepaged returns SCAN_PMD_MAPPED for PMD swap
entries, treating a swapped-out THP as still being a THP from
khugepaged's perspective and matching the existing migration-entry
handling.

change_huge_pmd() and pagemap_pmd_range_thp() drop redundant
thp_migration_supported() gates: when PMD softleaves are unsupported,
softleaf_from_pmd() returns a none entry and pmd_is_valid_softleaf()
is false.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 fs/proc/task_mmu.c | 46 ++++++++++++++++++++++++++--------------
 mm/hmm.c           | 11 +++++++++-
 mm/huge_memory.c   | 53 ++++++++++++++++++++++++++++++++++++----------
 mm/khugepaged.c    |  6 ++++++
 mm/madvise.c       | 14 +++++++++++-
 mm/mincore.c       | 45 ++++++++++++++++++++++++++++++++++++++-
 6 files changed, 145 insertions(+), 30 deletions(-)

diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 5c54aebe21182..8926392e33338 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -1046,6 +1046,27 @@ static void smaps_pte_hole_lookup(unsigned long addr, struct mm_walk *walk)
 #endif
 }
 
+static void smaps_account_swap(struct mem_size_stats *mss,
+			       softleaf_t entry, unsigned long size)
+{
+	unsigned long nr_pages = size >> PAGE_SHIFT;
+
+	mss->swap += size;
+	do {
+		int mapcount = swp_swapcount(entry);
+
+		if (mapcount >= 2) {
+			u64 pss_delta = (u64)PAGE_SIZE << PSS_SHIFT;
+
+			do_div(pss_delta, mapcount);
+			mss->swap_pss += pss_delta;
+		} else {
+			mss->swap_pss += (u64)PAGE_SIZE << PSS_SHIFT;
+		}
+		entry.val++;
+	} while (--nr_pages);
+}
+
 static void smaps_pte_entry(pte_t *pte, unsigned long addr,
 		struct mm_walk *walk)
 {
@@ -1067,18 +1088,7 @@ static void smaps_pte_entry(pte_t *pte, unsigned long addr,
 		const softleaf_t entry = softleaf_from_pte(ptent);
 
 		if (softleaf_is_swap(entry)) {
-			int mapcount;
-
-			mss->swap += PAGE_SIZE;
-			mapcount = swp_swapcount(entry);
-			if (mapcount >= 2) {
-				u64 pss_delta = (u64)PAGE_SIZE << PSS_SHIFT;
-
-				do_div(pss_delta, mapcount);
-				mss->swap_pss += pss_delta;
-			} else {
-				mss->swap_pss += (u64)PAGE_SIZE << PSS_SHIFT;
-			}
+			smaps_account_swap(mss, entry, PAGE_SIZE);
 		} else if (softleaf_has_pfn(entry)) {
 			if (softleaf_is_device_private(entry))
 				present = true;
@@ -1108,9 +1118,13 @@ static void smaps_pmd_entry(pmd_t *pmd, unsigned long addr,
 	if (pmd_present(*pmd)) {
 		page = vm_normal_page_pmd(vma, addr, *pmd);
 		present = true;
-	} else if (unlikely(thp_migration_supported())) {
+	} else {
 		const softleaf_t entry = softleaf_from_pmd(*pmd);
 
+		if (softleaf_is_swap(entry)) {
+			smaps_account_swap(mss, entry, HPAGE_PMD_SIZE);
+			return;
+		}
 		if (softleaf_has_pfn(entry))
 			page = softleaf_to_page(entry);
 	}
@@ -1755,7 +1769,7 @@ static inline void clear_soft_dirty_pmd(struct vm_area_struct *vma,
 		pmd = pmd_clear_soft_dirty(pmd);
 
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
-	} else if (pmd_is_migration_entry(pmd)) {
+	} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {
 		pmd = pmd_swp_clear_soft_dirty(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}
@@ -2115,7 +2129,7 @@ static int pagemap_pmd_range_thp(pmd_t *pmdp, unsigned long addr,
 			flags |= PM_UFFD_WP;
 		if (pm->show_pfn)
 			frame = pmd_pfn(pmd) + idx;
-	} else if (thp_migration_supported()) {
+	} else if (pmd_is_valid_softleaf(pmd)) {
 		const softleaf_t entry = softleaf_from_pmd(pmd);
 		unsigned long offset;
 
@@ -2581,7 +2595,7 @@ static void make_uffd_wp_pmd(struct vm_area_struct *vma,
 		old = pmdp_invalidate_ad(vma, addr, pmdp);
 		pmd = pmd_mkuffd(old);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
-	} else if (pmd_is_migration_entry(pmd)) {
+	} else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) {
 		pmd = pmd_swp_mkuffd(pmd);
 		set_pmd_at(vma->vm_mm, addr, pmdp, pmd);
 	}
diff --git a/mm/hmm.c b/mm/hmm.c
index 2f1e98c6b6440..95575ac378888 100644
--- a/mm/hmm.c
+++ b/mm/hmm.c
@@ -377,12 +377,21 @@ static int hmm_vma_handle_absent_pmd(struct mm_walk *walk, unsigned long start,
 	required_fault = hmm_range_need_fault(hmm_vma_walk, hmm_pfns,
 					      npages, 0);
 	if (required_fault) {
-		if (softleaf_is_device_private(entry))
+		if (softleaf_is_device_private(entry) ||
+		    softleaf_is_swap(entry))
 			return hmm_record_fault(addr, end, required_fault, walk);
 		else
 			return -EFAULT;
 	}
 
+	/*
+	 * A swapped-out THP is not resident. Report it as not-valid,
+	 * matching what hmm_vma_handle_pte() does for a PTE swap entry when
+	 * no fault was requested.
+	 */
+	if (softleaf_is_swap(entry))
+		return hmm_pfns_fill(start, end, range, 0);
+
 	return hmm_pfns_fill(start, end, range, HMM_PFN_ERROR);
 }
 #else
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 2735c3de7029c..54aef8394fa25 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2397,6 +2397,14 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *vmf)
 	return 0;
 }
 
+static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)
+{
+	pgtable_t pgtable;
+
+	pgtable = pgtable_trans_huge_withdraw(mm, pmd);
+	pte_free(mm, pgtable);
+	mm_dec_nr_ptes(mm);
+}
 /*
  * Return true if we do MADV_FREE successfully on entire pmd page.
  * Otherwise, return false.
@@ -2421,6 +2429,21 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 		goto out;
 
 	if (unlikely(!pmd_present(orig_pmd))) {
+		if (pmd_is_swap_entry(orig_pmd)) {
+			if (next - addr != HPAGE_PMD_SIZE) {
+				spin_unlock(ptl);
+				__split_huge_pmd(vma, pmd, addr, false);
+				goto out_unlocked;
+			}
+			softleaf_t sl = softleaf_from_pmd(orig_pmd);
+
+			pmdp_huge_get_and_clear(mm, addr, pmd);
+			zap_deposited_table(mm, pmd);
+			spin_unlock(ptl);
+			swap_put_entries_direct(sl, HPAGE_PMD_NR);
+			add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+			return true;
+		}
 		VM_WARN_ON_ONCE(!pmd_is_migration_entry(orig_pmd) &&
 				!pmd_is_device_private_entry(orig_pmd));
 		goto out;
@@ -2471,15 +2494,6 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	return ret;
 }
 
-static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)
-{
-	pgtable_t pgtable;
-
-	pgtable = pgtable_trans_huge_withdraw(mm, pmd);
-	pte_free(mm, pgtable);
-	mm_dec_nr_ptes(mm);
-}
-
 static void zap_huge_pmd_folio(struct mm_struct *mm, struct vm_area_struct *vma,
 		pmd_t pmdval, struct folio *folio, bool is_present)
 {
@@ -2572,6 +2586,16 @@ bool zap_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	arch_check_zapped_pmd(vma, orig_pmd);
 	tlb_remove_pmd_tlb_entry(tlb, pmd, addr);
 
+	if (pmd_is_swap_entry(orig_pmd)) {
+		softleaf_t sl = softleaf_from_pmd(orig_pmd);
+
+		zap_deposited_table(mm, pmd);
+		spin_unlock(ptl);
+		swap_put_entries_direct(sl, HPAGE_PMD_NR);
+		add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+		return true;
+	}
+
 	is_present = pmd_present(orig_pmd);
 	folio = normal_or_softleaf_folio_pmd(vma, addr, orig_pmd, is_present);
 	has_deposit = has_deposited_pgtable(vma, orig_pmd, folio);
@@ -2604,7 +2628,8 @@ static inline int pmd_move_must_withdraw(spinlock_t *new_pmd_ptl,
 static pmd_t move_soft_dirty_pmd(pmd_t pmd)
 {
 	if (pgtable_supports_soft_dirty()) {
-		if (unlikely(pmd_is_migration_entry(pmd)))
+		if (unlikely(pmd_is_migration_entry(pmd) ||
+			     pmd_is_swap_entry(pmd)))
 			pmd = pmd_swp_mksoft_dirty(pmd);
 		else if (pmd_present(pmd))
 			pmd = pmd_mksoft_dirty(pmd);
@@ -2695,6 +2720,12 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,
 	pmd_t newpmd;
 
 	VM_WARN_ON(!pmd_is_valid_softleaf(*pmd));
+
+	/*
+	 * Note that a PMD swap entry falls into the default branch below: it
+	 * does not encode write permission in the entry type, so only the
+	 * uffd_wp flag update at the end applies to it.
+	 */
 	if (softleaf_is_migration_write(entry)) {
 		const struct folio *folio = softleaf_to_folio(entry);
 
@@ -2755,7 +2786,7 @@ int change_huge_pmd(struct mmu_gather *tlb, struct vm_area_struct *vma,
 	if (!ptl)
 		return 0;
 
-	if (thp_migration_supported() && pmd_is_valid_softleaf(*pmd)) {
+	if (pmd_is_valid_softleaf(*pmd)) {
 		change_non_present_huge_pmd(mm, addr, pmd, uffd_prot,
 					    uffd_prot_resolve);
 		goto unlock;
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 5a06e3942e889..15e2d0c7384d3 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -1125,6 +1125,12 @@ static inline enum scan_result check_pmd_state(pmd_t *pmd)
 	 */
 	if (pmd_is_migration_entry(pmde))
 		return SCAN_PMD_MAPPED;
+	/*
+	 * A PMD-mapped THP that has been swapped out is still a THP from
+	 * khugepaged's perspective; treat it like a present huge PMD.
+	 */
+	if (pmd_is_swap_entry(pmde))
+		return SCAN_PMD_MAPPED;
 	if (!pmd_present(pmde))
 		return SCAN_NO_PTE_TABLE;
 	if (pmd_trans_huge(pmde))
diff --git a/mm/madvise.c b/mm/madvise.c
index c179938097bf0..16b39a06b038f 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -375,6 +375,15 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 					!can_do_file_pageout(vma);
 
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
+	/*
+	 * Swapped-out THPs have no resident folio to deactivate or reclaim.
+	 * Avoid descending into or splitting a PMD swap entry.
+	 */
+	if (pmd_is_swap_entry(*pmd)) {
+		walk->action = ACTION_CONTINUE;
+		return 0;
+	}
+
 	if (pmd_trans_huge(*pmd)) {
 		pmd_t orig_pmd;
 		unsigned long next = pmd_addr_end(addr, end);
@@ -385,6 +394,9 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 			return 0;
 
 		orig_pmd = *pmd;
+		if (pmd_is_swap_entry(orig_pmd))
+			goto huge_unlock;
+
 		if (is_huge_zero_pmd(orig_pmd))
 			goto huge_unlock;
 
@@ -666,7 +678,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	int nr, max_nr;
 
 	next = pmd_addr_end(addr, end);
-	if (pmd_trans_huge(*pmd))
+	if (pmd_trans_huge(*pmd) || pmd_is_swap_entry(*pmd))
 		if (madvise_free_huge_pmd(tlb, vma, pmd, addr, next))
 			return 0;
 
diff --git a/mm/mincore.c b/mm/mincore.c
index ff4ac82817683..3f0fba964c8a3 100644
--- a/mm/mincore.c
+++ b/mm/mincore.c
@@ -85,6 +85,41 @@ static unsigned char mincore_swap(swp_entry_t entry, bool shmem)
 	return present;
 }
 
+#ifdef CONFIG_THP_SWAP
+static void mincore_pmd_swap(swp_entry_t entry, unsigned long addr,
+			     unsigned long end, unsigned char *vec)
+{
+	unsigned long haddr = addr & HPAGE_PMD_MASK;
+	unsigned long start = (addr - haddr) >> PAGE_SHIFT;
+	unsigned long nr = (end - addr) >> PAGE_SHIFT;
+	struct folio *folio;
+	enum swap_pmd_cache state;
+	int i;
+
+	state = swap_pmd_cache_lookup(entry, &folio);
+	if (state == SWAP_PMD_CACHE_HUGE) {
+		memset(vec, folio_test_uptodate(folio), nr);
+		folio_put(folio);
+		return;
+	}
+
+	if (state == SWAP_PMD_CACHE_EMPTY) {
+		memset(vec, 0, nr);
+		return;
+	}
+
+	/*
+	 * The PMD swap entry is only a compact encoding for consecutive swap
+	 * slots. If the PMD-sized swapcache folio was split, report residency
+	 * from the individual slots covered by this mincore() range.
+	 */
+	for (i = 0; i < nr; i++)
+		vec[i] = mincore_swap(swp_entry(swp_type(entry),
+						swp_offset(entry) + start + i),
+				      false);
+}
+#endif
+
 /*
  * Later we can get more picky about what "in core" means precisely.
  * For now, simply check to see if the page is in the page cache,
@@ -171,7 +206,15 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 
 	ptl = pmd_trans_huge_lock(pmd, vma);
 	if (ptl) {
-		memset(vec, 1, nr);
+		if (pmd_is_swap_entry(*pmd)) {
+#ifdef CONFIG_THP_SWAP
+			mincore_pmd_swap(softleaf_from_pmd(*pmd), addr, end, vec);
+#else
+			memset(vec, 0, nr);
+#endif
+		} else {
+			memset(vec, 1, nr);
+		}
 		spin_unlock(ptl);
 		goto out;
 	}
-- 
2.53.0-Meta



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

* [PATCH v6 08/12] mm: handle PMD swap entries in MADV_WILLNEED
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (6 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 07/12] mm: handle PMD swap entries in non-present PMD walkers Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 09/12] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

swapin_walk_pmd_entry() walks PTEs and skips non-present PMDs, so
MADV_WILLNEED is a no-op on a PMD swap entry.

Handle PMD swap entries under pmd_trans_huge_lock(). If the covered
swap-cache range already has a PMD-sized folio, there is nothing left
to prefetch. If the range has split cache state, or any covered slot
currently has a zswap entry, split the PMD swap entry and ask the
walker to retry so the PTE path can handle the individual slots.

Otherwise pin the swap device and read the folio in at PMD order via
swapin_sync(BIT(HPAGE_PMD_ORDER)). This keeps the subsequent fault on
the do_huge_pmd_swap_page() path and avoids order-0 readahead
needlessly splitting the PMD swap entry.

Any failure of the PMD-order swapin splits the entry and retries
through the PTE path. That covers losing a race with per-slot
swap-cache population (-EBUSY) after dropping the PMD lock, but also
the -ENOMEM that a PMD-order allocation can easily hit: leaving the
entry alone would make MADV_WILLNEED prefetch nothing at all for the
range, while the PTE path can still read the 512 slots at order 0.

If per-page zswap state reappears during the read, remove the failed
clean PMD-sized folio from swap cache before splitting so the PTE path
can load each slot. This uses folio_trylock(): the lock is only free
once the read has completed, so MADV_WILLNEED never blocks on in-flight
I/O, and unlike testing folio_test_locked() directly it cannot race
with an unrelated lock holder.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 mm/madvise.c | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/mm/madvise.c b/mm/madvise.c
index 16b39a06b038f..3ef1af1e76daa 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -33,6 +33,7 @@
 #include <linux/shmem_fs.h>
 #include <linux/mmu_notifier.h>
 #include <linux/swap_ops.h>
+#include <linux/zswap.h>
 
 #include <asm/tlb.h>
 
@@ -185,6 +186,93 @@ static int madvise_update_vma(vm_flags_t new_flags,
 }
 
 #ifdef CONFIG_SWAP
+/*
+ * Prefetch a whole PMD swap entry as one PMD-order folio.
+ *
+ * Called with the PMD lock held; always drops it. Returns true when the
+ * caller should ask the walker to retry so the PTE path can handle the
+ * covered slots individually.
+ */
+static bool swapin_pmd_swap_entry(struct vm_area_struct *vma, pmd_t *pmd,
+				  unsigned long addr, softleaf_t entry,
+				  spinlock_t *ptl)
+{
+	struct vm_fault vmf = {
+		.vma = vma,
+		.address = addr,
+		.real_address = addr,
+		.pmd = pmd,
+	};
+	enum swap_pmd_cache cache_state;
+	struct swap_info_struct *si;
+	struct folio *folio;
+	bool split = false;
+
+	cache_state = swap_pmd_cache_lookup(entry, &folio);
+	if (cache_state == SWAP_PMD_CACHE_HUGE) {
+		/* Already cached as one PMD-sized folio, nothing to do. */
+		folio_put(folio);
+		spin_unlock(ptl);
+		return false;
+	}
+	if (cache_state == SWAP_PMD_CACHE_SPLIT ||
+	    zswap_is_present(entry, HPAGE_PMD_NR)) {
+		spin_unlock(ptl);
+		return true;
+	}
+
+	/*
+	 * Pin the swap device under the PMD lock so the PMD-swap-entry
+	 * observation keeps the entry valid for swapin_sync().
+	 */
+	si = get_swap_device(entry);
+	spin_unlock(ptl);
+	if (!si)
+		return false;
+
+	folio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE, BIT(HPAGE_PMD_ORDER),
+			    &vmf, NULL, 0);
+
+	/*
+	 * Fall back to PTE-order swapin: a PMD-order failure does not mean
+	 * that individual slots cannot be read.
+	 */
+	if (IS_ERR_OR_NULL(folio)) {
+		split = true;
+		goto out;
+	}
+
+	if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+		split = true;
+		goto out_put;
+	}
+
+	/*
+	 * A trylock only succeeds once the read has completed, so this never
+	 * blocks MADV_WILLNEED on in-flight I/O. A failed PMD-order zswap load
+	 * leaves the folio clean and not uptodate; drop it from the swap cache
+	 * so the PTE retry can load the per-page state. Another thread may
+	 * have removed it already, so revalidate the association first.
+	 */
+	if (!folio_trylock(folio))
+		goto out_put;
+
+	if (!folio_test_uptodate(folio) &&
+	    zswap_is_present(entry, HPAGE_PMD_NR)) {
+		if (folio_matches_swap_entry(folio, entry))
+			swap_cache_del_folio(folio);
+		split = true;
+	}
+	folio_unlock(folio);
+
+out_put:
+	folio_put(folio);
+out:
+	/* Keep the device pinned until the last use of @entry. */
+	put_swap_device(si);
+	return split;
+}
+
 static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
 		unsigned long end, struct mm_walk *walk)
 {
@@ -194,6 +282,23 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
 	spinlock_t *ptl;
 	unsigned long addr;
 
+	ptl = pmd_trans_huge_lock(pmd, vma);
+	if (ptl) {
+		pmd_t pmdval = *pmd;
+
+		if (pmd_is_swap_entry(pmdval)) {
+			/* swapin_pmd_swap_entry() always drops the PMD lock. */
+			if (swapin_pmd_swap_entry(vma, pmd, start,
+						  softleaf_from_pmd(pmdval),
+						  ptl)) {
+				__split_huge_pmd(vma, pmd, start, false);
+				walk->action = ACTION_AGAIN;
+			}
+			goto ret;
+		}
+		spin_unlock(ptl);
+	}
+
 	for (addr = start; addr < end; addr += PAGE_SIZE) {
 		pte_t pte;
 		softleaf_t entry;
@@ -222,6 +327,7 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned long start,
 	if (ptep)
 		pte_unmap_unlock(ptep, ptl);
 	swap_read_submit(&ctx);
+ret:
 	cond_resched();
 
 	return 0;
-- 
2.53.0-Meta



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

* [PATCH v6 09/12] mm: handle PMD swap entries in UFFDIO_MOVE
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (7 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 08/12] mm: handle PMD swap entries in MADV_WILLNEED Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 10/12] mm: handle PMD swap entry faults on swap-in Usama Arif
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

move_pages_huge_pmd() returned -ENOENT for any non-trans_huge,
non-migration PMD, which fails aligned UFFDIO_MOVE on a swapped-out
THP -- the PMD swap entry is a perfectly valid mapping that should
move whole. Splitting via the move_pages_ptes() fallback isn't a
substitute either: __split_huge_pmd_locked() splits a PMD swap entry
into HPAGE_PMD_NR PTE swap entries pointing at the same swap-cache
folio, but move_pages_pte() refuses any swap-cache folio that is still
large and returns -EBUSY.

Add move_swap_pmd(), modeled on move_swap_pte(), that moves the swap
entry whole-PMD and re-anchors a PMD-sized swap-cache folio's anon rmap
to the destination VMA. Reject !pmd_swp_exclusive() entries with
-EBUSY to preserve UFFDIO_MOVE's single-owner semantics, propagate
soft-dirty, arm the UFFD marker for an RWP-registered destination, and
carry the deposited page table across with the entry. The marker
handling matches move_swap_pte(): the source marker rides along with
the entry and is additionally armed for an RWP destination.

The dispatcher in move_pages_huge_pmd() routes PMD swap entries
through move_swap_pmd() after pinning the swap device and arming an
mmu_notifier range. Both are guarded by CONFIG_THP_SWAP, since
swap_pmd_cache_lookup() and friends only exist under CONFIG_SWAP and
PMD swap entries cannot exist without CONFIG_THP_SWAP.

Before moving, classify the whole PMD swap-cache range with
swap_pmd_cache_lookup(). A PMD swap entry can be moved whole only if
the covered range is empty or backed by one PMD-sized folio. If the
range already has per-slot cache state, split the PMD swap entry and
return -EAGAIN so the caller retries through the PTE path.

If a PMD-sized folio is cached, lock and revalidate that it still
matches the PMD swap entry. If no folio is cached, recheck all
HPAGE_PMD_NR slots under both PMD locks before moving the entry; any
per-slot folio that appears needs the PTE move path to update its rmap
metadata. This avoids moving the PMD while cached folios still point at
the old anon_vma/index.

Finally, reject a PMD swap entry at the *destination* with -EEXIST in
move_pages(). Such a destination is not a hole, and unlike a PMD
migration entry it does not resolve on its own: pte_alloc() skips a
!pmd_none PMD, pte_offset_map_rw_nolock() then fails on the non-present
PMD, and move_pages() would retry the resulting -EAGAIN forever, only
escapable with a fatal signal.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 mm/huge_memory.c | 140 ++++++++++++++++++++++++++++++++++++++++++++++-
 mm/userfaultfd.c |  14 +++++
 2 files changed, 153 insertions(+), 1 deletion(-)

diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 54aef8394fa25..0076d206c6a8a 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2913,6 +2913,77 @@ int change_huge_pud(struct mmu_gather *tlb, struct vm_area_struct *vma,
 #endif
 
 #ifdef CONFIG_USERFAULTFD
+#ifdef CONFIG_THP_SWAP
+/*
+ * Move a PMD-level swap entry from src_pmd to dst_pmd. Both PMD locks are
+ * acquired here; src_folio (if present) must already be locked. The deposited
+ * page table backing the source THP is moved across with the entry.
+ */
+static int move_swap_pmd(struct mm_struct *mm, struct vm_area_struct *dst_vma,
+			 unsigned long dst_addr, unsigned long src_addr,
+			 pmd_t *dst_pmd, pmd_t *src_pmd,
+			 pmd_t orig_dst_pmd, pmd_t orig_src_pmd,
+			 spinlock_t *dst_ptl, spinlock_t *src_ptl,
+			 struct folio *src_folio, swp_entry_t entry)
+{
+	pgtable_t src_pgtable;
+	pmd_t moved_pmd;
+
+	/*
+	 * The folio may have been freed and reused for a different swap entry
+	 * while it was unlocked. Re-verify the association.
+	 */
+	if (src_folio && unlikely(!folio_matches_swap_entry(src_folio, entry) ||
+				  folio_nr_pages(src_folio) != HPAGE_PMD_NR))
+		return -EAGAIN;
+
+	double_pt_lock(dst_ptl, src_ptl);
+
+	if (!pmd_same(*src_pmd, orig_src_pmd) ||
+	    !pmd_same(*dst_pmd, orig_dst_pmd)) {
+		double_pt_unlock(dst_ptl, src_ptl);
+		return -EAGAIN;
+	}
+
+	/*
+	 * If the folio is in the swap cache, re-anchor its anon rmap to the
+	 * destination VMA so a future swap-in fault at dst_addr finds it.
+	 * Otherwise, re-check the whole PMD swap range: a PMD swap entry is
+	 * only a compact encoding for 512 swap slots, and any per-slot cached
+	 * folio would need the PTE move path to update its rmap metadata.
+	 */
+	if (src_folio) {
+		folio_move_anon_rmap(src_folio, dst_vma);
+		src_folio->index = linear_page_index(dst_vma, dst_addr);
+	} else {
+		unsigned int type = swp_type(entry);
+		pgoff_t offset = swp_offset(entry);
+		int i;
+
+		for (i = 0; i < HPAGE_PMD_NR; i++) {
+			if (swap_cache_has_folio(swp_entry(type, offset + i))) {
+				double_pt_unlock(dst_ptl, src_ptl);
+				return -EAGAIN;
+			}
+		}
+	}
+
+	moved_pmd = pmdp_huge_get_and_clear(mm, src_addr, src_pmd);
+	if (pgtable_supports_soft_dirty())
+		moved_pmd = pmd_swp_mksoft_dirty(moved_pmd);
+	/* Re-arm RWP on the moved swap entry if dst_vma is RWP-registered. */
+	if (userfaultfd_rwp(dst_vma))
+		moved_pmd = pmd_swp_mkuffd(moved_pmd);
+	set_pmd_at(mm, dst_addr, dst_pmd, moved_pmd);
+
+	src_pgtable = pgtable_trans_huge_withdraw(mm, src_pmd);
+	pgtable_trans_huge_deposit(mm, dst_pmd, src_pgtable);
+
+	double_pt_unlock(dst_ptl, src_ptl);
+	return 0;
+}
+#endif /* CONFIG_THP_SWAP */
+
 /*
  * The PT lock for src_pmd and dst_vma/src_vma (for reading) are locked by
  * the caller, but it must return after releasing the page_table_lock.
@@ -2947,11 +3018,78 @@ int move_pages_huge_pmd(struct mm_struct *mm, pmd_t *dst_pmd, pmd_t *src_pmd, pm
 	}
 
 	if (!pmd_trans_huge(src_pmdval)) {
-		spin_unlock(src_ptl);
 		if (pmd_is_migration_entry(src_pmdval)) {
+			spin_unlock(src_ptl);
 			pmd_migration_entry_wait(mm, src_pmd);
 			return -EAGAIN;
 		}
+#ifdef CONFIG_THP_SWAP
+		if (pmd_is_swap_entry(src_pmdval)) {
+			swp_entry_t entry;
+			struct swap_info_struct *si;
+			enum swap_pmd_cache cache_state;
+
+			/*
+			 * UFFDIO_MOVE on anon mappings requires single-owner
+			 * semantics; refuse to move a shared swap entry.
+			 */
+			if (!pmd_swp_exclusive(src_pmdval)) {
+				spin_unlock(src_ptl);
+				return -EBUSY;
+			}
+
+			entry = softleaf_from_pmd(src_pmdval);
+			spin_unlock(src_ptl);
+
+			/* Pin the swap device against a racing swapoff. */
+			si = get_swap_device(entry);
+			if (unlikely(!si))
+				return -EAGAIN;
+
+			src_folio = NULL;
+			cache_state = swap_pmd_cache_lookup(entry, &src_folio);
+			if (cache_state == SWAP_PMD_CACHE_SPLIT) {
+				put_swap_device(si);
+				__split_huge_pmd(src_vma, src_pmd, src_addr, false);
+				return -EAGAIN;
+			}
+
+			mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0,
+						mm, src_addr,
+						src_addr + HPAGE_PMD_SIZE);
+			mmu_notifier_invalidate_range_start(&range);
+
+			if (src_folio) {
+				folio_lock(src_folio);
+				if (!folio_matches_swap_entry(src_folio, entry) ||
+				    folio_nr_pages(src_folio) != HPAGE_PMD_NR) {
+					err = -EAGAIN;
+					folio_unlock(src_folio);
+					folio_put(src_folio);
+					mmu_notifier_invalidate_range_end(&range);
+					put_swap_device(si);
+					__split_huge_pmd(src_vma, src_pmd,
+							 src_addr, false);
+					return err;
+				}
+			}
+
+			dst_ptl = pmd_lockptr(mm, dst_pmd);
+			err = move_swap_pmd(mm, dst_vma, dst_addr, src_addr,
+					    dst_pmd, src_pmd, dst_pmdval,
+					    src_pmdval, dst_ptl, src_ptl,
+					    src_folio, entry);
+
+			mmu_notifier_invalidate_range_end(&range);
+			if (src_folio) {
+				folio_unlock(src_folio);
+				folio_put(src_folio);
+			}
+			put_swap_device(si);
+			return err;
+		}
+#endif /* CONFIG_THP_SWAP */
+		spin_unlock(src_ptl);
 		return -ENOENT;
 	}
 
diff --git a/mm/userfaultfd.c b/mm/userfaultfd.c
index 23fb68fce000e..3692ffb326ffd 100644
--- a/mm/userfaultfd.c
+++ b/mm/userfaultfd.c
@@ -2106,6 +2106,20 @@ static ssize_t move_pages(struct userfaultfd_ctx *ctx, unsigned long dst_start,
 			break;
 		}
 
+		/*
+		 * A PMD swap entry at dst is a swapped-out THP, not a hole,
+		 * and unlike a PMD migration entry it will not resolve on its
+		 * own. Nothing below faults it back in: pte_alloc() skips a
+		 * !pmd_none PMD, pte_offset_map_rw_nolock() then fails on the
+		 * non-present PMD, and the -EAGAIN that produces would be
+		 * retried forever by the loop below. Be strict, exactly as for
+		 * a present THP.
+		 */
+		if (unlikely(pmd_is_swap_entry(dst_pmdval))) {
+			err = -EEXIST;
+			break;
+		}
+
 		ptl = pmd_trans_huge_lock(src_pmd, src_vma);
 		if (ptl) {
 			/* Check if we can move the pmd without splitting it. */
-- 
2.53.0-Meta



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

* [PATCH v6 10/12] mm: handle PMD swap entry faults on swap-in
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (8 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 09/12] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 11/12] mm: install PMD swap entries on swap-out Usama Arif
  2026-08-18 13:09 ` [PATCH v6 12/12] selftests/mm: add PMD swap entry tests Usama Arif
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Add do_huge_pmd_swap_page() and dispatch to it from __handle_mm_fault()
when vmf->orig_pmd encodes a swap entry.  The handler resolves the
entire 2 MB mapping in one shot, mirroring do_swap_page() (PTE path)
at PMD granularity:

  - Look up the folio in the swap cache; on a miss, allocate a
    PMD-order folio via swapin_sync(BIT(HPAGE_PMD_ORDER)) and read
    from swap.  This deliberately skips the existing order-0 swap
    readahead paths: the fault already asks for the whole PMD range,
    while order-0 readahead would populate per-page swap cache state
    and force the PMD swap entry to split.  If the range already has
    per-page swap-cache or zswap state, split and retry through PTEs.

  - After locking, re-validate that the folio still corresponds to our
    entry and is still PMD-sized.  Between the unlocked cache lookup
    and the lock, a racing swap-in on the same entry may have removed
    it from the cache via folio_free_swap(), or reclaim / memory_failure
    / deferred-split may have split the folio into smaller folios.

  - Refuse to map a folio that contains a hardware-poisoned subpage.
    On a hit, split the PMD swap entry so do_swap_page() can return
    VM_FAULT_HWPOISON per subpage, matching the PTE swap-in
    PageHWPoison check.  Disable large-folio PTE batching for such a
    folio so the fallback cannot map the poisoned subpage as part of
    a batch.

  - Restore soft_dirty and uffd_wp from the swap PMD.  For an RWP VMA,
    also restore PAGE_NONE so the next access reaches userfaultfd.
    Map writable only when the entry was exclusive, the VMA permits
    writes, and uffd-wp is not armed.  Drop the exclusive marker when
    the cached folio is under writeback to an SWP_STABLE_WRITES backend
    (zram) so the PMD is mapped read-only; a later write COWs
    into a fresh folio rather than corrupting the in-flight writeback.
    Mirrors do_swap_page().

  - When the resulting PMD is read-only but the fault was a write,
    update vmf->orig_pmd and call wp_huge_pmd() in the same handler
    to COW without waiting for a second fault, unless wp_huge_pmd()
    itself has to fall back to PTE level.  Leave an RWP-restored PMD
    for userfaultfd.  Mask VM_FAULT_FALLBACK from the return: a PMD-COW
    that splits to PTE-level is normal, but the bit is part of
    VM_FAULT_ERROR and arch fault handlers BUG() on it without
    SIGBUS/HWPOISON/SIGSEGV.

  - Free the swap slot via should_try_to_free_swap() (hoisted from
    mm/memory.c into mm/internal.h so PTE- and PMD-level swap-in share
    the heuristic).

When PMD-order resources are unavailable (folio allocation fails,
the cached folio was split, memcg charge fails, or swap-cache insertion
races) split the PMD swap entry into 512 PTE swap entries via
__split_huge_pmd() and return 0.  The fault retries and do_swap_page()
takes over per-PTE.  This avoids returning VM_FAULT_OOM for transient
PMD-order allocation failures.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 include/linux/huge_mm.h |  14 +++
 mm/huge_memory.c        | 242 ++++++++++++++++++++++++++++++++++++++++
 mm/internal.h           |  42 +++++++
 mm/memory.c             |  43 ++-----
 4 files changed, 306 insertions(+), 35 deletions(-)

diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index c745f7ad22987..e7107e0991ad7 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -552,6 +552,15 @@ vm_fault_t do_huge_pmd_uffd_rwp(struct vm_fault *vmf);
 
 vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf);
 
+#ifdef CONFIG_THP_SWAP
+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf);
+#else
+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	return 0;
+}
+#endif
+
 extern struct folio *huge_zero_folio;
 extern unsigned long huge_zero_pfn;
 
@@ -754,6 +763,11 @@ static inline vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf)
 	return 0;
 }
 
+static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	return 0;
+}
+
 static inline bool is_huge_zero_folio(const struct folio *folio)
 {
 	return false;
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 0076d206c6a8a..10d265c7e6331 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -42,6 +42,7 @@
 #include <linux/pgalloc_tag.h>
 #include <linux/pagewalk.h>
 #include <linux/cleanup.h>
+#include <linux/zswap.h>
 
 #include <asm/tlb.h>
 #include "internal.h"
@@ -2397,6 +2398,247 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *vmf)
 	return 0;
 }
 
+#ifdef CONFIG_THP_SWAP
+/**
+ * do_huge_pmd_swap_page() - Handle a fault on a PMD-level swap entry.
+ * @vmf: Fault context. vmf->orig_pmd contains the swap PMD.
+ *
+ * A PMD swap entry is a compact encoding for HPAGE_PMD_NR consecutive swap
+ * slots. If the swap cache still has one PMD-sized folio covering the range,
+ * map it directly at PMD level. If the range has been split into per-page
+ * cache state, or zswap may have per-page state for it, split the PMD swap
+ * entry and retry at PTE granularity.
+ *
+ * Return: VM_FAULT_* flags.
+ */
+vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
+{
+	struct vm_area_struct *vma = vmf->vma;
+	struct mm_struct *mm = vma->vm_mm;
+	struct folio *folio;
+	struct page *page;
+	struct swap_info_struct *si;
+	unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
+	softleaf_t entry;
+	swp_entry_t swp_entry;
+	pmd_t pmd;
+	vm_fault_t ret = 0;
+	bool exclusive, rwp_restore = false;
+	bool write = vmf->flags & FAULT_FLAG_WRITE;
+	rmap_t rmap_flags = RMAP_NONE;
+	enum swap_pmd_cache cache_state;
+
+	entry = softleaf_from_pmd(vmf->orig_pmd);
+	if (unlikely(!softleaf_is_swap(entry)))
+		return 0;
+
+	if (!thp_vma_allowable_order(vma, vma->vm_flags, TVA_PAGEFAULT,
+				     HPAGE_PMD_ORDER)) {
+		__split_huge_pmd(vma, vmf->pmd, haddr, false);
+		return 0;
+	}
+
+	swp_entry = entry;
+
+	/* Prevent swapoff from happening to us. */
+	si = get_swap_device(swp_entry);
+	if (unlikely(!si))
+		return 0;
+
+	cache_state = swap_pmd_cache_lookup(swp_entry, &folio);
+	if (cache_state == SWAP_PMD_CACHE_SPLIT)
+		goto split_fallback;
+	if (!folio) {
+		/*
+		 * PMD swap entries encode ordinary per-page swap slots. If any
+		 * slot is in zswap, split and let the PTE swap path load the
+		 * range per page. Otherwise the range is all on disk and can be
+		 * read back as one PMD-sized folio.
+		 */
+		if (zswap_is_present(swp_entry, HPAGE_PMD_NR))
+			goto split_fallback;
+
+		folio = swapin_sync(swp_entry, GFP_HIGHUSER_MOVABLE,
+				    BIT(HPAGE_PMD_ORDER), vmf, NULL, 0);
+		if (IS_ERR_OR_NULL(folio))
+			goto split_fallback;
+
+		/* Had to read from swap area: Major fault */
+		ret = VM_FAULT_MAJOR;
+		count_vm_event(PGMAJFAULT);
+		count_memcg_event_mm(mm, PGMAJFAULT);
+	}
+
+	ret |= folio_lock_or_retry(folio, vmf);
+	if (ret & VM_FAULT_RETRY)
+		goto out_release;
+
+	/* Verify the folio is still in swap cache and matches our entry */
+	if (unlikely(!folio_matches_swap_entry(folio, swp_entry)))
+		goto out_page;
+
+	/*
+	 * Folio should be PMD-sized; if not (e.g. split in swap cache),
+	 * split the PMD swap entry and retry at PTE level.
+	 */
+	if (folio_nr_pages(folio) != HPAGE_PMD_NR) {
+		folio_unlock(folio);
+		folio_put(folio);
+		goto split_fallback;
+	}
+
+	if (unlikely(!folio_test_uptodate(folio))) {
+		if (zswap_is_present(swp_entry, HPAGE_PMD_NR)) {
+			folio_unlock(folio);
+			folio_put(folio);
+			goto split_fallback;
+		}
+		ret = VM_FAULT_SIGBUS;
+		goto out_page;
+	}
+
+	/*
+	 * If any subpage is hardware-poisoned, split the PMD swap entry and
+	 * let the PTE swap-in path handle each page individually so
+	 * do_swap_page() can return VM_FAULT_HWPOISON for the poisoned
+	 * subpage rather than mapping the corrupted memory as one THP.
+	 */
+	if (unlikely(folio_contain_hwpoisoned_page(folio))) {
+		folio_unlock(folio);
+		folio_put(folio);
+		goto split_fallback;
+	}
+
+	page = folio_page(folio, 0);
+	arch_swap_restore(folio_swap(swp_entry, folio), folio);
+
+	folio_throttle_swaprate(folio, GFP_KERNEL);
+
+	/* Lock the PMD and verify it hasn't changed */
+	vmf->ptl = pmd_lock(mm, vmf->pmd);
+	if (unlikely(!pmd_same(vmf->orig_pmd, pmdp_get(vmf->pmd)))) {
+		spin_unlock(vmf->ptl);
+		goto out_page;
+	}
+
+	exclusive = pmd_swp_exclusive(vmf->orig_pmd);
+
+	/*
+	 * Some swap backends (e.g. zram) don't support concurrent page
+	 * modifications while under writeback. If we map exclusive on such
+	 * a backend while the folio is still under writeback, the writeback
+	 * may see partial modifications and corrupt the swap slot. Drop the
+	 * exclusive marker and only map R/O for that case; further GUP
+	 * references can't appear once the page is fully unmapped, so this
+	 * is safe.
+	 */
+	if (exclusive && folio_test_writeback(folio) &&
+	    data_race(si->flags & SWP_STABLE_WRITES))
+		exclusive = false;
+
+	/*
+	 * Set up the PMD mapping. Similar to do_swap_page() but at PMD level.
+	 */
+	add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR);
+	add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR);
+
+	pmd = folio_mk_pmd(folio, vma->vm_page_prot);
+	pmd = pmd_mkyoung(pmd);
+
+	if (pmd_swp_soft_dirty(vmf->orig_pmd))
+		pmd = pmd_mksoft_dirty(pmd);
+	if (pmd_swp_uffd(vmf->orig_pmd))
+		pmd = pmd_mkuffd(pmd);
+	if (pmd_swp_uffd(vmf->orig_pmd) && userfaultfd_rwp(vma)) {
+		pmd = pmd_modify(pmd, PAGE_NONE);
+		rwp_restore = true;
+	}
+
+	/*
+	 * Check exclusivity to determine if we can map writable.
+	 */
+	if (exclusive) {
+		if (!rwp_restore && (vma->vm_flags & VM_WRITE) &&
+		    !userfaultfd_huge_pmd_wp(vma, pmd) &&
+		    !pmd_needs_soft_dirty_wp(vma, pmd)) {
+			pmd = pmd_mkwrite(pmd, vma);
+			if (write)
+				pmd = pmd_mkdirty(pmd);
+		}
+		rmap_flags |= RMAP_EXCLUSIVE;
+	}
+
+	flush_icache_pages(vma, page, HPAGE_PMD_NR);
+
+	if (!folio_test_anon(folio))
+		folio_add_new_anon_rmap(folio, vma, haddr, rmap_flags);
+	else
+		folio_add_anon_rmap_pmd(folio, page, vma, haddr, rmap_flags);
+
+	folio_put_swap(folio, NULL);
+
+	set_pmd_at(mm, haddr, vmf->pmd, pmd);
+	update_mmu_cache_pmd(vma, haddr, vmf->pmd);
+
+	/* Update orig_pmd for any follow-up wp_huge_pmd() below. */
+	vmf->orig_pmd = pmd;
+
+	/*
+	 * Conditionally try to free up the swap cache. Do it after mapping,
+	 * so raced page faults will likely see the folio in swap cache and
+	 * wait on the folio lock.
+	 */
+	if (should_try_to_free_swap(si, folio, vma, exclusive, vmf->flags))
+		folio_free_swap(folio);
+
+	spin_unlock(vmf->ptl);
+
+	folio_unlock(folio);
+	put_swap_device(si);
+
+	/*
+	 * If the write fault wasn't satisfied above (folio is shared without
+	 * exclusivity), call wp_huge_pmd() to handle COW or
+	 * userfaultfd-wp without forcing a second fault.
+	 *
+	 * wp_huge_pmd() may return VM_FAULT_FALLBACK if it had to split the
+	 * PMD; that's a normal outcome, and the natural PTE-level refault will
+	 * complete the COW. Mask it so callers (and the arch fault handler)
+	 * don't see VM_FAULT_FALLBACK as a fatal VM_FAULT_ERROR.
+	 */
+	if (write && !pmd_write(pmd) && !rwp_restore) {
+		vm_fault_t wp_ret = wp_huge_pmd(vmf);
+
+		wp_ret &= ~VM_FAULT_FALLBACK;
+		ret |= wp_ret;
+		if (ret & VM_FAULT_ERROR)
+			ret &= VM_FAULT_ERROR;
+	}
+
+	return ret;
+
+out_page:
+	folio_unlock(folio);
+out_release:
+	folio_put(folio);
+	put_swap_device(si);
+	return ret;
+
+split_fallback:
+	/*
+	 * Only split if the PMD is still the swap entry we were called for.
+	 * All the reasons we get here (allocation failure, zswap state, a
+	 * split or poisoned cached folio) were observed without the PMD lock,
+	 * so a racing thread may already have swapped the range back in as a
+	 * THP -- splitting that would silently demote a perfectly good huge
+	 * mapping.
+	 */
+	if (pmd_same(vmf->orig_pmd, pmdp_get_lockless(vmf->pmd)))
+		__split_huge_pmd(vma, vmf->pmd, haddr, false);
+	put_swap_device(si);
+	return 0;
+}
+#endif /* CONFIG_THP_SWAP */
 static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd)
 {
 	pgtable_t pgtable;
diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c94..ae4e18e1b14ee 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -574,6 +574,48 @@ static inline vm_fault_t vmf_anon_prepare(struct vm_fault *vmf)
 }
 
 vm_fault_t do_swap_page(struct vm_fault *vmf);
+
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+vm_fault_t wp_huge_pmd(struct vm_fault *vmf);
+#else
+static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
+{
+	return VM_FAULT_FALLBACK;
+}
+#endif
+
+/*
+ * Check if we should call folio_free_swap to free the swap cache.
+ * folio_free_swap only frees the swap cache to release the slot if swap
+ * count is zero, so we don't need to check the swap count here.
+ */
+static inline bool should_try_to_free_swap(struct swap_info_struct *si,
+					   struct folio *folio,
+					   struct vm_area_struct *vma,
+					   bool exclusive,
+					   unsigned int fault_flags)
+{
+	if (!folio_test_swapcache(folio))
+		return false;
+	/*
+	 * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap
+	 * cache can help save some IO or memory overhead, but these devices
+	 * are fast, and meanwhile, swap cache pinning the slot deferring the
+	 * release of metadata or fragmentation is a more critical issue.
+	 */
+	if (data_race(si->flags & SWP_SYNCHRONOUS_IO))
+		return true;
+	if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) ||
+	    folio_test_mlocked(folio))
+		return true;
+
+	/*
+	 * Free the swapcache only if we are the exclusive user and
+	 * this is a write fault.
+	 */
+	return (fault_flags & FAULT_FLAG_WRITE) && exclusive;
+}
+
 void folio_rotate_reclaimable(struct folio *folio);
 bool __folio_end_writeback(struct folio *folio);
 void deactivate_file_folio(struct folio *folio);
diff --git a/mm/memory.c b/mm/memory.c
index 36ddca806be2f..6f2cf12d3a45f 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -4640,38 +4640,6 @@ static vm_fault_t remove_device_exclusive_entry(struct vm_fault *vmf)
 	return 0;
 }
 
-/*
- * Check if we should call folio_free_swap to free the swap cache.
- * folio_free_swap only frees the swap cache to release the slot if swap
- * count is zero, so we don't need to check the swap count here.
- */
-static inline bool should_try_to_free_swap(struct swap_info_struct *si,
-					   struct folio *folio,
-					   struct vm_area_struct *vma,
-					   bool exclusive,
-					   unsigned int fault_flags)
-{
-	if (!folio_test_swapcache(folio))
-		return false;
-	/*
-	 * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap
-	 * cache can help save some IO or memory overhead, but these devices
-	 * are fast, and meanwhile, swap cache pinning the slot deferring the
-	 * release of metadata or fragmentation is a more critical issue.
-	 */
-	if (data_race(si->flags & SWP_SYNCHRONOUS_IO))
-		return true;
-	if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) ||
-	    folio_test_mlocked(folio))
-		return true;
-
-	/*
-	 * Free the swapcache only if we are the exclusive user and
-	 * this is a write fault.
-	 */
-	return (fault_flags & FAULT_FLAG_WRITE) && exclusive;
-}
-
 static vm_fault_t pte_marker_clear(struct vm_fault *vmf)
 {
 	vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
@@ -5052,7 +5020,8 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
 	page_idx = 0;
 	address = vmf->address;
 	ptep = vmf->pte;
-	if (folio_test_large(folio) && folio_test_swapcache(folio)) {
+	if (folio_test_large(folio) && folio_test_swapcache(folio) &&
+	    !folio_contain_hwpoisoned_page(folio)) {
 		int nr = folio_nr_pages(folio);
 		unsigned long idx = folio_page_idx(folio, page);
 		unsigned long folio_start = address - idx * PAGE_SIZE;
@@ -6387,8 +6356,8 @@ static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)
 	return VM_FAULT_FALLBACK;
 }
 
-/* `inline' is required to avoid gcc 4.1.2 build error */
-static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
+#ifdef CONFIG_TRANSPARENT_HUGEPAGE
+vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
 {
 	struct vm_area_struct *vma = vmf->vma;
 	const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
@@ -6418,6 +6387,7 @@ static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf)
 
 	return VM_FAULT_FALLBACK;
 }
+#endif /* CONFIG_TRANSPARENT_HUGEPAGE */
 
 static vm_fault_t create_huge_pud(struct vm_fault *vmf)
 {
@@ -6681,6 +6651,9 @@ static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
 
 		if (pmd_is_migration_entry(vmf.orig_pmd))
 			pmd_migration_entry_wait(mm, vmf.pmd);
+		else if (IS_ENABLED(CONFIG_THP_SWAP) &&
+			 pmd_is_swap_entry(vmf.orig_pmd))
+			return do_huge_pmd_swap_page(&vmf);
 		return 0;
 	}
 	if (pmd_trans_huge(vmf.orig_pmd)) {
-- 
2.53.0-Meta



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

* [PATCH v6 11/12] mm: install PMD swap entries on swap-out
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (9 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 10/12] mm: handle PMD swap entry faults on swap-in Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  2026-08-18 13:09 ` [PATCH v6 12/12] selftests/mm: add PMD swap entry tests Usama Arif
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Reclaim today splits a PMD-mapped anonymous THP into 512 PTE swap
entries before unmap, losing the huge mapping across the swap
round-trip and forcing khugepaged to rebuild it later. The contiguous
swap range was already secured when the folio was added to the swap
cache (a non-contiguous allocation would have split the folio earlier),
so the PMD can be replaced by a single PMD-level swap entry instead.

This patch mirrors the existing PTE swap-out path at PMD granularity:
- shrink_folio_list() drops TTU_SPLIT_HUGE_PMD for PMD-mappable
  swapcache folios. zswap is handled by the PMD swap-in users: if any
  covered slot currently has a zswap entry, they split the PMD swap
  entry and fall back to the per-PTE path.
- try_to_unmap_one() now has a PMD branch that calls
  set_pmd_swap_entry() and adjusts MM_ANONPAGES / MM_SWAPENTS by
  HPAGE_PMD_NR before walk_done. TTU_SPLIT_HUGE_PMD remains the
  fallback.
- set_pmd_swap_entry() is the installer. Mirroring the PTE swap-out
  sequence at PMD granularity, it clears the present mapping (keeping
  the original for rollback), bumps the swap_map refcount for the
  folio's 512 slots, transfers the exclusive state in the swap entry,
  propagates the dirty bit to the folio so writeback is not lost,
  and installs a swap PMD that preserves the original
  soft-dirty / uffd-wp / exclusive bits. Any failing step rolls back
  the present mapping.

The swap entry value matches what 512 PTE swap entries would encode, so
swap_map refcounting is unchanged: each of the 512 slots carries a
count of 1, released individually on later split or together on swap-in.

Add thp_swpout_pmd to count each PMD mapping replaced by a PMD-level
swap entry. Unlike the folio-level thp_swpout counter, a fork-shared THP
can increment this counter once for each mapping; document that
distinction.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 Documentation/admin-guide/mm/transhuge.rst |  5 ++
 include/linux/huge_mm.h                    |  2 +
 include/linux/vm_event_item.h              |  1 +
 mm/huge_memory.c                           | 81 ++++++++++++++++++++++
 mm/rmap.c                                  | 19 +++++
 mm/vmscan.c                                |  9 ++-
 mm/vmstat.c                                |  1 +
 7 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/Documentation/admin-guide/mm/transhuge.rst b/Documentation/admin-guide/mm/transhuge.rst
index b187d618452f4..64d413d9fd83e 100644
--- a/Documentation/admin-guide/mm/transhuge.rst
+++ b/Documentation/admin-guide/mm/transhuge.rst
@@ -632,6 +632,11 @@ thp_swpout
 	is incremented every time a huge page is swapout in one
 	piece without splitting.
 
+thp_swpout_pmd
+	is incremented every time a PMD mapping is replaced by a PMD-level
+	swap entry. A fork-shared THP can increment this counter once for each
+	PMD mapping that is swapped out.
+
 thp_swpout_fallback
 	is incremented if a huge page has to be split before swapout.
 	Usually because failed to allocate some continuous swap space
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index e7107e0991ad7..41cf643a3f55f 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -554,6 +554,8 @@ vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf);
 
 #ifdef CONFIG_THP_SWAP
 vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf);
+int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,
+		       struct folio *folio);
 #else
 static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf)
 {
diff --git a/include/linux/vm_event_item.h b/include/linux/vm_event_item.h
index 2628ccda076a0..f8fd4e13698c3 100644
--- a/include/linux/vm_event_item.h
+++ b/include/linux/vm_event_item.h
@@ -108,6 +108,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT,
 		THP_ZERO_PAGE_ALLOC_FAILED,
 		THP_SWPOUT,
 		THP_SWPOUT_FALLBACK,
+		THP_SWPOUT_PMD,
 #endif
 #ifdef CONFIG_BALLOON
 		BALLOON_INFLATE,
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 10d265c7e6331..d2f7a22aae3ad 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -5640,3 +5640,84 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct folio *folio
 	trace_remove_migration_pmd(address, pmd_val(pmde));
 }
 #endif
+
+#ifdef CONFIG_THP_SWAP
+/**
+ * set_pmd_swap_entry() - Replace a PMD mapping with a PMD-level swap entry.
+ * @pvmw: Page vma mapped walk context, must have pvmw->pmd set and
+ *        pvmw->pte NULL (i.e. PMD-mapped).
+ * @folio: The folio being swapped out. Must be in the swap cache.
+ *
+ * This installs a PMD-level swap entry in place of a present PMD mapping,
+ * avoiding the need to split the PMD into PTE-level swap entries.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw,
+		       struct folio *folio)
+{
+	struct vm_area_struct *vma = pvmw->vma;
+	struct mm_struct *mm = vma->vm_mm;
+	unsigned long address = pvmw->address;
+	unsigned long haddr = address & HPAGE_PMD_MASK;
+	struct page *page = folio_page(folio, 0);
+	bool anon_exclusive;
+	pmd_t pmdval;
+	swp_entry_t entry;
+	pmd_t pmdswp;
+
+	if (WARN_ON_ONCE(!pvmw->pmd || pvmw->pte))
+		return -EINVAL;
+
+	VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);
+	VM_BUG_ON_FOLIO(!folio_test_anon(folio), folio);
+	VM_BUG_ON_FOLIO(folio_nr_pages(folio) != HPAGE_PMD_NR, folio);
+
+	if (unlikely(folio_test_swapbacked(folio) !=
+			folio_test_swapcache(folio))) {
+		WARN_ON_ONCE(1);
+		return -EBUSY;
+	}
+
+	flush_cache_range(vma, haddr, haddr + HPAGE_PMD_SIZE);
+
+	pmdval = pmdp_invalidate(vma, haddr, pvmw->pmd);
+
+	/* Update high watermark before we lower rss */
+	update_hiwater_rss(mm);
+
+	if (folio_dup_swap(folio, NULL) < 0) {
+		set_pmd_at(mm, haddr, pvmw->pmd, pmdval);
+		return -ENOMEM;
+	}
+
+	/* See folio_try_share_anon_rmap_pmd(): invalidate PMD first. */
+	anon_exclusive = PageAnonExclusive(page);
+	if (anon_exclusive && folio_try_share_anon_rmap_pmd(folio, page)) {
+		folio_put_swap(folio, NULL);
+		set_pmd_at(mm, haddr, pvmw->pmd, pmdval);
+		return -EBUSY;
+	}
+
+	mm_prepare_for_swap_entries(mm);
+
+	if (pmd_dirty(pmdval))
+		folio_mark_dirty(folio);
+
+	entry = folio->swap;
+	pmdswp = softleaf_to_pmd(entry);
+	if (pmd_soft_dirty(pmdval))
+		pmdswp = pmd_swp_mksoft_dirty(pmdswp);
+	if (pmd_uffd(pmdval))
+		pmdswp = pmd_swp_mkuffd(pmdswp);
+	if (anon_exclusive)
+		pmdswp = pmd_swp_mkexclusive(pmdswp);
+	set_pmd_at(mm, haddr, pvmw->pmd, pmdswp);
+
+	folio_remove_rmap_pmd(folio, page, vma);
+	folio_put(folio);
+
+	count_vm_event(THP_SWPOUT_PMD);
+	return 0;
+}
+#endif /* CONFIG_THP_SWAP */
diff --git a/mm/rmap.c b/mm/rmap.c
index d1819fd699380..4997724c2dd85 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -2282,6 +2282,25 @@ static bool try_to_unmap_one(struct folio *folio, struct vm_area_struct *vma,
 				goto walk_abort;
 			}
 
+#ifdef CONFIG_THP_SWAP
+			/*
+			 * If the folio is in the swap cache and we're not
+			 * asked to split, install a PMD-level swap entry.
+			 */
+			if (!(flags & TTU_SPLIT_HUGE_PMD) &&
+			    folio_test_anon(folio) &&
+			    folio_test_swapcache(folio)) {
+				if (set_pmd_swap_entry(&pvmw, folio))
+					goto walk_abort;
+
+				add_mm_counter(mm, MM_ANONPAGES,
+					       -HPAGE_PMD_NR);
+				add_mm_counter(mm, MM_SWAPENTS,
+					       HPAGE_PMD_NR);
+				goto walk_done;
+			}
+#endif
+
 			if (flags & TTU_SPLIT_HUGE_PMD) {
 				/*
 				 * We temporarily have to drop the PTL and
diff --git a/mm/vmscan.c b/mm/vmscan.c
index c1404a59523d6..94038e8cc64d0 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1329,7 +1329,14 @@ static unsigned int shrink_folio_list(struct list_head *folio_list,
 			enum ttu_flags flags = TTU_BATCH_FLUSH;
 			bool was_swapbacked = folio_test_swapbacked(folio);
 
-			if (folio_test_pmd_mappable(folio))
+			/*
+			 * With THP_SWAP, PMD-mappable folios already in the
+			 * swap cache can be unmapped with a PMD-level swap
+			 * entry, avoiding the cost of splitting the PMD.
+			 */
+			if (folio_test_pmd_mappable(folio) &&
+			    !(IS_ENABLED(CONFIG_THP_SWAP) &&
+			      folio_test_swapcache(folio)))
 				flags |= TTU_SPLIT_HUGE_PMD;
 			/*
 			 * Without TTU_SYNC, try_to_unmap will only begin to
diff --git a/mm/vmstat.c b/mm/vmstat.c
index cb57714539fb5..f40bca6aa45a0 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1435,6 +1435,7 @@ const char * const vmstat_text[] = {
 	[I(THP_ZERO_PAGE_ALLOC_FAILED)]		= "thp_zero_page_alloc_failed",
 	[I(THP_SWPOUT)]				= "thp_swpout",
 	[I(THP_SWPOUT_FALLBACK)]		= "thp_swpout_fallback",
+	[I(THP_SWPOUT_PMD)]			= "thp_swpout_pmd",
 #endif
 #ifdef CONFIG_BALLOON
 	[I(BALLOON_INFLATE)]			= "balloon_inflate",
-- 
2.53.0-Meta



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

* [PATCH v6 12/12] selftests/mm: add PMD swap entry tests
  2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
                   ` (10 preceding siblings ...)
  2026-08-18 13:09 ` [PATCH v6 11/12] mm: install PMD swap entries on swap-out Usama Arif
@ 2026-08-18 13:09 ` Usama Arif
  11 siblings, 0 replies; 20+ messages in thread
From: Usama Arif @ 2026-08-18 13:09 UTC (permalink / raw)
  To: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team,
	Usama Arif

Exercise the PMD swap entry paths.  Each test gets a fresh PMD-mapped
THP from fixture setup, fills it with a page-distinct pattern, swaps it
out with MADV_PAGEOUT, and verifies that thp_swpout_pmd increased.

The tests are:

  - basic: fault in a swapped PMD and verify its contents.
  - fork: verify parent and child can fault in the shared swap entry.
  - fork_cow: verify parent and child writes remain isolated.
  - write: fault in by writing one byte and preserve the rest of the THP.
  - rwp_swapin: verify userfaultfd RWP survives PMD-order swap-in.
  - munmap: unmap the full entry and check that VmSwap drops.
  - mprotect: change full-range protections without faulting the entry in.
  - split_mprotect: change half-range protections and verify the data.
  - split_munmap: unmap half, drop its accounting, and preserve the rest.
  - uffdio_move: move the entry and RWP state, then fault it in at dst.
  - mremap: force the entry to a new aligned address and verify the data.
  - pagemap: verify swapped bits and consecutive swap-slot offsets.
  - mincore: walk the entry without faulting it in.
  - madvise_free: release the slots, clear the entry, and verify zeroes.
  - madvise_willneed: prefetch the entry and verify subsequent swap-in.
  - swapoff: unuse the entry and preserve data and PMD/RWP state.

Fixture teardown owns the mappings and file descriptors and restores
swap after assertion failures.  PMD_SWAP_DEVICE remains optional for
swapoff.

Distinguish an environment that cannot allocate a PMD THP from a failure
to install a PMD swap entry, so the former skips while the latter fails.
Also check VmSwap accounting, pagemap slot offsets, swapped state after
non-faulting operations, and PMD restoration when zswap does not require
PTE fallback.

Register the test with run_vmtests.sh and the default kselftest runner.

Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
 tools/testing/selftests/mm/Makefile         |   2 +
 tools/testing/selftests/mm/ksft_pmd_swap.sh |   4 +
 tools/testing/selftests/mm/pmd_swap.c       | 742 ++++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh   |   4 +
 4 files changed, 752 insertions(+)
 create mode 100755 tools/testing/selftests/mm/ksft_pmd_swap.sh
 create mode 100644 tools/testing/selftests/mm/pmd_swap.c

diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 2d5366196e309..dafa3a482451d 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -104,6 +104,7 @@ TEST_GEN_FILES += guard-regions
 TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
+TEST_GEN_FILES += pmd_swap
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
@@ -165,6 +166,7 @@ TEST_PROGS += ksft_mremap.sh
 TEST_PROGS += ksft_pagemap.sh
 TEST_PROGS += ksft_pfnmap.sh
 TEST_PROGS += ksft_pkey.sh
+TEST_PROGS += ksft_pmd_swap.sh
 TEST_PROGS += ksft_process_madv.sh
 TEST_PROGS += ksft_process_mrelease.sh
 TEST_PROGS += ksft_rmap.sh
diff --git a/tools/testing/selftests/mm/ksft_pmd_swap.sh b/tools/testing/selftests/mm/ksft_pmd_swap.sh
new file mode 100755
index 0000000000000..0f070b4729a89
--- /dev/null
+++ b/tools/testing/selftests/mm/ksft_pmd_swap.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+# SPDX-License-Identifier: GPL-2.0
+
+./run_vmtests.sh -t pmd_swap
diff --git a/tools/testing/selftests/mm/pmd_swap.c b/tools/testing/selftests/mm/pmd_swap.c
new file mode 100644
index 0000000000000..30911ef6480f3
--- /dev/null
+++ b/tools/testing/selftests/mm/pmd_swap.c
@@ -0,0 +1,742 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Test PMD-level swap entries and their users. */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+#include <sys/random.h>
+#include <sys/swap.h>
+#include <sys/syscall.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <linux/userfaultfd.h>
+#include <time.h>
+
+#include "kselftest_harness.h"
+#include "vm_util.h"
+
+#define ZSWAP_ENABLED_PATH "/sys/module/zswap/parameters/enabled"
+
+/* pagemap: bits 0-54 hold the PFN, or type|offset for a swap entry. */
+#define PM_PFRAME_MASK		((1ULL << 55) - 1)
+/* Must match MAX_SWAPFILES_SHIFT in include/linux/swap.h. */
+#define MAX_SWAPFILES_SHIFT	5
+
+static bool check_swapped(int pagemap_fd, char *addr, unsigned long size)
+{
+	unsigned long off;
+
+	for (off = 0; off < size; off += getpagesize())
+		if (!pagemap_is_swapped(pagemap_fd, addr + off))
+			return false;
+	return true;
+}
+
+static bool zswap_enabled(void)
+{
+	char enabled = 0;
+	FILE *f;
+
+	f = fopen(ZSWAP_ENABLED_PATH, "r");
+	if (!f)
+		return false;
+
+	if (fscanf(f, " %c", &enabled) != 1)
+		enabled = 0;
+	fclose(f);
+
+	return enabled == 'Y' || enabled == 'y' || enabled == '1';
+}
+
+static bool swap_available(unsigned long required_bytes)
+{
+	unsigned long required_kb = (required_bytes + 1023) / 1024;
+	unsigned long size_kb, used_kb;
+	char line[256];
+	bool ret = false;
+	FILE *f;
+
+	f = fopen("/proc/swaps", "r");
+	if (!f)
+		return false;
+
+	/* Skip the header. */
+	if (!fgets(line, sizeof(line), f))
+		goto out;
+
+	while (fgets(line, sizeof(line), f)) {
+		if (sscanf(line, "%*s %*s %lu %lu", &size_kb, &used_kb) == 2 &&
+		    size_kb >= used_kb && size_kb - used_kb >= required_kb) {
+			ret = true;
+			break;
+		}
+	}
+
+out:
+	fclose(f);
+	return ret;
+}
+
+static unsigned long read_vm_event(const char *name)
+{
+	char line[256];
+	size_t name_len = strlen(name);
+	unsigned long val = 0;
+	FILE *f;
+
+	f = fopen("/proc/vmstat", "r");
+	if (!f)
+		return 0;
+	while (fgets(line, sizeof(line), f)) {
+		if (!strncmp(line, name, name_len) && line[name_len] == ' ') {
+			val = strtoul(line + name_len + 1, NULL, 10);
+			break;
+		}
+	}
+	fclose(f);
+	return val;
+}
+
+static unsigned int random_seed(void)
+{
+	unsigned int seed;
+
+	if (getrandom(&seed, sizeof(seed), 0) != sizeof(seed))
+		seed = (unsigned int)time(NULL);
+	return seed;
+}
+
+static unsigned char pattern_byte(unsigned int seed, unsigned long off)
+{
+	return (unsigned char)(seed + off + (off >> 8) + (off >> 16));
+}
+
+static void fill_pattern(char *buf, unsigned long size, unsigned int seed)
+{
+	unsigned long i;
+
+	for (i = 0; i < size; i++)
+		buf[i] = (char)pattern_byte(seed, i);
+}
+
+static bool verify_pattern_range(char *buf, unsigned long size,
+				 unsigned int seed, unsigned long offset)
+{
+	unsigned long i;
+
+	for (i = 0; i < size; i++)
+		if ((unsigned char)buf[i] != pattern_byte(seed, offset + i))
+			return false;
+	return true;
+}
+
+static bool verify_pattern(char *buf, unsigned long size, unsigned int seed)
+{
+	return verify_pattern_range(buf, size, seed, 0);
+}
+
+static bool verify_zero(char *buf, unsigned long size)
+{
+	unsigned long i;
+
+	for (i = 0; i < size; i++)
+		if (buf[i])
+			return false;
+	return true;
+}
+
+/*
+ * mmap an anonymous PMD-aligned region of pmd_size bytes. Over-allocates
+ * by one PMD and trims the unaligned head/tail so the returned address is
+ * PMD-aligned (required for whole-PMD UFFDIO_MOVE).
+ */
+static char *mmap_pmd_aligned(unsigned long pmd_size)
+{
+	unsigned long pad = pmd_size;
+	char *raw, *aligned;
+
+	raw = mmap(NULL, pmd_size + pad, PROT_READ | PROT_WRITE,
+		   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (raw == MAP_FAILED)
+		return MAP_FAILED;
+
+	aligned = (char *)(((uintptr_t)raw + pmd_size - 1) & ~(pmd_size - 1));
+	if (aligned != raw)
+		munmap(raw, aligned - raw);
+	if (aligned + pmd_size != raw + pmd_size + pad)
+		munmap(aligned + pmd_size,
+		       (raw + pmd_size + pad) - (aligned + pmd_size));
+	return aligned;
+}
+
+enum swap_thp_result {
+	SWAP_THP_OK,
+	SWAP_THP_UNAVAILABLE,
+	SWAP_THP_FAILED,
+};
+
+/* Per-process swapped size in bytes, from /proc/self/status VmSwap. */
+static unsigned long read_vmswap(void)
+{
+	char line[256];
+	unsigned long kb = 0;
+	FILE *f;
+
+	f = fopen("/proc/self/status", "r");
+	if (!f)
+		return 0;
+	while (fgets(line, sizeof(line), f)) {
+		if (!strncmp(line, "VmSwap:", 7)) {
+			kb = strtoul(line + 7, NULL, 10);
+			break;
+		}
+	}
+	fclose(f);
+	return kb * 1024;
+}
+
+static bool swap_out_pmd(char *mem, unsigned long pmd_size, int pagemap_fd)
+{
+	unsigned long before = read_vm_event("thp_swpout_pmd");
+	unsigned long after;
+
+	if (madvise(mem, pmd_size, MADV_PAGEOUT)) {
+		ksft_print_msg("MADV_PAGEOUT failed: %s\n", strerror(errno));
+		return false;
+	}
+	if (!check_swapped(pagemap_fd, mem, pmd_size)) {
+		ksft_print_msg("MADV_PAGEOUT did not swap the whole PMD range\n");
+		return false;
+	}
+
+	after = read_vm_event("thp_swpout_pmd");
+	ksft_print_msg("thp_swpout_pmd: %lu -> %lu\n", before, after);
+	return after > before;
+}
+
+static char *alloc_fill_swap_thp(unsigned long pmd_size, int pagemap_fd,
+				 unsigned int seed, enum swap_thp_result *res)
+{
+	char *mem;
+
+	*res = SWAP_THP_UNAVAILABLE;
+
+	mem = mmap_pmd_aligned(pmd_size);
+	if (mem == MAP_FAILED)
+		return MAP_FAILED;
+
+	if (madvise(mem, pmd_size, MADV_HUGEPAGE)) {
+		ksft_print_msg("MADV_HUGEPAGE failed: %s\n", strerror(errno));
+		munmap(mem, pmd_size);
+		return MAP_FAILED;
+	}
+	fill_pattern(mem, pmd_size, seed);
+
+	if (!check_huge_anon(mem, pmd_size, 1, pmd_size)) {
+		munmap(mem, pmd_size);
+		return MAP_FAILED;
+	}
+	*res = SWAP_THP_FAILED;
+
+	if (!swap_out_pmd(mem, pmd_size, pagemap_fd)) {
+		munmap(mem, pmd_size);
+		return MAP_FAILED;
+	}
+
+	*res = SWAP_THP_OK;
+	return mem;
+}
+
+struct rwp_access_args {
+	unsigned char *addr;
+	unsigned char expected;
+	bool write;
+	bool ok;
+};
+
+static void *rwp_access_thread(void *data)
+{
+	struct rwp_access_args *args = data;
+
+	if (args->write)
+		*args->addr = args->expected;
+	args->ok = *args->addr == args->expected;
+	return NULL;
+}
+
+static int register_rwp(char *addr, unsigned long size, bool protect)
+{
+	struct uffdio_register reg = {};
+	struct uffdio_rwprotect rwp = {};
+	struct uffdio_api api = {};
+	int uffd;
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0)
+		return -1;
+
+	api.api = UFFD_API;
+	api.features = UFFD_FEATURE_RWP;
+	if (ioctl(uffd, UFFDIO_API, &api) ||
+	    !(api.features & UFFD_FEATURE_RWP))
+		goto error;
+
+	reg.range.start = (unsigned long)addr;
+	reg.range.len = size;
+	reg.mode = UFFDIO_REGISTER_MODE_RWP;
+	if (ioctl(uffd, UFFDIO_REGISTER, &reg))
+		goto error;
+
+	if (!protect)
+		return uffd;
+
+	rwp.range.start = (unsigned long)addr;
+	rwp.range.len = size;
+	rwp.mode = UFFDIO_RWPROTECT_MODE_RWP;
+	if (!ioctl(uffd, UFFDIO_RWPROTECT, &rwp))
+		return uffd;
+
+error:
+	close(uffd);
+	return -1;
+}
+
+static bool expect_rwp_fault(int uffd, char *addr, unsigned long size,
+			     unsigned char expected, bool write)
+{
+	struct rwp_access_args args = {
+		.addr = (unsigned char *)addr,
+		.expected = expected,
+		.write = write,
+	};
+	struct uffdio_rwprotect rwp = {
+		.range = {
+			.start = (unsigned long)addr,
+			.len = size,
+		},
+	};
+	struct pollfd pollfd = {
+		.fd = uffd,
+		.events = POLLIN,
+	};
+	struct uffd_msg msg = {};
+	pthread_t thread;
+	bool saw_rwp = false;
+	int ret;
+
+	if (pthread_create(&thread, NULL, rwp_access_thread, &args))
+		return false;
+
+	ret = poll(&pollfd, 1, 5000);
+	if (ret == 1 && (pollfd.revents & POLLIN) &&
+	    read(uffd, &msg, sizeof(msg)) == (ssize_t)sizeof(msg)) {
+		saw_rwp = msg.event == UFFD_EVENT_PAGEFAULT &&
+			  (msg.arg.pagefault.flags & UFFD_PAGEFAULT_FLAG_RWP);
+	}
+
+	/* Resolve the access even on failure so the worker cannot remain blocked. */
+	ioctl(uffd, UFFDIO_RWPROTECT, &rwp);
+	if (pthread_join(thread, NULL))
+		return false;
+	return saw_rwp && args.ok;
+}
+
+FIXTURE(pmd_swap)
+{
+	unsigned long pmd_size;
+	unsigned long mem_len;
+	int pagemap_fd;
+	int uffd;
+	unsigned int seed;
+	bool zswap_enabled;
+	bool swap_disabled;
+	const char *swap_dev;
+	char *mem;
+	char *aux;
+};
+
+FIXTURE_SETUP(pmd_swap)
+{
+	enum swap_thp_result res;
+
+	self->pagemap_fd = -1;
+	self->uffd = -1;
+	self->mem = MAP_FAILED;
+	self->aux = MAP_FAILED;
+	self->mem_len = 0;
+	self->swap_disabled = false;
+	self->swap_dev = getenv("PMD_SWAP_DEVICE");
+	if (!strcmp(_metadata->name, "swapoff") && !self->swap_dev)
+		SKIP(return, "PMD_SWAP_DEVICE env var not set\n");
+
+	self->pmd_size = read_pmd_pagesize();
+	if (!self->pmd_size)
+		SKIP(return, "Cannot determine PMD size\n");
+
+	self->pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+	if (self->pagemap_fd < 0)
+		SKIP(return, "Cannot open /proc/self/pagemap\n");
+
+	if (!swap_available(self->pmd_size))
+		SKIP(return, "No active swap device has enough free space\n");
+
+	self->seed = random_seed();
+	self->zswap_enabled = zswap_enabled();
+	self->mem = alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd,
+					self->seed, &res);
+	if (self->mem == MAP_FAILED) {
+		ASSERT_NE(res, SWAP_THP_FAILED);
+		SKIP(return, "Could not create swapped THP\n");
+	}
+	self->mem_len = self->pmd_size;
+}
+
+FIXTURE_TEARDOWN(pmd_swap)
+{
+	int swap_err = 0;
+	int swap_ret = 0;
+
+	if (self->swap_disabled) {
+		swap_ret = swapon(self->swap_dev, 0);
+		swap_err = errno;
+	}
+	if (self->uffd >= 0)
+		close(self->uffd);
+	if (self->aux != MAP_FAILED)
+		munmap(self->aux, self->pmd_size);
+	if (self->mem != MAP_FAILED)
+		munmap(self->mem, self->mem_len);
+	if (self->pagemap_fd >= 0)
+		close(self->pagemap_fd);
+
+	EXPECT_EQ(swap_ret, 0) {
+		TH_LOG("swapon(%s) failed: %s", self->swap_dev,
+		       strerror(swap_err));
+	}
+}
+
+TEST_F(pmd_swap, basic)
+{
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+}
+
+TEST_F(pmd_swap, fork)
+{
+	pid_t pid;
+	int status;
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0)
+		_exit(verify_pattern(self->mem, self->pmd_size,
+				     self->seed) ? 0 : 1);
+
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_F(pmd_swap, fork_cow)
+{
+	unsigned int parent_seed = self->seed;
+	unsigned int child_seed = ~self->seed;
+	unsigned int new_seed = self->seed ^ 0xa5a5a5a5;
+	int release_child[2];
+	bool parent_ok;
+	char c = 0;
+	pid_t pid;
+	int status, ret;
+
+	ASSERT_EQ(pipe(release_child), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		close(release_child[1]);
+		if (read(release_child[0], &c, 1) != 1)
+			_exit(1);
+		if (!verify_pattern(self->mem, self->pmd_size, parent_seed))
+			_exit(2);
+		fill_pattern(self->mem, self->pmd_size, child_seed);
+		if (!verify_pattern(self->mem, self->pmd_size, child_seed))
+			_exit(3);
+		_exit(0);
+	}
+
+	close(release_child[0]);
+	fill_pattern(self->mem, self->pmd_size, new_seed);
+	parent_ok = verify_pattern(self->mem, self->pmd_size, new_seed);
+	ret = write(release_child[1], &c, 1);
+	close(release_child[1]);
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_EQ(ret, 1);
+	ASSERT_TRUE(parent_ok);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, new_seed));
+}
+
+TEST_F(pmd_swap, write)
+{
+	self->mem[0] = 0xbb;
+	ASSERT_EQ(self->mem[0], (char)0xbb);
+	ASSERT_TRUE(verify_pattern_range(self->mem + 1, self->pmd_size - 1,
+					 self->seed, 1));
+	if (!self->zswap_enabled)
+		ASSERT_TRUE(check_huge_anon(self->mem, self->pmd_size, 1,
+					    self->pmd_size));
+}
+
+TEST_F(pmd_swap, rwp_swapin)
+{
+	self->uffd = register_rwp(self->mem, self->pmd_size, true);
+	if (self->uffd < 0)
+		SKIP(return, "Userfaultfd RWP unsupported\n");
+
+	ASSERT_TRUE(expect_rwp_fault(self->uffd, self->mem, self->pmd_size,
+				     pattern_byte(self->seed, 0), false));
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+}
+
+TEST_F(pmd_swap, munmap)
+{
+	unsigned long swap_before, swap_after;
+	int ret;
+
+	swap_before = read_vmswap();
+	ASSERT_GE(swap_before, self->pmd_size);
+
+	ret = munmap(self->mem, self->pmd_size);
+	if (!ret) {
+		self->mem = MAP_FAILED;
+		self->mem_len = 0;
+	}
+	ASSERT_EQ(ret, 0);
+
+	swap_after = read_vmswap();
+	ASSERT_LE(swap_after, swap_before - self->pmd_size);
+}
+
+TEST_F(pmd_swap, mprotect)
+{
+	ASSERT_EQ(mprotect(self->mem, self->pmd_size, PROT_READ), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+	ASSERT_EQ(mprotect(self->mem, self->pmd_size,
+			   PROT_READ | PROT_WRITE), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+}
+
+TEST_F(pmd_swap, split_mprotect)
+{
+	unsigned long half = self->pmd_size / 2;
+
+	ASSERT_EQ(mprotect(self->mem, half, PROT_READ), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+	ASSERT_EQ(mprotect(self->mem, half, PROT_READ | PROT_WRITE), 0);
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+}
+
+TEST_F(pmd_swap, split_munmap)
+{
+	unsigned long half = self->pmd_size / 2;
+	unsigned long swap_before = read_vmswap();
+	unsigned long i;
+	char *base = self->mem;
+	int ret;
+
+	ASSERT_GE(swap_before, half);
+	ret = munmap(base, half);
+	if (!ret) {
+		self->mem = base + half;
+		self->mem_len = half;
+	}
+	ASSERT_EQ(ret, 0);
+	ASSERT_LE(read_vmswap(), swap_before - half);
+
+	for (i = 0; i < half; i += getpagesize())
+		ASSERT_TRUE(pagemap_is_swapped(self->pagemap_fd,
+					       self->mem + i));
+	ASSERT_TRUE(verify_pattern_range(self->mem, half, self->seed, half));
+}
+
+TEST_F(pmd_swap, uffdio_move)
+{
+	struct uffdio_register reg = {};
+	struct uffdio_move move = {};
+	struct uffdio_api api = {};
+	bool rwp;
+
+	self->aux = mmap_pmd_aligned(self->pmd_size);
+	if (self->aux == MAP_FAILED)
+		SKIP(return, "Could not mmap aligned dst\n");
+	ASSERT_EQ(madvise(self->aux, self->pmd_size, MADV_HUGEPAGE), 0);
+
+	self->uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (self->uffd < 0)
+		SKIP(return, "userfaultfd unavailable\n");
+
+	api.api = UFFD_API;
+	api.features = UFFD_FEATURE_MOVE | UFFD_FEATURE_RWP;
+	if (ioctl(self->uffd, UFFDIO_API, &api) ||
+	    !(api.features & UFFD_FEATURE_MOVE))
+		SKIP(return, "UFFD_FEATURE_MOVE unsupported\n");
+	rwp = api.features & UFFD_FEATURE_RWP;
+
+	reg.range.start = (unsigned long)self->aux;
+	reg.range.len = self->pmd_size;
+	reg.mode = UFFDIO_REGISTER_MODE_MISSING |
+		   (rwp ? UFFDIO_REGISTER_MODE_RWP : 0);
+	ASSERT_EQ(ioctl(self->uffd, UFFDIO_REGISTER, &reg), 0);
+
+	move.dst = (unsigned long)self->aux;
+	move.src = (unsigned long)self->mem;
+	move.len = self->pmd_size;
+	ASSERT_EQ(ioctl(self->uffd, UFFDIO_MOVE, &move), 0);
+	ASSERT_EQ(move.move, self->pmd_size);
+
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->aux,
+				  self->pmd_size));
+	if (rwp)
+		ASSERT_TRUE(expect_rwp_fault(self->uffd, self->aux,
+					     self->pmd_size,
+					     pattern_byte(self->seed, 0), false));
+	ASSERT_TRUE(verify_pattern(self->aux, self->pmd_size, self->seed));
+	if (!self->zswap_enabled)
+		ASSERT_TRUE(check_huge_anon(self->aux, self->pmd_size, 1,
+					    self->pmd_size));
+}
+
+TEST_F(pmd_swap, mremap)
+{
+	char *new_mem, *dst;
+
+	self->aux = mmap_pmd_aligned(self->pmd_size);
+	if (self->aux == MAP_FAILED)
+		SKIP(return, "Could not mmap aligned dst\n");
+	dst = self->aux;
+
+	new_mem = mremap(self->mem, self->pmd_size, self->pmd_size,
+			 MREMAP_MAYMOVE | MREMAP_FIXED, dst);
+	if (new_mem != MAP_FAILED) {
+		self->mem = new_mem;
+		self->aux = MAP_FAILED;
+	}
+	ASSERT_NE(new_mem, MAP_FAILED);
+	ASSERT_EQ(new_mem, dst);
+
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, new_mem, self->pmd_size));
+	ASSERT_TRUE(verify_pattern(new_mem, self->pmd_size, self->seed));
+}
+
+TEST_F(pmd_swap, pagemap)
+{
+	uint64_t entry, first = 0;
+	unsigned long off;
+
+	for (off = 0; off < self->pmd_size; off += getpagesize()) {
+		entry = pagemap_get_entry(self->pagemap_fd, self->mem + off);
+		ASSERT_TRUE(entry & (1ULL << 62));
+		ASSERT_FALSE(entry & (1ULL << 63));
+
+		if (entry & PM_PFRAME_MASK) {
+			uint64_t idx = off / getpagesize();
+
+			if (!off)
+				first = entry & PM_PFRAME_MASK;
+			ASSERT_EQ(entry & PM_PFRAME_MASK,
+				  first + (idx << MAX_SWAPFILES_SHIFT));
+		}
+	}
+}
+
+TEST_F(pmd_swap, mincore)
+{
+	unsigned long pages = self->pmd_size / getpagesize();
+	unsigned char vec[pages];
+
+	ASSERT_EQ(mincore(self->mem, self->pmd_size, vec), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+}
+
+TEST_F(pmd_swap, madvise_free)
+{
+	unsigned long swap_before = read_vmswap();
+	unsigned long i;
+
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+	ASSERT_GE(swap_before, self->pmd_size);
+	ASSERT_EQ(madvise(self->mem, self->pmd_size, MADV_FREE), 0);
+	for (i = 0; i < self->pmd_size; i += getpagesize())
+		ASSERT_FALSE(pagemap_is_swapped(self->pagemap_fd,
+						self->mem + i));
+	ASSERT_LE(read_vmswap(), swap_before - self->pmd_size);
+	ASSERT_TRUE(verify_zero(self->mem, self->pmd_size));
+}
+
+TEST_F(pmd_swap, madvise_willneed)
+{
+	ASSERT_EQ(madvise(self->mem, self->pmd_size, MADV_WILLNEED), 0);
+	ASSERT_TRUE(check_swapped(self->pagemap_fd, self->mem,
+				  self->pmd_size));
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+	if (!self->zswap_enabled)
+		ASSERT_TRUE(check_huge_anon(self->mem, self->pmd_size, 1,
+					    self->pmd_size));
+}
+
+TEST_F(pmd_swap, swapoff)
+{
+	int ret, err;
+
+	self->uffd = register_rwp(self->mem, self->pmd_size, true);
+
+	ret = swapoff(self->swap_dev);
+	err = errno;
+	if (!ret)
+		self->swap_disabled = true;
+	ASSERT_EQ(ret, 0) {
+		TH_LOG("swapoff(%s) failed: %s", self->swap_dev, strerror(err));
+	}
+
+	/*
+	 * Check residency before touching the memory. If we read
+	 * first, a bug that left a PMD swap entry in place after swapoff
+	 * would silently trigger do_huge_pmd_swap_page() and reinstall a
+	 * PMD mapping, masking the regression.
+	 */
+	if (!self->zswap_enabled)
+		ASSERT_TRUE(check_huge_anon(self->mem, self->pmd_size, 1,
+					    self->pmd_size));
+	if (self->uffd >= 0)
+		ASSERT_TRUE(expect_rwp_fault(self->uffd, self->mem,
+					     self->pmd_size,
+					     pattern_byte(self->seed, 0), false));
+	ASSERT_TRUE(verify_pattern(self->mem, self->pmd_size, self->seed));
+
+	ret = swapon(self->swap_dev, 0);
+	err = errno;
+	if (!ret)
+		self->swap_disabled = false;
+	ASSERT_EQ(ret, 0) {
+		TH_LOG("swapon(%s) failed: %s", self->swap_dev, strerror(err));
+	}
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index d09f9f6a384ee..ff53ff28c0042 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -69,6 +69,8 @@ separated by spaces:
 	test pagemap_scan IOCTL
 - pfnmap
 	tests for VM_PFNMAP handling
+- pmd_swap
+	tests for PMD-level swap entries
 - process_madv
 	test for process_madv
 - cow
@@ -399,6 +401,8 @@ CATEGORY="pagemap" run_test ./pagemap_ioctl
 
 CATEGORY="pfnmap" run_test ./pfnmap
 
+CATEGORY="pmd_swap" run_test ./pmd_swap
+
 # COW tests
 CATEGORY="cow" run_test ./cow
 
-- 
2.53.0-Meta



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

* Re: [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio()
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
@ 2026-08-18 14:24   ` David Hildenbrand (Arm)
  2026-08-18 18:36   ` Lorenzo Stoakes (ARM)
  2026-08-18 18:38   ` Zi Yan
  2 siblings, 0 replies; 20+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-18 14:24 UTC (permalink / raw)
  To: Usama Arif, Andrew Morton, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On 8/18/26 15:09, Usama Arif wrote:
> pmd_to_softleaf_folio() converts the softleaf entry encoded by a PMD
> to a folio. Rename it to pmd_softleaf_to_folio() to make the conversion
> direction explicit and align it with softleaf_to_folio().
> 
> No functional change.
> 
> Suggested-by: Dev Jain <dev.jain@arm.com>
> Signed-off-by: Usama Arif <usama.arif@linux.dev>
> ---

Acked-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David


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

* Re: [PATCH v6 02/12] mm: add PMD swap entry detection support
  2026-08-18 13:09 ` [PATCH v6 02/12] mm: add PMD swap entry detection support Usama Arif
@ 2026-08-18 14:40   ` David Hildenbrand (Arm)
  2026-08-18 18:42     ` Lorenzo Stoakes (ARM)
  0 siblings, 1 reply; 20+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-18 14:40 UTC (permalink / raw)
  To: Usama Arif, Andrew Morton, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On 8/18/26 15:09, Usama Arif wrote:
> Currently when a PMD-mapped THP is swapped out, the PMD is always
> split into HPAGE_PMD_NR PTE-level swap entries. To preserve huge
> page information across swap cycles, later patches will install a
> single PMD-level swap entry instead. Add the infrastructure to detect
> those entries.
> 
> Teach the softleaf layer to recognise PMD swap entries:
> pmd_is_swap_entry() detects them and softleaf_is_valid_pmd_entry()
> accepts them as a valid non-present type. Because swap entries do not
> encode a PFN, make pmd_softleaf_to_folio() warn and return NULL for them
> instead of passing the swap offset to softleaf_to_folio(). Clear the
> exclusive overlay bit in softleaf_from_pmd() before decoding, matching
> how soft_dirty and uffd bits are already stripped.
> 
> Add pmd_swp_mkexclusive(), pmd_swp_exclusive(), and
> pmd_swp_clear_exclusive() helpers to each architecture that supports
> PMD softleaf entries (x86, arm64, s390, riscv, loongarch, powerpc),
> mirroring the existing PTE swap exclusive helpers in each arch's
> pgtable.h.

Ah, for migration entries we still use a dedicated migratetype. I actually have
on my todo list to move to PTE bits as well.

(likely the _swp_ part should then be renamed to indicate that this is for
softdirty entries, not just swap entries)

> Provide generic no-op PMD swap exclusive fallbacks for
> architectures without PMD softleaf support, matching the generic PMD
> swap soft-dirty fallbacks.

No softleaf implies to migration and no swap, so this would work.

You should extend mm/debug_vm_pgtable.c in a separate commit to test what
pte_swap_exclusive_tests() tests for PMDs.

> 
> Signed-off-by: Usama Arif <usama.arif@linux.dev>
> ---
>  arch/arm64/include/asm/pgtable.h             |  6 +++++
>  arch/loongarch/include/asm/pgtable.h         | 19 ++++++++++++++
>  arch/powerpc/include/asm/book3s/64/pgtable.h | 17 +++++++++++++
>  arch/riscv/include/asm/pgtable.h             | 15 +++++++++++
>  arch/s390/include/asm/pgtable.h              | 17 +++++++++++++
>  arch/x86/include/asm/pgtable.h               | 17 +++++++++++++

I'm sorry for asking you to compete with Kiryll's series by creating one patch
for each architecture that directly jumps at arch maintainers :)

[...]

>  #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY
>  static inline pte_t pte_swp_mksoft_dirty(pte_t pte)
>  {
> diff --git a/include/linux/leafops.h b/include/linux/leafops.h
> index 7c13c58a5e218..4a6c52974b305 100644
> --- a/include/linux/leafops.h
> +++ b/include/linux/leafops.h
> @@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd)
>  		pmd = pmd_swp_clear_soft_dirty(pmd);
>  	if (pmd_swp_uffd(pmd))
>  		pmd = pmd_swp_clear_uffd(pmd);
> +	if (pmd_swp_exclusive(pmd))
> +		pmd = pmd_swp_clear_exclusive(pmd);

Can't we just unconditionally clear these flags?

pmd = pmd_swp_clear_uffd(pmd);
pmd = pmd_swp_clear_exclusive(pmd);

Avoids these rather unnecessary conditionals unless I am missing something.


>  	arch_entry = __pmd_to_swp_entry(pmd);
>  
>  	/* Temporary until swp_entry_t eliminated. */
> @@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd)
>   */
>  static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry)
>  {
> -	/* Only device private, migration entries valid for PMD. */
> +	/* Device private, migration, and swap entries valid for PMD. */

Can we just drop that comment? I mean, it's as clear as it gets in the code
immediately below :)

>  	return softleaf_is_device_private(entry) ||
> -		softleaf_is_migration(entry);
> +		softleaf_is_migration(entry) ||
> +		softleaf_is_swap(entry);
> +}
> +

-- 
Cheers,

David


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

* Re: [PATCH v6 03/12] mm: add PMD swap entry splitting support
  2026-08-18 13:09 ` [PATCH v6 03/12] mm: add PMD swap entry splitting support Usama Arif
@ 2026-08-18 17:53   ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 20+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-18 17:53 UTC (permalink / raw)
  To: Usama Arif, Andrew Morton, chrisl, kasong, ljs, ziy, linux-mm
  Cc: ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On 8/18/26 15:09, Usama Arif wrote:
> Add a swap branch in __split_huge_pmd_locked() that splits a PMD swap
> entry into 512 PTE swap entries. No folio reference is needed because
> swap entries point to swap slots rather than pages. Each PTE inherits
> the correct sub-slot offset and preserves soft_dirty, uffd_wp, and
> exclusive flags.
> 
> The folio_remove_rmap_pmd() gate at the end must inspect old_pmd
> rather than *pmd: for a present THP split, *pmd has already been
> cleared by pmdp_invalidate(), and that invalidated bit pattern can
> decode as a plausible swap entry.
> 
> This branch is reached from the explicit __split_huge_pmd() callers
> that hit a non-present PMD: partial-range mprotect / munmap, the
> wp_huge_pmd() PMD-COW fallback, and the swap-in / swapoff fallbacks
> added in later patches when the cached folio is no longer PMD-sized.
> page_vma_mapped_walk() does not iterate PMD swap entries, so
> try_to_unmap_one() and try_to_migrate_one() do not reach this branch
> and freeze=true cannot occur in this branch today.  page and folio
> are therefore left uninitialized in the swap branch; a
> VM_WARN_ON_ONCE(freeze) catches any future caller that breaks this
> invariant before the freeze path dereferences page_to_pfn(page + i)
> or put_page(page).
> 
> Signed-off-by: Usama Arif <usama.arif@linux.dev>
> ---
>  mm/huge_memory.c | 29 ++++++++++++++++++++++++++++-
>  1 file changed, 28 insertions(+), 1 deletion(-)
> 
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index 1b6b0aa2baa3b..a473e85d30f51 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -3252,6 +3252,14 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
>  			folio_add_anon_rmap_ptes(folio, page, HPAGE_PMD_NR,
>  						 vma, haddr, rmap_flags);
>  		}
> +	} else if (pmd_is_swap_entry(*pmd)) {
> +		VM_WARN_ON_ONCE(freeze);
> +		/* Swap entries have no page for the migration freeze path. */
> +		freeze = false;

It's odd to VM_WARN_ON_ONCE() and then set freeze=false;

I'd just add the comment above the VM_WARN_ON_ONCE() and drop the =false.

freeze=true really only applies during page migration, where swap entries don't
apply.

I think it's time to clean that up ... that is

a) Expose a helper called split_pmd_to_migration_entries() that is only used by
code that installs migration entries.

b) Hide that "freeze" flag from all other file-external functions

c) Rename the boolean to "use_migration_entries"

Then it's rather clear in this code that this should never happen.



> +		old_pmd = *pmd;
> +		soft_dirty = pmd_swp_soft_dirty(old_pmd);
> +		uffd_wp = pmd_swp_uffd(old_pmd);
> +		anon_exclusive = pmd_swp_exclusive(old_pmd);
>  	} else {
>  		/*
>  		 * Up to this point the pmd is present and huge and userland has
> @@ -3388,6 +3396,25 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
>  			VM_WARN_ON(!pte_none(ptep_get(pte + i)));
>  			set_pte_at(mm, addr, pte + i, entry);
>  		}
> +	} else if (pmd_is_swap_entry(old_pmd)) {
> +		softleaf_t sl_entry = softleaf_from_pmd(old_pmd);

No existing code uses "sl_entry". Maybe just call it "pmd_swp_entry"/"swp_entry"
and below "pte_swp_entry".



Apart from that nothing jumped at me :)

-- 
Cheers,

David


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

* Re: [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin
  2026-08-18 13:09 ` [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin Usama Arif
@ 2026-08-18 18:28   ` Yosry Ahmed
  0 siblings, 0 replies; 20+ messages in thread
From: Yosry Ahmed @ 2026-08-18 18:28 UTC (permalink / raw)
  To: Usama Arif
  Cc: Andrew Morton, david, chrisl, kasong, ljs, ziy, linux-mm,
	ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, kernel-team,
	Alexandre Ghiti

On Tue, Aug 18, 2026 at 06:09:46AM -0700, Usama Arif wrote:
> From: Alexandre Ghiti <alexghiti@fb.com>
> 
> A large folio reaches zswap_load() only when the caller expects
> the whole range to be on disk. Zswap still stores large folios as
> independent order-0 entries, so reconstructing a large folio from
> zswap entries would risk returning partially initialized data.
> 
> Teach zswap_load() to scan the covered range. If no slot is in zswap,
> return -ENOENT so swap_read_folio() reads the backing device. If any
> slot is still in zswap, fail the large-folio read so the caller can
> fall back to per-page swapin.
> 
> Return -EIO rather than -EINVAL for that conflict. Large-folio loads
> are now valid requests; the error means zswap cannot safely satisfy
> the request from partial per-page compressed state, not that the
> request is unsupported. Existing callers only distinguish -ENOENT,
> so this is a semantic clarification rather than a behavioral change.
> 
> Add zswap_is_present() so PMD swap-entry consumers can make the same
> range decision before attempting PMD-order swapin. Also use it from
> __swap_cache_add_check() for multi-page insertions while holding the
> swap cluster lock. That check runs before folio allocation and again
> immediately before swap-cache insertion, closing the race with zswap
> writeback and rejecting mixed zswap/disk backing with -EBUSY.
> 
> Signed-off-by: Alexandre Ghiti <alexghiti@fb.com>
> Signed-off-by: Usama Arif <usama.arif@linux.dev>
> ---
>  include/linux/zswap.h |  6 ++++++
>  mm/swap_state.c       | 10 ++++++++++
>  mm/zswap.c            | 46 +++++++++++++++++++++++++++++++------------
>  3 files changed, 49 insertions(+), 13 deletions(-)
> 
> diff --git a/include/linux/zswap.h b/include/linux/zswap.h
> index 30c193a1207e1..cd9efcf9dec94 100644
> --- a/include/linux/zswap.h
> +++ b/include/linux/zswap.h
> @@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec);
>  void zswap_folio_swapin(struct folio *folio);
>  bool zswap_is_enabled(void);
>  bool zswap_never_enabled(void);
> +bool zswap_is_present(swp_entry_t entry, unsigned int nr);
>  #else
>  
>  struct zswap_lruvec_state {};
> @@ -69,6 +70,11 @@ static inline bool zswap_never_enabled(void)
>  	return true;
>  }
>  
> +static inline bool zswap_is_present(swp_entry_t entry, unsigned int nr)
> +{
> +	return false;
> +}
> +
>  #endif
>  
>  #endif /* _LINUX_ZSWAP_H */
> diff --git a/mm/swap_state.c b/mm/swap_state.c
> index b76eb3d876fd7..15e200d6966b9 100644
> --- a/mm/swap_state.c
> +++ b/mm/swap_state.c
> @@ -12,6 +12,7 @@
>  #include <linux/kernel_stat.h>
>  #include <linux/mempolicy.h>
>  #include <linux/swap.h>
> +#include <linux/zswap.h>
>  #include <linux/leafops.h>
>  #include <linux/init.h>
>  #include <linux/pagemap.h>
> @@ -191,6 +192,15 @@ static int __swap_cache_add_check(struct swap_cluster_info *ci,
>  	if (nr == 1)
>  		return 0;
>  
> +	/*
> +	 * The cluster lock serializes swap-cache insertion with zswap
> +	 * writeback. Reject mixed zswap/disk backing before allocating a
> +	 * large folio and recheck it before adding the folio to swap cache.
> +	 */
> +	if (zswap_is_present(swp_entry(swp_type(targ_entry),
> +				       round_down(swp_offset(targ_entry), nr)), nr))


Sorry I didn't catch it in the discussion in v5, but I think it's
actually clearer to do have this check in __swap_cache_alloc() as you
initially suggested, but not necessarily under the lock.

I don't think the cluster lock is relevant per se, but rather the actual
swap cache allocation. Once you allocate the folio in the swapcache,
you cannot race with zswap store or writeback. I think the comment here
is a bit misleading in that regard. Especially that it mentions
writeback, but I think the real risk is racing with zswap store?

The check here is performed twice, once before allocating the folio and
once after. I think the one before allocating the folio is not really
useful, as we don't actually allocate the swapcache entry so nothing
actually prevents a zswap store/writeback from happening right after
releasing the lock.

We already have a failure path in __swap_cache_alloc() after allocating
the folio and dropping the lock. Can we use the same path to check if
zswap is present?

Maybe something along these lines (completely untested):

diff --git a/mm/swap_state.c b/mm/swap_state.c
index 8ccd03c39a407..ce00a2311c5fd 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -415,6 +415,7 @@ static struct folio *__swap_cache_alloc(struct swap_cluster_info *ci,
        swp_entry_t entry;
        struct folio *folio;
        void *shadow = NULL;
+       bool large_in_zswap;
        unsigned short memcg_id;
        unsigned long address, nr_pages = 1UL << order;
        struct vm_area_struct *vma = vmf ? vmf->vma : NULL;
@@ -459,7 +460,16 @@ static struct folio *__swap_cache_alloc(struct swap_cluster_info *ci,
        __swap_cache_do_add_folio(ci, folio, entry);
        spin_unlock(&ci->lock);

-       if (mem_cgroup_swapin_charge_folio(folio, memcg_id,
+       /*
+        * Check if any part of the folio is in zswap after stabilizing the swap
+        * cache entry to avoid races with zswap store/writeback. Reject
+        * high-order entries that are partially or fully in zswap, as it is not
+        * supported.
+        */
+       large_in_zswap = order && zswap_is_present(entry, nr_pages);
+
+       if (large_in_zswap ||
+           mem_cgroup_swapin_charge_folio(folio, memcg_id,
                                           vmf ? vmf->vma->vm_mm : NULL, gfp)) {
                spin_lock(&ci->lock);
                __swap_cache_do_del_folio(ci, folio, entry, shadow);


> +		return -EBUSY;
> +
>  	is_zero = __swap_table_test_zero(ci, ci_off);
>  	ci_off = round_down(ci_off, nr);
>  	ci_end = ci_off + nr;
> diff --git a/mm/zswap.c b/mm/zswap.c
> index 37f34e406c8e3..32671dc2bf84d 100644
> --- a/mm/zswap.c
> +++ b/mm/zswap.c
> @@ -1571,6 +1571,23 @@ bool zswap_store(struct folio *folio)
>  	return ret;
>  }
>  
> +/**
> + * zswap_is_present() - is any slot in [entry, entry + nr) in zswap?
> + * @entry: base swap entry of the range
> + * @nr: number of contiguous slots to check (pass 1 for a single-slot query)
> + */
> +bool zswap_is_present(swp_entry_t entry, unsigned int nr)
> +{
> +	pgoff_t offset = swp_offset(entry);
> +	struct xarray *tree = swap_zswap_tree(entry);
> +	unsigned long index = offset;
> +
> +	if (!nr || zswap_never_enabled())
> +		return false;
> +
> +	return xa_find(tree, &index, offset + nr - 1, XA_PRESENT);
> +}
> +
>  /**
>   * zswap_load() - load a folio from zswap
>   * @folio: folio to load
> @@ -1578,13 +1595,9 @@ bool zswap_store(struct folio *folio)
>   * Return: 0 on success, with the folio unlocked and marked up-to-date, or one
>   * of the following error codes:
>   *
> - *  -EIO: if the swapped out content was in zswap, but could not be loaded
> - *  into the page due to a decompression failure. The folio is unlocked, but
> - *  NOT marked up-to-date, so that an IO error is emitted (e.g. do_swap_page()
> - *  will SIGBUS).
> - *
> - *  -EINVAL: if the swapped out content was in zswap, but the page belongs
> - *  to a large folio, which is not supported by zswap. The folio is unlocked,
> + *  -EIO: if the swapped out content was in zswap but could not be handed
> + *  back, either because decompression failed or because a slot in a
> + *  large-folio range is unexpectedly still in zswap. The folio is unlocked,
>   *  but NOT marked up-to-date, so that an IO error is emitted (e.g.
>   *  do_swap_page() will SIGBUS).
>   *
> @@ -1605,13 +1618,20 @@ int zswap_load(struct folio *folio)
>  		return -ENOENT;
>  
>  	/*
> -	 * Large folios should not be swapped in while zswap is being used, as
> -	 * they are not properly handled. Zswap does not properly load large
> -	 * folios, and a large folio may only be partially in zswap.
> +	 * A large folio reaches zswap_load() only when its whole range is
> +	 * expected to be on disk: PMD swap-entry consumers split before
> +	 * calling into PMD-order swapin whenever any slot is still in zswap.
> +	 * Confirm the range is entirely absent from zswap and return -ENOENT
> +	 * so the caller reads it from disk; if a slot is unexpectedly still in
> +	 * zswap, fail the read rather than return partially-initialized data.
>  	 */
> -	if (WARN_ON_ONCE(folio_test_large(folio))) {
> -		folio_unlock(folio);
> -		return -EINVAL;
> +	if (folio_test_large(folio)) {
> +		if (WARN_ON_ONCE(zswap_is_present(swp,
> +						  folio_nr_pages(folio)))) {
> +			folio_unlock(folio);
> +			return -EIO;
> +		}
> +		return -ENOENT;
>  	}
>  
>  	entry = xa_load(tree, offset);
> -- 
> 2.53.0-Meta
> 


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

* Re: [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio()
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
  2026-08-18 14:24   ` David Hildenbrand (Arm)
@ 2026-08-18 18:36   ` Lorenzo Stoakes (ARM)
  2026-08-18 18:38   ` Zi Yan
  2 siblings, 0 replies; 20+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-18 18:36 UTC (permalink / raw)
  To: Usama Arif
  Cc: Andrew Morton, david, chrisl, kasong, ziy, linux-mm, ying.huang,
	Baoquan He, willy, youngjun.park, hannes, riel, shakeel.butt,
	alex, kas, baohua, dev.jain, baolin.wang, Nico Pache,
	Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
	linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On Tue, Aug 18, 2026 at 06:09:42AM -0700, Usama Arif wrote:
> pmd_to_softleaf_folio() converts the softleaf entry encoded by a PMD
> to a folio. Rename it to pmd_softleaf_to_folio() to make the conversion
> direction explicit and align it with softleaf_to_folio().
>
> No functional change.
>
> Suggested-by: Dev Jain <dev.jain@arm.com>
> Signed-off-by: Usama Arif <usama.arif@linux.dev>

Haha OK fair enough that's a valid correction!

Reviewed-by: Lorenzo Stoakes (ARM) <ljs@kernel.org>

> ---
>  include/linux/leafops.h | 4 ++--
>  mm/huge_memory.c        | 2 +-
>  2 files changed, 3 insertions(+), 3 deletions(-)
>
> diff --git a/include/linux/leafops.h b/include/linux/leafops.h
> index 4c1476ae32343..7c13c58a5e218 100644
> --- a/include/linux/leafops.h
> +++ b/include/linux/leafops.h
> @@ -657,7 +657,7 @@ static inline bool pmd_is_valid_softleaf(pmd_t pmd)
>  }
>
>  /**
> - * pmd_to_softleaf_folio() - Convert the PMD entry to a folio.
> + * pmd_softleaf_to_folio() - Convert the PMD softleaf entry to a folio.
>   * @pmd: PMD entry.
>   *
>   * The PMD entry is expected to be a valid PMD softleaf entry.
> @@ -665,7 +665,7 @@ static inline bool pmd_is_valid_softleaf(pmd_t pmd)
>   * Returns: the folio the softleaf entry references if this is a valid softleaf
>   * entry, otherwise NULL.
>   */
> -static inline struct folio *pmd_to_softleaf_folio(pmd_t pmd)
> +static inline struct folio *pmd_softleaf_to_folio(pmd_t pmd)
>  {
>  	const softleaf_t entry = softleaf_from_pmd(pmd);
>
> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
> index ced400f72d43a..1b6b0aa2baa3b 100644
> --- a/mm/huge_memory.c
> +++ b/mm/huge_memory.c
> @@ -2467,7 +2467,7 @@ static struct folio *normal_or_softleaf_folio_pmd(struct vm_area_struct *vma,
>
>  	if (!thp_migration_supported())
>  		WARN_ONCE(1, "Non present huge pmd without pmd migration enabled!");
> -	return pmd_to_softleaf_folio(pmdval);
> +	return pmd_softleaf_to_folio(pmdval);
>  }
>
>  static bool has_deposited_pgtable(struct vm_area_struct *vma, pmd_t pmdval,
> --
> 2.53.0-Meta
>

--
Cheers, Lorenzo


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

* Re: [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio()
  2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
  2026-08-18 14:24   ` David Hildenbrand (Arm)
  2026-08-18 18:36   ` Lorenzo Stoakes (ARM)
@ 2026-08-18 18:38   ` Zi Yan
  2 siblings, 0 replies; 20+ messages in thread
From: Zi Yan @ 2026-08-18 18:38 UTC (permalink / raw)
  To: Usama Arif
  Cc: Andrew Morton, david, chrisl, kasong, ljs, linux-mm, ying.huang,
	Baoquan He, willy, youngjun.park, hannes, riel, shakeel.butt,
	alex, kas, baohua, dev.jain, baolin.wang, Nico Pache,
	Liam R.Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
	linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On 18 Aug 2026, at 9:09, Usama Arif wrote:

> pmd_to_softleaf_folio() converts the softleaf entry encoded by a PMD
> to a folio. Rename it to pmd_softleaf_to_folio() to make the conversion
> direction explicit and align it with softleaf_to_folio().
>
> No functional change.
>
> Suggested-by: Dev Jain <dev.jain@arm.com>
> Signed-off-by: Usama Arif <usama.arif@linux.dev>
> ---
>  include/linux/leafops.h | 4 ++--
>  mm/huge_memory.c        | 2 +-
>  2 files changed, 3 insertions(+), 3 deletions(-)
>
LGTM.

Reviewed-by: Zi Yan <ziy@nvidia.com>

Best Regards,
Yan, Zi


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

* Re: [PATCH v6 02/12] mm: add PMD swap entry detection support
  2026-08-18 14:40   ` David Hildenbrand (Arm)
@ 2026-08-18 18:42     ` Lorenzo Stoakes (ARM)
  0 siblings, 0 replies; 20+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-18 18:42 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Usama Arif, Andrew Morton, chrisl, kasong, ziy, linux-mm,
	ying.huang, Baoquan He, willy, youngjun.park, hannes, riel,
	shakeel.butt, alex, kas, baohua, dev.jain, baolin.wang,
	Nico Pache, Liam R. Howlett, ryan.roberts, Vlastimil Babka,
	lance.yang, linux-kernel, nphamcs, shikemeng, yosry, kernel-team

On Tue, Aug 18, 2026 at 04:40:25PM +0200, David Hildenbrand (Arm) wrote:
> On 8/18/26 15:09, Usama Arif wrote:
> > Currently when a PMD-mapped THP is swapped out, the PMD is always
> > split into HPAGE_PMD_NR PTE-level swap entries. To preserve huge
> > page information across swap cycles, later patches will install a
> > single PMD-level swap entry instead. Add the infrastructure to detect
> > those entries.
> >
> > Teach the softleaf layer to recognise PMD swap entries:
> > pmd_is_swap_entry() detects them and softleaf_is_valid_pmd_entry()
> > accepts them as a valid non-present type. Because swap entries do not
> > encode a PFN, make pmd_softleaf_to_folio() warn and return NULL for them
> > instead of passing the swap offset to softleaf_to_folio(). Clear the
> > exclusive overlay bit in softleaf_from_pmd() before decoding, matching
> > how soft_dirty and uffd bits are already stripped.
> >
> > Add pmd_swp_mkexclusive(), pmd_swp_exclusive(), and
> > pmd_swp_clear_exclusive() helpers to each architecture that supports
> > PMD softleaf entries (x86, arm64, s390, riscv, loongarch, powerpc),
> > mirroring the existing PTE swap exclusive helpers in each arch's
> > pgtable.h.
>
> Ah, for migration entries we still use a dedicated migratetype. I actually have
> on my todo list to move to PTE bits as well.

We do seem to have rather hacked in PMD level stuff across the board
previously :)

>
> (likely the _swp_ part should then be renamed to indicate that this is for
> softdirty entries, not just swap entries)

You mean softleaf?

>
> > Provide generic no-op PMD swap exclusive fallbacks for
> > architectures without PMD softleaf support, matching the generic PMD
> > swap soft-dirty fallbacks.
>
> No softleaf implies to migration and no swap, so this would work.
>
> You should extend mm/debug_vm_pgtable.c in a separate commit to test what
> pte_swap_exclusive_tests() tests for PMDs.

BTW one thing I should maybe mention here is that I never quite _finished_
the softleaf thing, so there's some awkward overlap between swap stuff and
softleaf stuff (swap entries _are_ softleaf entries, just a specific kind).

Infinite TODO list etc. (maybe this is a hint/nudge to somebody else to do
it ;)

>
> >
> > Signed-off-by: Usama Arif <usama.arif@linux.dev>
> > ---
> >  arch/arm64/include/asm/pgtable.h             |  6 +++++
> >  arch/loongarch/include/asm/pgtable.h         | 19 ++++++++++++++
> >  arch/powerpc/include/asm/book3s/64/pgtable.h | 17 +++++++++++++
> >  arch/riscv/include/asm/pgtable.h             | 15 +++++++++++
> >  arch/s390/include/asm/pgtable.h              | 17 +++++++++++++
> >  arch/x86/include/asm/pgtable.h               | 17 +++++++++++++
>
> I'm sorry for asking you to compete with Kiryll's series by creating one patch
> for each architecture that directly jumps at arch maintainers :)
>
> [...]
>
> >  #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY
> >  static inline pte_t pte_swp_mksoft_dirty(pte_t pte)
> >  {
> > diff --git a/include/linux/leafops.h b/include/linux/leafops.h
> > index 7c13c58a5e218..4a6c52974b305 100644
> > --- a/include/linux/leafops.h
> > +++ b/include/linux/leafops.h
> > @@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd)
> >  		pmd = pmd_swp_clear_soft_dirty(pmd);
> >  	if (pmd_swp_uffd(pmd))
> >  		pmd = pmd_swp_clear_uffd(pmd);
> > +	if (pmd_swp_exclusive(pmd))
> > +		pmd = pmd_swp_clear_exclusive(pmd);
>
> Can't we just unconditionally clear these flags?
>
> pmd = pmd_swp_clear_uffd(pmd);
> pmd = pmd_swp_clear_exclusive(pmd);
>
> Avoids these rather unnecessary conditionals unless I am missing something.
>
>
> >  	arch_entry = __pmd_to_swp_entry(pmd);
> >
> >  	/* Temporary until swp_entry_t eliminated. */
> > @@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd)
> >   */
> >  static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry)
> >  {
> > -	/* Only device private, migration entries valid for PMD. */
> > +	/* Device private, migration, and swap entries valid for PMD. */
>
> Can we just drop that comment? I mean, it's as clear as it gets in the code
> immediately below :)
>
> >  	return softleaf_is_device_private(entry) ||
> > -		softleaf_is_migration(entry);
> > +		softleaf_is_migration(entry) ||
> > +		softleaf_is_swap(entry);
> > +}
> > +
>
> --
> Cheers,
>
> David

--
Cheers, Lorenzo


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

end of thread, other threads:[~2026-08-18 18:42 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 13:09 [PATCH v6 00/12] mm: PMD-level swap entries for anonymous THPs Usama Arif
2026-08-18 13:09 ` [PATCH v6 01/12] mm: rename pmd_to_softleaf_folio() to pmd_softleaf_to_folio() Usama Arif
2026-08-18 14:24   ` David Hildenbrand (Arm)
2026-08-18 18:36   ` Lorenzo Stoakes (ARM)
2026-08-18 18:38   ` Zi Yan
2026-08-18 13:09 ` [PATCH v6 02/12] mm: add PMD swap entry detection support Usama Arif
2026-08-18 14:40   ` David Hildenbrand (Arm)
2026-08-18 18:42     ` Lorenzo Stoakes (ARM)
2026-08-18 13:09 ` [PATCH v6 03/12] mm: add PMD swap entry splitting support Usama Arif
2026-08-18 17:53   ` David Hildenbrand (Arm)
2026-08-18 13:09 ` [PATCH v6 04/12] mm: handle PMD swap entries in fork path Usama Arif
2026-08-18 13:09 ` [PATCH v6 05/12] mm: zswap: add range lookup for large-folio swapin Usama Arif
2026-08-18 18:28   ` Yosry Ahmed
2026-08-18 13:09 ` [PATCH v6 06/12] mm: swap in PMD swap entries as whole THPs during swapoff Usama Arif
2026-08-18 13:09 ` [PATCH v6 07/12] mm: handle PMD swap entries in non-present PMD walkers Usama Arif
2026-08-18 13:09 ` [PATCH v6 08/12] mm: handle PMD swap entries in MADV_WILLNEED Usama Arif
2026-08-18 13:09 ` [PATCH v6 09/12] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
2026-08-18 13:09 ` [PATCH v6 10/12] mm: handle PMD swap entry faults on swap-in Usama Arif
2026-08-18 13:09 ` [PATCH v6 11/12] mm: install PMD swap entries on swap-out Usama Arif
2026-08-18 13:09 ` [PATCH v6 12/12] selftests/mm: add PMD swap entry tests Usama Arif

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