Skip to content

tools/diskbench.c reports page-cache throughput on Linux, not disk - #22

Closed
fab2s wants to merge 1 commit into
sqliteai:mainfrom
fab2s:diskbench
Closed

tools/diskbench.c reports page-cache throughput on Linux, not disk#22
fab2s wants to merge 1 commit into
sqliteai:mainfrom
fab2s:diskbench

Conversation

@fab2s

@fab2s fab2s commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

tools/diskbench.c reports page-cache throughput on Linux, not disk

diskbench documents itself as measuring "random record reads, cache-bypassed
<- the number that sets tok/s", with the page cache bypassed via
"F_NOCACHE / O_DIRECT". On Linux it does neither: O_DIRECT never appears in
the file
, and nocache() is a no-op outside macOS.

Reported numbers therefore exceed the drive's physical link limit.

Reproduction

Samsung 970 PRO, PCIe Gen3 x4 (link ceiling 3.94 GB/s), Ubuntu 26.04,
kernel 7.0, 16 GB file, 3 MB records:

$ cc -O2 -o diskbench tools/diskbench.c
$ ./diskbench /mnt/nvme/.bench 16 3 12
seq write   :   2.10 GB/s
seq read    :  44.67 GB/s  (cache bypassed)
rand  8 thr :  65.72 GB/s  -> 5.26 tok/s at 12.5 GB/token cold

44.67 and 65.72 GB/s are 11× and 17× the link ceiling.

Cause

static void nocache(int fd) {
#ifdef __APPLE__                 /* entire body is macOS-only */
    fcntl(fd, F_NOCACHE, 1);
    fcntl(fd, F_RDAHEAD, 0);
#endif
}

and all three opens are unqualified:

int fd = open(g_path, O_RDONLY);                              /* rand_reader */
int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);    /* seq write   */
fd = open(g_path, O_RDONLY);                                  /* seq read    */

The engine handles this correctly — src/model.c:1355 notes "macOS says so with
fcntl. Linux needs O_DIRECT and Windows FILE_FLAG_NO_BUFFERING", guarded by
#if defined(_WIN32) || (defined(__linux__) && defined(O_DIRECT)). The tool is
out of step with the code it exists to characterise.

Fix

Adding the flag is sufficient: the buffers are already posix_memalign'd to
4096 and the record size is a 4096 multiple, so O_DIRECT's alignment
requirements are already satisfied.

+#if defined(__linux__) && defined(O_DIRECT)
+#define DIO_FLAG O_DIRECT
+#else
+#define DIO_FLAG 0
+#endif
+
 static void nocache(int fd) {
 #ifdef __APPLE__
     fcntl(fd, F_NOCACHE, 1);
     fcntl(fd, F_RDAHEAD, 0);
 #endif
+    (void)fd;
 }
@@
-    int fd = open(g_path, O_RDONLY);
+    int fd = open(g_path, O_RDONLY | DIO_FLAG);
@@
-    int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+    int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC | DIO_FLAG, 0644);
@@
-    fd = open(g_path, O_RDONLY); nocache(fd);
+    fd = open(g_path, O_RDONLY | DIO_FLAG); nocache(fd);

Verification

Same drive, same invocation, after the patch:

before after link ceiling
seq read 44.67 GB/s 3.15 GB/s 3.94 GB/s
random, 1 thread 36.75 GB/s 2.91 GB/s
random, saturated 65.72 GB/s 3.33 GB/s

3.33 GB/s is 85% of the link ceiling and saturates at 2 threads, which is what a
Gen3 x4 drive should do.

Unrelated, same file

The derived column prints -> X tok/s at 12.5 GB/token cold, where 12.5 GB/token
is K3's figure hardcoded. On a 48B model (1.61 GB/token measured) that column is
off by ~8×. Taking the per-token figure from the container's plan --json, or
dropping the column, would avoid the trap.

@marcobambini

Copy link
Copy Markdown
Member

Confirmed, and thank you — the diagnosis is exact. nocache() had an #ifdef __APPLE__ body and nothing else in it, and O_DIRECT appeared nowhere in the file.

For the record on how it got there: this is LEARNED §14 in the one place §14 did not reach. The engine's Linux bypass was written blind, found and fixed on 2026-07-28 (bank_open, src/model.c); the tool that exists to characterise the engine's I/O kept measuring RAM. §46's standing rule — before claiming anything is disk-bound, run diskbench and divide — therefore returned a fiction on Linux for that whole window. No published number is affected: every diskbench figure in docs/GATES.md, docs/EFFICIENCY.md and LEARNED §44/§46 was measured on macOS, where F_NOCACHE did work.

I have pushed an extended version on diskbench-fallback (def83ef) with your fix as its base and you as co-author. Two things on top.

1. Probe and fallback. The flag alone is not sufficient, for the reason bank_open already knows: O_DIRECT is accepted at open and refused at transfer — tmpfs does exactly this, and so would a device wanting a bigger block than we align to. With a bare flag such a filesystem produces short read -1 and a table of zeroes with no cause given. So it now follows bank_open: probe with one aligned transfer, fall back to a plain open plus POSIX_FADV_RANDOM, and label every row (cache bypassed) or (PAGE CACHE, not the disk), with a trailer saying what happened. A bench that quietly measures something else is worse than one that says it could not.

2. Your write change was right, and I nearly talked myself out of it. I had drafted a comment asking you to leave the write buffered: row 1 stands for the download and the conversion landing, those write through the page cache like everything else, and a subsequent O_DIRECT read writes back and invalidates the range anyway, so the leftovers cannot flatter row 2. That reasoning holds on Linux and is wrong on macOS — F_NOCACHE stops new pages being cached but does not evict resident ones, so a buffered write leaves the whole file in the UBC and every read row below reports RAM. Measured, 1 GB file on an M5 Pro: 8.07 GB/s sequential read with the write bypassed, 26.04 GB/s with it buffered. The original nocache() on the write fd was load-bearing and I had missed it. Bypassing the write on both platforms, as you wrote it, is correct.

Verification and its limit: macOS is unchanged against main within noise (7.85 vs 8.01 GB/s sequential, 6.84 vs 6.74 random 1-thread). The Linux body compiles and runs here only against stubs for O_DIRECT and posix_fadvise — the same limitation §14 records for the engine itself. That covers both the probe-succeeds and probe-refused paths and confirms the write probe restores the file byte for byte, but it is not the kernel's real O_DIRECT behaviour. Your 3.15 / 3.33 GB/s remain the only numbers in this thread from actual Linux hardware. If you have the time to run diskbench-fallback on that 970 PRO — and the fallback path on tmpfs, where the probe should refuse and the rows should say so — that closes the last gap, and I would rather ship it with your numbers than with my stubs.

On the hardcoded 12.5 GB/token: agreed, and it is worse than the tool — docs/GATES.md carries it into a table header, so the trap is already in the docs. Separate change, left out of this one deliberately.

marcobambini added a commit that referenced this pull request Aug 5, 2026
…rint

The derived column read `-> %.2f tok/s at 12.5 GB/token cold` with 12.5
in the format string. That is K3's figure, so every run answered for K3
whatever container was being sized — ~8x off on a 48B model at 1.61
GB/token measured, and silent about the assumption, which is the part
that makes it a trap rather than an approximation.

It is now the fifth positional argument and has no default: without it
the column is not printed at all. Defaulting to 12.5 would have kept the
trap, and a tool cannot derive bytes-per-token from a scratch file — that
number belongs to a container, and `waste bench` already reports it as
"disk N GB total, M GB/token". The usage line says so.

docs/GATES.md's Gate H table keeps its "tok/s @12.5 GB/token" header: it
was a K3 decision, the figure is stated in the header rather than hidden
in a format string, and the numbers under it were measured with it.

Reported by fab2s alongside the O_DIRECT bug in
#22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@marcobambini

Copy link
Copy Markdown
Member

Superseded by #25, merged as ffb7ebc — your O_DIRECT fix is its base commit, with you as co-author, plus the probe-and-fallback for filesystems that refuse the flag, per-row labelling of whether the bypass was obtained, and the GB/token column you flagged (now an argument with no default, so it cannot silently answer for K3 again).

Thank you for the report. The numbers from real Linux hardware are the part this repo cannot produce on its own — the Linux path is still only stub-verified here, as LEARNED §14 records for the engine itself, so if you ever run the merged version on that 970 PRO the result is worth an issue.

@marcobambini

Copy link
Copy Markdown
Member

Both fixes are in v0.6.6, as def83ef and c65e13d — the PR was closed because they landed through #25 rather than by merge, but the O_DIRECT fix is yours as written.

The bypass. diskbench documented itself as reading with the cache out of the way and did neither on Linux: nocache() had an #ifdef __APPLE__ body and nothing else in it, and O_DIRECT appeared nowhere in the file. Your 44.67 GB/s sequential and 65.72 GB/s random over a 3.94 GB/s link — 11x and 17x the ceiling — is the whole diagnosis in two numbers.

The flag alone was not enough, for the reason bank_open already knew: O_DIRECT is accepted at open and refused at transfer (tmpfs does this, so would a device wanting a bigger block than we align to), so a bare flag turns a refusing filesystem into "short read -1" and a table of zeroes with no cause given. It now follows bank_open — probe with one aligned transfer, fall back to a plain open plus POSIX_FADV_RANDOM, and label every row. A bench that quietly measures something else is worse than one that says it could not.

One thing your patch had that I nearly dropped, and was wrong to doubt: bypassing the write. The argument for leaving it buffered is that it models the conversion landing, and an O_DIRECT read invalidates the range anyway. That is false on macOS — F_NOCACHE stops new pages being cached but does not evict resident ones, so a buffered write leaves the file in the UBC and every read row below it reports RAM. Measured on a 1 GB file, M5 Pro: 8.07 GB/s sequential with the write bypassed, 26.04 GB/s with it buffered. The nocache() on the write fd was load-bearing.

Two more found while in there: a sub-page record silently rounded to zero and was divided by, and a failed sequential read just ended the loop and shortened the row rather than saying anything.

The tok/s column. Fixed as you flagged — 12.5 GB/token was K3's figure sitting in a format string, so every run answered for K3 whatever was being sized, ~8x off on a 48B model at a measured 1.61 GB/token. It is now the fifth positional argument with no default: without it the column is simply not printed. Defaulting to anything would have kept the trap, and a tool cannot derive bytes-per-token from a scratch file — that number belongs to a container, and waste bench already reports it as "disk N GB total, M GB/token".

Scope, for anyone finding this later. No published figure moves: every diskbench number in docs/GATES.md, docs/EFFICIENCY.md and LEARNED §44/§46 was measured on macOS, where F_NOCACHE did work. What was affected is Linux users, from 2026-07-28 — when the engine's own bypass was written blind and fixed — until this release. That window matters more than a wrong number in a table, because §46's standing rule sends people to this tool before they are allowed to call anything disk-bound, so it was returning a fiction to exactly the question it exists to answer. It is written up as docs/LEARNED.md §49, "The bench that certified the disk was reading RAM".

Verified here on macOS as unchanged within noise (7.85 vs 8.01 GB/s sequential, 6.84 vs 6.74 random 1-thread). The Linux path compiles and runs here only against stubs, which covers probe-succeeds and probe-refused and confirms the write probe restores the file byte for byte — but not the kernel's actual O_DIRECT behaviour. Your 3.15 / 3.33 GB/s on the 970 PRO are the only real measurements of it, and they are credited as yours.

Thank you — this is the best kind of bug report: the tool that measures the thing was the thing that was wrong.

@fab2s

fab2s commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

diskbench v0.6.6 on real Linux hardware: two drives verified, tmpfs no longer refuses

Ran the merged tool on this machine. cc -O2 -o diskbench tools/diskbench.c,
kernel 7.0.0-28, Ubuntu 26.04, 16 GB file, 3 MB records, gb_per_token 1.61.

The bypass is real on both drives

Samsung 9100 PRO 2TB, PCIe Gen5 x4 (link ceiling 15.75 GB/s):

seq write   :  11.47 GB/s  (cache bypassed)
seq read    :  10.50 GB/s  (cache bypassed)
rand  1 thr :   7.97 GB/s  -> 4.95 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  2 thr :  10.64 GB/s  -> 6.61 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  4 thr :  10.70 GB/s  -> 6.65 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  8 thr :  10.80 GB/s  -> 6.71 tok/s at 1.61 GB/token cold  (cache bypassed)

Samsung 970 PRO 512GB, PCIe Gen3 x4 (link ceiling 3.94 GB/s):

seq write   :   2.01 GB/s  (cache bypassed)
seq read    :   3.16 GB/s  (cache bypassed)
rand  1 thr :   2.87 GB/s  -> 1.78 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  2 thr :   3.30 GB/s  -> 2.05 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  4 thr :   3.32 GB/s  -> 2.06 tok/s at 1.61 GB/token cold  (cache bypassed)
rand  8 thr :   3.32 GB/s  -> 2.06 tok/s at 1.61 GB/token cold  (cache bypassed)

Every row sits under its link ceiling, which is the check the old code could not
pass. The 970 PRO reproduces the numbers from the original report — 3.16 against
3.15 sequential, 3.32 against 3.33 random — so nothing about the fix moved them,
and the drive has since changed M.2 slots without changing its link (still
8.0 GT/s x4, still one hop from a root port).

The gb_per_token argument works as intended: same drive, 1.61 GB/token, and
the column simply does not appear when the argument is omitted.

The fallback path could not be exercised, and tmpfs is why

tmpfs accepts O_DIRECT on this kernel, at open and at transfer:

$ dd if=/dev/zero of=/dev/shm/dtest bs=4096 count=1 oflag=direct
1+0 records out

So the probe succeeds and the run is labelled bypassed:

$ ./diskbench /dev/shm/diskbench.tmp 4 3 4 1.61
seq read    :  19.00 GB/s  (cache bypassed)
rand  4 thr :  45.57 GB/s  -> 28.31 tok/s at 1.61 GB/token cold  (cache bypassed)

Nothing else here refuses either — ext4, tmpfs at /tmp, /dev/shm and
/run/user/1000 all accept it. So I could not reach the fallback branch, and the
(PAGE CACHE, not the disk) label is still stub-verified only.

One gap this exposes

That tmpfs table is 45.57 GB/s labelled (cache bypassed), and the label is
strictly true: the page cache genuinely was bypassed. But tmpfs is memory, so
there is no disk under it, and the row reads as a disk measurement. The tool's
implicit sanity premise — a figure above the link ceiling gives away a fiction —
cannot fire, because a memory-backed filesystem has no link.

That is a narrower version of the original bug rather than a return of it: the
old code reported RAM for a real disk, this reports RAM for RAM and calls it
bypassed. A one-line fstype note, or a warning when a bypassed read exceeds some
plausibility bound, would close it. Deliberately not proposing a patch — the
labelling is your design and you may consider this out of scope, since nobody
sizes a container against tmpfs on purpose.

What is still unverified

The probe-refused branch. If you know a filesystem that still refuses the flag on
a current kernel I will run it; on this machine there is not one.

pierre-x pushed a commit to pierre-x/waste that referenced this pull request Aug 6, 2026
`diskbench` documented itself as measuring cache-bypassed reads
(F_NOCACHE / O_DIRECT) and on Linux did neither: `nocache()` had an
`#ifdef __APPLE__` body and nothing else in it, and O_DIRECT appeared
nowhere in the file. Reported against a Samsung 970 PRO on Gen3 x4 —
44.67 GB/s sequential and 65.72 GB/s random against a 3.94 GB/s link,
11x and 17x the ceiling. With the flag, 3.15 and 3.33 GB/s, saturating
at two threads, which is what that drive should do.

This is LEARNED §14 in the one place §14 did not reach. The engine's
bypass was written blind on 2026-07-28 and fixed there; the tool that
exists to characterise the engine's I/O kept measuring RAM. §46's
standing rule — "before claiming anything is disk-bound, run diskbench
and divide" — therefore returned a fiction on Linux for that whole
window. No number in the docs is affected: every diskbench figure in
GATES.md, EFFICIENCY.md and LEARNED §44/§46 was measured on macOS,
where F_NOCACHE did work.

The flag alone is not enough, for the reason bank_open already knows:
O_DIRECT is accepted at open and refused at transfer (tmpfs does this,
so would a device wanting a bigger block than we align to), so a bare
flag turns a refusing filesystem into "short read -1" and a table of
zeroes with no cause given. So this follows bank_open: probe with one
aligned transfer, fall back to a plain open plus POSIX_FADV_RANDOM, and
label every row — a bench that quietly measures something else is worse
than one that says it could not.

The write is bypassed too, and that is not symmetry for its own sake.
Leaving it buffered — on the argument that it models the conversion
landing, and that an O_DIRECT read invalidates the range anyway — was
measured wrong on macOS: F_NOCACHE stops new pages being cached but does
not evict resident ones, so a buffered write leaves the file in the UBC
and every read row below reports RAM. 1 GB file, M5 Pro: 8.07 GB/s
sequential read with the write bypassed, 26.04 GB/s with it buffered.
The original `nocache()` on the write fd was load-bearing.

Also: a sub-page record silently rounded to zero and divided by it, and
a failed sequential read just ended the loop and shortened the row.

Verified on macOS — unchanged against main within noise (7.85 vs 8.01
GB/s sequential, 6.84 vs 6.74 random 1-thread). The Linux body compiles
and runs here only against stubs for O_DIRECT and posix_fadvise, which
covers both the probe-succeeds and probe-refused paths and confirms the
write probe restores the file byte for byte, but not the kernel's actual
O_DIRECT behaviour. The 3.15/3.33 GB/s figures above are the reporter's,
on hardware this machine does not have.

Reported and diagnosed by fab2s in
sqliteai#22, with the O_DIRECT fix as
written there.

Co-Authored-By: fab2s <fabrice.de.stefanis@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants