* [PATCH v4 06/11] mm: handle PMD swap entries in non-present PMD walkers
[not found] <20260713133613.2707815-1-usama.arif@linux.dev>
@ 2026-07-13 13:35 ` Usama Arif
2026-07-13 13:35 ` [PATCH v4 08/11] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
` (4 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-13 13:35 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, npache,
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.
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() faults in PMD swap entries via
hmm_vma_fault() instead of returning -EFAULT. 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.
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, leaking swap slots.
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 the pmd_trans_huge_lock() branch through
mincore_swap() for non-present PMDs, 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_swap() returns 1 for migration /
device-private entries (preserving the prior behavior for those)
and checks swap-cache residency for swap entries.
queue_folios_pmd() in mempolicy silently skips swap entries, matching
the PTE walker which only counts migration entries as failures.
Without this, mbind(MPOL_MF_STRICT) would spuriously return -EIO on
a swapped-out THP.
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.
Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
fs/proc/task_mmu.c | 43 +++++++++++++++++++++-------------
mm/hmm.c | 3 ++-
mm/huge_memory.c | 58 +++++++++++++++++++++++++++++++++++-----------
mm/khugepaged.c | 6 +++++
mm/madvise.c | 14 ++++++++++-
mm/mincore.c | 45 ++++++++++++++++++++++++++++++++++-
6 files changed, 137 insertions(+), 32 deletions(-)
diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c
index 8285f497178b..bf0fd594be59 100644
--- a/fs/proc/task_mmu.c
+++ b/fs/proc/task_mmu.c
@@ -1046,6 +1046,23 @@ 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)
+{
+ int mapcount;
+
+ mss->swap += size;
+ mapcount = swp_swapcount(entry);
+ if (mapcount >= 2) {
+ u64 pss_delta = (u64)size << PSS_SHIFT;
+
+ do_div(pss_delta, mapcount);
+ mss->swap_pss += pss_delta;
+ } else {
+ mss->swap_pss += (u64)size << PSS_SHIFT;
+ }
+}
+
static void smaps_pte_entry(pte_t *pte, unsigned long addr,
struct mm_walk *walk)
{
@@ -1067,18 +1084,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 +1114,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 +1765,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 +2125,8 @@ 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_swap_entry(pmd) ||
+ (thp_migration_supported() && pmd_is_migration_entry(pmd))) {
const softleaf_t entry = softleaf_from_pmd(pmd);
unsigned long offset;
@@ -2581,7 +2592,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 fc2e1cd0cb22..6da069c0dbfd 100644
--- a/mm/hmm.c
+++ b/mm/hmm.c
@@ -376,7 +376,8 @@ 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;
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index d16cdf684b41..991929ec17d8 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2347,6 +2347,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.
@@ -2371,6 +2379,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;
@@ -2421,15 +2444,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)
{
@@ -2522,6 +2536,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);
@@ -2554,7 +2578,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);
@@ -2645,7 +2670,14 @@ static void change_non_present_huge_pmd(struct mm_struct *mm,
pmd_t newpmd;
VM_WARN_ON(!pmd_is_valid_softleaf(*pmd));
- if (softleaf_is_migration_write(entry)) {
+
+ /*
+ * PMD swap entries don't encode write permission in the entry type,
+ * so only uffd_wp flag changes apply. No folio lookup needed.
+ */
+ if (softleaf_is_swap(entry)) {
+ newpmd = *pmd;
+ } else if (softleaf_is_migration_write(entry)) {
const struct folio *folio = softleaf_to_folio(entry);
/*
@@ -2705,7 +2737,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;
@@ -3252,7 +3284,7 @@ static void __split_huge_pmd_locked(struct vm_area_struct *vma, pmd_t *pmd,
VM_WARN_ON_ONCE(freeze);
old_pmd = *pmd;
soft_dirty = pmd_swp_soft_dirty(old_pmd);
- uffd_wp = pmd_swp_uffd_wp(old_pmd);
+ uffd_wp = pmd_swp_uffd(old_pmd);
anon_exclusive = pmd_swp_exclusive(old_pmd);
} else {
/*
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 45e8245d80da..3a44bf8206c0 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -1102,6 +1102,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 bf9ce199935a..e24166500ac5 100644
--- a/mm/madvise.c
+++ b/mm/madvise.c
@@ -374,6 +374,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);
@@ -384,6 +393,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;
@@ -665,7 +677,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 53b982803771..ddf7c96964b0 100644
--- a/mm/mincore.c
+++ b/mm/mincore.c
@@ -99,6 +99,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,
@@ -172,7 +207,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] 10+ messages in thread* [PATCH v4 08/11] mm: handle PMD swap entries in UFFDIO_MOVE
[not found] <20260713133613.2707815-1-usama.arif@linux.dev>
2026-07-13 13:35 ` [PATCH v4 06/11] mm: handle PMD swap entries in non-present PMD walkers Usama Arif
@ 2026-07-13 13:35 ` Usama Arif
2026-07-13 13:35 ` [PATCH v4 09/11] mm: handle PMD swap entry faults on swap-in Usama Arif
` (3 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-13 13:35 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, npache,
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_swap_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, and carry the deposited page table across with the entry.
The dispatcher in move_pages_huge_pmd() now waits for migration on a
PMD migration entry (matching the PTE path) and routes PMD swap
entries through move_swap_pmd() after pinning the swap device and
arming an mmu_notifier range so secondary MMUs see the move.
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.
Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
mm/huge_memory.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 132 insertions(+), 1 deletion(-)
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 991929ec17d8..b8e3566bd502 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -2864,6 +2864,72 @@ int change_huge_pud(struct mmu_gather *tlb, struct vm_area_struct *vma,
#endif
#ifdef CONFIG_USERFAULTFD
+/*
+ * 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);
+ 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;
+}
+
/*
* 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.
@@ -2898,11 +2964,76 @@ 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;
}
+ 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;
+ }
+ spin_unlock(src_ptl);
return -ENOENT;
}
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v4 09/11] mm: handle PMD swap entry faults on swap-in
[not found] <20260713133613.2707815-1-usama.arif@linux.dev>
2026-07-13 13:35 ` [PATCH v4 06/11] mm: handle PMD swap entries in non-present PMD walkers Usama Arif
2026-07-13 13:35 ` [PATCH v4 08/11] mm: handle PMD swap entries in UFFDIO_MOVE Usama Arif
@ 2026-07-13 13:35 ` Usama Arif
2026-07-13 13:35 ` [PATCH v4 10/11] mm: install PMD swap entries on swap-out Usama Arif
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-13 13:35 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, npache,
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.
- 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:
folio_contain_hwpoisoned_page() covers PG_hwpoison on the head
and PG_has_hwpoisoned on a large folio. 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.
- Restore soft_dirty and uffd_wp from the swap PMD. 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,
encrypted) 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 immediately rather than forcing a second fault. Mask
VM_FAULT_FALLBACK from its 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.
Requires exposing wp_huge_pmd() via mm/internal.h.
- 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 swapin_folio()
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 | 9 ++
mm/huge_memory.c | 228 ++++++++++++++++++++++++++++++++++++++++
mm/internal.h | 34 ++++++
mm/memory.c | 38 +------
4 files changed, 275 insertions(+), 34 deletions(-)
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index cae97c307280..b6ab28ff7eee 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -534,6 +534,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;
diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index b8e3566bd502..60f42d136945 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -41,6 +41,7 @@
#include <linux/pgalloc.h>
#include <linux/pgalloc_tag.h>
#include <linux/pagewalk.h>
+#include <linux/zswap.h>
#include <asm/tlb.h>
#include "internal.h"
@@ -2347,6 +2348,233 @@ 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;
+ 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;
+
+ 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);
+
+ if ((vmf->flags & FAULT_FLAG_WRITE) && !folio_test_lru(folio))
+ lru_add_drain();
+
+ 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);
+
+ /*
+ * Check exclusivity to determine if we can map writable.
+ */
+ if (exclusive || folio_ref_count(folio) == 1) {
+ if ((vma->vm_flags & VM_WRITE) &&
+ !userfaultfd_huge_pmd_wp(vma, pmd) &&
+ !pmd_needs_soft_dirty_wp(vma, pmd)) {
+ pmd = pmd_mkwrite(pmd, vma);
+ if (vmf->flags & FAULT_FLAG_WRITE) {
+ pmd = pmd_mkdirty(pmd);
+ vmf->flags &= ~FAULT_FLAG_WRITE;
+ }
+ }
+ 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), fall through to 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 — 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 (vmf->flags & FAULT_FLAG_WRITE) {
+ 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:
+ __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 874be94cf257..660cb27c216a 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -573,6 +573,40 @@ static inline vm_fault_t vmf_anon_prepare(struct vm_fault *vmf)
}
vm_fault_t do_swap_page(struct vm_fault *vmf);
+vm_fault_t wp_huge_pmd(struct vm_fault *vmf);
+
+/*
+ * 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 21ea76bb4c37..08baf528d4a4 100644
--- a/mm/memory.c
+++ b/mm/memory.c
@@ -4585,38 +4585,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,
@@ -6331,8 +6299,7 @@ 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)
+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;
@@ -6625,6 +6592,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] 10+ messages in thread* [PATCH v4 10/11] mm: install PMD swap entries on swap-out
[not found] <20260713133613.2707815-1-usama.arif@linux.dev>
` (2 preceding siblings ...)
2026-07-13 13:35 ` [PATCH v4 09/11] mm: handle PMD swap entry faults on swap-in Usama Arif
@ 2026-07-13 13:35 ` Usama Arif
2026-07-13 15:56 ` [PATCH v4 00/11] mm: PMD-level swap entries for anonymous THPs Yosry Ahmed
[not found] ` <20260713133613.2707815-5-usama.arif@linux.dev>
5 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-13 13:35 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, npache,
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.
Signed-off-by: Usama Arif <usama.arif@linux.dev>
---
include/linux/huge_mm.h | 2 +
include/linux/vm_event_item.h | 1 +
mm/huge_memory.c | 80 +++++++++++++++++++++++++++++++++++
mm/rmap.c | 19 +++++++++
mm/vmscan.c | 9 +++-
mm/vmstat.c | 1 +
6 files changed, 111 insertions(+), 1 deletion(-)
diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index b6ab28ff7eee..6d294d446f7b 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -536,6 +536,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 03fe95f5a020..7267c06674c0 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 60f42d136945..2f5c01a043fe 100644
--- a/mm/huge_memory.c
+++ b/mm/huge_memory.c
@@ -5544,3 +5544,83 @@ void remove_migration_pmd(struct page_vma_mapped_walk *pvmw, struct page *new)
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 (!(pvmw->pmd && !pvmw->pte))
+ return 0;
+
+ VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio);
+ VM_BUG_ON_FOLIO(!folio_test_anon(folio), 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 e854679553b9..b431d52cc2e3 100644
--- a/mm/rmap.c
+++ b/mm/rmap.c
@@ -2079,6 +2079,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 986dde8e7429..ffc72a2f635b 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -1322,7 +1322,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 3b5cb1031f72..08af18f2b4dc 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1422,6 +1422,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] 10+ messages in thread* Re: [PATCH v4 00/11] mm: PMD-level swap entries for anonymous THPs
[not found] <20260713133613.2707815-1-usama.arif@linux.dev>
` (3 preceding siblings ...)
2026-07-13 13:35 ` [PATCH v4 10/11] mm: install PMD swap entries on swap-out Usama Arif
@ 2026-07-13 15:56 ` Yosry Ahmed
2026-07-13 16:17 ` Usama Arif
[not found] ` <20260713133613.2707815-5-usama.arif@linux.dev>
5 siblings, 1 reply; 10+ messages in thread
From: Yosry Ahmed @ 2026-07-13 15:56 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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team
On Mon, Jul 13, 2026 at 6:36 AM Usama Arif <usama.arif@linux.dev> wrote:
>
> This is the PMD swap entry core series. The preparatory PMD softleaf [1]
> cleanup series has been merged in akpm/mm-new, so this series is now
> based directly on akpm/mm-new.
>
> 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.
>
> Patch breakdown:
>
> 1. mm: add PMD swap entry detection support
> Add pmd_is_swap_entry(), teach the softleaf layer that PMD swap
> entries are valid PMD softleaf entries, and add per-arch
> pmd_swp_*exclusive helpers.
>
> 2. mm: add PMD swap entry splitting support
> Teach __split_huge_pmd_locked() to split a PMD swap entry into
> HPAGE_PMD_NR PTE swap entries. This is the common fallback path
> whenever PMD-level handling is unsafe or unavailable.
>
> 3. mm: handle PMD swap entries in fork path
> Copy a PMD swap entry in fork via one
> swap_dup_entries_direct(HPAGE_PMD_NR) operation, with the same
> swapoff/mmlist preparation rules used by PTE entries.
>
> 4. mm: zswap: add range lookup for large-folio swapin
> Teach zswap_load() to let all-on-disk large folio reads proceed to
> the backing swap device, and add zswap_range_has_entry() so PMD
> swap-entry consumers can split only when a specific range has
> per-page zswap state.
>
> 5. mm: swap in PMD swap entries as whole THPs during swapoff
> Add swap_pmd_cache_lookup() and use it from swapoff. Empty cache
> and PMD-sized cache state can be handled at PMD order; split cache
> state, zswap-backed slots, allocation/read failure, or non-uptodate
> folios split the PMD and fall back to unuse_pte_range().
>
> 6. mm: handle PMD swap entries in non-present PMD walkers
> Teach zap, mprotect, soft-dirty, uffd-wp, smaps, mincore,
> mempolicy, khugepaged, HMM, and madvise walkers about PMD swap
> entries. mincore reports PMD-sized cache state directly and checks
> per-page slots after the cache has split.
>
> 7. mm: handle PMD swap entries in MADV_WILLNEED
> Let MADV_WILLNEED prefetch a PMD swap entry at PMD order when safe,
> treat an already cached PMD-sized folio as complete, and split/retry
> through PTEs for split cache state, zswap-backed slots, or races
> with per-slot cache population.
>
> 8. mm: handle PMD swap entries in UFFDIO_MOVE
> Move PMD swap entries whole when the covered range is empty or
> backed by one PMD-sized folio. Split/per-slot cache state returns
> -EAGAIN after splitting so retry can use the PTE move path and
> update per-page rmap metadata.
>
> 9. mm: handle PMD swap entry faults on swap-in
> Add do_huge_pmd_swap_page(). It maps a PMD-sized cached folio
> directly, or allocates/reads at PMD order when the cache is empty
> and the range has no zswap entries. Split cache state, zswap-backed
> slots, and PMD-order resource failures fall back to the PTE path.
>
> 10. mm: install PMD swap entries on swap-out
> Stop forcing TTU_SPLIT_HUGE_PMD for PMD-mappable swapcache folios
> and install one PMD swap entry instead. Zswap still stores the THP
> as per-page compressed entries; PMD-order swap-in consumers preserve
> a PMD-sized cached folio or read an all-on-disk range as a whole THP,
> and split before reading per-page zswap state.
>
> 11. selftests/mm: add PMD swap entry tests
> Add pmd_swap selftests covering swap-out/in, fork, fork+COW,
> repeated cycles, write fault, munmap, mprotect, mremap, pagemap,
> mincore, MADV_FREE, MADV_WILLNEED, UFFDIO_MOVE, and swapoff.
>
> 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.
So in the swapout path, a single PMD swap entry is created, but
order-0 pages are compressed and stored independently in zswap? Just
checking I understood this correctly.
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: [PATCH v4 00/11] mm: PMD-level swap entries for anonymous THPs
2026-07-13 15:56 ` [PATCH v4 00/11] mm: PMD-level swap entries for anonymous THPs Yosry Ahmed
@ 2026-07-13 16:17 ` Usama Arif
0 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-13 16:17 UTC (permalink / raw)
To: Yosry Ahmed
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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team
On 13/07/2026 16:56, Yosry Ahmed wrote:
> On Mon, Jul 13, 2026 at 6:36 AM Usama Arif <usama.arif@linux.dev> wrote:
>>
>> This is the PMD swap entry core series. The preparatory PMD softleaf [1]
>> cleanup series has been merged in akpm/mm-new, so this series is now
>> based directly on akpm/mm-new.
>>
>> 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.
>>
>> Patch breakdown:
>>
>> 1. mm: add PMD swap entry detection support
>> Add pmd_is_swap_entry(), teach the softleaf layer that PMD swap
>> entries are valid PMD softleaf entries, and add per-arch
>> pmd_swp_*exclusive helpers.
>>
>> 2. mm: add PMD swap entry splitting support
>> Teach __split_huge_pmd_locked() to split a PMD swap entry into
>> HPAGE_PMD_NR PTE swap entries. This is the common fallback path
>> whenever PMD-level handling is unsafe or unavailable.
>>
>> 3. mm: handle PMD swap entries in fork path
>> Copy a PMD swap entry in fork via one
>> swap_dup_entries_direct(HPAGE_PMD_NR) operation, with the same
>> swapoff/mmlist preparation rules used by PTE entries.
>>
>> 4. mm: zswap: add range lookup for large-folio swapin
>> Teach zswap_load() to let all-on-disk large folio reads proceed to
>> the backing swap device, and add zswap_range_has_entry() so PMD
>> swap-entry consumers can split only when a specific range has
>> per-page zswap state.
>>
>> 5. mm: swap in PMD swap entries as whole THPs during swapoff
>> Add swap_pmd_cache_lookup() and use it from swapoff. Empty cache
>> and PMD-sized cache state can be handled at PMD order; split cache
>> state, zswap-backed slots, allocation/read failure, or non-uptodate
>> folios split the PMD and fall back to unuse_pte_range().
>>
>> 6. mm: handle PMD swap entries in non-present PMD walkers
>> Teach zap, mprotect, soft-dirty, uffd-wp, smaps, mincore,
>> mempolicy, khugepaged, HMM, and madvise walkers about PMD swap
>> entries. mincore reports PMD-sized cache state directly and checks
>> per-page slots after the cache has split.
>>
>> 7. mm: handle PMD swap entries in MADV_WILLNEED
>> Let MADV_WILLNEED prefetch a PMD swap entry at PMD order when safe,
>> treat an already cached PMD-sized folio as complete, and split/retry
>> through PTEs for split cache state, zswap-backed slots, or races
>> with per-slot cache population.
>>
>> 8. mm: handle PMD swap entries in UFFDIO_MOVE
>> Move PMD swap entries whole when the covered range is empty or
>> backed by one PMD-sized folio. Split/per-slot cache state returns
>> -EAGAIN after splitting so retry can use the PTE move path and
>> update per-page rmap metadata.
>>
>> 9. mm: handle PMD swap entry faults on swap-in
>> Add do_huge_pmd_swap_page(). It maps a PMD-sized cached folio
>> directly, or allocates/reads at PMD order when the cache is empty
>> and the range has no zswap entries. Split cache state, zswap-backed
>> slots, and PMD-order resource failures fall back to the PTE path.
>>
>> 10. mm: install PMD swap entries on swap-out
>> Stop forcing TTU_SPLIT_HUGE_PMD for PMD-mappable swapcache folios
>> and install one PMD swap entry instead. Zswap still stores the THP
>> as per-page compressed entries; PMD-order swap-in consumers preserve
>> a PMD-sized cached folio or read an all-on-disk range as a whole THP,
>> and split before reading per-page zswap state.
>>
>> 11. selftests/mm: add PMD swap entry tests
>> Add pmd_swap selftests covering swap-out/in, fork, fork+COW,
>> repeated cycles, write fault, munmap, mprotect, mremap, pagemap,
>> mincore, MADV_FREE, MADV_WILLNEED, UFFDIO_MOVE, and swapoff.
>>
>> 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.
>
> So in the swapout path, a single PMD swap entry is created, but
> order-0 pages are compressed and stored independently in zswap? Just
> checking I understood this correctly.
Yes that is correct, swapout installs PMD swap entry in try_to_unmap_one().
When the swapped-out THP reaches zswap, current zswap still stores it as
independent order-0 entries keyed by the individual swap slots. On
PMD-order swap-in, we only proceed as a THP if the swap cache still has
one PMD-sized folio, or if none of the covered slots is present in zswap
and the whole range can be read from disk. If any slot is still backed by
zswap, we split the PMD swap entry and retry through the normal PTE path.
^ permalink raw reply [flat|nested] 10+ messages in thread
[parent not found: <20260713133613.2707815-5-usama.arif@linux.dev>]
* Re: [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin
[not found] ` <20260713133613.2707815-5-usama.arif@linux.dev>
@ 2026-07-13 16:02 ` Yosry Ahmed
2026-07-13 16:29 ` Usama Arif
0 siblings, 1 reply; 10+ messages in thread
From: Yosry Ahmed @ 2026-07-13 16:02 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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team, Alexandre Ghiti
> /**
> * zswap_load() - load a folio from zswap
> * @folio: folio to load
> @@ -1571,10 +1588,9 @@ bool zswap_store(struct folio *folio)
> * 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,
> - * but NOT marked up-to-date, so that an IO error is emitted (e.g.
> - * do_swap_page() will SIGBUS).
> + * -EIO: if 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).
Just curious, why the change from -EINVAL to -EIO here? Does it matter
in practice or is it just a matter of personal taste? :)
> *
> * -ENOENT: if the swapped out content was not in zswap. The folio remains
> * locked on return.
> @@ -1593,13 +1609,19 @@ 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 (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 [flat|nested] 10+ messages in thread* Re: [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin
2026-07-13 16:02 ` [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin Yosry Ahmed
@ 2026-07-13 16:29 ` Usama Arif
2026-07-13 16:55 ` Yosry Ahmed
0 siblings, 1 reply; 10+ messages in thread
From: Usama Arif @ 2026-07-13 16:29 UTC (permalink / raw)
To: Yosry Ahmed
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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team, Alexandre Ghiti
On 13/07/2026 17:02, Yosry Ahmed wrote:
>> /**
>> * zswap_load() - load a folio from zswap
>> * @folio: folio to load
>> @@ -1571,10 +1588,9 @@ bool zswap_store(struct folio *folio)
>> * 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,
>> - * but NOT marked up-to-date, so that an IO error is emitted (e.g.
>> - * do_swap_page() will SIGBUS).
>> + * -EIO: if 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).
>
> Just curious, why the change from -EINVAL to -EIO here? Does it matter
> in practice or is it just a matter of personal taste? :)
The caller only checks for != -ENOENT so no functional difference here. I viewed
it as a change in semantic, -EINVAL meant that you passed a large folio, which
wasn't supported to -EIO meaning the request is valid but zswap cannot safely
complete a PMD-sized read from per-page compressed state.
I dont want to speak for Alexandre, will let him comment on this properly :)
But above is what I thought of when I got the patch from Alex.
>
>> *
>> * -ENOENT: if the swapped out content was not in zswap. The folio remains
>> * locked on return.
>> @@ -1593,13 +1609,19 @@ 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 (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 [flat|nested] 10+ messages in thread* Re: [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin
2026-07-13 16:29 ` Usama Arif
@ 2026-07-13 16:55 ` Yosry Ahmed
2026-07-14 10:00 ` Usama Arif
0 siblings, 1 reply; 10+ messages in thread
From: Yosry Ahmed @ 2026-07-13 16:55 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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team, Alexandre Ghiti
On Mon, Jul 13, 2026 at 9:29 AM Usama Arif <usama.arif@linux.dev> wrote:
>
>
>
> On 13/07/2026 17:02, Yosry Ahmed wrote:
> >> /**
> >> * zswap_load() - load a folio from zswap
> >> * @folio: folio to load
> >> @@ -1571,10 +1588,9 @@ bool zswap_store(struct folio *folio)
> >> * 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,
> >> - * but NOT marked up-to-date, so that an IO error is emitted (e.g.
> >> - * do_swap_page() will SIGBUS).
> >> + * -EIO: if 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).
> >
> > Just curious, why the change from -EINVAL to -EIO here? Does it matter
> > in practice or is it just a matter of personal taste? :)
>
> The caller only checks for != -ENOENT so no functional difference here. I viewed
> it as a change in semantic, -EINVAL meant that you passed a large folio, which
> wasn't supported to -EIO meaning the request is valid but zswap cannot safely
> complete a PMD-sized read from per-page compressed state.
>
> I dont want to speak for Alexandre, will let him comment on this properly :)
> But above is what I thought of when I got the patch from Alex.
A mention of the reasoning in the changelog would be nice, even if
it's only semantic.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin
2026-07-13 16:55 ` Yosry Ahmed
@ 2026-07-14 10:00 ` Usama Arif
0 siblings, 0 replies; 10+ messages in thread
From: Usama Arif @ 2026-07-14 10:00 UTC (permalink / raw)
To: Yosry Ahmed
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, npache,
Liam R. Howlett, ryan.roberts, Vlastimil Babka, lance.yang,
linux-kernel, nphamcs, shikemeng, kernel-team, Alexandre Ghiti
On 13/07/2026 17:55, Yosry Ahmed wrote:
> On Mon, Jul 13, 2026 at 9:29 AM Usama Arif <usama.arif@linux.dev> wrote:
>>
>>
>>
>> On 13/07/2026 17:02, Yosry Ahmed wrote:
>>>> /**
>>>> * zswap_load() - load a folio from zswap
>>>> * @folio: folio to load
>>>> @@ -1571,10 +1588,9 @@ bool zswap_store(struct folio *folio)
>>>> * 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,
>>>> - * but NOT marked up-to-date, so that an IO error is emitted (e.g.
>>>> - * do_swap_page() will SIGBUS).
>>>> + * -EIO: if 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).
>>>
>>> Just curious, why the change from -EINVAL to -EIO here? Does it matter
>>> in practice or is it just a matter of personal taste? :)
>>
>> The caller only checks for != -ENOENT so no functional difference here. I viewed
>> it as a change in semantic, -EINVAL meant that you passed a large folio, which
>> wasn't supported to -EIO meaning the request is valid but zswap cannot safely
>> complete a PMD-sized read from per-page compressed state.
>>
>> I dont want to speak for Alexandre, will let him comment on this properly :)
>> But above is what I thought of when I got the patch from Alex.
>
> A mention of the reasoning in the changelog would be nice, even if
> it's only semantic.
Ack, Will add in next revision. Alexandre, let me know if there is something
specific you prefer to add in the commit message, otherwise will add something
similar to above.
^ permalink raw reply [flat|nested] 10+ messages in thread