Skip to content

Commit a40632a

Browse files
badrishcCopilot
andcommitted
[Docs] Align device-IO docs with final code (smart io_uring ring default + knob ranges)
Correct the benchmark READMEs and configuration reference so they reflect the final shape of the Native device IO tuning surface: - Device.benchmark/KV.benchmark READMEs: io_uring no longer needs a manual --device-io-contexts to reach the ceiling. Document the smart ring-count default min(2 x cores, 64) (floored at the drainer count, decoupled from --device-completion-threads); libaio stays at rings = drainers. Update the uring example to drop the explicit --device-io-contexts and record the out-of-box default-rings result (8.45 M) alongside the under-provisioned (~2.9 M) and explicit-32 (8.00 M) rows. - Resp.benchmark README: reduce the stale scenario-2 "quick" GET table (whose uring rows predated the smart default and understated it) to the libaio rows plus a pointer to the authoritative Sample results matrix. - configuration.md: fill the two blank range cells for --device-uring-sqpoll-idle-ms ([0, 600000]) and --device-aio-max-devices ([1, 4096]) to match their [IntRangeValidation] attributes. Docs only; no code or behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 87fb956 commit a40632a

4 files changed

Lines changed: 30 additions & 19 deletions

File tree

benchmark/Resp.benchmark/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ numactl --cpunodebind=1 --membind=1 dotnet $RB -s --op GET --dbsize 100000000 \
8585
|---|---|---|---|---|
8686
| Libaio | srv node-0 / cli node-1 | 1.95 M | 6.17 M | **7.21 M** |
8787
| Libaio | no pin | 1.66 M | 4.84 M | 6.38 M |
88-
| Uring (`--device-completion-threads 32`) | srv node-0 / cli node-1 | 1.93 M | 5.45 M | 6.08 M |
89-
| Uring (`--device-completion-threads 32`) | no pin | 1.73 M | 4.74 M | 5.41 M |
88+
89+
> `Libaio` (the Linux default) is shown here for a quick look. For the full
90+
> backend × pin matrix — including `Uring` on out-of-box defaults, which reaches the
91+
> same peak — see [Sample results](#sample-results--8-nvme-ssd-raid-0) below.
9092
9193
- `--index 8g` for 100 M keys (default 128 m → 3–4× slowdown from hash chains).
9294
- Peak is at **t=64**: unlike the raw device (peaks at t=32), the RESP server's

libs/storage/Tsavorite/cs/benchmark/Device.benchmark/README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,11 @@ numactl --membind=0 --cpunodebind=0 dotnet $DB \
5353
--file-size 12801015808 --sector-size 512 --segment-size 1073741824 \
5454
--batch-size 4096 --device-throttle-limit 4096 --runtime 8
5555

56-
# uring — needs one ring per submitter: set --device-io-contexts >= --threads:
56+
# uring — the smart default sizes rings to min(2×cores, 64) (>= submitters here),
57+
# so it reaches the ceiling out of the box; only set --device-io-contexts for >64 submitters:
5758
numactl --membind=0 --cpunodebind=0 dotnet $DB \
5859
--file-name /raid/devbench.dat --device-type Native --device-io-backend uring \
59-
--device-completion-threads 8 --device-io-contexts 32 --threads 32 \
60+
--device-completion-threads 8 --threads 32 \
6061
--file-size 12801015808 --sector-size 512 --segment-size 1073741824 \
6162
--batch-size 4096 --device-throttle-limit 4096 --runtime 8
6263
```
@@ -65,13 +66,16 @@ numactl --membind=0 --cpunodebind=0 dotnet $DB \
6566
|---|---|---|---|
6667
| Native libaio | ct=8 (8 io_contexts) | 32 | **8.23 M** |
6768
| Native libaio | ct=8 | 64 | 7.7 M |
68-
| Native uring | ct=8, `--device-io-contexts 8` | 32 | 2.9 M (per-ring SpinLock cap) |
69-
| Native uring | ct=8, `--device-io-contexts 32` | 32 | **8.00 M** |
70-
71-
Both backends hit the `fio` ceiling, but **libaio needs only ~8 kernel io_contexts**
72-
(its io_context mutex is cheap) whereas **io_uring needs one ring per submitter**
73-
(`--device-io-contexts 32`) to escape the managed per-ring `SpinLock` — with the default
74-
8 rings it caps at ~2.9 M. NUMA pinning is ~neutral at the raw device layer (node-0
69+
| Native uring | ct=8, default rings (smart → 64) | 32 | **8.45 M** |
70+
| Native uring | ct=8, `--device-io-contexts 8` (under-provisioned) | 32 | 2.9 M (per-ring SpinLock cap) |
71+
| Native uring | ct=8, `--device-io-contexts 32` | 32 | 8.00 M |
72+
73+
Both backends hit the `fio` ceiling. **libaio needs only ~8 kernel io_contexts**
74+
(its io_context mutex is cheap), whereas **io_uring needs one ring per submitter** to
75+
escape the managed per-ring `SpinLock`. io_uring's ring count is **smart-defaulted** to
76+
`min(2 × cores, 64)` (floored at the drainer count), so with 32 submitters it uses 64
77+
rings and reaches the ceiling **out of the box** (8.45 M); explicitly under-provisioning
78+
rings below the submitter count (`--device-io-contexts 8`) exposes the SpinLock cap (~2.9 M). NUMA pinning is ~neutral at the raw device layer (node-0
7579
pin vs no pin within ±2%); it matters far more up the stack (KV/RESP). Peak is at
7680
`--threads 32` (32 submit + 8 drain ≈ node-0's physical cores); `--threads 64`
7781
oversubscribes and falls to ~7.5 M.
@@ -113,9 +117,11 @@ Peaks near the physical core count, then falls off. Use a large `--device-thrott
113117
`--threads`.
114118
- **`--device-io-contexts`** — kernel io_contexts / io_uring rings, decoupled from drainers.
115119
**libaio**: leave at default (= ct rings) — its io_context mutex is cheap, more
116-
rings are a no-op. **io_uring**: set **>= submitter `--threads`** (e.g.
117-
`--device-io-contexts 32` for 32 threads) so each submitter gets its own ring and escapes
118-
the managed per-ring `SpinLock`; otherwise uring caps at ~a third of libaio.
120+
rings are a no-op. **io_uring**: the **smart default** already sizes rings to
121+
`min(2 × cores, 64)`, enough for ≤ 64 submitters, so leave it unset in the common
122+
case; set it explicitly (**>= submitter `--threads`**) only when submitters exceed 64
123+
or to pin an exact count. Rings **below** the submitter count share a per-ring
124+
`SpinLock` and cap uring at ~a third of libaio.
119125
- **`--threads`** — 32 is the NVMe-array sweet spot (submit + drain ≈ node-0 cores);
120126
>32 oversubscribes and falls off. LocalMemory peaks near core count.
121127
- **`numactl --membind=0 --cpunodebind=0`** — near-neutral at the raw device layer,

libs/storage/Tsavorite/cs/benchmark/KV.benchmark/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ numactl --cpunodebind=0 --membind=0 dotnet $KV -n 100000000 -v 100 \
6161
--rumd 100,0,0,0 --load-threads 8 --run-threads-sweep 8,32,64 \
6262
--runsec 12 --warmup-sec 4 --data-path /raid/kv
6363

64-
# uring: add --device-io-contexts 32 (one ring per submitter; see Device README).
64+
# uring: no extra flag needed — the smart default sizes rings to min(2×cores, 64),
65+
# covering these run-thread counts (see Device README).
6566
```
6667

6768
| backend | pin | t=8 | t=32 | t=64 |
@@ -118,8 +119,10 @@ datasets touch few NAND dies and understate IOPS.
118119
- **`--device-completion-threads`** — native/localmemory drainer count (**8** on a
119120
fast array; localmemory: one SPSC ring per thread).
120121
- **`--device-io-contexts`** — kernel io_contexts / io_uring rings (native, decoupled
121-
from drainers). Leave default for libaio; for **uring set >= run threads** (e.g.
122-
`32`) so each submitter gets its own ring, else uring caps well below libaio (see
122+
from drainers). Leave default for libaio. For **uring** the smart default sizes rings
123+
to `min(2 × cores, 64)`, enough for ≤ 64 submitters, so leave it unset in the common
124+
case; set it **>= run threads** only beyond 64 (or to pin an exact count) — rings below
125+
the submitter count cap uring well below libaio (see
123126
[Device README](../Device.benchmark/README.md#nvme-storage-bound)).
124127
- **`-b` / `--batch-size`** — run-phase batch depth (ops issued per chunk before an
125128
opportunistic non-blocking drain). Default 1024. In-flight is bounded by

website/docs/getting-started/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ For all available command line settings, run `GarnetServer.exe -h` or `GarnetSer
200200
| **DeviceIoContexts** | ```--device-io-contexts``` | ```int``` | Integer in range:<br/>[0, 4096] | Linux-only, DeviceType=Native: number of independent kernel io_contexts / io_uring rings (ring COUNT), decoupled from --device-completion-threads. Critical for io_uring: set at or above submitter concurrency (roughly your connection count) so each submitter owns a ring and io_submit is contention-free; too few rings serialize submitters on a per-ring lock and cost up to ~3x. libaio is largely indifferent. 0 = device default. |
201201
| **DeviceQueueDepth** | ```--device-queue-depth``` | ```int``` | Integer in range:<br/>[0, 32768] | Linux-only, DeviceType=Native: per-ring kernel submission depth (maxEvents for io_uring_queue_init / libaio io_setup). Orthogonal to --device-io-contexts (ring count) and --device-throttle-limit (aggregate in-flight). 0 = device default. Note: for libaio, io-contexts x queue-depth is drawn from the global fs.aio-max-nr budget. |
202202
| **DeviceUringSqPoll** | ```--device-uring-sqpoll``` | ```bool``` | | Linux-only, DeviceType=Native + --device-io-backend=Uring: enable io_uring SQPOLL (IORING_SETUP_SQPOLL) so a kernel thread polls the submission queue and submissions are syscall-free. Each ring gets its own poll thread, so submission stays parallel across rings. Off by default (opt-in); ignored for libaio. Busy-polling kernel threads consume CPU, so benchmark it against the default per-submit path. |
203-
| **DeviceUringSqPollIdleMs** | ```--device-uring-sqpoll-idle-ms``` | ```int``` | | io_uring SQPOLL poll-thread idle window in milliseconds (sq_thread_idle): how long the kernel poll thread spins after the last submit before parking. 0 = native default (10s). Only meaningful with --device-uring-sqpoll. |
204-
| **DeviceAioMaxDevices** | ```--device-aio-max-devices``` | ```int``` | | Linux-only, DeviceType=Native (libaio): target number of Native devices to fit within the machine-global fs.aio-max-nr libaio budget (default 32). libaio io_setup permanently reserves io-contexts x queue-depth events from that global budget per device, so the default per-device reservation is capped at fs.aio-max-nr / this. To recover from io_setup EAGAIN, raise fs.aio-max-nr or raise this (a larger divisor shrinks each device's reservation); lower this to give each device a deeper reservation. Ignored for io_uring (no global budget) and non-Linux. |
203+
| **DeviceUringSqPollIdleMs** | ```--device-uring-sqpoll-idle-ms``` | ```int``` | Integer in range:<br/>[0, 600000] | io_uring SQPOLL poll-thread idle window in milliseconds (sq_thread_idle): how long the kernel poll thread spins after the last submit before parking. 0 = native default (10s). Only meaningful with --device-uring-sqpoll. |
204+
| **DeviceAioMaxDevices** | ```--device-aio-max-devices``` | ```int``` | Integer in range:<br/>[1, 4096] | Linux-only, DeviceType=Native (libaio): target number of Native devices to fit within the machine-global fs.aio-max-nr libaio budget (default 32). libaio io_setup permanently reserves io-contexts x queue-depth events from that global budget per device, so the default per-device reservation is capped at fs.aio-max-nr / this. To recover from io_setup EAGAIN, raise fs.aio-max-nr or raise this (a larger divisor shrinks each device's reservation); lower this to give each device a deeper reservation. Ignored for io_uring (no global budget) and non-Linux. |
205205
| **RevivBinRecordSizes** | ```--reviv-bin-record-sizes``` | ```IEnumerable<int>``` | | #,#,...,#: The sizes of records in each revivification bin, in order of increasing size. Supersedes the default --reviv; cannot be used with --reviv-in-chain-only |
206206
| **RevivBinRecordCounts** | ```--reviv-bin-record-counts``` | ```IEnumerable<int>``` | | #,#,...,#: The number of records in each bin: Default (not specified): If reviv-bin-record-sizes is specified, each bin is 256 records # (one value): If reviv-bin-record-sizes is specified, then all bins have this number of records, else error #,#,...,# (multiple values): If reviv-bin-record-sizes is specified, then it must be the same size as that array, else error Supersedes the default --reviv; cannot be used with --reviv-in-chain-only |
207207
| **RevivifiableFraction** | ```--reviv-fraction``` | ```double``` | Double in range:<br/>[0, 1] | #: Fraction of mutable in-memory log space, from the highest log address down to the read-only region, that is eligible for revivification. |

0 commit comments

Comments
 (0)