ADFA-5188 | Enable KV cache quantization and flash attention - #76
ADFA-5188 | Enable KV cache quantization and flash attention#76jatezzz wants to merge 15 commits into
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
24429e7 to
55b74ac
Compare
n_ctx was a fixed 4096. It is now chosen per load from the model's advertised context_length and free RAM (ContextSizePolicy, floor 4096, ceiling 16384), and the native prefill feeds the batch in slices so the larger context cannot overrun it.
The cache was always f16. A load now asks for q8_0 where the model's head widths divide into whole blocks, costing 34 bytes per 32 elements instead of 64 and so buying about 1.88x the context from the same RAM budget. Flash attention is requested as AUTO, since llama.cpp needs it for a quantized value cache; if a context is refused anyway, the native side retries at f16 at a shorter length.
55b74ac to
c45cce1
Compare
hal-eisen-adfa
left a comment
There was a problem hiding this comment.
Four things worth a look before this merges — details inline. In short: the native fallback allocates about the same number of bytes as the attempt that just failed, a failed new_context leaks the whole model, the KV budget doesn't account for the weights that get mmap'd right after, and there's a 16 KB page-size ABI change riding along unannounced.
Floor the native trained-context clamp at 4096 and log the n_ctx actually created; price the pre-flight warning at the floor to break its circularity.
Charge the model weights against the budget the quantized cache is sized from, and floor clamp_context at DEFAULT_N_CTX so the f16 fallback cannot drop a short-context model below the context it always had.
The f16 retry was sized out of the same RAM budget as the q8_0 attempt, so it asked the allocator for roughly the bytes that had just failed, and was byte-identical when quantization was off. Adds a second retry at the DEFAULT_N_CTX floor, the only lever that answers memory pressure, and frees the model and any partial handles when a load step fails instead of pinning the weights for the life of the IDE process.
…rd sizes Clamp both KV-budget subtractions so an oversized model can no longer underflow into the 16384 ceiling, read the metadata block once for both the embedding guard and the sizing, take free RAM after the parse, and append GgufHeader.contextLength.
Keep the pure ModelContextResolver.resolve that takes an already-parsed header, and have it answer with the q8_0 cache type and the f16 fallback size. The clamped KV-budget subtractions now guard the quantized path too, which is the one that can actually grow the context.
…al load Rescan for the architecture alone when the strict parse gives up, so an entry it rejects can no longer make an embedding model read as chat-capable. Free the context, batch, sampler and model when load() fails partway; they had no owner, so each retry leaked another. Reconcile the mmap comments with charging weights.
Take the architecture-only rescan and its test from ADFA-5187, so a model whose strict parse fails can no longer read as chat-capable. Keep 5188's LLamaAndroid and prebuilt AAR: both branches implemented the same partial-load cleanup, and only 5188's carries the 4-arg new_context that the q8_0 path needs.
…o fix/ADFA-5188-kv-cache-quantization
…n IO Price the pre-flight through estimateForSelection, which takes no free-RAM figure at all; drop the dead classify overload and the resolver pass-through.
itsaky-adfa
left a comment
There was a problem hiding this comment.
Re-reviewed at 46d69ec against the stacked base fix/ADFA-5187-dynamic-n-ctx. The KV quantization arithmetic is sound and I verified it end to end; the findings are concentrated in the native fallback ladder, which is the part nothing exercises.
Re-check of the previous round (4 findings from @hal-eisen-adfa, all four threads currently marked resolved):
- Fallback retries at essentially the same allocation size (llama-android.cpp) - addressed, but incompletely, and the gap is the one IMPORTANT finding in this round. Replied in the thread and reopened it; f570d09 added the 4096-floor retry, but its guard still fires an attempt that needs more memory than the one that just failed in the common case.
- Failed
new_contextleaks the whole model - fixed, verified at head, not on the reply:LLamaAndroid.kt:286-293frees sampler, batch and context in reverse order and thenfree_model(model)before rethrowing, covering thenew_batchandnew_samplerbranches too. - KV budget ignores the weights mmap'd right after - fixed, verified at head:
ContextSizePolicy.kt:82-84now subtractsweightBytesbeforeRUN_BUFFER_BYTESand the divisor, andchoosereturns the floor outright whenmodelSizeBytesis null. - 16 KB page-size ABI change riding along unannounced - half fixed. The requested assertion landed and genuinely pins something: I extracted both AARs and
origin/mainhasp_align = 0x1000on five of six libraries (libomp.so was already0x4000) against0x4000on all six here, so the new test fails against the pre-fix artifact rather than passing vacuously. The description half was not done; replied in that thread and reopened it.
Verification run for this round
../gradlew testDebugUnitTestat head: 98 tests, 0 failures, including ContextSizePolicyTest 27, KvCacheTypeTest 10, ModelContextResolverTest 7, ModelMemoryEstimatorTest 5, PrebuiltAarAbiTest 5.- The committed AAR is genuinely rebuilt from the new
.cpp, not a stale copy -libllama-android.socarries the new log strings (kv cache = %s,retrying at f16 with flash attention off,retrying at the n_ctx). Worth stating because a stale AAR would not have failed anything: JNI resolvesnew_contextby name alone, so a 2-arg native function would have silently ignored the two new arguments. - The new ELF offsets in
PrebuiltAarAbiTestare correct for ELF64 (e_phoff 32, e_phentsize 54, e_phnum 56, p_align 48, magic0x464C457FLE), and the probe window is ample - the program-header table here ends at byte 624 of 4096. - The q8_0 arithmetic in
ContextSizePolicyTest:158checks out by hand: the same budget gives 5000 -> 4864 tokens at f16 and 9411 -> 9216 at q8_0. - Overflow reasoning in
kvBytesPerTokenstill holds after the refactor: at the declared ceilings, elements peak at 2^43, so the q8_0 intermediateelements * 34reaches ~2^48 and the f16 product ~2^58 - both inside a Long, and f16 is indeed the dearer of the two as the doc claims.
Not a finding, recording it because it was raised and is wrong: a parallel pass flagged KvCacheType.supports deriving head width from embeddingLength / headCount as a SIGABRT risk when the guess is wrong. It is not. llama.cpp re-checks against its own hparams.n_embd_head_k/n_embd_head_v and returns nullptr (llama-context.cpp:2311-2327), which is the whole reason AUTO is used over ENABLED. A wrong Kotlin guess costs a failed context creation and a retry, exactly as the comment at :420 claims.
Device verification is still owed. Per the repo's CLAUDE.md, assemblePlugin succeeding is not verification for these plugins, and I have not run this on a device. Two things need it specifically: the 16 KB page-size change on a 16 KB-page arm64 device, and any rung of the fallback ladder at all.
Severity key: CRITICAL / IMPORTANT / MINOR / NITPICK, one per comment. This repo has no written approve/request-changes rule - CLAUDE.md covers conventions and verification but sets no merge gate, and there is no REVIEW.md or CONTRIBUTING.md - so the default applied: any confirmed IMPORTANT blocks.
… it shrinks Gate the floor retry on n_ctx alone, since flash attention off is not cheaper, and leave flash_attn_type at AUTO on both rungs. Price the selection estimate at f16, default fallbackNCtx to the native floor, label the clamps, and log the FA request.
itsaky-adfa
left a comment
There was a problem hiding this comment.
Requesting changes on one confirmed IMPORTANT, detailed in the reopened llama-android.cpp thread.
The 4096-floor retry that f570d09 added to answer the previous round does not shrink the allocation in the case the previous round was about. Its guard fires on flash_attn_type != DISABLED alone, so whenever the policy already returned its floor - and ContextSizePolicy.choose never returns below 4096, the same value as native DEFAULT_N_CTX - the retry re-requests the identical n_ctx and identical f16 cache with flash attention turned off, which adds the [n_kv x n_ubatch x n_head] KQ buffer the FA path never allocates (llama-graph.cpp:1287 vs :1329). On the 3 GB device from that thread it asks for more than the attempt that just OOMd.
To clear this: gate the retry on floor_ctx < current_ctx alone, and drop flash attention only where a quantized cache forced it.
Everything else is MINOR or NITPICK and none of it blocks. The KV quantization arithmetic is correct - I re-derived the per-token costs, the overflow ceilings and the two exact-value assertions by hand, and the 98 unit tests pass at head. The three other findings from the previous round are genuinely fixed at head, not just claimed fixed.
Two things still owed before QA, both noted inline: device verification of the 16 KB page-size change on a 16 KB-page device, and any exercise at all of the fallback ladder - no test and neither model in the description can reach it.
itsaky-adfa
left a comment
There was a problem hiding this comment.
Re-reviewed at 7d51871 against the stacked base fix/ADFA-5187-dynamic-n-ctx. The IMPORTANT from the last round is fixed, so nothing here blocks: two MINORs, one NITPICK, and two reopened threads that are both asks about the PR description rather than the code.
plugin-examples has no written approve/request-changes rule, so the default applied and MINOR does not block. One thing to flag: my earlier CHANGES_REQUESTED still stands on this PR and a COMMENT does not lift it. Say the word and I will approve, since the finding it was raised for is resolved.
Previous rounds, verified at head rather than from the replies
Round 1 (@hal-eisen-adfa):
- Fallback retried at essentially the same allocation size - FIXED. The floor rung is gated on
floor_ctx < current_ctxalone now, so it fires whenever the current context is above 4096. Worked the roomy case through with the test header: 14080 q8_0 (368 MB) -> 7424 f16 (365 MB) -> 4096 f16 (201 MB). The ladder finally has a rung that shrinks. - Failed
new_contextleaked the wholellama_model- FIXED. Thecatchfrees sampler, batch and context, thenfree_model(model), then rethrows. - KV budget took no account of the model weights - FIXED.
ContextSizePolicy.choosesubtractsweightBytesandRUN_BUFFER_BYTESbefore halving. - 16 KB page-size ABI migration unpinned - PARTLY FIXED, thread reopened. The test is done and genuinely pins; the body line is not.
Round 2 (mine):
- Floor retry still asked for more memory than the attempt that just failed (IMPORTANT) - FIXED, as above.
- Fallback turned flash attention off - CODE FIXED, thread reopened for the description half only.
- Neither native rung is exercised by a test - NOT FIXED. Deferral accepted with a reason; details in the thread. Worth a follow-up ticket, not a blocker here.
- Pre-flight estimate was no longer an upper bound - FIXED.
estimateForSelectionhardcodesKvCacheType.F16, andgivenASelectedModel_whenEstimating_thenTheCacheIsPricedAtTheFloorContextasserts againstF16_PER_TOKEN, so a revert tochooseKvCachefails the suite. fallbackNCtxdefaulted tonCtx- FIXED. The default is0, whichclamp_contextmaps toDEFAULT_N_CTX.- Success line logged
jn_ctxinstead of the clamped ask - FIXED, logsrequested_ctx. - Unrelated churn in the leak-cleanup hunk - FIXED, reverted; the hunk is now the signature plus the
new_contextcall. - Redundant second
choosecall - FIXED, structural onkvType == F16now.
What I checked independently
- The committed AAR really was rebuilt from the C++ at head. Every added format string (
flash attention = %s,retrying at f16 with n_ctx %d,context: %s n_ctx %d exceeds) is inlibllama-android.soat7d51871and absent at46d69ec; every removed one is gone. Only that library changed - the other five are byte-identical, consistent with the llama.cpp submodule pointer not moving. - The fallback design holds at the llama.cpp level.
libllama.socarries bothquantized V cache was requested, but this requires Flash Attentionand%s: failed to initialize the context: %s, so a refusal reaches the retry as a null return rather than propagating out ofllama_init_from_model. - Nothing on the Kotlin side budgets prompts against the requested context.
reportEffectiveContextSizereads the nativen_ctxback and warns when it differs, and no other caller storescontextTokens- so a shortened f16 fallback cannot desync prompt limits. - The new tests are not vacuous. For the test header q8_0 costs 26112 B/token against f16's 49152, and the resolver returns 14080 vs 7424, so
fallbackContextTokens < contextTokenspins a real gap.assertNotEqualsandatQ8are both still live assertions.
Coverage of this review
No device run, so per this repo's own rule I am not calling the change verified: the native ladder has still never executed here, and both models in the description take the happy path (head_dim 128 and 64 both divide 32). The /code-review pass I kicked off resolved "76" against the wrong repository - it returned findings about deploy-to-firestore-app-distribution.yml and gradle.properties, which are not in this PR - so its output was discarded in full and every finding above is hand-derived from the diff at 7d51871.
This cannot merge before #75; the base is still that branch, and it is currently level with main.
itsaky-adfa
left a comment
There was a problem hiding this comment.
Approving - this lifts my earlier CHANGES_REQUESTED. The floor retry no longer fires an attempt larger than the one that just failed, and I verified the fix at 7d51871 rather than from the reply: the guard is floor_ctx < current_ctx alone, and the roomy case now steps 14080 q8_0 (368 MB) -> 7424 f16 (365 MB) -> 4096 f16 (201 MB), so the ladder finally has a rung that shrinks.
Nothing outstanding blocks the merge. Before it goes in, two things are just description edits and cost nothing:
- Add the 16 KB page-size line to the body (the reopened
build.gradle.ktsthread) - the text you drafted is fine as-is. - Correct the body's "falls back to
f16KV cache and flash attention disabled", which this PR deliberately stopped doing, and the framing of it as enabling flash attention whenllama_context_default_params()already set AUTO.
The two MINORs and the NITPICK on this round are safe to merge as they stand; the stale LocalLlmSettingsViewModel comment has a one-click suggestion. The untested native rungs stay a known residual - better as a follow-up ticket than a blocker here.
Note this still cannot merge before #75.
main squash-merged ADFA-5187 as fa1f624, whose tree is identical to ba1d14b, already an ancestor of this branch. Every conflict was a phantom from the lost lineage, so all resolve to ours. Also drops ModelContextSizingTest.kt, which the merge silently re-added after 5188 renamed it to ModelContextResolverTest.kt.
Description
This PR implements KV cache quantization (
q8_0) and enables flash attention in thellama.cppcontext parameters to drastically improve memory efficiency and generation speed.type_kandtype_vto useGGML_TYPE_Q8_0and enables flash attention (LLAMA_FLASH_ATTN_TYPE_AUTO).f16KV cache and flash attention disabled.q8_0halves its byte size, allowing for much longer conversations before context is dropped and drastically reducing mid-generation crashes on 4–6 GB RAM devices. Flash attention mitigates generation latency on longer contexts.Details
Logs confirming
n_ctxinitialization, cache type used (q8_0vsf16), and fallback activations.Flash Attention Enabled
Q8_0 KV cache
Ticket
ADFA-5188
Observation
This implementation works in tandem with dynamic
n_ctxsizing and should be validated alongside ADFA-5187, as the memory measurement relies on both features working concurrently.#75 Needs to be merged first