[PATCH 1/2] mm/hugetlb: account migration target folio in per-node NR_HUGETLB vmstat

From: Hongfu Li

Date: Mon Sep 21 2026 - 05:13:23 EST


From: Hongfu Li <lihongfu@xxxxxxxxxx>

The NR_HUGETLB vmstat counter is maintained per folio's node: incremented
when a huge page is handed to a user via hugetlb_alloc_folio() and
decremented when it is returned to the pool via free_huge_folio().

A folio obtained by alloc_hugetlb_folio_nodemask() never goes through
hugetlb_alloc_folio(), so it is never accounted, while its free always
is. For a migration target this means the target node gets no matching
increment for the decrement on the old node, so the global nr_hugetlb in
/proc/vmstat drops by nr_pages for each migration. The same asymmetry
affects the failed migration path, which frees the target again right
away, and the temporary folio hugetlb_mfill_atomic_pte() takes from the
same helper.

alloc_hugetlb_folio_reserve(), used to preallocate the memfd page cache
folios, has the same asymmetry: the folio is handed to a user without
being accounted, while its free is accounted through free_huge_folio().

Account the folio where it is obtained, in alloc_hugetlb_folio_nodemask()
and alloc_hugetlb_folio_reserve(), so that the increment pairs with the
decrement in free_huge_folio(): a successful migration hands the folio
to a user, a failed one frees it again.

Fixes: 05d4532b60e3 ("memcg/hugetlb: add hugeTLB counters to memcg")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Hongfu Li <lihongfu@xxxxxxxxxx>
---
mm/hugetlb.c | 35 +++++++++++++++++++++++------------
1 file changed, 23 insertions(+), 12 deletions(-)

diff --git a/mm/hugetlb.c b/mm/hugetlb.c
index 1b53ba991d36..8ccc769bf48c 100644
--- a/mm/hugetlb.c
+++ b/mm/hugetlb.c
@@ -2206,6 +2206,11 @@ struct folio *alloc_hugetlb_folio_reserve(struct hstate *h, int preferred_nid,
}

spin_unlock_irq(&hugetlb_lock);
+
+ if (folio)
+ lruvec_stat_mod_folio(folio, NR_HUGETLB,
+ folio_nr_pages(folio));
+
return folio;
}

@@ -2213,24 +2218,30 @@ struct folio *alloc_hugetlb_folio_reserve(struct hstate *h, int preferred_nid,
struct folio *alloc_hugetlb_folio_nodemask(struct hstate *h, int preferred_nid,
nodemask_t *nmask, gfp_t gfp_mask, bool allow_alloc_fallback)
{
- spin_lock_irq(&hugetlb_lock);
- if (available_huge_pages(h)) {
- struct folio *folio;
+ struct folio *folio = NULL;

+ spin_lock_irq(&hugetlb_lock);
+ if (available_huge_pages(h))
folio = dequeue_hugetlb_folio_nodemask(h, gfp_mask,
preferred_nid, nmask);
- if (folio) {
- spin_unlock_irq(&hugetlb_lock);
- return folio;
- }
- }
spin_unlock_irq(&hugetlb_lock);

- /* We cannot fallback to other nodes, as we could break the per-node pool. */
- if (!allow_alloc_fallback)
- gfp_mask |= __GFP_THISNODE;
+ if (!folio) {
+ /*
+ * We cannot fallback to other nodes, as we could break the
+ * per-node pool.
+ */
+ if (!allow_alloc_fallback)
+ gfp_mask |= __GFP_THISNODE;

- return alloc_migrate_hugetlb_folio(h, gfp_mask, preferred_nid, nmask);
+ folio = alloc_migrate_hugetlb_folio(h, gfp_mask, preferred_nid,
+ nmask);
+ }
+
+ if (folio)
+ lruvec_stat_mod_folio(folio, NR_HUGETLB, folio_nr_pages(folio));
+
+ return folio;
}

static nodemask_t *policy_mbind_nodemask(gfp_t gfp)

--
2.54.0