Skip to content

model: enforce shard size at open (round-robin layout) - #55

Merged
marcobambini merged 1 commit into
sqliteai:mainfrom
mfethe1:fix/shard-size-at-open
Aug 28, 2026
Merged

model: enforce shard size at open (round-robin layout)#55
marcobambini merged 1 commit into
sqliteai:mainfrom
mfethe1:fix/shard-size-at-open

Conversation

@mfethe1

@mfethe1 mfethe1 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Companion to #53. A shard's size was never checked at open: bank_open validates
the record geometry, but nothing bound the file against the layout split_banks.py
writes. Under round-robin placement (bank_fetch: shard = e % n_sh) a short shard
lets the reader serve a neighbor record's bytes as the expert asked for, and a long
one hides the same confusion one record over. Both cases previously returned rc=0
with correct-looking output on untouched experts.

Found by negative control while re-verifying parity on 0.7.0, not by a failing test.

This rebases onto f8833c8, which added the O_DIRECT probe read at the same site.
The two checks are complementary and both are kept:

  • size is the whole-file claim — catches short and long shards, and needs no
    read, so it also covers containers whose record size is not a whole number of
    blocks (where the probe deliberately skips).
  • probe is the readability claim — a file can be exactly the right length and
    still not be readable the way the engine will read it.

Neither subsumes the other, so the size check runs first and the probe is unchanged
in behavior. recs is now computed once and shared: the
ceil((n_experts - s) / n_sh) form from the probe is equivalent to the
floor(n/k) + (s < n%k) form the original commit used, so this is a
simplification, not a semantic change. The size check is gated on n_sh > 1
because a single unsharded bank is bounded by the manifest already.

Gate: make clean (no new warnings) and tests/run.sh = 55 passed, 0 failed,
13 skipped
on macOS arm64. The skips are all missing-asset (K3 container,
tokenizer, vision source), unchanged from main.

A short or long shard previously loaded silently: bank_open checks
nothing about size, and the round-robin reader would serve a
neighbor record's bytes as the expert asked for. Bound each shard
against the file: shard s must hold floor(n/k) records plus one
for the first n%k shards, exactly what split_banks.py writes.

Found by negative control while re-verifying parity on 0.7.0:
long/short shards both previously returned rc=0 with correct-
looking output on untouched experts.
@marcobambini
marcobambini merged commit d44be90 into sqliteai:main Aug 28, 2026
9 checks passed
marcobambini added a commit that referenced this pull request Aug 28, 2026
#55's check is right and I should have written it. waste_file_size has
been in platform.h with a POSIX and a Windows implementation the whole
time; I looked for a portable size helper before f8833c8, grepped for
fstat/st_size, missed the name, and built a probe read to get part of what
a direct call gives. The size check is cheaper and strictly stronger for
the claim it makes, and the two are kept because neither subsumes the
other: size is the whole-file claim and needs no read, the probe is the
readability claim and a file can be exactly the right length and still not
be readable the way the engine reads it.

The justification is corrected. The branch said a short shard "would serve
a neighbor record's bytes as the expert asked for". It cannot: every
record carries the expert it belongs to and record_check reads it on the
way past. Two shards padded to the sizes N=2 expects but carrying a 3-way
split's records refuse on the first read -- "expert 7 of layer 1: record
header is not what the bank index describes" -- and produce nothing. A
misplaced record is a refusal, never a substitution.

What the check actually buys is *when*, and that is worth having on its
own. A long shard loaded happily and generated correct output before it,
since the extra records are simply never read; a short one waited for the
router to reach the missing expert, which on K3 can be thousands of tokens
in. Both are a refusal at load now.

The negative controls, on this branch:

    a correct 2-way split                      rc=0
    a shard one record too long                rc=1   (was 0)
    a shard one record too short               rc=1
    a 3-way split mounted as 2                 rc=1
    right sizes, another split's records       rc=1   (record_check)

Synthetic 66 passed / 0 failed / 10 skipped, K3 70 / 0 / 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
marcobambini added a commit that referenced this pull request Aug 28, 2026
Enforce shard size at open, companion to #53. Rebased onto the O_DIRECT
probe and complementary to it: size is the whole-file claim and needs no
read, so it also covers containers whose record size is not a whole number
of blocks, where the probe skips; the probe is the readability claim.

Landed with the justification corrected to what a negative control shows.
A misplaced record cannot be served as another expert's -- record_check
reads the expert id out of every record header and refuses. What the size
check buys is when: a long shard loaded and generated correct output
before it, and a short one waited for the router to reach the missing
expert.
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