Re: [RFC PATCH v2 6/9] mm: provide anon locality evidence for zswap large swapin
From: Nhat Pham
Date: Fri May 29 2026 - 15:27:46 EST
On Fri, May 29, 2026 at 5:19 AM fujunjie <fujunjie1@xxxxxx> wrote:
>
> The common zswap large-swapin policy needs locality evidence from
> callers before it can admit a large folio. For anonymous faults, provide
> that evidence from existing VMA hints and from the PTE young state left
> by earlier zswap-backed large swapins.
>
> Keep non-faulting PTEs old when mapping a speculative all-zswap large
> folio. A later fault can then require a dense young previous range before
> admitting another large swapin without adding VMA state.
Makes sense to me.
>
> This also removes the old zswap-enabled guard from the THP swapin
> candidate scan. The common swapin path now classifies the backend range
> and falls back to order-0 for mixed zswap/disk ranges or races.
>
> Signed-off-by: fujunjie <fujunjie1@xxxxxx>
> ---
> mm/memory.c | 234 +++++++++++++++++++++++++++++++++++++++++++-----
> mm/swap.h | 6 ++
> mm/swap_state.c | 15 ++++
> 3 files changed, 235 insertions(+), 20 deletions(-)
>
> diff --git a/mm/memory.c b/mm/memory.c
> index 92a82008d583..7bbb89632000 100644
> --- a/mm/memory.c
> +++ b/mm/memory.c
> @@ -4556,6 +4556,35 @@ static void memcg1_swapin_retry_folio(struct folio *folio,
> folio_unlock(folio);
> }
>
> +static void set_swapin_ptes(struct vm_area_struct *vma,
> + unsigned long address, pte_t *ptep, pte_t pte,
> + unsigned int nr_pages, unsigned int fault_pte_idx,
> + bool fault_only_young)
> +{
> + struct mm_struct *mm = vma->vm_mm;
> + pte_t old_pte;
> +
> + if (!fault_only_young || nr_pages == 1) {
> + set_ptes(mm, address, ptep, pte, nr_pages);
> + return;
> + }
> +
> + old_pte = pte_mkold(pte);
> + if (fault_pte_idx)
> + set_ptes(mm, address, ptep, old_pte, fault_pte_idx);
> +
> + set_pte_at(mm, address + fault_pte_idx * PAGE_SIZE,
> + ptep + fault_pte_idx,
> + pte_mkyoung(pte_advance_pfn(pte, fault_pte_idx)));
Hmm, does this mean that without THP swapin, the faulting PTE is not
marked young, but it is marked young if there is a THP swapin. That's
a behavioral change right? Would this throw off other heuristics
relying on this bit, or any justification that this is fine?
> +
> + fault_pte_idx++;
> + if (fault_pte_idx < nr_pages)
> + set_ptes(mm, address + fault_pte_idx * PAGE_SIZE,
> + ptep + fault_pte_idx,
> + pte_advance_pfn(old_pte, fault_pte_idx),
> + nr_pages - fault_pte_idx);
> +}
> +
> static vm_fault_t pte_marker_clear(struct vm_fault *vmf)
> {
> vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
> @@ -4628,6 +4657,157 @@ static vm_fault_t handle_pte_marker(struct vm_fault *vmf)
> }
>
> #ifdef CONFIG_TRANSPARENT_HUGEPAGE
> +#define SWAPIN_ANON_YOUNG_MIN_PERCENT 75
> +#define SWAPIN_ANON_MAX_FAULT_SKIP_SHIFT 2
> +
> +static bool swapin_anon_prev_young_dense(struct vm_fault *vmf,
> + unsigned int order)
> +{
> + struct vm_area_struct *vma;
> + unsigned int nr_pages;
> + unsigned int threshold;
> + unsigned long size;
> + unsigned long base, prev, addr;
> + struct folio *first = NULL;
> + unsigned int present = 0;
> + unsigned int young = 0;
> + pmd_t *pmd;
> + pmd_t pmdval;
> + spinlock_t *ptl; /* protects the previous PTE range */
> + pte_t *ptep;
> + unsigned int i;
> +
> + if (!IS_ENABLED(CONFIG_MMU) || !arch_has_hw_pte_young() || !vmf ||
> + !vmf->vma || !vmf->pmd || !order || order > MAX_PAGE_ORDER)
> + return false;
> +
> + nr_pages = 1U << order;
> + threshold = DIV_ROUND_UP(nr_pages *
> + SWAPIN_ANON_YOUNG_MIN_PERCENT, 100);
> + size = PAGE_SIZE << order;
> +
> + vma = vmf->vma;
> + base = ALIGN_DOWN(vmf->address, size);
> + if (base < size)
> + return false;
> +
> + prev = base - size;
> + if (prev < vma->vm_start || prev + size > vma->vm_end)
> + return false;
> +
> + pmd = vmf->pmd;
> + if ((prev & PMD_MASK) != (base & PMD_MASK)) {
> + pmd = mm_find_pmd(vma->vm_mm, prev);
> + if (!pmd)
> + return false;
> + }
> +
> + pmdval = pmdp_get_lockless(pmd);
> + if (!pmd_present(pmdval) || pmd_leaf(pmdval))
> + return false;
> +
> + ptep = pte_offset_map_lock(vma->vm_mm, pmd, prev, &ptl);
> + if (!ptep)
> + return false;
> +
> + for (i = 0, addr = prev; i < nr_pages; i++, addr += PAGE_SIZE) {
> + struct folio *folio;
> + pte_t pte = ptep_get(ptep + i);
> +
> + if (!pte_present(pte))
> + break;
> +
> + folio = vm_normal_folio(vma, addr, pte);
> + if (!folio || folio_order(folio) != order)
> + break;
> + if (!first)
> + first = folio;
> + else if (folio != first)
> + break;
> +
> + present++;
> + if (pte_young(pte))
> + young++;
> + }
> +
> + pte_unmap_unlock(ptep, ptl);
> + if (present != nr_pages)
> + return false;
> +
> + return young >= threshold;
> +}
> +
> +static bool swapin_anon_accessed_neighbour(struct vm_fault *vmf,
> + unsigned int order)
> +{
> + unsigned long size;
> + unsigned long base;
> + unsigned long fault_idx;
> + unsigned long max_skip;
> +
> + if (!vmf || !vmf->vma || !order || order > MAX_PAGE_ORDER)
> + return false;
> +
> + size = PAGE_SIZE << order;
> + base = ALIGN_DOWN(vmf->address, size);
> +
> + /*
> + * Without a sequential hint, require prior young-density evidence and
> + * only allow faults near the start of the candidate range.
> + */
> + fault_idx = (vmf->address - base) >> PAGE_SHIFT;
> + max_skip = (1UL << order) >> SWAPIN_ANON_MAX_FAULT_SKIP_SHIFT;
> + if (fault_idx > max_skip)
> + return false;
> +
> + return swapin_anon_prev_young_dense(vmf, order);
> +}
> +
> +static bool swapin_anon_fault_starts_range(struct vm_fault *vmf,
> + unsigned int order)
> +{
> + struct vm_area_struct *vma;
> + unsigned long size;
> + unsigned long base;
> + unsigned long first;
> +
> + if (!vmf || !vmf->vma || !order || order > MAX_PAGE_ORDER)
> + return false;
> +
> + vma = vmf->vma;
> + size = PAGE_SIZE << order;
> + base = ALIGN_DOWN(vmf->address, size);
> + first = ALIGN(vma->vm_start, size);
> +
> + return base == first && vmf->address == base &&
> + base + size <= vma->vm_end;
> +}
> +
> +static unsigned long swapin_anon_locality_orders(struct vm_fault *vmf,
> + unsigned long orders)
> +{
> + struct vm_area_struct *vma = vmf ? vmf->vma : NULL;
> + unsigned long locality_orders = 0;
> + unsigned long candidates = orders & ~BIT(0);
> + int order;
> +
> + if (vma && (vma->vm_flags & VM_RAND_READ))
> + return 0;
> +
> + if (vma && (vma->vm_flags & VM_SEQ_READ))
> + return candidates;
> +
> + while (candidates) {
> + order = fls_long(candidates) - 1;
> + if (swapin_anon_fault_starts_range(vmf, order) ||
> + swapin_anon_accessed_neighbour(vmf, order))
> + locality_orders |= BIT(order);
> + candidates &= ~BIT(order);
> + }
> +
> + return locality_orders;
> +}
> +
> /*
> * Check if the PTEs within a range are contiguous swap entries.
> */
> @@ -4644,9 +4824,9 @@ static bool can_swapin_thp(struct vm_fault *vmf, pte_t *ptep, int nr_pages)
> if (!pte_same(pte, pte_move_swp_offset(vmf->orig_pte, -idx)))
> return false;
> /*
> - * swap_read_folio() can't handle the case a large folio is hybridly
> - * from different backends. And they are likely corner cases. Similar
> - * things might be added once zswap support large folios.
> + * swap_read_folio() can't do mixed-backend large folio IO. The common
> + * synchronous swapin path will recheck backend state and fall back to
> + * order-0 if a zswap/disk race makes the range mixed.
> */
> if (swap_pte_batch(ptep, nr_pages, pte) != nr_pages)
> return false;
> @@ -4693,14 +4873,6 @@ static unsigned long thp_swapin_suitable_orders(struct vm_fault *vmf)
> if (unlikely(userfaultfd_armed(vma)))
> return 0;
>
> - /*
> - * A large swapped out folio could be partially or fully in zswap. We
> - * lack handling for such cases, so fallback to swapping in order-0
> - * folio.
> - */
> - if (!zswap_never_enabled())
> - return 0;
> -
> entry = softleaf_from_pte(vmf->orig_pte);
> /*
> * Get a list of all the (large) orders below PMD_ORDER that are enabled
> @@ -4708,10 +4880,13 @@ static unsigned long thp_swapin_suitable_orders(struct vm_fault *vmf)
> */
> orders = thp_vma_allowable_orders(vma, vma->vm_flags, TVA_PAGEFAULT,
> BIT(PMD_ORDER) - 1);
> + if (!orders)
> + return 0;
> orders = thp_vma_suitable_orders(vma, vmf->address, orders);
> + if (!orders)
> + return 0;
> orders = thp_swap_suitable_orders(swp_offset(entry),
> vmf->address, orders);
> -
> if (!orders)
> return 0;
>
> @@ -4741,6 +4916,12 @@ static unsigned long thp_swapin_suitable_orders(struct vm_fault *vmf)
> {
> return 0;
> }
> +
> +static unsigned long swapin_anon_locality_orders(struct vm_fault *vmf,
> + unsigned long orders)
> +{
> + return 0;
> +}
> #endif /* CONFIG_TRANSPARENT_HUGEPAGE */
>
> /* Sanity check that a folio is fully exclusive */
> @@ -4777,6 +4958,7 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
> unsigned long page_idx;
> unsigned long address;
> pte_t *ptep;
> + bool fault_only_young = false;
>
> if (!pte_unmap_same(vmf))
> goto out;
> @@ -4845,13 +5027,22 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
> if (folio)
> swap_update_readahead(folio, vma, vmf->address);
> if (!folio) {
> - /* Swapin bypasses readahead for SWP_SYNCHRONOUS_IO devices */
> - if (data_race(si->flags & SWP_SYNCHRONOUS_IO))
> + /*
> + * Swapin bypasses readahead for SWP_SYNCHRONOUS_IO devices.
> + * The swap device is pinned while checking the flag, matching
> + * the existing fault path.
> + */
> + if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
> + unsigned long swapin_orders = thp_swapin_suitable_orders(vmf);
> + unsigned long locality_orders =
> + swapin_anon_locality_orders(vmf, swapin_orders);
> +
> folio = swapin_sync(entry, GFP_HIGHUSER_MOVABLE,
> - thp_swapin_suitable_orders(vmf) | BIT(0),
> - 0, vmf, NULL, 0);
> - else
> + swapin_orders | BIT(0),
> + locality_orders, vmf, NULL, 0);
> + } else {
> folio = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE, vmf);
> + }
>
> if (IS_ERR_OR_NULL(folio)) {
> /*
> @@ -5110,9 +5301,12 @@ vm_fault_t do_swap_page(struct vm_fault *vmf)
>
> VM_BUG_ON(!folio_test_anon(folio) ||
> (pte_write(pte) && !PageAnonExclusive(page)));
> - set_ptes(vma->vm_mm, address, ptep, pte, nr_pages);
> - arch_do_swap_page_nr(vma->vm_mm, vma, address,
> - pte, pte, nr_pages);
> + if (folio == swapcache && nr_pages == folio_nr_pages(folio) &&
> + arch_has_hw_pte_young())
> + fault_only_young = swapin_fault_only_young(folio);
> + set_swapin_ptes(vma, address, ptep, pte, nr_pages, page_idx,
> + fault_only_young);
> + arch_do_swap_page_nr(vma->vm_mm, vma, address, pte, pte, nr_pages);
>
> /*
> * Remove the swap entry and conditionally try to free up the swapcache.
> diff --git a/mm/swap.h b/mm/swap.h
> index dd35a310d06d..5d1c81ab49b9 100644
> --- a/mm/swap.h
> +++ b/mm/swap.h
> @@ -327,6 +327,7 @@ struct folio *swapin_readahead(swp_entry_t entry, gfp_t flag,
> struct folio *swapin_sync(swp_entry_t entry, gfp_t flag, unsigned long orders,
> unsigned long locality_orders, struct vm_fault *vmf,
> struct mempolicy *mpol, pgoff_t ilx);
> +bool swapin_fault_only_young(struct folio *folio);
> void swap_update_readahead(struct folio *folio, struct vm_area_struct *vma,
> unsigned long addr);
>
> @@ -430,6 +431,11 @@ static inline void swap_update_readahead(struct folio *folio,
> {
> }
>
> +static inline bool swapin_fault_only_young(struct folio *folio)
> +{
> + return false;
> +}
> +
> static inline int swap_writeout(struct folio *folio,
> struct swap_iocb **swap_plug)
> {
> diff --git a/mm/swap_state.c b/mm/swap_state.c
> index 5a4ca289009a..80dff6a1ee65 100644
> --- a/mm/swap_state.c
> +++ b/mm/swap_state.c
> @@ -747,6 +747,21 @@ static bool zswap_needs_order0_retry(struct folio *folio)
> ZSWAP_RANGE_MIXED;
> }
>
> +/*
> + * A speculative large swapin may install PTEs for pages that did not fault.
> + * Keep those non-faulting PTEs old so a later anon fault can report
> + * PTE-young density as caller-provided locality evidence without storing
> + * state in the VMA.
> + */
> +bool swapin_fault_only_young(struct folio *folio)
> +{
> + if (!folio_test_large(folio) || !folio_test_swapcache(folio))
> + return false;
> +
> + return zswap_probe_range(folio->swap, folio_nr_pages(folio)) ==
> + ZSWAP_RANGE_ALL_ZSWAP;
> +}
> +
> /*
> * If we are the only user, then try to free up the swap cache.
> *
> --
> 2.34.1
>