Skip to content

Linux direct reads: Engram falls back to buffered where a disk refuses sector reads; direct_gather names its error - #2610

Merged
ashhart merged 3 commits into
mainfrom
lead/dio-followups
Nov 8, 2026
Merged

ashhart merged 3 commits into
mainfrom
lead/dio-followups

Conversation

@ashhart

@ashhart ashhart commented Nov 8, 2026

Copy link
Copy Markdown
Owner

Two follow-ups from an audit of the engine's uncached read paths (the one that also found #2602's end-of-file error and #2608's ring hang).

Engram shards on disks whose direct reads need more than a sector

DeepSeek-V4.1's Engram reader opens shards O_DIRECT and reads row extents aligned to 512-byte sectors (engram_rows.zig:7, engram_io.zig:312-314). On a disk with 4 KiB logical blocks (or a file system that takes the O_DIRECT open but refuses the read), those reads fail with EINVAL, and no path fell back: good rows gave EngramReadFailed, in every reader mode (serial, threads, io_uring, AIO). Now openTable asks once, before the header read: it reads one sector at a sector offset on the O_DIRECT descriptor, and if that is refused with EINVAL, it clears O_DIRECT on the descriptor and reads the shard through the page cache, as a file system that refuses the O_DIRECT open already does. Every reader mode already handles a buffered descriptor, so the read path itself doesn't change. On a 512-byte-sector disk the check passes and nothing changes except one extra 512-byte read per shard at open. Bytes are identical either way.

direct_gather names its error

core/direct_gather (the direct-read loader for Spark 2.5, Dory, Gemma 4 and Qwen 3.5 on CUDA, and Flash Next's ModelOpt experts) turned any reader thread's error into ReadFailed. It now keeps the first failing reader's own error (for example EndOfFile from core/direct_io when a shard ends inside a span, or FileNotFound) and returns it after every reader has joined. Every caller still refuses (all use try; none matches ReadFailed).

Checks

  • Linux aarch64, ext4, as an ordinary user: zig build test-cuda-host test-dsv41-shared passes (exit 0), with the new tests: Engram rows byte-identical through each reader mode on a fake disk that refuses any offset or length that isn't 4096-aligned, a 512-aligned disk unchanged, and direct_gather/checkpoint_direct returning EndOfFile and FileNotFound by name for a shard shortened after open or removed.
  • macOS: zig build test 760/760 steps, test-cuda-host 345/345, test-dsv41-shared 101 pass (the Linux-only test skips); compile-checkpoint-consumers builds the x86_64-linux server; zig fmt --check clean; every file within 600 lines.

Any reader's failure used to reach the loaders as ReadFailed, so a
shard that ends inside a tensor it reads, or a source that is gone,
could not be told apart. readAll now returns the first reader's own
error (EndOfFile, FileNotFound, or direct_io's ReadFailed), through
Gather.read and Pair.wait alike, to checkpoint_direct's loaders and
Flash Next's experts, which pass it on with try.

Tests: a file cut inside the last of four pieces, on and off a 4 KiB
boundary, gives direct_io's own error for that read; a removed file
gives FileNotFound; on Linux, checkpoint_direct stops the layer a cut
lands in with EndOfFile after reading the layer before it.
Engram extents are 512-byte aligned. On a disk whose direct reads need
4 KiB (or a file system that takes the O_DIRECT open but not the read),
every extent's read failed with EINVAL, so good rows gave
EngramReadFailed in every reader mode; nothing fell back as direct_io
does. openTable now reads one sector at a sector first: refused with
EINVAL, it clears O_DIRECT on the shard's descriptor and the table is
buffered, as on a file system without O_DIRECT. The serial, threaded,
io_uring and AIO readers all read that descriptor, so the read path is
unchanged; a 512-byte disk keeps its direct reads and extents.

Test (Linux): a fake disk refusing direct reads off 4 KiB, or off 512
bytes, or none, under every reader mode: the same row bytes and the
same extents each time, direct except on the 4 KiB disk, which refused
only the check at open.
@ashhart
ashhart force-pushed the lead/dio-followups branch from 7625ced to e1ff593 Compare November 8, 2026 21:14
@ashhart
ashhart merged commit 52427c0 into main Nov 8, 2026
13 of 20 checks passed
@ashhart
ashhart deleted the lead/dio-followups branch November 10, 2026 16:34
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.

1 participant