Skip to contentSkip to conversation

s390/mm: Batch PTE updates in lazy MMU mode

RFC PATCH Superseded Alexander Gordeev 3 participants 17 messages Mar 25 – Apr 14

Subsystems (8)

v1 of 8 Superseded Latest is v8 →

lore
Share & tools
lore

Alexander Gordeev

author maintainer
#0 lore raw

cover s390/mm: Batch PTE updates in lazy MMU mode

Hi All!

This series addresses an s390-specific aspect of how page table entries are modified. In many cases, changing a valid PTE (for example, setting or clearing a hardware bit) requires issuing an Invalidate Page Table Entry (IPTE) instruction beforehand.

A disadvantage of the IPTE instruction is that it may initiate a machine-wide quiesce state. This state acts as an expensive global hardware lock and should be avoided whenever possible.

Currently, IPTE is invoked for each individual PTE update in most code paths. However, the instruction itself supports invalidating multiple PTEs at once, covering up to 256 entries. Using this capability can significantly reduce the number of quiesce events, with a positive impact on overall system performance. At present, this feature is not utilized.

An effort was therefore made to identify kernel code paths that update large numbers of consecutive PTEs. Such updates can be batched and handled by a single IPTE invocation, leveraging the hardware support described above.

A natural candidate for this optimization is page-table walkers that change attributes of memory ranges and thus modify contiguous ranges of PTEs. Many memory-management system calls enter lazy MMU mode while updating such ranges.

This lazy MMU mode can be leveraged to build on the already existing infrastructure and implement a software-level lazy MMU mechanism, allowing expensive PTE invalidations on s390 to be batched.

Patch #2 is the actual s390 rework, while patch #1 is a prerequisite that affects the generic code. It was briefly communicated upstream and got some approvals:

https://lore.kernel.org/linux-mm/[email protected]/#t
https://lore.kernel.org/linux-mm/[email protected]/

I am sending this as an RFC in the hope that I may have missed less obvious existing code paths or potential code changes that could further enable batching. Any ideas would be greatly appreciated!

Thanks!

Alexander Gordeev (2):
mm: make lazy MMU mode context-aware
s390/mm: Batch PTE updates in lazy MMU mode

 arch/s390/Kconfig               |   8 +
 arch/s390/include/asm/pgtable.h | 209 +++++++++++++++--
 arch/s390/mm/Makefile           |   1 +
 arch/s390/mm/ipte_batch.c       | 396 ++++++++++++++++++++++++++++++++
 arch/s390/mm/pgtable.c          |   8 +-
 fs/proc/task_mmu.c              |   2 +-
 include/linux/pgtable.h         |  42 ++++
 mm/madvise.c                    |   8 +-
 mm/memory.c                     |   8 +-
 mm/mprotect.c                   |   2 +-
 mm/mremap.c                     |   2 +-
 mm/vmalloc.c                    |   6 +-
 12 files changed, 659 insertions(+), 33 deletions(-)
 create mode 100644 arch/s390/mm/ipte_batch.c
Signature · 1 line
2.51.0
author maintainer
#1 lore raw

RFC PATCH 1/2 mm: make lazy MMU mode context-aware

Lazy MMU mode is assumed to be context-independent, in the sense that it does not need any additional information while operating. However, the s390 architecture benefits from knowing the exact page table entries being modified.

Introduce lazy_mmu_mode_enable_pte(), which is provided with the process address space and the page table being operated on. This information is required to enable s390-specific optimizations.

The function takes parameters that are typically passed to page- table level walkers, which implies that the span of PTE entries never crosses a page table boundary.

Architectures that do not require such information simply do not need to define the arch_enter_lazy_mmu_mode_pte() callback.

---
 fs/proc/task_mmu.c      |  2 +-
 include/linux/pgtable.h | 42 +++++++++++++++++++++++++++++++++++++++++
 mm/madvise.c            |  8 ++++----
 mm/memory.c             |  8 ++++----
 mm/mprotect.c           |  2 +-
 mm/mremap.c             |  2 +-
 mm/vmalloc.c            |  6 +++---
 7 files changed, 56 insertions(+), 14 deletions(-)
fs/proc/task_mmu.c +1 -1
index e091931d7ca1..4e3b1987874a 100644
@@ -2752,7 +2752,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,
 		return 0;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(vma->vm_mm, start, end, start_pte);
 
 	if ((p->arg.flags & PM_SCAN_WP_MATCHING) && !p->vec_out) {
 		/* Fast path for performing exclusive WP */
include/linux/pgtable.h +42 -0

54 lines not on this page · raw message

mm/madvise.c +4 -4
index dbb69400786d..02edc80f678b 100644
@@ -451,7 +451,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, addr, end, start_pte);
 	for (; addr < end; pte += nr, addr += nr * PAGE_SIZE) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -506,7 +506,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_pte(mm, addr, end, start_pte);
 				if (!err)
 					nr = 0;
 				continue;
@@ -673,7 +673,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, addr, end, start_pte);
 	for (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -733,7 +733,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_pte(mm, addr, end, pte);
 				if (!err)
 					nr = 0;
 				continue;
mm/memory.c +4 -4
index 2f815a34d924..43fa9965fb5f 100644
@@ -1269,7 +1269,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
 	orig_src_pte = src_pte;
 	orig_dst_pte = dst_pte;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(src_mm, addr, end, src_pte);
 
 	do {
 		nr = 1;
@@ -1917,7 +1917,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
 		return addr;
 
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, addr, end, start_pte);
 	do {
 		bool any_skipped = false;
 
@@ -2875,7 +2875,7 @@ static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
 	mapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
 	if (!pte)
 		return -ENOMEM;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, addr, end, mapped_pte);
 	do {
 		BUG_ON(!pte_none(ptep_get(pte)));
 		if (!pfn_modify_allowed(pfn, prot)) {
@@ -3235,7 +3235,7 @@ static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,
 			return -EINVAL;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, addr, end, mapped_pte);
 
 	if (fn) {
 		do {
mm/mprotect.c +1 -1
index c0571445bef7..43a2a65b8caf 100644
@@ -233,7 +233,7 @@ static long change_pte_range(struct mmu_gather *tlb,
 		is_private_single_threaded = vma_is_single_threaded_private(vma);
 
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(vma->vm_mm, addr, end, pte);
 	do {
 		nr_ptes = 1;
 		oldpte = ptep_get(pte);
mm/mremap.c +1 -1
index 2be876a70cc0..ac7f649f3aad 100644
@@ -260,7 +260,7 @@ static int move_ptes(struct pagetable_move_control *pmc,
 	if (new_ptl != old_ptl)
 		spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(mm, old_addr, old_end, old_ptep);
 
 	for (; old_addr < old_end; old_ptep += nr_ptes, old_addr += nr_ptes * PAGE_SIZE,
 		new_ptep += nr_ptes, new_addr += nr_ptes * PAGE_SIZE) {
mm/vmalloc.c +3 -3
index 61caa55a4402..5e702bcf03fd 100644
@@ -108,7 +108,7 @@ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(&init_mm, addr, end, pte);
 
 	do {
 		if (unlikely(!pte_none(ptep_get(pte)))) {
@@ -371,7 +371,7 @@ static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	unsigned long size = PAGE_SIZE;
 
 	pte = pte_offset_kernel(pmd, addr);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(&init_mm, addr, end, pte);
 
 	do {
 #ifdef CONFIG_HUGETLB_PAGE
@@ -538,7 +538,7 @@ static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(&init_mm, addr, end, pte);
 
 	do {
 		struct page *page = pages[*nr];
Signature · 1 line
2.51.0
#2 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 45 lines · Alexander Gordeev

Lazy MMU mode is assumed to be context-independent, in the sense that it does not need any additional information while operating. However, the s390 architecture benefits from knowing the exact page table entries being modified.

Introduce lazy_mmu_mode_enable_pte(), which is provided with the process address space and the page table being operated on. This information is required to enable s390-specific optimizations.

The function takes parameters that are typically passed to page- table level walkers, which implies that the span of PTE entries never crosses a page table boundary.

Architectures that do not require such information simply do not need to define the arch_enter_lazy_mmu_mode_pte() callback.

---
 fs/proc/task_mmu.c      |  2 +-
 include/linux/pgtable.h | 42 +++++++++++++++++++++++++++++++++++++++++
 mm/madvise.c            |  8 ++++----
 mm/memory.c             |  8 ++++----
 mm/mprotect.c           |  2 +-
 mm/mremap.c             |  2 +-
 mm/vmalloc.c            |  6 +++---
 7 files changed, 56 insertions(+), 14 deletions(-)
fs/proc/task_mmu.c +1 -1
index e091931d7ca1..4e3b1987874a 100644
@@ -2752,7 +2752,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,
 		return 0;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_pte(vma->vm_mm, start, end, start_pte);
 
 	if ((p->arg.flags & PM_SCAN_WP_MATCHING) && !p->vec_out) {
 		/* Fast path for performing exclusive WP */
include/linux/pgtable.h +0 -0
index a50df42a893f..481b45954800 100644

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 278

Quoted 8 lines
include/linux/pgtable.h +5 -0
@@ -271,6 +271,44 @@ static inline void lazy_mmu_mode_enable(void)
 		arch_enter_lazy_mmu_mode();
 }
 
+#ifndef arch_enter_lazy_mmu_mode_pte
+static inline void arch_enter_lazy_mmu_mode_pte(struct mm_struct *mm,
+						unsigned long addr,
+						unsigned long end,
+						pte_t *ptep)

Two tab alignment please. (applies to other things hwere as well)

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 285

Quoted 7 lines
include/linux/pgtable.h +7 -0
+{
+	arch_enter_lazy_mmu_mode();
+}
+#endif
+
+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

Quoted 4 lines
include/linux/pgtable.h +4 -0
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_pte().
+ *

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 299

Quoted 10 lines
include/linux/pgtable.h +10 -0
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */
+static inline void lazy_mmu_mode_enable_pte(struct mm_struct *mm,
+					    unsigned long addr,
+					    unsigned long end,
+					    pte_t *ptep)

It can be multiple ptes, so should this be some kind of "pte_range"/

lazy_mmu_mode_enable_for_pte_range()

A bit mouthful but clearer.

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 310

Quoted 11 lines
include/linux/pgtable.h +11 -0
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_pte(mm, addr, end, ptep);
+}

I'm wondering whether that could instead be some optional interface that we trigger after the lazy_mmu_mode_enable. But looking at lazy_mmu_mode_enable() users, there don't seem to be cases where we would process multiple different ranges under a single enable() call, right?

Signature · 3 lines
Cheers,

David
author maintainer
#3 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

On Wed, Mar 25, 2026 at 10:55:23AM +0100, David Hildenbrand (Arm) wrote:

Hi David,

Quoted 6 lines
Quoted 2 lines

+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Whether all or portion of PTEs were actually updated is not defined, just like in case of lazy_mmu_mode_enable_pte().

Makes sense?

Quoted 20 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 299

Quoted 13 lines

+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_pte().
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */

include/linux/pgtable.h +4 -0
+static inline void lazy_mmu_mode_enable_pte(struct mm_struct *mm,
+					    unsigned long addr,
+					    unsigned long end,
+					    pte_t *ptep)

It can be multiple ptes, so should this be some kind of "pte_range"/

lazy_mmu_mode_enable_for_pte_range()

A bit mouthful but clearer.

Quoted 10 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 309

Quoted 10 lines
include/linux/pgtable.h +10 -0
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_pte(mm, addr, end, ptep);

I will also change arch_enter_lazy_mmu_mode_pte() to arch_enter_lazy_mmu_mode_for_pte_range() then.

Quoted 4 lines
Quoted 1 line

+}

I'm wondering whether that could instead be some optional interface that we trigger after the lazy_mmu_mode_enable. But looking at

To me just two separate and (as you put it) mouthful names appeal better than an optional follow-up interface.

Quoted 2 lines

lazy_mmu_mode_enable() users, there don't seem to be cases where we would process multiple different ranges under a single enable() call, right?

Multiple different ranges still could be processed, but then one should continue using arch_enter_lazy_mmu_mode(). E.g. these were less obvious than traditional walkers and left them intact:

mm/migrate_device.c
mm/tests/lazy_mmu_mode_kunit.c
mm/userfaultfd.c
mm/vmscan.c

Quoted 4 lines
Signature · 3 lines
Cheers,

David

Thanks for the quick review!

author maintainer
#4 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 2 lines

Multiple different ranges still could be processed, but then one should continue using arch_enter_lazy_mmu_mode().

lazy_mmu_mode_enable()

#5 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

In reply to Alexander Gordeev
Quoted 9 lines · Alexander Gordeev

On Wed, Mar 25, 2026 at 10:55:23AM +0100, David Hildenbrand (Arm) wrote:

Hi David,

Quoted 5 lines
Quoted 2 lines

+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

Quoted 7 lines

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Whether all or portion of PTEs were actually updated is not defined, just like in case of lazy_mmu_mode_enable_pte().

Makes sense?

I also feel that the comment needs to be much more specific. From a brief glance at patch 2, it seems that __ipte_batch_set_pte() assumes that all PTEs processed after this function is called are contiguous. This should be documented.

Quoted 19 lines
Quoted 19 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 299

Quoted 13 lines

+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_pte().
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */

include/linux/pgtable.h +4 -0
+static inline void lazy_mmu_mode_enable_pte(struct mm_struct *mm,
+					    unsigned long addr,
+					    unsigned long end,
+					    pte_t *ptep)

It can be multiple ptes, so should this be some kind of "pte_range"/

lazy_mmu_mode_enable_for_pte_range()

A bit mouthful but clearer.

Quoted 12 lines
Quoted 10 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 309

Quoted 10 lines
include/linux/pgtable.h +10 -0
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_pte(mm, addr, end, ptep);

I will also change arch_enter_lazy_mmu_mode_pte() to arch_enter_lazy_mmu_mode_for_pte_range() then.

Makes sense. The interface looks reasonable to me with this new name.

One more comment though: in previous discussions you mentioned the need for arch_{pause,resume} hooks, is that no longer necessary simply because {pause,resume} are not used on the paths where you make use of the new enable function?

- Kevin

author maintainer
#6 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 13 lines · Kevin Brodsky
Quoted 12 lines · Alexander Gordeev

On Wed, Mar 25, 2026 at 10:55:23AM +0100, David Hildenbrand (Arm) wrote:

Hi David,

Quoted 5 lines
Quoted 2 lines

+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Quoted 8 lines
Quoted 4 lines

Whether all or portion of PTEs were actually updated is not defined, just like in case of lazy_mmu_mode_enable_pte().

Makes sense?

I also feel that the comment needs to be much more specific. From a brief glance at patch 2, it seems that __ipte_batch_set_pte() assumes that all PTEs processed after this function is called are contiguous.

No, this is actually not the case. __ipte_batch_set_pte() just sets ceilings for later processing. The PTEs within the range could be updated in any order and not necessarily all of them.

Quoted 1 line

This should be documented.

Will do.

Quoted 9 lines
Quoted 2 lines

I will also change arch_enter_lazy_mmu_mode_pte() to arch_enter_lazy_mmu_mode_for_pte_range() then.

Makes sense. The interface looks reasonable to me with this new name.

One more comment though: in previous discussions you mentioned the need for arch_{pause,resume} hooks, is that no longer necessary simply because {pause,resume} are not used on the paths where you make use of the new enable function?

Yes. I did implement arch_pause|resume_lazy_mmu_mode() for a custom KASAN sanitizer to catch direct PTE dereferences - ones that bypass ptep_get()/set_pte() in lazy mode.

But that code is not upstreamed and therefore there is no need to introduce the hooks just right now.

Quoted 1 line

- Kevin

Thanks for the review, Kevin!

#7 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 17 lines · Alexander Gordeev
Quoted 16 lines · Kevin Brodsky
Quoted 15 lines · Alexander Gordeev

On Wed, Mar 25, 2026 at 10:55:23AM +0100, David Hildenbrand (Arm) wrote:

Hi David,

Quoted 5 lines
Quoted 2 lines

+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Whether all or portion of PTEs were actually updated is not defined, just like in case of lazy_mmu_mode_enable_pte().

Quoted 7 lines
Quoted 4 lines
Quoted 1 line

Makes sense?

I also feel that the comment needs to be much more specific. From a brief glance at patch 2, it seems that __ipte_batch_set_pte() assumes that all PTEs processed after this function is called are contiguous.

No, this is actually not the case. __ipte_batch_set_pte() just sets ceilings for later processing. The PTEs within the range could be updated in any order and not necessarily all of them.

Fair enough, I suppose what I tried to say is that all PTEs must be in a certain range :)

Quoted 7 lines
Quoted 1 line

This should be documented.

Will do.

Quoted 4 lines
Quoted 2 lines

I will also change arch_enter_lazy_mmu_mode_pte() to arch_enter_lazy_mmu_mode_for_pte_range() then.

Makes sense. The interface looks reasonable to me with this new name.

Quoted 7 lines
Quoted 4 lines

One more comment though: in previous discussions you mentioned the need for arch_{pause,resume} hooks, is that no longer necessary simply because {pause,resume} are not used on the paths where you make use of the new enable function?

Yes. I did implement arch_pause|resume_lazy_mmu_mode() for a custom KASAN sanitizer to catch direct PTE dereferences - ones that bypass ptep_get()/set_pte() in lazy mode.

That sounds pretty useful!

Quoted 2 lines

But that code is not upstreamed and therefore there is no need to introduce the hooks just right now.

Ack that makes sense.

Quoted 2 lines
Quoted 1 line

- Kevin

Thanks for the review, Kevin!

No problem :)

- Kevin

#8 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

In reply to Alexander Gordeev
Quoted 7 lines · Alexander Gordeev

On Wed, Mar 25, 2026 at 10:55:23AM +0100, David Hildenbrand (Arm) wrote:

Hi David,

Quoted 3 lines
Quoted 2 lines

+/**
+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

Quoted 6 lines
Quoted 3 lines

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Probably phrase it stronger. "No ptes outside of this range must be updated" etc.

Quoted 3 lines

Whether all or portion of PTEs were actually updated is not defined, just like in case of lazy_mmu_mode_enable_pte().

Okay, then let's document that.

Quoted 3 lines

Makes sense?

Yes.

Quoted 34 lines
Quoted 20 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 299

Quoted 13 lines

+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_pte().
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */

include/linux/pgtable.h +4 -0
+static inline void lazy_mmu_mode_enable_pte(struct mm_struct *mm,
+					    unsigned long addr,
+					    unsigned long end,
+					    pte_t *ptep)

It can be multiple ptes, so should this be some kind of "pte_range"/

lazy_mmu_mode_enable_for_pte_range()

A bit mouthful but clearer.

Quoted 10 lines

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 309

Quoted 10 lines
include/linux/pgtable.h +10 -0
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_pte(mm, addr, end, ptep);

I will also change arch_enter_lazy_mmu_mode_pte() to arch_enter_lazy_mmu_mode_for_pte_range() then.

Quoted 7 lines
Quoted 4 lines
Quoted 1 line

+}

I'm wondering whether that could instead be some optional interface that we trigger after the lazy_mmu_mode_enable. But looking at

To me just two separate and (as you put it) mouthful names appeal better than an optional follow-up interface.

Yes, probably better.

Signature · 3 lines
Cheers,

David
author maintainer
#9 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 7 lines · David Hildenbrand (Arm)
Quoted 7 lines
Quoted 6 lines
Quoted 2 lines

+ * lazy_mmu_mode_enable_pte() - Enable the lazy MMU mode with parameters

You have to be a lot clearer about implications. For example, what happens if we would bail out and not process all ptes? What are the exact semantics.

Quoted 5 lines
Quoted 2 lines

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Probably phrase it stronger. "No ptes outside of this range must be updated" etc.

That turns out to be bit more complicated. The below cases do not fit such a strong requirement:

1. copy_pte_range() operates on two ranges: source and destination. Though lazy_mmu_mode_enable_for_pte_range() applies to the source one, updates to the destination are still happen while in tha lazy mode. (Although the lazy mode is not actually needed for the destination unattached MM).

2. move_ptes() also operates on a source and destination ranges, but unlike copy_pte_range() the destination range is also attached to the currently active task.

3. Though theoretical, nesting sections with interleaving calls to lazy_mmu_mode_enable() and lazy_mmu_mode_enable_for_pte_range() make it difficult to define (let alone to implement) which range is currently active, if any.

All of these goes away if we switch from for_pte_range() to fast_pte_range() semantics:

/**
* lazy_mmu_mode_enable_fast_pte_range() - Enable the lazy MMU mode with fast updates.
* @mm: Address space the ptes represent.
* @addr: Address of the first pte.
* @end: End address of the range.
* @ptep: Page table pointer for the first entry.
*
* Enters a new lazy MMU mode section and allows fast updates for PTEs
* within the specified range, while PTEs outside of the range are
* updated in the normal way - as if lazy_mmu_mode_enable() was called;
* if lazy MMU mode was not already enabled, enables it and calls
* arch_enter_lazy_mmu_mode_fast_pte_range(); if the mode was already
* enabled, the provided PTE range is ignored.
*
* The PTE range must belong to the provided memory space and must
* not cross a page table boundary.
*
* There are no requirements on the order or range completeness of PTE updates. *
* Must be paired with a call to lazy_mmu_mode_disable().
*
* Has no effect if called:
* - while paused (see lazy_mmu_mode_pause())
* - in interrupt context
*/

Thoughts?

Thanks!

#10 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 8 lines · Alexander Gordeev
Quoted 6 lines · David Hildenbrand (Arm)
Quoted 3 lines

The only implication is "only this address/PTE range could be updated and that range may span one page table at most".

Probably phrase it stronger. "No ptes outside of this range must be updated" etc.

Quoted 8 lines

That turns out to be bit more complicated. The below cases do not fit such a strong requirement:

1. copy_pte_range() operates on two ranges: source and destination. Though lazy_mmu_mode_enable_for_pte_range() applies to the source one, updates to the destination are still happen while in tha lazy mode. (Although the lazy mode is not actually needed for the destination unattached MM).

So, here a

"No ptes outside of this range in the provided @mm must be updated."

could be used.

Quoted 4 lines

2. move_ptes() also operates on a source and destination ranges, but unlike copy_pte_range() the destination range is also attached to the currently active task.

But not here.

Quoted 5 lines

3. Though theoretical, nesting sections with interleaving calls to lazy_mmu_mode_enable() and lazy_mmu_mode_enable_for_pte_range() make it difficult to define (let alone to implement) which range is currently active, if any.

Right. I assume you would specify the source here as well, or which one would it be in your case to speed it up?

Quoted 3 lines

All of these goes away if we switch from for_pte_range() to fast_pte_range() semantics:

I don't quite like the "fast" in there. I think you can keep the old name, but clarifying that it is merely a hint, and only ptes that fall into the hint might observe a speedup.

Could performance benefit from multiple ranges? (like in mremap, for example)?

In that case, an explicit hint interface could be reconsidered.

Signature · 3 lines
Cheers,

David
author maintainer
#11 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 10 lines · David Hildenbrand (Arm)
Quoted 5 lines

1. copy_pte_range() operates on two ranges: source and destination. Though lazy_mmu_mode_enable_for_pte_range() applies to the source one, updates to the destination are still happen while in tha lazy mode. (Although the lazy mode is not actually needed for the destination unattached MM).

So, here a

"No ptes outside of this range in the provided @mm must be updated."

Quoted 8 lines

could be used.

Quoted 4 lines

2. move_ptes() also operates on a source and destination ranges, but unlike copy_pte_range() the destination range is also attached to the currently active task.

But not here.

I did not quite understand these two comments ;), but I think I address them further below.

Quoted 7 lines
Quoted 4 lines

3. Though theoretical, nesting sections with interleaving calls to lazy_mmu_mode_enable() and lazy_mmu_mode_enable_for_pte_range() make it difficult to define (let alone to implement) which range is currently active, if any.

Right. I assume you would specify the source here as well, or which one would it be in your case to speed it up?

It is in all cases the source/old/existing one.

Quoted 6 lines
Quoted 2 lines

All of these goes away if we switch from for_pte_range() to fast_pte_range() semantics:

I don't quite like the "fast" in there. I think you can keep the old name, but clarifying that it is merely a hint, and only ptes that fall into the hint might observe a speedup.

Okay, that simplify things.

Quoted 2 lines

Could performance benefit from multiple ranges? (like in mremap, for example)?

No.

Quoted 1 line

In that case, an explicit hint interface could be reconsidered.

So all things considered, how does it look?

/**
* lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
* @mm: Address space the ptes represent.
* @addr: Address of the first pte.
* @end: End address of the range.
* @ptep: Page table pointer for the first entry.
*
* Enters a new lazy MMU mode section; if the mode was not already enabled,
* enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
*
* PTEs that fall within the specified range might observe update speedups.
* The PTE range must belong to the specified memory space and do not cross
* a page table boundary.
*
* There are no requirements on the order or range completeness of PTE
* updates for the specified range.
*
* Must be paired with a call to lazy_mmu_mode_disable().
*
* Has no effect if called:
* - While paused - see lazy_mmu_mode_pause()
* - In interrupt context
*/

Quoted 4 lines
Signature · 3 lines
Cheers,

David

Thanks!

#12 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 14 lines · Alexander Gordeev
Quoted 13 lines · David Hildenbrand (Arm)
Quoted 5 lines

1. copy_pte_range() operates on two ranges: source and destination. Though lazy_mmu_mode_enable_for_pte_range() applies to the source one, updates to the destination are still happen while in tha lazy mode. (Although the lazy mode is not actually needed for the destination unattached MM).

So, here a

"No ptes outside of this range in the provided @mm must be updated."

could be used.

Quoted 1 line
Quoted 8 lines
Quoted 5 lines
Quoted 3 lines

2. move_ptes() also operates on a source and destination ranges, but unlike copy_pte_range() the destination range is also attached to the currently active task.

But not here.

I did not quite understand these two comments ;), but I think I address them further below.

I'm saying that the second case is the problematic one ;)

Quoted 10 lines
Quoted 8 lines
Quoted 5 lines

3. Though theoretical, nesting sections with interleaving calls to lazy_mmu_mode_enable() and lazy_mmu_mode_enable_for_pte_range() make it difficult to define (let alone to implement) which range is currently active, if any.

Right. I assume you would specify the source here as well, or which one would it be in your case to speed it up?

It is in all cases the source/old/existing one.

Make sense.

Quoted 34 lines
Quoted 7 lines
Quoted 3 lines

All of these goes away if we switch from for_pte_range() to fast_pte_range() semantics:

I don't quite like the "fast" in there. I think you can keep the old name, but clarifying that it is merely a hint, and only ptes that fall into the hint might observe a speedup.

Okay, that simplify things.

Quoted 2 lines

Could performance benefit from multiple ranges? (like in mremap, for example)?

No.

Quoted 1 line

In that case, an explicit hint interface could be reconsidered.

So all things considered, how does it look?

/**
* lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
* @mm: Address space the ptes represent.
* @addr: Address of the first pte.
* @end: End address of the range.
* @ptep: Page table pointer for the first entry.
*
* Enters a new lazy MMU mode section; if the mode was not already enabled,
* enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
*
* PTEs that fall within the specified range might observe update speedups.
* The PTE range must belong to the specified memory space and do not cross
* a page table boundary.
*
* There are no requirements on the order or range completeness of PTE

Quoted 8 lines

* updates for the specified range.
*
* Must be paired with a call to lazy_mmu_mode_disable(). *
* Has no effect if called:
* - While paused - see lazy_mmu_mode_pause()
* - In interrupt context
*/

LGTM!

Signature · 3 lines
Cheers,

David
#13 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

In reply to Alexander Gordeev
Quoted 3 lines · Alexander Gordeev

/**
* lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
* @mm: Address space the ptes represent.

Not sure that makes sense, maybe something like "Address space the PTEs belong to"?

Quoted 1 line

* @addr: Address of the first pte.

Isn't it the address of the underlying memory rather?

Quoted 8 lines

* @end: End address of the range.
* @ptep: Page table pointer for the first entry.
*
* Enters a new lazy MMU mode section; if the mode was not already enabled,
* enables it and calls arch_enter_lazy_mmu_mode_for_pte_range(). *
* PTEs that fall within the specified range might observe update speedups.
* The PTE range must belong to the specified memory space and do not cross

s/do not/not/

Quoted 11 lines

* a page table boundary.
*
* There are no requirements on the order or range completeness of PTE
* updates for the specified range.
*
* Must be paired with a call to lazy_mmu_mode_disable().
*
* Has no effect if called:
* - While paused - see lazy_mmu_mode_pause()
* - In interrupt context
*/

Looks reasonable to me otherwise.

- Kevin

author maintainer
#14 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 8 lines · Kevin Brodsky
Quoted 1 line

* @mm: Address space the ptes represent.

Not sure that makes sense, maybe something like "Address space the PTEs belong to"?

Quoted 1 line

* @addr: Address of the first pte.

Isn't it the address of the underlying memory rather?

Will update both and also the clear_not_present_full_ptes() comment, where I copied it from.

...

Quoted 4 lines
Quoted 2 lines

* PTEs that fall within the specified range might observe update speedups.
* The PTE range must belong to the specified memory space and do not cross

s/do not/not/

...

Quoted 1 line

Looks reasonable to me otherwise.

Thanks!

Quoted 1 line

- Kevin

#15 lore raw

Re: [RFC PATCH 1/2] mm: make lazy MMU mode context-aware

Quoted 9 lines · Alexander Gordeev
Quoted 6 lines · Kevin Brodsky
Quoted 1 line

* @mm: Address space the ptes represent.

Not sure that makes sense, maybe something like "Address space the PTEs belong to"?

Quoted 1 line

* @addr: Address of the first pte.

Isn't it the address of the underlying memory rather?

Will update both and also the clear_not_present_full_ptes() comment, where I copied it from.

Oh I hadn't realised those comments came from there, that would be nice, thanks. A better reference might be a function like clear_ptes(), which was probably the inspiration for clear_not_present_full_ptes() (but wording adjustments were needed for the latter since the PTEs don't point to any memory).

- Kevin

author maintainer
#16 lore raw

RFC PATCH 2/2 s390/mm: Batch PTE updates in lazy MMU mode

In reply to Alexander Gordeev

Make use of the IPTE instruction's "Additional Entries" field to invalidate multiple PTEs in one go while in lazy MMU mode. This is the mode in which many memory-management system calls (like mremap(), mprotect(), etc.) update memory attributes.

To achieve that, the set_pte() and ptep_get() primitives use a per-CPU cache to store and retrieve PTE values and apply the cached values to the real page table once lazy MMU mode is left.

The same is done for memory-management platform callbacks that would otherwise cause intense per-PTE IPTE traffic, reducing the number of IPTE instructions from up to PTRS_PER_PTE to a single instruction in the best case. The average reduction is of course smaller.

Since all existing page table iterators called in lazy MMU mode handle one table at a time, the per-CPU cache does not need to be larger than PTRS_PER_PTE entries. That also naturally aligns with the IPTE instruction, which must not cross a page table boundary.

Before this change, the system calls did:

	lazy_mmu_mode_enable_pte()
	...
	<update PTEs>		// up to PTRS_PER_PTE single-IPTEs
	...
	lazy_mmu_mode_disable()

With this change, the system calls do:

lazy_mmu_mode_enable_pte()
...
<store new PTE values in the per-CPU cache>
...
lazy_mmu_mode_disable() // apply cache with one multi-IPTE

When applied to large memory ranges, some system calls show significant speedups:

    mprotect()    ~15x
    munmap()      ~3x
    mremap()      ~28x

At the same time, fork() shows a measurable slowdown of ~1.5x.

The overall results depend on memory size and access patterns, but the change generally does not degrade performance.

In addition to a process-wide impact, the rework affects the whole Central Electronics Complex (CEC). Each (global) IPTE instruction initiates a quiesce state in a CEC, so reducing the number of IPTE calls relieves CEC-wide quiesce traffic.

In an extreme case of mprotect() contiguously triggering the quiesce state on four LPARs in parallel, measurements show ~25x fewer quiesce events.

---
 arch/s390/Kconfig               |   8 +
 arch/s390/include/asm/pgtable.h | 209 +++++++++++++++--
 arch/s390/mm/Makefile           |   1 +
 arch/s390/mm/ipte_batch.c       | 396 ++++++++++++++++++++++++++++++++
 arch/s390/mm/pgtable.c          |   8 +-
 5 files changed, 603 insertions(+), 19 deletions(-)
 create mode 100644 arch/s390/mm/ipte_batch.c
arch/s390/Kconfig +8 -0
index 7828fbe0fc42..5821d4d42d1d 100644
@@ -732,6 +732,14 @@ config MAX_PHYSMEM_BITS
 	  Increasing the number of bits also increases the kernel image size.
 	  By default 46 bits (64TB) are supported.
 
+config IPTE_BATCH
+	def_bool y
+	prompt "Enables Additional Entries for IPTE instruction"
+	select ARCH_HAS_LAZY_MMU_MODE
+	help
+	  This option enables using of "Additional Entries" field of the IPTE
+	  instruction, which capitalizes on the lazy MMU mode infrastructure.
+
 endmenu
 
 menu "I/O subsystem"
arch/s390/include/asm/pgtable.h +194 -15

285 lines not on this page · raw message

arch/s390/mm/Makefile +1 -0
index 193899c39ca7..0f6c6de447d4 100644
@@ -11,5 +11,6 @@ obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_HUGETLB_PAGE)	+= hugetlbpage.o
 obj-$(CONFIG_PTDUMP)		+= dump_pagetables.o
 obj-$(CONFIG_PFAULT)		+= pfault.o
+obj-$(CONFIG_IPTE_BATCH)	+= ipte_batch.o
 
 obj-$(subst m,y,$(CONFIG_KVM))	+= gmap_helpers.o
arch/s390/mm/ipte_batch.c new file +396 -0

396 lines not on this page · raw message

arch/s390/mm/pgtable.c +4 -4
index 4acd8b140c4b..df36523bcbbb 100644
@@ -166,14 +166,14 @@ pte_t ptep_xchg_lazy(struct mm_struct *mm, unsigned long addr,
 }
 EXPORT_SYMBOL(ptep_xchg_lazy);
 
-pte_t ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep)
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep)
 {
 	return ptep_flush_lazy(vma->vm_mm, addr, ptep, 1);
 }
 
-void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep, pte_t old_pte, pte_t pte)
+void ___ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep, pte_t old_pte, pte_t pte)
 {
 	set_pte(ptep, pte);
 }
Signature · 1 line
2.51.0