Repository navigation
fix(ple): strict shard schema/geometry, nofollow + bounded discovery, mutation evidence, and pinned-bank admission/rollback - #565
Open
Avicennasis wants to merge 4 commits into
Conversation
… before either backend allocates `source_from_safetensors` (disk) and `load_ple_table` (pinned) each parsed the `shard_<i>` headers on their own and accepted a table that merely covered the hash range: dtype and equal shapes were checked, but not the byte extent against `rows x cols`, extents against the file, the scale's dtype/shape/value, the layer the tensors belong to, or the total row count against the config. Both now share `ple_table_layout()`, which refuses by name (tensor, file, expected vs actual): a part that is not F8_E4M3 or not a positive 2-D shape, a shape that differs from the first part's, `data_offsets` that are not `rows x cols x 1` byte or run past the payload, a `weight_scale` that is not exactly one finite positive BF16 scalar, tensors from two layers, duplicate or non-contiguous part indices. `check_ple_geometry()` then requires the layout to be exactly the table the config addresses: `split_ngram_parts` parts, `ngram_head_dim` wide, on a configured PLE layer, and a total row count EQUAL to the padded sum of the per-head n-gram primes (`expected_ple_rows`, derived with the same `derive_ngram_hash_constants` the dummy-weight path uses; Qwen3.8-Flash-Next: 320,001,536 = 128 x 2,500,012). The model passes its args to `resolve_row_source`, so the disk backend is gated the same way as pinned. The scale is read as its two raw bytes instead of through `safe_open`, so a malformed header can no longer surface as a `SafetensorError`. The two existing callers that passed a bare `SimpleNamespace` now carry the geometry fields; the 28-row toy table in test_weight derives from primes 3+5+7+11 padded to 4.
`_safetensors_header` opened whatever path the index named with a plain `open()` and read as many bytes as the first eight said: a shard swapped for a symlink was followed, a FIFO would park the loader, and a corrupt length field was handed straight to `fh.read(n)` (u64 max -> OverflowError, a length one past the JSON -> an uncaught UnicodeDecodeError from the payload bytes, a six-byte file -> struct.error). Discovery now canonicalises each shard path exactly once (`realpath(strict)`, because the HF cache is symlinks into blobs/; a dangling link is refused by name) and every later open is `O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK` on that target, accepting only a regular file: a symlink that has since appeared at the canonical path is refused as "is a symlink; refusing to follow it". The header length is checked against a 64 MiB budget and the file size before a byte of it is read, and the refusal carries both numbers; the decoded header must be a UTF-8 JSON object or the refusal is a typed ValueError naming the file. The scale's two bytes are read through the same open.
…ts rows are read
Both backends parsed the shard headers once and then read payload bytes from
whatever sat at those paths later: the pinned loader per O_DIRECT read, the
disk store when it pinned its descriptors. Nothing tied the read to the file
preflight had admitted.
Preflight now records, per shard file, what it saw through the nofollow open
(`PleShardIdentity`: device, inode, size, mtime, ctime, header base and the
SHA-256 of the header bytes it parsed). `revalidate_ple_shard` re-opens the
canonical path nofollow and refuses with the field that moved ("changed after
preflight: st_ctime_ns A -> B", "... st_ino ...", "is a symlink") and, for a
same-inode edit whose timestamps were put back, with the digest that moved
("header changed after preflight: sha256 A -> B"). The pinned loader runs it
right before each file's first read and re-stats every file once the bank is
filled ("changed while reading"); `PleRowSource` carries the identities and
`DiskRowTable` runs it before `PleStore` opens its descriptors. Best-effort
concurrent-change evidence, as the reviewed T16 branch framed it, not
authentication of an untrusted checkpoint.
… any later failure
`--ple-backend pinned` allocated the 47.7 GiB `HostBank` with no look at the
host, and a failure after that (a read error, a shard that changed, a refused
cudaHostRegister) closed the progress bar and leaked the bank: the lazy mmap
stayed in `_LIVE_BUFFERS` with whatever had been read into it.
Admission now happens after the schema/geometry checks and before the bank
exists: `effective_memory_available()` is the tighter of `MemAvailable` and
the cgroup v2 headroom (`memory.max - memory.current`, a known zero when over
the limit; `None` when neither is readable, which admits best-effort like the
rest of the loader). The table must fit under 88% of it, and the refusal
carries the numbers ("needs N bytes ...; effective memory available is A
bytes, C after the 12% headroom"). `HostBank.discard()` unmaps an unpublished
bank and forgets it (a registered mmap cannot be unmapped without
cudaHostUnregister, which the extension does not expose, so such a bank is
only dropped by the object); the loader calls it from a `BaseException`
handler around fill and pin, releasing its memoryview first and never letting
the cleanup mask the failure. The `disk` backend allocates ~21 MiB of fixed
staging and needs neither.
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 3, 2026
… before allocating Both PLE backends share one scan that checks dtype, shape, data_offsets, the scale and the shard indices against the rows the config addresses; the pinned path also refuses a table that does not fit host RAM and frees its bank if the fill or pin fails. Alternative to upstream PR FlashML-org#565: keeps its schema/geometry check (shared by both backends, exact row count) and its pinned admission + rollback, built on available_host_memory(); leaves out the nofollow discovery and the per-shard identity/digest re-validation, which guard a local engine against tampered files at a third of the PR's size. Co-authored-by: Léon Avic Simmons <Avicennasis@gmail.com> Assisted-by: Claude Opus 5.5
KarrAcaRn
pushed a commit
to KarrAcaRn/FreeToken-ByAI
that referenced
this pull request
Oct 3, 2026
Logs FlashML-org#565 (reimplemented), FlashML-org#564, FlashML-org#562, FlashML-org#559 (with fixups), FlashML-org#558 (superseded by FlashML-org#305) and FlashML-org#305 (pulled forward, adopted). Assisted-by: Claude Opus 5.5
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on nothing: four commits directly on
main@0d652e7(git merge-tree --write-tree origin/main <head>is clean). 7 files, +979/-105; no C++ touched.What this is
The Qwen3.8-Flash-Next PLE n-gram table (128
shard_<i>F8_E4M3 tensors over 33 checkpoint files, 320,001,536 rows / 51,200,245,760 bytes) is read by two backends that share nothing:source_from_safetensors(--ple-backend disk, #311/#548) andload_ple_table(--ple-backend pinned). Each parsed the headers on its own and accepted whatever covered the hash range; neither tied a payload read to the file it had parsed; the pinned backend allocated 47.7 GiB without looking at the host and leaked the bank on any failure after that. This PR gives both backends one preflight and closes those gaps, one concern per commit, each with a failing test first.723a5ae).ple_table_layout()parses every PLE-bearing shard header and refuses, naming the tensor and file with expected vs actual: a part that is not F8_E4M3 or not a positive 2-D shape, a shape different from the first part's,data_offsetsthat are notrows x cols x 1byte or run past the payload, aweight_scalethat is not exactly one finite positive BF16 scalar, tensors from two layers, duplicate or non-contiguous indices.check_ple_geometry()then requires the table to be exactly what the config addresses:split_ngram_partsparts,ngram_head_dimwide, on a configured PLE layer, and a total row count equal (not merely covering) toexpected_ple_rows()— the per-head n-gram primes from the samederive_ngram_hash_constantsthe dummy-weight path uses, summed and padded tomake_ngram_vocab_size_divisible_by. The model passes its args toresolve_row_source, so the disk backend is gated the same way as pinned. The scale is read as its two raw bytes instead of throughsafe_open, so a malformed header no longer surfaces as aSafetensorError.6324c51). Shard paths are canonicalised once (realpath(strict=True), because the HF cache is symlinks intoblobs/; a dangling link is refused by name) and every later open isO_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_NONBLOCKon that target, accepting only a regular file — a symlink that has since appeared at the canonical path is refused as "is a symlink; refusing to follow it". The header length is checked against a 64 MiB budget and the file size before a byte of it is read, with both numbers in the refusal; the header must decode as a UTF-8 JSON object (a length one past the JSON used to escape as an uncaughtUnicodeDecodeError, u64-max asOverflowError, a six-byte file asstruct.error).45058cf). Preflight records, per shard file, what it saw through the nofollow open (PleShardIdentity: device, inode, size, mtime, ctime, header base, SHA-256 of the header bytes).revalidate_ple_shard()re-opens the canonical path nofollow and refuses with the field that moved ("changed after preflight: st_ctime_ns A -> B") or, for a same-inode edit whose timestamps were put back, with the digest ("header changed after preflight: sha256 A -> B"). The pinned loader runs it right before each file's first read and re-stats every file after the fill ("changed while reading");PleRowSourcecarries the identities andDiskRowTableruns it beforePleStoreopens its descriptors. Best-effort concurrent-change evidence, not authentication.1e18e28). Before theHostBankexists, the table must fit under 88% ofeffective_memory_available()(the tighter ofMemAvailableand cgroup v2memory.max - memory.current;Noneadmits best-effort like the rest of the loader), and the refusal carries the numbers.HostBank.discard()unmaps an unpublished bank and forgets it; the loader calls it from aBaseExceptionhandler around fill and pin, releasing its memoryview first and never letting the cleanup mask the failure.diskallocates ~21 MiB of fixed staging and needs neither.Not in this PR — already on main via #311 / #548
Discovery through the index and the consolidated-shard filter, bounded fd-pinned range reads (the C++ row store validates every extent against
fstatat construction, refuses out-of-range row ids, keeps one O_DIRECT fd per file, io_uring), admission-by-construction and no rollback need for the defaultdiskbackend. The C++ store is untouched.Evidence on the real table
Machine: worklappy — Intel Core Ultra 9 185H, 62 GiB RAM, NVIDIA RTX 3000 Ada Laptop GPU (8188 MiB, driver 580.178.04, CUDA 13.0), torch 2.11.0+cu130,
Qwen/Qwen3.8-Flash-Next-FP8@236dfdf2(33 PLE shards, sha256 verified against the Hub earlier the same day). An independent oracle (os.preadon the shards + numpy re-derivation of the hash from the checkpoint's own constants) is the reference throughout.expected_ple_rows(config)(0.096 s);check_ple_geometrypasses; forcingsplit_ngram_parts=129-> "PLE table needs shards 0..128, found 128";ple_layer_ids=(5,)-> "PLE table tensors are for layer 1, config ple_layer_ids is [5]".max_memory_allocated32.0 MiB, nvidia-smi <= 208 MiB, RSS 0.872 GiB while the 47.7 GiB table is served.model-00005as a link intoblobs/resolves at discovery (HF-cache layout loads,total_rows320,001,536); opening the link directly, or a link swapped in at the canonical blob path after preflight, -> "is a symlink; refusing to follow it".model-00035(same inode, same size, mtime restored): stat gate -> "changed after preflight: st_ctime_ns 1790637455308811250 -> 1790637455393811633"; with ctime forged back, the digest gate -> "header changed after preflight: sha256 ab0db134... -> 9805e6f0..."; the same edit against a builtPleRowSourcemakesDiskRowTable(...)refuse before the store opens a descriptor; a one-byte append -> "st_size 1600008336 -> 1600008337".effective_memory_available()61,184,512,000 B (56.98 GiB), ceiling 53,842,370,560 -> admitted; forced 40 GiB -> "PLE table needs 51200245760 bytes of host memory for the pinned bank; effective memory available is 42949672960 bytes, 37795712204 after the 12% headroom"; 54 GiB -> refused (ceiling 51,024,211,476); 55 GiB -> admitted and stopped atHostBank((320001536, 160)). No bank existed before any of the refusals (RSS delta <= 1 MiB).model-00021, holds part 64,utimed once 6.17 GiB had been read): refused after 14.4 s with "changed after preflight: st_mtime_ns 1790637463669848903 -> 1790637467496866138"; RSS 0.655 -> 23.168 -> 0.66 GiB, live mmaps 0 -> 0,torch.cuda.memory_allocated0 -> 0,Mlocked32768 -> 32768, no live CPU tensor > 256 MiB. Nothing published.PINNED; UVA gathers for T=1/7/64/256/1024 bitwise == oracle, GPU max|diff| 0.0, 0.066 ms/token; peakmax_memory_allocated6.4 MiB, nvidia-smi <= 286 MiB (273 samples at 500 ms, desktop included), RSS 48.503 GiB.tests/models/qwen4_exp/test_ple.py::test_pinned_uva_real_tablewithFREETOKEN_QWEN4EXP_MODELset: 1 passed in 33.11 s.test_ple_integrity,test_ple,test_ple_disk,test_weight,tests/kernels/test_row_store): 102 passed, 4 skipped, 1 failed, normal and-O— the failure istest_ple.py::test_track_snapshot_equals_a_prefill_stopped_at_the_boundary(torch.equalon tensors identical to four decimals), which fails identically on unmodifiedmainon this host and is unrelated to this change.Notes for review
load_ple_tablenow needs the geometry fields on its args (ngram_size,heads_per_ngram,ngram_vocab_size_base,make_ngram_vocab_size_divisible_by,ple_layer_ids); the model already passesQwen4ExpArgs, the two in-repo tests that passed a bareSimpleNamespacewere updated.HostBank.discard()cannot unmap a registered mmap (nocudaHostUnregisterbinding); the loader only discards before/at a failedpin(), where the mapping is unregistered.