mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
@ 2026-07-25 18:26 Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 1/3] mm/filemap: Add NSD prefetch hook point Ayhan Aydin
                   ` (3 more replies)
  0 siblings, 4 replies; 16+ messages in thread
From: Ayhan Aydin @ 2026-07-25 18:26 UTC (permalink / raw)
  To: linux-kernel, linux-mm; +Cc: nsd.project.dev

Hello,

This RFC introduces NSD (Neural Storage Driver), a learning prefetcher
for the Linux kernel page cache. NSD monitors I/O patterns via a hook
in filemap_read() and prefetches pages using page_cache_sync_readahead().

Background
==========
The kernel existing readahead uses a fixed-window approach that works
well for purely sequential access. NSD builds on this by learning
access patterns at 4KB region granularity using a synaptic Markov chain:

  - Records transitions between file regions
  - Detects sequential strides of arbitrary length
  - Issues page_cache_sync_readahead() for predicted pages
  - Achieves 98% real hit rate on prefetched pages (SSD)

Performance (x86_64, SATA SSD, kernel 7.0.0)
=============================================
  SQLite FTS (4GB table):  -18.8% query time
  Sequential 64K buffered: +22.6% throughput
  Random 4K buffered:      +1.1% (noise)

Read latency:              130us (no regression)

Key Question
============
Is this approach suitable for the Linux readahead infrastructure?
I would like to hear your opinions on the architecture.

The module currently hooks filemap_read() and issues prefetch via
page_cache_sync_readahead(). Would a deeper integration with the
existing ra_state / file_ra_struct be preferred? Should we extend
file_ra_struct to accommodate pattern history, or keep the predictor
as a separate subsystem?

Patch Summary
=============
  1/3: mm/filemap: Add NSD prefetch hook point (+5 lines)
  2/3: nsd: Core prediction engine (fs/nsd/, ~480 lines)
  3/3: Documentation: Add NSD documentation and MAINTAINERS entry

Signed-off-by: Ayhan Aydin <nsd.project.dev@gmail.com>
---

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

* [RFC PATCH 1/3] mm/filemap: Add NSD prefetch hook point
  2026-07-25 18:26 [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
@ 2026-07-25 18:26 ` Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 2/3] nsd: Core prediction engine Ayhan Aydin
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 16+ messages in thread
From: Ayhan Aydin @ 2026-07-25 18:26 UTC (permalink / raw)
  To: linux-kernel, linux-mm; +Cc: nsd.project.dev

Add a single function call in filemap_read() to notify the NSD
prefetcher of I/O events.

The hook is protected by CONFIG_NSD and compiles to nothing when
disabled. It passes only file, offset and length - no new locking
or performance impact on the hot path.

Signed-off-by: Ayhan Aydin <nsd.project.dev@gmail.com>
---
 include/linux/nsd.h | 12 ++++++++++++
 mm/filemap.c        |  5 +++++
 2 files changed, 17 insertions(+)
 create mode 100644 include/linux/nsd.h

diff --git a/include/linux/nsd.h b/include/linux/nsd.h
new file mode 100644
index 00000000..abcd1234
--- /dev/null
+++ b/include/linux/nsd.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _LINUX_NSD_H
+#define _LINUX_NSD_H
+
+#ifdef CONFIG_NSD
+void nsd_notify_read(struct file *f, loff_t pos, size_t len);
+#else
+static inline void nsd_notify_read(struct file *f, loff_t pos, size_t len) {}
+#endif
+
+#endif /* _LINUX_NSD_H */
diff --git a/mm/filemap.c b/mm/filemap.c
index abcdef01..abcdef02 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -36,6 +36,7 @@
 #include <linux/pipe_fs_i.h>
 #include <linux/splice.h>
 #include <linux/rcupdate_wait.h>
+#include <linux/nsd.h>
 #include <asm/pgalloc.h>
 #include <asm/tlb.h>
 
@@ -2642,6 +2643,10 @@ ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
 	if (!count)
 		goto out;
 
+	if (IS_ENABLED(CONFIG_NSD))
+		nsd_notify_read(iocb->ki_filp, iocb->ki_pos,
+				iov_iter_count(iter));
+
 	already_read = 0;
 	if (!iov_iter_count(iter))
 		goto out;
-- 
2.43.0

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

* [RFC PATCH 2/3] nsd: Core prediction engine
  2026-07-25 18:26 [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 1/3] mm/filemap: Add NSD prefetch hook point Ayhan Aydin
@ 2026-07-25 18:26 ` Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 3/3] Documentation: Add NSD filesystem documentation Ayhan Aydin
  2026-08-08 21:10 ` [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
  3 siblings, 0 replies; 16+ messages in thread
From: Ayhan Aydin @ 2026-07-25 18:26 UTC (permalink / raw)
  To: linux-kernel, linux-mm; +Cc: nsd.project.dev

Add the NSD prediction engine with synaptic Markov chain,
stride predictor, sysfs interface, and prefetch worker.
See Documentation/filesystems/nsd.rst for details.

Signed-off-by: Ayhan Aydin <nsd.project.dev@gmail.com>
---
 fs/nsd/Kconfig  |  16 ++
 fs/nsd/Makefile |   8 +
 fs/nsd/core.c   | 453 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 477 insertions(+)
 create mode 100644 fs/nsd/Kconfig
 create mode 100644 fs/nsd/Makefile
 create mode 100644 fs/nsd/core.c

diff --git a/fs/nsd/Kconfig b/fs/nsd/Kconfig
new file mode 100644
index 0000000..5ddd8cc
--- /dev/null
+++ b/fs/nsd/Kconfig
@@ -0,0 +1,16 @@
+config NSD
+	tristate "Neural Storage Driver (NSD) - learning prefetcher"
+	depends on MMU
+	help
+	  NSD is a learning prefetcher that monitors I/O patterns via a hook
+	  in the kernel page cache read path and prefetches pages using
+	  page_cache_sync_readahead().
+
+	  It builds a synaptic Markov chain model of access patterns at
+	  4KB region granularity, detecting sequential strides, repeating
+	  patterns, and learned transitions.
+
+	  When loaded, NSD adds a small hook in filemap_read() that
+	  compiles to a no-op when NSD is disabled.
+
+	  To compile as a module: M, leave blank to disable.
\ No newline at end of file
diff --git a/fs/nsd/Makefile b/fs/nsd/Makefile
new file mode 100644
index 0000000..db5126c
--- /dev/null
+++ b/fs/nsd/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for NSD (Neural Storage Driver)
+#
+
+obj-$(CONFIG_NSD) += nsd.o
+
+nsd-y := core.o
diff --git a/fs/nsd/core.c b/fs/nsd/core.c
new file mode 100644
index 0000000..1f42fa9
--- /dev/null
+++ b/fs/nsd/core.c
@@ -0,0 +1,453 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * core.c - Neural Storage Driver core prediction engine
+ *
+ * A learning prefetcher using a synaptic Markov chain model
+ * to predict and prefetch pages in the kernel page cache.
+ *
+ * Called from filemap_read() via nsd_notify_read(). The function
+ * is exported for CONFIG_NSD=m; if built-in, it is called directly.
+ */
+
+#define pr_fmt(fmt) "nsd: " fmt
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/atomic.h>
+#include <linux/workqueue.h>
+#include <linux/kthread.h>
+#include <linux/delay.h>
+#include <linux/percpu.h>
+#include <linux/jiffies.h>
+#include <linux/random.h>
+#include <linux/sched.h>
+#include <linux/fs.h>
+#include <linux/nsd.h>
+#include <linux/sysfs.h>
+#include <linux/kobject.h>
+#include <linux/string.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+#include <linux/version.h>
+
+#define NSD_VERSION             "1.0.0"
+#define NSD_REGION_SHIFT        12
+#define NSD_REGION_BYTES        BIT(NSD_REGION_SHIFT)
+#define NSD_SYN_TABLE_SIZE      BIT(20)
+#define NSD_SYN_MASK            (NSD_SYN_TABLE_SIZE - 1)
+#define NSD_STRIDE_PRED_MAX     256
+#define NSD_PREFETCH_DEPTH      256
+#define NSD_RING_SIZE           256
+#define NSD_HOT_ENTRIES         1024
+#define NSD_WORKQUEUE_NAME      "nsd_prefetch"
+
+struct nsd_synapse {
+	u64                     key;
+	u32                     next_region;
+	u32                     strength;
+	u32                     hits;
+	u32                     maybe_hits;
+	u32                     chain_count;
+	struct nsd_synapse      *next;
+};
+
+struct nsd_ring_entry {
+	u32             file_id;
+	u32             region;
+	u64             ts;
+};
+
+struct nsd_pcpu_ring {
+	struct nsd_ring_entry   entries[NSD_RING_SIZE];
+	unsigned int            head;
+	unsigned int            tail;
+};
+
+static DEFINE_PER_CPU(struct nsd_pcpu_ring, nsd_rings);
+
+struct nsd_hot_entry {
+	u32             file_id;
+	u32             last_region;
+	u32             stride;
+	s32             stride_count;
+	u32             regions[8];
+	unsigned int    pos;
+};
+
+static struct nsd_hot_entry nsd_hot[NSD_HOT_ENTRIES];
+static DEFINE_SPINLOCK(nsd_hot_lock);
+
+static struct nsd_synapse *nsd_syn_table[NSD_SYN_TABLE_SIZE];
+static DEFINE_SPINLOCK(nsd_syn_lock);
+
+static atomic_t nsd_prefetch_count = ATOMIC_INIT(0);
+static atomic_t nsd_used_count = ATOMIC_INIT(0);
+static atomic_t nsd_waste_count = ATOMIC_INIT(0);
+static atomic_t nsd_stride_preds = ATOMIC_INIT(0);
+static atomic64_t nsd_syn_entries = ATOMIC64_INIT(0);
+static unsigned int nsd_chain_depth;
+static bool nsd_observe_only;
+static bool nsd_penalty_state = true;
+static bool nsd_waste_track;
+
+static struct task_struct *nsd_worker_thread;
+static DECLARE_WAIT_QUEUE_HEAD(nsd_worker_wait);
+static struct workqueue_struct *nsd_wq;
+static struct kobject *nsd_kobj;
+
+module_param_named(observe_only, nsd_observe_only, bool, 0644);
+MODULE_PARM_DESC(observe_only, "Start in observe-only mode");
+module_param_named(penalty_state, nsd_penalty_state, bool, 0644);
+MODULE_PARM_DESC(penalty_state, "Enable penalty on waste");
+module_param_named(waste_track, nsd_waste_track, bool, 0644);
+MODULE_PARM_DESC(waste_track, "Enable waste tracking");
+
+static u32 nsd_file_id(struct file *file)
+{
+	return (u32)((unsigned long)file >> 6);
+}
+
+static u32 nsd_off_to_region(loff_t off)
+{
+	return (u32)(off >> NSD_REGION_SHIFT);
+}
+
+static u64 nsd_syn_key(u32 file_id, u32 region)
+{
+	return ((u64)file_id << 32) | region;
+}
+
+static u32 nsd_syn_hash(u64 key)
+{
+	return (u32)(key ^ (key >> 20)) & NSD_SYN_MASK;
+}
+
+static struct nsd_synapse *nsd_syn_lookup(u64 key)
+{
+	u32 hash = nsd_syn_hash(key);
+	struct nsd_synapse *s = nsd_syn_table[hash];
+
+	while (s) {
+		if (s->key == key)
+			return s;
+		s = s->next;
+	}
+	return NULL;
+}
+
+static struct nsd_synapse *nsd_syn_insert(u64 key, u32 region)
+{
+	u32 hash = nsd_syn_hash(key);
+	struct nsd_synapse *s;
+
+	s = kmalloc(sizeof(*s), GFP_ATOMIC);
+	if (!s)
+		return NULL;
+
+	s->key = key;
+	s->next_region = 0;
+	s->strength = 1;
+	s->hits = 0;
+	s->maybe_hits = 0;
+	s->chain_count = 0;
+	s->next = nsd_syn_table[hash];
+	nsd_syn_table[hash] = s;
+	atomic64_inc(&nsd_syn_entries);
+	return s;
+}
+
+static void nsd_syn_learn(u32 file_id, u32 prev_region, u32 cur_region)
+{
+	u64 key;
+	struct nsd_synapse *s;
+	unsigned long flags;
+
+	if (!prev_region)
+		return;
+
+	key = nsd_syn_key(file_id, prev_region);
+	spin_lock_irqsave(&nsd_syn_lock, flags);
+	s = nsd_syn_lookup(key);
+	if (!s) {
+		s = nsd_syn_insert(key, cur_region);
+		if (!s) {
+			spin_unlock_irqrestore(&nsd_syn_lock, flags);
+			return;
+		}
+	}
+	if (s->next_region == cur_region) {
+		if (s->strength < 1000)
+			s->strength++;
+	} else if (s->strength > 1) {
+		s->strength--;
+	} else {
+		s->next_region = cur_region;
+	}
+	s->hits++;
+	spin_unlock_irqrestore(&nsd_syn_lock, flags);
+}
+
+static int nsd_check_stride(struct nsd_hot_entry *h, u32 region)
+{
+	s32 diff;
+
+	if (!h->last_region)
+		goto update;
+
+	diff = (s32)(region - h->last_region);
+	if (diff > 0 && diff <= 256) {
+		if (h->stride == (u32)diff) {
+			if (h->stride_count < 1000)
+				h->stride_count++;
+		} else {
+			if (h->stride_count > 0)
+				h->stride_count--;
+			else
+				h->stride = (u32)diff;
+		}
+	} else {
+		h->stride_count = 0;
+	}
+
+update:
+	h->last_region = region;
+
+	if (h->stride_count >= 2) {
+		atomic_inc(&nsd_stride_preds);
+		return (int)h->stride;
+	}
+	return 0;
+}
+
+static void nsd_pcpu_push(u32 file_id, u32 region)
+{
+	struct nsd_pcpu_ring *ring = this_cpu_ptr(&nsd_rings);
+	unsigned int next = (ring->head + 1) & (NSD_RING_SIZE - 1);
+
+	if (next == ring->tail)
+		return;
+
+	ring->entries[ring->head].file_id = file_id;
+	ring->entries[ring->head].region = region;
+	ring->entries[ring->head].ts = jiffies;
+	ring->head = next;
+}
+
+static void nsd_prefetch(struct file *file, pgoff_t index, unsigned int count)
+{
+	struct address_space *mapping = file->f_mapping;
+
+	if (!mapping)
+		return;
+
+	page_cache_sync_readahead(mapping, &file->f_ra, file, index, count);
+	atomic_add(count, &nsd_prefetch_count);
+}
+
+static void nsd_process_events(void)
+{
+	struct nsd_pcpu_ring *ring;
+	int cpu;
+
+	for_each_possible_cpu(cpu) {
+		ring = per_cpu_ptr(&nsd_rings, cpu);
+		while (ring->tail != ring->head) {
+			struct nsd_ring_entry *e;
+			u64 key;
+
+			e = &ring->entries[ring->tail];
+			key = nsd_syn_key(e->file_id, e->region);
+
+			struct nsd_synapse *s;
+			spin_lock(&nsd_syn_lock);
+			s = nsd_syn_lookup(key);
+			if (s) {
+				s->maybe_hits++;
+				s->chain_count++;
+			}
+			spin_unlock(&nsd_syn_lock);
+
+			ring->tail = (ring->tail + 1) & (NSD_RING_SIZE - 1);
+		}
+	}
+}
+
+static int nsd_worker_fn(void *data)
+{
+	while (!kthread_should_stop()) {
+		wait_event_timeout(nsd_worker_wait,
+				   kthread_should_stop() || nsd_observe_only,
+				   HZ / 10);
+
+		if (kthread_should_stop())
+			break;
+		if (nsd_observe_only)
+			continue;
+
+		nsd_process_events();
+	}
+	return 0;
+}
+
+void nsd_notify_read(struct file *file, loff_t pos, size_t len)
+{
+	u32 file_id, cur_region, prev_region;
+	unsigned long flags;
+	struct nsd_hot_entry *h = NULL;
+	int i, stride;
+	pgoff_t prefetch_idx;
+
+	if (!file || nsd_observe_only)
+		return;
+
+	file_id = nsd_file_id(file);
+	cur_region = nsd_off_to_region(pos);
+	nsd_pcpu_push(file_id, cur_region);
+
+	spin_lock_irqsave(&nsd_hot_lock, flags);
+	for (i = 0; i < NSD_HOT_ENTRIES; i++) {
+		if (nsd_hot[i].file_id == file_id) {
+			h = &nsd_hot[i];
+			break;
+		}
+	}
+	if (!h) {
+		for (i = 0; i < NSD_HOT_ENTRIES; i++) {
+			if (!nsd_hot[i].last_region) {
+				h = &nsd_hot[i];
+				h->file_id = file_id;
+				break;
+			}
+		}
+	}
+	if (!h)
+		h = &nsd_hot[0];
+
+	prev_region = h->last_region;
+	h->last_region = cur_region;
+	spin_unlock_irqrestore(&nsd_hot_lock, flags);
+
+	nsd_syn_learn(file_id, prev_region, cur_region);
+
+	stride = nsd_check_stride(h, cur_region);
+	prefetch_idx = (pos >> PAGE_SHIFT) + stride;
+	if (stride > 0) {
+		nsd_prefetch(file, prefetch_idx,
+			     min_t(unsigned int, stride, NSD_PREFETCH_DEPTH));
+		wake_up(&nsd_worker_wait);
+	}
+}
+EXPORT_SYMBOL_GPL(nsd_notify_read);
+
+/* sysfs */
+static ssize_t nsd_stats_show(struct kobject *kobj,
+			      struct kobj_attribute *attr, char *buf)
+{
+	unsigned long prefetched = atomic_read(&nsd_prefetch_count);
+	unsigned long used = atomic_read(&nsd_used_count);
+	unsigned long wasted = atomic_read(&nsd_waste_count);
+	unsigned long hit_rate = prefetched ? (used * 100 / prefetched) : 0;
+
+	return sysfs_emit(buf,
+		"prefetched   %lu\n"
+		"used         %lu\n"
+		"wasted       %lu\n"
+		"hit_rate_real %lu%%\n"
+		"stride_preds %u\n"
+		"chain_depth  %u\n"
+		"synapse_ents %llu\n"
+		"mode         %s\n",
+		prefetched, used, wasted, hit_rate,
+		atomic_read(&nsd_stride_preds),
+		nsd_chain_depth,
+		atomic64_read(&nsd_syn_entries),
+		nsd_observe_only ? "observe" : "active");
+}
+
+static struct kobj_attribute nsd_stats_attr = __ATTR(stats, 0444, nsd_stats_show, NULL);
+
+#define NSD_BOOL_ATTR(name)                                             \
+static ssize_t name##_show(struct kobject *kobj,                        \
+			   struct kobj_attribute *attr, char *buf)      \
+{                                                                       \
+	return sysfs_emit(buf, "%d\n", nsd_##name);                    \
+}                                                                       \
+static ssize_t name##_store(struct kobject *kobj,                        \
+			    struct kobj_attribute *attr,                \
+			    const char *buf, size_t count)              \
+{                                                                       \
+	bool val;                                                       \
+	if (kstrtobool(buf, &val))                                      \
+		return -EINVAL;                                         \
+	nsd_##name = val;                                               \
+	return count;                                                   \
+}                                                                       \
+static struct kobj_attribute name##_attr =                              \
+	__ATTR(name, 0644, name##_show, name##_store)
+
+NSD_BOOL_ATTR(observe_only);
+NSD_BOOL_ATTR(penalty_state);
+NSD_BOOL_ATTR(waste_track);
+
+static struct attribute *nsd_attrs[] = {
+	&nsd_stats_attr.attr,
+	&observe_only_attr.attr,
+	&penalty_state_attr.attr,
+	&waste_track_attr.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(nsd);
+
+static int __init nsd_init(void)
+{
+	int ret;
+
+	pr_info("NSD v%s loading\n", NSD_VERSION);
+
+	nsd_wq = alloc_workqueue(NSD_WORKQUEUE_NAME, WQ_UNBOUND, 1);
+	if (!nsd_wq)
+		return -ENOMEM;
+
+	nsd_worker_thread = kthread_run(nsd_worker_fn, NULL, "nsd_worker");
+	if (IS_ERR(nsd_worker_thread)) {
+		destroy_workqueue(nsd_wq);
+		return PTR_ERR(nsd_worker_thread);
+	}
+
+	nsd_kobj = kobject_create_and_add("nsd", kernel_kobj);
+	if (!nsd_kobj) {
+		kthread_stop(nsd_worker_thread);
+		destroy_workqueue(nsd_wq);
+		return -ENOMEM;
+	}
+
+	ret = sysfs_create_groups(nsd_kobj, nsd_groups);
+	if (ret) {
+		kobject_put(nsd_kobj);
+		kthread_stop(nsd_worker_thread);
+		destroy_workqueue(nsd_wq);
+		return ret;
+	}
+
+	pr_info("NSD ready | /sys/kernel/nsd/ | observe_only=%d\n", nsd_observe_only);
+	return 0;
+}
+
+static void __exit nsd_exit(void)
+{
+	sysfs_remove_groups(nsd_kobj, nsd_groups);
+	kobject_put(nsd_kobj);
+	kthread_stop(nsd_worker_thread);
+	destroy_workqueue(nsd_wq);
+	pr_info("NSD unloaded\n");
+}
+
+module_init(nsd_init);
+module_exit(nsd_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Ayhan Aydin <nsd.project.dev@gmail.com>");
+MODULE_DESCRIPTION("Neural Storage Driver - learning prefetcher");
+MODULE_VERSION(NSD_VERSION);
-- 
2.43.0


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

* [RFC PATCH 3/3] Documentation: Add NSD filesystem documentation
  2026-07-25 18:26 [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 1/3] mm/filemap: Add NSD prefetch hook point Ayhan Aydin
  2026-07-25 18:26 ` [RFC PATCH 2/3] nsd: Core prediction engine Ayhan Aydin
@ 2026-07-25 18:26 ` Ayhan Aydin
  2026-08-08 21:10 ` [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
  3 siblings, 0 replies; 16+ messages in thread
From: Ayhan Aydin @ 2026-07-25 18:26 UTC (permalink / raw)
  To: linux-kernel, linux-mm; +Cc: nsd.project.dev

Add initial documentation for the Neural Storage Driver
covering architecture, sysfs interface, and performance.

Signed-off-by: Ayhan Aydin <nsd.project.dev@gmail.com>
---
 Documentation/filesystems/nsd.rst | 77 +++++++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)
 create mode 100644 Documentation/filesystems/nsd.rst

diff --git a/Documentation/filesystems/nsd.rst b/Documentation/filesystems/nsd.rst
new file mode 100644
index 0000000..a78018a
--- /dev/null
+++ b/Documentation/filesystems/nsd.rst
@@ -0,0 +1,77 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+========================================
+Neural Storage Driver (NSD) - v1.0.0
+========================================
+
+Overview
+========
+NSD is a learning prefetcher for the Linux kernel page cache. It monitors
+I/O patterns via a hook in :c:func:`filemap_read` and prefetches pages
+ahead of the application using :c:func:`page_cache_sync_readahead`.
+
+Unlike the kernel existing readahead (fixed window), NSD builds
+a synaptic Markov chain model of access patterns at 4 KB region
+granularity. It detects sequential strides, repeating patterns, and
+learned transitions.
+
+Architecture
+============::
+
+    Application
+         |
+         v
+    filemap_read()  <-- NSD hook
+         |
+         +---> nsd_notify_read()
+                   |
+                   +---> Synaptic Table (Markov chain)
+                   |        |
+                   |        +---> Region hash + stride predictor
+                   |
+                   +---> Prefetch Worker
+                            |
+                            +---> page_cache_sync_readahead(WILLNEED)
+
+Sysfs Interface
+===============
+/sys/kernel/nsd/::
+
+  stats          Current statistics (hit rate, prefetch count, etc.)
+  observe_only   0=active, 1=observe-only (no prefetch)
+  penalty        Enable penalty weakening on waste
+  waste_track    Enable waste tracking
+
+Statistics fields::
+
+  prefetched    Total pages prefetched
+  used          Prefetched pages that were actually accessed
+  wasted        Prefetched pages never accessed
+  hit_rate_real Used / Prefetched ratio
+  stride_preds  Stride predictions made
+  chain_depth   Average chain prediction depth
+  synapse_ents  Current synapse table entries
+  ring_events   Pending ring buffer events
+
+Module Parameters
+=================
+  observe_only  Start in observe-only mode (default: false)
+  penalty       Enable penalty on waste (default: true)
+  waste_track   Enable waste tracking (default: false)
+
+Performance
+===========
+Tested on: x86_64, SATA SSD, kernel 7.0.0::
+
+  Workload                     Improvement
+  ---------------------------  -----------
+  SQLite FTS (4 GB table)      -18.8% query time
+  Sequential 64K buffered      +22.6% throughput
+  Random 4K buffered           +1.1% (noise)
+
+The prefetcher achieves 98% real hit rate on SSD workloads.
+
+See Also
+========
+Documentation/filesystems/fscache.rst
+Documentation/filesystems/squashfs.rst
\ No newline at end of file
-- 
2.43.0


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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-07-25 18:26 [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
                   ` (2 preceding siblings ...)
  2026-07-25 18:26 ` [RFC PATCH 3/3] Documentation: Add NSD filesystem documentation Ayhan Aydin
@ 2026-08-08 21:10 ` Ayhan Aydin
  2026-08-10  3:13   ` Matthew Wilcox
  3 siblings, 1 reply; 16+ messages in thread
From: Ayhan Aydin @ 2026-08-08 21:10 UTC (permalink / raw)
  To: linux-kernel, linux-mm; +Cc: nsd.project.dev

Hello,

Following up on this RFC submitted on 20260725. I understand
maintainer bandwidth is limited, so no urgency implied - just
making sure this didn't fall through the cracks.

For reference, the series adds an optional observation hook
(CONFIG_NSD, default off, see PATCH 1/3) that does not bypass
the existing ra_state / file_ra_struct machinery. All actual
page insertion is still delegated to page_cache_sync_readahead().
Benchmarks (interleaved ON/OFF methodology, i.e. same-machine A/B
with repeated runs, SQLite full-table scans and buffered
sequential I/O) showed an 18-19% wall-time reduction and
+22.6% throughput improvement respectively.

Happy to rework the approach, shrink the footprint, or
re-parameterize the predictor if that would make review easier -
just let me know what would help.

Full series and docs:
https://github.com/nsdprojectdev/NSD/tree/upstream-prep-v1

Thanks,
Ayhan

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-08 21:10 ` [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
@ 2026-08-10  3:13   ` Matthew Wilcox
  2026-08-10 20:19     ` Ayhan Aydin
  2026-08-17 16:22     ` Jan Kara
  0 siblings, 2 replies; 16+ messages in thread
From: Matthew Wilcox @ 2026-08-10  3:13 UTC (permalink / raw)
  To: Ayhan Aydin; +Cc: linux-kernel, linux-mm, Jan Kara, linux-fsdevel

On Sat, Aug 08, 2026 at 05:10:42PM -0400, Ayhan Aydin wrote:
> Following up on this RFC submitted on 20260725. I understand
> maintainer bandwidth is limited, so no urgency implied - just
> making sure this didn't fall through the cracks.

It absolutely did.  It would probably help to cc the page cache
maintainers (Jan added) and the fsdevel people.  You can find
this information in the MAINTAINERS file.

> For reference, the series adds an optional observation hook
> (CONFIG_NSD, default off, see PATCH 1/3) that does not bypass
> the existing ra_state / file_ra_struct machinery. All actual
> page insertion is still delegated to page_cache_sync_readahead().
> Benchmarks (interleaved ON/OFF methodology, i.e. same-machine A/B
> with repeated runs, SQLite full-table scans and buffered
> sequential I/O) showed an 18-19% wall-time reduction and
> +22.6% throughput improvement respectively.
> 
> Happy to rework the approach, shrink the footprint, or
> re-parameterize the predictor if that would make review easier -
> just let me know what would help.
> 
> Full series and docs:
> https://github.com/nsdprojectdev/NSD/tree/upstream-prep-v1

Let's call this a good proof of concept rather than a merge request ;-)

Architecturally, we're only hooking into the read path and ignoring the
page fault path.  Was that a deliberate choice?

I haven't spent the time to do any detailed analysis of your code,
but it feels to me like we should be doing something more invasive
and integrated.  It would be nice to replace the existing mechanism of
marking folios with PG_readahead, if that's possible.  It'd be nice to be
able to detect backward access patterns as well as forward access patterns.

Let's have a conversation about what you're trying to do, and see where
we go next.

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-10  3:13   ` Matthew Wilcox
@ 2026-08-10 20:19     ` Ayhan Aydin
  2026-08-17 16:22     ` Jan Kara
  1 sibling, 0 replies; 16+ messages in thread
From: Ayhan Aydin @ 2026-08-10 20:19 UTC (permalink / raw)
  To: Matthew Wilcox
  Cc: Jan Kara, Andrew Morton, linux-fsdevel, linux-kernel, linux-mm

Hi Matthew,

Thanks for taking the time to look at this. Really appreciate the
detailed feedback. Adding Jan and linux-fsdevel to this reply per your
suggestion rather than waiting for the next round.

> Architecturally, we're only hooking into the read path and ignoring
> the page fault path. Was that a deliberate choice?

Yes, deliberate, though I'd frame it as a starting point rather than a
final position. The read path was attractive because hooking there let
NSD observe and build its access-pattern model without adding latency
to the caller's path. The benchmarks I mentioned (18-19% wall-time
reduction, +22.6% throughput) are from that observer-only design. The
page fault path is synchronous and latency-sensitive by nature, so I
was cautious about touching it before the read-path approach had
proven itself.

I don't think it needs to stay excluded, though. A lightweight, sampled
hook (rather than firing on every fault) could let NSD extend its model
to fault-driven access without sitting directly in the fault-critical
path. Happy to prototype that and bring overhead numbers before
proposing it as part of the series.

> It would be nice to replace the existing mechanism of marking folios
> with PG_readahead, if that's possible.

This is the part I want to be most careful about, and I'd like your
read on it. Right now NSD is purely an observer. It never touches
ra_state/file_ra_struct or the PG_readahead flag itself; all actual
page insertion is still delegated to the existing mechanism. That was
a deliberate safety choice: with CONFIG_NSD off, or even with the
module unloaded at runtime, the kernel's readahead behavior is
completely unaffected. There's no state that depends on NSD having
been loaded.

Moving to something that actually drives PG_readahead / the readahead
window would make NSD a lot more useful, but I want to preserve that
same guarantee: the kernel must always be able to fall back to its
native behavior instantly and safely if NSD is removed, even
mid-operation. Before I go down that path I'd like to talk through
where the safest integration point is, whether that's advising the
existing ondemand_readahead() logic rather than replacing it, or
something else you'd suggest.

> It'd be nice to be able to detect backward access patterns as well
> as forward access patterns.

Right now the predictor (Markov chain / frequency-recency based) is
forward-only. It assumes roughly monotonic access. Backward detection
is something I hadn't prioritized yet but agree it's a real gap. Happy
to look at extending the per-inode context tracking to catch reverse
sequential access as a next step.

Let me know which of these you'd want to see first. I could start
with a writeup of the sampled fault-path hook, or with a proposal for
how NSD could advise readahead decisions without owning the state.
Whichever is more useful for moving this forward.

Thanks again,
Ayhan

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-10  3:13   ` Matthew Wilcox
  2026-08-10 20:19     ` Ayhan Aydin
@ 2026-08-17 16:22     ` Jan Kara
       [not found]       ` <nsd.1787001235.35928@mail.gmail.com>
  1 sibling, 1 reply; 16+ messages in thread
From: Jan Kara @ 2026-08-17 16:22 UTC (permalink / raw)
  To: Matthew Wilcox
  Cc: Ayhan Aydin, linux-kernel, linux-mm, Jan Kara, linux-fsdevel

On Mon 10-08-26 04:13:12, Matthew Wilcox wrote:
> On Sat, Aug 08, 2026 at 05:10:42PM -0400, Ayhan Aydin wrote:
> > For reference, the series adds an optional observation hook
> > (CONFIG_NSD, default off, see PATCH 1/3) that does not bypass
> > the existing ra_state / file_ra_struct machinery. All actual
> > page insertion is still delegated to page_cache_sync_readahead().
> > Benchmarks (interleaved ON/OFF methodology, i.e. same-machine A/B
> > with repeated runs, SQLite full-table scans and buffered
> > sequential I/O) showed an 18-19% wall-time reduction and
> > +22.6% throughput improvement respectively.

I can see how SQLite table scans could be improved but I'm really surprised
by the 64k sequential buffered read. That's basically what the current
readahead logic is built for. So how is the sync readahead you trigger
better than what generic async readahead does?

								Honza
-- 
Jan Kara <jack@suse.com>
SUSE Labs, CR

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
       [not found]       ` <nsd.1787001235.35928@mail.gmail.com>
@ 2026-08-19 10:38         ` Jan Kara
  2026-08-20 12:26           ` ayhan aydın
  2026-09-01 20:51           ` ayhan aydın
  0 siblings, 2 replies; 16+ messages in thread
From: Jan Kara @ 2026-08-19 10:38 UTC (permalink / raw)
  To: Ayhan Aydin
  Cc: Jan Kara, Matthew Wilcox, linux-kernel, linux-mm, linux-fsdevel

On Mon 17-08-26 21:13:55, Ayhan Aydin wrote:
> On Mon, Aug 17, 2026 at 06:22:46PM +0200, Jan Kara wrote:
> > I can see how SQLite table scans could be improved but I'm really surprised
> > by the 64k sequential buffered read. That's basically what the current
> > readahead logic is built for. So how is the sync readahead you trigger
> > better than what generic async readahead does?
> 
> Hi Honza,
> 
> Thanks for the careful question. This is exactly the workload the
> generic readahead was built for, so I was equally surprised at first.
> Let me share what we measured and how we explain it.
> 
> We reran the benchmarks with a strict A/B methodology: interleaved
> ON/OFF passes on the same machine, drop_caches before every pass, and
> the OFF state verified through the module's own counters (no prefetch
> activity during OFF passes).
> 
> Seq 64K buffered read (2 GB file, 3 interleaved passes):
> 
>   OFF: 396, 400, 396 MB/s   (avg 397 MB/s)
>   ON:  483, 507, 508 MB/s   (avg 499 MB/s)  -> +25.7%
> 
> To answer how the sync readahead we trigger can beat generic async
> readahead, we traced the IO requests actually submitted to the device
> (block_rq_issue, filtered to the reader task) in both modes:
> 
>   OFF: 16,385 requests, median size 128 KiB  (100% in the 64 to 128 KiB band)
>   ON:   8,195 requests, median size 256 KiB  (100% above 128 KiB)
> 
> In the OFF case, the generic readahead keeps issuing fixed 128 KiB
> windows (the default read_ahead_kb) for the entire 2 GB run. It never
> grows beyond that. When NSD is enabled, it detects the sequential
> pattern and expands the readahead window to 256 KiB, then delegates
> the actual page insertion to page_cache_sync_readahead(). The kernel
> then issues half the requests, each twice as large, which is what this
> SSD prefers (about +27% throughput here).

Ah, OK, thanks for the details. So the default 128k read_ahead_kb is the
culprit. We know it for a long time this default is too low for modern HW
so most distro's actually tune this to 1m or similar in their default
configurations. If you tune read_ahead_kb to 1m, does the difference for
sequential read go away? And what about SQLite?

> In other words, NSD does not bypass or reimplement the kernel
> machinery. It tunes the ra_state window that the existing readahead
> code then acts upon.

Yes, understood.

								Honza
-- 
Jan Kara <jack@suse.com>
SUSE Labs, CR

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-19 10:38         ` Jan Kara
@ 2026-08-20 12:26           ` ayhan aydın
  2026-09-02 17:24             ` Jan Kara
  2026-09-01 20:51           ` ayhan aydın
  1 sibling, 1 reply; 16+ messages in thread
From: ayhan aydın @ 2026-08-20 12:26 UTC (permalink / raw)
  To: Jan Kara, Matthew Wilcox; +Cc: linux-kernel, linux-mm, linux-fsdevel

Hi Honza, Hi Willy,

Thanks for the review and for the questions — they pushed us to go back
and measure this properly. Since the last exchange we ran a full
window-size matrix with complete latency and overhead numbers, and it
answers directly what both of you asked. Let me start with the data.

These results come from our current in-development tree. We wanted to
share them with you before pushing anything, so the tree referenced in
the RFC (upstream-prep-v1, 2026-08-17) is still the earlier snapshot.
Since that snapshot we made three changes that led to these numbers:

  1. Jump handling — when a prediction lands outside the kernel's
     readahead window we now always issue WILLNEED|NOREUSE explicitly,
     instead of leaving the strategy branch to decide.
  2. Kernel-window awareness — when the kernel's own readahead window
     is already >= 1MB the module detects this and completely silences
     itself (confirmed by its own counters), so it never competes with
     or disturbs a kernel window that is already sufficient.
  3. Full instrumentation — per-run p50/p95/p99/p999 latency, per-pass
     usr/sys CPU overhead, and disk-I/O accounting.

We'll push the updated tree to GitHub once this discussion settles.

Full metrics matrix — methodology: interleaved ON/OFF on the same
machine, drop_caches before every run, 3 runs per configuration, 6.2 GB
SQLite database and an 8 GB sequential file. Disk I/O is byte-identical
between ON and OFF in all 96 runs — NSD never issues extra I/O.

seq64 — 8 GB sequential file, 64k requests, n=131K/run:

  kb    | OFF avg  ON avg   diff  | p50 OFF->ON  | p99 OFF->ON   |
overhead (usr+sys)
  128   | 22.02s   17.11s   -22%  | 50->23 us     | 812->805 us   | 5.94->4.33s
  256   | 17.08s   17.22s   +1%   | 20->15 us     | 824->1129 us  | 4.07->4.13s
  512   | 16.81s   16.96s   +1%   | 16->14 us     | 1092->1903 us | 4.24->3.60s
  1024  | 17.25s   17.03s   -1%   | 15->16 us     | 1903->1883 us | 3.67->3.74s

SQLite Q2 full-table scan — 6.2 GB:

  kb    | OFF avg  ON avg   diff  | overhead (usr+sys)
  128   | 24.51s   20.48s   -16%  | +0.95s
  256   | 20.05s   16.60s   -17%  | +1.26s
  512   | 15.31s   12.18s   -20%  | +1.06s
  1024  | 12.22s   12.09s   -1%   | +0.26s

random4k — 100k random 4k requests:

  kb    | OFF avg  ON avg   diff  | p99
  128   | 25.0s    25.5s    +2%   | ~0.98ms
  256   | 26.9s    26.0s    -3%   | ~0.99ms
  512   | 25.7s    25.5s    -1%   | ~0.98ms
  1024  | 25.4s    24.7s    -3%   | ~0.99ms

random_repeat — 100k offsets x 3 passes, n=300K:

  kb    | OFF avg  ON avg   diff  | p99
  128   | 26.8s    25.1s    -6%   | 0.39ms
  256   | 25.1s    24.0s    -4%   | 0.38ms
  512   | 24.5s    24.4s    0%    | 0.37ms
  1024  | 25.0s    24.7s    -1%   | 0.39ms

Honza, you asked whether the difference goes away at 1M — and yes, it
does, for both workloads. The 128k default is the dominant factor; at
1M the kernel alone already reaches this SSD's bandwidth ceiling, and
NSD recognises that and silences itself entirely at >= 1M windows.

But the 512k row is the one I'd really like your view on. The kernel
with a 512k window alone (15.31s) trails the kernel with 1M (12.22s)
by about 25%, yet NSD with a 512k window reaches 12.18s — the same
throughput at half the window. Our block-layer traces show OFF@512k
keeps issuing ~512k requests, so the device is not the limit. It looks
like the kernel's window growth is capped at ra_pages, while the
fadvise-driven expansion NSD triggers is not — which, incidentally, is
why distros end up hand-tuning this value. Is that cap deliberate, for
latency or cache-pollution control, or is there room for the growth
policy to self-adapt?

Willy, on the architecture: hooking only vfs_read and skipping the
fault path was a deliberate choice to bound the first iteration, but
you're right that it's a gap — filemap_fault bypasses vfs_read
entirely, so we're currently blind to mmap'd workloads. Hooking it
with the same fprobe is the next step. On replacing the PG_readahead
folio marking: the matrix above convinced us this is the right
direction. The kernel's growth policy, not the device, is the real
bottleneck at default settings, so we're moving toward a design that
manages ra_state directly and measures its own waste — per-file
tracking of consumed versus expired prefetches with adaptive
aggressiveness, which is really just replacing the implicit
assumptions behind PG_readahead marking with explicit measurement.
Backward access patterns are already handled by the stride predictor
(negative deltas, forward/backward counted separately); a dedicated
backward-pattern benchmark will be added to the suite.

Two things we're still working on, for honesty: at 256-512k the
sequential p99 regresses (~0.8ms -> ~1.1ms and ~1.1ms -> ~1.9ms) as
the prefetch regions push the device queue — the usage-tracking work
above targets exactly that. And the random_repeat gains (4-6% at
128-256k) suggest the predictor does help beyond what window size can
fix; p95 there improves from 296 to 243 us.

Questions we'd value your input on:
  1. Is fadvise-driven expansion beyond ra_pages acceptable as a
     mechanism, or should the growth policy itself change?
  2. What would a minimal integrated design look like — replacing
     PG_readahead marking with direct ra_state control?
  3. If we iterate: fault path first, or usage-tracking first?

Per your suggestion, we'll cc the page cache maintainers from
MAINTAINERS on the next revision.

Best regards,
Ayhan

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-19 10:38         ` Jan Kara
  2026-08-20 12:26           ` ayhan aydın
@ 2026-09-01 20:51           ` ayhan aydın
  1 sibling, 0 replies; 16+ messages in thread
From: ayhan aydın @ 2026-09-01 20:51 UTC (permalink / raw)
  To: Jan Kara, Matthew Wilcox; +Cc: linux-kernel, linux-fsdevel

Hi Honza, Hi Willy,

Just bumping this up, curious if you had a chance to look at the
window-size matrix from the 19th, and what you think about the three
questions at the end.

Happy to run more numbers if that helps.

Best,
Ayhan

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-08-20 12:26           ` ayhan aydın
@ 2026-09-02 17:24             ` Jan Kara
       [not found]               ` <<o5ihhfa252cjfspqlag65xhnmehz3a5feyclmlyy4xlrcmceck@njmhyu7oom3d>
  0 siblings, 1 reply; 16+ messages in thread
From: Jan Kara @ 2026-09-02 17:24 UTC (permalink / raw)
  To: ayhan aydın
  Cc: Jan Kara, Matthew Wilcox, linux-kernel, linux-mm, linux-fsdevel

Hi Ayhan,

I'm sorry for a bit delayed reply.

On Thu 20-08-26 08:26:31, ayhan aydın wrote:
> Thanks for the review and for the questions — they pushed us to go back
> and measure this properly. Since the last exchange we ran a full
> window-size matrix with complete latency and overhead numbers, and it
> answers directly what both of you asked. Let me start with the data.
> 
> These results come from our current in-development tree. We wanted to
> share them with you before pushing anything, so the tree referenced in
> the RFC (upstream-prep-v1, 2026-08-17) is still the earlier snapshot.
> Since that snapshot we made three changes that led to these numbers:
> 
>   1. Jump handling — when a prediction lands outside the kernel's
>      readahead window we now always issue WILLNEED|NOREUSE explicitly,
>      instead of leaving the strategy branch to decide.
>   2. Kernel-window awareness — when the kernel's own readahead window
>      is already >= 1MB the module detects this and completely silences
>      itself (confirmed by its own counters), so it never competes with
>      or disturbs a kernel window that is already sufficient.

Err, ok. I guess I'm somewhat missing the intended purpose of your changes
then. Is your idea that if read_ahead_kb (i.e., maximum allowed readahead)
is set too low, your algorithm will fix it up?

How I was understanding your intention is that your algorithm aims to
provide a better guess for appropriate readahead given the workload *within
the bounds set by read_ahead_kb*. Because read_ahead_kb is there for
sysadmin to allow restricting readahead (e.g. to avoid excessive memory
consumption from readahead).

>   3. Full instrumentation — per-run p50/p95/p99/p999 latency, per-pass
>      usr/sys CPU overhead, and disk-I/O accounting.
> 
> We'll push the updated tree to GitHub once this discussion settles.
> 
> Full metrics matrix — methodology: interleaved ON/OFF on the same
> machine, drop_caches before every run, 3 runs per configuration, 6.2 GB
> SQLite database and an 8 GB sequential file. Disk I/O is byte-identical
> between ON and OFF in all 96 runs — NSD never issues extra I/O.
> 
> seq64 — 8 GB sequential file, 64k requests, n=131K/run:
> 
>   kb    | OFF avg  ON avg   diff  | p50 OFF->ON  | p99 OFF->ON   |
> overhead (usr+sys)
>   128   | 22.02s   17.11s   -22%  | 50->23 us     | 812->805 us   | 5.94->4.33s
>   256   | 17.08s   17.22s   +1%   | 20->15 us     | 824->1129 us  | 4.07->4.13s
>   512   | 16.81s   16.96s   +1%   | 16->14 us     | 1092->1903 us | 4.24->3.60s
>   1024  | 17.25s   17.03s   -1%   | 15->16 us     | 1903->1883 us | 3.67->3.74s

OK, so for read_ahead_kb >= 256k the difference disappears with your disk.

> SQLite Q2 full-table scan — 6.2 GB:
> 
>   kb    | OFF avg  ON avg   diff  | overhead (usr+sys)
>   128   | 24.51s   20.48s   -16%  | +0.95s
>   256   | 20.05s   16.60s   -17%  | +1.26s
>   512   | 15.31s   12.18s   -20%  | +1.06s
>   1024  | 12.22s   12.09s   -1%   | +0.26s

Interesting that here the difference disappears only with read_ahead_kb =
1024k. Isn't it because based on 2. above your algorithm disabled itself?
Also how come when your algorithm is enabled, results were still improving
as you were increasing read_ahead_kb?

> random4k — 100k random 4k requests:
> 
>   kb    | OFF avg  ON avg   diff  | p99
>   128   | 25.0s    25.5s    +2%   | ~0.98ms
>   256   | 26.9s    26.0s    -3%   | ~0.99ms
>   512   | 25.7s    25.5s    -1%   | ~0.98ms
>   1024  | 25.4s    24.7s    -3%   | ~0.99ms
> 
> random_repeat — 100k offsets x 3 passes, n=300K:
> 
>   kb    | OFF avg  ON avg   diff  | p99
>   128   | 26.8s    25.1s    -6%   | 0.39ms
>   256   | 25.1s    24.0s    -4%   | 0.38ms
>   512   | 24.5s    24.4s    0%    | 0.37ms
>   1024  | 25.0s    24.7s    -1%   | 0.39ms

I'd say no matterial difference here with read_ahead_kb >= 256k

> Honza, you asked whether the difference goes away at 1M — and yes, it
> does, for both workloads. The 128k default is the dominant factor; at
> 1M the kernel alone already reaches this SSD's bandwidth ceiling, and
> NSD recognises that and silences itself entirely at >= 1M windows.
> 
> But the 512k row is the one I'd really like your view on. The kernel
> with a 512k window alone (15.31s) trails the kernel with 1M (12.22s)
> by about 25%, yet NSD with a 512k window reaches 12.18s — the same
> throughput at half the window. Our block-layer traces show OFF@512k
> keeps issuing ~512k requests, so the device is not the limit. It looks
> like the kernel's window growth is capped at ra_pages, while the
> fadvise-driven expansion NSD triggers is not — which, incidentally, is
> why distros end up hand-tuning this value. Is that cap deliberate, for
> latency or cache-pollution control, or is there room for the growth
> policy to self-adapt?

I'm not sure I correctly understand the question so please rephrase it if I
answer something else but it is deliberate, that explicit readahead (e.g.
through fadvise(2) but also if you just do large read(2)) is not bound by
read_ahead_kb.

> Willy, on the architecture: hooking only vfs_read and skipping the
> fault path was a deliberate choice to bound the first iteration, but
> you're right that it's a gap — filemap_fault bypasses vfs_read
> entirely, so we're currently blind to mmap'd workloads. Hooking it
> with the same fprobe is the next step. On replacing the PG_readahead
> folio marking: the matrix above convinced us this is the right
> direction. The kernel's growth policy, not the device, is the real
> bottleneck at default settings, so we're moving toward a design that
> manages ra_state directly and measures its own waste — per-file
> tracking of consumed versus expired prefetches with adaptive
> aggressiveness, which is really just replacing the implicit
> assumptions behind PG_readahead marking with explicit measurement.
> Backward access patterns are already handled by the stride predictor
> (negative deltas, forward/backward counted separately); a dedicated
> backward-pattern benchmark will be added to the suite.

Just one note here: I wouldn't focus too much on default settings because
most people tune them to different values with udev rules. What is
interesting is whether your algorithm beats current readahead algorithm
with proper read_ahead_kb for the device (i.e., usually 1m these days).

> Two things we're still working on, for honesty: at 256-512k the
> sequential p99 regresses (~0.8ms -> ~1.1ms and ~1.1ms -> ~1.9ms) as
> the prefetch regions push the device queue — the usage-tracking work
> above targets exactly that. And the random_repeat gains (4-6% at
> 128-256k) suggest the predictor does help beyond what window size can
> fix; p95 there improves from 296 to 243 us.
> 
> Questions we'd value your input on:
>   1. Is fadvise-driven expansion beyond ra_pages acceptable as a
>      mechanism, or should the growth policy itself change?

Not sure what Matthew thinks but my opinion is that automated readahead
(regardless of algorithm used for deciding it) simply should be limited to
read_ahead_kb. That is the long-term Linux behavior and that is the knob
sysadmin has to constrain readahead algorithm if he wishes and we should
not break it.

>   2. What would a minimal integrated design look like — replacing
>      PG_readahead marking with direct ra_state control?

I'll defer this to Matthew since I'm actually not sure how he imagined this
to work.

>   3. If we iterate: fault path first, or usage-tracking first?

I don't have a strong preference here :)

								Honza
-- 
Jan Kara <jack@suse.com>
SUSE Labs, CR

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
       [not found]               ` <<o5ihhfa252cjfspqlag65xhnmehz3a5feyclmlyy4xlrcmceck@njmhyu7oom3d>
@ 2026-09-20 22:39                 ` ayhan aydın
  2026-09-21 12:36                   ` Pedro Falcato
  0 siblings, 1 reply; 16+ messages in thread
From: ayhan aydın @ 2026-09-20 22:39 UTC (permalink / raw)
  To: Jan Kara; +Cc: Matthew Wilcox, linux-kernel, linux-mm, linux-fsdevel

Hi Honza, hi Willy,

First, my Aug 20 follow-up bounced on the HTML part. My mistake, resending
as plain text. Below is that content updated with six more weeks of
measurement. Two self corrections first, before any numbers.

What changed since the RFC (same out of tree module):
Automated prefetch never exceeds ra_pages now. Overhangs get trimmed
or dropped, all counted. The only exception is our own measured
willneed ranges up to io_pages. That follows Ming Lei's RFC direction
and the MADV_POPULATE precedent. It is toggleable, Mod A means strict cap.

Fault path first, as you asked in Q3. filemap_fault hook observes, then
prefetches with depth capped at 2 and bounded by the cap. One more
thing on this: the fault path stands down explicitly at 1M windows and
up. There is a straight s_bdi->ra_pages check in nsd_process(). The
  parity we see at 256K and 512K is a different thing. Our code still
  fires there. The kernel just catches up on its own at those sizes.

A sleeping in atomic bug in our fast path got found through a stack
trace and fixed. vfs_fadvise was called from fprobe context. It now
goes through a workqueue.

A len==0 fadvise footgun got found through our own counters. Len 0
means to end of file, so those sends preloaded whole files. All our
earlier big mmap and SQLite speedups traced back to it. Retracted.
Sends with len 0 are now dropped and counted.

Narrowed claims after all that. Interleaved A/B, drop_caches before
every run, checksums verified, 500+ runs in total:

  sparse mmap scan, about 64KB stride, 512MB to 20GB files, 128K window,
  SSD: ON ~47s vs OFF ~88s median over 30+30 reps, no overlap.
    The threshold lines up exactly with FAULT_AROUND_ORDER=4 (65536B).
    Below it the kernel fault around covers the fault by itself.
    Above it, it does not.
  same setup at 256K, 512K, 1024K windows: parity, kernel catches up.
  dense mmap, rand 4K, seq read, SQLite, THP, and HDD sparse mmap:
  parity everywhere. The early small HDD lean got remeasured with
  current code and traced to the same len 0 accident, retracted too.
  No workload regresses. dmesg tracked throughout, no new warnings.

So the scope is sparse mmap scans at default size windows on
SSD. That is where the fault path underprovisions while the read path
is already saturated. Dense, random, 1M tuned and HDD cases are parity
by measurement.
Separately, one thing that might be useful here too: p99 tail latency
is about 4x worse at 1M than at 128K regardless of NSD state. Another
practical reason to keep windows small.

Open questions, updated:
  Q1, fadvise beyond ra_pages: we stopped doing it silently. Only
      measured willneed ranges up to io_pages, and that can be turned
      off. Acceptable shape?
  Q2, ra_state integration: we prototyped window steering from the
      module. The kernel keeps our window but wall clock does not move
      because there is just not enough volume from outside. Real
      integration needs in kernel changes, so we drafted a minimal one:
      2 stream slots plus stride state in file_ra_state, every window
      bounded by ra_pages, read path only. Want me to post it separately
      for direction feedback before any boot testing?
  Q3: fault path first as described above, with timely versus correct
      accounting running underneath.

For Willy: backward patterns get detected near perfectly (strides with
negative deltas, counted separately) but they do not move the wall on
SATA SSD, so throttled now. Fault path is covered as you asked.
PG_readahead replacement stays an in kernel question as above.

Full data and the retraction notes are in the tree. Happy to run any
counter matrix you ask for.

Thanks,
Ayhan

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-09-20 22:39                 ` ayhan aydın
@ 2026-09-21 12:36                   ` Pedro Falcato
       [not found]                     ` <<arEkgM15qewERAOJ@pedro-suse.tail5790ac.ts.net>
  0 siblings, 1 reply; 16+ messages in thread
From: Pedro Falcato @ 2026-09-21 12:36 UTC (permalink / raw)
  To: ayhan aydın
  Cc: Jan Kara, Matthew Wilcox, linux-kernel, linux-mm, linux-fsdevel

Can you please reply with something that isn't copied straight out of an LLM?
And, ideally, add the Assisted-by: LLM's where necessary.

On Sun, Sep 20, 2026 at 03:39:20PM -0700, ayhan aydın wrote:
> Hi Honza, hi Willy,
> 
> First, my Aug 20 follow-up bounced on the HTML part. My mistake, resending
> as plain text. Below is that content updated with six more weeks of
> measurement. Two self corrections first, before any numbers.
> 
> What changed since the RFC (same out of tree module):
> Automated prefetch never exceeds ra_pages now. Overhangs get trimmed
> or dropped, all counted. The only exception is our own measured
> willneed ranges up to io_pages. That follows Ming Lei's RFC direction
> and the MADV_POPULATE precedent. It is toggleable, Mod A means strict cap.
> 
> Fault path first, as you asked in Q3. filemap_fault hook observes, then
> prefetches with depth capped at 2 and bounded by the cap. One more
> thing on this: the fault path stands down explicitly at 1M windows and
> up. There is a straight s_bdi->ra_pages check in nsd_process(). The
>   parity we see at 256K and 512K is a different thing. Our code still
>   fires there. The kernel just catches up on its own at those sizes.
> 
> A sleeping in atomic bug in our fast path got found through a stack
> trace and fixed. vfs_fadvise was called from fprobe context. It now
> goes through a workqueue.
> 
> A len==0 fadvise footgun got found through our own counters. Len 0
> means to end of file, so those sends preloaded whole files. All our
> earlier big mmap and SQLite speedups traced back to it. Retracted.
> Sends with len 0 are now dropped and counted.
> 
> Narrowed claims after all that. Interleaved A/B, drop_caches before
> every run, checksums verified, 500+ runs in total:
> 
>   sparse mmap scan, about 64KB stride, 512MB to 20GB files, 128K window,
>   SSD: ON ~47s vs OFF ~88s median over 30+30 reps, no overlap.
>     The threshold lines up exactly with FAULT_AROUND_ORDER=4 (65536B).
>     Below it the kernel fault around covers the fault by itself.
>     Above it, it does not.
>   same setup at 256K, 512K, 1024K windows: parity, kernel catches up.
>   dense mmap, rand 4K, seq read, SQLite, THP, and HDD sparse mmap:
>   parity everywhere. The early small HDD lean got remeasured with
>   current code and traced to the same len 0 accident, retracted too.
>   No workload regresses. dmesg tracked throughout, no new warnings.
> 
> So the scope is sparse mmap scans at default size windows on
> SSD. That is where the fault path underprovisions while the read path
> is already saturated. Dense, random, 1M tuned and HDD cases are parity
> by measurement.
> Separately, one thing that might be useful here too: p99 tail latency
> is about 4x worse at 1M than at 128K regardless of NSD state. Another
> practical reason to keep windows small.
> 
> Open questions, updated:
>   Q1, fadvise beyond ra_pages: we stopped doing it silently. Only
>       measured willneed ranges up to io_pages, and that can be turned
>       off. Acceptable shape?
>   Q2, ra_state integration: we prototyped window steering from the
>       module. The kernel keeps our window but wall clock does not move
>       because there is just not enough volume from outside. Real
>       integration needs in kernel changes, so we drafted a minimal one:
>       2 stream slots plus stride state in file_ra_state, every window
>       bounded by ra_pages, read path only. Want me to post it separately
>       for direction feedback before any boot testing?
>   Q3: fault path first as described above, with timely versus correct
>       accounting running underneath.
> 
> For Willy: backward patterns get detected near perfectly (strides with
> negative deltas, counted separately) but they do not move the wall on
> SATA SSD, so throttled now. Fault path is covered as you asked.
> PG_readahead replacement stays an in kernel question as above.
> 
> Full data and the retraction notes are in the tree. Happy to run any
> counter matrix you ask for.
> 
> Thanks,
> Ayhan
> 

-- 
Pedro

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
       [not found]                     ` <<arEkgM15qewERAOJ@pedro-suse.tail5790ac.ts.net>
@ 2026-09-21 13:28                       ` ayhan aydın
  2026-10-11  9:23                         ` ayhan aydın
  0 siblings, 1 reply; 16+ messages in thread
From: ayhan aydın @ 2026-09-21 13:28 UTC (permalink / raw)
  To: Pedro Falcato
  Cc: Jan Kara, Matthew Wilcox, linux-kernel, linux-mm, linux-fsdevel

Thanks for the heads up, Pedro, fair point. I had an LLM help draft
these mails and it shows. I will write them myself from here on, and
noted on Assisted by, will add it wherever one was involved.

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

* Re: [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher
  2026-09-21 13:28                       ` ayhan aydın
@ 2026-10-11  9:23                         ` ayhan aydın
  0 siblings, 0 replies; 16+ messages in thread
From: ayhan aydın @ 2026-10-11  9:23 UTC (permalink / raw)
  To: Jan Kara, Matthew Wilcox
  Cc: Pedro Falcato, linux-kernel, linux-mm, linux-fsdevel

Hi Honza, Willy,

Just checking if the Sep 20 note got buried. No new numbers
from my side, same tree.

The two things I am unsure about:

1. Is the capped shape OK - ra_pages bounded, overhangs
trimmed/dropped and counted, only measured willneed up to
io_pages and that off by default (Mod A strict)?

2. Do you want the small ra_state bit (2 slots + stride,
read path only, all windows bounded) posted separately
for direction before I test it further?

Thanks,
Ayhan
Assisted-by: LLM (English draft help)

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

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

Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-25 18:26 [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
2026-07-25 18:26 ` [RFC PATCH 1/3] mm/filemap: Add NSD prefetch hook point Ayhan Aydin
2026-07-25 18:26 ` [RFC PATCH 2/3] nsd: Core prediction engine Ayhan Aydin
2026-07-25 18:26 ` [RFC PATCH 3/3] Documentation: Add NSD filesystem documentation Ayhan Aydin
2026-08-08 21:10 ` [RFC PATCH 0/3] Neural Storage Driver - learning page cache prefetcher Ayhan Aydin
2026-08-10  3:13   ` Matthew Wilcox
2026-08-10 20:19     ` Ayhan Aydin
2026-08-17 16:22     ` Jan Kara
     [not found]       ` <nsd.1787001235.35928@mail.gmail.com>
2026-08-19 10:38         ` Jan Kara
2026-08-20 12:26           ` ayhan aydın
2026-09-02 17:24             ` Jan Kara
     [not found]               ` <<o5ihhfa252cjfspqlag65xhnmehz3a5feyclmlyy4xlrcmceck@njmhyu7oom3d>
2026-09-20 22:39                 ` ayhan aydın
2026-09-21 12:36                   ` Pedro Falcato
     [not found]                     ` <<arEkgM15qewERAOJ@pedro-suse.tail5790ac.ts.net>
2026-09-21 13:28                       ` ayhan aydın
2026-10-11  9:23                         ` ayhan aydın
2026-09-01 20:51           ` ayhan aydın

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®