Shell and Python tooling for running distributed storage benchmarks and turning their output into tables and plots.
Tests are supported for:
- Filesystems: data and metadata operations with elbencho
- S3-compatible object storage with Warp
- Storage networks with elbencho netbench (beta)
These tests create, overwrite, and delete files and objects. Use only dedicated test paths and empty buckets. Root access is neither required nor recommended.
- Getting Started — quickstart
- Benchmark recipes — filesystem
env.shsettings by use case - Roadmap — planned work
- Design & requirements: docs/DESIGN.md, docs/REQUIREMENTS.md, docs/ARCHITECTURE_DIAGRAMS.md
- Contributing and Security policy
- Licensing: LICENSE (Apache-2.0) and NOTICE — third-party benchmark tools carry their own licenses, including GPL-3.0 and AGPL-3.0
- Linux on the orchestration host and on every client. Benchmark execution
relies on bash 4.3 or newer and a Linux userland. macOS can run static checks
and unit tests that do not require benchmark binaries, but it cannot run the
storage benchmarks. In particular, elbencho is not available for Apple
silicon; binary-dependent tests are skipped when it is unavailable. Running
the developer unit tests on macOS requires a newer Bash and GNU coreutils,
which can be installed with
brew install bash coreutils. - Python 3.12 or newer is required on the host that runs the analysis and reporting wrappers. The pinned current NumPy release establishes this minimum.
- Clients must be available through Slurm or passwordless SSH. For Slurm, gather the account, reservation, and partition required to submit jobs.
- Use tuned clients with enough aggregate network bandwidth to saturate the
target. A starting count is
1.1 * target_Gbps / measured_per_client_Gbps.
The fastest path from a fresh checkout to a first result is a single-node
filesystem sweep. The same pattern (configure env.sh → validate → sweep →
report) applies to every test type.
-
Build a tarball and copy it to a client host. From your workstation:
./utils/build_tarball.sh
This produces
storage-scale-test.tar.gz. See Deployment Tarballs and Benchmark Binaries for what it includes and the binaries you must supply. Copy and unpack it on a host that can reach your client nodes (a Slurm login node, or any host with passwordless SSH to the clients). -
Configure
env.sh. Copy the template and edit it for your environment:cp env.sh.template env.sh
At minimum, set how clients are reached and what to test:
# --- How to reach the client nodes (pick ONE) --- # Slurm mode: set your account / reservation / partition (see env.sh.template). # SSH mode: point at a host list file (SLURM_* vars are then ignored). # export SSH_HOST_LIST=/absolute/path/to/host_list # --- Filesystem test target (mounted on every client) --- declare -A TEST_DIRS=(["/mnt/fs1/scaletest"]=1) # --- A small, fast sweep to start with --- export ELBENCHO_SCALE_IO_SIZES=("r4K" "1M") export ELBENCHO_SCALE_THREAD_LIST=("1" "32" "64" "128") export ELBENCHO_SCALE_READ_WRITE_DURATION=60
-
Validate the environment. Iterate until this runs cleanly:
./validate_env.sh
-
Run a single-node sweep.
./storage-tests/fs/nv-elbencho-sweep.sh --nodes 1
-
Generate a report from the results directory it prints:
./utils/extract-elbencho.sh "$RESULTS_DIR"/elbencho-<datestamp>/
From here, pick the thread counts that saturated a single node and move on to a multi-node sweep. For tuned, use-case-specific parameter sets (peak-finding, requirements confirmation, post-maintenance validation, single-large-file testing), see BENCHMARK_RECIPES_FILESYSTEM.md.
This repository does not ship benchmark binaries. utils/build_tarball.sh
creates a user-local storage-scale-test.tar.gz with the available tools:
- elbencho: reuses
utils/elbencho{,.aarch64}or downloads pinned upstream static releases when missing or when--force-downloadis set. Download failure does not prevent tarball creation. - Warp: includes existing
utils/warp{,.aarch64}only. The suggestedutils/build/build_warp_from_source.shdefaults to NVIDIA/warp-minio, a fork of MinIO Warp, and accepts another GitHub/GitLab URL plus a tag, branch, or commit. - s3test: builds
utils/s3test{,.aarch64}from the in-tree C source with local compilers or Docker/Buildx when missing or stale.
Review third-party licenses and provenance — see NOTICE, which records that elbencho is GPL-3.0 and Warp is AGPL-3.0. Verify that the tarball contains working binaries for every target architecture; NVIDIA and this project do not publish or distribute the resulting tarball or its binaries.
Slurm is the default execution substrate. Setting SSH_HOST_LIST switches the
tool to passwordless SSH mode; the two modes are mutually exclusive.
The host-list file accepts comma- or whitespace-separated hosts on multiple lines and ignores comment lines:
192.0.2.10,192.0.2.11
# comment
node1 node2 node3
Configure SSH mode in env.sh:
export SSH_HOST_LIST=/absolute/path/to/host_list
# export SSH_USER=ubuntu # otherwise the user or SSH config wins
# export SSH_HOMEDIR_SHARED=1 # any non-empty valueSetting SSH_HOST_LIST disables Slurm settings. Authentication must be
non-interactive. Run validate_env.sh to verify connectivity.
SLURM_JOB_NAME_PREFIX prefixes job names. SLURM_EXTRA_ARGS is a Bash array
appended after generated sbatch/srun options, so later duplicate options can
override earlier ones while arguments with spaces remain intact:
SLURM_EXTRA_ARGS=("--constraint=ib" "--comment=storage validation run")SLURM_EXCLUSIVE_USER=1 uses --exclusive=user and derives
--cpus-per-task when the target CPU count is available; the default uses
--exclusive.
Random subsets from mixed-throughput clients can make adjacent sweep points
incomparable. ORDER_NODES and utils/slurm/group_into_bins.py create an
ordered host list with balanced cumulative throughput.
Create an input CSV:
InstanceName,Gbps
node-01,100
node-02,50
node-03,100
node-04,50Generate the ordered list on stdout and a suggested descending --nodes list
on stderr:
python3 utils/slurm/group_into_bins.py --bin-size 25 nodes.csv \
> ordered_nodesUse ordered_nodes as SSH_HOST_LIST or SLURM_NODE_INCLUDES, set
ORDER_NODES=1, and pass the emitted node counts to a sweep. SSH then takes the
first N hosts; Slurm with an include list requests exactly its first N
expanded nodes. Netbench still shuffles its selected nodes into traffic groups.
Smaller bins add sweep points; larger bins provide more balancing freedom.
A working elbencho binary for each client architecture must be available under
utils/. Mount and tune each filesystem at the same path on every client, then
configure weighted test roots:
declare -A TEST_DIRS=(
["/mnt/fs1/scaletest"]=4
["/mnt/fs2/scaletest"]=1
)Weights distribute load between unequal targets. Tune FS_MAX_AGG_THROUGHPUT,
FS_MAX_NODE_THROUGHPUT_GBPS, and FS_MAX_NODE_IOPS when using multiple
targets because they determine generated file counts and bounded write time.
Set TEST_DIRS, tune the elbencho variables in env.sh, and run
validate_env.sh. ELBENCHO_SCALE_IO_SIZES, ELBENCHO_SCALE_THREAD_LIST,
and ELBENCHO_IODEPTH_LIST form a Cartesian product for every requested node
count. An IO-size entry may be 4K, r4K, or a write/read pair such as
1M,r4K; see env.sh.template.
Start with one node, analyze it, then retain only useful thread, size, and I/O depth values for the scale sweep:
./storage-tests/fs/nv-elbencho-sweep.sh --nodes 1
./utils/extract-elbencho.sh "$RESULTS_DIR"/elbencho-<datestamp>/
./storage-tests/fs/nv-elbencho-sweep.sh --nodes 1,2,4,8All sweep scripts accept comma-separated positive integers and ascending
inclusive ranges: X, X-Y, or X-Y+Z. The endpoint is included even when
the step does not land on it; for example, 3-10+2 expands to
3,5,7,9,10. The order is preserved. Run a script with --help for its full
mode and argument contract.
The default worker-directory workload is time-based. Set
ELBENCHO_SCALE_READ_WRITE_DURATION long enough to measure sustained I/O;
60 seconds is useful for exploration, while 300 seconds or more is a typical
starting point for publishable runs. Generated shared-directory workloads are
completion-based instead.
The default lifecycle is mkdir, write, read, and cleanup.
--write-no-read skips the read and still cleans up; the staged-data modes
below retain, reread, or delete an operator-selected dataset.
Slurm uses one coordinator allocation sized to the largest node count and runs
all cells sequentially; SSH also runs cells sequentially. Size Slurm
run_time for the complete product and all phases, startup, pauses, and
cleanup.
The sweep records each cell separately and stops at the first failure. After correcting the cause, continue it with:
./storage-tests/fs/nv-elbencho-sweep.sh --resume \
"$RESULTS_DIR"/elbencho-<datestamp>/--resume must be the only argument. It restores the original environment and
CLI modes from the result directory, skips successful cells, resets stale
running cells, and dispatches the remainder in their original order in either
Slurm or SSH mode. Do not run concurrent resumes. A Slurm resume also refuses
to reset work while its prior coordinator may still be active. Resume sources
shell files in the result directory, so use only a trusted, unmodified result
directory created by a resume-capable revision.
Use --write-only, --read-from, and --delete-only to create a dataset once,
run repeated read sweeps, and remove it without rewriting it for each read.
- Configure one value in each sweep dimension and run
--write-only. Each successful cell retains a unique target and prints itsELBENCHO_WRITE_ONLY_DATA_DIR. - Run
--read-from <path> --nodes <spec>as needed. A many-file read caches its scan in<path>/.storage-scale-test-elbencho-treefile.txt; remove that file after changing the dataset. - Run
--delete-only <path>. The path must be a strict descendant of a configuredTEST_DIRSroot.
To build a mixed-size tree, move retained targets below one common directory under the configured test root, read that parent, then delete it once.
This layout models ranks writing distinct checkpoint files into one flat shared
directory. Configure one TEST_DIRS entry with weight 1:
export ELBENCHO_SINGLE_BIG_FILE=0
export ELBENCHO_FILE_LAYOUT=shared-directory
export ELBENCHO_FILES_PER_NODE=1
export ELBENCHO_FILE_SIZE=64G
export ELBENCHO_SCALE_THREAD_LIST=("1")
export ELBENCHO_IODEPTH_LIST=("1")For N nodes, F files per node, and T threads per node:
total_files = N * F
files_per_worker = F / T
F must be at least and evenly divisible by every T; the harness does not
round. Each worker owns complete files. I/O depth controls outstanding block
I/O per worker, not the number of writers. To model one file per single-threaded
saving rank, set both F and T to the rank count per node and use I/O depth
1.
Generated shared-directory phases are completion-based. They make one pass,
ignore duration and -s, and verify exact file and byte counts. Normal and
--write-no-read runs delete with distributed elbencho workers;
--write-only retains the dataset. ELBENCHO_FILE_SIZE is optional; otherwise
the write block size and ELBENCHO_FILE_SIZE_MULTIPLIER determine it. Direct or
random I/O requires the file size to be divisible by the effective block size.
With --read-from <directory>, the scanned tree defines aggregate file and
byte counts; the configured layout and file count do not repartition it.
Staged reads remain time-based.
See Recipe 5 in BENCHMARK_RECIPES_FILESYSTEM.md.
This mode benchmarks sequential I/O to one regular file. It requires one
TEST_DIRS entry, ELBENCHO_SINGLE_BIG_FILE=1, and
ELBENCHO_SINGLE_BIG_FILE_SIZE for generated data. Random I/O is rejected.
By default, elbencho partitions the file into non-overlapping host ranges.
ELBENCHO_ALL_NODES_ACCESS_ALL_DATA=1 makes every host process the complete
file, transferring nodes * file_size bytes. The default is direct I/O; pass
-b for buffered I/O.
With --read-from, pass a file, not a directory. Its metadata supplies the
extent, so ELBENCHO_SINGLE_BIG_FILE_SIZE is optional. Direct reads repeat
until the time limit; buffered reads make at most one logical pass to avoid
re-reading warm page cache. Host assignment rotates between read cells to
reduce cross-run client-cache reuse.
utils/extract-elbencho.sh handles these results normally. See Recipe 4 in
BENCHMARK_RECIPES_FILESYSTEM.md.
This is the recommended metadata test. It runs create, stat, and delete phases
without MPI and rotates host assignment between phases to reduce client-cache
reuse. Configure MDTEST_BRANCH_FACTOR, MDTEST_ITEMS_PER_DIR, and
MDTEST_ITERATIONS, then run:
./storage-tests/fs/nv-mdtest-elbencho.sh \
--nodes 1,2,4,8 --tasks 64,128
./utils/extract-mdtest-elbencho.sh \
"$RESULTS_DIR"/mdtest-elbencho-<datestamp>/Dense single-directory runs (--single-dir-file-target):
The default layout creates a branched tree. To measure contention in one flat
directory, pass --single-dir-file-target <count>:
./storage-tests/fs/nv-mdtest-elbencho.sh --nodes 2 --tasks 64 --single-dir-file-target 1000000Dense mode requires one node count, one task count, and one generated target.
Every worker creates, stats, and deletes uniquely named zero-byte files in that
directory. Because elbencho assigns an integer count to each worker, the actual
total is nodes * tasks * round(target / (nodes * tasks)); requested and
actual counts are recorded.
Both analysis wrappers accept one or more result directories, filters, CSV
export/import, and --markdown. Elbencho reports IOPS or throughput and latency
by operation, size, thread count, node count, and I/O depth. Metadata reports
create/stat/delete rates, elapsed times, latency distributions, variance, and
scaling efficiency.
For per-client filesystem I/O diagnostics, set
ELBENCHO_LIVE_CSV_EXTENDED=1 before the run and analyze with
utils/extract-elbencho.sh --per-client-plots. Extended capture can generate
large files at scale; tune ELBENCHO_LIVEINT deliberately.
Network testing is beta and needs more work before it is ready for at-scale usage. The available netbench test still provides useful TCP throughput and latency measurements between small sets of clients.
It requires elbencho and at least two nodes. Permit peer traffic on
NETBENCH_PORT and that port plus 1000; --bidirectional also uses both ports
plus 1. Configure NIC speed, target runtime, block/response sizes, threads, and
iterations in env.sh, then run:
./storage-tests/network/nv-netbench.sh --nodes 2,4,8
./storage-tests/network/nv-netbench.sh --nodes 2,4,8 --bidirectionalBoth modes split nodes into groups A and B without localhost traffic. The
default runs A→B and B→A sequentially; --bidirectional runs them
concurrently. Transfer size is scaled by thread count so a saturated NIC runs
for approximately NETBENCH_TARGET_RUNTIME.
Analyze $RESULTS_DIR/netbench-{half|bidir}-<datestamp>/ with:
./utils/extract-netbench.sh "$RESULTS_DIR"/netbench-half-<datestamp>/The analyzer reports throughput, latency distributions, variance, and scaling
efficiency in terminal tables and plots. It also supports Markdown, CSV
export/import, and scale filters; see --help.
Set OBJ_BUCKET, OBJ_REGION, OBJ_HOST, OBJ_HOST_PORT, and an absolute
OBJ_AUTH_FILE path in env.sh. The bucket must already exist and must contain
no valuable objects: the test cleans it and deletes its benchmark objects.
Store credentials in a mode-0400 file:
export WARP_ACCESS_KEY="..."
export WARP_SECRET_KEY="..."Configure object sizes, threads, PUT/GET durations, and the minimum object count
with WARP_* variables, run validate_env.sh, then establish a single-node
baseline before scaling:
./storage-tests/object/nv-warp-sweep.sh --nodes 1
./utils/extract-warp.sh "$RESULTS_DIR"/warp-<datestamp>/
./storage-tests/object/nv-warp-sweep.sh --nodes 1,2,4,8For each node count and object size, the test cleans the bucket, PUTs until at
least nodes * WARP_PUT_MIN_FILES_PER_CLIENT objects exist, sweeps GET thread
counts, and performs final DELETE cleanup. Each node count is a separate Slurm
job or SSH invocation.
Optional modes are --multipart, --ranged, and --s3-express. Ranged reads
PUT one WARP_RANGE_OBJ_SIZE object per node and use WARP_OBJ_SIZES as range
sizes.
Aggregate PUT and GET request-rate budgets are available through
WARP_RPS_BUDGET_PUT and WARP_RPS_BUDGET_GET. WARP_PREFIXES selects static
prefixes and requires NVIDIA/warp-minio.
extract-warp.sh generates terminal tables and PNG plots for throughput, TTFB
latency, and scaling efficiency. It supports size/thread filters,
--per-client-plots, --to-json, and --from-json. Repeat --only-sizes or
separate several sizes with ;; commas are not split. Markdown output is not
implemented.
This project is not currently accepting external code contributions or pull requests. Bug reports, documentation corrections, and feature suggestions are welcome. See CONTRIBUTING.md for details.
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This project will download and install additional third-party open source software projects. Review the license terms of these open source projects before use.