* [PATCH 1/4] mm: generalize vmemmap remap architecture support
2026-09-03 12:21 [PATCH 0/4] mm: Reduce struct page overhead for FS-DAX pmem Muchun Song
@ 2026-09-03 12:21 ` Muchun Song
2026-09-03 12:21 ` [PATCH 2/4] nvdimm/pmem: avoid HWPoison flag updates for clean pages Muchun Song
` (2 subsequent siblings)
3 siblings, 0 replies; 6+ messages in thread
From: Muchun Song @ 2026-09-03 12:21 UTC (permalink / raw)
To: Andrew Morton, Dan Williams, David Hildenbrand
Cc: linux-mm, nvdimm, linux-kernel, linux-fsdevel, linux-cxl,
Vishal Verma, Dave Jiang, Alison Schofield, Mike Rapoport,
Oscar Salvador, Ira Weiny, Jan Kara, Matthew Wilcox,
Lorenzo Stoakes, Vlastimil Babka, Michal Hocko, Qi Zheng,
Muchun Song, muchun.song
ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP is named after its first user, but
the underlying requirement is not HugeTLB-specific. Generic MM only needs
to know whether an architecture can safely remap populated vmemmap PTEs at
run time.
Rename the architecture opt-in to ARCH_SUPPORTS_VMEMMAP_REMAP and make
HugeTLB vmemmap optimization depend on that generic capability. This also
prepares for FS-DAX vmemmap optimization, which relies on the same runtime
remap support.
Signed-off-by: Muchun Song <songmuchun@bytedance.com>
---
arch/loongarch/Kconfig | 2 +-
arch/riscv/Kconfig | 2 +-
arch/x86/Kconfig | 2 +-
fs/Kconfig | 2 +-
mm/Kconfig | 6 ++++--
5 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/arch/loongarch/Kconfig b/arch/loongarch/Kconfig
index 9c5def706222..e9bf19172e86 100644
--- a/arch/loongarch/Kconfig
+++ b/arch/loongarch/Kconfig
@@ -72,6 +72,7 @@ config LOONGARCH
select ARCH_SUPPORTS_RT
select ARCH_SUPPORTS_SCHED_SMT if SMP
select ARCH_SUPPORTS_SCHED_MC if SMP
+ select ARCH_SUPPORTS_VMEMMAP_REMAP if 64BIT
select ARCH_USE_BUILTIN_BSWAP
select ARCH_USE_CMPXCHG_LOCKREF
select ARCH_USE_MEMTEST
@@ -80,7 +81,6 @@ config LOONGARCH
select ARCH_WANT_DEFAULT_BPF_JIT if HAVE_EBPF_JIT
select ARCH_WANT_DEFAULT_TOPDOWN_MMAP_LAYOUT
select ARCH_WANT_LD_ORPHAN_WARN
- select ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP if 64BIT
select ARCH_WANTS_NO_INSTR
select ARCH_WANTS_THP_SWAP if HAVE_ARCH_TRANSPARENT_HUGEPAGE
select BUILDTIME_TABLE_SORT
diff --git a/arch/riscv/Kconfig b/arch/riscv/Kconfig
index 5965666194b0..3614001cafdb 100644
--- a/arch/riscv/Kconfig
+++ b/arch/riscv/Kconfig
@@ -76,6 +76,7 @@ config RISCV
select ARCH_SUPPORTS_RT
select ARCH_SUPPORTS_SHADOW_CALL_STACK if HAVE_SHADOW_CALL_STACK
select ARCH_SUPPORTS_SCHED_MC if SMP
+ select ARCH_SUPPORTS_VMEMMAP_REMAP
select ARCH_USE_CMPXCHG_LOCKREF if 64BIT
select ARCH_USE_MEMTEST
select ARCH_USE_QUEUED_RWLOCKS
@@ -88,7 +89,6 @@ config RISCV
select ARCH_WANT_HUGE_PMD_SHARE if 64BIT
select ARCH_WANT_LD_ORPHAN_WARN
select ARCH_WANT_OPTIMIZE_DAX_VMEMMAP
- select ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP
select ARCH_WANTS_NO_INSTR
select ARCH_WANTS_THP_SWAP if HAVE_ARCH_TRANSPARENT_HUGEPAGE
select ARCH_WEAK_RELEASE_ACQUIRE if ARCH_USE_QUEUED_SPINLOCKS
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index a8c3b3d31a27..53f72d1500c5 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -133,6 +133,7 @@ config X86
select ARCH_SUPPORTS_LTO_CLANG
select ARCH_SUPPORTS_LTO_CLANG_THIN
select ARCH_SUPPORTS_RT
+ select ARCH_SUPPORTS_VMEMMAP_REMAP if X86_64
select ARCH_USE_BUILTIN_BSWAP
select ARCH_USE_CMPXCHG_LOCKREF
select ARCH_USE_MEMTEST
@@ -148,7 +149,6 @@ config X86
select ARCH_WANT_HUGE_PMD_SHARE if X86_64
select ARCH_WANT_LD_ORPHAN_WARN
select ARCH_WANT_OPTIMIZE_DAX_VMEMMAP if X86_64
- select ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP if X86_64
select ARCH_WANTS_THP_SWAP if X86_64
select ARCH_HAS_PARANOID_L1D_FLUSH
select ARCH_WANT_IRQS_OFF_ACTIVATE_MM
diff --git a/fs/Kconfig b/fs/Kconfig
index d1c210c6508f..655795fbbd1b 100644
--- a/fs/Kconfig
+++ b/fs/Kconfig
@@ -276,7 +276,7 @@ config HUGETLB_PAGE
config HUGETLB_PAGE_OPTIMIZE_VMEMMAP
def_bool HUGETLB_PAGE
- depends on ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP
+ depends on ARCH_SUPPORTS_VMEMMAP_REMAP
depends on SPARSEMEM_VMEMMAP
config HUGETLB_PMD_PAGE_TABLE_SHARING
diff --git a/mm/Kconfig b/mm/Kconfig
index c1ddf59c0d71..7ec734d89beb 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -463,12 +463,14 @@ config SPARSEMEM_VMEMMAP
#
# Select this config option from the architecture Kconfig, if it is preferred
-# to enable the feature of HugeTLB/dev_dax vmemmap optimization.
+# to enable the feature of dev_dax vmemmap optimization.
#
config ARCH_WANT_OPTIMIZE_DAX_VMEMMAP
bool
-config ARCH_WANT_OPTIMIZE_HUGETLB_VMEMMAP
+# Select this from architectures where generic MM can safely remap populated
+# vmemmap PTEs at run time.
+config ARCH_SUPPORTS_VMEMMAP_REMAP
bool
config HAVE_MEMBLOCK_PHYS_MAP
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH 3/4] mm: add shared read-only vmemmap support for FS-DAX
2026-09-03 12:21 [PATCH 0/4] mm: Reduce struct page overhead for FS-DAX pmem Muchun Song
2026-09-03 12:21 ` [PATCH 1/4] mm: generalize vmemmap remap architecture support Muchun Song
2026-09-03 12:21 ` [PATCH 2/4] nvdimm/pmem: avoid HWPoison flag updates for clean pages Muchun Song
@ 2026-09-03 12:21 ` Muchun Song
[not found] ` <20260903122128.12264-5-songmuchun@bytedance.com>
3 siblings, 0 replies; 6+ messages in thread
From: Muchun Song @ 2026-09-03 12:21 UTC (permalink / raw)
To: Andrew Morton, Dan Williams, David Hildenbrand
Cc: linux-mm, nvdimm, linux-kernel, linux-fsdevel, linux-cxl,
Vishal Verma, Dave Jiang, Alison Schofield, Mike Rapoport,
Oscar Salvador, Ira Weiny, Jan Kara, Matthew Wilcox,
Lorenzo Stoakes, Vlastimil Babka, Michal Hocko, Qi Zheng,
Muchun Song, muchun.song
FS-DAX registers persistent-memory ranges as ZONE_DEVICE memory, and the
kernel normally allocates and initializes vmemmap storage for every
advertised PFN up front. Sparse pmem images and workloads that only use the
DAX direct-access path may never need writable per-PFN state for most of
that range, but still pay the memory and initialization cost.
Add an opt-in dev_pagemap mode that populates FS-DAX vmemmap PTEs from a
shared read-only metadata page. The shared page is initialized with the
common ZONE_DEVICE and dev_pagemap state, so every PFN still has a valid
struct page representation while private metadata allocation is deferred.
This relies on sizeof(struct page) being a power of two, so each vmemmap
page contains a naturally aligned and repeatable set of struct page slots.
It also requires architecture support for runtime vmemmap remapping,
because shared mappings must be replaced with private writable pages before
a PFN can enter userspace mappings.
The initial implementation is deliberately limited to a single
memory-block-aligned range. That is not a fundamental requirement, but keeps
the registration and teardown paths simple; support for multiple ranges or
less strict alignment can be added later.
Provide vmemmap_materialize_page() to replace shared mappings in the
requested metadata range with private writable copies. A later patch will
call it from the FS-DAX fault path.
No caller enables the mode yet.
Signed-off-by: Muchun Song <songmuchun@bytedance.com>
---
include/linux/memremap.h | 11 ++++++-
mm/memremap.c | 38 ++++++++++++++++++++++--
mm/mm_init.c | 11 +++++++
mm/sparse-vmemmap.c | 64 +++++++++++++++++++++++++++++++++++++---
4 files changed, 116 insertions(+), 8 deletions(-)
diff --git a/include/linux/memremap.h b/include/linux/memremap.h
index e3c2ccf872a8..21c9b6aeef67 100644
--- a/include/linux/memremap.h
+++ b/include/linux/memremap.h
@@ -9,6 +9,7 @@
struct resource;
struct device;
+struct page;
/**
* struct vmem_altmap - pre-allocated storage for vmemmap_populate
@@ -108,7 +109,8 @@ struct dev_pagemap_ops {
void (*folio_split)(struct folio *head, struct folio *tail);
};
-#define PGMAP_ALTMAP_VALID (1 << 0)
+#define PGMAP_ALTMAP_VALID BIT(0)
+#define PGMAP_VMEMMAP_OPTIMIZATION BIT(1)
/**
* struct dev_pagemap - metadata for ZONE_DEVICE mappings
@@ -122,6 +124,7 @@ struct dev_pagemap_ops {
* A zero value (default) uses base pages as the vmemmap metadata
* representation. A bigger value will set up compound struct pages
* of the requested order value.
+ * @vmemmap_shared_page: shared read-only vmemmap page for optimized FS-DAX
* @ops: method table
* @owner: an opaque pointer identifying the entity that manages this
* instance. Used by various helpers to make sure that no
@@ -137,6 +140,7 @@ struct dev_pagemap {
enum memory_type type;
unsigned int flags;
unsigned long vmemmap_shift;
+ struct page *vmemmap_shared_page;
const struct dev_pagemap_ops *ops;
void *owner;
int nr_range;
@@ -232,6 +236,7 @@ void *devm_memremap_pages(struct device *dev, struct dev_pagemap *pgmap);
void devm_memunmap_pages(struct device *dev, struct dev_pagemap *pgmap);
struct dev_pagemap *get_dev_pagemap(unsigned long pfn);
bool pgmap_pfn_valid(struct dev_pagemap *pgmap, unsigned long pfn);
+int vmemmap_materialize_page(struct page *page, unsigned int order);
unsigned long memremap_compat_align(void);
@@ -307,4 +312,8 @@ static inline void put_dev_pagemap(struct dev_pagemap *pgmap)
percpu_ref_put(&pgmap->ref);
}
+static inline bool pgmap_vmemmap_optimizable(struct dev_pagemap *pgmap)
+{
+ return pgmap && pgmap->vmemmap_shared_page != NULL;
+}
#endif /* _LINUX_MEMREMAP_H_ */
diff --git a/mm/memremap.c b/mm/memremap.c
index accba23aef28..a53d09b84eaa 100644
--- a/mm/memremap.c
+++ b/mm/memremap.c
@@ -3,6 +3,7 @@
#include <linux/device.h>
#include <linux/io.h>
#include <linux/kasan.h>
+#include <linux/memory.h>
#include <linux/memory_hotplug.h>
#include <linux/memremap.h>
#include <linux/swap.h>
@@ -83,6 +84,30 @@ static unsigned long pfn_len(struct dev_pagemap *pgmap, unsigned long range_id)
pfn_first(pgmap, range_id)) >> pgmap->vmemmap_shift;
}
+static int pgmap_vmemmap_shared_page_alloc(struct dev_pagemap *pgmap, int nid)
+{
+ const struct range *range = &pgmap->range;
+
+ if (!is_power_of_2(sizeof(struct page)) ||
+ !IS_ENABLED(CONFIG_ARCH_SUPPORTS_VMEMMAP_REMAP) ||
+ !(pgmap->flags & PGMAP_VMEMMAP_OPTIMIZATION))
+ return 0;
+
+ if (pgmap->nr_range != 1 ||
+ !IS_ALIGNED(range->start | range_len(range), MIN_MEMORY_BLOCK_SIZE))
+ return 0;
+
+ pgmap->vmemmap_shared_page = alloc_pages_node(nid, GFP_KERNEL, 0);
+
+ return pgmap->vmemmap_shared_page ? 0 : -ENOMEM;
+}
+
+static inline void pgmap_vmemmap_shared_page_free(struct dev_pagemap *pgmap)
+{
+ if (pgmap->vmemmap_shared_page)
+ put_page(pgmap->vmemmap_shared_page);
+}
+
static void pageunmap_range(struct dev_pagemap *pgmap, int range_id)
{
struct range *range = &pgmap->ranges[range_id];
@@ -93,8 +118,9 @@ static void pageunmap_range(struct dev_pagemap *pgmap, int range_id)
/* pages are dead and unused, undo the arch mapping */
mem_hotplug_begin();
- remove_pfn_range_from_zone(page_zone(first_page), PHYS_PFN(range->start),
- PHYS_PFN(range_len(range)));
+ if (!pgmap_vmemmap_optimizable(pgmap))
+ remove_pfn_range_from_zone(page_zone(first_page), PHYS_PFN(range->start),
+ PHYS_PFN(range_len(range)));
if (pgmap->type == MEMORY_DEVICE_PRIVATE) {
__remove_pages(PHYS_PFN(range->start),
PHYS_PFN(range_len(range)), NULL, pgmap);
@@ -123,6 +149,7 @@ void memunmap_pages(struct dev_pagemap *pgmap)
for (i = 0; i < pgmap->nr_range; i++)
pageunmap_range(pgmap, i);
+ pgmap_vmemmap_shared_page_free(pgmap);
percpu_ref_exit(&pgmap->ref);
WARN_ONCE(pgmap->altmap.alloc, "failed to free all reserved pages\n");
@@ -310,6 +337,9 @@ void *memremap_pages(struct dev_pagemap *pgmap, int nid)
break;
case MEMORY_DEVICE_FS_DAX:
params.pgprot = pgprot_decrypted(params.pgprot);
+ error = pgmap_vmemmap_shared_page_alloc(pgmap, nid);
+ if (error)
+ return ERR_PTR(error);
break;
case MEMORY_DEVICE_GENERIC:
break;
@@ -324,8 +354,10 @@ void *memremap_pages(struct dev_pagemap *pgmap, int nid)
init_completion(&pgmap->done);
error = percpu_ref_init(&pgmap->ref, dev_pagemap_percpu_release, 0,
GFP_KERNEL);
- if (error)
+ if (error) {
+ pgmap_vmemmap_shared_page_free(pgmap);
return ERR_PTR(error);
+ }
/*
* Clear the pgmap nr_range as it will be incremented for each
diff --git a/mm/mm_init.c b/mm/mm_init.c
index 2ed17cc707ed..7dd03b8a8d28 100644
--- a/mm/mm_init.c
+++ b/mm/mm_init.c
@@ -33,6 +33,7 @@
#include <linux/vmstat.h>
#include <linux/kexec_handover.h>
#include <linux/hugetlb.h>
+#include <linux/memremap.h>
#include "internal.h"
#include "mm_init.h"
#include "page_alloc.h"
@@ -1133,6 +1134,15 @@ void __ref memmap_init_zone_device(struct zone *zone,
if (!nr_pages)
return;
+ if (pgmap_vmemmap_optimizable(pgmap)) {
+ struct page *page = page_address(pgmap->vmemmap_shared_page);
+
+ for (int i = 0; i < PAGE_SIZE / sizeof(struct page); i++)
+ __init_zone_device_page(page + i, start_pfn + i,
+ ZONE_DEVICE, nid, pgmap);
+ goto pageblock_init;
+ }
+
/*
* Seed the reusable head-page template from the first real struct
* page. The normal page-init and refcount helpers must operate on
@@ -1164,6 +1174,7 @@ void __ref memmap_init_zone_device(struct zone *zone,
compound_nr_pages(pfn, altmap, pgmap));
}
+pageblock_init:
pageblock_migratetype_init_range(start_pfn, nr_pages, MIGRATE_MOVABLE,
/* isolate */ false, /* atomic */ false);
diff --git a/mm/sparse-vmemmap.c b/mm/sparse-vmemmap.c
index e62e6aa07f12..4dc7f020ed10 100644
--- a/mm/sparse-vmemmap.c
+++ b/mm/sparse-vmemmap.c
@@ -37,6 +37,8 @@
*/
/* Get a ref on the head page struct page, for ZONE_DEVICE compound pages */
#define VMEMMAP_POPULATE_PAGEREF 0x0001
+/* Read-only shared vmemmap mappings for FS-DAX base pages */
+#define VMEMMAP_POPULATE_FSDAX_SHARED 0x0002
#include "internal.h"
#include "mm_init.h"
@@ -262,10 +264,11 @@ static pte_t * __meminit vmemmap_pte_populate(pmd_t *pmd, unsigned long addr, in
* and through vmemmap_populate_compound_pages() when
* slab is available.
*/
- if (flags & VMEMMAP_POPULATE_PAGEREF)
+ if (flags & (VMEMMAP_POPULATE_PAGEREF | VMEMMAP_POPULATE_FSDAX_SHARED))
get_page(pfn_to_page(ptpfn));
}
- entry = pfn_pte(ptpfn, PAGE_KERNEL);
+ entry = pfn_pte(ptpfn, flags & VMEMMAP_POPULATE_FSDAX_SHARED ?
+ PAGE_KERNEL_RO : PAGE_KERNEL);
set_pte_at(&init_mm, addr, pte, entry);
} else if (WARN_ON_ONCE(vmemmap_optimizable_pfn(pfn)))
return NULL;
@@ -379,6 +382,54 @@ int __meminit vmemmap_populate_basepages(unsigned long start, unsigned long end,
return vmemmap_populate_range(start, end, node, altmap, -1, 0);
}
+#ifdef CONFIG_ZONE_DEVICE
+static int __vmemmap_materialize_page(struct page *page)
+{
+ unsigned long addr = PAGE_ALIGN_DOWN((unsigned long)page);
+ struct dev_pagemap *pgmap = page_pgmap(page);
+ struct page *candidate, *template = pgmap->vmemmap_shared_page;
+ pte_t *pte = virt_to_kpte(addr);
+
+ if (pte_page(ptep_get(pte)) != template)
+ return 0;
+
+ candidate = alloc_pages_node(page_to_nid(page), GFP_KERNEL, 0);
+ if (!candidate)
+ return -ENOMEM;
+ copy_page(page_address(candidate), page_address(template));
+
+ spin_lock(&init_mm.page_table_lock);
+ if (pte_page(ptep_get(pte)) != template) {
+ __free_page(candidate);
+ goto out;
+ }
+ /* Make the copied struct page contents visible before the PTE update. */
+ smp_wmb();
+ set_pte_at(&init_mm, addr, pte, mk_pte(candidate, PAGE_KERNEL));
+ flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
+ put_page(template);
+out:
+ spin_unlock(&init_mm.page_table_lock);
+
+ return 0;
+}
+
+int vmemmap_materialize_page(struct page *page, unsigned int order)
+{
+ struct dev_pagemap *pgmap = page_pgmap(page);
+ unsigned long end = (unsigned long)(page + (1UL << order));
+
+ if (!pgmap_vmemmap_optimizable(pgmap))
+ return 0;
+
+ for (unsigned long addr = (unsigned long)page; addr < end; addr += PAGE_SIZE)
+ if (__vmemmap_materialize_page((struct page *)addr))
+ return -ENOMEM;
+
+ return 0;
+}
+#endif /* CONFIG_ZONE_DEVICE */
+
/*
* Write protect the mirrored tail page structs for HVO. This will be
* called from the hugetlb code when gathering and initializing the
@@ -581,7 +632,11 @@ struct page * __meminit __populate_section_memmap(unsigned long pfn,
!IS_ALIGNED(nr_pages, PAGES_PER_SUBSECTION)))
return NULL;
- if (vmemmap_can_optimize(altmap, pgmap))
+ if (pgmap_vmemmap_optimizable(pgmap))
+ r = vmemmap_populate_range(start, end, nid, NULL,
+ page_to_pfn(pgmap->vmemmap_shared_page),
+ VMEMMAP_POPULATE_FSDAX_SHARED);
+ else if (vmemmap_can_optimize(altmap, pgmap))
r = vmemmap_populate_compound_pages(pfn, start, end, nid, pgmap);
else
r = vmemmap_populate(start, end, nid, altmap);
@@ -887,7 +942,8 @@ int __meminit sparse_add_section(int nid, unsigned long start_pfn,
* Poison uninitialized struct pages in order to catch invalid flags
* combinations.
*/
- page_init_poison(memmap, sizeof(struct page) * nr_pages);
+ if (!pgmap_vmemmap_optimizable(pgmap))
+ page_init_poison(memmap, sizeof(struct page) * nr_pages);
ms = __nr_to_section(section_nr);
__section_mark_present(ms, section_nr);
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread