feat(io): one loader with quantPolicy × staging, one source factory for every format, suspending reads (SKEEP-003 P5, S2.4) - #1083
Merged
Conversation
…or every format, suspending reads Closes #1037 (SKEEP-003 P5, S2.4, proposal §7, decision #7). IO had three separate stories: a streaming loader that always read onto the heap, a `MappedGgufWeights` helper that mapped files but loaded no model, and three copies of the same `createRandomAccessSource` expect/actual — which had already drifted, the safetensors copy returning null on Kotlin/Native where the GGUF copy used pread(2). - `StagingPolicy { HEAP, MAPPED }` beside `QuantPolicy` on `StreamingGgufParametersLoader`: quantPolicy says *what the values are*, staging says *where the bytes live*. MAPPED maps the file once and serves dense F32 tensors as zero-heap views over its pages — what mattered on Android, where every heap array counts against the ART cap (#921/#922). Defaults to HEAP, so today's behaviour is unchanged, and it falls back to HEAP when the platform cannot map or the source has no path. - `MappedFile` + `openMappedFile` in io-core (JVM/Android actual over `MmapTensorSource`; null elsewhere), and `RandomAccessSource.filePath` — a defaulted member — so a source can name the file a loader should map. - `openRandomAccessSource` in io-core with one actual per platform. The three per-format functions are deprecated delegates; their eighteen platform actuals are gone. Safetensors gains native positional reads it should always have had. - `SuspendingRandomAccessSource` with `read(...)` (deliberately not `readAt`, so one class can serve both interfaces) plus `RandomAccessSource.asSuspending()`. `JsBlobRandomAccessSource` now implements it: a browser can read *any* range instead of only the preloaded 50 MB window. A remote HTTP-range implementation belongs with the module that brings an HTTP client and is not in this slice. Packed tensors still arrive as heap arrays under MAPPED, because that is what their kernels take until #973; that is stated where it happens. `StagingPolicyParityTest`: HEAP and MAPPED produce identical values under both quant policies, dense F32 is `MmapFloatTensorData` under MAPPED and `FloatArrayTensorData` under HEAP, and a source without a path falls back to the heap rather than failing. `RandomAccessSourcesTest`: the shared factory opens and names a file, returns null for a missing path or a directory, adapts to the suspending interface, and maps dense floats. Gate: scripts/pr-gate.sh — all legs passed (451 io tests green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Closes #1037 · Phase P5 · Milestone M2 · PRD §7 outcome · Proposal §7, §10 decision #7
Three stories, one pipeline
IO had a streaming loader that always read onto the heap, a
MappedGgufWeightshelper that mapped files but loaded no model, and three copies of the samecreateRandomAccessSourceexpect/actual — which had already drifted: the safetensors copy returnednullon Kotlin/Native while the GGUF copy usedpread(2), so the same file was streamable through one format's loader and not the other's.quantPolicy × stagingStagingPolicy { HEAP, MAPPED }joinsQuantPolicyonStreamingGgufParametersLoader. The two axes answer different questions — quantPolicy says what the values are, staging says where the bytes live — and the streaming-dequant path (#782) and the mapped path are now configurations of one loader rather than two code paths that can disagree.MAPPEDmaps the file once and serves dense F32 tensors as zero-heap views over its pages. That is the difference that mattered on Android, where every heap array counts against the ART cap (#921, #922). It defaults toHEAP, so existing behaviour is unchanged, and falls back toHEAPwhen the platform cannot map or the source has no path — a browser build behaves exactly as before.Supporting pieces:
MappedFile+openMappedFilein io-core (JVM/Android actual overMmapTensorSource,nullelsewhere), andRandomAccessSource.filePathas a defaulted member so a source can name the file to map without any implementation changing.One source factory
openRandomAccessSourcenow lives in io-core with one actual per platform. The three per-format functions became deprecated delegates and their eighteen platform actuals are deleted. Safetensors gains native positional reads it should have had all along.Suspending reads
SuspendingRandomAccessSourceexposesread(...)— deliberately notreadAt, so a single class can serve both interfaces — withRandomAccessSource.asSuspending()for file sources.JsBlobRandomAccessSourceimplements it, which means a browser can read any range instead of only the preloaded 50 MB window it had to keep to satisfy the blocking interface.A remote (HTTP range) implementation belongs with the module that brings an HTTP client; it is not in this slice, and the interface is what makes it possible without touching this code again.
What MAPPED does not do yet
Packed tensors still arrive as heap arrays under
MAPPED, because that is what their kernels take until #973 lets a packed kernel consume a view. Said where it happens, in the loader, not left to be discovered.Acceptance
StagingPolicyParityTest:HEAPandMAPPEDproduce identical values under both quant policies (element by element, four loads of the same file); dense F32 isMmapFloatTensorDataunderMAPPEDandFloatArrayTensorDataunderHEAP; packed staging is unchanged; a source without a path falls back to the heap and still returns the right numbers.RandomAccessSourcesTest: the shared factory opens a file and reports its path, returnsnullfor a missing path or a directory rather than throwing, adapts to the suspending interface, and maps dense floats out of a file region.Gate
scripts/pr-gate.sh— all legs passed:jvmTest·apiCheck·verifyNpmPins jsTest wasmJsTest wasmWasiTest(JS and Wasm compile and run with the new interfaces) ·linuxX64Test·assemble·:skainet-test:skainet-test-java:test.One note for the record: the first gate run failed on
SlicingTest.testPerformanceAccessPatternsin the browser — a pre-existing test that walks 500 000 elements through a slice view under Karma's per-test budget. It passed on a forced re-run and on the full second gate; it is unrelated to this change (no slicing code is touched here), but it is a flake worth knowing about.Keeps develop green by
New parameters with defaults equal to today's behaviour; a defaulted interface member; deprecated delegates instead of removed functions. The only deletions are the eighteen duplicated platform actuals, whose public entry points still exist and still work.
🤖 Generated with Claude Code