Skip to contentSkip to conversation

mm/damon: add node_eligible_mem_bp goal metric

PATCH Superseded Ravi Jonnalagadda 3 participants 5 messages Apr 26

Subsystems (3)

v8 of 9 Superseded Latest is v9 →

lore
Share & tools
lore
#0 lore raw

PATCH v8 1/1 mm/damon: add node_eligible_mem_bp goal metric

Background and Motivation
=========================

In heterogeneous memory systems, controlling memory distribution across NUMA nodes is essential for performance optimization. This patch enables system-wide page distribution with target-state goals such as "maintain 60% of scheme-eligible memory on DRAM" using PA-mode DAMON schemes.

Rather than using absolute thresholds, this metric tracks the ratio of memory that matches each scheme's access pattern filters on a target node, enabling the quota system to automatically adjust migration aggressiveness to maintain the desired distribution.

What This Metric Measures
=========================

node_eligible_mem_bp:
scheme_eligible_bytes_on_node / total_scheme_eligible_bytes * 10000

Two-Scheme Setup for Hot Page Distribution
==========================================

For maintaining 60% of hot memory on DRAM (node 0) and 40% on CXL (node 1):

PULL scheme: migrate_hot to node 0
goal: node_eligible_mem_bp, nid=0, target=6000
addr filter: node 1 address range (only migrate FROM CXL) "Move hot pages to DRAM if less than 60% of hot data is in DRAM"

PUSH scheme: migrate_hot to node 1
goal: node_eligible_mem_bp, nid=1, target=4000
addr filter: node 0 address range (only migrate FROM DRAM) "Move hot pages to CXL if less than 40% of hot data is in CXL"

Each scheme independently measures its own eligible memory and adjusts its quota to achieve its target ratio. The schemes work in concert through DAMON's unified monitoring context, with the quota autotuner balancing their relative aggressiveness.

Implementation Details
======================

The implementation adds a new quota goal metric type DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP to the existing DAMOS quota goal framework. When this metric is configured for a scheme:

1. During each quota adjustment cycle, damos_get_node_eligible_mem_bp() is called to calculate the current memory distribution.

2. The function iterates through all regions that match the scheme's access pattern (via __damos_valid_target()) and calculates:
- Total eligible bytes across all nodes
- Eligible bytes specifically on the target node (goal->nid)

3. For each eligible region, damos_calc_eligible_bytes() walks through the physical address range, using damon_get_folio() to look up each folio and determine its NUMA node via folio_nid().

4. Large folios are handled by calculating the exact overlap between the region boundaries and folio boundaries, ensuring accurate byte counts even when regions partially span folios.

5. The ratio (node_eligible / total_eligible * 10000) is returned as basis points, which the quota autotuner uses to adjust the scheme's effective quota size (esz).

The implementation requires CONFIG_DAMON_PADDR since damon_get_folio() is only available for physical address space monitoring.

Testing Results
===============

Functionally tested on a two-node heterogeneous memory system with DRAM (node 0) and CXL memory (node 1). A PUSH+PULL scheme configuration using migrate_hot actions was used to reach a target hot memory ratio between the two tiers.

With the TEMPORAL tuner, the system converges quickly to the target distribution. The tuner drives esz to maximum when under goal and to zero once the goal is met, forming a simple on/off feedback loop that stabilizes at the desired ratio.

With the CONSIST tuner, the scheme still converges but more slowly, as it migrates and then throttles itself based on quota feedback. The time to reach the goal varies depending on workload intensity.

Note: This metric works with both TEMPORAL and CONSIST goal tuners.

---
Changes since v7:
=================
https://lore.kernel.org/linux-mm/[email protected]/

- Wrapped if condition for goal->metric check to fix 80-column violation
- Used PAGE_ALIGN_DOWN(addr + PAGE_SIZE) instead of addr += PAGE_SIZE for proper page-aligned advancement
- Wrapped damos_goal_tune_esz_bp_temporal() function signature for 80-column compliance
- Removed unintended damos_trace_esz() call from first charge window
- Added addr filter usage note in two-scheme setup documentation

Changes since v6:
=================
https://lore.kernel.org/linux-mm/[email protected]/

- Dropped node_ineligible_mem_bp metric per maintainer feedback
- Updated two-scheme setup to use 6:4 ratio with both schemes using node_eligible_mem_bp (per SeongJae Park's suggestion)

Changes since v5:
=================
https://lore.kernel.org/linux-mm/[email protected]/

- Rebased onto mm-new instead of damon/next for sashiko review
- Removed Reported-by/Closes tags per maintainer feedback (not needed for bugs found before merge)

Changes since v4:
=================
https://lore.kernel.org/linux-mm/[email protected]/

- Fixed commit message description for DAMOS_QUOTA_NODE_INELIGIBLE_MEM_BP per review feedback
- Added clarifying comment for ops-common.h include (for damon_get_folio())
- Fixed build error when CONFIG_DAMON_PADDR is disabled by adding #ifdef CONFIG_DAMON_PADDR guards around functions using damon_get_folio()
- Dropped RFC tag per maintainer feedback

Changes since RFC v3:
=====================
https://lore.kernel.org/linux-mm/[email protected]/

- Fixed phys_addr_t overflow on 32-bit PAE systems by using phys_addr_t for intermediate byte calculations
- Added cond_resched() per region to prevent CPU soft lockups
- Improved folio overlap calculation for accurate byte counting

Changes since RFC v2:
=====================
https://lore.kernel.org/linux-mm/[email protected]/

- Added node_ineligible_mem_bp complementary metric
- Fixed DAMON_OPS_PADDR validation in damon_commit_ctx()
- Improved commit message with two-scheme setup documentation

Changes since RFC v1:
=====================
https://lore.kernel.org/linux-mm/[email protected]/

- Initial implementation based on SeongJae Park's suggestion
- Added basic node_eligible_mem_bp metric

---
 include/linux/damon.h    |   3 +
 mm/damon/core.c          | 171 +++++++++++++++++++++++++++++++++++----
 mm/damon/sysfs-schemes.c |   7 ++
 3 files changed, 165 insertions(+), 16 deletions(-)
include/linux/damon.h +3 -0
index f2cdb7c3f5e6..986b8c902585 100644
@@ -159,6 +159,8 @@ enum damos_action {
  * @DAMOS_QUOTA_NODE_MEMCG_FREE_BP:	MemFree ratio of a node for a cgroup.
  * @DAMOS_QUOTA_ACTIVE_MEM_BP:		Active to total LRU memory ratio.
  * @DAMOS_QUOTA_INACTIVE_MEM_BP:	Inactive to total LRU memory ratio.
+ * @DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP:	Scheme-eligible memory ratio of a
+ *					node in basis points (0-10000).
  * @NR_DAMOS_QUOTA_GOAL_METRICS:	Number of DAMOS quota goal metrics.
  *
  * Metrics equal to larger than @NR_DAMOS_QUOTA_GOAL_METRICS are unsupported.
@@ -172,6 +174,7 @@ enum damos_quota_goal_metric {
 	DAMOS_QUOTA_NODE_MEMCG_FREE_BP,
 	DAMOS_QUOTA_ACTIVE_MEM_BP,
 	DAMOS_QUOTA_INACTIVE_MEM_BP,
+	DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP,
 	NR_DAMOS_QUOTA_GOAL_METRICS,
 };
 
mm/damon/core.c +155 -16

272 lines not on this page · raw message

mm/damon/sysfs-schemes.c +7 -0
index 245d63808411..f21191d31f58 100644
@@ -1094,6 +1094,10 @@ struct damos_sysfs_qgoal_metric_name damos_sysfs_qgoal_metric_names[] = {
 		.metric = DAMOS_QUOTA_INACTIVE_MEM_BP,
 		.name = "inactive_mem_bp",
 	},
+	{
+		.metric = DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP,
+		.name = "node_eligible_mem_bp",
+	},
 };
 
 static ssize_t target_metric_show(struct kobject *kobj,
@@ -2685,6 +2689,9 @@ static int damos_sysfs_add_quota_score(
 			}
 			goal->nid = sysfs_goal->nid;
 			break;
+		case DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP:
+			goal->nid = sysfs_goal->nid;
+			break;
 		default:
 			break;
 		}

base-commit: 80a5456b3cf475eea155b66d7a06ff266dc05647

Signature · 1 line
2.43.0
#1 lore raw
Sashiko AI review found 4 potential issue(s): - [High] Casting phys_addr_t to unsigned long causes truncation and potential divide-by-zero on 32-bit systems - [High] Missing cond_resched() inside…

Sashiko AI review found 4 potential issue(s):
- [High] Casting phys_addr_t to unsigned long causes truncation and potential divide-by-zero on 32-bit systems
- [High] Missing cond_resched() inside the PFN-scanning loop leads to CPU soft lockups
- [High] Synchronous O(N) full-memory scanning defeats DAMON's bounded-overhead design
- [Medium] damos_calc_eligible_bytes() skips core filters, corrupting the metric calculation --

commit efcd6d55e5292f8b5cd0c6202c9d95000e99dc4c
Author: Ravi Jonnalagadda <[email protected]>

mm/damon: add node_eligible_mem_bp goal metric

This commit introduces a new quota goal metric, DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP, to track the ratio of scheme-eligible memory on a specific NUMA node. It implements calculation functions that iterate over eligible regions and examine the NUMA node of each folio.

Quoted 4 lines
mm/damon/core.c +0 -0
index 3dbbbfdeff71..a9303a8c4384 100644

[ ... ]

Quoted 1 line
mm/damon/core.c +0 -0
@@ -2287,7 +2306,112 @@ static unsigned long damos_get_node_memcg_used_bp(

[ ... ]

Quoted 4 lines
mm/damon/core.c +4 -0
+static phys_addr_t damos_calc_eligible_bytes(struct damon_ctx *c,
+		struct damos *s, int nid, phys_addr_t *total)
+{
+	struct damon_target *t;

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2339

Quoted 10 lines
mm/damon/core.c +10 -0
+	struct damon_region *r;
+	phys_addr_t total_eligible = 0;
+	phys_addr_t node_eligible = 0;
+
+	damon_for_each_target(t, c) {
+		damon_for_each_region(r, t) {
+			phys_addr_t addr, end_addr;
+
+			if (!__damos_valid_target(r, s))
+				continue;

Does this also need to check damos_core_filter_out(c, t, r, s)? By skipping the core filters, the eligible bytes calculation might erroneously include memory that the scheme is explicitly configured to ignore.

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2344

Quoted 5 lines
mm/damon/core.c +5 -0
+
+			/* Convert from core address units to physical bytes */
+			addr = (phys_addr_t)r->ar.start * c->addr_unit;
+			end_addr = (phys_addr_t)r->ar.end * c->addr_unit;
+			while (addr < end_addr) {

Since DAMON is designed to provide low, constant-overhead memory monitoring via region-based tracking, does performing a synchronous PFN-to-folio lookup for every physical page across all eligible regions break this bounded-overhead contract?

Because this function is called on every quota reset interval, scanning potentially gigabytes of memory might introduce severe latency spikes and CPU overhead.

Additionally, since DAMON regions can cover extremely large areas of memory, should cond_resched() be called inside this inner while loop rather than outside it? Iterating through millions of pages without yielding might trigger RCU stalls and soft lockups.

Quoted 6 lines
mm/damon/core.c +6 -0
+				struct folio *folio;
+				phys_addr_t folio_start, folio_end;
+				phys_addr_t overlap_start, overlap_end;
+				phys_addr_t counted;
+
+				folio = damon_get_folio(PHYS_PFN(addr));

[ ... ]

Quoted 22 lines
mm/damon/core.c +22 -0
+
+				/* Advance past the entire folio */
+				addr = folio_end;
+				folio_put(folio);
+			}
+			cond_resched();
+		}
+	}
+
+	*total = total_eligible;
+	return node_eligible;
+}
+
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+		struct damos *s, int nid)
+{
+	phys_addr_t total_eligible = 0;
+	phys_addr_t node_eligible;
+
+	if (c->ops.id != DAMON_OPS_PADDR)
+		return 0;
+

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2405

Quoted 10 lines
mm/damon/core.c +10 -0
+	if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+		return 0;
+
+	node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+	if (!total_eligible)
+		return 0;
+
+	return mult_frac((unsigned long)node_eligible, 10000,
+			(unsigned long)total_eligible);

Can casting total_eligible from phys_addr_t to unsigned long cause a divide-by-zero on 32-bit systems?

On 32-bit architectures with PAE, phys_addr_t is 64-bit while unsigned long is only 32-bit. If the total eligible memory is exactly a multiple of 4GB, the 64-bit value passes the if (!total_eligible) check, but the truncated 32-bit value passed as the divisor to mult_frac() evaluates to 0, which would trigger a kernel panic.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
maintainer
#2 lore raw
Quoted 2 lines · On Sun, 26 Apr 2026 00:53:41 +0000 [email protected] wrote:

Sashiko AI review found 4 potential issue(s):
- [High] Casting phys_addr_t to unsigned long causes truncation and potential divide-by-zero on 32-bit systems

This is a new and good finding. Ravi, could we fix this?

Quoted 1 line

- [High] Missing cond_resched() inside the PFN-scanning loop leads to CPU soft lockups

Unlikely.

Quoted 1 line

- [High] Synchronous O(N) full-memory scanning defeats DAMON's bounded-overhead design

Intentded design.

Quoted 1 line

- [Medium] damos_calc_eligible_bytes() skips core filters, corrupting the metric calculation

Intended design.

Quoted 29 lines

--

commit efcd6d55e5292f8b5cd0c6202c9d95000e99dc4c
Author: Ravi Jonnalagadda <[email protected]>

mm/damon: add node_eligible_mem_bp goal metric

This commit introduces a new quota goal metric, DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP, to track the ratio of scheme-eligible memory on a specific NUMA node. It implements calculation functions that iterate over eligible regions and examine the NUMA node of each folio.

Quoted 4 lines
mm/damon/core.c +0 -0
index 3dbbbfdeff71..a9303a8c4384 100644

[ ... ]

Quoted 1 line
mm/damon/core.c +0 -0
@@ -2287,7 +2306,112 @@ static unsigned long damos_get_node_memcg_used_bp(

[ ... ]

Quoted 10 lines
mm/damon/core.c +10 -0
+static phys_addr_t damos_calc_eligible_bytes(struct damon_ctx *c,
+		struct damos *s, int nid, phys_addr_t *total)
+{
+	struct damon_target *t;
+	struct damon_region *r;
+	phys_addr_t total_eligible = 0;
+	phys_addr_t node_eligible = 0;
+
+	damon_for_each_target(t, c) {
+		damon_for_each_region(r, t) {
Quoted 8 lines

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2339

Quoted 4 lines
mm/damon/core.c +4 -0
+			phys_addr_t addr, end_addr;
+
+			if (!__damos_valid_target(r, s))
+				continue;

Does this also need to check damos_core_filter_out(c, t, r, s)? By skipping the core filters, the eligible bytes calculation might erroneously include memory that the scheme is explicitly configured to ignore.

As previously answered, this is an intended design.

Quoted 11 lines

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2344

Quoted 6 lines
mm/damon/core.c +5 -0
+
+			/* Convert from core address units to physical bytes */
+			addr = (phys_addr_t)r->ar.start * c->addr_unit;
+			end_addr = (phys_addr_t)r->ar.end * c->addr_unit;
+			while (addr < end_addr) {

Since DAMON is designed to provide low, constant-overhead memory monitoring via region-based tracking, does performing a synchronous PFN-to-folio lookup for every physical page across all eligible regions break this bounded-overhead contract?

The bounded onverhead is only for monitoring part. It is not promised for DAMOS.

Quoted 4 lines

Because this function is called on every quota reset interval, scanning potentially gigabytes of memory might introduce severe latency spikes and CPU overhead.

So no problem.

Quoted 5 lines

Additionally, since DAMON regions can cover extremely large areas of memory, should cond_resched() be called inside this inner while loop rather than outside it? Iterating through millions of pages without yielding might trigger RCU stalls and soft lockups.

Unlikely. We could improve this when such issue really happens and is reported.

Quoted 44 lines
Quoted 7 lines
mm/damon/core.c +6 -0
+				struct folio *folio;
+				phys_addr_t folio_start, folio_end;
+				phys_addr_t overlap_start, overlap_end;
+				phys_addr_t counted;
+
+				folio = damon_get_folio(PHYS_PFN(addr));

[ ... ]

Quoted 22 lines
mm/damon/core.c +22 -0
+
+				/* Advance past the entire folio */
+				addr = folio_end;
+				folio_put(folio);
+			}
+			cond_resched();
+		}
+	}
+
+	*total = total_eligible;
+	return node_eligible;
+}
+
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+		struct damos *s, int nid)
+{
+	phys_addr_t total_eligible = 0;
+	phys_addr_t node_eligible;
+
+	if (c->ops.id != DAMON_OPS_PADDR)
+		return 0;
+

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2405

Quoted 10 lines
mm/damon/core.c +10 -0
+	if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+		return 0;
+
+	node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+	if (!total_eligible)
+		return 0;
+
+	return mult_frac((unsigned long)node_eligible, 10000,
+			(unsigned long)total_eligible);

Can casting total_eligible from phys_addr_t to unsigned long cause a divide-by-zero on 32-bit systems?

Quoted 5 lines

On 32-bit architectures with PAE, phys_addr_t is 64-bit while unsigned long is only 32-bit. If the total eligible memory is exactly a multiple of 4GB, the 64-bit value passes the if (!total_eligible) check, but the truncated 32-bit value passed as the divisor to mult_frac() evaluates to 0, which would trigger a kernel panic.

Good finding. Ravi, could we fix this? Maybe we can simply remove the casting?

Quoted 3 lines
Signature · 2 lines
-- 
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1

Thanks,
SJ

maintainer Reviewed-by on v8 1/1
#3 lore raw
In reply to Ravi Jonnalagadda
Quoted 83 lines · On Sat, 25 Apr 2026 17:32:45 -0700 Ravi Jonnalagadda <[email protected]>…

Background and Motivation
=========================

In heterogeneous memory systems, controlling memory distribution across NUMA nodes is essential for performance optimization. This patch enables system-wide page distribution with target-state goals such as "maintain 60% of scheme-eligible memory on DRAM" using PA-mode DAMON schemes.

Rather than using absolute thresholds, this metric tracks the ratio of memory that matches each scheme's access pattern filters on a target node, enabling the quota system to automatically adjust migration aggressiveness to maintain the desired distribution.

What This Metric Measures
=========================

node_eligible_mem_bp:
scheme_eligible_bytes_on_node / total_scheme_eligible_bytes * 10000

Two-Scheme Setup for Hot Page Distribution
==========================================

For maintaining 60% of hot memory on DRAM (node 0) and 40% on CXL (node 1):

PULL scheme: migrate_hot to node 0
goal: node_eligible_mem_bp, nid=0, target=6000
addr filter: node 1 address range (only migrate FROM CXL) "Move hot pages to DRAM if less than 60% of hot data is in DRAM"

PUSH scheme: migrate_hot to node 1
goal: node_eligible_mem_bp, nid=1, target=4000
addr filter: node 0 address range (only migrate FROM DRAM) "Move hot pages to CXL if less than 40% of hot data is in CXL"

Each scheme independently measures its own eligible memory and adjusts its quota to achieve its target ratio. The schemes work in concert through DAMON's unified monitoring context, with the quota autotuner balancing their relative aggressiveness.

Implementation Details
======================

The implementation adds a new quota goal metric type DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP to the existing DAMOS quota goal framework. When this metric is configured for a scheme:

1. During each quota adjustment cycle, damos_get_node_eligible_mem_bp() is called to calculate the current memory distribution.

2. The function iterates through all regions that match the scheme's access pattern (via __damos_valid_target()) and calculates:
- Total eligible bytes across all nodes
- Eligible bytes specifically on the target node (goal->nid)

3. For each eligible region, damos_calc_eligible_bytes() walks through the physical address range, using damon_get_folio() to look up each folio and determine its NUMA node via folio_nid().

4. Large folios are handled by calculating the exact overlap between the region boundaries and folio boundaries, ensuring accurate byte counts even when regions partially span folios.

5. The ratio (node_eligible / total_eligible * 10000) is returned as basis points, which the quota autotuner uses to adjust the scheme's effective quota size (esz).

The implementation requires CONFIG_DAMON_PADDR since damon_get_folio() is only available for physical address space monitoring.

Testing Results
===============

Functionally tested on a two-node heterogeneous memory system with DRAM (node 0) and CXL memory (node 1). A PUSH+PULL scheme configuration using migrate_hot actions was used to reach a target hot memory ratio between the two tiers.

With the TEMPORAL tuner, the system converges quickly to the target distribution. The tuner drives esz to maximum when under goal and to zero once the goal is met, forming a simple on/off feedback loop that stabilizes at the desired ratio.

Quoted 8 lines

With the CONSIST tuner, the scheme still converges but more slowly, as it migrates and then throttles itself based on quota feedback. The time to reach the goal varies depending on workload intensity.

Note: This metric works with both TEMPORAL and CONSIST goal tuners.

Assuming below two minor things are addressed,

[...]

Quoted 9 lines
mm/damon/core.c +9 -0
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+		struct damos *s, int nid)
+{
+	phys_addr_t total_eligible = 0;
+	phys_addr_t node_eligible;
+
+	if (c->ops.id != DAMON_OPS_PADDR)
+		return 0;
+

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2405

Quoted 10 lines
mm/damon/core.c +10 -0
+	if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+		return 0;
+
+	node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+	if (!total_eligible)
+		return 0;
+
+	return mult_frac((unsigned long)node_eligible, 10000,
+			(unsigned long)total_eligible);

Sashiko found [1] total_eligible after the casting could be zero on 32bit system, resulting in divide-by-zero. As I also replied to Sashiko review, could you please fix this? It seems we can simply remove the castings.

[...]

mm/damon/core.c static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota) · line 2531

Quoted 7 lines
mm/damon/core.c +1 -2
@@ -2389,9 +2528,9 @@ static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota)
 /*
  * Called only if quota->ms, or quota->sz are set, or quota->goals is not empty
  */
-static void damos_set_effective_quota(struct damos_quota *quota,
-		struct damon_ctx *ctx)
+static void damos_set_effective_quota(struct damon_ctx *c, struct damos *s)

Sorry for finding this late. Could we keep the dmon_ctx parameter name? Otherwise, we introduce unnecessary change below.

If the mult_frac() divide-by-zero is not a real issue, I wouldn't insist this change. But, if we will make a new version, let's do this together.

Quoted 18 lines
mm/damon/core.c +3 -2
 {
+	struct damos_quota *quota = &s->quota;
 	unsigned long throughput;
 	unsigned long esz = ULONG_MAX;
 
@@ -2402,9 +2541,9 @@ static void damos_set_effective_quota(struct damos_quota *quota,
 
 	if (!list_empty(&quota->goals)) {
 		if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_CONSIST)
-			damos_goal_tune_esz_bp_consist(quota);
+			damos_goal_tune_esz_bp_consist(c, s);
 		else if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_TEMPORAL)
-			damos_goal_tune_esz_bp_temporal(quota);
+			damos_goal_tune_esz_bp_temporal(c, s);
 		esz = quota->esz_bp / 10000;
 	}
 

mm/damon/core.c static void damos_set_effective_quota(struct damos_quota *quota, · line 2557

Quoted 5 lines
mm/damon/core.c +1 -1
@@ -2415,7 +2554,7 @@ static void damos_set_effective_quota(struct damos_quota *quota,
 		else
 			throughput = PAGE_SIZE * 1024;
 		esz = min(throughput * quota->ms, esz);
-		esz = max(ctx->min_region_sz, esz);
+		esz = max(c->min_region_sz, esz);

Above change is unnecessarily introduced. Could we keep the old damon_ctx parameter name?

[1] https://lore.kernel.org/[email protected]

Thanks,
SJ

[...]

#4 lore raw
Quoted 90 lines · SeongJae Park
Quoted 87 lines · On Sat, 25 Apr 2026 17:32:45 -0700 Ravi Jonnalagadda <[email protected]>…

Background and Motivation
=========================

In heterogeneous memory systems, controlling memory distribution across NUMA nodes is essential for performance optimization. This patch enables system-wide page distribution with target-state goals such as "maintain 60% of scheme-eligible memory on DRAM" using PA-mode DAMON schemes.

Rather than using absolute thresholds, this metric tracks the ratio of memory that matches each scheme's access pattern filters on a target node, enabling the quota system to automatically adjust migration aggressiveness to maintain the desired distribution.

What This Metric Measures
=========================

node_eligible_mem_bp:
scheme_eligible_bytes_on_node / total_scheme_eligible_bytes * 10000

Two-Scheme Setup for Hot Page Distribution
==========================================

For maintaining 60% of hot memory on DRAM (node 0) and 40% on CXL (node 1):

PULL scheme: migrate_hot to node 0
goal: node_eligible_mem_bp, nid=0, target=6000
addr filter: node 1 address range (only migrate FROM CXL) "Move hot pages to DRAM if less than 60% of hot data is in DRAM"

PUSH scheme: migrate_hot to node 1
goal: node_eligible_mem_bp, nid=1, target=4000
addr filter: node 0 address range (only migrate FROM DRAM) "Move hot pages to CXL if less than 40% of hot data is in CXL"

Each scheme independently measures its own eligible memory and adjusts its quota to achieve its target ratio. The schemes work in concert through DAMON's unified monitoring context, with the quota autotuner balancing their relative aggressiveness.

Implementation Details
======================

The implementation adds a new quota goal metric type DAMOS_QUOTA_NODE_ELIGIBLE_MEM_BP to the existing DAMOS quota goal framework. When this metric is configured for a scheme:

1. During each quota adjustment cycle, damos_get_node_eligible_mem_bp() is called to calculate the current memory distribution.

2. The function iterates through all regions that match the scheme's access pattern (via __damos_valid_target()) and calculates:
- Total eligible bytes across all nodes
- Eligible bytes specifically on the target node (goal->nid)

3. For each eligible region, damos_calc_eligible_bytes() walks through the physical address range, using damon_get_folio() to look up each folio and determine its NUMA node via folio_nid().

4. Large folios are handled by calculating the exact overlap between the region boundaries and folio boundaries, ensuring accurate byte counts even when regions partially span folios.

5. The ratio (node_eligible / total_eligible * 10000) is returned as basis points, which the quota autotuner uses to adjust the scheme's effective quota size (esz).

The implementation requires CONFIG_DAMON_PADDR since damon_get_folio() is only available for physical address space monitoring.

Testing Results
===============

Functionally tested on a two-node heterogeneous memory system with DRAM (node 0) and CXL memory (node 1). A PUSH+PULL scheme configuration using migrate_hot actions was used to reach a target hot memory ratio between the two tiers.

With the TEMPORAL tuner, the system converges quickly to the target distribution. The tuner drives esz to maximum when under goal and to zero once the goal is met, forming a simple on/off feedback loop that stabilizes at the desired ratio.

With the CONSIST tuner, the scheme still converges but more slowly, as it migrates and then throttles itself based on quota feedback. The time to reach the goal varies depending on workload intensity.

Quoted 8 lines
Quoted 4 lines

Note: This metric works with both TEMPORAL and CONSIST goal tuners.

Assuming below two minor things are addressed,

Thank you SJ. Will send v9 addressing these two changes.

Quoted 75 lines

[...]

Quoted 9 lines
mm/damon/core.c +9 -0
+static unsigned long damos_get_node_eligible_mem_bp(struct damon_ctx *c,
+             struct damos *s, int nid)
+{
+     phys_addr_t total_eligible = 0;
+     phys_addr_t node_eligible;
+
+     if (c->ops.id != DAMON_OPS_PADDR)
+             return 0;
+

mm/damon/core.c static unsigned long damos_get_node_memcg_used_bp( · line 2405

Quoted 10 lines
mm/damon/core.c +10 -0
+     if (nid < 0 || nid >= MAX_NUMNODES || !node_online(nid))
+             return 0;
+
+     node_eligible = damos_calc_eligible_bytes(c, s, nid, &total_eligible);
+
+     if (!total_eligible)
+             return 0;
+
+     return mult_frac((unsigned long)node_eligible, 10000,
+                     (unsigned long)total_eligible);

Sashiko found [1] total_eligible after the casting could be zero on 32bit system, resulting in divide-by-zero. As I also replied to Sashiko review, could you please fix this? It seems we can simply remove the castings.

[...]

mm/damon/core.c static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota) · line 2531

Quoted 7 lines
mm/damon/core.c +1 -2
@@ -2389,9 +2528,9 @@ static void damos_goal_tune_esz_bp_temporal(struct damos_quota *quota)
 /*
  * Called only if quota->ms, or quota->sz are set, or quota->goals is not empty
  */
-static void damos_set_effective_quota(struct damos_quota *quota,
-             struct damon_ctx *ctx)
+static void damos_set_effective_quota(struct damon_ctx *c, struct damos *s)

Sorry for finding this late. Could we keep the dmon_ctx parameter name? Otherwise, we introduce unnecessary change below.

If the mult_frac() divide-by-zero is not a real issue, I wouldn't insist this change. But, if we will make a new version, let's do this together.

Quoted 18 lines
mm/damon/core.c +3 -2
 {
+     struct damos_quota *quota = &s->quota;
      unsigned long throughput;
      unsigned long esz = ULONG_MAX;
 
@@ -2402,9 +2541,9 @@ static void damos_set_effective_quota(struct damos_quota *quota,
 
      if (!list_empty(&quota->goals)) {
              if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_CONSIST)
-                     damos_goal_tune_esz_bp_consist(quota);
+                     damos_goal_tune_esz_bp_consist(c, s);
              else if (quota->goal_tuner == DAMOS_QUOTA_GOAL_TUNER_TEMPORAL)
-                     damos_goal_tune_esz_bp_temporal(quota);
+                     damos_goal_tune_esz_bp_temporal(c, s);
              esz = quota->esz_bp / 10000;
      }
 

mm/damon/core.c static void damos_set_effective_quota(struct damos_quota *quota, · line 2557

Quoted 5 lines
mm/damon/core.c +1 -1
@@ -2415,7 +2554,7 @@ static void damos_set_effective_quota(struct damos_quota *quota,
              else
                      throughput = PAGE_SIZE * 1024;
              esz = min(throughput * quota->ms, esz);
-             esz = max(ctx->min_region_sz, esz);
+             esz = max(c->min_region_sz, esz);

Above change is unnecessarily introduced. Could we keep the old damon_ctx parameter name?

[1] https://lore.kernel.org/[email protected]

Thanks,
SJ

[...]