feat(android): mapped weights as the Android loader configuration + a two-pool fit check before load (SKEEP-003 P5, S2.5) - #1084
Merged
Conversation
…d a two-pool fit check before load Closes #1038 (SKEEP-003 P5, S2.5, SKEEP-002, #921, #922). A phone has two memory pools and the planner totalled one number. The ART managed heap is hard-capped per app (256 MB, 512 MB with largeHeap) and holds every Kotlin array; mapped weights live in file-backed pages that do not count against that cap at all and compete for physical RAM instead, which the OS reclaims rather than killing the app. A single total cannot tell a model that will not fit from one that fits perfectly well as long as its weights are mapped. - `DeviceMemory` (RAM, the ART cap, the OS's own low-memory threshold) and `MemoryPlan.fitOn(device, weightsMapped)`: managed heap carries KV + forward + headroom, plus the weights only when they are not mapped; physical RAM carries everything, mapped pages included — they are evictable, not free. The RAM budget stays above the threshold the device itself declares. A failing fit names the pool that ran out and prices its advice against the actual shortfall, with "load with staging = MAPPED" first when that is what is missing. - `AndroidGguf`: `loader()` is the Android configuration of #1037's pipeline — `staging = MAPPED` by default; `deviceMemory(context)` reads `ActivityManager.MemoryInfo` plus `Runtime.maxMemory()`; `fits()` answers from the GGUF header before a byte of payload is read. - CI gains an `android` leg (`testAndroidHostTest`), and so does scripts/pr-gate.sh. Those host tests compile against androidMain, so nothing else in the gate proved that code even builds — `assemble` compiles it and runs nothing. SKEEP-002 stays Draft, with an implementation-status section recording what landed and what does not: packed weights still reach the heap, because the packed matmul SPI takes ByteArrays and a buffer-aware kernel needs the byte-order contract of #973 settled first. So the ceiling is lifted for dense checkpoints, not yet for a Q4_K_M one — which is exactly the M2-A5 criterion that stays open, together with the device measurements this repository has no hardware to make. DeviceFitTest (8 cases, every target): 640 MB of weights cannot live under a 512 MB cap and can when mapped; mapped pages still need RAM; the RAM budget respects the device's kill threshold; the first advice is to map; a fitting plan has no blocking pool. AndroidGgufLoadingHostTest (3 cases, Android compilation): the default loader maps, staging does not change the numbers, and the header-derived fit differs between mapped and heap by exactly the weight bytes. Gate: scripts/pr-gate.sh — all legs passed, including the new Android leg. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
📖 Documentation Preview The documentation has been built successfully for this PR. Generated Files:
Artifacts:
This comment will be updated automatically when the PR is updated. |
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 #1038 · Phase P5 · Milestone M2 · PRD M2-A5 · Proposal §4.8.2 · SKEEP-002, #921, #922
A phone has two memory pools
The planner totalled one number. On Android that cannot express the situation: the ART managed heap is hard-capped per app (256 MB, 512 MB with
largeHeap) and holds every Kotlin array, while mapped weights live in file-backed pages that do not count against that cap at all — they compete for physical RAM, which the OS reclaims under pressure instead of killing the app. One total cannot distinguish a model that will not fit from one that fits perfectly well as long as its weights are mapped.DeviceMemory— RAM, the ART cap, current heap use, and the OS's own low-memory threshold.MemoryPlan.fitOn(device, weightsMapped)— managed heap carries KV + forward + headroom, plus the weights only when they are not mapped; physical RAM carries everything, mapped pages included, because evictable is not free. The RAM budget keeps the device above the kill threshold it declares itself, floored for devices that report none.The Android configuration of one pipeline
AndroidGguf.loader()is #1037's loader withstaging = MAPPEDas the default — not a separate helper.deviceMemory(context)readsActivityManager.MemoryInfoplusRuntime.maxMemory(), andfits(context, path, ctx)answers from the GGUF header before a byte of payload is read, so an app learns it will not fit instead of finding out by being killed.Two things I did not claim
SKEEP-002 stays
Draft. I added an implementation-status section recording exactly what landed (phases 1–3, plus the fit check the SKEEP never had) and what did not: packed weights still reach the managed heap, because the packed matmul SPI takesByteArrays and a buffer-aware kernel first needs the byte-order contract of #973 settled — it has to know which order it is reading. So the ceiling is lifted for dense checkpoints, not yet for a Q4_K_M one.M2-A5 is not met. "Llama-1B Q4_K_M loads on a 2 GB device with no OOM" needs both the packed-mapping work above and a physical device this repository's CI does not have. That measurement belongs to the
skainet-decodesample in SKaiNET-transformers. Flipping the SKEEP to Implemented on the strength of the mechanism alone would have been a claim about hardware I never ran on.CI gained a leg it was missing
The Android host tests compile against
androidMain, and nothing in CI ran them —assemblecompiles that code and runs nothing, so the existingMappedGgufWeightsAndroidHostTestfrom #921 has never executed in CI.build.ymlnow has anandroidleg (testAndroidHostTest), and so doesscripts/pr-gate.sh.Acceptance
DeviceFitTest(8 cases, every target): 640 MB of weights cannot live under a 512 MB cap and can when mapped; mapped pages still need RAM (a device with 300 MB free fails on the RAM pool with plenty of heap); the RAM budget respects the device's own threshold; the first suggestion is to map, priced at exactly the weight bytes; a fitting plan has no blocking pool and no advice; heap already in use counts against the cap.AndroidGgufLoadingHostTest(3 cases, Android compilation): the default loader serves dense F32 from file-backed pages, the heap path is still reachable and produces identical numbers, the header-derived fit check answers before loading, and mapped vs heap differ by exactly the weight bytes.Gate
scripts/pr-gate.sh— all legs passed, including the new Android leg (:skainet-io-ggufand:skainet-io-corehost tests).Keeps develop green by
Additive: new types in the planner, a new Android-only facade, an extra optional parameter path already merged in #1037. Nothing changes on non-Android platforms, and nothing changes by default on Android either —
AndroidGgufis a facade a caller opts into.🤖 Generated with Claude Code