Skip to contentSkip to conversation

mm/mglru: speed up inc_min_seq() and fix cold/hot inversions

PATCH Superseded Barry Song (Xiaomi) 4 participants 22 messages Aug 27 – Aug 30

Subsystems (3)

v2 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Barry Song (Xiaomi)

author reviewer
#0 lore raw

v2 cover mm/mglru: speed up inc_min_seq() and fix cold/hot inversions

This is an aging speedup series split out from the MGLRU swappiness series [1], with the inc_min_seq changes separated to make them easier to review.

Currently, inc_min_seq performance is crucial to both the swappiness fix and proactive aging. There are two problems with it:

1. It processes each folio one by one, while many operations can be batched or skipped. For example, a batch of folios can be moved together from the oldest generation to the second-oldest generation, and the associated counting can also be done in batches.

2. It may cause potential cold/hot inversion by placing promoted folios (which have been scanned and found to have young PTEs) behind non-promoted folios. A similar inversion can also occur among non-promoted folios, as tail folios from the oldest generation are placed before head folios when moving them to the second-oldest generation.

This series tries to batch operations as much as possible and fix the potential cold/hot inversion by keeping promoted folios ahead of non-promoted folios, while also preserving the order of non-promoted folios when moving them from the oldest generation to the second-oldest generation.

Minor issue: inc_min_seq() also counts protected folios improperly, as promoted folios should be skipped, as in sort_folio().

We need a stable workload with a stable number of folios to measure aging and evaluate the speedup in inc_min_seq(). So I asked ChatGPT to generate the microbenchmark below. It ages an LRU vec containing 512 MB of memory 100 times:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sys/mman.h>

#define SIZE (512UL * 1024 * 1024)
#define LRU_GEN "/sys/kernel/debug/lru_gen"
#define TARGET_CGROUP "/system.slice/agetest.scope"
#define START_GEN 3
#define END_GEN 103

static long long nsec_diff(const struct timespec *start,
const struct timespec *end)
{
return (end->tv_sec - start->tv_sec) * 1000000000LL +
(end->tv_nsec - start->tv_nsec);
}

static int find_memcg_id(void)
{
FILE *fp;
char line[4096];
int memcg_id;

fp = fopen(LRU_GEN, "r");
if (!fp) {
perror("fopen lru_gen");
return -1;
}

while (fgets(line, sizeof(line), fp)) {
char *p;

if (strncmp(line, "memcg ", 6))
continue;

p = line + 6;

if (sscanf(p, "%d", &memcg_id) != 1)
continue;

/*
* The memcg path follows the numeric ID. */
p = strchr(p, ' ');
if (!p)
continue;

if (strstr(p, TARGET_CGROUP)) {
fclose(fp);
return memcg_id;
}
}

fclose(fp);

fprintf(stderr, "Cannot find %s\n", TARGET_CGROUP); return -1;
}

int main(void)
{
void *addr;
int memcg_id;
int fd;
long long total_ns = 0;

/*
* mmap 512 MB and touch every page.
*/
addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}

memset(addr, 0x55, SIZE);

printf("mmap: %p, size: %lu MB\n",
addr, SIZE / 1024 / 1024);

/*
* Find the memcg ID automatically.
*/
memcg_id = find_memcg_id();
if (memcg_id < 0)
return 1;

printf("memcg: %d (%s)\n", memcg_id, TARGET_CGROUP); printf("aging generation %d -> %d\n",
START_GEN, END_GEN);

fd = open(LRU_GEN, O_WRONLY);
if (fd < 0) {
perror("open lru_gen");
return 1;
}

for (int gen = START_GEN; gen <= END_GEN; gen++) {
char buf[128];
int len;
struct timespec start, end;
long long ns;

len = snprintf(buf, sizeof(buf),
"+ %d 0 %d\n", memcg_id, gen);

clock_gettime(CLOCK_MONOTONIC, &start);

if (write(fd, buf, len) != len) {
perror("write lru_gen");
close(fd);
return 1;
}

clock_gettime(CLOCK_MONOTONIC, &end);

ns = nsec_diff(&start, &end);
total_ns += ns;

printf("gen %3d: %8.3f ms\n",
gen, ns / 1000000.0);
fflush(stdout);
}

close(fd);

printf("\nTotal: %.3f ms\n",
total_ns / 1000000.0);
printf("Average: %.3f ms\n",
total_ns / (double)(END_GEN - START_GEN + 1) / 1000000.0);

while (1)
sleep(1);

return 0;
}

Run the above microbenchmark with:
systemd-run --scope --unit=agetest -p MemoryMax=1024M ./agetest

I’m seeing a significant speedup in inc_min_seq() on my x86 PC:

W/o patch:

Running scope as unit: agetest.scope
mmap: 0x72c1b5a00000, size: 512 MB
memcg: 12673 (/system.slice/agetest.scope)
aging generation 3 -> 103

gen   3:    7.433 ms
gen   4:    0.949 ms
gen   5:    2.535 ms
gen   6:    5.043 ms
gen   7:    5.041 ms
gen   8:    5.027 ms
...
gen 100:    5.035 ms
gen 101:    5.011 ms
gen 102:    5.029 ms
gen 103:    5.056 ms
Total:   503.946 ms
Average: 4.990 ms

W/ patch:

Running scope as unit: agetest.scope
mmap: 0x775ec5200000, size: 512 MB
memcg: 12717 (/system.slice/agetest.scope)
aging generation 3 -> 103

gen   3:    7.558 ms
gen   4:    0.916 ms
gen   5:    2.277 ms
gen   6:    2.328 ms
…
gen 100:    2.303 ms
gen 101:    2.297 ms
gen 102:    2.305 ms
gen 103:    2.312 ms
Total:   236.635 ms
Average: 2.343 ms

The average aging time drops from 4.990 ms to 2.343 ms!

Thanks, Xueyuan, for testing this on ARM[2]. It actually shows an even larger improvement.
Xueyuan tested this series on his arm64 machine (24 cores, 4K base pages) and reproduced the improvement:

THP=never (PTE):
baseline: 7.644 ms
patched: 2.964 ms (-61.2%)

THP=always (PMD):
baseline: 0.0373 ms
patched: 0.0292 ms (-21.8%)

[1] https://lore.kernel.org/linux-mm/[email protected]/
[2] https://lore.kernel.org/linux-mm/[email protected]/

-v2:
* Collect tags from Kairui, Baoquan, and Xueyuan, thanks!
* Split __folio_inc_gen() into a separate patch and add VM_WARN_ON checks for the generation and active state, per Baoquan and Kairui, thanks!
* Make delta a long instead of unsigned long, per Lian Wang, thanks!
* Fix checkpatch issues by converting macros to static inline functions, per Kairui, thanks!
* Drop the batched protected update since it doesn't show a significant performance improvement, following Baoquan's comments.

-v1:
https://lore.kernel.org/linux-mm/[email protected]/

Barry Song (Xiaomi) (7):
mm/mglru: separate folio generation update from LRU accounting mm/mglru: batch update lrugen->nr_pages in inc_min_seq() mm/mglru: enhance cold/hot inversion handling in inc_min_seq() mm/mglru: exclude folios promoted by aging from protected in

    inc_min_seq()

mm/mglru: make LRU folio prefetch helper an inline function mm/mglru: move folios from oldest gen to second-oldest gen from head
to tail
mm/mglru: batch move folios to the second-oldest gen's LRU

 mm/vmscan.c | 129 ++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 100 insertions(+), 29 deletions(-)
Signature · 1 line
2.34.1
author reviewer
#1 lore raw

PATCH v2 1/7 mm/mglru: separate folio generation update from LRU accounting

folio_inc_gen() currently updates both the folio's generation and the LRU size accounting. This makes it difficult to batch the LRU size updates when moving multiple folios.

Extract the generation update into __folio_inc_gen(), which only updates the folio's generation and reports whether the generation was actually increased. Keep folio_inc_gen() as the wrapper that performs the LRU size accounting when needed.

This separates the per-folio generation update from LRU accounting and allows the latter to be batched by subsequent changes.

---
 mm/vmscan.c | 26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)
mm/vmscan.c +20 -6
index fdd13299a04a..8f187d296b8e 100644
@@ -3296,20 +3296,21 @@ static int folio_update_gen(struct folio *folio, int gen, const vma_flags_t *vma
 }
 
 /* protect pages accessed multiple times through file descriptors */
-static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
+static int __folio_inc_gen(struct folio *folio, int old_gen, bool *increased)
 {
-	int type = folio_is_file_lru(folio);
-	struct lru_gen_folio *lrugen = &lruvec->lrugen;
-	int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
 	unsigned long new_flags, old_flags = READ_ONCE(folio->flags.f);
+	int new_gen;
 
 	VM_WARN_ON_ONCE_FOLIO(!(old_flags & LRU_GEN_MASK), folio);
 
 	do {
 		new_gen = ((old_flags & LRU_GEN_MASK) >> LRU_GEN_PGOFF) - 1;
 		/* folio_update_gen() has promoted this page? */
-		if (new_gen >= 0 && new_gen != old_gen)
+		if (new_gen >= 0 && new_gen != old_gen) {
+			if (increased)
+				*increased = false;
 			return new_gen;
+		}
 
 		new_gen = (old_gen + 1) % MAX_NR_GENS;
 
@@ -3317,8 +3318,21 @@ static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
 		new_flags |= (new_gen + 1UL) << LRU_GEN_PGOFF;
 	} while (!try_cmpxchg(&folio->flags.f, &old_flags, new_flags));
 
-	lru_gen_update_size(lruvec, folio, old_gen, new_gen);
+	if (increased)
+		*increased = true;
+	return new_gen;
+}
+
+static int folio_inc_gen(struct lruvec *lruvec, struct folio *folio)
+{
+	int type = folio_is_file_lru(folio);
+	struct lru_gen_folio *lrugen = &lruvec->lrugen;
+	int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
+	bool gen_increased;
 
+	new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
+	if (gen_increased)
+		lru_gen_update_size(lruvec, folio, old_gen, new_gen);
 	return new_gen;
 }
 
Signature · 1 line
2.34.1
Reviewed-by on v2 1/7
#2 lore raw

Re: [PATCH v2 1/7] mm/mglru: separate folio generation update from LRU accounting

Quoted 4 lines

Extract the generation update into __folio_inc_gen(), which only updates the folio's generation and reports whether the generation was actually increased. Keep folio_inc_gen() as the wrapper that performs the LRU size accounting when needed.

This is a nice split. Separating the per-folio generation update from the LRU accounting makes the batching path straightforward. Looks good to me.

author reviewer
#3 lore raw

PATCH v2 2/7 mm/mglru: batch update lrugen->nr_pages in inc_min_seq()

In reply to Barry Song (Xiaomi)

Currently, folio_inc_gen() updates lrugen->nr_pages for every folio as it advances generations. Instead, accumulate the size changes and update lrugen->nr_pages in a batch after scanning the entire oldest generation, or when the scan stops because remaining reaches zero.

Since we only move folios from the oldest generation to the second oldest generation, the active/inactive state cannot change. We can therefore skip __lru_update_size().

---
 mm/vmscan.c | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)
mm/vmscan.c +18 -5
index 8f187d296b8e..07c22d51debd 100644
@@ -3918,6 +3918,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	struct lru_gen_folio *lrugen = &lruvec->lrugen;
 	int hist = lru_hist_from_seq(lrugen->min_seq[type]);
 	int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
+	int target_gen = (old_gen + 1) % MAX_NR_GENS;
 
 	/* For file type, skip the check if swappiness is anon only */
 	if (type && (swappiness == SWAPPINESS_ANON_ONLY))
@@ -3927,35 +3928,47 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	if (!type && !swappiness)
 		goto done;
 
+	VM_WARN_ON_ONCE(get_nr_gens(lruvec, type) != MAX_NR_GENS);
+	VM_WARN_ON_ONCE(lru_gen_is_active(lruvec, old_gen) !=
+			lru_gen_is_active(lruvec, target_gen));
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
+		long delta = 0;
 
 		while (!list_empty(head)) {
 			struct folio *folio = lru_to_folio(head);
+			long nr_pages = folio_nr_pages(folio);
 			int refs = folio_lru_refs(folio);
 			bool workingset = folio_test_workingset(folio);
+			bool gen_increased;
 
 			VM_WARN_ON_ONCE_FOLIO(folio_test_unevictable(folio), folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_test_active(folio), folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
-			new_gen = folio_inc_gen(lruvec, folio);
+			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-
+			if (gen_increased)
+				delta += nr_pages;
 			/* don't count the workingset being lazily promoted */
 			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
 				int tier = lru_tier_from_refs(refs, workingset);
-				int delta = folio_nr_pages(folio);
 
 				WRITE_ONCE(lrugen->protected[hist][type][tier],
-					   lrugen->protected[hist][type][tier] + delta);
+					   lrugen->protected[hist][type][tier] + nr_pages);
 			}
 
 			if (!--remaining)
-				return false;
+				break;
 		}
+		WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
+			   lrugen->nr_pages[old_gen][type][zone] - delta);
+		WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
+			   lrugen->nr_pages[target_gen][type][zone] + delta);
+		if (!remaining)
+			return false;
 	}
 done:
 	reset_ctrl_pos(lruvec, type, true);
Signature · 1 line
2.34.1
#4 lore raw

Re: [PATCH v2 2/7] mm/mglru: batch update lrugen->nr_pages in inc_min_seq()

Quoted 66 lines · On Fri, 28 Aug 2026 07:46:59 +0800 "Barry Song (Xiaomi)" <[email protected]>…

Currently, folio_inc_gen() updates lrugen->nr_pages for every folio as it advances generations. Instead, accumulate the size changes and update lrugen->nr_pages in a batch after scanning the entire oldest generation, or when the scan stops because remaining reaches zero.

Since we only move folios from the oldest generation to the second oldest generation, the active/inactive state cannot change. We can therefore skip __lru_update_size().

---
 mm/vmscan.c | 23 ++++++++++++++++++-----
 1 file changed, 18 insertions(+), 5 deletions(-)
mm/vmscan.c +11 -4
index 8f187d296b8e..07c22d51debd 100644
@@ -3918,6 +3918,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	struct lru_gen_folio *lrugen = &lruvec->lrugen;
 	int hist = lru_hist_from_seq(lrugen->min_seq[type]);
 	int new_gen, old_gen = lru_gen_from_seq(lrugen->min_seq[type]);
+	int target_gen = (old_gen + 1) % MAX_NR_GENS;
 
 	/* For file type, skip the check if swappiness is anon only */
 	if (type && (swappiness == SWAPPINESS_ANON_ONLY))
@@ -3927,35 +3928,47 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	if (!type && !swappiness)
 		goto done;
 
+	VM_WARN_ON_ONCE(get_nr_gens(lruvec, type) != MAX_NR_GENS);
+	VM_WARN_ON_ONCE(lru_gen_is_active(lruvec, old_gen) !=
+			lru_gen_is_active(lruvec, target_gen));
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
+		long delta = 0;
 
 		while (!list_empty(head)) {
 			struct folio *folio = lru_to_folio(head);
+			long nr_pages = folio_nr_pages(folio);
 			int refs = folio_lru_refs(folio);
 			bool workingset = folio_test_workingset(folio);
+			bool gen_increased;
 
 			VM_WARN_ON_ONCE_FOLIO(folio_test_unevictable(folio), folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_test_active(folio), folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
-			new_gen = folio_inc_gen(lruvec, folio);
+			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-
+			if (gen_increased)
+				delta += nr_pages;
 			/* don't count the workingset being lazily promoted */
 			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
 				int tier = lru_tier_from_refs(refs, workingset);
-				int delta = folio_nr_pages(folio);
 
 				WRITE_ONCE(lrugen->protected[hist][type][tier],
-					   lrugen->protected[hist][type][tier] + delta);
+					   lrugen->protected[hist][type][tier] + nr_pages);

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3969

Quoted 10 lines
mm/vmscan.c +5 -1
 			}
 
 			if (!--remaining)
-				return false;
+				break;
 		}
+		WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
+			   lrugen->nr_pages[old_gen][type][zone] - delta);
+		WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
+			   lrugen->nr_pages[target_gen][type][zone] + delta);

Hi Barry,

One subtle point about the remaining handling:
when remaining reaches zero, we now break rather than return so that the accumulated delta is applied before returning.

As I understand it, this is required because __folio_inc_gen() has already changed the generation of the scanned folios, while lrugen->nr_pages[] is now updated only in batch.

So the invariant is that every successful generation increment must have its corresponding delta flushed before inc_min_seq() returns.

Is this the intended accounting invariant?

Thanks,
KunWu

Quoted 9 lines
mm/vmscan.c +2 -0
+		if (!remaining)
+			return false;
 	}
 done:
 	reset_ctrl_pos(lruvec, type, true);
Signature · 1 line
2.34.1
author reviewer
#5 lore raw

Re: [PATCH v2 2/7] mm/mglru: batch update lrugen->nr_pages in inc_min_seq()

Quoted 17 lines · Kunwu Chan
Quoted 14 lines · On Fri, 28 Aug 2026 07:46:59 +0800 "Barry Song (Xiaomi)" <[email protected]>…

Currently, folio_inc_gen() updates lrugen->nr_pages for every folio as it advances generations. Instead, accumulate the size changes and update lrugen->nr_pages in a batch after scanning the entire oldest generation, or when the scan stops because remaining reaches zero.

Since we only move folios from the oldest generation to the second oldest generation, the active/inactive state cannot change. We can therefore skip __lru_update_size().

---
 mm/vmscan.c | 23 ++++++++++++++++++-----

[...]

Quoted 16 lines

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3969

Quoted 9 lines
                      if (!--remaining)
mm/vmscan.c +5 -1
-                             return false;
+                             break;
              }
+             WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
+                        lrugen->nr_pages[old_gen][type][zone] - delta);
+             WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
+                        lrugen->nr_pages[target_gen][type][zone] + delta);

Hi Barry,

One subtle point about the remaining handling:
when remaining reaches zero, we now break rather than return so that the accumulated delta is applied before returning.

Quoted 8 lines

As I understand it, this is required because __folio_inc_gen() has already changed the generation of the scanned folios, while lrugen->nr_pages[] is now updated only in batch.

So the invariant is that every successful generation increment must have its corresponding delta flushed before inc_min_seq() returns.

Is this the intended accounting invariant?

Yes, Kunwu, you're right. We can't return when remaining == 0, otherwise we will miss the chance to flush the delta.

So changing return to break is intentional. Another option would be to duplicate the code that flushes the delta and keep the return, but that would be ugly since we would have to duplicate the code below:

             WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
                       lrugen->nr_pages[old_gen][type][zone] - delta);
             WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
                        lrugen->nr_pages[target_gen][type][zone] + delta);

Thanks
Barry

Reviewed-by
#6 lore raw

Re: [PATCH v2 2/7] mm/mglru: batch update lrugen->nr_pages in inc_min_seq()

Quoted 43 lines · Barry Song
Quoted 17 lines · Kunwu Chan
Quoted 14 lines · On Fri, 28 Aug 2026 07:46:59 +0800 "Barry Song (Xiaomi)" <[email protected]>…

Currently, folio_inc_gen() updates lrugen->nr_pages for every folio as it advances generations. Instead, accumulate the size changes and update lrugen->nr_pages in a batch after scanning the entire oldest generation, or when the scan stops because remaining reaches zero.

Since we only move folios from the oldest generation to the second oldest generation, the active/inactive state cannot change. We can therefore skip __lru_update_size().

---
 mm/vmscan.c | 23 ++++++++++++++++++-----

[...]

Quoted 23 lines

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3969

Quoted 9 lines
                      if (!--remaining)
mm/vmscan.c +5 -1
-                             return false;
+                             break;
              }
+             WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
+                        lrugen->nr_pages[old_gen][type][zone] - delta);
+             WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
+                        lrugen->nr_pages[target_gen][type][zone] + delta);

Hi Barry,

One subtle point about the remaining handling:
when remaining reaches zero, we now break rather than return so that the accumulated delta is applied before returning.

As I understand it, this is required because __folio_inc_gen() has already changed the generation of the scanned folios, while lrugen->nr_pages[] is now updated only in batch.

So the invariant is that every successful generation increment must have its corresponding delta flushed before inc_min_seq() returns.

Quoted 9 lines
Quoted 1 line

Is this the intended accounting invariant?

Yes, Kunwu, you're right. We can't return when remaining == 0, otherwise we will miss the chance to flush the delta.

So changing return to break is intentional. Another option would be to duplicate the code that flushes the delta and keep the return, but that would be ugly since we would have to duplicate the code below:

Hi Barry,

Thanks for confirming the invariant and the control flow reasoning. The logic makes complete sense to me now.

Thanks,
KunWu

Quoted 7 lines
             WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
                       lrugen->nr_pages[old_gen][type][zone] - delta);
             WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
                        lrugen->nr_pages[target_gen][type][zone] + delta);

Thanks
Barry

Reviewed-by on v2 2/7
#7 lore raw

Re: [PATCH v2 2/7] mm/mglru: batch update lrugen->nr_pages in inc_min_seq()

In reply to Barry Song (Xiaomi)
Quoted 3 lines

Instead, accumulate the size changes and update lrugen->nr_pages in a batch after scanning the entire oldest generation, or when the scan stops because remaining reaches zero.

I have reviewed this patch and the related discussion. The accounting logic and the break-to-flush control flow make sense to me.

author reviewer
#8 lore raw

PATCH v2 3/7 mm/mglru: enhance cold/hot inversion handling in inc_min_seq()

In reply to Barry Song (Xiaomi)

During aging, a folio's generation may already have been updated by folio_update_gen(), even though it has not yet been moved to the corresponding generation list. Such folios are hotter than those already in that generation.

It makes sense for inc_min_seq() to increment the generation of folios that were never promoted during aging and move them to the tail of the new oldest generation. However, folios that were already promoted should instead be moved to the head of their updated generation, just as sort_folio() does in scan_folios().

Otherwise, promoted folios could end up behind folios that were never promoted, effectively inverting their hot/cold ordering.

---
 mm/vmscan.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)
mm/vmscan.c +5 -2
index 07c22d51debd..b10d1703d907 100644
@@ -3949,9 +3949,12 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
 			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
-			list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-			if (gen_increased)
+			if (gen_increased) {
 				delta += nr_pages;
+				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+			} else {
+				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+			}
 			/* don't count the workingset being lazily promoted */
 			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
 				int tier = lru_tier_from_refs(refs, workingset);
Signature · 1 line
2.34.1
Reviewed-by on v2 3/7
#9 lore raw

Re: [PATCH v2 3/7] mm/mglru: enhance cold/hot inversion handling in inc_min_seq()

Quoted 26 lines · Barry Song (Xiaomi)

During aging, a folio's generation may already have been updated by folio_update_gen(), even though it has not yet been moved to the corresponding generation list. Such folios are hotter than those already in that generation.

It makes sense for inc_min_seq() to increment the generation of folios that were never promoted during aging and move them to the tail of the new oldest generation. However, folios that were already promoted should instead be moved to the head of their updated generation, just as sort_folio() does in scan_folios().

Otherwise, promoted folios could end up behind folios that were never promoted, effectively inverting their hot/cold ordering.

---
  mm/vmscan.c | 7 +++++--
  1 file changed, 5 insertions(+), 2 deletions(-)
mm/vmscan.c +0 -0
index 07c22d51debd..b10d1703d907 100644

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3954

Quoted 8 lines
mm/vmscan.c +2 -2
@@ -3949,9 +3949,12 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
  			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
  			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
-			list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-			if (gen_increased)
+			if (gen_increased) {
  				delta += nr_pages;
+				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);

Nit.
This is a bit confusing to me.

When I read the code, I thought this implementation contradicts what the commit message says, which means I thought that we move the folios that have been promoted to the tail.

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3955

Quoted 1 line
mm/vmscan.c +1 -0
+			} else {

Maybe adding a comment would make it clearer, like:

If gen_increased == false, it means the folio has been promoted, so move it to the head.

Quoted 8 lines
mm/vmscan.c +2 -0
+				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+			}
 
 
 
  			/* don't count the workingset being lazily promoted */
  			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
  				int tier = lru_tier_from_refs(refs, workingset);

Overall, looks good to me.

Signature · 2 lines
Best regards
Ridong
author reviewer
#10 lore raw

Re: [PATCH v2 3/7] mm/mglru: enhance cold/hot inversion handling in inc_min_seq()

On Sun, Aug 30, 2026 at 10:44 AM Ridong Chen <[email protected]> wrote:
[...]

Quoted 19 lines

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3954

Quoted 11 lines
mm/vmscan.c +2 -2
@@ -3949,9 +3949,12 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
                      VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
                      new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
-                     list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
-                     if (gen_increased)
+                     if (gen_increased) {
                              delta += nr_pages;
+                             list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);

Nit.
This is a bit confusing to me.

When I read the code, I thought this implementation contradicts what the commit message says, which means I thought that we move the folios that have been promoted to the tail.

Quoted 6 lines

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3955

Quoted 1 line
mm/vmscan.c +1 -0
+                     } else {

Maybe adding a comment would make it clearer, like:

If gen_increased == false, it means the folio has been promoted, so move it to the head.

Good idea. will do it in v3.

Quoted 6 lines
Quoted 3 lines
mm/vmscan.c +2 -0
+                             list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+                     }
Quoted 7 lines

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3960

Quoted 3 lines
mm/vmscan.c +0 -0
                      /* don't count the workingset being lazily promoted */
                      if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
                              int tier = lru_tier_from_refs(refs, workingset);

Overall, looks good to me.

thanks!

Best Regards
Barry

author reviewer
#12 lore raw

PATCH v2 4/7 mm/mglru: exclude folios promoted by aging from protected in inc_min_seq()

In reply to Barry Song (Xiaomi)

Some folios may have been promoted during aging, so don't count them as protected, similar to sort_folio().

---
 mm/vmscan.c | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)
mm/vmscan.c +8 -8
index b10d1703d907..2b33d3f44682 100644
@@ -3952,17 +3952,17 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			if (gen_increased) {
 				delta += nr_pages;
 				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+
+				/* don't count the workingset being lazily promoted */
+				if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
+					int tier = lru_tier_from_refs(refs, workingset);
+
+					WRITE_ONCE(lrugen->protected[hist][type][tier],
+						   lrugen->protected[hist][type][tier] + nr_pages);
+				}
 			} else {
 				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
 			}
-			/* don't count the workingset being lazily promoted */
-			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
-				int tier = lru_tier_from_refs(refs, workingset);
-
-				WRITE_ONCE(lrugen->protected[hist][type][tier],
-					   lrugen->protected[hist][type][tier] + nr_pages);
-			}
-
 			if (!--remaining)
 				break;
 		}
Signature · 1 line
2.34.1
#13 lore raw

Re: [PATCH v2 4/7] mm/mglru: exclude folios promoted by aging from protected in inc_min_seq()

Quoted 30 lines · Barry Song (Xiaomi)

Some folios may have been promoted during aging, so don't count them as protected, similar to sort_folio().

---
  mm/vmscan.c | 16 ++++++++--------
  1 file changed, 8 insertions(+), 8 deletions(-)
mm/vmscan.c +8 -1
index b10d1703d907..2b33d3f44682 100644
@@ -3952,17 +3952,17 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
  			if (gen_increased) {
  				delta += nr_pages;
  				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+
+				/* don't count the workingset being lazily promoted */
+				if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
+					int tier = lru_tier_from_refs(refs, workingset);
+
+					WRITE_ONCE(lrugen->protected[hist][type][tier],
+						   lrugen->protected[hist][type][tier] + nr_pages);
+				}
  			} else {
  				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
  			}
-			/* don't count the workingset being lazily promoted */

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3968

Quoted 10 lines
mm/vmscan.c +0 -7
-			if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
-				int tier = lru_tier_from_refs(refs, workingset);
-
-				WRITE_ONCE(lrugen->protected[hist][type][tier],
-					   lrugen->protected[hist][type][tier] + nr_pages);
-			}
-
  			if (!--remaining)
  				break;
  		}
Signature · 2 lines
Best regards
Ridong
Reviewed-by on v2 4/7
#14 lore raw

Re: [PATCH v2 4/7] mm/mglru: exclude folios promoted by aging from protected in inc_min_seq()

In reply to Barry Song (Xiaomi)
Quoted 2 lines

Some folios may have been promoted during aging, so don't count them as protected, similar to sort_folio().

This makes sense to me and matches sort_folio().

author reviewer
#15 lore raw

PATCH v2 5/7 mm/mglru: make LRU folio prefetch helper an inline function

In reply to Barry Song (Xiaomi)

prefetchw_prev_lru_folio() is currently implemented as a macro with a potentially unused argument. This makes the helper harder to read and can also trigger checkpatch warnings about unused macro arguments.

Make it a static inline function and remove the unnecessary _field argument. The helper always prefetches the previous folio's flags, so there is no need to make the field configurable.

---
 mm/vmscan.c | 26 +++++++++++++++-----------
 1 file changed, 15 insertions(+), 11 deletions(-)
mm/vmscan.c +15 -11
index 2b33d3f44682..81f95a968447 100644
@@ -182,17 +182,21 @@ struct scan_control {
 };
 
 #ifdef ARCH_HAS_PREFETCHW
-#define prefetchw_prev_lru_folio(_folio, _base, _field)			\
-	do {								\
-		if ((_folio)->lru.prev != _base) {			\
-			struct folio *prev;				\
-									\
-			prev = lru_to_folio(&(_folio->lru));		\
-			prefetchw(&prev->_field);			\
-		}							\
-	} while (0)
+static inline void prefetchw_prev_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+	if (folio->lru.prev != base) {
+		struct folio *prev;
+
+		prev = lru_to_folio(&folio->lru);
+		prefetchw(&prev->flags);
+	}
+}
 #else
-#define prefetchw_prev_lru_folio(_folio, _base, _field) do { } while (0)
+static inline void prefetchw_prev_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+}
 #endif
 
 /*
@@ -1695,7 +1699,7 @@ static unsigned long isolate_lru_folios(unsigned long nr_to_scan,
 		struct folio *folio;
 
 		folio = lru_to_folio(src);
-		prefetchw_prev_lru_folio(folio, src, flags);
+		prefetchw_prev_lru_folio(folio, src);
 
 		nr_pages = folio_nr_pages(folio);
 		total_scan += nr_pages;
Signature · 1 line
2.34.1
Reviewed-by on v2 5/7
#16 lore raw

Re: [PATCH v2 5/7] mm/mglru: make LRU folio prefetch helper an inline function

Quoted 60 lines · On Fri, 28 Aug 2026 07:47:02 +0800 "Barry Song (Xiaomi)" <[email protected]>…

prefetchw_prev_lru_folio() is currently implemented as a macro with a potentially unused argument. This makes the helper harder to read and can also trigger checkpatch warnings about unused macro arguments.

Make it a static inline function and remove the unnecessary _field argument. The helper always prefetches the previous folio's flags, so there is no need to make the field configurable.

---
 mm/vmscan.c | 26 +++++++++++++++-----------
 1 file changed, 15 insertions(+), 11 deletions(-)
mm/vmscan.c +15 -12
index 2b33d3f44682..81f95a968447 100644
@@ -182,17 +182,21 @@ struct scan_control {
 };
 
 #ifdef ARCH_HAS_PREFETCHW
-#define prefetchw_prev_lru_folio(_folio, _base, _field)			\
-	do {								\
-		if ((_folio)->lru.prev != _base) {			\
-			struct folio *prev;				\
-									\
-			prev = lru_to_folio(&(_folio->lru));		\
-			prefetchw(&prev->_field);			\
-		}							\
-	} while (0)
+static inline void prefetchw_prev_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+	if (folio->lru.prev != base) {
+		struct folio *prev;
+
+		prev = lru_to_folio(&folio->lru);
+		prefetchw(&prev->flags);
+	}
+}
 #else
-#define prefetchw_prev_lru_folio(_folio, _base, _field) do { } while (0)
+static inline void prefetchw_prev_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+}
 #endif
 
 /*
@@ -1695,7 +1699,7 @@ static unsigned long isolate_lru_folios(unsigned long nr_to_scan,
 		struct folio *folio;
 
 		folio = lru_to_folio(src);
-		prefetchw_prev_lru_folio(folio, src, flags);
+		prefetchw_prev_lru_folio(folio, src);
 
 		nr_pages = folio_nr_pages(folio);
 		total_scan += nr_pages;
--
Quoted 1 line

2.34.1

This preserves the prefetch behavior while making the helper interface clearer. Looks good to me.

author reviewer
#17 lore raw

PATCH v2 6/7 mm/mglru: move folios from oldest gen to second-oldest gen from head to tail

In reply to Barry Song (Xiaomi)

For reclamation, it makes sense to reclaim folios from tail to head, as folios near the head are relatively hot. However, when moving folios from the oldest generation to the second-oldest generation, using the tail-to-head order would effectively cause a cold/hot inversion.

---
 mm/vmscan.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)
mm/vmscan.c +21 -2
index 81f95a968447..17524e96fe64 100644
@@ -192,11 +192,27 @@ static inline void prefetchw_prev_lru_folio(struct folio *folio,
 		prefetchw(&prev->flags);
 	}
 }
+
+static inline void prefetchw_next_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+	if (folio->lru.next != base) {
+		struct folio *next;
+
+		next = list_entry(folio->lru.next, struct folio, lru);
+		prefetchw(&next->flags);
+	}
+}
 #else
 static inline void prefetchw_prev_lru_folio(struct folio *folio,
 		struct list_head *base)
 {
 }
+
+static inline void prefetchw_next_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+}
 #endif
 
 /*
@@ -3938,10 +3954,11 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
+		struct list_head *pos = head->next;
 		long delta = 0;
 
-		while (!list_empty(head)) {
-			struct folio *folio = lru_to_folio(head);
+		while (pos != head) {
+			struct folio *folio = list_entry(pos, struct folio, lru);
 			long nr_pages = folio_nr_pages(folio);
 			int refs = folio_lru_refs(folio);
 			bool workingset = folio_test_workingset(folio);
@@ -3952,6 +3969,8 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
+			prefetchw_next_lru_folio(folio, head);
+			pos = pos->next;
 			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			if (gen_increased) {
 				delta += nr_pages;
Signature · 1 line
2.34.1
Reviewed-by on v2 6/7
#18 lore raw

Re: [PATCH v2 6/7] mm/mglru: move folios from oldest gen to second-oldest gen from head to tail

Quoted 60 lines · On Fri, 28 Aug 2026 07:47:03 +0800 "Barry Song (Xiaomi)" <[email protected]>…

For reclamation, it makes sense to reclaim folios from tail to head, as folios near the head are relatively hot. However, when moving folios from the oldest generation to the second-oldest generation, using the tail-to-head order would effectively cause a cold/hot inversion.

---
 mm/vmscan.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)
mm/vmscan.c +19 -2
index 81f95a968447..17524e96fe64 100644
@@ -192,11 +192,27 @@ static inline void prefetchw_prev_lru_folio(struct folio *folio,
 		prefetchw(&prev->flags);
 	}
 }
+
+static inline void prefetchw_next_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+	if (folio->lru.next != base) {
+		struct folio *next;
+
+		next = list_entry(folio->lru.next, struct folio, lru);
+		prefetchw(&next->flags);
+	}
+}
 #else
 static inline void prefetchw_prev_lru_folio(struct folio *folio,
 		struct list_head *base)
 {
 }
+
+static inline void prefetchw_next_lru_folio(struct folio *folio,
+		struct list_head *base)
+{
+}
 #endif
 
 /*
@@ -3938,10 +3954,11 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
+		struct list_head *pos = head->next;
 		long delta = 0;
 
-		while (!list_empty(head)) {
-			struct folio *folio = lru_to_folio(head);
+		while (pos != head) {
+			struct folio *folio = list_entry(pos, struct folio, lru);
 			long nr_pages = folio_nr_pages(folio);
 			int refs = folio_lru_refs(folio);
 			bool workingset = folio_test_workingset(folio);

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 3973

Quoted 5 lines
mm/vmscan.c +2 -0
@@ -3952,6 +3969,8 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			VM_WARN_ON_ONCE_FOLIO(folio_is_file_lru(folio) != type, folio);
 			VM_WARN_ON_ONCE_FOLIO(folio_zonenum(folio) != zone, folio);
 
+			prefetchw_next_lru_folio(folio, head);
+			pos = pos->next;

Saving pos->next before moving the current folio keeps the forward iterator valid. Across partial-batch restarts, processing from head to tail and appending to the target tail preserves the relative hot-to-cold order of these folios.

Quoted 5 lines
mm/vmscan.c +0 -1
 			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			if (gen_increased) {
 				delta += nr_pages;
--

2.34.1

Looks good to me.

author reviewer
#19 lore raw

PATCH v2 7/7 mm/mglru: batch move folios to the second-oldest gen's LRU

In reply to Barry Song (Xiaomi)

Detect folios that need to move from the oldest generation to the second-oldest generation, and batch-move them together. This can significantly reduce the sys time of inc_min_seq(), especially when the other type is significantly behind the preferred type.

Assisted-by gemini:gemini-3.6-flash
---
 mm/vmscan.c | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)
mm/vmscan.c +19 -1
index 17524e96fe64..2b6f3f05ce60 100644
@@ -3931,6 +3931,19 @@ static void clear_mm_walk(void)
 		kfree(walk);
 }
 
+static inline void flush_lru_batch(struct list_head *head, struct list_head **batch_end,
+				   struct list_head *dst)
+{
+	LIST_HEAD(movable);
+
+	if (!*batch_end)
+		return;
+
+	list_cut_position(&movable, head, *batch_end);
+	list_splice_tail_init(&movable, dst);
+	*batch_end = NULL;
+}
+
 static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 {
 	int zone;
@@ -3953,8 +3966,10 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			lru_gen_is_active(lruvec, target_gen));
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
+		struct list_head *target_list = &lrugen->folios[target_gen][type][zone];
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
 		struct list_head *pos = head->next;
+		struct list_head *batch_end = NULL;
 		long delta = 0;
 
 		while (pos != head) {
@@ -3974,7 +3989,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			if (gen_increased) {
 				delta += nr_pages;
-				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+				batch_end = &folio->lru;
 
 				/* don't count the workingset being lazily promoted */
 				if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {
@@ -3984,11 +3999,14 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 						   lrugen->protected[hist][type][tier] + nr_pages);
 				}
 			} else {
+				flush_lru_batch(head, &batch_end, target_list);
 				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
 			}
 			if (!--remaining)
 				break;
 		}
+		flush_lru_batch(head, &batch_end, target_list);
+
 		WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
 			   lrugen->nr_pages[old_gen][type][zone] - delta);
 		WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
Signature · 1 line
2.34.1
author reviewer
#20 lore raw

Re: [PATCH v2 7/7] mm/mglru: batch move folios to the second-oldest gen's LRU

Quoted 23 lines · Barry Song (Xiaomi)

Detect folios that need to move from the oldest generation to the second-oldest generation, and batch-move them together. This can significantly reduce the sys time of inc_min_seq(), especially when the other type is significantly behind the preferred type.

Assisted-by gemini:gemini-3.6-flash
---
 mm/vmscan.c | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)
mm/vmscan.c +0 -0
index 17524e96fe64..2b6f3f05ce60 100644
@@ -3931,6 +3931,19 @@ static void clear_mm_walk(void)
                kfree(walk);
 }
 

[...]

Quoted 11 lines
                while (pos != head) {
mm/vmscan.c +1 -1
@@ -3974,7 +3989,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
                        new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
                        if (gen_increased) {
                                delta += nr_pages;
-                               list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+                               batch_end = &folio->lru;
 
                                /* don't count the workingset being lazily promoted */
                                if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 4003

Quoted 5 lines
mm/vmscan.c +1 -0
@@ -3984,11 +3999,14 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
                                                   lrugen->protected[hist][type][tier] + nr_pages);
                                }
                        } else {
+                               flush_lru_batch(head, &batch_end, target_list);
                                list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);

Sashiko says

"Does this change from list_move_tail() to list_move() unintentionally reverse the relative LRU ordering of lazily promoted folios?
In the baseline code, list_move_tail() was used for all folios, preserving their relative order when moving them to the new generation's list. Because the loop in inc_min_seq() processes folios sequentially from head to tail, inserting them one by one at the head of the new list using list_move() will reverse their relative order (for example, folios A, B, and C will end up as C, B, A in the new list).
Could this subtly distort eviction fairness and page replacement optimality during MGLRU reclaim under memory pressure?"

I feel this comment is not particularly useful. We are focusing on resolving two specific issues:

1. Make sure promoted folios are always placed ahead of non-promoted folios. Right now, mainline can place promoted folios after non-promoted folios.

2. Preserve the existing order of non-promoted folios instead of reversing them, as mainline currently does.

Sashiko seems to be concerned about the ordering among promoted folios. I don't think that ordering is particularly meaningful, because we don't know which folio was actually accessed more recently. Accessed bits are accumulated over time and handled together. The only way to establish a meaningful order among promoted folios would be to make every access trigger a page fault, which is obviously not feasible.

Best Regards
Barry

Reviewed-by on v2 7/7
#21 lore raw

Re: [PATCH v2 7/7] mm/mglru: batch move folios to the second-oldest gen's LRU

In reply to Barry Song (Xiaomi)
Quoted 24 lines · On Fri, 28 Aug 2026 07:47:04 +0800 "Barry Song (Xiaomi)" <[email protected]>…

Detect folios that need to move from the oldest generation to the second-oldest generation, and batch-move them together. This can significantly reduce the sys time of inc_min_seq(), especially when the other type is significantly behind the preferred type.

Assisted-by gemini:gemini-3.6-flash
---
 mm/vmscan.c | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)
mm/vmscan.c +2 -0
index 17524e96fe64..2b6f3f05ce60 100644
@@ -3931,6 +3931,19 @@ static void clear_mm_walk(void)
 		kfree(walk);
 }
 
+static inline void flush_lru_batch(struct list_head *head, struct list_head **batch_end,
+				   struct list_head *dst)

mm/vmscan.c static void clear_mm_walk(void) · line 3945

Quoted 10 lines
mm/vmscan.c +10 -0
+{
+	LIST_HEAD(movable);
+
+	if (!*batch_end)
+		return;
+
+	list_cut_position(&movable, head, *batch_end);
+	list_splice_tail_init(&movable, dst);
+	*batch_end = NULL;
+}

This is safe because batch_end always marks a contiguous prefix of head:
the loop walks from head to tail and flushes the prefix before moving any folio that was already promoted by aging.

Quoted 25 lines
mm/vmscan.c +4 -1
+
 static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 {
 	int zone;
@@ -3953,8 +3966,10 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			lru_gen_is_active(lruvec, target_gen));
 	/* prevent cold/hot inversion if the type is evictable */
 	for (zone = 0; zone < MAX_NR_ZONES; zone++) {
+		struct list_head *target_list = &lrugen->folios[target_gen][type][zone];
 		struct list_head *head = &lrugen->folios[old_gen][type][zone];
 		struct list_head *pos = head->next;
+		struct list_head *batch_end = NULL;
 		long delta = 0;
 
 		while (pos != head) {
@@ -3974,7 +3989,7 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 			new_gen = __folio_inc_gen(folio, old_gen, &gen_increased);
 			if (gen_increased) {
 				delta += nr_pages;
-				list_move_tail(&folio->lru, &lrugen->folios[new_gen][type][zone]);
+				batch_end = &folio->lru;
 
 				/* don't count the workingset being lazily promoted */
 				if (refs + workingset != BIT(LRU_REFS_WIDTH) + 1) {

mm/vmscan.c static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness) · line 4008

Quoted 10 lines
mm/vmscan.c +2 -0
@@ -3984,11 +3999,14 @@ static bool inc_min_seq(struct lruvec *lruvec, int type, int swappiness)
 						   lrugen->protected[hist][type][tier] + nr_pages);
 				}
 			} else {
+				flush_lru_batch(head, &batch_end, target_list);
 				list_move(&folio->lru, &lrugen->folios[new_gen][type][zone]);
 			}
 			if (!--remaining)
 				break;
 		}
+		flush_lru_batch(head, &batch_end, target_list);

The flush in the else keeps every already-promoted folio ahead of the normal old-to-target batch. This final flush also covers the batch-limit exit before the counters are updated and the lock can be dropped.

Quoted 6 lines
mm/vmscan.c +1 -1
+
 		WRITE_ONCE(lrugen->nr_pages[old_gen][type][zone],
 			   lrugen->nr_pages[old_gen][type][zone] - delta);
 		WRITE_ONCE(lrugen->nr_pages[target_gen][type][zone],
--

2.34.1

The moved list entries and delta therefore cover the same folios, while the relative order of each normal-folio run is preserved. Looks good to me.