* [PATCH v2 0/3] mm/filemap: read ahead for mmap readers that move backwards
@ 2026-11-09 10:34 Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 1/3] mm/filemap: move the mmap hit check of filemap_map_pages() into helpers Qiliang Yuan
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Qiliang Yuan @ 2026-11-09 10:34 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jan Kara, Andrew Morton, Vishal Moola, fujunjie
Cc: Roman Gushchin, Usama Arif, linux-fsdevel, linux-mm,
linux-kernel, Qiliang Yuan, stable
A reader that walks a file mapping towards lower offsets gets almost no
readahead from mmap faults when the mapping asks for it. glibc's
memmove() and memcpy() copy backwards when the destination lies above
the source, so this happens to loaders that copy large files out of a
mapping, such as LLM inference engines copying model weights out of a
safetensors mapping. Both MADV_HUGEPAGE and MADV_SEQUENTIAL read ahead
only towards higher offsets, so each fault of such a reader reads just
the part it is missing.
MADV_SEQUENTIAL was slow for this before 7.2 as well. MADV_HUGEPAGE
became much slower in 7.2: since commit 0b9c0aeba938 ("mm/filemap:
count only the faulting address as a mmap hit"), the PTEs mapped around
a fault no longer count as hits and PMD mappings never did, so a
backward reader's mmap_miss passes MMAP_LOTSAMISS after about a hundred
folios and readahead stops. Each page after that is read on its own.
Patch 1 moves the mmap hit check of filemap_map_pages() into helpers.
Patch 2 fixes the regression: on a descending VM_HUGEPAGE fault it also
reads the folio below, and it counts a PMD mapping of the faulting
address as a hit, as a PTE mapping is. Patch 3 reads the window that
ends at the fault on a descending VM_SEQ_READ fault.
Reading one byte from each page of a 2 GiB file, last page first, with
a cold page cache, read_ahead_kb 8192, in a 32 vCPU VM:
v7.4-rc2 patched
MADV_HUGEPAGE 42.23 s 0.58 s
MADV_SEQUENTIAL 52.42 s 0.96 s
The same walk took 0.53 s with MADV_HUGEPAGE just before
0b9c0aeba938.
Forward walks take the same time, and a fault on an uncached 2 MiB hole
between two cached ranges of a MADV_HUGEPAGE mapping is still mapped
with a PMD, 300 out of 300 times with and without the series. Megabytes
read and major faults for other access patterns over a 16 GiB file:
v7.4-rc2 patched
1% random, MADV_NORMAL 1002 MB, 39617 1013 MB, 39617
1% random, MADV_HUGEPAGE 1584 MB, 38760 1587 MB, 38756
1% stride 64 pages 10503 MB, 2 10496 MB, 2
5% random in 2 GiB memcg,
MADV_HUGEPAGE 2205 MB, 187416 2225 MB, 187402
The walk above, run as "revwalk FILE" for MADV_HUGEPAGE and "revwalk FILE
seq" for MADV_SEQUENTIAL, is below. I can turn it into a selftest if that
is wanted.
/* revwalk FILE [seq]: read each page of FILE, last page first */
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char **argv)
{
long ps = sysconf(_SC_PAGESIZE);
int fd = open(argv[1], O_RDONLY);
struct rusage ru;
struct stat st;
size_t i;
char *p;
fstat(fd, &st);
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
p = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
madvise(p, st.st_size, argc > 2 && !strcmp(argv[2], "seq") ?
MADV_SEQUENTIAL : MADV_HUGEPAGE);
for (i = st.st_size / ps; i > 0; i--)
(void)*(volatile char *)(p + (i - 1) * ps);
getrusage(RUSAGE_SELF, &ru);
printf("major faults: %ld\n", ru.ru_majflt);
return 0;
}
Signed-off-by: Qiliang Yuan <qiliang.yuan@linux.dev>
---
V1 -> V2:
- Split the mmap hit helpers out of the fix into patch 1
- Patch 2: take a fault as descending only when the folio below it is
not cached either, so a hole between two cached ranges is still read
as one large folio (Sashiko review)
- Rebase onto v7.4-rc2 and remeasure
- Send from my linux.dev address
v1: https://lore.kernel.org/r/20261022-bug-mm-mmap-readahead-descending-v1-0-6d08b34862f2@gmail.com
---
Qiliang Yuan (3):
mm/filemap: move the mmap hit check of filemap_map_pages() into helpers
mm/filemap: read ahead downwards on descending VM_HUGEPAGE faults
mm/filemap: extend VM_SEQ_READ readahead below descending faults
mm/filemap.c | 119 ++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 89 insertions(+), 30 deletions(-)
---
base-commit: 7475ea8b58616b33c04afb976cf0fbaad9dd63fb
change-id: 20261022-bug-mm-mmap-readahead-descending-9cab86703bbb
Best regards,
--
Qiliang Yuan <qiliang.yuan@linux.dev>
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2 1/3] mm/filemap: move the mmap hit check of filemap_map_pages() into helpers
2026-11-09 10:34 [PATCH v2 0/3] mm/filemap: read ahead for mmap readers that move backwards Qiliang Yuan
@ 2026-11-09 10:34 ` Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 2/3] mm/filemap: read ahead downwards on descending VM_HUGEPAGE faults Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 3/3] mm/filemap: extend VM_SEQ_READ readahead below descending faults Qiliang Yuan
2 siblings, 0 replies; 4+ messages in thread
From: Qiliang Yuan @ 2026-11-09 10:34 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jan Kara, Andrew Morton, Vishal Moola, fujunjie
Cc: Roman Gushchin, Usama Arif, linux-fsdevel, linux-mm,
linux-kernel, Qiliang Yuan
filemap_map_pages() lowers mmap_miss for a folio it maps with PTEs at
the faulting address, unless the fault is a retry, the folio was
recently evicted, or the mapping skips the matching mmap_miss increment
in do_sync_mmap_readahead().
The PMD path of filemap_map_pages() returns before that loop, and the
next patch needs the same check there.
Move the check into filemap_mmap_hit() and the decrement into
filemap_count_mmap_hit().
Signed-off-by: Qiliang Yuan <qiliang.yuan@linux.dev>
---
mm/filemap.c | 57 +++++++++++++++++++++++++++++++++++----------------------
1 file changed, 35 insertions(+), 22 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index b04be64a5069f..c38f0e4cf6123 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -3914,6 +3914,39 @@ static vm_fault_t filemap_map_order0_folio(struct vm_fault *vmf,
return ret;
}
+/*
+ * Whether mapping @folio at the faulting address counts as an mmap hit,
+ * which lowers mmap_miss in do_sync_mmap_readahead().
+ */
+static bool filemap_mmap_hit(struct vm_fault *vmf, struct folio *folio)
+{
+ /* Skip the retry of a fault, which maps the folio its miss read */
+ if (vmf->flags & FAULT_FLAG_TRIED)
+ return false;
+ /*
+ * If there are too many folios that are recently evicted in a file,
+ * they will probably continue to be evicted. In such situation,
+ * read-ahead is only a waste of IO. Don't decrease mmap_miss in this
+ * scenario to make sure we can stop read-ahead.
+ */
+ if (folio_test_workingset(folio))
+ return false;
+ /*
+ * VM_SEQ_READ and VM_EXEC mappings skip the mmap_miss increment in
+ * do_sync_mmap_readahead(), so skip the decrement as well to keep
+ * the counter symmetric.
+ */
+ return !(vmf->vma->vm_flags & (VM_SEQ_READ | VM_EXEC));
+}
+
+static void filemap_count_mmap_hit(struct file *file)
+{
+ unsigned short mmap_miss = READ_ONCE(file->f_ra.mmap_miss);
+
+ if (mmap_miss)
+ WRITE_ONCE(file->f_ra.mmap_miss, mmap_miss - 1);
+}
+
vm_fault_t filemap_map_pages(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff)
{
@@ -3986,28 +4019,8 @@ vm_fault_t filemap_map_pages(struct vm_fault *vmf,
}
ret |= map_ret;
- /*
- * If there are too many folios that are recently evicted
- * in a file, they will probably continue to be evicted.
- * In such situation, read-ahead is only a waste of IO.
- * Don't decrease mmap_miss in this scenario to make sure
- * we can stop read-ahead.
- *
- * VM_SEQ_READ and VM_EXEC mappings skip the mmap_miss
- * increment in do_sync_mmap_readahead(), so skip the
- * decrement here as well to keep the counter symmetric.
- */
- if ((map_ret & VM_FAULT_NOPAGE) &&
- !(vmf->flags & FAULT_FLAG_TRIED) &&
- !folio_test_workingset(folio) &&
- !(vma->vm_flags & (VM_SEQ_READ | VM_EXEC))) {
- unsigned short mmap_miss;
-
- mmap_miss = READ_ONCE(file->f_ra.mmap_miss);
- if (mmap_miss)
- WRITE_ONCE(file->f_ra.mmap_miss,
- mmap_miss - 1);
- }
+ if ((map_ret & VM_FAULT_NOPAGE) && filemap_mmap_hit(vmf, folio))
+ filemap_count_mmap_hit(file);
folio_unlock(folio);
} while ((folio = next_uptodate_folio(&xas, mapping, end_pgoff)) != NULL);
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2 2/3] mm/filemap: read ahead downwards on descending VM_HUGEPAGE faults
2026-11-09 10:34 [PATCH v2 0/3] mm/filemap: read ahead for mmap readers that move backwards Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 1/3] mm/filemap: move the mmap hit check of filemap_map_pages() into helpers Qiliang Yuan
@ 2026-11-09 10:34 ` Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 3/3] mm/filemap: extend VM_SEQ_READ readahead below descending faults Qiliang Yuan
2 siblings, 0 replies; 4+ messages in thread
From: Qiliang Yuan @ 2026-11-09 10:34 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jan Kara, Andrew Morton, Vishal Moola, fujunjie
Cc: Roman Gushchin, Usama Arif, linux-fsdevel, linux-mm,
linux-kernel, Qiliang Yuan, stable
On a VM_HUGEPAGE file mapping, a synchronous fault reads the PMD-sized
folio that holds the faulting address and the one above it, and maps
the faulting folio with a PMD. do_sync_mmap_readahead() counts each such
fault in mmap_miss and stops reading ahead once it passes
MMAP_LOTSAMISS, while filemap_map_pages() lowers the count for faults
served from the page cache, but only when it maps them with PTEs.
A reader that moves towards lower offsets, such as memmove() copying
backwards out of a mapping, never finds its next folio cached: the
folio above is the one it just read. Every folio is a miss and none is a
hit, so after about a hundred folios mmap_miss passes the limit and
every page after that is read on its own. Since the PTEs mapped around
a fault no longer count as hits, nothing lowers the count for it.
When the folio above the faulting one is cached and the one below is
not, read the folio below instead, without a readahead mark, and count a
PMD mapping of the faulting address as a hit the way a PTE mapping is.
Such a reader then misses on every other folio and hits on the rest.
Reading one byte from each page of a 2 GiB file, last page first, in a
MADV_HUGEPAGE mapping with a cold page cache, read_ahead_kb 8192:
before after
time 42.83 s 0.57 s
major faults 473189 514
Fixes: 0b9c0aeba938 ("mm/filemap: count only the faulting address as a mmap hit")
Cc: stable@vger.kernel.org # 7.2.x: needs the previous patch
Signed-off-by: Qiliang Yuan <qiliang.yuan@linux.dev>
---
mm/filemap.c | 48 +++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 41 insertions(+), 7 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index c38f0e4cf6123..e445279cc561f 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -3336,6 +3336,27 @@ static int lock_folio_maybe_drop_mmap(struct vm_fault *vmf, struct folio *folio,
return 1;
}
+/*
+ * Whether a miss at @index, right below @nr cached pages and right above
+ * @nr uncached ones, comes from a stream that runs towards lower offsets.
+ */
+static bool filemap_descending(struct address_space *mapping, pgoff_t index,
+ unsigned long nr)
+{
+ void *entry;
+
+ if (index < nr)
+ return false;
+ rcu_read_lock();
+ entry = xa_load(&mapping->i_pages, index + nr);
+ rcu_read_unlock();
+ if (!entry || xa_is_value(entry))
+ return false;
+ return !filemap_range_has_page(mapping,
+ (loff_t)(index - nr) << PAGE_SHIFT,
+ ((loff_t)index << PAGE_SHIFT) - 1);
+}
+
/*
* Synchronous readahead happens when we don't even find a page in the page
* cache at all. We don't want to perform IO under the mmap sem, so if we have
@@ -3409,13 +3430,20 @@ static struct file *do_sync_mmap_readahead(struct vm_fault *vmf)
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
ractl._index &= ~(folio_nr_pages - 1);
ra->size = folio_nr_pages;
+ ra->async_size = folio_nr_pages;
/*
* Fetch two folios so we get the chance to actually
- * readahead, unless we've been told not to.
+ * readahead, unless we've been told not to. A stream that
+ * runs towards lower offsets gets the folio below.
*/
- if (!(vm_flags & VM_RAND_READ))
+ if (!(vm_flags & VM_RAND_READ)) {
ra->size *= 2;
- ra->async_size = folio_nr_pages;
+ if (filemap_descending(mapping, ractl._index,
+ folio_nr_pages)) {
+ ractl._index -= folio_nr_pages;
+ ra->async_size = 0;
+ }
+ }
ra->order = thp_order;
page_cache_ra_order(&ractl, ra);
return fpin;
@@ -3981,10 +4009,16 @@ vm_fault_t filemap_map_pages(struct vm_fault *vmf,
* Make an exception for shmem/tmpfs that for long time
* intentionally mapped with PMDs across i_size.
*/
- if ((file_end >= folio_next_index(folio) || shmem_mapping(mapping)) &&
- filemap_map_pmd(vmf, folio, start_pgoff)) {
- ret = VM_FAULT_NOPAGE;
- goto out;
+ if (file_end >= folio_next_index(folio) || shmem_mapping(mapping)) {
+ /* Check before filemap_map_pmd(), which may drop the last ref */
+ bool hit = filemap_mmap_hit(vmf, folio);
+
+ if (filemap_map_pmd(vmf, folio, start_pgoff)) {
+ if (hit)
+ filemap_count_mmap_hit(file);
+ ret = VM_FAULT_NOPAGE;
+ goto out;
+ }
}
addr = vma->vm_start +
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v2 3/3] mm/filemap: extend VM_SEQ_READ readahead below descending faults
2026-11-09 10:34 [PATCH v2 0/3] mm/filemap: read ahead for mmap readers that move backwards Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 1/3] mm/filemap: move the mmap hit check of filemap_map_pages() into helpers Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 2/3] mm/filemap: read ahead downwards on descending VM_HUGEPAGE faults Qiliang Yuan
@ 2026-11-09 10:34 ` Qiliang Yuan
2 siblings, 0 replies; 4+ messages in thread
From: Qiliang Yuan @ 2026-11-09 10:34 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jan Kara, Andrew Morton, Vishal Moola, fujunjie
Cc: Roman Gushchin, Usama Arif, linux-fsdevel, linux-mm,
linux-kernel, Qiliang Yuan
On a VM_SEQ_READ mapping, a synchronous fault reads ahead from the
faulting page towards higher offsets through page_cache_sync_ra().
A reader that moves towards lower offsets, such as memmove() copying
backwards out of a mapping, finds the pages above the fault already
cached, so each of its faults reads a single new page.
page_cache_sync_ra() also looks for a stream only below the fault and
takes it for a small random read.
When the page above the fault is already cached, read the window of
ra_pages that ends at the faulting page instead, without a readahead
mark. Reading one byte from each page of a 2 GiB file, last page first,
in a MADV_SEQUENTIAL mapping with a cold page cache, read_ahead_kb 8192:
before after
time 50.92 s 0.96 s
major faults 524289 258
Signed-off-by: Qiliang Yuan <qiliang.yuan@linux.dev>
---
mm/filemap.c | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index e445279cc561f..be587f8d0e8e6 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -3405,7 +3405,19 @@ static struct file *do_sync_mmap_readahead(struct vm_fault *vmf)
if (vm_flags & VM_SEQ_READ) {
fpin = maybe_unlock_mmap_for_io(vmf, fpin);
- page_cache_sync_ra(&ractl, ra->ra_pages);
+ if (!filemap_descending(mapping, vmf->pgoff, 1)) {
+ page_cache_sync_ra(&ractl, ra->ra_pages);
+ return fpin;
+ }
+ /* Read the window that ends at the faulting page */
+ ra->start = vmf->pgoff + 1 -
+ min_t(unsigned long, ra->ra_pages,
+ vmf->pgoff + 1);
+ ra->size = vmf->pgoff + 1 - ra->start;
+ ra->async_size = 0;
+ ra->order = 0;
+ ractl._index = ra->start;
+ page_cache_ra_order(&ractl, ra);
return fpin;
}
}
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-09 10:35 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-09 10:34 [PATCH v2 0/3] mm/filemap: read ahead for mmap readers that move backwards Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 1/3] mm/filemap: move the mmap hit check of filemap_map_pages() into helpers Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 2/3] mm/filemap: read ahead downwards on descending VM_HUGEPAGE faults Qiliang Yuan
2026-11-09 10:34 ` [PATCH v2 3/3] mm/filemap: extend VM_SEQ_READ readahead below descending faults Qiliang Yuan
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®