* [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
@ 2026-07-23 8:49 Yuan Liu
2026-07-23 8:49 ` [PATCH v6 1/2] " Yuan Liu
` (2 more replies)
0 siblings, 3 replies; 15+ messages in thread
From: Yuan Liu @ 2026-07-23 8:49 UTC (permalink / raw)
To: David Hildenbrand, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm, Nanhai Zou, Yuan Liu, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
This series introduces a pages_with_online_memmap member into struct
zone to avoid pageblock-by-pageblock scans across the entire zone and
improve memory hotplug performance.
Reworked how pages_with_online_memmap accounts for PFNs in early
boot memory in response to Sashiko's review comments [2], which
identified potential issues with the previous implementation.
Approach
========
Add a new zone member pages_with_online_memmap that tracks the number of
pages within the zone span that have an online memory map (including
present pages and memory holes smaller than a subsection whose memory
map has been initialized). When spanned_pages == pages_with_online_memmap
the zone is contiguous and pfn_to_page() can be called on any PFN in the
zone span without further pfn_valid() checks.
For early boot memory, pages_with_online_memmap is calculated in
memmap_init_zone_range(). Since every PFN within a memblock region
satisfies pfn_to_online_page(), the calculation aligns both boundaries
of each memblock range to PAGES_PER_SUBSECTION and counts the aligned
pages that fall within the zone span. If two adjacent memblock regions
share a subsection (i.e. the hole between them is smaller than a
subsection), the overlapping subsection pages are counted only once.
For hotplugged memory, pages_with_online_memmap is updated through
adjust_present_page_count(), which is called during memory online and
offline.
Only pages that fall within the current zone span are accounted towards
pages_with_online_memmap. A "too small" value is safe, it merely
prevents detecting a contiguous zone.
The contiguity check using pages_with_online_memmap is stricter than the
old pageblock-by-pageblock scan. The old set_zone_contiguous() iterated
at pageblock granularity via pageblock_pfn_to_page(), so a zone could be
marked contiguous even if a subsection-sized hole existed within a
pageblock. The new check requires spanned_pages == pages_with_online_memmap,
meaning every PFN in the zone span must satisfy pfn_to_online_page().
Performance
===========
1. For VM hotplug performance data, please refer to Patch 1.
2. This series also benefits CXL hotplug. Performance results are
as follows
https://lore.kernel.org/all/20260409023552.GA2807@AE/
Tested cases
============
1. Hotplug/unplug correctness with both online_movable and online
policies, including partial unplug, middle-block offline gaps, and
edge-block span shrink.
2. Large scale (256G/512G) plug/unplug performance.
3. Boot-time subsection holes (aligned/unaligned, in-zone and
cross-zone) with correct pages_with_online_memmap accounting.
4. kernelcore=mirror: verified no overcounting.
Patch overview
==============
This series is based on the kernelcore-mirror branch [1].
Patch 1 introduces pages_with_online_memmap to replace
pageblock-by-pageblock scans across the entire zone for zone contiguity
checks.
Patch 2 avoids incorrectly shrinking the zone span during memory unplug
when a memory or hole boundary falls in the middle of a subsection.
v6 changes:
1. Dropped patches 1-3 from v5. The kernelcore=mirror overlap issue
that caused pages_with_online_memmap to be overcounted has been
resolved by the series [1].
2. Reworked pages_with_online_memmap accounting during early boot.
v5:
https://lore.kernel.org/linux-mm/20260520093457.3719960-1-yuan1.liu@intel.com/
v4:
https://lore.kernel.org/linux-mm/20260421125508.2317429-1-yuan1.liu@intel.com/
v3:
https://lore.kernel.org/linux-mm/20260408031615.1831922-1-yuan1.liu@intel.com/
[1] https://git.kernel.org/pub/scm/linux/kernel/git/rppt/memblock.git/log/?h=kernelcore-mirror
[2] https://sashiko.dev/#/patchset/20260520093457.3719960-3-yuan1.liu@intel.com
Yuan Liu (2):
mm/memory_hotplug: optimize zone contiguous check when changing pfn
range
mm/memory_hotplug: improve shrink_zone_span() subsection boundary
checks
Documentation/mm/physical_memory.rst | 13 +++++++
drivers/base/memory.c | 6 +++
include/linux/mmzone.h | 47 ++++++++++++++++++++++
mm/internal.h | 8 +---
mm/memory_hotplug.c | 54 ++++++++++++++------------
mm/mm_init.c | 58 +++++++++++++++++-----------
6 files changed, 132 insertions(+), 54 deletions(-)
base-commit: b69c2a1fa9beb4c3db24b4013e07f5cb7e7260aa
--
2.47.3
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-07-23 8:49 [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Yuan Liu
@ 2026-07-23 8:49 ` Yuan Liu
2026-08-05 11:53 ` David Hildenbrand (Arm)
2026-07-23 8:49 ` [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks Yuan Liu
2026-08-05 9:40 ` [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Liu, Yuan1
2 siblings, 1 reply; 15+ messages in thread
From: Yuan Liu @ 2026-07-23 8:49 UTC (permalink / raw)
To: David Hildenbrand, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm, Nanhai Zou, Yuan Liu, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
When move_pfn_range_to_zone() or remove_pfn_range_from_zone() updates a
zone, set_zone_contiguous() rescans the entire zone
pageblock-by-pageblock to rebuild zone->contiguous. For large zones this
is a significant cost during memory hotplug and hot-unplug.
Add a new zone member pages_with_online_memmap that tracks the number of
pages within the zone span that have an online memory map (including
present pages and memory holes smaller than a subsection whose memory
map has been initialized). When spanned_pages == pages_with_online_memmap
the zone is contiguous and pfn_to_page() can be called on any PFN in the
zone span without further pfn_valid() checks.
For early boot memory, pages_with_online_memmap is calculated in
memmap_init_zone_range(). Since every PFN within a memblock region
satisfies pfn_to_online_page(), the calculation aligns both boundaries
of each memblock range to PAGES_PER_SUBSECTION and counts the aligned
pages that fall within the zone span. If two adjacent memblock regions
share a subsection (i.e. the hole between them is smaller than a
subsection), the overlapping subsection pages are counted only once.
For hotplugged memory, pages_with_online_memmap is updated through
adjust_present_page_count(), which is called during memory online and
offline.
Only pages that fall within the current zone span are accounted towards
pages_with_online_memmap. A "too small" value is safe, it merely
prevents detecting a contiguous zone.
The contiguity check using pages_with_online_memmap is stricter than the
old pageblock-by-pageblock scan. The old set_zone_contiguous() iterated
at pageblock granularity via pageblock_pfn_to_page(), so a zone could be
marked contiguous even if a subsection-sized hole existed within a
pageblock. The new check requires
spanned_pages == pages_with_online_memmap, meaning every PFN in the zone
span must satisfy pfn_to_online_page().
The following test cases of memory hotplug for a VM [1], tested in the
environment [2], show that this optimization can significantly reduce
the memory hotplug time [3].
+----------------+------+---------------+--------------+----------------+
| | Size | Time (before) | Time (after) | Time Reduction |
| +------+---------------+--------------+----------------+
| Plug Memory | 256G | 10s | 3s | 70% |
| +------+---------------+--------------+----------------+
| | 512G | 36s | 7s | 81% |
+----------------+------+---------------+--------------+----------------+
+----------------+------+---------------+--------------+----------------+
| | Size | Time (before) | Time (after) | Time Reduction |
| +------+---------------+--------------+----------------+
| Unplug Memory | 256G | 11s | 4s | 64% |
| +------+---------------+--------------+----------------+
| | 512G | 36s | 9s | 75% |
+----------------+------+---------------+--------------+----------------+
[1] Qemu commands to hotplug 256G/512G memory for a VM:
object_add memory-backend-ram,id=hotmem0,size=256G/512G,share=on
device_add virtio-mem-pci,id=vmem1,memdev=hotmem0,bus=port1
qom-set vmem1 requested-size 256G/512G (Plug Memory)
qom-set vmem1 requested-size 0G (Unplug Memory)
[2] Hardware : Intel Icelake server
Guest Kernel : v7.0-rc4
Qemu : v9.0.0
Launch VM :
qemu-system-x86_64 -accel kvm -cpu host \
-drive file=./Centos10_cloud.qcow2,format=qcow2,if=virtio \
-drive file=./seed.img,format=raw,if=virtio \
-smp 3,cores=3,threads=1,sockets=1,maxcpus=3 \
-m 2G,slots=10,maxmem=2052472M \
-device pcie-root-port,id=port1,bus=pcie.0,slot=1,multifunction=on \
-device pcie-root-port,id=port2,bus=pcie.0,slot=2 \
-nographic -machine q35 \
-nic user,hostfwd=tcp::3000-:22
Guest kernel auto-onlines newly added memory blocks:
echo online > /sys/devices/system/memory/auto_online_blocks
[3] The time from typing the QEMU commands in [1] to when the output of
'grep MemTotal /proc/meminfo' on Guest reflects that all hotplugged
memory is recognized.
Reported-by: Nanhai Zou <nanhai.zou@intel.com>
Reported-by: Chen Zhang <zhangchen.kidd@jd.com>
Reviewed-by: Pan Deng <pan.deng@intel.com>
Reviewed-by: Jason Zeng <jason.zeng@intel.com>
Co-developed-by: Tianyou Li <tianyou.li@intel.com>
Signed-off-by: Tianyou Li <tianyou.li@intel.com>
Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
---
Documentation/mm/physical_memory.rst | 13 +++++++
drivers/base/memory.c | 6 +++
include/linux/mmzone.h | 47 ++++++++++++++++++++++
mm/internal.h | 8 +---
mm/memory_hotplug.c | 12 +-----
mm/mm_init.c | 58 +++++++++++++++++-----------
6 files changed, 105 insertions(+), 39 deletions(-)
diff --git a/Documentation/mm/physical_memory.rst b/Documentation/mm/physical_memory.rst
index b76183545e5b..0aa65e6b5499 100644
--- a/Documentation/mm/physical_memory.rst
+++ b/Documentation/mm/physical_memory.rst
@@ -483,6 +483,19 @@ General
``present_pages`` should use ``get_online_mems()`` to get a stable value. It
is initialized by ``calculate_node_totalpages()``.
+``pages_with_online_memmap``
+ Tracks pages within the zone that have an online memory map (present pages
+ and memory holes whose memory map has been initialized). When
+ ``spanned_pages`` == ``pages_with_online_memmap``, ``pfn_to_page()`` can be
+ performed without further checks on any PFN within the zone span.
+
+ Note: this counter may temporarily undercount when pages with an online
+ memory map exist outside the current zone span. This can only happen during
+ boot, when initializing the memory map of pages that do not fall into any
+ zone span. Growing the zone to cover such pages and later shrinking it back
+ may result in a "too small" value. This is safe: it merely prevents
+ detecting a contiguous zone.
+
``present_early_pages``
The present pages existing within the zone located on memory available since
early boot, excluding hotplugged memory. Defined only when
diff --git a/drivers/base/memory.c b/drivers/base/memory.c
index bcfe2d9f4adb..237ace435372 100644
--- a/drivers/base/memory.c
+++ b/drivers/base/memory.c
@@ -246,6 +246,7 @@ static int memory_block_online(struct memory_block *mem)
nr_vmemmap_pages = mem->altmap->free;
mem_hotplug_begin();
+ clear_zone_contiguous(zone);
if (nr_vmemmap_pages) {
ret = mhp_init_memmap_on_memory(start_pfn, nr_vmemmap_pages, zone);
if (ret)
@@ -270,6 +271,7 @@ static int memory_block_online(struct memory_block *mem)
mem->zone = zone;
out:
+ set_zone_contiguous(zone);
mem_hotplug_done();
return ret;
}
@@ -282,6 +284,7 @@ static int memory_block_offline(struct memory_block *mem)
unsigned long start_pfn = section_nr_to_pfn(mem->start_section_nr);
unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
unsigned long nr_vmemmap_pages = 0;
+ struct zone *zone;
int ret;
if (!mem->zone)
@@ -294,7 +297,9 @@ static int memory_block_offline(struct memory_block *mem)
if (mem->altmap)
nr_vmemmap_pages = mem->altmap->free;
+ zone = mem->zone;
mem_hotplug_begin();
+ clear_zone_contiguous(zone);
if (nr_vmemmap_pages)
adjust_present_page_count(pfn_to_page(start_pfn), mem->group,
-nr_vmemmap_pages);
@@ -314,6 +319,7 @@ static int memory_block_offline(struct memory_block *mem)
mem->zone = NULL;
out:
+ set_zone_contiguous(zone);
mem_hotplug_done();
return ret;
}
diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
index ca2712187147..ac1467ac3336 100644
--- a/include/linux/mmzone.h
+++ b/include/linux/mmzone.h
@@ -1032,6 +1032,20 @@ struct zone {
* cma pages is present pages that are assigned for CMA use
* (MIGRATE_CMA).
*
+ * pages_with_online_memmap tracks pages within the zone that have
+ * an online memory map (present pages and memory holes whose memory
+ * map has been initialized). When spanned_pages ==
+ * pages_with_online_memmap, pfn_to_page() can be performed without
+ * further checks on any PFN within the zone span.
+ *
+ * Note: this counter may temporarily undercount when pages with an
+ * online memory map exist outside the current zone span. This can
+ * only happen during boot, when initializing the memory map of
+ * pages that do not fall into any zone span. Growing the zone to
+ * cover such pages and later shrinking it back may result in a
+ * "too small" value. This is safe: it merely prevents detecting a
+ * contiguous zone.
+ *
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
@@ -1056,6 +1070,7 @@ struct zone {
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
+ unsigned long pages_with_online_memmap;
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages;
#endif
@@ -1681,6 +1696,38 @@ static inline bool zone_is_zone_device(const struct zone *zone)
}
#endif
+/**
+ * zone_is_contiguous - test whether a zone is contiguous
+ * @zone: the zone to test.
+ *
+ * In a contiguous zone, it is valid to call pfn_to_page() on any PFN in the
+ * spanned zone without requiring pfn_valid() or pfn_to_online_page() checks.
+ *
+ * Note that missing synchronization with memory offlining makes any PFN
+ * traversal prone to races.
+ *
+ * ZONE_DEVICE zones are always marked non-contiguous.
+ *
+ * Return: true if contiguous, otherwise false.
+ */
+static inline bool zone_is_contiguous(const struct zone *zone)
+{
+ return zone->contiguous;
+}
+
+static inline void set_zone_contiguous(struct zone *zone)
+{
+ if (zone_is_zone_device(zone))
+ return;
+ if (zone->spanned_pages == zone->pages_with_online_memmap)
+ zone->contiguous = true;
+}
+
+static inline void clear_zone_contiguous(struct zone *zone)
+{
+ zone->contiguous = false;
+}
+
/*
* Returns true if a zone has pages managed by the buddy allocator.
* All the reclaim decisions have to use this function rather than
diff --git a/mm/internal.h b/mm/internal.h
index 181e79f1d6a2..f932b7577c92 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -805,21 +805,15 @@ extern struct page *__pageblock_pfn_to_page(unsigned long start_pfn,
static inline struct page *pageblock_pfn_to_page(unsigned long start_pfn,
unsigned long end_pfn, struct zone *zone)
{
- if (zone->contiguous)
+ if (zone_is_contiguous(zone))
return pfn_to_page(start_pfn);
return __pageblock_pfn_to_page(start_pfn, end_pfn, zone);
}
-void set_zone_contiguous(struct zone *zone);
bool pfn_range_intersects_zones(int nid, unsigned long start_pfn,
unsigned long nr_pages);
-static inline void clear_zone_contiguous(struct zone *zone)
-{
- zone->contiguous = false;
-}
-
extern int __isolate_free_page(struct page *page, unsigned int order);
extern void __putback_isolated_page(struct page *page, unsigned int order,
int mt);
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 7ac19fab2263..4c699fd9c479 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -557,18 +557,13 @@ void remove_pfn_range_from_zone(struct zone *zone,
/*
* Zone shrinking code cannot properly deal with ZONE_DEVICE. So
- * we will not try to shrink the zones - which is okay as
- * set_zone_contiguous() cannot deal with ZONE_DEVICE either way.
+ * we will not try to shrink it.
*/
if (zone_is_zone_device(zone))
return;
- clear_zone_contiguous(zone);
-
shrink_zone_span(zone, start_pfn, start_pfn + nr_pages);
update_pgdat_span(pgdat);
-
- set_zone_contiguous(zone);
}
/**
@@ -746,8 +741,6 @@ void move_pfn_range_to_zone(struct zone *zone, unsigned long start_pfn,
struct pglist_data *pgdat = zone->zone_pgdat;
int nid = pgdat->node_id;
- clear_zone_contiguous(zone);
-
if (zone_is_empty(zone))
init_currently_empty_zone(zone, start_pfn, nr_pages);
resize_zone_range(zone, start_pfn, nr_pages);
@@ -775,8 +768,6 @@ void move_pfn_range_to_zone(struct zone *zone, unsigned long start_pfn,
memmap_init_range(nr_pages, nid, zone_idx(zone), start_pfn, 0,
MEMINIT_HOTPLUG, altmap, migratetype,
isolate_pageblock);
-
- set_zone_contiguous(zone);
}
struct auto_movable_stats {
@@ -1072,6 +1063,7 @@ void adjust_present_page_count(struct page *page, struct memory_group *group,
if (early_section(__pfn_to_section(page_to_pfn(page))))
zone->present_early_pages += nr_pages;
zone->present_pages += nr_pages;
+ zone->pages_with_online_memmap += nr_pages;
zone->zone_pgdat->node_present_pages += nr_pages;
if (group && movable)
diff --git a/mm/mm_init.c b/mm/mm_init.c
index f1afe023e4e7..73d1fde17040 100644
--- a/mm/mm_init.c
+++ b/mm/mm_init.c
@@ -916,6 +916,40 @@ void __meminit memmap_init_range(unsigned long size, int nid, unsigned long zone
}
}
+static void __init update_zone_online_memmap_pages(struct zone *zone,
+ unsigned long start_pfn,
+ unsigned long end_pfn,
+ unsigned long *hole_pfn)
+{
+#ifdef CONFIG_SPARSEMEM_VMEMMAP
+ unsigned long zone_start_pfn = zone->zone_start_pfn;
+ unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
+ unsigned long sub_start, sub_end;
+
+ sub_start = max(ALIGN_DOWN(start_pfn, PAGES_PER_SUBSECTION),
+ zone_start_pfn);
+ sub_end = min(ALIGN(end_pfn, PAGES_PER_SUBSECTION), zone_end_pfn);
+
+ /*
+ * For an in-zone hole smaller than a subsection, the adjacent
+ * memblock ranges share a subsection. Avoid counting the same
+ * subsection's pages twice.
+ */
+ if (*hole_pfn > zone_start_pfn) {
+ unsigned long prev_sub_end;
+
+ prev_sub_end = min(ALIGN(*hole_pfn, PAGES_PER_SUBSECTION),
+ zone_end_pfn);
+ sub_start = max(sub_start, prev_sub_end);
+ }
+
+ if (sub_start < sub_end)
+ zone->pages_with_online_memmap += sub_end - sub_start;
+#else
+ zone->pages_with_online_memmap += end_pfn - start_pfn;
+#endif
+}
+
static void __init memmap_init_zone_range(struct zone *zone,
unsigned long start_pfn,
unsigned long end_pfn,
@@ -932,6 +966,8 @@ static void __init memmap_init_zone_range(struct zone *zone,
if (start_pfn >= end_pfn)
return;
+ update_zone_online_memmap_pages(zone, start_pfn, end_pfn, hole_pfn);
+
memmap_init_range(end_pfn - start_pfn, nid, zone_id, start_pfn,
zone_end_pfn, MEMINIT_EARLY, NULL, mt, false);
@@ -2195,28 +2231,6 @@ void __init init_cma_pageblock(struct page *page)
}
#endif
-void set_zone_contiguous(struct zone *zone)
-{
- unsigned long block_start_pfn = zone->zone_start_pfn;
- unsigned long block_end_pfn;
-
- block_end_pfn = pageblock_end_pfn(block_start_pfn);
- for (; block_start_pfn < zone_end_pfn(zone);
- block_start_pfn = block_end_pfn,
- block_end_pfn += pageblock_nr_pages) {
-
- block_end_pfn = min(block_end_pfn, zone_end_pfn(zone));
-
- if (!__pageblock_pfn_to_page(block_start_pfn,
- block_end_pfn, zone))
- return;
- cond_resched();
- }
-
- /* We confirm that there is no hole */
- zone->contiguous = true;
-}
-
/*
* Check if a PFN range intersects multiple zones on one or more
* NUMA nodes. Specify the @nid argument if it is known that this
--
2.47.3
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-23 8:49 [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Yuan Liu
2026-07-23 8:49 ` [PATCH v6 1/2] " Yuan Liu
@ 2026-07-23 8:49 ` Yuan Liu
2026-07-25 2:49 ` Wei Yang
2026-08-05 11:02 ` David Hildenbrand (Arm)
2026-08-05 9:40 ` [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Liu, Yuan1
2 siblings, 2 replies; 15+ messages in thread
From: Yuan Liu @ 2026-07-23 8:49 UTC (permalink / raw)
To: David Hildenbrand, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm, Nanhai Zou, Yuan Liu, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
When shrinking a zone span after removing a PFN range,
find_smallest_section_pfn() and find_biggest_section_pfn()
only checked one edge PFN in each subsection for nid/zone matching.
If a memory or hole boundary falls in the middle of a subsection,
that edge PFN may belong to a different nid/zone, causing the helpers
to miss a valid PFN within that subsection.
Fix this by checking both subsection edge PFNs for nid/zone matching.
Keep a single pfn_to_online_page() check per subsection, since online
state is the same for all PFNs in a subsection.
Reviewed-by: Jason Zeng <jason.zeng@intel.com>
Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
---
mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
1 file changed, 27 insertions(+), 15 deletions(-)
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 4c699fd9c479..3a281d595207 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int nid, struct zone *zone,
unsigned long start_pfn,
unsigned long end_pfn)
{
- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
- if (unlikely(!pfn_to_online_page(start_pfn)))
- continue;
+ unsigned long next_pfn;
- if (unlikely(pfn_to_nid(start_pfn) != nid))
- continue;
+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
+ unsigned long tail_pfn;
- if (zone != page_zone(pfn_to_page(start_pfn)))
+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
+ tail_pfn = next_pfn - 1;
+
+ if (unlikely(!pfn_to_online_page(start_pfn)))
continue;
- return start_pfn;
+ if (likely(pfn_to_nid(start_pfn) == nid) &&
+ zone == page_zone(pfn_to_page(start_pfn)))
+ return start_pfn;
+
+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
+ zone == page_zone(pfn_to_page(tail_pfn)))
+ return start_pfn;
}
return 0;
@@ -448,21 +455,26 @@ static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
unsigned long start_pfn,
unsigned long end_pfn)
{
- unsigned long pfn;
+ unsigned long pfn, prev_pfn;
/* pfn is the end pfn of a memory section. */
pfn = end_pfn - 1;
- for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
- if (unlikely(!pfn_to_online_page(pfn)))
- continue;
+ for (; pfn >= start_pfn; pfn = prev_pfn) {
+ unsigned long head_pfn;
- if (unlikely(pfn_to_nid(pfn) != nid))
- continue;
+ prev_pfn = pfn - PAGES_PER_SUBSECTION;
+ head_pfn = prev_pfn + 1;
- if (zone != page_zone(pfn_to_page(pfn)))
+ if (unlikely(!pfn_to_online_page(pfn)))
continue;
- return pfn;
+ if (likely(pfn_to_nid(pfn) == nid) &&
+ zone == page_zone(pfn_to_page(pfn)))
+ return pfn;
+
+ if (likely(pfn_to_nid(head_pfn) == nid) &&
+ zone == page_zone(pfn_to_page(head_pfn)))
+ return pfn;
}
return 0;
--
2.47.3
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-23 8:49 ` [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks Yuan Liu
@ 2026-07-25 2:49 ` Wei Yang
2026-07-27 9:49 ` Liu, Yuan1
2026-08-05 11:02 ` David Hildenbrand (Arm)
1 sibling, 1 reply; 15+ messages in thread
From: Wei Yang @ 2026-07-25 2:49 UTC (permalink / raw)
To: Yuan Liu
Cc: David Hildenbrand, Oscar Salvador, Mike Rapoport, Wei Yang,
linux-mm, Nanhai Zou, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
On Thu, Jul 23, 2026 at 04:49:46AM -0400, Yuan Liu wrote:
>When shrinking a zone span after removing a PFN range,
>find_smallest_section_pfn() and find_biggest_section_pfn()
>only checked one edge PFN in each subsection for nid/zone matching.
>
>If a memory or hole boundary falls in the middle of a subsection,
>that edge PFN may belong to a different nid/zone, causing the helpers
>to miss a valid PFN within that subsection.
>
>Fix this by checking both subsection edge PFNs for nid/zone matching.
>Keep a single pfn_to_online_page() check per subsection, since online
>state is the same for all PFNs in a subsection.
>
>Reviewed-by: Jason Zeng <jason.zeng@intel.com>
>Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
>---
> mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
> 1 file changed, 27 insertions(+), 15 deletions(-)
>
>diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
>index 4c699fd9c479..3a281d595207 100644
>--- a/mm/memory_hotplug.c
>+++ b/mm/memory_hotplug.c
>@@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int nid, struct zone *zone,
> unsigned long start_pfn,
> unsigned long end_pfn)
> {
>- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
>- if (unlikely(!pfn_to_online_page(start_pfn)))
>- continue;
>+ unsigned long next_pfn;
>
>- if (unlikely(pfn_to_nid(start_pfn) != nid))
>- continue;
>+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
>+ unsigned long tail_pfn;
>
>- if (zone != page_zone(pfn_to_page(start_pfn)))
>+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
>+ tail_pfn = next_pfn - 1;
>+
>+ if (unlikely(!pfn_to_online_page(start_pfn)))
> continue;
>
>- return start_pfn;
>+ if (likely(pfn_to_nid(start_pfn) == nid) &&
>+ zone == page_zone(pfn_to_page(start_pfn)))
>+ return start_pfn;
>+
>+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
>+ zone == page_zone(pfn_to_page(tail_pfn)))
>+ return start_pfn;
Here we are checking range [start_pfn, tail_pfn]. When we come here, it means
start_pfn's nid or zone doesn't match our expectation. But if tail_pfn does,
why it still return start_pfn?
> }
>
> return 0;
>@@ -448,21 +455,26 @@ static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
> unsigned long start_pfn,
> unsigned long end_pfn)
> {
>- unsigned long pfn;
>+ unsigned long pfn, prev_pfn;
>
> /* pfn is the end pfn of a memory section. */
> pfn = end_pfn - 1;
>- for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
>- if (unlikely(!pfn_to_online_page(pfn)))
>- continue;
>+ for (; pfn >= start_pfn; pfn = prev_pfn) {
>+ unsigned long head_pfn;
>
>- if (unlikely(pfn_to_nid(pfn) != nid))
>- continue;
>+ prev_pfn = pfn - PAGES_PER_SUBSECTION;
>+ head_pfn = prev_pfn + 1;
>
>- if (zone != page_zone(pfn_to_page(pfn)))
>+ if (unlikely(!pfn_to_online_page(pfn)))
> continue;
>
>- return pfn;
>+ if (likely(pfn_to_nid(pfn) == nid) &&
>+ zone == page_zone(pfn_to_page(pfn)))
>+ return pfn;
>+
>+ if (likely(pfn_to_nid(head_pfn) == nid) &&
>+ zone == page_zone(pfn_to_page(head_pfn)))
>+ return pfn;
> }
>
> return 0;
>--
>2.47.3
--
Wei Yang
Help you, Help me
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-25 2:49 ` Wei Yang
@ 2026-07-27 9:49 ` Liu, Yuan1
2026-07-30 2:36 ` Wei Yang
0 siblings, 1 reply; 15+ messages in thread
From: Liu, Yuan1 @ 2026-07-27 9:49 UTC (permalink / raw)
To: Wei Yang
Cc: David Hildenbrand, Oscar Salvador, Mike Rapoport,
linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
> -----Original Message-----
> From: Wei Yang <richard.weiyang@gmail.com>
> Sent: Saturday, July 25, 2026 10:50 AM
> To: Liu, Yuan1 <yuan1.liu@intel.com>
> Cc: David Hildenbrand <david@kernel.org>; Oscar Salvador
> <osalvador@suse.de>; Mike Rapoport <rppt@kernel.org>; Wei Yang
> <richard.weiyang@gmail.com>; linux-mm@kvack.org; Zou, Nanhai
> <nanhai.zou@intel.com>; Deng, Pan <pan.deng@intel.com>; Li, Tianyou
> <tianyou.li@intel.com>; Chen Zhang <zhangchen.kidd@jd.com>; Zeng, Jason
> <jason.zeng@intel.com>; linux-kernel@vger.kernel.org
> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span()
> subsection boundary checks
>
> On Thu, Jul 23, 2026 at 04:49:46AM -0400, Yuan Liu wrote:
> >When shrinking a zone span after removing a PFN range,
> >find_smallest_section_pfn() and find_biggest_section_pfn()
> >only checked one edge PFN in each subsection for nid/zone matching.
> >
> >If a memory or hole boundary falls in the middle of a subsection,
> >that edge PFN may belong to a different nid/zone, causing the helpers
> >to miss a valid PFN within that subsection.
> >
> >Fix this by checking both subsection edge PFNs for nid/zone matching.
> >Keep a single pfn_to_online_page() check per subsection, since online
> >state is the same for all PFNs in a subsection.
> >
> >Reviewed-by: Jason Zeng <jason.zeng@intel.com>
> >Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
> >---
> > mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
> > 1 file changed, 27 insertions(+), 15 deletions(-)
> >
> >diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
> >index 4c699fd9c479..3a281d595207 100644
> >--- a/mm/memory_hotplug.c
> >+++ b/mm/memory_hotplug.c
> >@@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int
> nid, struct zone *zone,
> > unsigned long start_pfn,
> > unsigned long end_pfn)
> > {
> >- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> >- if (unlikely(!pfn_to_online_page(start_pfn)))
> >- continue;
> >+ unsigned long next_pfn;
> >
> >- if (unlikely(pfn_to_nid(start_pfn) != nid))
> >- continue;
> >+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
> >+ unsigned long tail_pfn;
> >
> >- if (zone != page_zone(pfn_to_page(start_pfn)))
> >+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
> >+ tail_pfn = next_pfn - 1;
> >+
> >+ if (unlikely(!pfn_to_online_page(start_pfn)))
> > continue;
> >
> >- return start_pfn;
> >+ if (likely(pfn_to_nid(start_pfn) == nid) &&
> >+ zone == page_zone(pfn_to_page(start_pfn)))
> >+ return start_pfn;
> >+
> >+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
> >+ zone == page_zone(pfn_to_page(tail_pfn)))
> >+ return start_pfn;
>
> Here we are checking range [start_pfn, tail_pfn]. When we come here, it
> means
> start_pfn's nid or zone doesn't match our expectation. But if tail_pfn
> does,
> why it still return start_pfn?
Hi Wei
If start_pfn falls into a hole while tail_pfn still belongs to a valid
memblock in this zone, skipping the subsection would cause
shrink_zone_span() to shrink the zone span too aggressively, excluding
valid PFNs from the zone.
Since init_unavailable_range() initializes hole pages with the correct
zone/nid, the start_pfn check always succeeds here, making the tail_pfn
check redundant today.
That said, I wonder if it is still worth keeping this check so that
shrink_zone_span() does not depend on how hole pages are initialized.
>
> > }
> >
> > return 0;
> >@@ -448,21 +455,26 @@ static unsigned long find_biggest_section_pfn(int
> nid, struct zone *zone,
> > unsigned long start_pfn,
> > unsigned long end_pfn)
> > {
> >- unsigned long pfn;
> >+ unsigned long pfn, prev_pfn;
> >
> > /* pfn is the end pfn of a memory section. */
> > pfn = end_pfn - 1;
> >- for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
> >- if (unlikely(!pfn_to_online_page(pfn)))
> >- continue;
> >+ for (; pfn >= start_pfn; pfn = prev_pfn) {
> >+ unsigned long head_pfn;
> >
> >- if (unlikely(pfn_to_nid(pfn) != nid))
> >- continue;
> >+ prev_pfn = pfn - PAGES_PER_SUBSECTION;
> >+ head_pfn = prev_pfn + 1;
> >
> >- if (zone != page_zone(pfn_to_page(pfn)))
> >+ if (unlikely(!pfn_to_online_page(pfn)))
> > continue;
> >
> >- return pfn;
> >+ if (likely(pfn_to_nid(pfn) == nid) &&
> >+ zone == page_zone(pfn_to_page(pfn)))
> >+ return pfn;
> >+
> >+ if (likely(pfn_to_nid(head_pfn) == nid) &&
> >+ zone == page_zone(pfn_to_page(head_pfn)))
> >+ return pfn;
> > }
> >
> > return 0;
> >--
> >2.47.3
>
> --
> Wei Yang
> Help you, Help me
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-27 9:49 ` Liu, Yuan1
@ 2026-07-30 2:36 ` Wei Yang
2026-07-30 7:57 ` Liu, Yuan1
0 siblings, 1 reply; 15+ messages in thread
From: Wei Yang @ 2026-07-30 2:36 UTC (permalink / raw)
To: Liu, Yuan1
Cc: Wei Yang, David Hildenbrand, Oscar Salvador, Mike Rapoport,
linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
On Mon, Jul 27, 2026 at 09:49:39AM +0000, Liu, Yuan1 wrote:
>> -----Original Message-----
>> From: Wei Yang <richard.weiyang@gmail.com>
>> Sent: Saturday, July 25, 2026 10:50 AM
>> To: Liu, Yuan1 <yuan1.liu@intel.com>
>> Cc: David Hildenbrand <david@kernel.org>; Oscar Salvador
>> <osalvador@suse.de>; Mike Rapoport <rppt@kernel.org>; Wei Yang
>> <richard.weiyang@gmail.com>; linux-mm@kvack.org; Zou, Nanhai
>> <nanhai.zou@intel.com>; Deng, Pan <pan.deng@intel.com>; Li, Tianyou
>> <tianyou.li@intel.com>; Chen Zhang <zhangchen.kidd@jd.com>; Zeng, Jason
>> <jason.zeng@intel.com>; linux-kernel@vger.kernel.org
>> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span()
>> subsection boundary checks
>>
>> On Thu, Jul 23, 2026 at 04:49:46AM -0400, Yuan Liu wrote:
>> >When shrinking a zone span after removing a PFN range,
>> >find_smallest_section_pfn() and find_biggest_section_pfn()
>> >only checked one edge PFN in each subsection for nid/zone matching.
>> >
>> >If a memory or hole boundary falls in the middle of a subsection,
>> >that edge PFN may belong to a different nid/zone, causing the helpers
>> >to miss a valid PFN within that subsection.
>> >
>> >Fix this by checking both subsection edge PFNs for nid/zone matching.
>> >Keep a single pfn_to_online_page() check per subsection, since online
>> >state is the same for all PFNs in a subsection.
>> >
>> >Reviewed-by: Jason Zeng <jason.zeng@intel.com>
>> >Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
>> >---
>> > mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
>> > 1 file changed, 27 insertions(+), 15 deletions(-)
>> >
>> >diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
>> >index 4c699fd9c479..3a281d595207 100644
>> >--- a/mm/memory_hotplug.c
>> >+++ b/mm/memory_hotplug.c
>> >@@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int
>> nid, struct zone *zone,
>> > unsigned long start_pfn,
>> > unsigned long end_pfn)
>> > {
>> >- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
>> >- if (unlikely(!pfn_to_online_page(start_pfn)))
>> >- continue;
>> >+ unsigned long next_pfn;
>> >
>> >- if (unlikely(pfn_to_nid(start_pfn) != nid))
>> >- continue;
>> >+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
>> >+ unsigned long tail_pfn;
>> >
>> >- if (zone != page_zone(pfn_to_page(start_pfn)))
>> >+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
>> >+ tail_pfn = next_pfn - 1;
>> >+
>> >+ if (unlikely(!pfn_to_online_page(start_pfn)))
>> > continue;
>> >
>> >- return start_pfn;
>> >+ if (likely(pfn_to_nid(start_pfn) == nid) &&
>> >+ zone == page_zone(pfn_to_page(start_pfn)))
>> >+ return start_pfn;
>> >+
>> >+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
>> >+ zone == page_zone(pfn_to_page(tail_pfn)))
>> >+ return start_pfn;
>>
>> Here we are checking range [start_pfn, tail_pfn]. When we come here, it
>> means
>> start_pfn's nid or zone doesn't match our expectation. But if tail_pfn
>> does,
>> why it still return start_pfn?
>
>Hi Wei
>
>If start_pfn falls into a hole while tail_pfn still belongs to a valid
>memblock in this zone, skipping the subsection would cause
>shrink_zone_span() to shrink the zone span too aggressively, excluding
>valid PFNs from the zone.
>
>Since init_unavailable_range() initializes hole pages with the correct
>zone/nid, the start_pfn check always succeeds here, making the tail_pfn
>check redundant today.
>
>That said, I wonder if it is still worth keeping this check so that
>shrink_zone_span() does not depend on how hole pages are initialized.
>
Looks reasonable.
I search the discussion history, and found David suggest this fix in [1] with
following statement.
Well, unless we have an odd case where the hole+memory starts in the
middle of a "PAGES_PER_SUBSECTION". That would already be problematic if
memory starts/ends in the middle of a PAGES_PER_SUBSECTION chunk. I
don't such a case exists.
We could improve shrink_zone_span() to let
find_smallest_section_pfn/find_biggest_section_pfn test the pfn_to_nid()
and page_zone() not on;y on the smallest/highest pfn, but also on the
highest/smallest PFN in a PAGES_PER_SUBSECTION chunk.
I am trying to understand the exact case David described, but not fully get
it. Would you mind describing more, so we would make sure not missing the
point.
[1]: https://lore.kernel.org/all/e86fee84-08d8-4563-8596-e40d8e196799@kernel.org/T/#u
--
Wei Yang
Help you, Help me
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-30 2:36 ` Wei Yang
@ 2026-07-30 7:57 ` Liu, Yuan1
2026-08-01 0:59 ` Wei Yang
0 siblings, 1 reply; 15+ messages in thread
From: Liu, Yuan1 @ 2026-07-30 7:57 UTC (permalink / raw)
To: Wei Yang
Cc: David Hildenbrand, Oscar Salvador, Mike Rapoport,
linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
> -----Original Message-----
> From: Wei Yang <richard.weiyang@gmail.com>
> Sent: Thursday, July 30, 2026 10:36 AM
> To: Liu, Yuan1 <yuan1.liu@intel.com>
> Cc: Wei Yang <richard.weiyang@gmail.com>; David Hildenbrand
> <david@kernel.org>; Oscar Salvador <osalvador@suse.de>; Mike Rapoport
> <rppt@kernel.org>; linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>;
> Deng, Pan <pan.deng@intel.com>; Li, Tianyou <tianyou.li@intel.com>; Chen
> Zhang <zhangchen.kidd@jd.com>; Zeng, Jason <jason.zeng@intel.com>; linux-
> kernel@vger.kernel.org
> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span()
> subsection boundary checks
>
> On Mon, Jul 27, 2026 at 09:49:39AM +0000, Liu, Yuan1 wrote:
> >> -----Original Message-----
> >> From: Wei Yang <richard.weiyang@gmail.com>
> >> Sent: Saturday, July 25, 2026 10:50 AM
> >> To: Liu, Yuan1 <yuan1.liu@intel.com>
> >> Cc: David Hildenbrand <david@kernel.org>; Oscar Salvador
> >> <osalvador@suse.de>; Mike Rapoport <rppt@kernel.org>; Wei Yang
> >> <richard.weiyang@gmail.com>; linux-mm@kvack.org; Zou, Nanhai
> >> <nanhai.zou@intel.com>; Deng, Pan <pan.deng@intel.com>; Li, Tianyou
> >> <tianyou.li@intel.com>; Chen Zhang <zhangchen.kidd@jd.com>; Zeng, Jason
> >> <jason.zeng@intel.com>; linux-kernel@vger.kernel.org
> >> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve
> shrink_zone_span()
> >> subsection boundary checks
> >>
> >> On Thu, Jul 23, 2026 at 04:49:46AM -0400, Yuan Liu wrote:
> >> >When shrinking a zone span after removing a PFN range,
> >> >find_smallest_section_pfn() and find_biggest_section_pfn()
> >> >only checked one edge PFN in each subsection for nid/zone matching.
> >> >
> >> >If a memory or hole boundary falls in the middle of a subsection,
> >> >that edge PFN may belong to a different nid/zone, causing the helpers
> >> >to miss a valid PFN within that subsection.
> >> >
> >> >Fix this by checking both subsection edge PFNs for nid/zone matching.
> >> >Keep a single pfn_to_online_page() check per subsection, since online
> >> >state is the same for all PFNs in a subsection.
> >> >
> >> >Reviewed-by: Jason Zeng <jason.zeng@intel.com>
> >> >Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
> >> >---
> >> > mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
> >> > 1 file changed, 27 insertions(+), 15 deletions(-)
> >> >
> >> >diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
> >> >index 4c699fd9c479..3a281d595207 100644
> >> >--- a/mm/memory_hotplug.c
> >> >+++ b/mm/memory_hotplug.c
> >> >@@ -427,17 +427,24 @@ static unsigned long
> find_smallest_section_pfn(int
> >> nid, struct zone *zone,
> >> > unsigned long start_pfn,
> >> > unsigned long end_pfn)
> >> > {
> >> >- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> >> >- if (unlikely(!pfn_to_online_page(start_pfn)))
> >> >- continue;
> >> >+ unsigned long next_pfn;
> >> >
> >> >- if (unlikely(pfn_to_nid(start_pfn) != nid))
> >> >- continue;
> >> >+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
> >> >+ unsigned long tail_pfn;
> >> >
> >> >- if (zone != page_zone(pfn_to_page(start_pfn)))
> >> >+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
> >> >+ tail_pfn = next_pfn - 1;
> >> >+
> >> >+ if (unlikely(!pfn_to_online_page(start_pfn)))
> >> > continue;
> >> >
> >> >- return start_pfn;
> >> >+ if (likely(pfn_to_nid(start_pfn) == nid) &&
> >> >+ zone == page_zone(pfn_to_page(start_pfn)))
> >> >+ return start_pfn;
> >> >+
> >> >+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
> >> >+ zone == page_zone(pfn_to_page(tail_pfn)))
> >> >+ return start_pfn;
> >>
> >> Here we are checking range [start_pfn, tail_pfn]. When we come here, it
> >> means
> >> start_pfn's nid or zone doesn't match our expectation. But if tail_pfn
> >> does,
> >> why it still return start_pfn?
> >
> >Hi Wei
> >
> >If start_pfn falls into a hole while tail_pfn still belongs to a valid
> >memblock in this zone, skipping the subsection would cause
> >shrink_zone_span() to shrink the zone span too aggressively, excluding
> >valid PFNs from the zone.
> >
> >Since init_unavailable_range() initializes hole pages with the correct
> >zone/nid, the start_pfn check always succeeds here, making the tail_pfn
> >check redundant today.
> >
> >That said, I wonder if it is still worth keeping this check so that
> >shrink_zone_span() does not depend on how hole pages are initialized.
> >
>
> Looks reasonable.
>
> I search the discussion history, and found David suggest this fix in [1]
> with
> following statement.
>
> Well, unless we have an odd case where the hole+memory starts in the
> middle of a "PAGES_PER_SUBSECTION". That would already be problematic if
> memory starts/ends in the middle of a PAGES_PER_SUBSECTION chunk. I
> don't such a case exists.
>
> We could improve shrink_zone_span() to let
> find_smallest_section_pfn/find_biggest_section_pfn test the pfn_to_nid()
> and page_zone() not on;y on the smallest/highest pfn, but also on the
> highest/smallest PFN in a PAGES_PER_SUBSECTION chunk.
>
> I am trying to understand the exact case David described, but not fully
> get
> it. Would you mind describing more, so we would make sure not missing the
> point.
>
> [1]: https://lore.kernel.org/all/e86fee84-08d8-4563-8596-
> e40d8e196799@kernel.org/T/#u
I am not sure whether this scenario can occur on real systems, but
from reading the current code it seems there may be a corner case.
For example, consider a zone whose tail ends with a single-PFN hole.
init_unavailable_range() initializes this hole page and assigns its
nid/zone to the adjacent next zone, since the PFN itself belongs outside
the current zone.
When memory is hot-added after this hole, the hotplug granularity
(PAGES_PER_SUBSECTION) extends the zone span to include the entire
subsection containing the hole.
Later, when the hot-added memory is removed,
find_biggest_section_pfn() examines the hole page first. Because its
nid/zone belongs to the adjacent next zone, the boundary subsection may
be considered removable even though it still contains boot memory pages.
This appears to be asymmetric between the zone head and the zone tail.
At the zone head (find_smallest_section_pfn()), the boundary subsection
has a hole + memory layout. The hole pages belong to the current zone,
so the subsection is retained.
At the zone tail (find_biggest_section_pfn()), the boundary subsection
has a memory + hole layout. The trailing hole pages belong to the
adjacent next zone, so the subsection may be discarded.
> --
> Wei Yang
> Help you, Help me
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-30 7:57 ` Liu, Yuan1
@ 2026-08-01 0:59 ` Wei Yang
0 siblings, 0 replies; 15+ messages in thread
From: Wei Yang @ 2026-08-01 0:59 UTC (permalink / raw)
To: Liu, Yuan1
Cc: Wei Yang, David Hildenbrand, Oscar Salvador, Mike Rapoport,
linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
On Thu, Jul 30, 2026 at 07:57:13AM +0000, Liu, Yuan1 wrote:
>> -----Original Message-----
>> From: Wei Yang <richard.weiyang@gmail.com>
>> Sent: Thursday, July 30, 2026 10:36 AM
>> To: Liu, Yuan1 <yuan1.liu@intel.com>
>> Cc: Wei Yang <richard.weiyang@gmail.com>; David Hildenbrand
>> <david@kernel.org>; Oscar Salvador <osalvador@suse.de>; Mike Rapoport
>> <rppt@kernel.org>; linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>;
>> Deng, Pan <pan.deng@intel.com>; Li, Tianyou <tianyou.li@intel.com>; Chen
>> Zhang <zhangchen.kidd@jd.com>; Zeng, Jason <jason.zeng@intel.com>; linux-
>> kernel@vger.kernel.org
>> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span()
>> subsection boundary checks
>>
>> On Mon, Jul 27, 2026 at 09:49:39AM +0000, Liu, Yuan1 wrote:
>> >> -----Original Message-----
>> >> From: Wei Yang <richard.weiyang@gmail.com>
>> >> Sent: Saturday, July 25, 2026 10:50 AM
>> >> To: Liu, Yuan1 <yuan1.liu@intel.com>
>> >> Cc: David Hildenbrand <david@kernel.org>; Oscar Salvador
>> >> <osalvador@suse.de>; Mike Rapoport <rppt@kernel.org>; Wei Yang
>> >> <richard.weiyang@gmail.com>; linux-mm@kvack.org; Zou, Nanhai
>> >> <nanhai.zou@intel.com>; Deng, Pan <pan.deng@intel.com>; Li, Tianyou
>> >> <tianyou.li@intel.com>; Chen Zhang <zhangchen.kidd@jd.com>; Zeng, Jason
>> >> <jason.zeng@intel.com>; linux-kernel@vger.kernel.org
>> >> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve
>> shrink_zone_span()
>> >> subsection boundary checks
>> >>
>> >> On Thu, Jul 23, 2026 at 04:49:46AM -0400, Yuan Liu wrote:
>> >> >When shrinking a zone span after removing a PFN range,
>> >> >find_smallest_section_pfn() and find_biggest_section_pfn()
>> >> >only checked one edge PFN in each subsection for nid/zone matching.
>> >> >
>> >> >If a memory or hole boundary falls in the middle of a subsection,
>> >> >that edge PFN may belong to a different nid/zone, causing the helpers
>> >> >to miss a valid PFN within that subsection.
>> >> >
>> >> >Fix this by checking both subsection edge PFNs for nid/zone matching.
>> >> >Keep a single pfn_to_online_page() check per subsection, since online
>> >> >state is the same for all PFNs in a subsection.
>> >> >
>> >> >Reviewed-by: Jason Zeng <jason.zeng@intel.com>
>> >> >Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
>> >> >---
>> >> > mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
>> >> > 1 file changed, 27 insertions(+), 15 deletions(-)
>> >> >
>> >> >diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
>> >> >index 4c699fd9c479..3a281d595207 100644
>> >> >--- a/mm/memory_hotplug.c
>> >> >+++ b/mm/memory_hotplug.c
>> >> >@@ -427,17 +427,24 @@ static unsigned long
>> find_smallest_section_pfn(int
>> >> nid, struct zone *zone,
>> >> > unsigned long start_pfn,
>> >> > unsigned long end_pfn)
>> >> > {
>> >> >- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
>> >> >- if (unlikely(!pfn_to_online_page(start_pfn)))
>> >> >- continue;
>> >> >+ unsigned long next_pfn;
>> >> >
>> >> >- if (unlikely(pfn_to_nid(start_pfn) != nid))
>> >> >- continue;
>> >> >+ for (; start_pfn < end_pfn; start_pfn = next_pfn) {
>> >> >+ unsigned long tail_pfn;
>> >> >
>> >> >- if (zone != page_zone(pfn_to_page(start_pfn)))
>> >> >+ next_pfn = start_pfn + PAGES_PER_SUBSECTION;
>> >> >+ tail_pfn = next_pfn - 1;
>> >> >+
>> >> >+ if (unlikely(!pfn_to_online_page(start_pfn)))
>> >> > continue;
>> >> >
>> >> >- return start_pfn;
>> >> >+ if (likely(pfn_to_nid(start_pfn) == nid) &&
>> >> >+ zone == page_zone(pfn_to_page(start_pfn)))
>> >> >+ return start_pfn;
>> >> >+
>> >> >+ if (likely(pfn_to_nid(tail_pfn) == nid) &&
>> >> >+ zone == page_zone(pfn_to_page(tail_pfn)))
>> >> >+ return start_pfn;
>> >>
>> >> Here we are checking range [start_pfn, tail_pfn]. When we come here, it
>> >> means
>> >> start_pfn's nid or zone doesn't match our expectation. But if tail_pfn
>> >> does,
>> >> why it still return start_pfn?
>> >
>> >Hi Wei
>> >
>> >If start_pfn falls into a hole while tail_pfn still belongs to a valid
>> >memblock in this zone, skipping the subsection would cause
>> >shrink_zone_span() to shrink the zone span too aggressively, excluding
>> >valid PFNs from the zone.
>> >
>> >Since init_unavailable_range() initializes hole pages with the correct
>> >zone/nid, the start_pfn check always succeeds here, making the tail_pfn
>> >check redundant today.
>> >
>> >That said, I wonder if it is still worth keeping this check so that
>> >shrink_zone_span() does not depend on how hole pages are initialized.
>> >
>>
>> Looks reasonable.
>>
>> I search the discussion history, and found David suggest this fix in [1]
>> with
>> following statement.
>>
>> Well, unless we have an odd case where the hole+memory starts in the
>> middle of a "PAGES_PER_SUBSECTION". That would already be problematic if
>> memory starts/ends in the middle of a PAGES_PER_SUBSECTION chunk. I
>> don't such a case exists.
>>
>> We could improve shrink_zone_span() to let
>> find_smallest_section_pfn/find_biggest_section_pfn test the pfn_to_nid()
>> and page_zone() not on;y on the smallest/highest pfn, but also on the
>> highest/smallest PFN in a PAGES_PER_SUBSECTION chunk.
>>
>> I am trying to understand the exact case David described, but not fully
>> get
>> it. Would you mind describing more, so we would make sure not missing the
>> point.
>>
>> [1]: https://lore.kernel.org/all/e86fee84-08d8-4563-8596-
>> e40d8e196799@kernel.org/T/#u
>
>I am not sure whether this scenario can occur on real systems, but
>from reading the current code it seems there may be a corner case.
>
>For example, consider a zone whose tail ends with a single-PFN hole.
>init_unavailable_range() initializes this hole page and assigns its
>nid/zone to the adjacent next zone, since the PFN itself belongs outside
>the current zone.
>
>When memory is hot-added after this hole, the hotplug granularity
>(PAGES_PER_SUBSECTION) extends the zone span to include the entire
>subsection containing the hole.
>
IIUC, for memory hotplug, the alignment is guaranteed by
check_hotplug_memory_range() with min block size which is section aligned.
>Later, when the hot-added memory is removed,
>find_biggest_section_pfn() examines the hole page first. Because its
>nid/zone belongs to the adjacent next zone, the boundary subsection may
>be considered removable even though it still contains boot memory pages.
>
>This appears to be asymmetric between the zone head and the zone tail.
>
>At the zone head (find_smallest_section_pfn()), the boundary subsection
>has a hole + memory layout. The hole pages belong to the current zone,
>so the subsection is retained.
>
>At the zone tail (find_biggest_section_pfn()), the boundary subsection
>has a memory + hole layout. The trailing hole pages belong to the
>adjacent next zone, so the subsection may be discarded.
>
I am trying to see what the real case it would be.
Hmm.. First I don't expect there would be memory hotplug happens between zones
or nodes. So the new added memory range always sits after boot memory(early
sections).
Then let's assume below memory layout after bootup.
|< Zone Normal >| |< Zone Movable >|
+----------------+.....+----------------+.....+
| |hole1| |hole2|
+----------------+.....+----------------+.....+
Both hole is less than subsection size, and the hole2 expand to section
aligned address. According to the init_unavailable_range() during
memmap_init(), both holes are init to Zone Movable.
Then we can hot-add next section and hot-online it to:
1) Zone Movable
2) Zone Normal
For case 1), if we offline this section, we run into
find_biggest_section_pfn() for Zone Movable. It test the last pfn in hole2,
and see it meets the condition. So Zone Movable results in spanning hole2,
which is larger than original value. This is not that accurate, but still
could work.
For case 2), if we offline this section, we run into
find_biggest_section_pfn() for Zone Normal. Here we also have two cases based
on the location of the hole:
a) hole starts at the beginning of subsection (hole + memory)
b) hole ends at the end of subsection (memory + hole)
For a), it looks good, because the subsection hole is treated as Zone
Movable, and Zone Normal is correctly sized.
For b), it seems not right, as we only test subsection's last pfn. As it
belongs to Zone Movable we would skip the subsection and set Normal Zone
span to previous subsection. Even some of the pfn belongs to Zone Normal.
Hmm... I guess this is the case David mentioned.
And with your patch, it checks head_pfn of this subsection, so it fixes this
by extend Zone Normal to just ahead Zone Movable. This works something like
a), but make Zone Normal span to some pfn initialized to Zone Movable.
Based on above analysis, let's see when find_smallest_section_pfn() will have
trouble.
The case in my mind is like below:
|< Zone Normal >|< Zone Movable >|
+----------------+----------------+.....+----------------+
| | |hole | |
+----------------+----------------+.....+----------------+
Hole should sits at the beginning of a section, otherwise offline_pages()
would prevent offline the range. Then when offline those beginning Movable
memory, find_smallest_section_pfn() is triggered. But as hole is init to Zone
Movable, it looks good.
On summary:
* find_biggest_section_pfn() may truncate zone span range if there is a
hole less than subsection sits at the end of subsection
* find_smallest_section_pfn() looks good even there is a hole less than
subsection
Above is my understanding about the possible cases when re-sizing zone. In
case missing some point, just let me know :-)
--
Wei Yang
Help you, Help me
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-07-23 8:49 [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Yuan Liu
2026-07-23 8:49 ` [PATCH v6 1/2] " Yuan Liu
2026-07-23 8:49 ` [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks Yuan Liu
@ 2026-08-05 9:40 ` Liu, Yuan1
2 siblings, 0 replies; 15+ messages in thread
From: Liu, Yuan1 @ 2026-08-05 9:40 UTC (permalink / raw)
To: David Hildenbrand
Cc: linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, Oscar Salvador, Mike Rapoport, Wei Yang,
linux-kernel@vger.kernel.org
Hi David,
The reworked implementation updates pages_with_online_memmap in
memmap_init_zone_range(), counting only PFNs that satisfy
pfn_to_online_page(). This eliminates the extra PFN range walk used
in the previous version.
Does this look like the right direction, or am I missing something?
I'd appreciate your thoughts.
Thanks!
Best Regards,
Liu, Yuan1
> -----Original Message-----
> From: Liu, Yuan1 <yuan1.liu@intel.com>
> Sent: Thursday, July 23, 2026 4:50 PM
> To: David Hildenbrand <david@kernel.org>; Oscar Salvador
> <osalvador@suse.de>; Mike Rapoport <rppt@kernel.org>; Wei Yang
> <richard.weiyang@gmail.com>
> Cc: linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>; Liu, Yuan1
> <yuan1.liu@intel.com>; Deng, Pan <pan.deng@intel.com>; Li, Tianyou
> <tianyou.li@intel.com>; Chen Zhang <zhangchen.kidd@jd.com>; Zeng, Jason
> <jason.zeng@intel.com>; linux-kernel@vger.kernel.org
> Subject: [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check
> when changing pfn range
>
> This series introduces a pages_with_online_memmap member into struct
> zone to avoid pageblock-by-pageblock scans across the entire zone and
> improve memory hotplug performance.
>
> Reworked how pages_with_online_memmap accounts for PFNs in early
> boot memory in response to Sashiko's review comments [2], which
> identified potential issues with the previous implementation.
>
> Approach
> ========
> Add a new zone member pages_with_online_memmap that tracks the number of
> pages within the zone span that have an online memory map (including
> present pages and memory holes smaller than a subsection whose memory
> map has been initialized). When spanned_pages == pages_with_online_memmap
> the zone is contiguous and pfn_to_page() can be called on any PFN in the
> zone span without further pfn_valid() checks.
>
> For early boot memory, pages_with_online_memmap is calculated in
> memmap_init_zone_range(). Since every PFN within a memblock region
> satisfies pfn_to_online_page(), the calculation aligns both boundaries
> of each memblock range to PAGES_PER_SUBSECTION and counts the aligned
> pages that fall within the zone span. If two adjacent memblock regions
> share a subsection (i.e. the hole between them is smaller than a
> subsection), the overlapping subsection pages are counted only once.
> For hotplugged memory, pages_with_online_memmap is updated through
> adjust_present_page_count(), which is called during memory online and
> offline.
>
> Only pages that fall within the current zone span are accounted towards
> pages_with_online_memmap. A "too small" value is safe, it merely
> prevents detecting a contiguous zone.
>
> The contiguity check using pages_with_online_memmap is stricter than the
> old pageblock-by-pageblock scan. The old set_zone_contiguous() iterated
> at pageblock granularity via pageblock_pfn_to_page(), so a zone could be
> marked contiguous even if a subsection-sized hole existed within a
> pageblock. The new check requires spanned_pages ==
> pages_with_online_memmap,
> meaning every PFN in the zone span must satisfy pfn_to_online_page().
>
> Performance
> ===========
> 1. For VM hotplug performance data, please refer to Patch 1.
> 2. This series also benefits CXL hotplug. Performance results are
> as follows
> https://lore.kernel.org/all/20260409023552.GA2807@AE/
>
> Tested cases
> ============
> 1. Hotplug/unplug correctness with both online_movable and online
> policies, including partial unplug, middle-block offline gaps, and
> edge-block span shrink.
> 2. Large scale (256G/512G) plug/unplug performance.
> 3. Boot-time subsection holes (aligned/unaligned, in-zone and
> cross-zone) with correct pages_with_online_memmap accounting.
> 4. kernelcore=mirror: verified no overcounting.
>
> Patch overview
> ==============
> This series is based on the kernelcore-mirror branch [1].
>
> Patch 1 introduces pages_with_online_memmap to replace
> pageblock-by-pageblock scans across the entire zone for zone contiguity
> checks.
>
> Patch 2 avoids incorrectly shrinking the zone span during memory unplug
> when a memory or hole boundary falls in the middle of a subsection.
>
> v6 changes:
> 1. Dropped patches 1-3 from v5. The kernelcore=mirror overlap issue
> that caused pages_with_online_memmap to be overcounted has been
> resolved by the series [1].
> 2. Reworked pages_with_online_memmap accounting during early boot.
>
> v5:
> https://lore.kernel.org/linux-mm/20260520093457.3719960-1-
> yuan1.liu@intel.com/
>
> v4:
> https://lore.kernel.org/linux-mm/20260421125508.2317429-1-
> yuan1.liu@intel.com/
>
> v3:
> https://lore.kernel.org/linux-mm/20260408031615.1831922-1-
> yuan1.liu@intel.com/
>
> [1]
> https://git.kernel.org/pub/scm/linux/kernel/git/rppt/memblock.git/log/?h=k
> ernelcore-mirror
> [2] https://sashiko.dev/#/patchset/20260520093457.3719960-3-
> yuan1.liu@intel.com
>
> Yuan Liu (2):
> mm/memory_hotplug: optimize zone contiguous check when changing pfn
> range
> mm/memory_hotplug: improve shrink_zone_span() subsection boundary
> checks
>
> Documentation/mm/physical_memory.rst | 13 +++++++
> drivers/base/memory.c | 6 +++
> include/linux/mmzone.h | 47 ++++++++++++++++++++++
> mm/internal.h | 8 +---
> mm/memory_hotplug.c | 54 ++++++++++++++------------
> mm/mm_init.c | 58 +++++++++++++++++-----------
> 6 files changed, 132 insertions(+), 54 deletions(-)
>
>
> base-commit: b69c2a1fa9beb4c3db24b4013e07f5cb7e7260aa
> --
> 2.47.3
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-07-23 8:49 ` [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks Yuan Liu
2026-07-25 2:49 ` Wei Yang
@ 2026-08-05 11:02 ` David Hildenbrand (Arm)
2026-08-06 7:14 ` Liu, Yuan1
1 sibling, 1 reply; 15+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-05 11:02 UTC (permalink / raw)
To: Yuan Liu, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm, Nanhai Zou, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
On 7/23/26 10:49, Yuan Liu wrote:
> When shrinking a zone span after removing a PFN range,
> find_smallest_section_pfn() and find_biggest_section_pfn()
> only checked one edge PFN in each subsection for nid/zone matching.
>
> If a memory or hole boundary falls in the middle of a subsection,
> that edge PFN may belong to a different nid/zone, causing the helpers
> to miss a valid PFN within that subsection.
>
> Fix this by checking both subsection edge PFNs for nid/zone matching.
> Keep a single pfn_to_online_page() check per subsection, since online
> state is the same for all PFNs in a subsection.
>
> Reviewed-by: Jason Zeng <jason.zeng@intel.com>
> Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
> ---
> mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
> 1 file changed, 27 insertions(+), 15 deletions(-)
>
> diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
> index 4c699fd9c479..3a281d595207 100644
> --- a/mm/memory_hotplug.c
> +++ b/mm/memory_hotplug.c
> @@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int nid, struct zone *zone,
> unsigned long start_pfn,
> unsigned long end_pfn)
> {
> - for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> - if (unlikely(!pfn_to_online_page(start_pfn)))
> - continue;
> + unsigned long next_pfn;
>
> - if (unlikely(pfn_to_nid(start_pfn) != nid))
> - continue;
> + for (; start_pfn < end_pfn; start_pfn = next_pfn) {
> + unsigned long tail_pfn;
>
> - if (zone != page_zone(pfn_to_page(start_pfn)))
> + next_pfn = start_pfn + PAGES_PER_SUBSECTION;
> + tail_pfn = next_pfn - 1;
> +
> + if (unlikely(!pfn_to_online_page(start_pfn)))
> continue;
>
> - return start_pfn;
> + if (likely(pfn_to_nid(start_pfn) == nid) &&
> + zone == page_zone(pfn_to_page(start_pfn)))
> + return start_pfn;
> +
> + if (likely(pfn_to_nid(tail_pfn) == nid) &&
> + zone == page_zone(pfn_to_page(tail_pfn)))
> + return start_pfn;
> }
>
> return 0;
> @@ -448,21 +455,26 @@ static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
> unsigned long start_pfn,
> unsigned long end_pfn)
> {
> - unsigned long pfn;
> + unsigned long pfn, prev_pfn;
>
> /* pfn is the end pfn of a memory section. */
> pfn = end_pfn - 1;
> - for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
> - if (unlikely(!pfn_to_online_page(pfn)))
> - continue;
> + for (; pfn >= start_pfn; pfn = prev_pfn) {
> + unsigned long head_pfn;
>
> - if (unlikely(pfn_to_nid(pfn) != nid))
> - continue;
> + prev_pfn = pfn - PAGES_PER_SUBSECTION;
> + head_pfn = prev_pfn + 1;
>
> - if (zone != page_zone(pfn_to_page(pfn)))
> + if (unlikely(!pfn_to_online_page(pfn)))
> continue;
>
> - return pfn;
> + if (likely(pfn_to_nid(pfn) == nid) &&
> + zone == page_zone(pfn_to_page(pfn)))
> + return pfn;
> +
> + if (likely(pfn_to_nid(head_pfn) == nid) &&
> + zone == page_zone(pfn_to_page(head_pfn)))
> + return pfn;
> }
>
> return 0;
I think improving that should be patch #1.
But I think we can do much better code-wise. What about the following
cleanup instead:
From 6e9ea094daf8d04c0b119b780572a846334f8164 Mon Sep 17 00:00:00 2001
From: "David Hildenbrand (Arm)" <david@kernel.org>
Date: Wed, 5 Aug 2026 12:52:55 +0200
Subject: [PATCH] mm/memory_hotplug: make shrink_zone_span() more robust
Let's make shrink_zone_span() more robust by checking in
find_smallest_section_pfn() / find_biggest_section_pfn() that start+end
of the subsection.
While at it, clean up the function heavily, factoring the core check
out into subsection_overlaps_zone().
There likely is no need to check for the nid first: we require
SPARSEMEM_VMEMMAP_ENABLE where pfn_to_page() is cheap, and the
pfn_to_nid() on CONFIG_NUMA would do a pfn_to_page() either way. So
let's just drop that for now.
Signed-off-by: David Hildenbrand (Arm) <david@kernel.org>
---
mm/memory_hotplug.c | 59 ++++++++++++++++++---------------------------
1 file changed, 24 insertions(+), 35 deletions(-)
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 226ab9cb078ad..79937f9672d01 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -425,49 +425,39 @@ int __add_pages(int nid, unsigned long pfn, unsigned long nr_pages,
return err;
}
-/* find the smallest valid pfn in the range [start_pfn, end_pfn) */
-static unsigned long find_smallest_section_pfn(int nid, struct zone *zone,
- unsigned long start_pfn,
- unsigned long end_pfn)
+static bool subsection_overlaps_zone(unsigned long pfn, struct zone *zone)
{
- for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
- if (unlikely(!pfn_to_online_page(start_pfn)))
- continue;
+ const unsigned long start_pfn = ALIGN_DOWN(pfn, PAGES_PER_SUBSECTION);
+ const unsigned long end_pfn = start_pfn + PAGES_PER_SUBSECTION - 1;
- if (unlikely(pfn_to_nid(start_pfn) != nid))
- continue;
+ /* All pages in a subsection are either online or offline. */
+ if (unlikely(!pfn_to_online_page(start_pfn)))
+ return false;
- if (zone != page_zone(pfn_to_page(start_pfn)))
- continue;
+ /* Checking start+end is sufficient. */
+ return zone == page_zone(pfn_to_page(start_pfn)) ||
+ zone == page_zone(pfn_to_page(end_pfn));
+}
- return start_pfn;
+/* find the smallest valid pfn in the range [start_pfn, end_pfn) */
+static unsigned long find_smallest_section_pfn(struct zone *zone,
+ unsigned long start_pfn, unsigned long end_pfn)
+{
+ for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
+ if (subsection_overlaps_zone(start_pfn, zone))
+ return start_pfn;
}
-
return 0;
}
/* find the biggest valid pfn in the range [start_pfn, end_pfn). */
-static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
- unsigned long start_pfn,
- unsigned long end_pfn)
+static unsigned long find_biggest_section_pfn(struct zone *zone,
+ unsigned long start_pfn, unsigned long end_pfn)
{
- unsigned long pfn;
-
- /* pfn is the end pfn of a memory section. */
- pfn = end_pfn - 1;
- for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
- if (unlikely(!pfn_to_online_page(pfn)))
- continue;
-
- if (unlikely(pfn_to_nid(pfn) != nid))
- continue;
-
- if (zone != page_zone(pfn_to_page(pfn)))
- continue;
-
- return pfn;
+ for (; end_pfn >= start_pfn; end_pfn -= PAGES_PER_SUBSECTION) {
+ if (subsection_overlaps_zone(end_pfn - 1, zone))
+ return end_pfn - 1;
}
-
return 0;
}
@@ -475,7 +465,6 @@ static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
unsigned long end_pfn)
{
unsigned long pfn;
- int nid = zone_to_nid(zone);
if (zone->zone_start_pfn == start_pfn) {
/*
@@ -484,7 +473,7 @@ static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
* In this case, we find second smallest valid mem_section
* for shrinking zone.
*/
- pfn = find_smallest_section_pfn(nid, zone, end_pfn,
+ pfn = find_smallest_section_pfn(zone, end_pfn,
zone_end_pfn(zone));
if (pfn) {
zone->spanned_pages = zone_end_pfn(zone) - pfn;
@@ -500,7 +489,7 @@ static void shrink_zone_span(struct zone *zone, unsigned long start_pfn,
* In this case, we find second biggest valid mem_section for
* shrinking zone.
*/
- pfn = find_biggest_section_pfn(nid, zone, zone->zone_start_pfn,
+ pfn = find_biggest_section_pfn(zone, zone->zone_start_pfn,
start_pfn);
if (pfn)
zone->spanned_pages = pfn - zone->zone_start_pfn + 1;
--
2.43.0
--
Cheers,
David
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-07-23 8:49 ` [PATCH v6 1/2] " Yuan Liu
@ 2026-08-05 11:53 ` David Hildenbrand (Arm)
2026-08-06 7:23 ` Liu, Yuan1
0 siblings, 1 reply; 15+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-05 11:53 UTC (permalink / raw)
To: Yuan Liu, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm, Nanhai Zou, Pan Deng, Tianyou Li, Chen Zhang,
Jason Zeng, linux-kernel
On 7/23/26 10:49, Yuan Liu wrote:
> When move_pfn_range_to_zone() or remove_pfn_range_from_zone() updates a
> zone, set_zone_contiguous() rescans the entire zone
> pageblock-by-pageblock to rebuild zone->contiguous. For large zones this
> is a significant cost during memory hotplug and hot-unplug.
>
> Add a new zone member pages_with_online_memmap that tracks the number of
> pages within the zone span that have an online memory map (including
> present pages and memory holes smaller than a subsection whose memory
> map has been initialized). When spanned_pages == pages_with_online_memmap
> the zone is contiguous and pfn_to_page() can be called on any PFN in the
> zone span without further pfn_valid() checks.
>
> For early boot memory, pages_with_online_memmap is calculated in
> memmap_init_zone_range(). Since every PFN within a memblock region
> satisfies pfn_to_online_page(), the calculation aligns both boundaries
> of each memblock range to PAGES_PER_SUBSECTION and counts the aligned
> pages that fall within the zone span. If two adjacent memblock regions
> share a subsection (i.e. the hole between them is smaller than a
> subsection), the overlapping subsection pages are counted only once.
> For hotplugged memory, pages_with_online_memmap is updated through
> adjust_present_page_count(), which is called during memory online and
> offline.
>
> Only pages that fall within the current zone span are accounted towards
> pages_with_online_memmap. A "too small" value is safe, it merely
> prevents detecting a contiguous zone.
>
> The contiguity check using pages_with_online_memmap is stricter than the
> old pageblock-by-pageblock scan. The old set_zone_contiguous() iterated
> at pageblock granularity via pageblock_pfn_to_page(), so a zone could be
> marked contiguous even if a subsection-sized hole existed within a
> pageblock. The new check requires
> spanned_pages == pages_with_online_memmap, meaning every PFN in the zone
> span must satisfy pfn_to_online_page().
>
> The following test cases of memory hotplug for a VM [1], tested in the
> environment [2], show that this optimization can significantly reduce
> the memory hotplug time [3].
>
> +----------------+------+---------------+--------------+----------------+
> | | Size | Time (before) | Time (after) | Time Reduction |
> | +------+---------------+--------------+----------------+
> | Plug Memory | 256G | 10s | 3s | 70% |
> | +------+---------------+--------------+----------------+
> | | 512G | 36s | 7s | 81% |
> +----------------+------+---------------+--------------+----------------+
>
> +----------------+------+---------------+--------------+----------------+
> | | Size | Time (before) | Time (after) | Time Reduction |
> | +------+---------------+--------------+----------------+
> | Unplug Memory | 256G | 11s | 4s | 64% |
> | +------+---------------+--------------+----------------+
> | | 512G | 36s | 9s | 75% |
> +----------------+------+---------------+--------------+----------------+
>
> [1] Qemu commands to hotplug 256G/512G memory for a VM:
> object_add memory-backend-ram,id=hotmem0,size=256G/512G,share=on
> device_add virtio-mem-pci,id=vmem1,memdev=hotmem0,bus=port1
> qom-set vmem1 requested-size 256G/512G (Plug Memory)
> qom-set vmem1 requested-size 0G (Unplug Memory)
>
> [2] Hardware : Intel Icelake server
> Guest Kernel : v7.0-rc4
> Qemu : v9.0.0
>
> Launch VM :
> qemu-system-x86_64 -accel kvm -cpu host \
> -drive file=./Centos10_cloud.qcow2,format=qcow2,if=virtio \
> -drive file=./seed.img,format=raw,if=virtio \
> -smp 3,cores=3,threads=1,sockets=1,maxcpus=3 \
> -m 2G,slots=10,maxmem=2052472M \
> -device pcie-root-port,id=port1,bus=pcie.0,slot=1,multifunction=on \
> -device pcie-root-port,id=port2,bus=pcie.0,slot=2 \
> -nographic -machine q35 \
> -nic user,hostfwd=tcp::3000-:22
>
> Guest kernel auto-onlines newly added memory blocks:
> echo online > /sys/devices/system/memory/auto_online_blocks
>
> [3] The time from typing the QEMU commands in [1] to when the output of
> 'grep MemTotal /proc/meminfo' on Guest reflects that all hotplugged
> memory is recognized.
>
> Reported-by: Nanhai Zou <nanhai.zou@intel.com>
> Reported-by: Chen Zhang <zhangchen.kidd@jd.com>
> Reviewed-by: Pan Deng <pan.deng@intel.com>
> Reviewed-by: Jason Zeng <jason.zeng@intel.com>
> Co-developed-by: Tianyou Li <tianyou.li@intel.com>
> Signed-off-by: Tianyou Li <tianyou.li@intel.com>
> Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
> ---
> Documentation/mm/physical_memory.rst | 13 +++++++
> drivers/base/memory.c | 6 +++
> include/linux/mmzone.h | 47 ++++++++++++++++++++++
> mm/internal.h | 8 +---
> mm/memory_hotplug.c | 12 +-----
> mm/mm_init.c | 58 +++++++++++++++++-----------
> 6 files changed, 105 insertions(+), 39 deletions(-)
>
> diff --git a/Documentation/mm/physical_memory.rst b/Documentation/mm/physical_memory.rst
> index b76183545e5b..0aa65e6b5499 100644
> --- a/Documentation/mm/physical_memory.rst
> +++ b/Documentation/mm/physical_memory.rst
> @@ -483,6 +483,19 @@ General
> ``present_pages`` should use ``get_online_mems()`` to get a stable value. It
> is initialized by ``calculate_node_totalpages()``.
>
> +``pages_with_online_memmap``
> + Tracks pages within the zone that have an online memory map (present pages
> + and memory holes whose memory map has been initialized). When
> + ``spanned_pages`` == ``pages_with_online_memmap``, ``pfn_to_page()`` can be
> + performed without further checks on any PFN within the zone span.
> +
> + Note: this counter may temporarily undercount when pages with an online
> + memory map exist outside the current zone span. This can only happen during
> + boot, when initializing the memory map of pages that do not fall into any
> + zone span. Growing the zone to cover such pages and later shrinking it back
> + may result in a "too small" value. This is safe: it merely prevents
> + detecting a contiguous zone.
It's suboptimal that we repeat the same comment that we already have in struct
zone. Can we just keep it vry simple here?
"Pages within the zone that have an online memory map: present pages and memory
holes whose memory map has been initialized. See XXX for more details."
> +
> ``present_early_pages``
> The present pages existing within the zone located on memory available since
> early boot, excluding hotplugged memory. Defined only when
> diff --git a/drivers/base/memory.c b/drivers/base/memory.c
> index bcfe2d9f4adb..237ace435372 100644
> --- a/drivers/base/memory.c
> +++ b/drivers/base/memory.c
> @@ -246,6 +246,7 @@ static int memory_block_online(struct memory_block *mem)
> nr_vmemmap_pages = mem->altmap->free;
>
> mem_hotplug_begin();
> + clear_zone_contiguous(zone);
> if (nr_vmemmap_pages) {
> ret = mhp_init_memmap_on_memory(start_pfn, nr_vmemmap_pages, zone);
> if (ret)
> @@ -270,6 +271,7 @@ static int memory_block_online(struct memory_block *mem)
>
> mem->zone = zone;
> out:
> + set_zone_contiguous(zone);
> mem_hotplug_done();
> return ret;
> }
> @@ -282,6 +284,7 @@ static int memory_block_offline(struct memory_block *mem)
> unsigned long start_pfn = section_nr_to_pfn(mem->start_section_nr);
> unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
> unsigned long nr_vmemmap_pages = 0;
> + struct zone *zone;
Why the temporary variable, and why not initialize it directly here? Note that
> int ret;
>
> if (!mem->zone)
We already use mem->zone here. So if you add a variable, convert that one as
well. But I guess we can just life without one.
> @@ -294,7 +297,9 @@ static int memory_block_offline(struct memory_block *mem)
> if (mem->altmap)
> nr_vmemmap_pages = mem->altmap->free;
>
> + zone = mem->zone;
> mem_hotplug_begin();
> + clear_zone_contiguous(zone);
> if (nr_vmemmap_pages)
> adjust_present_page_count(pfn_to_page(start_pfn), mem->group,
> -nr_vmemmap_pages);
> @@ -314,6 +319,7 @@ static int memory_block_offline(struct memory_block *mem)
>
> mem->zone = NULL;
> out:
> + set_zone_contiguous(zone);
> mem_hotplug_done();
> return ret;
> }
[...]
> +static inline void set_zone_contiguous(struct zone *zone)
> +{
> + if (zone_is_zone_device(zone))
> + return;
> + if (zone->spanned_pages == zone->pages_with_online_memmap)
> + zone->contiguous = true;
Maybe it was already discussed (and I recall that we previously had that), but I
think we really need READ_ONCE semantics here and WRITE_ONCE semantics in memory
hotplug code. Otherwise concurrent updates could lead to weird things when the
compiler does load-tearing.
[...]
>
> +static void __init update_zone_online_memmap_pages(struct zone *zone,
> + unsigned long start_pfn,
> + unsigned long end_pfn,
> + unsigned long *hole_pfn)
> +{
> +#ifdef CONFIG_SPARSEMEM_VMEMMAP
> + unsigned long zone_start_pfn = zone->zone_start_pfn;
> + unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
These two can be const.
> + unsigned long sub_start, sub_end;
> +
> + sub_start = max(ALIGN_DOWN(start_pfn, PAGES_PER_SUBSECTION),
> + zone_start_pfn);
> + sub_end = min(ALIGN(end_pfn, PAGES_PER_SUBSECTION), zone_end_pfn);
Hm, I don't immediately understand why we do the PAGES_PER_SUBSECTION thing
here. Why is that required?
--
Cheers,
David
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks
2026-08-05 11:02 ` David Hildenbrand (Arm)
@ 2026-08-06 7:14 ` Liu, Yuan1
0 siblings, 0 replies; 15+ messages in thread
From: Liu, Yuan1 @ 2026-08-06 7:14 UTC (permalink / raw)
To: David Hildenbrand (Arm), Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
> -----Original Message-----
> From: David Hildenbrand (Arm) <david@kernel.org>
> Sent: Wednesday, August 5, 2026 7:02 PM
> To: Liu, Yuan1 <yuan1.liu@intel.com>; Oscar Salvador <osalvador@suse.de>;
> Mike Rapoport <rppt@kernel.org>; Wei Yang <richard.weiyang@gmail.com>
> Cc: linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>; Deng, Pan
> <pan.deng@intel.com>; Li, Tianyou <tianyou.li@intel.com>; Chen Zhang
> <zhangchen.kidd@jd.com>; Zeng, Jason <jason.zeng@intel.com>; linux-
> kernel@vger.kernel.org
> Subject: Re: [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span()
> subsection boundary checks
>
> On 7/23/26 10:49, Yuan Liu wrote:
> > When shrinking a zone span after removing a PFN range,
> > find_smallest_section_pfn() and find_biggest_section_pfn()
> > only checked one edge PFN in each subsection for nid/zone matching.
> >
> > If a memory or hole boundary falls in the middle of a subsection,
> > that edge PFN may belong to a different nid/zone, causing the helpers
> > to miss a valid PFN within that subsection.
> >
> > Fix this by checking both subsection edge PFNs for nid/zone matching.
> > Keep a single pfn_to_online_page() check per subsection, since online
> > state is the same for all PFNs in a subsection.
> >
> > Reviewed-by: Jason Zeng <jason.zeng@intel.com>
> > Signed-off-by: Yuan Liu <yuan1.liu@intel.com>
> > ---
> > mm/memory_hotplug.c | 42 +++++++++++++++++++++++++++---------------
> > 1 file changed, 27 insertions(+), 15 deletions(-)
> >
> > diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
> > index 4c699fd9c479..3a281d595207 100644
> > --- a/mm/memory_hotplug.c
> > +++ b/mm/memory_hotplug.c
> > @@ -427,17 +427,24 @@ static unsigned long find_smallest_section_pfn(int
> nid, struct zone *zone,
> > unsigned long start_pfn,
> > unsigned long end_pfn)
> > {
> > - for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> > - if (unlikely(!pfn_to_online_page(start_pfn)))
> > - continue;
> > + unsigned long next_pfn;
> >
> > - if (unlikely(pfn_to_nid(start_pfn) != nid))
> > - continue;
> > + for (; start_pfn < end_pfn; start_pfn = next_pfn) {
> > + unsigned long tail_pfn;
> >
> > - if (zone != page_zone(pfn_to_page(start_pfn)))
> > + next_pfn = start_pfn + PAGES_PER_SUBSECTION;
> > + tail_pfn = next_pfn - 1;
> > +
> > + if (unlikely(!pfn_to_online_page(start_pfn)))
> > continue;
> >
> > - return start_pfn;
> > + if (likely(pfn_to_nid(start_pfn) == nid) &&
> > + zone == page_zone(pfn_to_page(start_pfn)))
> > + return start_pfn;
> > +
> > + if (likely(pfn_to_nid(tail_pfn) == nid) &&
> > + zone == page_zone(pfn_to_page(tail_pfn)))
> > + return start_pfn;
> > }
> >
> > return 0;
> > @@ -448,21 +455,26 @@ static unsigned long find_biggest_section_pfn(int
> nid, struct zone *zone,
> > unsigned long start_pfn,
> > unsigned long end_pfn)
> > {
> > - unsigned long pfn;
> > + unsigned long pfn, prev_pfn;
> >
> > /* pfn is the end pfn of a memory section. */
> > pfn = end_pfn - 1;
> > - for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
> > - if (unlikely(!pfn_to_online_page(pfn)))
> > - continue;
> > + for (; pfn >= start_pfn; pfn = prev_pfn) {
> > + unsigned long head_pfn;
> >
> > - if (unlikely(pfn_to_nid(pfn) != nid))
> > - continue;
> > + prev_pfn = pfn - PAGES_PER_SUBSECTION;
> > + head_pfn = prev_pfn + 1;
> >
> > - if (zone != page_zone(pfn_to_page(pfn)))
> > + if (unlikely(!pfn_to_online_page(pfn)))
> > continue;
> >
> > - return pfn;
> > + if (likely(pfn_to_nid(pfn) == nid) &&
> > + zone == page_zone(pfn_to_page(pfn)))
> > + return pfn;
> > +
> > + if (likely(pfn_to_nid(head_pfn) == nid) &&
> > + zone == page_zone(pfn_to_page(head_pfn)))
> > + return pfn;
> > }
> >
> > return 0;
>
>
> I think improving that should be patch #1.
>
> But I think we can do much better code-wise. What about the following
> cleanup instead:
>
>
> From 6e9ea094daf8d04c0b119b780572a846334f8164 Mon Sep 17 00:00:00 2001
> From: "David Hildenbrand (Arm)" <david@kernel.org>
> Date: Wed, 5 Aug 2026 12:52:55 +0200
> Subject: [PATCH] mm/memory_hotplug: make shrink_zone_span() more robust
>
> Let's make shrink_zone_span() more robust by checking in
> find_smallest_section_pfn() / find_biggest_section_pfn() that start+end
> of the subsection.
>
> While at it, clean up the function heavily, factoring the core check
> out into subsection_overlaps_zone().
>
> There likely is no need to check for the nid first: we require
> SPARSEMEM_VMEMMAP_ENABLE where pfn_to_page() is cheap, and the
> pfn_to_nid() on CONFIG_NUMA would do a pfn_to_page() either way. So
> let's just drop that for now.
>
> Signed-off-by: David Hildenbrand (Arm) <david@kernel.org>
> ---
> mm/memory_hotplug.c | 59 ++++++++++++++++++---------------------------
> 1 file changed, 24 insertions(+), 35 deletions(-)
>
> diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
> index 226ab9cb078ad..79937f9672d01 100644
> --- a/mm/memory_hotplug.c
> +++ b/mm/memory_hotplug.c
> @@ -425,49 +425,39 @@ int __add_pages(int nid, unsigned long pfn, unsigned
> long nr_pages,
> return err;
> }
>
> -/* find the smallest valid pfn in the range [start_pfn, end_pfn) */
> -static unsigned long find_smallest_section_pfn(int nid, struct zone
> *zone,
> - unsigned long start_pfn,
> - unsigned long end_pfn)
> +static bool subsection_overlaps_zone(unsigned long pfn, struct zone
> *zone)
> {
> - for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> - if (unlikely(!pfn_to_online_page(start_pfn)))
> - continue;
> + const unsigned long start_pfn = ALIGN_DOWN(pfn,
> PAGES_PER_SUBSECTION);
> + const unsigned long end_pfn = start_pfn + PAGES_PER_SUBSECTION - 1;
>
> - if (unlikely(pfn_to_nid(start_pfn) != nid))
> - continue;
> + /* All pages in a subsection are either online or offline. */
> + if (unlikely(!pfn_to_online_page(start_pfn)))
> + return false;
>
> - if (zone != page_zone(pfn_to_page(start_pfn)))
> - continue;
> + /* Checking start+end is sufficient. */
> + return zone == page_zone(pfn_to_page(start_pfn)) ||
> + zone == page_zone(pfn_to_page(end_pfn));
> +}
>
> - return start_pfn;
> +/* find the smallest valid pfn in the range [start_pfn, end_pfn) */
> +static unsigned long find_smallest_section_pfn(struct zone *zone,
> + unsigned long start_pfn, unsigned long end_pfn)
> +{
> + for (; start_pfn < end_pfn; start_pfn += PAGES_PER_SUBSECTION) {
> + if (subsection_overlaps_zone(start_pfn, zone))
> + return start_pfn;
> }
> -
> return 0;
> }
>
> /* find the biggest valid pfn in the range [start_pfn, end_pfn). */
> -static unsigned long find_biggest_section_pfn(int nid, struct zone *zone,
> - unsigned long start_pfn,
> - unsigned long end_pfn)
> +static unsigned long find_biggest_section_pfn(struct zone *zone,
> + unsigned long start_pfn, unsigned long end_pfn)
> {
> - unsigned long pfn;
> -
> - /* pfn is the end pfn of a memory section. */
> - pfn = end_pfn - 1;
> - for (; pfn >= start_pfn; pfn -= PAGES_PER_SUBSECTION) {
> - if (unlikely(!pfn_to_online_page(pfn)))
> - continue;
> -
> - if (unlikely(pfn_to_nid(pfn) != nid))
> - continue;
> -
> - if (zone != page_zone(pfn_to_page(pfn)))
> - continue;
> -
> - return pfn;
> + for (; end_pfn >= start_pfn; end_pfn -= PAGES_PER_SUBSECTION) {
> + if (subsection_overlaps_zone(end_pfn - 1, zone))
> + return end_pfn - 1;
> }
> -
> return 0;
> }
>
> @@ -475,7 +465,6 @@ static void shrink_zone_span(struct zone *zone,
> unsigned long start_pfn,
> unsigned long end_pfn)
> {
> unsigned long pfn;
> - int nid = zone_to_nid(zone);
>
> if (zone->zone_start_pfn == start_pfn) {
> /*
> @@ -484,7 +473,7 @@ static void shrink_zone_span(struct zone *zone,
> unsigned long start_pfn,
> * In this case, we find second smallest valid mem_section
> * for shrinking zone.
> */
> - pfn = find_smallest_section_pfn(nid, zone, end_pfn,
> + pfn = find_smallest_section_pfn(zone, end_pfn,
> zone_end_pfn(zone));
> if (pfn) {
> zone->spanned_pages = zone_end_pfn(zone) - pfn;
> @@ -500,7 +489,7 @@ static void shrink_zone_span(struct zone *zone,
> unsigned long start_pfn,
> * In this case, we find second biggest valid mem_section for
> * shrinking zone.
> */
> - pfn = find_biggest_section_pfn(nid, zone, zone-
> >zone_start_pfn,
> + pfn = find_biggest_section_pfn(zone, zone->zone_start_pfn,
> start_pfn);
> if (pfn)
> zone->spanned_pages = pfn - zone->zone_start_pfn + 1;
Hi David
Thanks for drafting this patch and for the guidance. I'll fold it into
the next revision as patch 1.
>
>
> --
> Cheers,
>
> David
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-08-05 11:53 ` David Hildenbrand (Arm)
@ 2026-08-06 7:23 ` Liu, Yuan1
2026-08-06 8:45 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 15+ messages in thread
From: Liu, Yuan1 @ 2026-08-06 7:23 UTC (permalink / raw)
To: David Hildenbrand (Arm), Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
> -----Original Message-----
> From: David Hildenbrand (Arm) <david@kernel.org>
> Sent: Wednesday, August 5, 2026 7:53 PM
> To: Liu, Yuan1 <yuan1.liu@intel.com>; Oscar Salvador <osalvador@suse.de>;
> Mike Rapoport <rppt@kernel.org>; Wei Yang <richard.weiyang@gmail.com>
> Cc: linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>; Deng, Pan
> <pan.deng@intel.com>; Li, Tianyou <tianyou.li@intel.com>; Chen Zhang
> <zhangchen.kidd@jd.com>; Zeng, Jason <jason.zeng@intel.com>; linux-
> kernel@vger.kernel.org
> Subject: Re: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous
> check when changing pfn range
>
[...]
> >
> > diff --git a/Documentation/mm/physical_memory.rst
> b/Documentation/mm/physical_memory.rst
> > index b76183545e5b..0aa65e6b5499 100644
> > --- a/Documentation/mm/physical_memory.rst
> > +++ b/Documentation/mm/physical_memory.rst
> > @@ -483,6 +483,19 @@ General
> > ``present_pages`` should use ``get_online_mems()`` to get a stable
> value. It
> > is initialized by ``calculate_node_totalpages()``.
> >
> > +``pages_with_online_memmap``
> > + Tracks pages within the zone that have an online memory map (present
> pages
> > + and memory holes whose memory map has been initialized). When
> > + ``spanned_pages`` == ``pages_with_online_memmap``, ``pfn_to_page()``
> can be
> > + performed without further checks on any PFN within the zone span.
> > +
> > + Note: this counter may temporarily undercount when pages with an
> online
> > + memory map exist outside the current zone span. This can only happen
> during
> > + boot, when initializing the memory map of pages that do not fall into
> any
> > + zone span. Growing the zone to cover such pages and later shrinking
> it back
> > + may result in a "too small" value. This is safe: it merely prevents
> > + detecting a contiguous zone.
>
> It's suboptimal that we repeat the same comment that we already have in
> struct
> zone. Can we just keep it vry simple here?
>
> "Pages within the zone that have an online memory map: present pages and
> memory
> holes whose memory map has been initialized. See XXX for more details."
Thanks for pointing out, I will simplify to this short description.
> > +
> > ``present_early_pages``
> > The present pages existing within the zone located on memory
> available since
> > early boot, excluding hotplugged memory. Defined only when
> > diff --git a/drivers/base/memory.c b/drivers/base/memory.c
> > index bcfe2d9f4adb..237ace435372 100644
> > --- a/drivers/base/memory.c
> > +++ b/drivers/base/memory.c
> > @@ -246,6 +246,7 @@ static int memory_block_online(struct memory_block
> *mem)
> > nr_vmemmap_pages = mem->altmap->free;
> >
> > mem_hotplug_begin();
> > + clear_zone_contiguous(zone);
> > if (nr_vmemmap_pages) {
> > ret = mhp_init_memmap_on_memory(start_pfn, nr_vmemmap_pages,
> zone);
> > if (ret)
> > @@ -270,6 +271,7 @@ static int memory_block_online(struct memory_block
> *mem)
> >
> > mem->zone = zone;
> > out:
> > + set_zone_contiguous(zone);
> > mem_hotplug_done();
> > return ret;
> > }
> > @@ -282,6 +284,7 @@ static int memory_block_offline(struct memory_block
> *mem)
> > unsigned long start_pfn = section_nr_to_pfn(mem->start_section_nr);
> > unsigned long nr_pages = PAGES_PER_SECTION * sections_per_block;
> > unsigned long nr_vmemmap_pages = 0;
> > + struct zone *zone;
>
> Why the temporary variable, and why not initialize it directly here? Note
> that
>
> > int ret;
> >
> > if (!mem->zone)
>
> We already use mem->zone here. So if you add a variable, convert that one
> as
> well. But I guess we can just life without one.
Will remove the temporary variable in next version.
> > @@ -294,7 +297,9 @@ static int memory_block_offline(struct memory_block
> *mem)
> > if (mem->altmap)
> > nr_vmemmap_pages = mem->altmap->free;
> >
> > + zone = mem->zone;
> > mem_hotplug_begin();
> > + clear_zone_contiguous(zone);
> > if (nr_vmemmap_pages)
> > adjust_present_page_count(pfn_to_page(start_pfn), mem->group,
> > -nr_vmemmap_pages);
> > @@ -314,6 +319,7 @@ static int memory_block_offline(struct memory_block
> *mem)
> >
> > mem->zone = NULL;
> > out:
> > + set_zone_contiguous(zone);
> > mem_hotplug_done();
> > return ret;
> > }
>
> [...]
>
> > +static inline void set_zone_contiguous(struct zone *zone)
> > +{
> > + if (zone_is_zone_device(zone))
> > + return;
> > + if (zone->spanned_pages == zone->pages_with_online_memmap)
> > + zone->contiguous = true;
>
> Maybe it was already discussed (and I recall that we previously had that),
> but I
> think we really need READ_ONCE semantics here and WRITE_ONCE semantics in
> memory
> hotplug code. Otherwise concurrent updates could lead to weird things when
> the
> compiler does load-tearing.
Thanks for your guidance. I should have considered the
READ_ONCE/WRITE_ONCE semantics here. I'll add them in the next
Revision.
> [...]
>
> >
> > +static void __init update_zone_online_memmap_pages(struct zone *zone,
> > + unsigned long start_pfn,
> > + unsigned long end_pfn,
> > + unsigned long *hole_pfn)
> > +{
> > +#ifdef CONFIG_SPARSEMEM_VMEMMAP
> > + unsigned long zone_start_pfn = zone->zone_start_pfn;
> > + unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
>
> These two can be const.
Will do.
> > + unsigned long sub_start, sub_end;
> > +
> > + sub_start = max(ALIGN_DOWN(start_pfn, PAGES_PER_SUBSECTION),
> > + zone_start_pfn);
> > + sub_end = min(ALIGN(end_pfn, PAGES_PER_SUBSECTION), zone_end_pfn);
>
> Hm, I don't immediately understand why we do the PAGES_PER_SUBSECTION
> thing
> here. Why is that required?
pages_with_online_memmap counts all PFNs where pfn_to_online_page() is
valid. With CONFIG_SPARSEMEM_VMEMMAP, pfn_section_valid() operates at
PAGES_PER_SUBSECTION granularity — when any page in a subsection has
memory, the entire subsection is valid/online. So we align to subsection
boundaries to include hole pages within partially-populated subsections.
I can also add a comment in the code to make this intent clearer.
/*
* With CONFIG_SPARSEMEM_VMEMMAP, pfn_section_valid() operates at
* PAGES_PER_SUBSECTION granularity, so align to subsection boundaries
* to include all PFNs for which pfn_to_online_page() is valid.
*/
Best Regards,
Liu, Yuan1
> Cheers,
>
> David
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-08-06 7:23 ` Liu, Yuan1
@ 2026-08-06 8:45 ` David Hildenbrand (Arm)
2026-08-06 9:52 ` Liu, Yuan1
0 siblings, 1 reply; 15+ messages in thread
From: David Hildenbrand (Arm) @ 2026-08-06 8:45 UTC (permalink / raw)
To: Liu, Yuan1, Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
>>> +static void __init update_zone_online_memmap_pages(struct zone *zone,
>>> + unsigned long start_pfn,
>>> + unsigned long end_pfn,
>>> + unsigned long *hole_pfn)
>>> +{
>>> +#ifdef CONFIG_SPARSEMEM_VMEMMAP
>>> + unsigned long zone_start_pfn = zone->zone_start_pfn;
>>> + unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
>>
>> These two can be const.
>
> Will do.
>
>>> + unsigned long sub_start, sub_end;
>>> +
>>> + sub_start = max(ALIGN_DOWN(start_pfn, PAGES_PER_SUBSECTION),
>>> + zone_start_pfn);
>>> + sub_end = min(ALIGN(end_pfn, PAGES_PER_SUBSECTION), zone_end_pfn);
>>
>> Hm, I don't immediately understand why we do the PAGES_PER_SUBSECTION
>> thing
>> here. Why is that required?
>
> pages_with_online_memmap counts all PFNs where pfn_to_online_page() is
> valid. With CONFIG_SPARSEMEM_VMEMMAP, pfn_section_valid() operates at
> PAGES_PER_SUBSECTION granularity — when any page in a subsection has
> memory, the entire subsection is valid/online. So we align to subsection
> boundaries to include hole pages within partially-populated subsections.
But we must never account exceeding the zone range. So I don't understand why we
would have to care about PAGES_PER_SUBSECTION here at all?
--
Cheers,
David
^ permalink raw reply [flat|nested] 15+ messages in thread
* RE: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range
2026-08-06 8:45 ` David Hildenbrand (Arm)
@ 2026-08-06 9:52 ` Liu, Yuan1
0 siblings, 0 replies; 15+ messages in thread
From: Liu, Yuan1 @ 2026-08-06 9:52 UTC (permalink / raw)
To: David Hildenbrand (Arm), Oscar Salvador, Mike Rapoport, Wei Yang
Cc: linux-mm@kvack.org, Zou, Nanhai, Deng, Pan, Li, Tianyou,
Chen Zhang, Zeng, Jason, linux-kernel@vger.kernel.org
> -----Original Message-----
> From: David Hildenbrand (Arm) <david@kernel.org>
> Sent: Thursday, August 6, 2026 4:46 PM
> To: Liu, Yuan1 <yuan1.liu@intel.com>; Oscar Salvador <osalvador@suse.de>;
> Mike Rapoport <rppt@kernel.org>; Wei Yang <richard.weiyang@gmail.com>
> Cc: linux-mm@kvack.org; Zou, Nanhai <nanhai.zou@intel.com>; Deng, Pan
> <pan.deng@intel.com>; Li, Tianyou <tianyou.li@intel.com>; Chen Zhang
> <zhangchen.kidd@jd.com>; Zeng, Jason <jason.zeng@intel.com>; linux-
> kernel@vger.kernel.org
> Subject: Re: [PATCH v6 1/2] mm/memory_hotplug: optimize zone contiguous
> check when changing pfn range
>
>
> >>> +static void __init update_zone_online_memmap_pages(struct zone *zone,
> >>> + unsigned long start_pfn,
> >>> + unsigned long end_pfn,
> >>> + unsigned long *hole_pfn)
> >>> +{
> >>> +#ifdef CONFIG_SPARSEMEM_VMEMMAP
> >>> + unsigned long zone_start_pfn = zone->zone_start_pfn;
> >>> + unsigned long zone_end_pfn = zone_start_pfn + zone->spanned_pages;
> >>
> >> These two can be const.
> >
> > Will do.
> >
> >>> + unsigned long sub_start, sub_end;
> >>> +
> >>> + sub_start = max(ALIGN_DOWN(start_pfn, PAGES_PER_SUBSECTION),
> >>> + zone_start_pfn);
> >>> + sub_end = min(ALIGN(end_pfn, PAGES_PER_SUBSECTION), zone_end_pfn);
> >>
> >> Hm, I don't immediately understand why we do the PAGES_PER_SUBSECTION
> >> thing
> >> here. Why is that required?
> >
> > pages_with_online_memmap counts all PFNs where pfn_to_online_page() is
> > valid. With CONFIG_SPARSEMEM_VMEMMAP, pfn_section_valid() operates at
> > PAGES_PER_SUBSECTION granularity — when any page in a subsection has
> > memory, the entire subsection is valid/online. So we align to subsection
> > boundaries to include hole pages within partially-populated subsections.
>
> But we must never account exceeding the zone range. So I don't understand
> why we
> would have to care about PAGES_PER_SUBSECTION here at all?
We never account beyond the zone range, because `sub_start` and
`sub_end` are still clamped to the zone boundaries after the
alignment.
The subsection alignment is needed for hole PFNs between memblocks
within the zone. These hole PFNs are valid for
`pfn_to_online_page()`, because they sit in a subsection that has
memory, so the whole subsection's memmap is online.
|----------- zone range -----------|
+-----------+---------+------------+
+ memblock 1| hole | memblock 2 |
+-----------+---------+------------+
^
|
|
subsection boundary
Best Regards,
Liu, Yuan
> --
> Cheers,
>
> David
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-08-06 9:52 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-23 8:49 [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Yuan Liu
2026-07-23 8:49 ` [PATCH v6 1/2] " Yuan Liu
2026-08-05 11:53 ` David Hildenbrand (Arm)
2026-08-06 7:23 ` Liu, Yuan1
2026-08-06 8:45 ` David Hildenbrand (Arm)
2026-08-06 9:52 ` Liu, Yuan1
2026-07-23 8:49 ` [PATCH v6 2/2] mm/memory_hotplug: improve shrink_zone_span() subsection boundary checks Yuan Liu
2026-07-25 2:49 ` Wei Yang
2026-07-27 9:49 ` Liu, Yuan1
2026-07-30 2:36 ` Wei Yang
2026-07-30 7:57 ` Liu, Yuan1
2026-08-01 0:59 ` Wei Yang
2026-08-05 11:02 ` David Hildenbrand (Arm)
2026-08-06 7:14 ` Liu, Yuan1
2026-08-05 9:40 ` [PATCH v6 0/2] mm/memory_hotplug: optimize zone contiguous check when changing pfn range Liu, Yuan1
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox