Documentation

Docs & FAQ

The deep documentation lives in the source repository beside the code it describes. This page is the map and the FAQ in full.

In the repository

How it is put together

Kotlin UI → com.arm.aichat inference library → JNI → llama.cpp. AiChat.getInferenceEngine() returns one process-wide engine that chat and benchmark share, and every native call runs on a single-threaded dispatcher - llama.cpp's context is not thread-safe across calls, so serialization is structural, not defensive. The native layer (ai_chat.cpp) owns the model, context, sampler, the two persistent thread pools (decode and prefill), affinity, and the graceful context-shift path that trims the oldest turns instead of hard-stopping. Chat history persists in SQLite; restoring a conversation re-primes the native KV through the same chat-template path as live turns.

Full component map and token flow: ARCHITECTURE.md.

FAQ

Does ENTITY need internet access?

No. Model loading, prompt processing, generation, chat storage and runtime metrics run locally, with no inference server or cloud API. Since v3.2.0 the app declares the INTERNET permission for one optional feature - the model catalog, which downloads a GGUF from a public host only when you tap one. Import your own model from storage instead and nothing in ENTITY ever opens a connection.

Which devices are supported?

arm64 Android phones on Android 13 or later. Seven Arm CPU backend variants ship and the strongest one the device supports is selected at runtime; there are no x86 binaries.

Which models can I use?

Pick one from the in-app catalog - seven vendors, 0.36B to 7B, each sized against the memory your phone reports free right now and tagged for whether it reaches Arm's KleidiAI kernels - or import any runnable GGUF through the document picker. A 1B model is a good starting point; a 3B-class model needs more free memory and may receive a smaller Auto context window.

How can the app recommend a model it has not downloaded?

It measures the two things inference is bound by, in about 60 milliseconds, with no model present. Decode reads essentially every weight once per token, so tokens per second tracks memory bandwidth divided by file size - measured with a 32 MB copy, too large for any phone's last-level cache, so it reads DRAM rather than SRAM. Prompt processing is a GEMM and tracks integer throughput on the performance cores, measured with a dependency-chained multiply-accumulate loop. Both are scaled against one device that was measured properly with llama-bench. It is an estimate and the screen says so: it cannot see thermal headroom, other apps' memory pressure, or the vendor's scheduler. It exists to keep you off a model your phone cannot serve, not to replace the benchmark.

Why does the app show free memory instead of my phone's RAM?

Because free memory is what a model has to fit into. A 6 GB phone with a browser and background apps resident often has under 2 GB to give, and sizing against 6 GB recommends models into memory that is already spoken for. Both figures are displayed so the number never looks like a wrong spec. The trade-off is that the verdict moves depending on what else is running - which is honest, since the phone really does have less room when it is busy.

What do the token details on a reply mean?

Long-press any answer. Prompt tokens are what the model read after the chat template was applied; generated tokens are what it wrote. The two rates are reported separately and deliberately never averaged: reading a prompt is compute-bound and happens in batches, while each generated token re-reads every weight and is bandwidth-bound. Timings cover model execution only, so markdown rendering and list redraws cannot show up as a slow model - which also makes these numbers comparable with the benchmark screen's pp and tg figures.

What does Auto mode change?

It ranks online cores by advertised max frequency for decode (2 to 6 cores, clamped), ranks cores by kernel cpu_capacity for prompt processing since v3.5.0 (usually wider, never narrower), sizes context from model size plus available memory, and enables the thermal guard.

Why not use every CPU core?

Because it is measurably slower - for both phases. Decode: 8 threads gives 8.8 tok/s where 4 gives 16.9 on the reference phone, because the efficiency cores gate the token-by-token path. Prompt eval: 135 tok/s on the 4 fast cores against 86 across all 8 - an A55 is about a third of an A78, so every GEMM waited on stragglers.

Is a smaller quantization always faster?

No - and on Arm this is the most expensive thing to get wrong. KleidiAI ships matmul kernels for Q4_0 and Q8_0 only; a Q3_K_L model is smaller on disk than Q4_0 and still leaves Arm's kernels idle: 42.7 vs 121 tok/s prompt, 12.1 s vs 4.3 s TTFT on the same phone and config. Decode moves the other way (16.9 vs 14.7) because it tracks bytes per weight. The model-info card tells you which case you are in.

Are power numbers trustworthy while charging?

No. Charging changes the battery-current reading, so ENTITY hides power and tokens per watt during charging. For comparable results: unplug, let the phone cool, run the same model and configuration.

So how much does core pinning actually earn?

Most of the gain is the thread count, not the pin - but the pin is real and device-dependent, not zero. Across twelve July runs, dropping 8 threads to 4 earned +81% to +106% decode; pinning measured ~0% then, +21% on the same phone in the later five-run export, +1%-but-lower-power on the Snapdragon 6 Gen 4, and -8.5% to +29.3% across the contributed dataset. It ships on by default because it wins on the reference device, credited as a second-order effect and exposed as a setting.

Does ENTITY use realtime scheduling or root-only controls?

No, and it cannot: cpufreq governors, scheduler policy, /proc/sys and realtime priority classes all need root. ENTITY uses the sanctioned unprivileged surface - sched_setaffinity, ggml thread pools with explicit masks, a cooperative thermal delay, and (since v3.6.0) an Android performance-hint session, shipped to be measured rather than assumed: the bench app carries an adpf arm and no claim is made for it until devices report back.

Can I turn core pinning off?

Yes, since v3.5.0: Settings > Inference > Core placement - Auto, Perf cores, or Scheduler. It is a setting rather than a default because the measurements do not support one answer; after a benchmark run, Settings reports which placement won on your phone and offers one-tap apply. The energy half of the verdict is suppressed if the run was charging.

Why does the app use two different thread counts?

Decode is memory-bandwidth-bound and saturates on a couple of cores; prompt processing is compute-bound and scales with width. Until v3.5.0 both used one number - correct on a 4+4 phone, wrong on any phone with a prime core, where prefill ended up on two threads. The contributed dataset is what exposed it.

Does ENTITY render LaTeX?

Yes, since v3.5.0. Inline and display delimiters are recognised; symbols and scripts map to Unicode where possible, and fractions and radicals draw on a Canvas with a proper rule and vinculum. Hand-rolled in Latex.kt - no dependency added - and currency is not mistaken for maths.

Can I reuse the Arm runtime logic in another project?

Yes - the Arm64 Android starter kit in the repository contains the pure-Kotlin runtime policy, a portable C++ affinity helper and a retargeting checklist. A starting point, not a substitute for measuring on the target phone.