[PATCH] added new type of random distribution
[email protected] Wed, 6 May 2026 06:55:07 +0000
| Newsgroups | org.kernel.vger.fio |
|---|---|
| Message-ID | <[email protected]> |
From: vadlakondaswetha <[email protected]> This PR introduces a new generic random distribution primitive: sequence. While fio currently provides excellent primitives for linear, pseudo-random (randread), and strided access patterns, it lacks a mechanism to simulate deterministic, local non-linearity within a repeating stride. This access pattern has become a critical benchmark vector for modern workloads, most notably: *LLM Inference Weights Loading* Reading .safetensors model files which contain a list of tensor files. Even though the file is read in sequential mode, within a tensor, data will be requested out of order. *Database Engine Log-Merging / LSM-Trees* Scenarios where specific block indices (like parity blocks or metadata headers) must be systematically read out-of-order within every repeating chunk or block group. Using a generated `read_iolog` for multi-terabyte model benchmarks is sub-optimal as the trace files become massive, unscalable, and lack dynamic flexibility across varying block sizes. The `sequence` distribution resolves this by calculating offsets algorithmically on the fly. NEW OPTIONS INTRODUCED ---------------------- *random_distribution=sequence:N1,N2,N3* Intercepts the random offset generator to utilize the pattern sequence. *random_sequence_stride* Boolean option to modify `sequence` distribution behavior. - When disabled (0, default), the sequence loops over absolute block indices. - When enabled (1), it switches to a Strided Block Group pattern, advancing the base block index by the sequence length after each cycle to progress through the file. Signed-off-by: vadlakondaswetha <[email protected]> --- cconv.c | 22 ++++++++ fio.h | 1 + io_u.c | 24 ++++++++ options.c | 84 ++++++++++++++++++++++++++++ t/sequence.py | 140 +++++++++++++++++++++++++++++++++++++++++++++++ thread_options.h | 7 +++ 6 files changed, 278 insertions(+) create mode 100644 t/sequence.py diff --git a/cconv.c b/cconv.c index 1c1b7273..c8e99f07 100644 --- a/cconv.c +++ b/cconv.c @@ -56,6 +56,10 @@ static void free_thread_options_to_cpu(struct thread_options *o) free(o->bssplit[i]); free(o->zone_split[i]); } + if (o->random_sequence) { + free(o->random_sequence); + o->random_sequence = NULL; + } } size_t thread_options_pack_size(struct thread_options *o) @@ -239,6 +243,13 @@ int convert_thread_options_to_cpu(struct thread_options *o, o->fsync_on_close = le32_to_cpu(top->fsync_on_close); o->bs_is_seq_rand = le32_to_cpu(top->bs_is_seq_rand); o->random_distribution = le32_to_cpu(top->random_distribution); + o->random_sequence_nr = le32_to_cpu(top->random_sequence_nr); + if (o->random_sequence_nr) { + o->random_sequence = malloc(o->random_sequence_nr * sizeof(unsigned int)); + for (j = 0; j < o->random_sequence_nr; j++) + o->random_sequence[j] = le32_to_cpu(top->random_sequence[j]); + } + o->random_sequence_stride = le32_to_cpu(top->random_sequence_stride); o->exitall_error = le32_to_cpu(top->exitall_error); o->zipf_theta.u.f = fio_uint64_to_double(le64_to_cpu(top->zipf_theta.u.i)); o->pareto_h.u.f = fio_uint64_to_double(le64_to_cpu(top->pareto_h.u.i)); @@ -495,6 +506,17 @@ void convert_thread_options_to_net(struct thread_options_pack *top, top->fsync_on_close = cpu_to_le32(o->fsync_on_close); top->bs_is_seq_rand = cpu_to_le32(o->bs_is_seq_rand); top->random_distribution = cpu_to_le32(o->random_distribution); + top->random_sequence_nr = cpu_to_le32(o->random_sequence_nr); + if (o->random_sequence_nr) { + unsigned int seq_nr = o->random_sequence_nr; + if (seq_nr > FIO_SEQ_MAX) { + log_err("fio: FIO_SEQ_MAX is too small\n"); + seq_nr = FIO_SEQ_MAX; + } + for (j = 0; j < seq_nr; j++) + top->random_sequence[j] = cpu_to_le32(o->random_sequence[j]); + } + top->random_sequence_stride = cpu_to_le32(o->random_sequence_stride); top->exitall_error = cpu_to_le32(o->exitall_error); top->zipf_theta.u.i = __cpu_to_le64(fio_double_to_uint64(o->zipf_theta.u.f)); top->pareto_h.u.i = __cpu_to_le64(fio_double_to_uint64(o->pareto_h.u.f)); diff --git a/fio.h b/fio.h index 18196123..b0d1e8dd 100644 --- a/fio.h +++ b/fio.h @@ -957,6 +957,7 @@ enum { FIO_RAND_DIST_GAUSS, FIO_RAND_DIST_ZONED, FIO_RAND_DIST_ZONED_ABS, + FIO_RAND_DIST_SEQUENCE, }; #define FIO_DEF_ZIPF 1.1 diff --git a/io_u.c b/io_u.c index 24d8b7de..e4b863b2 100644 --- a/io_u.c +++ b/io_u.c @@ -291,6 +291,28 @@ bail: return 0; } +static int __get_next_rand_offset_sequence(struct thread_data *td, + struct fio_file *f, enum fio_ddir ddir, + uint64_t *b) +{ + uint64_t io_count; + unsigned int idx; + + if (!td->o.random_sequence_nr) + return 1; + + io_count = td->io_issues[ddir]; + idx = io_count % td->o.random_sequence_nr; + + if (td->o.random_sequence_stride) { + uint64_t group_idx = io_count / td->o.random_sequence_nr; + *b = group_idx * td->o.random_sequence_nr + td->o.random_sequence[idx]; + } else { + *b = td->o.random_sequence[idx]; + } + return 0; +} + static int get_next_rand_offset(struct thread_data *td, struct fio_file *f, enum fio_ddir ddir, uint64_t *b) { @@ -314,6 +336,8 @@ static int get_next_rand_offset(struct thread_data *td, struct fio_file *f, return __get_next_rand_offset_zoned(td, f, ddir, b); else if (td->o.random_distribution == FIO_RAND_DIST_ZONED_ABS) return __get_next_rand_offset_zoned_abs(td, f, ddir, b); + else if (td->o.random_distribution == FIO_RAND_DIST_SEQUENCE) + return __get_next_rand_offset_sequence(td, f, ddir, b); log_err("fio: unknown random distribution: %d\n", td->o.random_distribution); return 1; diff --git a/options.c b/options.c index f418179b..ac5a09bf 100644 --- a/options.c +++ b/options.c @@ -1340,6 +1340,54 @@ static int parse_zoned_distribution(struct thread_data *td, const char *input, return ret; } +static int parse_sequence_distribution(struct thread_data *td, const char *input) +{ + char *str, *p, *n; + unsigned int i = 0; + + p = str = get_opt_postfix(input); + if (!str) { + log_err("fio: missing pattern in sequence distribution\n"); + return 1; + } + + /* Count elements first to allocate memory */ + td->o.random_sequence_nr = 1; + while ((n = strchr(p, ',')) != NULL) { + td->o.random_sequence_nr++; + p = n + 1; + } + + if (td->o.random_sequence) { + free(td->o.random_sequence); + td->o.random_sequence = NULL; + } + + td->o.random_sequence = malloc(td->o.random_sequence_nr * sizeof(unsigned int)); + if (!td->o.random_sequence) { + free(str); + return 1; + } + + p = str; + while ((n = strsep(&p, ",")) != NULL) { + if (*n == '\0') { + log_err("fio: empty element in random_sequence\n"); + goto err; + } + td->o.random_sequence[i++] = atoi(n); + } + + free(str); + return 0; +err: + free(td->o.random_sequence); + td->o.random_sequence = NULL; + td->o.random_sequence_nr = 0; + free(str); + return 1; +} + static int str_random_distribution_cb(void *data, const char *str) { struct thread_data *td = cb_data_to_td(data); @@ -1357,6 +1405,8 @@ static int str_random_distribution_cb(void *data, const char *str) return parse_zoned_distribution(td, str, false); else if (td->o.random_distribution == FIO_RAND_DIST_ZONED_ABS) return parse_zoned_distribution(td, str, true); + else if (td->o.random_distribution == FIO_RAND_DIST_SEQUENCE) + return parse_sequence_distribution(td, str); else return 0; @@ -1404,6 +1454,9 @@ static int str_random_distribution_cb(void *data, const char *str) return 0; } + + + static bool is_valid_steadystate(unsigned int state) { return (state == FIO_SS_IOPS || state == FIO_SS_IOPS_SLOPE || @@ -2811,10 +2864,24 @@ struct fio_option fio_options[FIO_MAX_OPTS] = { .oval = FIO_RAND_DIST_ZONED_ABS, .help = "Zoned absolute random distribution", }, + { .ival = "sequence", + .oval = FIO_RAND_DIST_SEQUENCE, + .help = "Fixed sequence of blocks", + }, }, .category = FIO_OPT_C_IO, .group = FIO_OPT_G_RANDOM, }, + { + .name = "random_sequence_stride", + .lname = "Random Sequence Stride", + .type = FIO_OPT_BOOL, + .off1 = offsetof(struct thread_options, random_sequence_stride), + .help = "Stride sequence of blocks for random distribution", + .def = "0", + .category = FIO_OPT_C_IO, + .group = FIO_OPT_G_RANDOM, + }, { .name = "percentage_random", .lname = "Percentage Random", @@ -6151,6 +6218,18 @@ void fio_options_mem_dupe(struct thread_data *td) { options_mem_dupe(fio_options, &td->o); + if (td->o.random_sequence) { + unsigned int size = td->o.random_sequence_nr * sizeof(unsigned int); + unsigned int *seq = malloc(size); + if (seq) { + memcpy(seq, td->o.random_sequence, size); + td->o.random_sequence = seq; + } else { + td->o.random_sequence = NULL; + td->o.random_sequence_nr = 0; + } + } + if (td->o.ioengine_so_path) td->o.ioengine_so_path = strdup(td->o.ioengine_so_path); @@ -6263,6 +6342,11 @@ void del_opt_posval(const char *optname, const char *ival) void fio_options_free(struct thread_data *td) { options_free(fio_options, &td->o); + if (td->o.random_sequence) { + free(td->o.random_sequence); + td->o.random_sequence = NULL; + td->o.random_sequence_nr = 0; + } if (td->o.ioengine_so_path) { free(td->o.ioengine_so_path); td->o.ioengine_so_path = NULL; diff --git a/t/sequence.py b/t/sequence.py new file mode 100644 index 00000000..bf640020 --- /dev/null +++ b/t/sequence.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only + +""" +# sequence.py +# +# Test random_distribution=sequence and random_sequence_stride. +# +""" + +import os +import sys +import time +import argparse +from pathlib import Path +from fiotestlib import FioJobCmdTest, run_fio_tests + + +class SequenceTest(FioJobCmdTest): + """Test random_distribution=sequence.""" + + def setup(self, parameters): + fio_args = [ + "--name=sequence", + "--rw=randread", + f"--write_iops_log={self.filenames['iopslog']}", + f"--output={self.filenames['output']}", + f"--random_distribution=sequence:{self.fio_opts['random_sequence']}", + f"--bs={self.fio_opts['bs']}", + f"--size={self.fio_opts['size']}", + f"--io_size={self.fio_opts['io_size']}", + "--log_offset=1", + "--ioengine=null", + ] + if 'random_sequence_stride' in self.fio_opts: + fio_args.append(f"--random_sequence_stride={self.fio_opts['random_sequence_stride']}") + + super().setup(fio_args) + + def check_result(self): + super().check_result() + if not self.passed: + return + + seq_str = self.fio_opts['random_sequence'] + expected_seq = [int(x) for x in seq_str.split(',')] + seq_len = len(expected_seq) + bs = self.fio_opts['bs'] + stride = self.fio_opts.get('random_sequence_stride', 0) + + lines = self.iops_log_lines.split('\n') + io_count = 0 + + for line in lines: + if len(line) == 0: + continue + + tokens = line.split(',') + offset = int(tokens[4]) + + idx = io_count % seq_len + if stride: + group_idx = io_count // seq_len + expected_block = group_idx * seq_len + expected_seq[idx] + else: + expected_block = expected_seq[idx] + + expected_offset = expected_block * bs + + if offset != expected_offset: + print(f"IO {io_count}: Expected offset {expected_offset} (block {expected_block}), got {offset}") + self.passed = False + return + + io_count += 1 + + +TEST_LIST = [ + { + "test_id": 1, + "fio_opts": { + "random_sequence": "2,0,1", + "bs": 4096, + "size": "48k", + "io_size": "24k", + "random_sequence_stride": 0, + }, + "test_class": SequenceTest, + }, + { + "test_id": 2, + "fio_opts": { + "random_sequence": "2,0,1", + "bs": 4096, + "size": "48k", + "io_size": "24k", + "random_sequence_stride": 1, + }, + "test_class": SequenceTest, + }, +] + + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser() + parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)') + parser.add_argument('-a', '--artifact-root', help='artifact root directory') + parser.add_argument('-s', '--skip', nargs='+', type=int, + help='list of test(s) to skip') + parser.add_argument('-o', '--run-only', nargs='+', type=int, + help='list of test(s) to run, skipping all others') + return parser.parse_args() + + +def main(): + """Run sequence tests.""" + args = parse_args() + + fio_path = str(Path(args.fio).absolute()) if args.fio else 'fio' + print(f"fio path is {fio_path}") + + artifact_root = args.artifact_root if args.artifact_root else \ + f"sequence-test-{time.strftime('%Y%m%d-%H%M%S')}" + os.mkdir(artifact_root) + print(f"Artifact directory is {artifact_root}") + + test_env = { + 'fio_path': fio_path, + 'fio_root': str(Path(__file__).absolute().parent.parent), + 'artifact_root': artifact_root, + 'basename': 'sequence', + } + + _, failed, _ = run_fio_tests(TEST_LIST, test_env, args) + sys.exit(failed) + + +if __name__ == '__main__': + main() diff --git a/thread_options.h b/thread_options.h index 1b7f67eb..165ca640 100644 --- a/thread_options.h +++ b/thread_options.h @@ -43,6 +43,7 @@ enum dedupe_mode { #define BSSPLIT_MAX 64 #define ZONESPLIT_MAX 256 +#define FIO_SEQ_MAX 64 struct split { unsigned int nr; @@ -190,6 +191,9 @@ struct thread_options { unsigned int verify_only; unsigned int random_distribution; + unsigned int *random_sequence; + unsigned int random_sequence_nr; + unsigned int random_sequence_stride; unsigned int exitall_error; struct zone_split *zone_split[DDIR_RWDIR_CNT]; @@ -527,6 +531,9 @@ struct thread_options_pack { uint32_t bs_is_seq_rand; uint32_t random_distribution; + uint32_t random_sequence[FIO_SEQ_MAX]; + uint32_t random_sequence_nr; + uint32_t random_sequence_stride; uint32_t exitall_error; uint32_t sync_file_range; -- 2.54.0.545.g6539524ca2-goog