[RFC] mm: khugepaged: use largest enabled hugepage order for min_free_kbytes

Usama Arif
2025-06-06T14:37:07Z

On arm64 machines with 64K PAGE_SIZE, the min_free_kbytes and hence the
watermarks are evaluated to extremely high values, for e.g. a server with
480G of memory, only 2M mTHP hugepage size set to madvise, with the rest
of the sizes set to never, the min, low and high watermarks evaluate to
11.2G, 14G and 16.8G respectively.
In contrast for 4K PAGE_SIZE of the same machine, with only 2M THP hugepage
size set to madvise, the min, low and high watermarks evaluate to 86M, 566M
and 1G respectively.
This is because set_recommended_min_free_kbytes is designed for PMD
hugepages (pageblock_order = min(HPAGE_PMD_ORDER, PAGE_BLOCK_ORDER)).
Such high watermark values can cause performance and latency issues in
memory bound applications on arm servers that use 64K PAGE_SIZE, eventhough
most of them would never actually use a 512M PMD THP.

Instead of using HPAGE_PMD_ORDER for pageblock_order use the highest large
folio order enabled in set_recommended_min_free_kbytes.
With this patch, when only 2M THP hugepage size is set to madvise for the
same machine with 64K page size, with the rest of the sizes set to never,
the min, low and high watermarks evaluate to 2.08G, 2.6G and 3.1G
respectively. When 512M THP hugepage size is set to madvise for the same
machine with 64K page size, the min, low and high watermarks evaluate to
11.2G, 14G and 16.8G respectively, the same as without this patch.

An alternative solution would be to change PAGE_BLOCK_ORDER by changing
ARCH_FORCE_MAX_ORDER to a lower value for ARM64_64K_PAGES. However, this
is not dynamic with hugepage size, will need different kernel builds for
different hugepage sizes and most users won't know that this needs to be
done as it can be difficult to detmermine that the performance and latency
issues are coming from the high watermark values.

All watermark numbers are for zones of nodes that had the highest number
of pages, i.e. the value for min size for 4K is obtained using:
cat /proc/zoneinfo | grep -i min | awk '{print $2}' | sort -n | tail -n 1 | awk '{print $1 * 4096 / 1024 / 1024}';
and for 64K using:
cat /proc/zoneinfo | grep -i min | awk '{print $2}' | sort -n | tail -n 1 | awk '{print $1 * 65536 / 1024 / 1024}';

An arbirtary min of 128 pages is used for when no hugepage sizes are set
enabled.

Signed-off-by: Usama Arif <usamaarif642@gmail.com>
---
include/linux/huge_mm.h | 25 +++++++++++++++++++++++++
mm/khugepaged.c | 32 ++++++++++++++++++++++++++++----
mm/shmem.c | 29 +++++------------------------
3 files changed, 58 insertions(+), 28 deletions(-)

diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h
index 2f190c90192d..fb4e51ef0acb 100644
--- a/include/linux/huge_mm.h
+++ b/include/linux/huge_mm.h
@@ -170,6 +170,25 @@ static inline void count_mthp_stat(int order, enum mthp_stat_item item)
}
#endif

+/*
+ * Definitions for "huge tmpfs": tmpfs mounted with the huge= option
+ *
+ * SHMEM_HUGE_NEVER:
+ * disables huge pages for the mount;
+ * SHMEM_HUGE_ALWAYS:
+ * enables huge pages for the mount;
+ * SHMEM_HUGE_WITHIN_SIZE:
+ * only allocate huge pages if the page will be fully within i_size,
+ * also respect madvise() hints;
+ * SHMEM_HUGE_ADVISE:
+ * only allocate huge pages if requested with madvise();
+ */
+
+ #define SHMEM_HUGE_NEVER 0
+ #define SHMEM_HUGE_ALWAYS 1
+ #define SHMEM_HUGE_WITHIN_SIZE 2
+ #define SHMEM_HUGE_ADVISE 3
+
#ifdef CONFIG_TRANSPARENT_HUGEPAGE

extern unsigned long transparent_hugepage_flags;
@@ -177,6 +196,12 @@ extern unsigned long huge_anon_orders_always;
extern unsigned long huge_anon_orders_madvise;
extern unsigned long huge_anon_orders_inherit;

+extern int shmem_huge __read_mostly;
+extern unsigned long huge_shmem_orders_always;
+extern unsigned long huge_shmem_orders_madvise;
+extern unsigned long huge_shmem_orders_inherit;
+extern unsigned long huge_shmem_orders_within_size;
+
static inline bool hugepage_global_enabled(void)
{
return transparent_hugepage_flags &
diff --git a/mm/khugepaged.c b/mm/khugepaged.c
index 15203ea7d007..e64cba74eb2a 100644
--- a/mm/khugepaged.c
+++ b/mm/khugepaged.c
@@ -2607,6 +2607,26 @@ static int khugepaged(void *none)
return 0;
}

+static int thp_highest_allowable_order(void)
+{
+ unsigned long orders = READ_ONCE(huge_anon_orders_always)
+ | READ_ONCE(huge_anon_orders_madvise)
+ | READ_ONCE(huge_shmem_orders_always)
+ | READ_ONCE(huge_shmem_orders_madvise)
+ | READ_ONCE(huge_shmem_orders_within_size);
+ if (hugepage_global_enabled())
+ orders |= READ_ONCE(huge_anon_orders_inherit);
+ if (shmem_huge != SHMEM_HUGE_NEVER)
+ orders |= READ_ONCE(huge_shmem_orders_inherit);
+
+ return orders == 0 ? 0 : fls(orders) - 1;
+}
+
+static unsigned long min_thp_pageblock_nr_pages(void)
+{
+ return (1UL << min(thp_highest_allowable_order(), PAGE_BLOCK_ORDER));
+}
+
static void set_recommended_min_free_kbytes(void)
{
struct zone *zone;
@@ -2638,12 +2658,16 @@ static void set_recommended_min_free_kbytes(void)
* second to avoid subsequent fallbacks of other types There are 3
* MIGRATE_TYPES we care about.
*/
- recommended_min += pageblock_nr_pages * nr_zones *
+ recommended_min += min_thp_pageblock_nr_pages() * nr_zones *
MIGRATE_PCPTYPES * MIGRATE_PCPTYPES;

- /* don't ever allow to reserve more than 5% of the lowmem */
- recommended_min = min(recommended_min,
- (unsigned long) nr_free_buffer_pages() / 20);
+ /*
+ * Don't ever allow to reserve more than 5% of the lowmem.
+ * Use a min of 128 pages when all THP orders are set to never.
+ */
+ recommended_min = clamp(recommended_min, 128,
+ (unsigned long) nr_free_buffer_pages() / 20);
+
recommended_min <<= (PAGE_SHIFT-10);

if (recommended_min > min_free_kbytes) {
diff --git a/mm/shmem.c b/mm/shmem.c
index 0c5fb4ffa03a..8e92678d1175 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -136,10 +136,10 @@ struct shmem_options {
};

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
-static unsigned long huge_shmem_orders_always __read_mostly;
-static unsigned long huge_shmem_orders_madvise __read_mostly;
-static unsigned long huge_shmem_orders_inherit __read_mostly;
-static unsigned long huge_shmem_orders_within_size __read_mostly;
+unsigned long huge_shmem_orders_always __read_mostly;
+unsigned long huge_shmem_orders_madvise __read_mostly;
+unsigned long huge_shmem_orders_inherit __read_mostly;
+unsigned long huge_shmem_orders_within_size __read_mostly;
static bool shmem_orders_configured __initdata;
#endif

@@ -516,25 +516,6 @@ static bool shmem_confirm_swap(struct address_space *mapping,
return xa_load(&mapping->i_pages, index) == swp_to_radix_entry(swap);
}

-/*
- * Definitions for "huge tmpfs": tmpfs mounted with the huge= option
- *
- * SHMEM_HUGE_NEVER:
- * disables huge pages for the mount;
- * SHMEM_HUGE_ALWAYS:
- * enables huge pages for the mount;
- * SHMEM_HUGE_WITHIN_SIZE:
- * only allocate huge pages if the page will be fully within i_size,
- * also respect madvise() hints;
- * SHMEM_HUGE_ADVISE:
- * only allocate huge pages if requested with madvise();
- */
-
-#define SHMEM_HUGE_NEVER 0
-#define SHMEM_HUGE_ALWAYS 1
-#define SHMEM_HUGE_WITHIN_SIZE 2
-#define SHMEM_HUGE_ADVISE 3
-
/*
* Special values.
* Only can be set via /sys/kernel/mm/transparent_hugepage/shmem_enabled:
@@ -551,7 +532,7 @@ static bool shmem_confirm_swap(struct address_space *mapping,
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
/* ifdef here to avoid bloating shmem.o when not necessary */

-static int shmem_huge __read_mostly = SHMEM_HUGE_NEVER;
+int shmem_huge __read_mostly = SHMEM_HUGE_NEVER;
static int tmpfs_huge __read_mostly = SHMEM_HUGE_NEVER;

/**
--
2.47.1