fix(local-inference): RAM-class inference-memory policy — idle unload + pressure release so lmkd stops killing the app (#11760) - #11822
Conversation
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…le unload + pressure release + constrained caps (#11760) - InferenceMemoryPolicy.java: pure RAM-class policy (CONSTRAINED <7GiB usable per Pixel-6a #11506 forensics) — n_ctx 4096/8192, idle-unload 5/30 min, trim-level + availMem release decisions; 17 JVM unit tests. - ElizaBionicInferenceServer: idle/pressure policy scheduler frees the resident model+ctx+stream (the 2.25 GB GL mtrack from the forensics); next request reloads via ensureResidentCtx. releaseResidentAsync for onTrimMemory relay from ElizaAgentService. - ElizaAgentService: onTrimMemory relay, ActivityManager-backed pressure probe, RAM-class agent env (ELIZA_INFERENCE_RAM_CLASS, ELIZA_LOCAL_IDLE_UNLOAD_MS), constrained branded n_ctx 8192. - plugin-aosp-local-inference: mirrored TS policy + InferenceIdleUnloader wired around the fused in-process loader (18 bun tests). - mobile-resource: idle-reclaim workload + maxPostIdleUnloadRssMb gate so a policy regression fails loud. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…process leg (#11760) The bun agent never receives onTrimMemory (not an Android component), so /proc/meminfo MemAvailable is its pressure signal: constrained hosts free the loaded model below 12% available, standard below 5%. Pure decision fns + unloader tick coverage (6 new tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t op start (#11760) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…8_0 K / f16 V) (#11760) The retired QJL/Polar KV names now route into stock llama.cpp V-quant checks, which hard-fail without flash-attn (off on Android — scalar-FA race), killing every in-process generate with 'llm_stream_open: failed to init llama context'. K=q8_0 halves the K cache with no FA needed and matches ELIZA_1_KV_QUANT; V stays f16. ELIZA_LLAMA_CACHE_TYPE_K/V env overrides added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… release w/ correlated RSS reclaim (1.36GB->640MB->1.5GB), lmkd-survival (28 other apps killed, app 0) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7fc6762 to
d55adf3
Compare
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
d55adf3 to
64c0301
Compare
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
Rebased and cleaned the PR branch on top of current Validation on
Full Android Gradle without |
|
Local validation after rebase/scope cleanup:
Also verified the PR tree no longer includes generated |
…11734, refs #11760) (#11908) Hardware-lab acceptance row for the inference memory policy merged in PR #11822, on real Pixel 6a hardware (serial 27051JEGR10034, MemTotal 5589MB -> CONSTRAINED, nCtx=4096, idleUnloadMs=300000, no debug overrides), build 35f61d1 md5-verified on device after evicting a foreign APK a sibling session had installed. - 12.1 min foregrounded soak, 10/10 real local-inference turns (mobile-local-direct-reply, 52-58s warm, 95s cold first load) - 4 pressure releases observed: PSS 2.16->0.28GB, GL mtrack 1.60->0.05GB per release, MemAvailable ~0.7->2.0GB; released state held for the entire 7-min idle leg - transparent reload after each release: 58s turn (~+4s vs warm) - lmkd killed 48 processes across 27 other packages in the window; ai.elizaos.app killed 0 times, pid stable, exit-info before/after byte-identical (no new LOW_MEMORY of the main process) - key device-exact finding: the CONSTRAINED pressure lever (availMem < lmkThreshold+512MB = 728MB) structurally preempts the 300s idle lever on this device profile; idle firing remains covered by the emulator soak + JVM unit tests Verdict: PASS. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
LifeOps Multi-Tier BenchmarkSuite:
|
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Closes #11760.
Policy chosen
RAM-class-scoped footprint caps + idle unload + pressure-reactive release of the resident inference state — in the existing processes. No
android:processisolation.The #11506 forensics show lmkd killing
ai.elizaos.appat ~3.2 GB RSS on the 5.7 GB Pixel 6a because the resident inference state (model weights + KV cache + compute buffers — 2.25 GB of GL mtrack on the Vulkan host, ~1.2 GB of anon heap on the CPU path) is pinned for the life of the process, keeping the app the largest resident process at all times. Three levers fix that root cause directly:CONSTRAINEDbelow 7 GiB usable (Pixel 6a reports 5.7 GiB; 8 GB-nominal devices report ≥7.3 GiB) — drives the resident LLM context:ELIZA_LLM_N_CTX4096 (constrained) vs 8192 (standard) on the bionic host, and the branded in-process default drops 16384→8192 on constrained devices.ELIZA_LLM_N_BATCH=128(the compute-buffer lever) was already in place from the 8 GB-Mali work and is kept. Model tier needs no change: 2B Q4 is already both the catalog floor and the hardcoded mobile default.debug.eliza.inference.idle_unload_ms/ELIZA_LOCAL_IDLE_UNLOAD_MSoverride;0disables). Reload cost is the documented ~7-8 s resident rebuild (ensureResidentCtx) or the in-process lifecycle reload (ensureChatLoaded) — both pre-existing, both lossless (agent/app state lives outside the inference context; per-turn KV is re-prefilled anyway).onTrimMemoryrelay (RUNNING_LOW/RUNNING_CRITICAL/BACKGROUND+) + a 30 sActivityManager.getMemoryInfo()poll that releases within 512 MiB of the lmkd threshold on constrained devices (trim callbacks are deprecated/unreliable on API 34+; the poll is the dependable path).onTrimMemory, so/proc/meminfoMemAvailableis its pressure signal — release below 12 % available (constrained) / 5 % (standard).Why not process isolation: the Vulkan inference already runs in the only process that can reach the Mali driver (
ElizaBionicInferenceServer, in-app JNI — the musl bun agent's linker namespace cannot load the Vulkan HIDL closure). A separate:inferenceprocess would duplicate the JNI host + UDS plumbing, add its own overhead, and complicate the #11738-hardened service lifecycle — while the reclaim semantics it buys ("inference memory dies, app survives") are exactly what explicit release already delivers:resetResident()frees the GL mtrack deterministically and the next turn reloads. If post-policy field data still shows lmkd taking the app while a session is actively hot, isolation is the documented escalation; it is not needed to meet the acceptance.The #11738 FGS-restart behavior is untouched (no changes to the restart/denial paths;
onTrimMemoryis additive).Implementation
Android (app process):
InferenceMemoryPolicy.java(new) — pure decision logic: RAM-class classification (+debug.eliza.inference.ram_classoverride for emulator verification), per-class n_ctx + idle windows, trim-level decisions, availMem-margin decisions. 17 JVM unit tests (InferenceMemoryPolicyTest.java, first tests in:app:testDebugUnitTest).ElizaBionicInferenceServer.java— RAM-classELIZA_LLM_N_CTXdefault; single daemon scheduler (30 s tick) for idle + pressure release;releaseResident(reason)frees the resident model/ctx/stream underresidentLock(an in-flight decode always finishes its turn first; the idle window is re-checked under the lock); loud structured release logs.ElizaAgentService.java—onTrimMemoryrelay (async; never blocks the main thread),ActivityManager-backed pressure probe, policy-decision log, and RAM-class env export to the bun agent (ELIZA_INFERENCE_RAM_CLASS,ELIZA_LOCAL_IDLE_UNLOAD_MS), constrained-branded n_ctx 8192.Bun agent (in-process AOSP leg):
plugins/plugin-aosp-local-inference/src/inference-memory-policy.ts(new) — mirrored classifier (env →/proc/meminfo), idle resolution,MemAvailablepressure check, andInferenceIdleUnloader(in-flight-aware; never unloads mid-generate).aosp-local-inference-bootstrap.ts— every loader call routes through idle tracking; unload flows through the existingunloadModel+markEvicted→ensureChatLoadedreload plumbing (same path the voice handlers' out-of-band eviction already uses). 24 policy tests + full plugin suite green (99).Bug found + fixed by the on-device verification: the in-process AOSP chat load still requested the retired QJL/PolarQuant KV types (
qjl1_256/q4_polar). Current fused libs route those into the stock llama.cpp V-quant check, which hard-fails without flash-attn (deliberately off on Android — the scalar-FA race), so every in-process generate died withllm_stream_open: failed to init llama contexton the stock debug APK. Chat KV is now the Gemma path —q8_0K (halves the K cache, no FA needed, matchesELIZA_1_KV_QUANT) /f16V — withELIZA_LLAMA_CACHE_TYPE_K/Venv overrides.Gate (issue ask 3):
packages/benchmarks/mobile-resource: newidle-reclaimworkload (runs last, after the chat workloads leave the model warm) +maxPostIdleUnloadRssMbbudget — android-phone 1600 MB for both tiers. A regression that stops the unload leaves tail RSS at the warm footprint (≥2.3 GB measured) → fails loud.checkBudgetsgained per-workload check selection; 13node --testcases green.Emulator verification (arm64 AVD, Android 15,
CONSTRAINED— MemTotal ≈ 2.9 GiB at soak time)Full bundle:
.github/issue-evidence/11760-inference-memory-policy/. Highlights:ramClass=CONSTRAINED totalMem=2975MB lmkThreshold=216MB nCtx=4096 idleUnloadMs=120000.fast path done, latencyMs=3782,cacheTypeK=q8_0 cacheTypeV=f16.MemAvailable=233MBand321MB, bothramClass=constrained). The second correlates exactly inmeminfo-timeline.tsv: bun agent VmRSS 1.36 GB (loaded) → 640 MB (freed) → 1.5 GB (reloaded on the next request) while MemAvailable rose ~570 MB — the app was never killed.ai.elizaos.appwas killed 0 times;exit-infois clean ofreason=3 (LOW_MEMORY)(its only exits are this session'sUSER REQUESTEDforce-stops +PACKAGE UPDATEDreinstalls). This is the on-device inference memory: LMK kills the app at ~3.2 GB RSS (in-process Vulkan pins 2.3-2.7 GB) — need an unload / smaller-tier / isolate-inference policy #11760 acceptance shape: the app is no longer lmkd's target under real ambient pressure.Honest deltas vs the Pixel 6a target
CONSTRAINED(2.9 GiB — harder than the 5.7 GB Pixel 6a) and runs the CPU path (nolibggml-vulkan.sostaged → no Mali GL-mtrack term; the identicalresetResident/unloadModelrelease plumbing is unit-tested on both legs). The idle lever specifically kept losing the race to the pressure lever (correct: the emulator was genuinely pressured) and the app was restarted by sibling sessions every ~2-3 min, so the idle trigger on-device is covered by the unit tests rather than an isolated soak capture; the release mechanism it shares with pressure is proven on-device.maxPostIdleUnloadRssMb=1600) is a first emulator-informed ceiling; ratchet per BASELINE.md once the Pixel row lands.Verification
bun test __tests__(plugin-aosp-local-inference): 99 pass.:app:testDebugUnitTest: 17 pass;:app:compileDebugJavaWithJavacclean.node --test metrics.test.mjs: 13 pass.bunx tsc --noEmit(plugin): clean; biome clean on touched files.🤖 Generated with Claude Code