Re: [PATCH v8 4/8] mm: pghot: Precision mode for pghot

Bharata B Rao <[email protected]>
Newsgroups org.kvack.linux-mm,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
[Reply to Shashiko review]

On 28-Jul-26 11:13 AM, Bharata B Rao wrote:
> Default pghot stores hotness in a 1‑byte record per PFN, limiting
> frequency to 2 bits, time to a 5‑bit bucket, and preventing storage
> of per‑PFN toptier NID. This restricts time granularity and forces
> all promotions to use the global pghot_target_nid.
> 
> This patch adds an optional precision mode (CONFIG_PGHOT_PRECISE)
> that expands the hotness record to 4 bytes (u32) and provides:
> 
> - 10‑bit NID field for per‑PFN promotion target,
> - 3‑bit frequency field (freq_threshold range 1–7),
> - 14‑bit time field offering finer recency tracking,
> - MSB migrate‑ready bit.
> 
> Precision mode improves placement accuracy on systems with multiple
> toptier nodes and provides higher‑resolution hotness tracking, at
> the cost of increasing metadata to 4 bytes per PFN.
> 
> Documentation, tunables, and the record layout are updated accordingly.
> 
> Signed-off-by: Bharata B Rao <[email protected]>
> ---
>  Documentation/admin-guide/mm/pghot.rst |  4 +-
>  include/linux/pghot.h                  | 39 ++++++++++++-
>  mm/Kconfig                             | 11 ++++
>  mm/Makefile                            |  7 ++-
>  mm/pghot-precise.c                     | 77 ++++++++++++++++++++++++++
>  mm/pghot.c                             | 13 +++--
>  6 files changed, 143 insertions(+), 8 deletions(-)
>  create mode 100644 mm/pghot-precise.c
> 
> diff --git a/Documentation/admin-guide/mm/pghot.rst b/Documentation/admin-guide/mm/pghot.rst
> index 0edbe0082816..6d9b3d9522d1 100644
> --- a/Documentation/admin-guide/mm/pghot.rst
> +++ b/Documentation/admin-guide/mm/pghot.rst
> @@ -56,7 +56,7 @@ Path: /proc/sys/vm/pghot_target_nid
>  Path: /proc/sys/vm/pghot_freq_threshold
>  
>  - Minimum access frequency before a page is marked ready for promotion.
> -  Range is 1 to 3 in default mode.
> +  Range is 1 to 3 in default mode and 1 to 7 in precision mode.
>  - Default: 2
>  - Example:
>    # sysctl vm.pghot_freq_threshold=1
> @@ -68,7 +68,7 @@ Path: /proc/sys/vm/pghot_promote_freq_window_ms
>  - Controls the time window (in ms) for counting access frequency. A page is
>    considered hot only when **pghot_freq_threshold** number of accesses occur
>    with this time period.
> -- Default: 3000 (3 seconds)
> +- Default: 3000 (3 seconds) in default mode and 5000 (5s) in precision mode.
>  - Example:
>    # sysctl vm.pghot_promote_freq_window_ms=3000
>  
> diff --git a/include/linux/pghot.h b/include/linux/pghot.h
> index 7b85d717f410..313e5a6973e4 100644
> --- a/include/linux/pghot.h
> +++ b/include/linux/pghot.h
> @@ -37,8 +37,40 @@ DECLARE_STATIC_KEY_FALSE(pghot_src_hwhints);
>  
>  #define PGHOT_DEFAULT_NODE		0
>  
> +#if defined(CONFIG_PGHOT_PRECISE)
>  #define PGHOT_FREQ_WINDOW_MIN		(1 * MSEC_PER_SEC)
> -#define PGHOT_FREQ_WINDOW_DEFAULT	(3 * MSEC_PER_SEC)
> +#define PGHOT_FREQ_WINDOW_DEFAULT	(5 * MSEC_PER_SEC)
> +
> +/*
> + * Bits 0-26 are used to store nid, frequency and time.
> + * Bits 27-30 are unused now.
> + * Bit 31 is used to indicate the page is ready for migration.
> + */
> +#define PGHOT_MIGRATE_READY		31
> +
> +#define PGHOT_NID_WIDTH			10
> +#define PGHOT_FREQ_WIDTH		3
> +/* time is stored in 14 bits which can represent up to 16s with HZ=1000 */
> +#define PGHOT_TIME_WIDTH		14
> +
> +#define PGHOT_NID_SHIFT			0
> +#define PGHOT_FREQ_SHIFT		(PGHOT_NID_SHIFT + PGHOT_NID_WIDTH)
> +#define PGHOT_TIME_SHIFT		(PGHOT_FREQ_SHIFT + PGHOT_FREQ_WIDTH)
> +
> +#define PGHOT_NID_MASK			GENMASK(PGHOT_NID_WIDTH - 1, 0)
> +#define PGHOT_FREQ_MASK			GENMASK(PGHOT_FREQ_WIDTH - 1, 0)
> +#define PGHOT_TIME_MASK			GENMASK(PGHOT_TIME_WIDTH - 1, 0)
> +
> +#define PGHOT_NID_MAX			((1 << PGHOT_NID_WIDTH) - 1)
> +#define PGHOT_FREQ_MAX			((1 << PGHOT_FREQ_WIDTH) - 1)
> +#define PGHOT_TIME_MAX			((1 << PGHOT_TIME_WIDTH) - 1)
> +#define PGHOT_FREQ_WINDOW_MAX		PGHOT_TIME_MAX
> +
> +typedef u32 phi_t;

> This is a pre-existing issue, but expanding the size of phi_t to 4 bytes
> significantly worsens the reliability of hot_map allocations.
> In mm/pghot.c, pghot_alloc_hot_map() allocates the tracking structure:
>    hot_map = kzalloc_node(struct_size(hot_map, phi, PAGES_PER_SECTION),
>                           GFP_KERNEL, nid);
>With 4 bytes per PFN, the allocation size for a standard section grows to
>over 131KB, requiring an order-6 contiguous physical allocation. Since
>high-order GFP_KERNEL allocations frequently fail under memory fragmentation,
>could this cause memory hotplug operations to silently fail to track hot
>pages for newly onlined memory sections?
>Would it be better to convert this to use kvzalloc_node() and kvfree_rcu()
>to avoid the contiguous physical memory requirement?

Agreed. Will switch to kvzalloc_node() and kvfree_rcu().

> +
> +#else	/* !CONFIG_PGHOT_PRECISE */
> +#define PGHOT_FREQ_WINDOW_MIN		(1 * MSEC_PER_SEC)
> +#define PGHOT_FREQ_WINDOW_DEFAULT       (3 * MSEC_PER_SEC)
>  
>  /*
>   * Bits 0-6 are used to store frequency and time.
> @@ -66,6 +98,11 @@ DECLARE_STATIC_KEY_FALSE(pghot_src_hwhints);
>  
>  typedef u8 phi_t;
>  
> +static_assert(MAX_NUMNODES <= (1 << PGHOT_NID_WIDTH),
> +	      "pghot precise nid field too narrow for MAX_NUMNODES");

> Is this static_assert placed in the correct block?
> It appears to be inside the #else block for CONFIG_PGHOT_PRECISE, which means
> it will be disabled precisely when precision mode is enabled.
> If a kernel is compiled with CONFIG_PGHOT_PRECISE=y and MAX_NUMNODES > 1024,
> the compiler won't catch the narrow NID field. Could this lead to NIDs being
> silently truncated during runtime, causing pages to be migrated to incorrect
> NUMA nodes?

Yes, I got the placement wrong, it should have been within CONFIG_PGHOT_PRECISE.

> +
> +#endif /* CONFIG_PGHOT_PRECISE */
> +
>  #define PGHOT_RECORD_SIZE		sizeof(phi_t)
>  
>  #define PGHOT_SECTION_HOT_BIT		0
> diff --git a/mm/Kconfig b/mm/Kconfig
> index 0a5bcd5d45ed..955a826ecfe9 100644
> --- a/mm/Kconfig
> +++ b/mm/Kconfig
> @@ -1523,6 +1523,17 @@ config PGHOT
>  	  This adds 1 byte of metadata overhead per page in lower-tier
>  	  memory nodes.
>  
> +config PGHOT_PRECISE
> +	bool "Hot page tracking precision mode"
> +	default n
> +	depends on PGHOT
> +	help
> +	  Enables precision mode for tracking hot pages with pghot sub-system.
> +	  Adds fine-grained access time tracking and explicit toptier target
> +	  NID tracking. Precise hot page tracking comes at the cost of using
> +	  4 bytes per page against the default one byte per page. Preferable
> +	  to enable this on systems with multiple nodes in toptier.
> +
>  source "mm/damon/Kconfig"
>  
>  endmenu
> diff --git a/mm/Makefile b/mm/Makefile
> index 4939a1a74c1d..83bac64cab26 100644
> --- a/mm/Makefile
> +++ b/mm/Makefile
> @@ -147,4 +147,9 @@ obj-$(CONFIG_SHRINKER_DEBUG) += shrinker_debug.o
>  obj-$(CONFIG_EXECMEM) += execmem.o
>  obj-$(CONFIG_TMPFS_QUOTA) += shmem_quota.o
>  obj-$(CONFIG_LAZY_MMU_MODE_KUNIT_TEST) += tests/lazy_mmu_mode_kunit.o
> -obj-$(CONFIG_PGHOT) += pghot.o pghot-default.o
> +obj-$(CONFIG_PGHOT) += pghot.o
> +ifdef CONFIG_PGHOT_PRECISE
> +obj-$(CONFIG_PGHOT) += pghot-precise.o
> +else
> +obj-$(CONFIG_PGHOT) += pghot-default.o
> +endif
> diff --git a/mm/pghot-precise.c b/mm/pghot-precise.c
> new file mode 100644
> index 000000000000..df82f2068a99
> --- /dev/null
> +++ b/mm/pghot-precise.c
> @@ -0,0 +1,77 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * pghot: Precision mode
> + *
> + * 4 byte hotness record per PFN (u32)
> + * NID, time and frequency tracked as part of the record.
> + */
> +
> +#include <linux/pghot.h>
> +#include <linux/jiffies.h>
> +#include <linux/memory-tiers.h>
> +
> +bool pghot_nid_valid(int nid)
> +{
> +	if (nid != NUMA_NO_NODE &&
> +	    (!numa_valid_node(nid) || nid > PGHOT_NID_MAX ||
> +	     !node_online(nid) || !node_is_toptier(nid)))
> +		return false;
> +
> +	return true;
> +}
> +
> +unsigned long pghot_access_latency(unsigned long old_time, unsigned long time)
> +{
> +	return jiffies_to_msecs((time - old_time) & PGHOT_TIME_MASK);
> +}
> +
> +bool pghot_update_record(phi_t *phi, int nid, unsigned long now)
> +{
> +	phi_t freq, old_freq, hotness, old_hotness, old_time;
> +	phi_t time = now & PGHOT_TIME_MASK;
> +
> +	nid = (nid == NUMA_NO_NODE) ? sysctl_pghot_target_nid : nid;
> +	old_hotness = READ_ONCE(*phi);
> +
> +	do {
> +		bool new_window = false;
> +
> +		old_freq = (old_hotness >> PGHOT_FREQ_SHIFT) & PGHOT_FREQ_MASK;
> +		old_time = (old_hotness >> PGHOT_TIME_SHIFT) & PGHOT_TIME_MASK;
> +
> +		if (pghot_access_latency(old_time, time) > sysctl_pghot_freq_window)
> +			new_window = true;

> Can the 14-bit time mask cause cold pages to be incorrectly evaluated as hot?
> Since PGHOT_TIME_MASK is 14 bits, the tracked time value wraps around roughly
> every 16.38 seconds (assuming HZ=1000). 
> If a cold page is accessed once, left completely idle for a long period, and
> then accessed again, is it possible that the elapsed time calculated using the
> wrapped mask randomly falls within the default 5-second sysctl window?
> Could this lead to spurious hotness promotions and unnecessary memory
> migrations for pages that aren't actually accessed frequently?

For the reasons explained in the similar path in pghot-default, this is okay.
This is in fact better than pghot-default as 14bits give a better range of
~16s.

Regards,
Bharata.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.