xnvmeperf#
xnvmeperf is a multi-threaded async I/O benchmark for NVMe devices. It
runs a time-bounded async I/O loop and reports per-device and aggregate IOPS
and throughput. A verify subcommand checks data integrity by writing a known
per-LBA pattern and reading it back.
When built with CUDA support (XNVME_BE_UPCIE_CUDA_ENABLED), two additional
subcommands are available: cuda-run and cuda-verify. These run NVMe I/O
directly from CUDA kernels via the uPCIe CUDA backend, bypassing
the host I/O path entirely.
Usage: xnvmeperf <command> [<args>]
Where <command> is one of:
run | Run a benchmark against the given devices
verify | Verify data integrity by writing and reading back a known pattern
cuda-run | Run a GPU benchmark against the given devices (requires upcie-cuda backend)
cuda-verify | Verify GPU NVMe I/O data integrity (requires upcie-cuda backend)
htod-roofline | Host-to-device copy bandwidth ceiling (requires a GPU backend build)
See 'xnvmeperf <command> --help' for the description of [<args>]
See 'xnvmeperf --version' for the library version and the revision it was built from
xnvmeperf - NVMe async IO benchmark -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
run — Benchmark#
Runs async I/O against one or more NVMe devices for a fixed duration. Devices
are listed as trailing positional arguments. One thread is spawned per CPU in
--cpumask, e.g. 0x3 spawns two threads on CPUs 0 and 1. If there are
more devices than CPUs, devices are split evenly so each thread owns a slice and
runs one async job per owned device. If there are more CPUs than devices,
devices are assigned round-robin so multiple threads drive the same device
concurrently.
Usage: xnvmeperf run [<uri>...] [<args>]
Run a time-bounded async IO benchmark against one or more NVMe devices.
Devices are distributed across CPU threads pinned by --cpumask or --cpulist.
With --be 'io_uring_dmabuf' the IO buffers are allocated on the GPU given by
--gpu-id and exported as a dma-buf for the kernel to DMA into.
Positional arguments:
[uri ...] ; Device URI e.g. '/dev/nvme0n1', '0000:01:00.1', '10.9.8.1:8888', '\\.\PhysicalDrive1'
Where <args> include:
--iopattern STRING ; IO pattern (read, write, randread, randwrite)
[ --nqueues NUM ] ; Number of queues per device
--qdepth NUM ; Use given 'NUM' as queue max capacity
--iosize NUM ; Use given 'NUM' as bs/iosize
--runtime NUM ; Run for 'NUM' seconds
[ --cpumask STRING ] ; Hex CPU bitmask for thread pinning (e.g. 0x3)
[ --cpulist STRING ] ; Comma-separated list of CPU ids or CPU id ranges for thread pinning (e.g. 0-3,4,6)
With <args> for backend:
[ --be STRING ] ; xNVMe backend, e.g. 'spdk', 'libvfn', 'upcie'
[ --subnqn STRING ] ; Subsystem NQN of the NVMe over Fabrics endpoint e.g. 'nqn.2022-06.io.xnvme:ctrlnode1'
[ --direct ] ; Bypass layers
[ --poll_io NUM ] ; For async={io_uring,libaio,kqueue}, enable hipri/io-compl.polling
[ --poll_sq NUM ] ; For async=io_uring, enable kernel-side sqthread-poll
[ --gpu-id NUM ] ; For be={upcie-cuda,upcie-hip}, GPU ordinal to use
[ --homi-id NUM ] ; The HOMI identifier to share controllers under
[ --report-freq FLOAT ] ; Report progress every 'FLOAT' seconds
[ --p2p-cq-mirror ] ; Complete into GPU memory, beside the data (XNVME_QUEUE_P2P_CQ_MIRROR)
[ --buf-host-bounce ] ; Read into a host buffer and copy it to the GPU, instead of P2P
[ --help ] ; Show usage / help
See 'xnvmeperf --help' for other commands
xnvmeperf - NVMe async IO benchmark -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
The title of the results block carries the library version and the revision
the library was built from, so a measurement can be traced back to the exact
commit. The revision is what git describe --tags --dirty=+ --always
reported at build time; a trailing + means the tree had uncommitted
changes. A source archive made with meson dist, such as the
xnvme-0.7.5.tar.gz CI builds from, carries the revision it was cut from.
A copy of the sources with neither git metadata nor that stamp shows
unknown in its place.
Example — sequential read on a single device:
xnvmeperf run --iopattern read --qdepth 32 --iosize 4096 \
--runtime 10 --cpumask 0x1 /dev/nvme0n1
Example — random write across two devices on two CPUs:
xnvmeperf run --iopattern randwrite --qdepth 64 --iosize 4096 \
--runtime 10 --cpumask 0x3 /dev/nvme0n1 /dev/nvme1n1
With --be upcie-cuda or --be upcie-hip the payloads land in GPU memory
while the CPU drives the queues. Adding --p2p-cq-mirror places the completion
queues there as well, so the controller’s completion writes no longer queue
behind its data writes; see Completions in device memory for when
that matters. Example:
xnvmeperf run --iopattern randread --qdepth 128 --iosize 512 \
--runtime 10 --cpulist 0 --be upcie-cuda --p2p-cq-mirror 0000:01:00.0
For cuda-run and cuda-verify, where the GPU issues the I/O, the queue
pair lives in GPU memory. Adding --sq-hostmem moves the submission queue
into host memory, which the controller fetches from faster than from GPU
memory; see GPU-Resident Queue API for the trade-off.
--buf-host-bounce keeps the I/O on a host backend and moves each payload
on to the GPU named by --gpu-id once it completes, with the runtime’s
asynchronous copy. Every queue slot has a host buffer of its own, page-locked
so the copy runs at link speed, and a slot is reused only once its copy has
drained, so when the copy is slower than the drive, submission waits for it.
At most 128 MiB of such buffers are in flight per device, so that the heap
stays within the GPU’s IOMMU aperture; the host heap is sized with headroom
for the per-buffer overhead and for the hugepages a served homi shares,
and kept under the 4 GiB at which the DMA mapping collides in that aperture,
which is what caps the usable --iosize. A buffer the runtime refuses to
page-lock is copied at the pageable rate, which the report then shows.
Example:
xnvmeperf run --iopattern randread --qdepth 64 --iosize 4096 \
--runtime 10 --be upcie --buf-host-bounce --gpu-id 0 0000:01:00.0
htod-roofline measures what that copy delivers on its own: it keeps
--qdepth page-locked copies of --iosize in flight to the GPU for
--runtime seconds and reports the host-to-device GB/s, taking no
devices:
xnvmeperf htod-roofline --iosize 4096 --qdepth 64 --runtime 10 --gpu-id 0
verify — Data integrity check#
Writes --count sequential I/Os with a per-LBA stamp pattern, then reads
them back and compares against the expected data. Reports the number of
mismatches and I/O errors per device, and exits non-zero when either is not
zero. On a namespace whose metadata is kept apart from the data, the metadata
is written and compared too, unless the backend takes no metadata buffer.
Usage: xnvmeperf verify [<uri>...] [<args>]
For each device: writes --count sequential IOs with a per-LBA pattern,
then reads them back and compares against the expected data.
Positional arguments:
[uri ...] ; Device URI e.g. '/dev/nvme0n1', '0000:01:00.1', '10.9.8.1:8888', '\\.\PhysicalDrive1'
Where <args> include:
--iosize NUM ; Use given 'NUM' as bs/iosize
--count NUM ; Use given 'NUM' as count
With <args> for backend:
[ --be STRING ] ; xNVMe backend, e.g. 'spdk', 'libvfn', 'upcie'
[ --subnqn STRING ] ; Subsystem NQN of the NVMe over Fabrics endpoint e.g. 'nqn.2022-06.io.xnvme:ctrlnode1'
[ --direct ] ; Bypass layers
[ --poll_io NUM ] ; For async={io_uring,libaio,kqueue}, enable hipri/io-compl.polling
[ --poll_sq NUM ] ; For async=io_uring, enable kernel-side sqthread-poll
[ --gpu-id NUM ] ; For be={upcie-cuda,upcie-hip}, GPU ordinal to use
[ --homi-id NUM ] ; The HOMI identifier to share controllers under
[ --p2p-cq-mirror ] ; Complete into GPU memory, beside the data (XNVME_QUEUE_P2P_CQ_MIRROR)
[ --help ] ; Show usage / help
See 'xnvmeperf --help' for other commands
xnvmeperf - NVMe async IO benchmark -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
Example:
xnvmeperf verify --iosize 4096 --count 256 /dev/nvme0n1
cuda-run — GPU benchmark#
See also
- uPCIe CUDA
Backend setup, system configuration, and memory architecture.
- GPU IOMMU domain
Required with an IOMMU enabled. Without it the run succeeds while every I/O fails.
- GPU-Resident Queue API
The
libxnvme_cudaAPI used to create queues and dispatch commands from CUDA kernels.
Requires the upcie-cuda backend. All queues across all devices are driven
by a single CUDA kernel: each CUDA block owns one NVMe queue and each thread
within the block owns one queue slot. The grid has ndevs × --nqueues
blocks in total.
The slots of a queue form groups of one warp, 32 slots, that take turns: a group reaps the queue’s next batch of completions and refills the room they leave, while the other groups’ commands stay in service. Submitting the whole depth and then waiting for it would leave the queue empty for one GPU round trip per depth’s worth of I/O, so a single queue per drive could not reach the drive’s rate however deep it is. A depth below 32 is a single group.
A slot’s previous command may still be in flight when a group reuses it, since a group reaps the queue’s oldest completions and not necessarily its own, so a command identifier advances by the depth on every submission. Each thread fences its own entry before the group’s barrier: with two batches in flight, a single fence by the thread that rings the doorbell let the controller fetch entries before they were written and execute what it found there, a few per million. The stop flag lives in host memory, where reading it is a PCIe round trip on the turn’s critical path, so the kernel samples it every 32nd turn, and it rings the doorbell without a second fence, since every entry was fenced before the barrier.
Both --qdepth and --iosize must be powers of 2. Supported patterns are
read, write, randread, and randwrite.
Usage: xnvmeperf cuda-run [<uri>...] [<args>]
Run a time-bounded GPU NVMe I/O benchmark against one or more devices.
All devices run in parallel: one CUDA block per queue, all queues launched
in a single kernel. All devices must use the same LBA size.
Positional arguments:
[uri ...] ; Device URI e.g. '/dev/nvme0n1', '0000:01:00.1', '10.9.8.1:8888', '\\.\PhysicalDrive1'
Where <args> include:
--iopattern STRING ; IO pattern (read, write, randread, randwrite)
[ --nqueues NUM ] ; Number of queues per device
--qdepth NUM ; Use given 'NUM' as queue max capacity
--iosize NUM ; Use given 'NUM' as bs/iosize
--runtime NUM ; Run for 'NUM' seconds
With <args> for backend:
[ --be STRING ] ; xNVMe backend, e.g. 'spdk', 'libvfn', 'upcie'
[ --gpu-id NUM ] ; For be={upcie-cuda,upcie-hip}, GPU ordinal to use
[ --homi-id NUM ] ; The HOMI identifier to share controllers under
[ --report-freq FLOAT ] ; Report progress every 'FLOAT' seconds
[ --sq-hostmem ] ; Submit GPU-issued I/O from host memory (XNVME_QUEUE_SQ_HOSTMEM)
[ --help ] ; Show usage / help
See 'xnvmeperf --help' for other commands
xnvmeperf - NVMe async IO benchmark -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
Example — sequential read, four queues of depth 32 on two devices:
xnvmeperf cuda-run --iopattern read --nqueues 4 --qdepth 32 --iosize 4096 \
--runtime 10 --be upcie-cuda 0000:01:00.0 0000:02:00.0
Example — random write, single queue:
xnvmeperf cuda-run --iopattern randwrite --qdepth 64 --iosize 4096 \
--runtime 10 --be upcie-cuda 0000:01:00.0
cuda-verify — GPU data integrity check#
Uses the same queue topology as cuda-run. For each queue slot the kernel
writes a unique LBA-stamped pattern, then reads it back on the host and
compares against the expected data. This is intended to confirm that
cuda-run results reflect correct I/O rather than silent data corruption.
Usage: xnvmeperf cuda-verify [<uri>...] [<args>]
Write an LBA-stamped pattern to each device through GPU queues and read
it back, verifying that the data matches. Uses the same queue topology
as cuda-run so results are directly comparable.
Positional arguments:
[uri ...] ; Device URI e.g. '/dev/nvme0n1', '0000:01:00.1', '10.9.8.1:8888', '\\.\PhysicalDrive1'
Where <args> include:
--iosize NUM ; Use given 'NUM' as bs/iosize
[ --nqueues NUM ] ; Number of queues per device
--qdepth NUM ; Use given 'NUM' as queue max capacity
With <args> for backend:
[ --be STRING ] ; xNVMe backend, e.g. 'spdk', 'libvfn', 'upcie'
[ --gpu-id NUM ] ; For be={upcie-cuda,upcie-hip}, GPU ordinal to use
[ --homi-id NUM ] ; The HOMI identifier to share controllers under
[ --sq-hostmem ] ; Submit GPU-issued I/O from host memory (XNVME_QUEUE_SQ_HOSTMEM)
[ --help ] ; Show usage / help
See 'xnvmeperf --help' for other commands
xnvmeperf - NVMe async IO benchmark -- ver: {major: 0, minor: 7, patch: 5, vcs: '94a52e5'}
Example:
xnvmeperf cuda-verify --iosize 4096 --qdepth 32 --be upcie-cuda 0000:01:00.0