Recent changes (master)

Jens Axboe <[email protected]> Sat, 23 Aug 2025 06:00:01 -0600 (MDT)
Newsgroups org.kernel.vger.fio
Message-ID <[email protected]>
The following changes since commit ac2aa2ca02dec925fa05cb9e9d4a1cd25e78ae84:

  Kill of IO engine cancelation support (2025-08-21 16:23:59 -0600)

are available in the Git repository at:

  git://git.kernel.dk/fio.git master

for you to fetch changes up to 1fb2d4acde3d7e205cf941167c7efad49baf525c:

  verify: use new buffer for threads with %o format (2025-08-22 13:57:09 -0400)

----------------------------------------------------------------
Tomas Winkler (13):
      sprandom: add command line options
      sprandom: add debug facility
      sprandom: examples: add sprandom example file
      sprandom: implement region computation and invalidation percentage
      sprandom: set up LFSR random generator and disable randommap
      num2str: add bytes2str_simple()
      unittests: add bytes2str_simple()
      sprandom: pcbuf.h add two-phase circular buffer header-only library
      unittests: add pcbuf simple unit test
      sprandom: initialize random state
      sprandom: implement sprandom_get_next_offset()
      sprandom: initialize sprandom for file
      sprandom: integrate sprandom_get_next_offset() into io_u path

Vincent Fu (3):
      Merge branch 'sprandom' of https://github.com/tomas-winkler-sndk/fio
      sprandom: abort when invalid options specified
      verify: use new buffer for threads with %o format

 HOWTO.rst               |  31 ++
 Makefile                |   4 +-
 cconv.c                 |   6 +
 debug.h                 |   1 +
 examples/sprandom.fio   |  41 +++
 file.h                  |   3 +
 filesetup.c             |  13 +
 fio.1                   |  28 ++
 fio.h                   |   2 +
 init.c                  |  35 ++
 io_u.c                  |  21 +-
 lib/num2str.c           |  32 +-
 lib/num2str.h           |   2 +
 options.c               |  37 +++
 pcbuf.h                 | 211 ++++++++++++
 server.h                |   2 +-
 sprandom.c              | 835 ++++++++++++++++++++++++++++++++++++++++++++++++
 sprandom.h              |  78 +++++
 thread_options.h        |   6 +
 unittests/lib/num2str.c |  35 ++
 unittests/lib/pcbuf.c   | 116 +++++++
 unittests/unittest.c    |   1 +
 unittests/unittest.h    |   1 +
 verify.c                |   2 +-
 24 files changed, 1538 insertions(+), 5 deletions(-)
 create mode 100644 examples/sprandom.fio
 create mode 100644 pcbuf.h
 create mode 100644 sprandom.c
 create mode 100644 sprandom.h
 create mode 100644 unittests/lib/pcbuf.c

---

Diff of recent changes:

diff --git a/HOWTO.rst b/HOWTO.rst
index 3eb0d9fe..3a4e018f 100644
--- a/HOWTO.rst
+++ b/HOWTO.rst
@@ -1632,6 +1632,37 @@ I/O type
 	space exceeds 2^32 blocks. If it does, then **tausworthe64** is
 	selected automatically.
 
+.. option:: sprandom=bool
+
+
+        SPRandom is a method designed to rapidly precondition SSDs for
+        steady-state random write workloads. It divides the device into
+        equally sized regions and writes the device's entire physical capacity
+        once, selecting offsets so that the regions have a distribution of
+        invalid blocks matching the distribution that occurs at steady state.
+
+        Default: false.
+
+        It uses **random_generator=lfsr**, which fio will set by default.
+        Selecting any other random generator will result in an error.
+
+
+.. option:: spr_num_regions=int
+
+	See :option:`sprandom`. Specifies the number of regions used for SPRandom.
+        For large devices it is better to use more regions, to increase precision
+        and reduce memory allocation. The allocation is proportional to the region size.
+
+        Default=100
+
+
+.. option:: spr_op=float
+
+	See :option:`sprandom`. Over-provisioning ratio in the range (0, 1),
+        as specified by the SSD manufacturer.
+
+        Default=0.15
+
 
 Block size
 ~~~~~~~~~~
diff --git a/Makefile b/Makefile
index ec6249f3..ba7ad6dc 100644
--- a/Makefile
+++ b/Makefile
@@ -62,7 +62,8 @@ SOURCE :=	$(sort $(patsubst $(SRCDIR)/%,%,$(wildcard $(SRCDIR)/crc/*.c)) \
 		gettime-thread.c helpers.c json.c idletime.c td_error.c \
 		profiles/tiobench.c profiles/act.c io_u_queue.c filelock.c \
 		workqueue.c rate-submit.c optgroup.c helper_thread.c \
-		steadystate.c zone-dist.c zbd.c dedupe.c dataplacement.c
+		steadystate.c zone-dist.c zbd.c dedupe.c dataplacement.c \
+		sprandom.c
 
 ifdef CONFIG_LIBHDFS
   HDFSFLAGS= -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/linux -I $(FIO_LIBHDFS_INCLUDE)
@@ -448,6 +449,7 @@ UT_OBJS = unittests/unittest.o
 UT_OBJS += unittests/lib/memalign.o
 UT_OBJS += unittests/lib/num2str.o
 UT_OBJS += unittests/lib/strntol.o
+UT_OBJS += unittests/lib/pcbuf.o
 UT_OBJS += unittests/oslib/strlcat.o
 UT_OBJS += unittests/oslib/strndup.o
 UT_OBJS += unittests/oslib/strcasestr.o
diff --git a/cconv.c b/cconv.c
index 4e72ae16..e7bbfc53 100644
--- a/cconv.c
+++ b/cconv.c
@@ -228,6 +228,9 @@ int convert_thread_options_to_cpu(struct thread_options *o,
 	o->job_start_clock_id = le32_to_cpu(top->job_start_clock_id);
 	o->norandommap = le32_to_cpu(top->norandommap);
 	o->softrandommap = le32_to_cpu(top->softrandommap);
+	o->sprandom = le32_to_cpu(top->sprandom);
+	o->spr_num_regions = le32_to_cpu(top->spr_num_regions);
+	o->spr_over_provisioning.u.f = fio_uint64_to_double(le64_to_cpu(top->spr_over_provisioning.u.i));
 	o->bs_unaligned = le32_to_cpu(top->bs_unaligned);
 	o->fsync_on_close = le32_to_cpu(top->fsync_on_close);
 	o->bs_is_seq_rand = le32_to_cpu(top->bs_is_seq_rand);
@@ -476,6 +479,9 @@ void convert_thread_options_to_net(struct thread_options_pack *top,
 	top->job_start_clock_id = cpu_to_le32(o->job_start_clock_id);
 	top->norandommap = cpu_to_le32(o->norandommap);
 	top->softrandommap = cpu_to_le32(o->softrandommap);
+	top->sprandom = cpu_to_le32(o->sprandom);
+	top->spr_num_regions = cpu_to_le32(o->spr_num_regions);
+	top->spr_over_provisioning.u.i = __cpu_to_le64(fio_double_to_uint64(o->spr_over_provisioning.u.f));
 	top->bs_unaligned = cpu_to_le32(o->bs_unaligned);
 	top->fsync_on_close = cpu_to_le32(o->fsync_on_close);
 	top->bs_is_seq_rand = cpu_to_le32(o->bs_is_seq_rand);
diff --git a/debug.h b/debug.h
index 51b18de2..49a8791d 100644
--- a/debug.h
+++ b/debug.h
@@ -23,6 +23,7 @@ enum {
 	FD_STEADYSTATE,
 	FD_HELPERTHREAD,
 	FD_ZBD,
+	FD_SPRANDOM,
 	FD_DEBUG_MAX,
 };
 
diff --git a/examples/sprandom.fio b/examples/sprandom.fio
new file mode 100644
index 00000000..b94b226e
--- /dev/null
+++ b/examples/sprandom.fio
@@ -0,0 +1,41 @@
+; (SPRandom) SanDisk Random preconditioning example
+; Requirements
+;  1. Single file
+;  2. Single job (numjobs=1)
+;  3. Assumes norandommap=1
+;  4. Assumes random_generator=lfsr
+;
+; FIO_BS should be set to driver indirection unit (IU) size.
+; IU is the smallest unit of data that can be mapped from a LBA
+; on the host to a physical location on the SSD's flash memory.
+;
+; Basic execution example, run with io_uring
+; env FIO_BS=4096 \
+; fio --filename=/dev/nvme0n1 --ioengine=io_uring examples/sprandom.fio
+;
+; Enable debug output for the 'sprandom' module
+; env FIO_BS=4096 \
+; fio --debug=sprandom --filename=/dev/nvme0n1 examples/sprandom.fio
+;
+; Set over-provisioning according to vendor recommendation (21%)
+; env FIO_BS=4096 \
+; fio --spr_op=0.21 --filename=/dev/nvme0n1 examples/sprandom.fio
+;
+; For large devices it is better to use more regions, to increase precision
+; and reduce memory allocation. The allocation is proportional to the region size.
+; env FIO_BS=4096 \
+; fio --spr_num_regions=400 --filename=/dev/nvme0n1 examples/sprandom.fio
+;
+[global]
+ioengine=libaio
+rw=randwrite
+bs=${FIO_BS}
+blockalign=${FIO_BS}
+direct=1
+norandommap=1
+iodepth=64
+[preconditioning]
+sprandom=1
+spr_op=0.15
+spr_num_regions=100
+
diff --git a/file.h b/file.h
index 8fd40cdf..f400155f 100644
--- a/file.h
+++ b/file.h
@@ -113,6 +113,9 @@ struct fio_file {
 	uint32_t min_zone;	/* inclusive */
 	uint32_t max_zone;	/* exclusive */
 
+	/* SP Random Info */
+	struct sprandom_info *spr_info;
+
 	/*
 	 * Track last end and last start of IO for a given data direction
 	 */
diff --git a/filesetup.c b/filesetup.c
index bcbd871e..597bf4c5 100644
--- a/filesetup.c
+++ b/filesetup.c
@@ -15,6 +15,7 @@
 #include "lib/axmap.h"
 #include "rwlock.h"
 #include "zbd.h"
+#include "sprandom.h"
 
 #ifdef CONFIG_LINUX_FALLOCATE
 #include <linux/falloc.h>
@@ -1401,6 +1402,16 @@ done:
 			goto err_out;
 	}
 
+	if (td->o.sprandom) {
+		if (td->o.nr_files != 1) {
+			 log_err("fio: SPRandom supports only one file");
+			 goto err_out;
+		}
+		err = sprandom_init(td, td->files[0]);
+		if (err)
+			goto err_out;
+	}
+
 	if (o->create_only)
 		td->done = 1;
 
@@ -1590,6 +1601,8 @@ void fio_file_free(struct fio_file *f)
 		axmap_free(f->io_axmap);
 	if (f->ruhs_info)
 		sfree(f->ruhs_info);
+	if (f->spr_info)
+		sprandom_free(f->spr_info);
 	if (!fio_file_smalloc(f)) {
 		free(f->file_name);
 		free(f);
diff --git a/fio.1 b/fio.1
index 6aa23f7d..d9f4fd15 100644
--- a/fio.1
+++ b/fio.1
@@ -1440,6 +1440,34 @@ multiple times. The default value is \fBtausworthe\fR, unless the required
 space exceeds 2^32 blocks. If it does, then \fBtausworthe64\fR is
 selected automatically.
 .RE
+.TP
+.B sprandom=bool
+SPRandom is a method designed to rapidly precondition SSDs for
+steady-state random write workloads. It divides the device into
+equally sized regions and writes the device's entire physical capacity
+once, selecting offsets so that the regions have a distribution of
+invalid blocks matching the distribution that occurs at steady state.
+Default: false.
+
+It uses \fBrandom_generator=lfsr\fR, which fio will set by default.
+Selecting any other random generator will result in an error.
+.TP
+.B spr_num_regions=int
+See
+.BR sprandom .
+Specifies the number of regions used for SPRandom. Default=100
+.P
+.RS
+For large devices it is better to use more regions, to increase precision
+and reduce memory allocation. The allocation is proportional to the region size.
+.RE
+.TP
+.B spr_op=float
+See
+.BR sprandom .
+Over-provisioning ratio in the range (0, 1), as specified by the SSD manufacturer.
+The default is 0.15.
+.RE
 .SS "Block size"
 .TP
 .BI blocksize \fR=\fPint[,int][,int] "\fR,\fB bs" \fR=\fPint[,int][,int]
diff --git a/fio.h b/fio.h
index e11b9261..44788899 100644
--- a/fio.h
+++ b/fio.h
@@ -157,6 +157,7 @@ enum {
 	FIO_RAND_PRIO_CMDS,
 	FIO_RAND_DEDUPE_WORKING_SET_IX,
 	FIO_RAND_FDP_OFF,
+	FIO_RAND_SPRANDOM_OFF,
 	FIO_RAND_NR_OFFS,
 };
 
@@ -286,6 +287,7 @@ struct thread_data {
 	struct frand_state prio_state;
 	struct frand_state dedupe_working_set_index_state;
 	struct frand_state *dedupe_working_set_states;
+	struct frand_state sprandom_state;
 
 	unsigned long long num_unique_pages;
 
diff --git a/init.c b/init.c
index 20f5462d..cf66ac2c 100644
--- a/init.c
+++ b/init.c
@@ -690,6 +690,36 @@ static int fixup_options(struct thread_data *td)
 	if (o->zone_mode == ZONE_MODE_STRIDED && !o->zone_range)
 		o->zone_range = o->zone_size;
 
+	/*
+	 * SPRandom Requires: random write, random_generator=lfsr, norandommap=1
+	 */
+	if (o->sprandom) {
+		if (td_write(td) && td_random(td)) {
+			if (fio_option_is_set(o, random_generator)) {
+				if (o->random_generator != FIO_RAND_GEN_LFSR) {
+					log_err("fio: sprandom requires random_generator=lfsr\n");
+					ret |= 1;
+				}
+			} else {
+				log_info("fio: sprandom sets random_generator=lfsr\n");
+				o->random_generator = FIO_RAND_GEN_LFSR;
+			}
+			if (fio_option_is_set(o, norandommap)) {
+				if (o->norandommap == 0) {
+					log_err("fio: sprandom requires norandommap=1\n");
+					ret |= 1;
+				}
+				/* if == 1, OK */
+			} else {
+				log_info("fio: sprandom sets norandommap=1\n");
+				o->norandommap = 1;
+			}
+		} else {
+			log_err("fio: sprandom requires random write, random_generator=lfsr, norandommap=1");
+			ret |= 1;
+		}
+	}
+
 	/*
 	 * Reads can do overwrites, we always need to pre-create the file
 	 */
@@ -1165,6 +1195,7 @@ void td_fill_rand_seeds(struct thread_data *td)
 	frand_copy(&td->buf_state_prev, &td->buf_state);
 
 	init_rand_seed(&td->fdp_state, td->rand_seeds[FIO_RAND_FDP_OFF], use64);
+	init_rand_seed(&td->sprandom_state, td->rand_seeds[FIO_RAND_SPRANDOM_OFF], false);
 }
 
 static int setup_random_seeds(struct thread_data *td)
@@ -2470,6 +2501,10 @@ const struct debug_level debug_levels[] = {
 	  .help = "Zoned Block Device logging",
 	  .shift = FD_ZBD,
 	},
+	{ .name = "sprandom",
+	  .help = "SPRandom logging",
+	  .shift = FD_SPRANDOM,
+	},
 	{ .name = NULL, },
 };
 
diff --git a/io_u.c b/io_u.c
index 78dbac9c..ab1bf7c9 100644
--- a/io_u.c
+++ b/io_u.c
@@ -11,6 +11,7 @@
 #include "lib/pow2.h"
 #include "minmax.h"
 #include "zbd.h"
+#include "sprandom.h"
 
 struct io_completion_data {
 	int nr;				/* input */
@@ -84,6 +85,22 @@ static uint64_t last_block(struct thread_data *td, struct fio_file *f,
 	return max_blocks;
 }
 
+
+static int __get_next_rand_offset_sprandom(struct thread_data *td, struct fio_file *f,
+					   enum fio_ddir ddir, uint64_t *b,
+					   uint64_t lastb)
+{
+	assert(ddir == DDIR_WRITE);
+
+	/* SP RANDOM writes all addresses once */
+	if (sprandom_get_next_offset(f->spr_info, f, b)) {
+		dprint(FD_SPRANDOM, "sprandom is done\n");
+		td->done = 1;
+		return 1;
+	}
+	return 0;
+}
+
 static int __get_next_rand_offset(struct thread_data *td, struct fio_file *f,
 				  enum fio_ddir ddir, uint64_t *b,
 				  uint64_t lastb)
@@ -277,7 +294,9 @@ bail:
 static int get_next_rand_offset(struct thread_data *td, struct fio_file *f,
 				enum fio_ddir ddir, uint64_t *b)
 {
-	if (td->o.random_distribution == FIO_RAND_DIST_RANDOM) {
+	if (td->o.sprandom && ddir == DDIR_WRITE) {
+		return __get_next_rand_offset_sprandom(td, f, ddir, b, 0);
+	} else if (td->o.random_distribution == FIO_RAND_DIST_RANDOM) {
 		uint64_t lastb;
 
 		lastb = last_block(td, f, ddir);
diff --git a/lib/num2str.c b/lib/num2str.c
index cd89a0e5..e48a483c 100644
--- a/lib/num2str.c
+++ b/lib/num2str.c
@@ -7,6 +7,37 @@
 #include "../oslib/asprintf.h"
 #include "num2str.h"
 
+
+static const char *iecstr[] = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei" };
+
+/**
+ * bytes2str_simple - Converts a byte value to a human-readable string.
+ * @buf:      buffer to store the resulting string
+ * @bufsize:  size of the buffer
+ * @bytes:    number of bytes to convert
+ * @returns : pointer to the buf containing the formatted string.
+ * Converts the given byte value into a human-readable string using IEC units
+ * (e.g., KiB, MiB, GiB), and stores the result in the provided buffer.
+ * The output is formatted with two decimal places of precision.
+ */
+const char *bytes2str_simple(char *buf, size_t bufsize, uint64_t bytes)
+{
+	int unit = 0;
+	double size = (double)bytes;
+
+	buf[0] = '\0';
+
+	while (size >= 1024.0 && unit < FIO_ARRAY_SIZE(iecstr) - 1) {
+		size /= 1024.0;
+		unit++;
+	}
+
+	snprintf(buf, bufsize, "%.2f %sB", size, iecstr[unit]);
+
+	return buf;
+}
+
+
 /**
  * num2str() - Cheesy number->string conversion, complete with carry rounding error.
  * @num: quantity (e.g., number of blocks, bytes or bits)
@@ -19,7 +50,6 @@
 char *num2str(uint64_t num, int maxlen, int base, int pow2, enum n2s_unit units)
 {
 	const char *sistr[] = { "", "k", "M", "G", "T", "P", "E" };
-	const char *iecstr[] = { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei" };
 	const char **unitprefix;
 	static const char *const unitstr[] = {
 		[N2S_NONE]	= "",
diff --git a/lib/num2str.h b/lib/num2str.h
index 797288b5..8ca2050a 100644
--- a/lib/num2str.h
+++ b/lib/num2str.h
@@ -14,4 +14,6 @@ enum n2s_unit {
 
 extern char *num2str(uint64_t, int, int, int, enum n2s_unit);
 
+extern const char *bytes2str_simple(char *buf, size_t bufsize, uint64_t bytes);
+
 #endif
diff --git a/options.c b/options.c
index 6295a616..337a3a52 100644
--- a/options.c
+++ b/options.c
@@ -2628,6 +2628,43 @@ struct fio_option fio_options[FIO_MAX_OPTS] = {
 		.category = FIO_OPT_C_IO,
 		.group	= FIO_OPT_G_RANDOM,
 	},
+	{
+		.name	= "sprandom",
+		.lname	= "Sandisk Pseudo Random Preconditioning",
+		.type	= FIO_OPT_BOOL,
+		.off1	= offsetof(struct thread_options, sprandom),
+		.help	= "Set up Sandisk Pseudo Random Preconditioning",
+		.parent	= "rw",
+		.hide	= 1,
+		.def	= "0",
+		.category = FIO_OPT_C_IO,
+		.group	= FIO_OPT_G_RANDOM,
+	},
+	{
+		.name	= "spr_num_regions",
+		.lname	= "SPRandom number of regions",
+		.type	= FIO_OPT_INT,
+		.off1	= offsetof(struct thread_options, spr_num_regions),
+		.help	= "Number of regions for sprandom",
+		.parent	= "sprandom",
+		.hide	= 1,
+		.def    = "100",
+		.category = FIO_OPT_C_IO,
+		.group	= FIO_OPT_G_RANDOM,
+	},
+	{
+		.name	= "spr_op",
+		.lname	= "SPRandom Over provisioning",
+		.type	= FIO_OPT_FLOAT_LIST,
+		.off1	= offsetof(struct thread_options, spr_over_provisioning),
+		.help	= "Over provisioning ratio for SPRandom",
+		.parent	= "sprandom",
+		.maxlen = 1,
+		.hide	= 1,
+		.def    = "0.15",
+		.category = FIO_OPT_C_IO,
+		.group	= FIO_OPT_G_RANDOM,
+	},
 	{
 		.name	= "random_generator",
 		.lname	= "Random Generator",
diff --git a/pcbuf.h b/pcbuf.h
new file mode 100644
index 00000000..df23b233
--- /dev/null
+++ b/pcbuf.h
@@ -0,0 +1,211 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0 only
+ *
+ * Copyright (c) 2025 Sandisk Corporation or its affiliates.
+ */
+/**
+ * Two-phase circular buffer implementation for producer/consumer separation.
+ *
+ * This header defines the data structures and inline functions for a two-phase
+ * circular buffer, allowing staged writes and explicit commit of data batches.
+ * Useful for double-buffered systems or scenarios requiring controlled visibility
+ * of produced data to consumers.
+ */
+#ifndef PHASE_CIRCULAR_BUFFER_H
+#define PHASE_CIRCULAR_BUFFER_H
+
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+
+/**
+ * struct pc_buf - Two-phase circular buffer.
+ * @commit_head:  Index of the next committed element in the buffer (visible to consumer).
+ * @staging_head: Index of the next staged (but not yet committed) element (written by producer).
+ * @read_tail:    Index of the next element to be read by the consumer.
+ * @capacity:     Total capacity of the buffer (number of elements).
+ * @buffer:       Buffer data.
+ *
+ * This structure implements a two-phase circular buffer, where data is first staged
+ * by advancing @staging_head, and only becomes visible to the consumer when @commit_head
+ * is explicitly updated. This allows for controlled commit of data batches, useful in
+ * double-buffered systems or producer/consumer separation.
+ */
+struct pc_buf {
+	uint64_t commit_head;
+	uint64_t staging_head;
+	uint64_t read_tail;
+	uint64_t capacity;
+	uint64_t buffer[];
+};
+
+/**
+ * pcb_alloc - Allocate and initialize buffer.
+ * @capacity: Number of elements the buffer can hold.
+ *
+ * Returns a pointer to the allocated buffer, or NULL on failure.
+ */
+static inline struct pc_buf *pcb_alloc(uint64_t capacity)
+{
+	size_t size = sizeof(struct pc_buf) + sizeof(uint64_t) * capacity;
+	struct pc_buf *cb = (struct pc_buf *)malloc(size);
+
+	if (!cb)
+		return NULL;
+	cb->commit_head = 0;
+	cb->staging_head = 0;
+	cb->read_tail = 0;
+	cb->capacity = capacity;
+	return cb;
+}
+
+/**
+ * pcb_is_empty - Check if the buffer is empty.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Returns true if the buffer has no committed data.
+ */
+static inline bool pcb_is_empty(const struct pc_buf *cb)
+{
+	return cb->read_tail == cb->commit_head;
+}
+
+/**
+ * pcb_is_full - Check if the buffer is full.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Returns true if the buffer cannot accept more staged data.
+ */
+
+static inline bool pcb_is_full(const struct pc_buf *cb)
+{
+	return ((cb->staging_head + 1) % cb->capacity) == cb->read_tail;
+}
+
+/**
+ * pcb_push_staged - Push a value into the staged buffer.
+ * @cb: pointer to the pc_buf structure.
+ * @value: value to be staged.
+ *
+ * Returns true if the value was successfully staged, false if the buffer is full.
+ */
+static inline bool pcb_push_staged(struct pc_buf *cb, uint64_t value)
+{
+	if (pcb_is_full(cb))
+		return false;
+
+	cb->buffer[cb->staging_head] = value;
+	cb->staging_head = (cb->staging_head + 1) % cb->capacity;
+	return true;
+}
+
+/**
+ * pcb_commit - Commit the staged data to make it visible to consumers.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Updates the commit head to the current staging head, making
+ * all staged data visible to consumers. It should be called after staging data.
+ */
+static inline void pcb_commit(struct pc_buf *cb)
+{
+	cb->commit_head = cb->staging_head;
+}
+
+/**
+ * pcb_pop - Pop a value from the committed buffer.
+ * @cb: pointer to the pc_buf structure.
+ * @out: pointer to the variable to store the popped value.
+ *
+ * Returns true if a value was successfully popped, false if the buffer is empty.
+ */
+static inline bool pcb_pop(struct pc_buf *cb, uint64_t *out)
+{
+	if (pcb_is_empty(cb))
+		return false;
+
+	*out = cb->buffer[cb->read_tail];
+	cb->read_tail = (cb->read_tail + 1) % cb->capacity;
+	return true;
+}
+
+/**
+ * pcb_print_committed - Print the contents of the committed buffer.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * This function prints all committed data in the buffer.
+ */
+static inline void pcb_print_committed(const struct pc_buf *cb)
+{
+	uint64_t i = cb->read_tail;
+
+	printf("Committed buffer: ");
+	while (i != cb->commit_head) {
+		printf("%" PRIu64 " ", cb->buffer[i]);
+		i = (i + 1) % cb->capacity;
+	}
+	printf("\n");
+}
+
+/**
+ * pcb_print_staged - Print the contents of the staged buffer.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * This function prints all staged data that has not yet been committed.
+ */
+static inline void pcb_print_staged(const struct pc_buf *cb)
+{
+	uint64_t i = cb->commit_head;
+
+	printf("Staged (not visible yet): ");
+	while (i != cb->staging_head) {
+		printf("%" PRIu64 " ", cb->buffer[i]);
+		i = (i + 1) % cb->capacity;
+	}
+	printf("\n");
+}
+
+/**
+ * pcb_committed_size - Get the size of committed data in the buffer.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Returns the number of elements that have been committed and are visible to consumers.
+ */
+static inline uint64_t pcb_committed_size(const struct pc_buf *cb)
+{
+	if (cb->commit_head >= cb->read_tail)
+		return cb->commit_head - cb->read_tail;
+	else
+		return cb->capacity - cb->read_tail + cb->commit_head;
+}
+
+/**
+ * pcb_staged_size - Get the size of staged data in the buffer.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Returns the number of elements that have been staged but not yet committed.
+ */
+static inline uint64_t pcb_staged_size(const struct pc_buf *cb)
+{
+	if (cb->staging_head >= cb->commit_head)
+		return cb->staging_head - cb->commit_head;
+	else
+		return cb->capacity - cb->commit_head + cb->staging_head;
+}
+
+/**
+ * pcb_space_available - Check if there is space available for staging.
+ * @cb: pointer to the pc_buf structure.
+ *
+ * Returns true if there is space available for staging new data, false if the buffer is full.
+ */
+static inline bool pcb_space_available(const struct pc_buf *cb)
+{
+	uint64_t used = pcb_committed_size(cb) + pcb_staged_size(cb);
+	/* keep 1 slot reserved to distinguish full from empty */
+	return used < (cb->capacity - 1);
+}
+
+#endif /* PHASE_CIRCULAR_BUFFER_H */
+
diff --git a/server.h b/server.h
index f0b15a22..5246af5c 100644
--- a/server.h
+++ b/server.h
@@ -51,7 +51,7 @@ struct fio_net_cmd_reply {
 };
 
 enum {
-	FIO_SERVER_VER			= 112,
+	FIO_SERVER_VER			= 113,
 
 	FIO_SERVER_MAX_FRAGMENT_PDU	= 1024,
 	FIO_SERVER_MAX_CMD_MB		= 2048,
diff --git a/sprandom.c b/sprandom.c
new file mode 100644
index 00000000..93e8609d
--- /dev/null
+++ b/sprandom.c
@@ -0,0 +1,835 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0 only
+ *
+ * Copyright (c) 2025 Sandisk Corporation or its affiliates.
+ */
+#include <stdlib.h>
+#include <stdint.h>
+#include <math.h>
+#include "lib/pow2.h"
+#include "fio.h"
+#include "file.h"
+#include "sprandom.h"
+
+/*
+ * Model for Estimating Steady-State Data Distribution in SSDs
+ *
+ * This model estimates the distribution of valid data across a flash drive
+ * in a steady state. It is based on the key insight from Desnoyers' research,
+ * which establishes a relationship between data validity and the physical
+ * space it occupies.
+ *
+ * P. Desnoyers, "Analytic Models of SSD Write Performance,"
+ * ACM Transactions on Storage,
+ * vol. 8, no. 2, pp. 1–18, Jun. 2012, doi: 10.1145/2133360.2133364.
+ *
+ * The Core Principle
+ * ==================
+ *
+ * The fundamental concept is that for a drive in a steady state, the product
+ * of a block's validity and the fraction of drive space occupied by such
+ * blocks is constant.
+ *
+ * Key Equation (1): i * f(i) = k
+ *
+ * Where:
+ * - i: The number of valid pages in a block.
+ * - f(i): The fraction of the drive composed of blocks with 'i' valid pages.
+ * - k: A constant for the drive.
+ *
+ * This implies that for any two validity levels i and j: i * f(i) = j * f(j).
+ * In other words, regions with lower validity (more invalid data) must
+ * occupy proportionally more physical space than regions with high validity.
+ *
+ *
+ * Modeling Steps
+ * ==============
+ * The model is built by following these steps:
+ *
+ * 1. Normalize Validity & Relate to Write Amplification (WA)
+ * We normalize 'i' into a validity fraction:
+ *
+ *      valid_frac(i) = i / num_pages_per_region
+ *
+ * A greedy garbage collection (GC) algorithm reclaims the block with the
+ * lowest validity. The validity of this GC block (`valid_frac_gc`) is
+ * determined by the drive's WA:
+ *
+ *      valid_frac_gc = 1 - (1 / WA)
+ *
+ * 2. Determine Write Amplification (WA) from Over-Provisioning (OP)
+ * The WA can be calculated from the drive's OP. A simple approximation
+ * is often sufficient for most cases:
+ *
+ *      WA ≈ 0.5 / OP + 0.7
+ *
+ * Note: The precise formula from Desnoyers uses
+ *      alpha = T/U
+ * where
+ *      OP = alpha - 1
+ *
+ * in the equation:
+ *                   alpha
+ *      WA = ----------------------------
+ *           (alpha + W(-alpha*e^-alpha)
+ *
+ * with W being the Lambert W function).
+ *
+ * 3. Define the Distribution Curve
+ *
+ * Using the steady-state principle, we can find the relative size f(i) of a
+ * region given its validity (`valid_frac_i`) by comparing it to the GC block.
+ *
+ *       valid_frac(i) * f(i) = valid_frac_gc * f_gc
+ *
+ * By defining the base size f_gc = 1, we get a simple relationship:
+ *
+ *       f(i) = valid_frac_gc / valid_frac(i)
+ *
+ * This formula defines a curve where points are spaced equally by validity.
+ *
+ * 4. Resample for Equal-Sized Regions
+ *
+ * The final step is to make the model practical. We take the curve defined
+ * above and resample it to get points that are equally spaced by region
+ * size f(i). This resampling gives the expected validity for each
+ * equal-sized region of the drive, completing the model.
+ */
+
+#define PCT_PRECISION 10000
+
+static inline double *d_alloc(size_t n)
+{
+	return calloc(n, sizeof(double));
+}
+
+struct point {
+	double x;
+	double y;
+};
+
+static inline struct point *p_alloc(size_t n)
+{
+	return calloc(n, sizeof(struct point));
+}
+
+static void print_d_array(const char *hdr, double *darray, size_t len)
+{
+	struct buf_output out;
+	int i;
+
+	buf_output_init(&out);
+
+	log_buf(&out, "[");
+	for (i = 0; i < len - 1; i++)
+		log_buf(&out, "%.2f, ", darray[i]);
+
+	log_buf(&out, "%.2f]\n", darray[len - 1]);
+	if (hdr)
+		dprint(FD_SPRANDOM, "%s: ", hdr);
+
+	dprint(FD_SPRANDOM, "%s", out.buf);
+	buf_output_free(&out);
+}
+
+static void print_d_points(struct point *parray, size_t len)
+{
+	struct buf_output out;
+	unsigned int i;
+
+	buf_output_init(&out);
+
+	log_buf(&out, "[");
+	for (i = 0; i < len - 1; i++)
+		log_buf(&out, "(%.2f %.2f), ", parray[i].x, parray[i].y);
+
+	log_buf(&out, "(%.2f %.2f)]\n", parray[len - 1].x, parray[len - 1].y);
+	dprint(FD_SPRANDOM, "%s", out.buf);
+	buf_output_free(&out);
+}
+
+/* Comparison function for qsort to sort points by x-value */
+static int compare_points(const void *a, const void *b)
+{
+	/* Cast void pointers to struct point pointers */
+	const struct point *point_a = (const struct point *)a;
+	const struct point *point_b = (const struct point *)b;
+
+	if (point_a->x < point_b->x)
+		return -1;
+
+	if (point_a->x > point_b->x)
+		return 1;
+
+	return 0;
+}
+
+/**
+ * reverse - Reverses the elements of a double array in place.
+ * @arr: pointer to the array of doubles to be reversed.
+ * @size: number of elements in the array.
+ */
+static void reverse(double arr[], size_t size)
+{
+	size_t left = 0;
+	size_t right = size - 1;
+
+	if (size <= 1)
+		return;
+
+	while (left < right) {
+		double temp = arr[left];
+		arr[left] = arr[right];
+		arr[right] = temp;
+		left++;
+		right--;
+	}
+}
+
+/**
+ * linspace - Generates a linearly spaced array of doubles.
+ * @start: The starting value of the sequence.
+ * @end: The ending value of the sequence.
+ * @num: The number of elements to generate.
+ *
+ * Allocates and returns an array of @num doubles, linearly spaced
+ * between @start and @end (inclusive). If @num is 0, returns NULL.
+ * If @num is 1, the array contains only @start.
+ *
+ * Return: allocated array, or NULL on allocation failure or if @num is 0.
+ */
+static double *linspace(double start, double end, unsigned int num)
+{
+	double *arr;
+	unsigned int i;
+	double step;
+
+	if (num == 0)
+		return NULL;
+
+	dprint(FD_SPRANDOM, "linespace start=%0.2f end=%0.2f num=%d\n",
+	       start, end, num);
+
+	arr = d_alloc(num);
+	if (arr == NULL)
+		return NULL;
+
+	if (num == 1) {
+		arr[0] = start;
+		return arr;
+	}
+
+	/* Calculate step size */
+	step = (end - start) / ((double)num - 1.0);
+
+	for (i = 0; i < num; i++)
+		arr[i] = start + (double)i * step;
+
+	return arr;
+}
+
+/**
+ * linear_interp - Performs linear interpolation or extrapolation.
+ * @new_x: The x-value at which to interpolate.
+ * @x_arr: Array of x-values (must be sorted in strictly increasing order).
+ * @y_arr: Array of y-values corresponding to x_arr.
+ * @num: Number of points in x_arr and y_arr.
+ *
+ * Returns the interpolated y-value at new_x using linear interpolation
+ * between the points in x_arr and y_arr. If new_x is outside the range
+ * of x_arr, returns the nearest endpoint's y-value (extrapolation).
+ * Handles edge cases for zero or one point, and avoids division by zero
+ * if two x-values are nearly identical.
+ */
+static double linear_interp(double new_x, const double *x_arr,
+			    const double *y_arr, unsigned int num)
+{
+	unsigned int i;
+	double x1, y1, x2, y2;
+
+	if (num == 0)
+		return 0.0;
+
+	if (num == 1)
+		return y_arr[0]; /* If only one point, return its y-value */
+
+	/* Handle extrapolation outside the range */
+	if (new_x <= x_arr[0])
+		return y_arr[0];
+
+	if (new_x >= x_arr[num - 1])
+		return y_arr[num - 1];
+
+	/* Find the interval [x_arr[i], x_arr[i + 1]] that contains new_x */
+	for (i = 0; i < num - 1; i++) {
+		if (new_x >= x_arr[i] && new_x <= x_arr[i + 1]) {
+			x1 = x_arr[i];
+			y1 = y_arr[i];
+			x2 = x_arr[i + 1];
+			y2 = y_arr[i + 1];
+
+			/* Avoid division by zero if x values are identical
+			 * Using a small epsilon for float comparison
+			 * Return y1 if x1 and x2 are almost identical
+			 */
+			if (fabs(x2 - x1) < 1e-9)
+				return y1;
+
+			return y1 + (y2 - y1) * ((new_x - x1) / (x2 - x1));
+		}
+	}
+	/* Should not reach here if new_x is within bounds
+	 * and x_arr is strictly increasing
+	 */
+	return 0.0;
+}
+
+/**
+ * sample_curve_equally_on_x - Resamples a curve at equally spaced x-values.
+ * @points: array of input points (must have strictly increasing x-values).
+ * @num: Number of input points.
+ * @num_resampled: number of points to resample to.
+ * @resampled_points: An output array of resampled points.
+ *
+ * Sorts the input points by x-value, checks for strictly increasing x-values,
+ * and generates a new set of points with x-values equally spaced between the
+ * minimum and maximum x of the input. Uses linear interpolation to compute
+ * corresponding y-values.
+ * Note: The function allocates memory for the output array.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int sample_curve_equally_on_x(struct point *points, unsigned int num,
+				     unsigned int num_resampled,
+				     struct point **resampled_points)
+{
+	double *x_orig = (double *)0;
+	double *y_orig = (double *)0;
+	double *new_x_arr = (double *)0;
+	struct point *new_points_arr = (struct point *)0;
+	unsigned int i;
+	int ret = 0;
+
+	if (points == NULL || resampled_points == NULL)
+		return -EINVAL;
+
+	if (num == 0) {
+		log_err("fio: original points array cannot be empty.\n");
+		return -EINVAL;
+	}
+
+	if (num_resampled == 0) {
+		*resampled_points = NULL;
+		return 0;
+	}
+
+	qsort(points, num, sizeof(struct point), compare_points);
+
+	/* Check if x-values are strictly increasing and sort them */
+	for (i = 0; i < num - 1; i++) {
+		if (points[i+1].x <= points[i].x) {
+			log_err("fio: x-values must be strictly increasing.\n");
+			ret = -EINVAL;
+			goto cleanup;
+		}
+	}
+
+	/* 2. Extract x and y into separate arrays for interpolation */
+	x_orig = d_alloc(num);
+	y_orig = d_alloc(num);
+	if (x_orig == NULL || y_orig == NULL) {
+		log_err("fio: Memory allocation failed for x_orig or y_orig.\n");
+		ret = -ENOMEM;
+		goto cleanup;
+	}
+	for (i = 0; i < num; i++) {
+		x_orig[i] = points[i].x;
+		y_orig[i] = points[i].y;
+	}
+
+	/* 4. Generate new_x values using linspace */
+	new_x_arr = linspace(x_orig[0], x_orig[num - 1], num_resampled);
+	if (new_x_arr == NULL) {
+		ret = -ENOMEM;
+		goto cleanup;
+	}
+
+	/* 5. Allocate memory for new resampled points */
+	new_points_arr = p_alloc(num_resampled);
+	if (new_points_arr == NULL) {
+		log_err("fio: Memory allocation failed for new_points_arr.\n");
+		ret = -ENOMEM;
+		goto cleanup;
+	}
+
+	/* 6. Perform linear interpolation for each new_x to get new_y */
+	for (i = 0; i < num_resampled; i++) {
+		new_points_arr[i].x = new_x_arr[i];
+		new_points_arr[i].y = linear_interp(new_x_arr[i], x_orig, y_orig, num);
+	}
+
+	*resampled_points = new_points_arr;
+
+cleanup:
+	free(x_orig);
+	free(y_orig);
+	free(new_x_arr);
+
+	return ret;
+}
+
+/**
+ * compute_waf - Compute the write amplification factor (WAF)
+ * @over_provisioning: The over-provisioning ratio (0 < over_provisioning < 1)
+ *
+ * write amplification approximation equation
+ *
+ *                   0.5
+ *     WAF = ------------------ + 0.7
+ *           over_provisioning
+ *
+ * Return: The computed write amplification factor as a double.
+ */
+static inline double compute_waf(double over_provisioning)
+{
+	return 0.5 / over_provisioning + 0.7;
+}
+
+/**
+ * compute_gc_validity - validity of the block selected for GC (garbage collector)
+ *
+ * @waf: The Write Amplification Factor, must be greater than 1.0.
+ *
+ * Return: The computed gavalidity;
+ */
+static inline double compute_gc_validity(double waf)
+{
+	assert(waf > 1.0); /* Ensure WAF is greater than 1.0 */
+	return 1.0 - (double)1.0 / waf;
+}
+
+/**
+ * compute_validity_dist - Computes a resampled validity distribution for regions.
+ * @n_regions: Number of regions to divide the distribution into.
+ * @over_provisioning: Over-provisioning factor used to calculate WAF and validity.
+ *
+ * Calculates the validity distribution across a specified number of regions,
+ * based on the write amplification factor (WAF) and over-provisioning.
+ * Steps:
+ * - Allocates and fills arrays for:
+ *   - validity distribution
+ *   - block ratios
+ *   - accumulated ratios
+ * - Constructs a set of points representing the curve.
+ * - Resamples the curve to ensure equal spacing along the x-axis.
+ * - Reverses the resulting validity distribution before returning.
+ *
+ * Note: The function allocates memory for the validity distribution array.
+ *
+ * Return: resampled and reversed validity distribution array or NULL on error.
+ */
+static double *compute_validity_dist(unsigned int n_regions, double over_provisioning)
+{
+	double waf = compute_waf(over_provisioning);
+	double validity = compute_gc_validity(waf);
+	double *validity_distribution = NULL;
+	double *blocks_ratio = NULL;
+	double *acc_ratio = NULL;
+	double acc;
+	unsigned int i;
+	struct point *points = NULL;
+	struct point *points_resampled = NULL;
+	int ret;
+
+	if (n_regions == 0) {
+		log_err("fio: requires at least one region");
+		goto out;
+	}
+
+	/*
+	 * Use linspace to get equally distributed validity values,
+	 * along the y-axis of the curve we want to generate.
+	 */
+	validity_distribution = linspace(1.0, validity, n_regions);
+
+	blocks_ratio = d_alloc(n_regions);
+	if (blocks_ratio == NULL) {
+		log_err("fio: memory allocation failed for linspace.\n");
+		goto out;
+	}
+
+	for (i = 0; i < n_regions; i++)
+		blocks_ratio[i] = 1.0 / validity_distribution[i];
+
+	acc_ratio = d_alloc(n_regions);
+	if (acc_ratio == NULL) {
+		log_err("fio: memory allocation failed for linspace_c.\n");
+		goto out;
+	}
+
+	acc = 0.0;
+	for (i = 0; i < n_regions; i++) {
+		acc_ratio[i] = acc + blocks_ratio[i];
+		acc = acc_ratio[i];
+	}
+
+	print_d_array("validity_distribution", validity_distribution, n_regions);
+	print_d_array("blocks ratio", blocks_ratio, n_regions);
+	print_d_array("accumulated ratio:", acc_ratio, n_regions);
+
+	points = p_alloc(n_regions);
+
+	for (i = 0; i < n_regions; i++) {
+		points[i].x = acc_ratio[i];
+		points[i].y = validity_distribution[i];
+	}
+	print_d_points(points, n_regions);
+
+	/*
+	 * Use linspace again to get uniformly distributed x-values,
+	 * and then interpolate the curve to find the validity at those
+	 * uniformly distributed x-values.
+	 */
+	ret = sample_curve_equally_on_x(points, n_regions, n_regions,
+					&points_resampled);
+
+	if (ret == 0) {
+		print_d_points(points_resampled, n_regions);
+	} else {
+		log_err("fio: failed to resample curve. Error code: %d\n", ret);
+		free(validity_distribution);
+		validity_distribution = NULL;
+		goto out;
+	}
+
+	for (i = 0; i < n_regions; i++)
+		validity_distribution[i] = points_resampled[i].y;
+
+	print_d_array("validity resampled", validity_distribution, n_regions);
+
+out:
+	free(points);
+	free(points_resampled);
+	free(blocks_ratio);
+	free(acc_ratio);
+
+	reverse(validity_distribution, n_regions);
+
+	return validity_distribution;
+}
+
+/**
+ * Calculate the physical size based on logical size and over-provisioning
+ *
+ * @over_provisioning:   over provisioning factor (e.g. 0.2 for 20%)
+ * @logical_sz:          Logical size in bytes
+ * @align_bs:            Block size for alignment in bytes
+ *
+ * return: Physical size in bytes, including over-provisioning and aligned to align_bs
+ */
+static uint64_t sprandom_physical_size(double over_provisioning, uint64_t logical_sz,
+				       uint64_t align_bs)
+{
+	uint64_t size;
+
+	size = logical_sz + ceil((double)logical_sz * over_provisioning);
+	return (size + (align_bs - 1)) & ~(align_bs - 1);
+}
+
+/**
+ * estimate_inv_capacity - Estimates the invalid capacity of a region.
+ * @region_cnt: number of offsets in the region.
+ * @validity: invalidation ration in the regions (between 0 and 1).
+ *
+ * Calculates the expected number of invalidion in regions, adding a margin
+ * of 6 standard deviations to account for statistical variation.
+ *
+ * Returns: Estimated invalid capacity
+ */
+static uint64_t estimate_inv_capacity(uint64_t region_cnt, double validity)
+{
+	double sigma = sqrt((double)region_cnt * validity * (1.0 - validity));
+	return (uint64_t)ceil(region_cnt * (1.0 - validity) + 6.0 * sigma);
+}
+
+/**
+ * sprandom_setup - Initialize and configure sprandom_info structure.
+ * @spr_info: Pointer to sprandom_info structure to be initialized.
+ * @logical_size: Logical size of the storage region.
+ * @align_bs: Alignment block size.
+ *
+ * Calculates physical size and region parameters based on logical size,
+ * alignment, and over-provisioning. Allocates and initializes validity
+ * distribution and invalid percentage arrays for regions. Precomputes
+ * invalid buffer capacity and allocates buffer. Sets up region size,
+ * write counts, and resets region/phase counters.
+ *
+ * Returns 0 on success, enagative value on failure.
+ */
+static int sprandom_setup(struct sprandom_info *spr_info, uint64_t logical_size,
+			  uint64_t align_bs)
+{
+	double over_provisioning = spr_info->over_provisioning;
+	uint64_t physical_size;
+	uint64_t region_sz;
+	uint64_t region_write_count;
+	double *validity_dist;
+	size_t invalid_capacity;
+	size_t total_alloc = 0;
+	char bytes2str_buf[40];
+	int i;
+
+	physical_size = sprandom_physical_size(over_provisioning,
+					       logical_size, align_bs);
+
+	validity_dist = compute_validity_dist(spr_info->num_regions,
+					      spr_info->over_provisioning);
+	if (!validity_dist)
+		return -ENOMEM;
+
+	/* Initialize validity_distribution */
+	print_d_array("validity resampled:", validity_dist, spr_info->num_regions);
+
+	spr_info->validity_dist = validity_dist;
+	total_alloc += spr_info->num_regions * sizeof(spr_info->validity_dist[0]);
+
+	/* Precompute invalidity percentage array */
+	spr_info->invalid_pct = calloc(spr_info->num_regions,
+				       sizeof(spr_info->invalid_pct[0]));
+	if (!spr_info->invalid_pct)
+		goto err;
+
+	total_alloc += spr_info->num_regions * sizeof(spr_info->invalid_pct[0]);
+
+	for (i = 0; i < spr_info->num_regions; i++) {
+		double inv = (1.0 - validity_dist[i]) * (double)PCT_PRECISION;
+		spr_info->invalid_pct[i] = (int)round(inv);
+	}
+
+	region_sz = physical_size / spr_info->num_regions;
+	region_write_count = region_sz / align_bs;
+
+	invalid_capacity = estimate_inv_capacity(region_write_count,
+						 validity_dist[0]);
+	spr_info->invalid_capacity = invalid_capacity;
+
+	spr_info->invalid_buf = pcb_alloc(invalid_capacity);
+
+	total_alloc += invalid_capacity * sizeof(uint64_t);
+
+	spr_info->region_sz = region_sz;
+	spr_info->invalid_count[0] = 0;
+	spr_info->invalid_count[1] = 0;
+	spr_info->curr_phase = 0;
+	spr_info->current_region = 0;
+	spr_info->region_write_count = region_write_count;
+	spr_info->writes_remaining = region_write_count;
+
+	/* Display overall allocation */
+	dprint(FD_SPRANDOM, "Summary:\n");
+	dprint(FD_SPRANDOM, "  logical_size:      %"PRIu64": %s\n",
+		logical_size,
+		bytes2str_simple(bytes2str_buf, sizeof(bytes2str_buf), logical_size));
+	dprint(FD_SPRANDOM, "  physical_size:     %"PRIu64": %s\n",
+		physical_size,
+		bytes2str_simple(bytes2str_buf, sizeof(bytes2str_buf), physical_size));
+	dprint(FD_SPRANDOM, "  op: %02f\n", spr_info->over_provisioning);
+	dprint(FD_SPRANDOM, "  region_size:        %"PRIu64"\n", region_sz);
+	dprint(FD_SPRANDOM, "  num_regions:        %u\n", spr_info->num_regions);
+	dprint(FD_SPRANDOM, "  region_write_count: %"PRIu64"\n", region_write_count);
+	dprint(FD_SPRANDOM, "  invalid_capacity:   %zu\n", invalid_capacity);
+	dprint(FD_SPRANDOM, "  dynamic memory:     %zu: %s\n",
+		total_alloc,
+		bytes2str_simple(bytes2str_buf, sizeof(bytes2str_buf), total_alloc));
+
+	return 0;
+err:
+	free(spr_info->validity_dist);
+	free(spr_info->invalid_pct);
+	return -ENOMEM;
+}
+
+/**
+ * sprandom_add_with_probability - Adds an offset to the invalid buffer with
+ * a probability.
+ *
+ * @info: sprandom_info structure containing random state and buffers.
+ * @offset: The offset value to potentially add to the invalid buffer.
+ * @phase: The current phase index for invalid count tracking.
+ *
+ * Generates a random value and, based on the current region's invalid percentage,
+ * decides whether to add the offset to the invalid buffer.
+ * If the buffer is full, ogs an error and asserts failure.
+ */
+static void sprandom_add_with_probability(struct sprandom_info *info,
+					  uint64_t offset, unsigned int phase)
+{
+
+	int v = rand_between(info->rand_state, 0, PCT_PRECISION);
+
+	if (v <= info->invalid_pct[info->current_region]) {
+		if (pcb_space_available(info->invalid_buf)) {
+			pcb_push_staged(info->invalid_buf, offset);
+			info->invalid_count[phase]++;
+		} else {
+			dprint(FD_SPRANDOM, "pcb buffer would be overriten\n");
+			assert(false);
+		}
+	}
+}
+
+static void dprint_invalidation(const struct sprandom_info *info)
+{
+	uint32_t phase = info->curr_phase;
+	double inv = 0;
+	double inv_act; /* actually invalidation percentage */
+
+	inv_act = (double)info->invalid_count[phase] / (double)info->region_write_count;
+	if (info->current_region > 0)
+		inv = (double)info->invalid_pct[info->current_region - 1] / PCT_PRECISION;
+
+	dprint(FD_SPRANDOM, "Invalidation[%d] %"PRIu64" %zu %.04f %.04f\n",
+		info->current_region,
+		info->region_write_count,
+		info->invalid_count[phase],
+		inv, inv_act);
+}
+
+/**
+ * sprandom_get_next_offset - Generate the next write offset for a region,
+ * managing invalidation, and region transitions.
+ *
+ * @info: sprandom_info structure containing state and configuration.
+ * @f: fio file associated with the ssd device.
+ * @b: block offset to store the next write offset.
+ *
+ * Generates offsets to write a region and saves a fraction of the offsets
+ * in a two phase circular buffer.
+ * When transitioning to the next region (phase is flipped),it first writes
+ * all saved offsets to achieve the desired fraction of invalid blocks in the
+ * previous region. The remainder of the current region is then filled with
+ * new offsets.
+ *
+ * Returns:
+ *   0 if a valid offset is found and stored in @b,
+ *   1 if no more offsets are available (end of regions or LFSR exhausted).
+ */
+int sprandom_get_next_offset(struct sprandom_info *info, struct fio_file *f, uint64_t *b)
+{
+	uint64_t offset = 0;
+	uint32_t phase = info->curr_phase;
+
+	/* replay invalidation */
+	if (pcb_pop(info->invalid_buf, &offset)) {
+		sprandom_add_with_probability(info, offset,  phase ^ 1);
+		dprint(FD_SPRANDOM, "Write %"PRIu64" over %d\n", *b, info->current_region);
+		goto out;
+	}
+
+	/* Move to next region */
+	if (info->writes_remaining == 0) {
+		if (info->current_region >= info->num_regions) {
+			dprint(FD_SPRANDOM, "End: Last Region %d cur%d\n",
+			       info->current_region, info->num_regions);
+			return 1;
+		}
+
+		dprint_invalidation(info);
+
+		info->invalid_count[phase] = 0;
+
+		info->current_region++;
+		phase ^= 1;
+		info->writes_remaining = info->region_write_count -
+						info->invalid_count[phase];
+		info->curr_phase = phase;
+		pcb_commit(info->invalid_buf);
+	}
+
+	/* Fetch new offset */
+	if (lfsr_next(&f->lfsr, &offset)) {
+		dprint(FD_SPRANDOM, "End: LFSR exhausted %d [%zu] [%zu]\n",
+			info->current_region,
+			info->invalid_count[phase],
+			info->invalid_count[phase ^ 1]);
+
+		dprint_invalidation(info);
+
+		return 1;
+	}
+
+	if (info->writes_remaining > 0)
+		info->writes_remaining--;
+
+	sprandom_add_with_probability(info, offset,  phase ^ 1);
+	dprint(FD_SPRANDOM, "Write %"PRIu64" lfsr %d\n", offset, info->current_region);
+out:
+	*b = offset;
+	return 0;
+}
+
+/**
+ * sprandom_init - initialize sprandom info
+ * @td: fio thread data
+ * @f: fio file associated with the ssd device.
+ *
+ * Sets up the sprandom_info structure for the given file according:
+ * region count, over-provisioning, and file/device size.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+int sprandom_init(struct thread_data *td, struct fio_file *f)
+{
+	struct sprandom_info *info = NULL;
+	double over_provisioning;
+	uint64_t logical_size;
+	uint64_t align_bs = td->o.bs[DDIR_WRITE];
+	int ret;
+
+	if (!td->o.sprandom)
+		return 0;
+
+	if (!is_power_of_2(align_bs)) {
+		log_err("fio: sprandom: bs [%"PRIu64"] should be power of 2",
+			align_bs);
+		return -EINVAL;
+	}
+
+	info = calloc(1, sizeof(*info));
+	if (!info)
+		return -ENOMEM;
+
+	logical_size = min(f->real_file_size, f->io_size);
+	over_provisioning = td->o.spr_over_provisioning.u.f;
+	info->num_regions = td->o.spr_num_regions;
+	info->over_provisioning = over_provisioning;
+	td->o.io_size = sprandom_physical_size(over_provisioning,
+					       logical_size, align_bs);
+	info->rand_state = &td->sprandom_state;
+	ret = sprandom_setup(info, logical_size, align_bs);
+	if (ret)
+		goto err;
+
+	f->spr_info = info;
+	return 0;
+err:
+	free(info);
+	return ret;
+}
+
+/**
+ * sprandom_free - Frees resources associated with a sprandom_info structure.
+ * @info: Pointer to the sprandom_info structure to be freed.
+ *
+ * Releases memory allocated for validity_dist, invalid_buf, and the spr_info
+ * structure itself. Does nothing if @spr_info is NULL.
+ */
+void sprandom_free(struct sprandom_info *info)
+{
+	if (!info)
+		return;
+
+	free(info->validity_dist);
+	free(info->invalid_buf);
+	free(info);
+}
diff --git a/sprandom.h b/sprandom.h
new file mode 100644
index 00000000..ea8b829d
--- /dev/null
+++ b/sprandom.h
@@ -0,0 +1,78 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0 only
+ *
+ * Copyright (c) 2025 Sandisk Corporation or its affiliates.
+ */
+
+#ifndef FIO_SPRANDOM_H
+#define FIO_SPRANDOM_H
+
+#include <stdint.h>
+#include "lib/rand.h"
+#include "pcbuf.h"
+
+/**
+ * struct sprandom_info - information for sprandom operations.
+ *
+ * @over_provisioning:  Over-provisioning ratio for the flash device.
+ * @region_sz:          Size of each region in bytes.
+ * @num_regions:        Number of SPRandom regions.
+ * @validity_dist:      validity for each region.
+ * @invalid_pct:        invalidation percentages per region.
+ * @invalid_buf:        invalidation offsets two pahse buffer.
+ * @invalid_capacity:   maximal size of invalidation buffer for a region.
+ * @invalid_count:      number of invalid offsets in each phase.
+ * @current_region:     index of the current region being processed.
+ * @curr_phase:         current phase of the invalidation process (0 or 1).
+ * @region_write_count: number of writes performed in the current region.
+ * @writes_remaining:   umber of writes left to perform.
+ * @rand_state:         state for the random number generator.
+ */
+struct sprandom_info {
+	double    over_provisioning;
+	uint64_t  region_sz;
+	uint32_t  num_regions;
+
+	double    *validity_dist;
+	uint32_t  *invalid_pct;
+
+	/* Invalidation list*/
+	struct pc_buf *invalid_buf;
+	uint64_t  invalid_capacity;
+	size_t   invalid_count[2];
+	uint32_t current_region;
+	uint32_t curr_phase;
+
+	/* Region and write tracking */
+	uint64_t region_write_count;
+	uint64_t writes_remaining;
+
+	struct frand_state *rand_state;
+};
+
+/**
+ * sprandom_init - Initialize the sprandom for a given file and thread.
+ * @td: FIO thread data
+ * @f: FIO file
+ *
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int sprandom_init(struct thread_data *td, struct fio_file *f);
+
+/**
+ * sprandom_free - Frees resources associated with a sprandom_info structure.
+ * @info: sprandom_info structure to be freed.
+ */
+void sprandom_free(struct sprandom_info *info);
+
+/**
+ * sprandom_get_next_offset - Get the next random offset for a file.
+ * @info: sprandom_info structure containing the state
+ * @f: FIO file
+ * @b: Output pointer to store the next offset.
+ *
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int sprandom_get_next_offset(struct sprandom_info *info, struct fio_file *f, uint64_t *b);
+
+#endif /* FIO_SPRANDOM_H */
diff --git a/thread_options.h b/thread_options.h
index 1b26ab58..3abce731 100644
--- a/thread_options.h
+++ b/thread_options.h
@@ -178,6 +178,9 @@ struct thread_options {
 	unsigned int log_alternate_epoch_clock_id;
 	unsigned int norandommap;
 	unsigned int softrandommap;
+	unsigned int sprandom;
+	unsigned int spr_num_regions;
+	fio_fp64_t spr_over_provisioning;
 	unsigned int bs_unaligned;
 	unsigned int fsync_on_close;
 	unsigned int bs_is_seq_rand;
@@ -510,6 +513,9 @@ struct thread_options_pack {
 	uint32_t log_alternate_epoch_clock_id;
 	uint32_t norandommap;
 	uint32_t softrandommap;
+	uint32_t sprandom;
+	uint32_t spr_num_regions;
+	fio_fp64_t spr_over_provisioning;
 	uint32_t bs_unaligned;
 	uint32_t fsync_on_close;
 	uint32_t bs_is_seq_rand;
diff --git a/unittests/lib/num2str.c b/unittests/lib/num2str.c
index 8f12cf83..49e80346 100644
--- a/unittests/lib/num2str.c
+++ b/unittests/lib/num2str.c
@@ -37,11 +37,46 @@ static void test_num2str(void)
 	}
 }
 
+struct bytes2str_testcase {
+        uint64_t bytes;
+        const char *expected;
+};
+
+static const struct bytes2str_testcase bytes2str_testcases[] = {
+        { 0, "0.00 B" },
+        { 512, "512.00 B" },
+        { 1024, "1.00 KiB" },
+        { 1536, "1.50 KiB" },
+        { 1048576, "1.00 MiB" },
+        { 1073741824ULL, "1.00 GiB" },
+        { 1099511627776ULL, "1.00 TiB" },
+        { 1125899906842624ULL, "1.00 PiB" },
+        { 1152921504606846976ULL, "1.00 EiB" },
+};
+
+static void test_bytes2str_simple(void)
+{
+        char buf[64];
+        int i;
+
+        for (i = 0; i < FIO_ARRAY_SIZE(bytes2str_testcases); ++i) {
+                const struct bytes2str_testcase *tc = &bytes2str_testcases[i];
+                const char *result = bytes2str_simple(buf, sizeof(buf), tc->bytes);
+
+                CU_ASSERT_PTR_EQUAL(result, buf);
+                CU_ASSERT_STRING_EQUAL(result, tc->expected);
+        }
+}
+
 static struct fio_unittest_entry tests[] = {
 	{
 		.name	= "num2str/1",
 		.fn	= test_num2str,
 	},
+	{
+                .name   = "bytes2str_simple/1",
+                .fn     = test_bytes2str_simple,
+        },
 	{
 		.name	= NULL,
 	},
diff --git a/unittests/lib/pcbuf.c b/unittests/lib/pcbuf.c
new file mode 100644
index 00000000..f6167423
--- /dev/null
+++ b/unittests/lib/pcbuf.c
@@ -0,0 +1,116 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0 only
+ *
+ * Copyright (c) 2025 Sandisk Corporation or its affiliates.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "../unittest.h"
+#include "pcbuf.h"
+
+#define TEST_CAPACITY 8  /* Small capacity for wrap-around testing */
+
+static void test_pcbuf_basic_ops(void)
+{
+	struct pc_buf *cb = pcb_alloc(TEST_CAPACITY);
+	uint64_t i;
+
+	CU_ASSERT_PTR_NOT_NULL(cb);
+
+	CU_ASSERT_TRUE(pcb_is_empty(cb));
+	CU_ASSERT_FALSE(pcb_is_full(cb));
+	CU_ASSERT_EQUAL(pcb_committed_size(cb), 0);
+	CU_ASSERT_EQUAL(pcb_staged_size(cb), 0);
+	CU_ASSERT_TRUE(pcb_space_available(cb));
+
+	/* Stage data up to capacity-1 (since 1 slot is reserved) */
+	for (i = 0; i < TEST_CAPACITY - 1; ++i) {
+		CU_ASSERT_TRUE(pcb_push_staged(cb, i + 100));
+	}
+
+	/* Next push should fail (buffer full) */
+	CU_ASSERT_FALSE(pcb_push_staged(cb, 999));
+
+	CU_ASSERT_EQUAL(pcb_staged_size(cb), TEST_CAPACITY - 1);
+	CU_ASSERT_EQUAL(pcb_committed_size(cb), 0);
+	CU_ASSERT_TRUE(pcb_is_empty(cb));
+	CU_ASSERT_TRUE(pcb_is_full(cb));
+
+	/* Commit staged data */
+	pcb_commit(cb);
+
+	CU_ASSERT_EQUAL(pcb_committed_size(cb), TEST_CAPACITY - 1);
+	CU_ASSERT_EQUAL(pcb_staged_size(cb), 0);
+	CU_ASSERT_FALSE(pcb_is_empty(cb));
+
+	/* Pop all committed data */
+	for (i = 0; i < TEST_CAPACITY - 1; ++i) {
+		uint64_t val;
+		CU_ASSERT_TRUE(pcb_pop(cb, &val));
+		CU_ASSERT_EQUAL(val, i + 100);
+	}
+
+	/* Buffer should now be empty again */
+	CU_ASSERT_TRUE(pcb_is_empty(cb));
+	CU_ASSERT_FALSE(pcb_is_full(cb));
+	CU_ASSERT_TRUE(pcb_space_available(cb));
+
+	free(cb);
+}
+
+static void test_pcbuf_wraparound(void)
+{
+	struct pc_buf *cb = pcb_alloc(TEST_CAPACITY);
+	uint64_t expected[] = {201, 202, 203, 204, 205, 999};
+	size_t num_expected = sizeof(expected)/sizeof(expected[0]);
+	uint64_t val;
+	uint64_t i;
+
+	CU_ASSERT_PTR_NOT_NULL(cb);
+
+	/* Stage up to near capacity and commit */
+	for (i = 0; i < TEST_CAPACITY - 2; ++i)
+		CU_ASSERT_TRUE(pcb_push_staged(cb, i + 200));
+
+	pcb_commit(cb);
+
+	/* Pop one item to move read_tail forward */
+	CU_ASSERT_TRUE(pcb_pop(cb, &val));
+	CU_ASSERT_EQUAL(val, 200);
+
+	/* Now stage one more item to cause wraparound */
+	CU_ASSERT_TRUE(pcb_push_staged(cb, 999));
+	pcb_commit(cb);
+
+	/* Pop remaining items, ensure correctness */
+	for (i = 0; i < num_expected; ++i) {
+		CU_ASSERT_TRUE(pcb_pop(cb, &val));
+		CU_ASSERT_EQUAL(val, expected[i]);
+	}
+
+	CU_ASSERT_TRUE(pcb_is_empty(cb));
+	free(cb);
+}
+
+static struct fio_unittest_entry tests[] = {
+	{
+		.name   = "pcbuf/basic_ops",
+		.fn     = test_pcbuf_basic_ops,
+	},
+	{
+		.name   = "pcbuf/wraparound",
+		.fn     = test_pcbuf_wraparound,
+	},
+	{
+		.name   = NULL,
+	},
+};
+
+CU_ErrorCode fio_unittest_lib_pcbuf(void)
+{
+	return fio_unittest_add_suite("pcbuf.h", NULL, NULL, tests);
+}
diff --git a/unittests/unittest.c b/unittests/unittest.c
index f490b485..4a034b40 100644
--- a/unittests/unittest.c
+++ b/unittests/unittest.c
@@ -50,6 +50,7 @@ int main(void)
 	fio_unittest_register(fio_unittest_lib_memalign);
 	fio_unittest_register(fio_unittest_lib_num2str);
 	fio_unittest_register(fio_unittest_lib_strntol);
+	fio_unittest_register(fio_unittest_lib_pcbuf);
 	fio_unittest_register(fio_unittest_oslib_strlcat);
 	fio_unittest_register(fio_unittest_oslib_strndup);
 	fio_unittest_register(fio_unittest_oslib_strcasestr);
diff --git a/unittests/unittest.h b/unittests/unittest.h
index ecb7d124..0f45bfbd 100644
--- a/unittests/unittest.h
+++ b/unittests/unittest.h
@@ -17,6 +17,7 @@ CU_ErrorCode fio_unittest_add_suite(const char*, CU_InitializeFunc,
 CU_ErrorCode fio_unittest_lib_memalign(void);
 CU_ErrorCode fio_unittest_lib_num2str(void);
 CU_ErrorCode fio_unittest_lib_strntol(void);
+CU_ErrorCode fio_unittest_lib_pcbuf(void);
 CU_ErrorCode fio_unittest_oslib_strlcat(void);
 CU_ErrorCode fio_unittest_oslib_strndup(void);
 CU_ErrorCode fio_unittest_oslib_strcasestr(void);
diff --git a/verify.c b/verify.c
index c7f43c06..20c49a94 100644
--- a/verify.c
+++ b/verify.c
@@ -431,7 +431,7 @@ done:
  */
 static inline bool pattern_need_buffer(struct thread_data *td)
 {
-	return td->o.verify_async &&
+	return (td->o.verify_async || td->o.use_thread) &&
 		td->o.verify_fmt_sz &&
 		td->o.verify_fmt[0].desc->paste == paste_blockoff;
 }