mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC v2 0/1] mm: add source selector to memory.reclaim
@ 2026-11-02  7:04 Li Zhe
  2026-11-02  7:04 ` [RFC v2 1/1] " Li Zhe
  2026-11-12 23:48 ` [RFC v2 0/1] " Joshua Hahn
  0 siblings, 2 replies; 3+ messages in thread
From: Li Zhe @ 2026-11-02  7:04 UTC (permalink / raw)
  To: akpm, david, gourry, joshua.hahnjy, ljs, mhocko, muchun.song,
	rakie.kim, roman.gushchin, shakeel.butt, weixugc, ying.huang,
	ziy
  Cc: cgroups, linux-doc, linux-kernel, linux-mm, Li Zhe

memory.reclaim is a useful proactive reclaim interface for memcgs, but
tiered memory makes "reclaim N bytes from this memcg" underspecified for
userspace controllers that manage memory placement across tiers. The
kernel reclaim path may demote some folios and evict others. Without a
way to declare where reclaim pressure should be applied, userspace can
only reclaim from the memcg as a whole and observe one aggregate progress
value.

There is prior history here [1]. The old nodes= proposal tried to let
memory.reclaim scan selected NUMA nodes, but the discussion left several
ABI questions unresolved:

- nodes= did not clearly say whether the mask was a source selector, a
  reclaim domain, or a demotion-target constraint.
- The implementation did not apply the mask to the whole reclaim path,
  so demotion could allocate outside the mask. If userspace interpreted
  the mask as a domain or target constraint, the ABI did not provide
  that.
- Selecting top-tier nodes could become an implicit way to request
  demotion without uncharge. That would make userspace depend on a
  particular reclaim policy and could make future policy changes hard.
- The separate completion-accounting question remained: demotion can
  count as reclaim progress even though memory.current is not reduced.

This series takes a narrower approach. It adds a typed source= key which
declares only the resident source to which the existing memory.reclaim
path should apply pressure. It is not a demotion target selector, not a
demotion-only goal, not an eviction-only goal, and not a change to the
existing completion accounting. Leaving source unspecified preserves the
existing memory.reclaim behavior.

In that sense, this series extends node-scoped reclaim pressure to the
memcg memory.reclaim interface. The existing node memory_reclaim
interface can apply pressure to a node, but it is not scoped to a
particular memcg. This series lets userspace apply the same kind of
source-scoped pressure to a cgroup's resident memory on selected nodes,
while still using the normal memcg reclaim path.

The typed source= form also follows a direction that has recently been
discussed for memory.reclaim. In the zswap proactive writeback series,
the interface was changed from a one-off zswap_writeback_only key to
source=zswap so that other reclaim sources could be selected in the
future [2][3]. This series uses the same extensible key shape, but
defines a different source class: resident NUMA nodes. source=zswap
bypasses ordinary reclaim and performs zswap writeback; source=nodes:
<nodelist> only constrains where the normal memcg reclaim path applies
pressure.

The main benefit of this series is to give userspace a steering knob for
per-node memory placement control. A tiering controller can still build
per-tier policy on top of it by translating a tier to its node list, for
example from /sys/devices/virtual/memory_tiering/memory_tierN/nodelist.
It can then apply reclaim pressure to the nodes that should be relieved.
For example, it can apply pressure to top-tier nodes so that, under the
existing tiering policy, cold folios can usually be demoted to lower
tiers. Another controller may first reclaim from lower-tier nodes so that
later top-tier reclaim has room for demotion. A controller can also
reclaim from terminal-tier nodes when it needs actual charge reduction.

The existing memory.reclaim interface cannot express this. It applies
pressure to the memcg as a whole, so on a multi-tier system the kernel
may make progress in a tier different from the one userspace is trying
to control. That makes a userspace feedback loop for tier ratios
ineffective: the controller can observe that a specific node or tier is
over its desired resident footprint, but it cannot direct the next
reclaim request to the corresponding source nodes.

This version only adds source=nodes:<nodelist>, which is the basic
source selector. More convenience selectors can be considered later, but
the tier use case is already expressible by resolving a tier's current
nodelist in userspace and passing that nodelist to
source=nodes:<nodelist>. Starting with the node-based interface keeps the
ABI smaller while preserving the source= namespace for future selectors.

The remaining issues from the old discussion are intentionally left out
of this series. If userspace needs to control demotion targets, that
should be a separate target/demotion-policy ABI rather than folded into
source=. If userspace needs a strict "bytes actually uncharged"
completion mode, that should be discussed as a separate accounting or
goal extension because it would change the observable meaning of
memory.reclaim progress. This series only adds source selection for the
existing reclaim policy.

Testing:

I ran a simple userspace feedback controller on a CXL test machine. The
machine had nodes 0-2 as the DRAM tier and node 3 as the CXL tier. The
basic matrix used node 0 as the DRAM source and node 3 as the CXL
source for the target memcg, with fio and stress-ng as the load
generators. The controller:

- read anon/file bytes from memory.numa_stat by node;
- aggregated them into DRAM and CXL resident bytes;
- targeted a 1:1 DRAM:CXL resident ratio for the target memcg;
- selected the side above 55%;
- wrote memory.reclaim with source=nodes:<selected nodes>; and
- slept for the configured control period before the next sample.

The controller loop was approximately:

while running:
        stat = read_memory_numa_stat()
        dram = resident_bytes(stat, dram_nodes)
        cxl = resident_bytes(stat, cxl_nodes)
        total = dram + cxl

        if dram > total * 0.55:
                source_nodes = dram_nodes
                excess = dram - total / 2
        else if cxl > total * 0.55:
                source_nodes = cxl_nodes
                excess = cxl - total / 2
        else:
                sleep(period)
                continue

        bytes = clamp(excess * gain, min_step, max_step)
        write(memory.reclaim,
              "$bytes source=nodes:<source_nodes>")
        sleep(period)

The requested reclaim size was proportional to the excess bytes, with
per-workload min/max bounds. I tested 1G, 5G, 10G, 20G and 50G
workloads with 1s, 2s, 5s and 10s control periods. A run was considered
within target when max(abs(dram_ratio - 0.5), abs(cxl_ratio - 0.5)) was
not greater than 5 percentage points.

Basic feedback-controller matrix, final deviation from the 1:1 target:

The 1s, 2s, 5s and 10s columns are controller periods.

The file-cache-CXL-high case used fio under numactl to create about 25%
DRAM and 75% CXL page cache. It reached within 5% by at most the
eighth control cycle, and all 20 runs ended within 5%.

size  1s     2s     5s     10s
 1G   3.96%  3.96%  4.45%  3.96%
 5G   2.65%  2.75%  2.65%  2.65%
10G   2.65%  2.68%  2.65%  2.69%
20G   2.65%  2.66%  2.66%  2.64%
50G   4.31%  4.31%  4.31%  4.31%

The anon-DRAM-high case used stress-ng under numactl to create
anonymous memory on DRAM and used swappiness=max so that the normal
reclaim path could demote or reclaim anonymous memory. It reached
within 5% by at most the seventh control cycle, and all 20 runs ended
within 5%.

size  1s     2s     5s     10s
 1G   0.15%  0.02%  0.16%  0.02%
 5G   3.16%  3.13%  3.14%  3.13%
10G   3.13%  3.13%  3.15%  3.12%
20G   3.12%  3.13%  3.12%  3.12%
50G   1.88%  1.88%  1.88%  1.88%

For comparison, I also ran the same controller and workloads without
the source=nodes selector. The controller still chose the side above the
target and computed the same request size, but wrote plain
memory.reclaim without a source constraint.

case                  final <=5%  reached <=5%  worst deviation
file-cache CXL high        1/20          1/20     50.00%
anon DRAM high            20/20         20/20      5.00%

This shows why the source selector matters for placement control. In
the file-cache-CXL-high case, plain memory.reclaim could not reliably
control the target ratio without source=nodes. In the anon-DRAM-high
case, plain memory.reclaim worked well. This is likely because, in this
setup, the whole-memcg reclaim walk reached DRAM nodes before CXL nodes,
so it already applied pressure to the over-target DRAM side first for
this workload.

I also ran a disturbed case with external reclaim and demotion activity.
The target memcg used stress-ng under numactl to create about 25% DRAM
and 75% CXL anonymous memory. NUMA demotion and zswap were enabled, and
watermark_scale_factor was raised to 3000 so background reclaim became
visible in a short run. A ballast cgroup kept DRAM free memory low, and
a separate CXL-heavy cgroup kept reclaim activity present on the CXL
tier. The result below was measured on the target memcg.

During the matrix, pgdemote_kswapd increased in 16/20 runs. The
CXL-free-down/DRAM-free-up/pgdemote pattern appeared in 13/20 runs.
Even with that external activity, the target memcg controller still
drove its own DRAM:CXL resident ratio toward 1:1.

Disturbed case, final deviation from the 1:1 target. The 1s, 2s, 5s and
10s columns are controller periods:

size  1s     2s     5s     10s
 1G   1.18%  1.11%  1.11%  1.42%
 5G   2.14%  2.06%  2.14%  2.14%
10G   2.13%  2.10%  2.15%  2.11%
20G   2.15%  2.23%  2.15%  2.43%
50G   4.87%  4.87%  4.89%  4.87%

All 20 disturbed runs reached within 5% by the third control cycle and
ended within 5%. This does not make memory.reclaim a one-shot footprint
guarantee, but it shows that source=nodes can act as a useful
feedback-loop actuator.

References:
[1] https://lore.kernel.org/all/Y5bsmpCyeryu3Zz1@dhcp22.suse.cz/
[2] https://lore.kernel.org/all/20260629112032.20423-1-jiahao.kernel@gmail.com/
[3] https://lore.kernel.org/all/20260629112032.20423-5-jiahao.kernel@gmail.com/

Li Zhe (1):
  mm: add source selector to memory.reclaim

 Documentation/admin-guide/cgroup-v2.rst |  9 +++++
 mm/bpf_memcontrol.c                     |  3 +-
 mm/internal.h                           |  3 +-
 mm/memcontrol-v1.c                      |  6 ++-
 mm/memcontrol.c                         | 11 ++++--
 mm/vmscan.c                             | 49 +++++++++++++++++++++++--
 6 files changed, 70 insertions(+), 11 deletions(-)

---
v1: https://lore.kernel.org/all/20261019141401.11927-1-lizhe.67@bytedance.com/

Changes since v1:
- Reworded the cover letter and documentation to describe the interface
  as applying reclaim pressure to a selected resident source.
- Added the lower-tier use case where userspace reclaims from a lower
  tier first so that later top-tier reclaim has room for demotion.
- Dropped patch 2, the tier convenience selector, for now. Tier-aware
  userspace can resolve a tier to a nodelist and use source=nodes.
- Added references to the zswap proactive writeback discussion, which
  also uses the typed source= form to keep memory.reclaim extensible.
- Added CXL feedback-controller test results using fio and stress-ng
  load generators, including a no-source baseline.

-- 
2.20.1

^ permalink raw reply	[flat|nested] 3+ messages in thread

* [RFC v2 1/1] mm: add source selector to memory.reclaim
  2026-11-02  7:04 [RFC v2 0/1] mm: add source selector to memory.reclaim Li Zhe
@ 2026-11-02  7:04 ` Li Zhe
  2026-11-12 23:48 ` [RFC v2 0/1] " Joshua Hahn
  1 sibling, 0 replies; 3+ messages in thread
From: Li Zhe @ 2026-11-02  7:04 UTC (permalink / raw)
  To: akpm, david, gourry, joshua.hahnjy, ljs, mhocko, muchun.song,
	rakie.kim, roman.gushchin, shakeel.butt, weixugc, ying.huang,
	ziy
  Cc: cgroups, linux-doc, linux-kernel, linux-mm, Li Zhe

memory.reclaim was added as a generic proactive reclaim
interface for memcgs, but memory tiering makes its practical
meaning less obvious. Reclaim may demote cold folios from a
faster tier to a slower tier before they are eventually evicted.
As a result, a write to memory.reclaim can either move folios
between tiers or evict them from the cgroup, depending on where
the reclaim walk finds eligible memory and whether demotion is
available.

That ambiguity is awkward for userspace controllers. A controller
that is trying to manage DRAM, CXL, or other tier occupancy cannot
direct reclaim pressure to the resident memory it wants to relieve.
It can only ask the kernel to reclaim from the memcg as a whole,
while the resulting progress is reported as one aggregate value.
This makes effective tier-ratio control largely infeasible:
userspace can observe that the distribution of memory across tiers
has drifted away from the controller's preferred balance, but it
cannot direct the next reclaim request to the corresponding source
nodes. It also makes controlled charge reduction unreliable: a
controller that wants to free memory from a terminal tier cannot ask
reclaim to start from that tier.

Add a typed source= selector to memory.reclaim. The first
supported form is source=nodes:<nodelist>. The selector declares
the source nodes to which userspace wants the kernel to apply the
normal memcg reclaim path. It constrains the source to which reclaim
pressure is applied; it is not a demotion-only goal, an eviction-only
goal, or a demotion target selector.

The selector only chooses where the existing reclaim policy is
applied. It does not change when the kernel decides to demote,
evict, activate, or keep a folio.

With this primitive, userspace can build higher-level policies by
choosing different sources across requests. For example, a
controller can issue one request against top-tier nodes for tier
balancing, another request against lower-tier nodes to make room for
later demotion, and another request against terminal-tier nodes when
it needs actual charge reduction. This composes tiering policies from
a simple source selector instead of adding policy-specific
memory.reclaim keys. This patch does not change the reclaim policy:
memory.reclaim continues to run the kernel's existing reclaim path,
now with an optional source constraint.

Leaving source unspecified preserves the existing memory.reclaim
behavior.

The source= key is intentionally typed so future selectors can be
added without reinterpreting source=nodes:<nodelist>.

Signed-off-by: Li Zhe <lizhe.67@bytedance.com>
---
 Documentation/admin-guide/cgroup-v2.rst |  9 +++++
 mm/bpf_memcontrol.c                     |  3 +-
 mm/internal.h                           |  3 +-
 mm/memcontrol-v1.c                      |  6 ++-
 mm/memcontrol.c                         | 11 ++++--
 mm/vmscan.c                             | 49 +++++++++++++++++++++++--
 6 files changed, 70 insertions(+), 11 deletions(-)

diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 9e3c67b02ae2e..ee42da817c062 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -1458,6 +1458,7 @@ The following nested keys are defined.
 
 	  ==========            ================================
 	  swappiness            Swappiness value to reclaim with
+	  source                Source to apply reclaim pressure to
 	  ==========            ================================
 
 	Specifying a swappiness value instructs the kernel to perform
@@ -1468,6 +1469,14 @@ The following nested keys are defined.
 	The valid range for swappiness is [0-200, max], setting
 	swappiness=max exclusively reclaims anonymous memory.
 
+	Specifying a source value constrains where proactive reclaim applies
+	pressure. Currently only source=nodes:<nodelist> is supported. For
+	example::
+
+	  echo "1G source=nodes:0-1" > memory.reclaim
+
+	This reclaims memory of the target cgroup resident on nodes 0-1.
+
   memory.peak
 	A read-write single value file which exists on non-root cgroups.
 
diff --git a/mm/bpf_memcontrol.c b/mm/bpf_memcontrol.c
index d8f579c285600..7d98f55a044fa 100644
--- a/mm/bpf_memcontrol.c
+++ b/mm/bpf_memcontrol.c
@@ -201,7 +201,8 @@ __bpf_kfunc long bpf_proactive_reclaim(struct mem_cgroup *memcg,
 	nr_reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages, GFP_KERNEL,
 						    MEMCG_RECLAIM_MAY_SWAP |
 						    MEMCG_RECLAIM_PROACTIVE,
-						    swappiness == -1 ? NULL : &swappiness);
+						    swappiness == -1 ? NULL : &swappiness,
+						    NULL);
 
 	return nr_reclaimed * PAGE_SIZE;
 }
diff --git a/mm/internal.h b/mm/internal.h
index 566cbd2dce485..b16efceb58458 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -83,7 +83,8 @@ unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,
 					   unsigned long nr_pages,
 					   gfp_t gfp_mask,
 					   unsigned int reclaim_options,
-					   int *swappiness);
+					   int *swappiness,
+					   nodemask_t *source_nodes);
 
 #ifdef CONFIG_NUMA
 extern int sysctl_min_unmapped_ratio;
diff --git a/mm/memcontrol-v1.c b/mm/memcontrol-v1.c
index b7f2868885071..74aa06fee76a0 100644
--- a/mm/memcontrol-v1.c
+++ b/mm/memcontrol-v1.c
@@ -1464,7 +1464,8 @@ static int mem_cgroup_resize_max(struct mem_cgroup *memcg,
 		}
 
 		if (!try_to_free_mem_cgroup_pages(memcg, 1, GFP_KERNEL,
-				memsw ? 0 : MEMCG_RECLAIM_MAY_SWAP, NULL)) {
+				memsw ? 0 : MEMCG_RECLAIM_MAY_SWAP,
+				NULL, NULL)) {
 			ret = -EBUSY;
 			break;
 		}
@@ -1500,7 +1501,8 @@ static int mem_cgroup_force_empty(struct mem_cgroup *memcg)
 			break;
 
 		if (!try_to_free_mem_cgroup_pages(memcg, 1, GFP_KERNEL,
-						  MEMCG_RECLAIM_MAY_SWAP, NULL))
+						  MEMCG_RECLAIM_MAY_SWAP,
+						  NULL, NULL))
 			nr_retries--;
 	}
 
diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index a05d9a8baa1b6..f07dc44b9e4fc 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -2444,7 +2444,7 @@ static unsigned long reclaim_high(struct mem_cgroup *memcg,
 		nr_reclaimed += try_to_free_mem_cgroup_pages(memcg, nr_pages,
 							gfp_mask,
 							MEMCG_RECLAIM_MAY_SWAP,
-							NULL);
+							NULL, NULL);
 		psi_memstall_leave(&pflags);
 	} while ((memcg = parent_mem_cgroup(memcg)) &&
 		 !mem_cgroup_is_root(memcg));
@@ -2771,7 +2771,8 @@ static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
 
 	psi_memstall_enter(&pflags);
 	nr_reclaimed = try_to_free_mem_cgroup_pages(mem_over_limit, nr_pages,
-						    gfp_mask, reclaim_options, NULL);
+						    gfp_mask, reclaim_options,
+						    NULL, NULL);
 	psi_memstall_leave(&pflags);
 
 	if (mem_cgroup_margin(mem_over_limit) >= nr_pages)
@@ -4996,7 +4997,8 @@ static ssize_t memory_high_write(struct kernfs_open_file *of,
 		}
 
 		reclaimed = try_to_free_mem_cgroup_pages(memcg, nr_pages - high,
-					GFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP, NULL);
+					GFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP,
+					NULL, NULL);
 
 		if (!reclaimed && !nr_retries--)
 			break;
@@ -5055,7 +5057,8 @@ static ssize_t memory_max_write(struct kernfs_open_file *of,
 
 		if (nr_reclaims) {
 			if (!try_to_free_mem_cgroup_pages(memcg, nr_pages - max,
-					GFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP, NULL))
+					GFP_KERNEL, MEMCG_RECLAIM_MAY_SWAP,
+					NULL, NULL))
 				nr_reclaims--;
 			continue;
 		}
diff --git a/mm/vmscan.c b/mm/vmscan.c
index 9959ca158ce83..265807e437af4 100644
--- a/mm/vmscan.c
+++ b/mm/vmscan.c
@@ -7013,12 +7013,14 @@ unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,
 					   unsigned long nr_pages,
 					   gfp_t gfp_mask,
 					   unsigned int reclaim_options,
-					   int *swappiness)
+					   int *swappiness,
+					   nodemask_t *source_nodes)
 {
 	unsigned long nr_reclaimed;
 	unsigned int noreclaim_flag;
 	struct scan_control sc = {
 		.nr_to_reclaim = max(nr_pages, SWAP_CLUSTER_MAX),
+		.nodemask = source_nodes,
 		.proactive_swappiness = swappiness,
 		.gfp_mask = (current_gfp_context(gfp_mask) & GFP_RECLAIM_MASK) |
 				(GFP_HIGHUSER_MOVABLE & ~GFP_RECLAIM_MASK),
@@ -7054,7 +7056,8 @@ unsigned long try_to_free_mem_cgroup_pages(struct mem_cgroup *memcg,
 					   unsigned long nr_pages,
 					   gfp_t gfp_mask,
 					   unsigned int reclaim_options,
-					   int *swappiness)
+					   int *swappiness,
+					   nodemask_t *source_nodes)
 {
 	return 0;
 }
@@ -8098,20 +8101,51 @@ static unsigned long __node_reclaim(struct pglist_data *pgdat,
 enum {
 	MEMORY_RECLAIM_SWAPPINESS = 0,
 	MEMORY_RECLAIM_SWAPPINESS_MAX,
+	MEMORY_RECLAIM_SOURCE,
 	MEMORY_RECLAIM_NULL,
 };
 static const match_table_t tokens = {
 	{ MEMORY_RECLAIM_SWAPPINESS, "swappiness=%d"},
 	{ MEMORY_RECLAIM_SWAPPINESS_MAX, "swappiness=max"},
+	{ MEMORY_RECLAIM_SOURCE, "source=%s"},
 	{ MEMORY_RECLAIM_NULL, NULL },
 };
 
+#define MEMORY_RECLAIM_SOURCE_NODES	"nodes:"
+#define MEMORY_RECLAIM_SOURCE_NODES_LEN	(sizeof(MEMORY_RECLAIM_SOURCE_NODES) - 1)
+
+static int parse_memory_reclaim_source(const char *source,
+				       nodemask_t *source_nodes)
+{
+	const char *nodes;
+
+	if (strncmp(source, MEMORY_RECLAIM_SOURCE_NODES,
+		    MEMORY_RECLAIM_SOURCE_NODES_LEN))
+		return -EINVAL;
+
+	nodes = source + MEMORY_RECLAIM_SOURCE_NODES_LEN;
+	if (!*nodes)
+		return -EINVAL;
+
+	nodes_clear(*source_nodes);
+	if (nodelist_parse(nodes, *source_nodes))
+		return -EINVAL;
+
+	nodes_and(*source_nodes, *source_nodes, node_states[N_MEMORY]);
+	if (nodes_empty(*source_nodes))
+		return -EINVAL;
+
+	return 0;
+}
+
 int user_proactive_reclaim(char *buf,
 			   struct mem_cgroup *memcg, pg_data_t *pgdat)
 {
 	unsigned int nr_retries = MAX_RECLAIM_RETRIES;
 	unsigned long nr_to_reclaim, nr_reclaimed = 0;
 	int swappiness = -1;
+	nodemask_t source_nodes;
+	nodemask_t *source_nodemask = NULL;
 	char *old_buf, *start;
 	substring_t args[MAX_OPT_ARGS];
 	gfp_t gfp_mask = GFP_KERNEL;
@@ -8142,6 +8176,14 @@ int user_proactive_reclaim(char *buf,
 		case MEMORY_RECLAIM_SWAPPINESS_MAX:
 			swappiness = SWAPPINESS_ANON_ONLY;
 			break;
+		case MEMORY_RECLAIM_SOURCE:
+			if (!memcg || source_nodemask)
+				return -EINVAL;
+			if (parse_memory_reclaim_source(args[0].from,
+							&source_nodes))
+				return -EINVAL;
+			source_nodemask = &source_nodes;
+			break;
 		default:
 			return -EINVAL;
 		}
@@ -8181,7 +8223,8 @@ int user_proactive_reclaim(char *buf,
 			reclaimed = try_to_free_mem_cgroup_pages(memcg,
 						 batch_size, gfp_mask,
 						 reclaim_options,
-						 swappiness == -1 ? NULL : &swappiness);
+						 swappiness == -1 ? NULL : &swappiness,
+						 source_nodemask);
 		} else {
 			struct scan_control sc = {
 				.gfp_mask = current_gfp_context(gfp_mask),
-- 
2.20.1

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [RFC v2 0/1] mm: add source selector to memory.reclaim
  2026-11-02  7:04 [RFC v2 0/1] mm: add source selector to memory.reclaim Li Zhe
  2026-11-02  7:04 ` [RFC v2 1/1] " Li Zhe
@ 2026-11-12 23:48 ` Joshua Hahn
  1 sibling, 0 replies; 3+ messages in thread
From: Joshua Hahn @ 2026-11-12 23:48 UTC (permalink / raw)
  To: Li Zhe
  Cc: akpm, david, gourry, ljs, mhocko, muchun.song, rakie.kim,
	roman.gushchin, shakeel.butt, weixugc, ying.huang, ziy, cgroups,
	linux-doc, linux-kernel, linux-mm

On Mon, Nov 02, 2026 at 03:04:39PM +0800, Li Zhe wrote:

Hi Zhe,

Sorry that I got to this so late. I must have missed it in my inbox
somehow!

> memory.reclaim is a useful proactive reclaim interface for memcgs, but
> tiered memory makes "reclaim N bytes from this memcg" underspecified for
> userspace controllers that manage memory placement across tiers. The
> kernel reclaim path may demote some folios and evict others. Without a
> way to declare where reclaim pressure should be applied, userspace can
> only reclaim from the memcg as a whole and observe one aggregate progress
> value.

Does this series do anything to distinguish demotions from evicting?
If not, I'm not sure why it is mentioned above.

> There is prior history here [1]. The old nodes= proposal tried to let
> memory.reclaim scan selected NUMA nodes, but the discussion left several
> ABI questions unresolved:
> 
> - nodes= did not clearly say whether the mask was a source selector, a
>   reclaim domain, or a demotion-target constraint.
> - The implementation did not apply the mask to the whole reclaim path,
>   so demotion could allocate outside the mask. If userspace interpreted
>   the mask as a domain or target constraint, the ABI did not provide
>   that.
> - Selecting top-tier nodes could become an implicit way to request
>   demotion without uncharge. That would make userspace depend on a
>   particular reclaim policy and could make future policy changes hard.
> - The separate completion-accounting question remained: demotion can
>   count as reclaim progress even though memory.current is not reduced.
> 
> This series takes a narrower approach. It adds a typed source= key which
> declares only the resident source to which the existing memory.reclaim
> path should apply pressure. It is not a demotion target selector, not a
> demotion-only goal, not an eviction-only goal, and not a change to the
> existing completion accounting. Leaving source unspecified preserves the
> existing memory.reclaim behavior.

Right, I was a bit confused why you presented not distinguishing between
demotion and eviction as a problem, since this patch just preserves
the existing behavior for reclaim.

> In that sense, this series extends node-scoped reclaim pressure to the
> memcg memory.reclaim interface. The existing node memory_reclaim
> interface can apply pressure to a node, but it is not scoped to a
> particular memcg. This series lets userspace apply the same kind of
> source-scoped pressure to a cgroup's resident memory on selected nodes,
> while still using the normal memcg reclaim path.
> 
> The typed source= form also follows a direction that has recently been
> discussed for memory.reclaim. In the zswap proactive writeback series,
> the interface was changed from a one-off zswap_writeback_only key to
> source=zswap so that other reclaim sources could be selected in the
> future [2][3]. This series uses the same extensible key shape, but
> defines a different source class: resident NUMA nodes. source=zswap
> bypasses ordinary reclaim and performs zswap writeback; source=nodes:
> <nodelist> only constrains where the normal memcg reclaim path applies
> pressure.
> 
> The main benefit of this series is to give userspace a steering knob for
> per-node memory placement control. A tiering controller can still build
> per-tier policy on top of it by translating a tier to its node list, for
> example from /sys/devices/virtual/memory_tiering/memory_tierN/nodelist.
> It can then apply reclaim pressure to the nodes that should be relieved.
> For example, it can apply pressure to top-tier nodes so that, under the
> existing tiering policy, cold folios can usually be demoted to lower
> tiers. Another controller may first reclaim from lower-tier nodes so that
> later top-tier reclaim has room for demotion. A controller can also
> reclaim from terminal-tier nodes when it needs actual charge reduction.
> 
> The existing memory.reclaim interface cannot express this. It applies
> pressure to the memcg as a whole, so on a multi-tier system the kernel
> may make progress in a tier different from the one userspace is trying
> to control. That makes a userspace feedback loop for tier ratios
> ineffective: the controller can observe that a specific node or tier is
> over its desired resident footprint, but it cannot direct the next
> reclaim request to the corresponding source nodes.
> 
> This version only adds source=nodes:<nodelist>, which is the basic
> source selector. More convenience selectors can be considered later, but
> the tier use case is already expressible by resolving a tier's current
> nodelist in userspace and passing that nodelist to
> source=nodes:<nodelist>. Starting with the node-based interface keeps the
> ABI smaller while preserving the source= namespace for future selectors.
> 
> The remaining issues from the old discussion are intentionally left out
> of this series. If userspace needs to control demotion targets, that
> should be a separate target/demotion-policy ABI rather than folded into
> source=. If userspace needs a strict "bytes actually uncharged"
> completion mode, that should be discussed as a separate accounting or
> goal extension because it would change the observable meaning of
> memory.reclaim progress. This series only adds source selection for the
> existing reclaim policy.
> 
> Testing:
> 
> I ran a simple userspace feedback controller on a CXL test machine. The
> machine had nodes 0-2 as the DRAM tier and node 3 as the CXL tier. The
> basic matrix used node 0 as the DRAM source and node 3 as the CXL
> source for the target memcg, with fio and stress-ng as the load
> generators. The controller:
> 
> - read anon/file bytes from memory.numa_stat by node;
> - aggregated them into DRAM and CXL resident bytes;
> - targeted a 1:1 DRAM:CXL resident ratio for the target memcg;
> - selected the side above 55%;
> - wrote memory.reclaim with source=nodes:<selected nodes>; and
> - slept for the configured control period before the next sample.
> 
> The controller loop was approximately:
> 
> while running:
>         stat = read_memory_numa_stat()
>         dram = resident_bytes(stat, dram_nodes)
>         cxl = resident_bytes(stat, cxl_nodes)
>         total = dram + cxl
> 
>         if dram > total * 0.55:
>                 source_nodes = dram_nodes
>                 excess = dram - total / 2
>         else if cxl > total * 0.55:
>                 source_nodes = cxl_nodes
>                 excess = cxl - total / 2
>         else:
>                 sleep(period)
>                 continue
> 
>         bytes = clamp(excess * gain, min_step, max_step)
>         write(memory.reclaim,
>               "$bytes source=nodes:<source_nodes>")
>         sleep(period)
> 
> The requested reclaim size was proportional to the excess bytes, with
> per-workload min/max bounds. I tested 1G, 5G, 10G, 20G and 50G
> workloads with 1s, 2s, 5s and 10s control periods. A run was considered
> within target when max(abs(dram_ratio - 0.5), abs(cxl_ratio - 0.5)) was
> not greater than 5 percentage points.
> 
> Basic feedback-controller matrix, final deviation from the 1:1 target:
> 
> The 1s, 2s, 5s and 10s columns are controller periods.
> 
> The file-cache-CXL-high case used fio under numactl to create about 25%
> DRAM and 75% CXL page cache. It reached within 5% by at most the
> eighth control cycle, and all 20 runs ended within 5%.

These results are really confusing. What does the % mean here?
% deviation from a 50-50 split? I want to review the code but I want
to understand what the test is actually trying to do first.

In any case, I think the important tests to demonstrate the code are:
- How does this perform when the entire system is under pressure?
- How are multiple cgroups simultaneously present on the host dealt
  with? Are they also able to manage an effective split? 

> size  1s     2s     5s     10s
>  1G   3.96%  3.96%  4.45%  3.96%
>  5G   2.65%  2.75%  2.65%  2.65%
> 10G   2.65%  2.68%  2.65%  2.69%
> 20G   2.65%  2.66%  2.66%  2.64%
> 50G   4.31%  4.31%  4.31%  4.31%
> 
> The anon-DRAM-high case used stress-ng under numactl to create
> anonymous memory on DRAM and used swappiness=max so that the normal
> reclaim path could demote or reclaim anonymous memory. It reached
> within 5% by at most the seventh control cycle, and all 20 runs ended
> within 5%.
> 
> size  1s     2s     5s     10s
>  1G   0.15%  0.02%  0.16%  0.02%
>  5G   3.16%  3.13%  3.14%  3.13%
> 10G   3.13%  3.13%  3.15%  3.12%
> 20G   3.12%  3.13%  3.12%  3.12%
> 50G   1.88%  1.88%  1.88%  1.88%
> 
> For comparison, I also ran the same controller and workloads without
> the source=nodes selector. The controller still chose the side above the
> target and computed the same request size, but wrote plain
> memory.reclaim without a source constraint.
> 
> case                  final <=5%  reached <=5%  worst deviation
> file-cache CXL high        1/20          1/20     50.00%
> anon DRAM high            20/20         20/20      5.00%
> 
> This shows why the source selector matters for placement control. In
> the file-cache-CXL-high case, plain memory.reclaim could not reliably
> control the target ratio without source=nodes. In the anon-DRAM-high
> case, plain memory.reclaim worked well. This is likely because, in this
> setup, the whole-memcg reclaim walk reached DRAM nodes before CXL nodes,
> so it already applied pressure to the over-target DRAM side first for
> this workload.
> 
> I also ran a disturbed case with external reclaim and demotion activity.
> The target memcg used stress-ng under numactl to create about 25% DRAM
> and 75% CXL anonymous memory. NUMA demotion and zswap were enabled, and
> watermark_scale_factor was raised to 3000 so background reclaim became
> visible in a short run. A ballast cgroup kept DRAM free memory low, and
> a separate CXL-heavy cgroup kept reclaim activity present on the CXL
> tier. The result below was measured on the target memcg.
> 
> During the matrix, pgdemote_kswapd increased in 16/20 runs. The
> CXL-free-down/DRAM-free-up/pgdemote pattern appeared in 13/20 runs.
> Even with that external activity, the target memcg controller still
> drove its own DRAM:CXL resident ratio toward 1:1.
> 
> Disturbed case, final deviation from the 1:1 target. The 1s, 2s, 5s and
> 10s columns are controller periods:
> 
> size  1s     2s     5s     10s
>  1G   1.18%  1.11%  1.11%  1.42%
>  5G   2.14%  2.06%  2.14%  2.14%
> 10G   2.13%  2.10%  2.15%  2.11%
> 20G   2.15%  2.23%  2.15%  2.43%
> 50G   4.87%  4.87%  4.89%  4.87%
> 
> All 20 disturbed runs reached within 5% by the third control cycle and
> ended within 5%. This does not make memory.reclaim a one-shot footprint
> guarantee, but it shows that source=nodes can act as a useful
> feedback-loop actuator.
> 

Zhe, it would be great if the commit message could be a lot, lot
shorter. I feel that this is just really too hard to read. And I'm not
sure if there's a need to have a separate cover letter for a single
patch.

Please, I feel like this is really difficult to read. I think the
commit message could be as short as 3 paragraphs and I think it would
still capture everything that you're trying to say. 

Sorry again for the super late reply, Zhe. I'll be more on top of review
next time! Have a great day,
Joshua

> References:
> [1] https://lore.kernel.org/all/Y5bsmpCyeryu3Zz1@dhcp22.suse.cz/
> [2] https://lore.kernel.org/all/20260629112032.20423-1-jiahao.kernel@gmail.com/
> [3] https://lore.kernel.org/all/20260629112032.20423-5-jiahao.kernel@gmail.com/
> 
> Li Zhe (1):
>   mm: add source selector to memory.reclaim
> 
>  Documentation/admin-guide/cgroup-v2.rst |  9 +++++
>  mm/bpf_memcontrol.c                     |  3 +-
>  mm/internal.h                           |  3 +-
>  mm/memcontrol-v1.c                      |  6 ++-
>  mm/memcontrol.c                         | 11 ++++--
>  mm/vmscan.c                             | 49 +++++++++++++++++++++++--
>  6 files changed, 70 insertions(+), 11 deletions(-)
> 
> ---
> v1: https://lore.kernel.org/all/20261019141401.11927-1-lizhe.67@bytedance.com/
> 
> Changes since v1:
> - Reworded the cover letter and documentation to describe the interface
>   as applying reclaim pressure to a selected resident source.
> - Added the lower-tier use case where userspace reclaims from a lower
>   tier first so that later top-tier reclaim has room for demotion.
> - Dropped patch 2, the tier convenience selector, for now. Tier-aware
>   userspace can resolve a tier to a nodelist and use source=nodes.
> - Added references to the zswap proactive writeback discussion, which
>   also uses the typed source= form to keep memory.reclaim extensible.
> - Added CXL feedback-controller test results using fio and stress-ng
>   load generators, including a no-source baseline.
> 
> -- 
> 2.20.1

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-11-12 23:48 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-02  7:04 [RFC v2 0/1] mm: add source selector to memory.reclaim Li Zhe
2026-11-02  7:04 ` [RFC v2 1/1] " Li Zhe
2026-11-12 23:48 ` [RFC v2 0/1] " Joshua Hahn

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®