# Device Report — Pixel 3a XL The tender's first measured device: a 2019 phone pulled out of a drawer, wiped, stripped, and put under load. Short verdict — **it runs a 1.24B model at 8–10 tokens a second and cannot run a 3B**, so it is a grunt mind at the bottom of the scale and a very good station. Every number below was measured on the device, not read off a spec sheet. ## Identity | | | |---|---| | Model | Pixel 3a XL (`bonito`), made by Google, released 2019 | | SoC | Qualcomm SDM670 (Snapdragon 670) — `/proc/cpuinfo` reports `SDM670`; `ro.board.platform` says `sdm710` | | CPU | 8 cores: 6 silver at 300 MHz–1.708 GHz, 2 gold at 1.997 GHz | | GPU | Adreno 615, GLES 3.2, Vulkan 1.1 with `vulkan.compute` declared | | Memory | 3.68 GB usable of 4 GB nominal | | Storage | 50 GB data partition | | OS | Android 12L (API 32), build `SP2A.220505.008` | | Security patch | 2022-05-05 — this is the final build Google ever shipped for the device | | Screen | 1080×2160, 400 dpi | The patch level is not neglect. Support ended in May 2022, a VoLTE fix followed in June, and the September 2022 release was the last; it never bumped the patch date. There is no later Android 12 for this phone, and no Android 13. The only route to a current OS is a community ROM, which was not taken up. ## What was done to it Wiped to factory state, with the Google account removed *before* the erase so factory-reset protection had nothing to demand. Set up with **no accounts at all**: no Play Store, no Play Services, no sync. This is **scooping out its brain**, and the project has three words for the process this report is the first worked case of: - **Ingestion** — putting the fleet on a device at all. The umbrella, and the one to say when the mode does not matter. - **Scooping out its brain** — hollowing it: wipe, remove the accounts, disable everything that is not the node, and it becomes nothing else. This device. It is the step that made the difference here, and a better word than "stripping", which sounds like something done to a machine rather than a deliberate emptying. - **Giving it an implant** — the other mode: the fleet takes up residence on a device that keeps doing its other work, living alongside the owner's apps, accounts and battery. The phone in a pocket as if nothing happened, with a node in it. The two are not equal in trust, which the specification records: a hollowed device's only secrets are the fleet's, while an implanted one shares a boundary with whatever its owner does there. A consort or a station client can live that way; a mind host holding a bearer token should not. Then stripped. Four measures, in order of yield: | Measure | Effect | |---|---| | `pm trim-caches` | 3.1 → 12 GB free | | Removed the 8 restored apps, disabled Android System Intelligence | +0.2 GB available | | Disabled Play Services | +0.3 GB available | | Disabled 109 of 241 packages | the rest | Disabled: Play Store, GMS and its dependent apps (Photos, Gmail, Maps, YouTube, Chrome, Camera, the Google app, TTS, Android Auto, eSIM), the carrier stack (IMS/RCS, VoLTE provisioning, tethering entitlement, carrier setup), contacts, dialer, calendar, clock, calculator, printing, accessibility services, NFC, hotspot enrolment, OMA-DM, and the Verizon/Sprint/USCC provisioning plugins. Kept deliberately: launcher, SystemUI, Settings, package installer, permission controller, WebView, media and storage providers, the Wi-Fi and network stack, the RIL core (touching those tends to break the radio), Bluetooth, keychain and certificate installer, DocumentsUI, captive-portal login. Two things had to be replaced rather than removed: - **Play Services is the thing the Google apps refuse to live without.** With it gone, the dependent apps spend their time posting "enable Google Play services" alerts — Photos and Gboard worst of all. The answer is not to silence the alert but to remove the app that raises it. Notification and ring volumes were briefly zeroed while the chorus was being traced; both were restored to 5 of 7 afterwards, and the alerts stopped because their source was gone. - **Gboard is the only stock input method**, so disabling it would leave no keyboard. Simple Keyboard (630 KB, no Google dependency) was installed over `adb`, made the default IME, and then Gboard was disabled. One thing did need lending back. **"Wallpaper & style" will not run on a hollowed device**: it calls Play Services and crashes on the failed connection, and its "My photos" tab hands off to the gallery, which by then did not exist. Setting the wallpaper took re-enabling GMS, Photos and the picker for ten minutes, and disabling all of them afterwards. That is a fair picture of what hollowing costs — the settings that are really Google features rather than Android features stop working, and some of them are cosmetic enough that nobody notices until they want one. The implementation note now carries the fix rather than leaving it to a person with a phone in their hand: the mind-host app sets the wallpaper itself from a bundled image, because `SET_WALLPAPER` is an ordinary permission and the wallpaper lives in a system service the app can call. Provisioning cosmetics became part of the app's scope on the strength of this afternoon. ## Memory, at each stage | Stage | Available RAM | Free storage | User apps | Accounts | |---|---|---|---|---| | As found | 0.76 GB | 3.1 GB | 61 | 6 | | Clean, no accounts | 1.08 GB | 46 GB | 8 | 0 | | Stripped and silenced | **1.86 GB** | 47 GB | 0 | 0 | The as-found reading is the important one: on the stock device, **0.76 GB available** would have refused a 1B model outright, and the obvious conclusion would have been that 4 GB of RAM is not enough. It was not the RAM, it was the load. ## Load test llama.cpp build b12230, `android-arm64`, run from `/data/local/tmp` as the shell user — no root, nothing installed, no app. Model: Llama-3.2-1B-Instruct Q4_K_M, 762.81 MiB, 1.24 B parameters, pushed to the device and executed in place. **Thread sweep**, generation of 64 tokens, two repetitions each: | Threads | Generation t/s | |---|---| | 2 | 6.16 | | 4 | 7.53 | | 6 | 8.60 | | 8 | **9.82** | Throughput keeps climbing past the two gold cores, so the right default on this SoC is the whole cluster — 8 threads is 60% faster than 2. **Sustained and prompt runs**, 8 threads: | Test | Tokens | t/s | |---|---|---| | Generation, short | 128 | 9.88 | | Generation, two runs | 512 | 9.11 ± 0.29 | | Prompt processing | 512 | 18.57 | Eight to ten tokens a second is a hundred tokens in ten to twelve seconds: slow beside a desktop mind and entirely usable for grunt work — classify, extract, summarise, answer a short question. A minute of work costs 7% against a six-second run; twelve minutes of it costs 16%, which is the phone managing its own heat and is set out below. **A larger model fits, at about two-thirds the speed.** The same tests against Qwen2.5-1.5B Q4_K_M, 935 MiB: | | 1B | 1.5B | |---|---|---| | Weights | 763 MiB | 935 MiB | | Prompt, 512 tokens | 18.57 t/s | 13.33 t/s | | Generation, 128 tokens | 9.88 t/s | 6.82 t/s | | Generation, 256 × 2 | 8.25 ± 0.87 t/s | 6.52 ± 0.18 t/s | So the ceiling is not 1B — 50% more parameters cost about 30% of the speed, and the model still fits in 1.86 GB with room for a 4K context. What does not fit is 3B, at roughly 2 GB of weights before any context at all. The boundary lies between 1.5B and 3B, and the 1B remains the better bargain for grunt work. ## Context **The cache costs 32 KB per token.** Measured by restarting the server at two context sizes and reading the process: 4096 tokens put it at 980 MB resident, 16384 at 1,374 MB — 394 MB for 12,288 more tokens. So 8K costs 256 MB, 16K costs 512 MB, and 32K would cost 1 GB and still leave about 1.2 GB free. **Memory is not what limits context here**; the phone will hold 32K if asked. What limits it is the wait, and the wait is not constant — prompt processing slows as the prompt grows: | Prompt | Prompt processing | Before the first token | |---|---|---| | 512 | 18.57 t/s | 28 s | | 1,024 | 16.98 t/s | 60 s | | 2,048 | 15.10 t/s | 136 s | | 4,096 | 13.51 t/s | 303 s | Each doubling of the prompt costs about 8% of prefill speed, so the cost of context is superlinear in time even though it is linear in memory. A four-thousand-token domain is **five minutes** before the first token appears; eight thousand would be around twelve. For interactive work the useful ceiling is therefore **2–4K of prompt**, and the lower half of it if you want answers this minute — not because the device cannot hold more, but because beyond it the wait dominates the answer, and a 1.24B model is not really attending to 8K of it anyway. That is the same arithmetic the design's **context domains** are built on, one scale down. ## Thermals Sampled from `dumpsys thermalservice` — the HAL block, not the cached one — through continuous generation, with throughput read from the server's own timing lines: | Run | Duration | Throughput | |---|---|---| | Short | 64 tokens | 9.82 t/s | | Medium | 512 tokens, two runs | 9.11 ± 0.29 t/s | | **Sustained** | **24 × 256 tokens, about 12 minutes** | **8.25 ± 0.87 t/s** | The first two made heat look like a non-issue: a 512-token run peaked at 51 °C and finished cooler than it started. Sustained load says otherwise. - Peak **77.1 °C** on the CPU sensors, about two minutes in. - Gold cores pinned at their ceiling, **1.997 GHz**, for as long as the load lasted. - Battery climbed from 28.5 °C to **39 °C**, on charge throughout. - Throughput slid from 9.33 to 8.40 t/s over the first three minutes, in step with the temperature. - Idle again, the cores fell to 825 MHz and the sensors were back under 40 °C within two minutes. **Sustained work costs about a sixth of throughput, and it is not a cliff.** The tell is the variance: ±0.03 t/s on the short runs against ±0.87 on the sustained one, which is what a controller trimming clocks and letting them back up looks like from outside. A 1B model does not overwhelm this phone; it makes it work, and the phone pays about 15% of its speed for the privilege. Two things this run established that are not about heat: - **The model ends its own answers.** Both long generations stopped around 1,450 tokens with `truncated = 0` and no context shift — the essay had finished. Asking for 5,000 tokens simply stops when the text ends. - **Cancellation is unreliable.** A client killed with SIGTERM left the server generating for a further 177 seconds and 1,500 tokens before the socket failed; a client that timed out cleanly was cancelled within a second. What the endpoint does on disconnect depends on how the connection dies, so a station cannot assume that abandoning a request stops the work. The case has been fitted for years and is hard to get off; removing it would buy headroom. Worth doing for larger models — at 1B there is about 15% to recover, which is real but not decisive. ## Verdict | Role | Verdict | |---|---| | Mind host, 1B class | **Yes** — 8.25 t/s over twelve minutes, 9.8 at best, 1.86 GB available, 47 GB disk | | Mind host, 1.5B | **Yes, at 6.5 t/s** — 935 MiB of weights, and a 4K context still fits | | Mind host, 3B | **No** — ~2 GB of weights before any context | | Station | Yes, over-qualified — sensors, camera, Wi-Fi, always on charge | | Erlang node | No — Android is never a node | ## What this changes in the project - **The device floor table was pessimistic at the bottom end.** [[Low Passage Mind Host]] puts a 1B–3B grunt mind at 6–8 GB of RAM; this 4 GB phone runs 1.24B at 8.25 t/s over twelve minutes of continuous work. Free RAM after stripping is the number that decides, and nominal RAM predicts it badly. - **The preflight is a prediction and was right to be one.** From the spec sheet this device looked like a refusal; measured, it is capacity. The order still holds — refuse cheaply, then measure — but the refusal threshold wants to be about *stripped* memory rather than nominal. - **Android can host a model as a plain process.** No app, no Play Services, no root: push the binaries and the GGUF to `/data/local/tmp` and run. That is a cheaper mind host than the implementation note assumes, and it is worth knowing before writing a Kotlin app. - **But a plain process does not survive on its own.** The same server was killed by Android's low-memory killer when a second model loaded beside it, and nothing announced the death. So the mind-host app's real contribution is **survival rather than capability**: a foreground service, a wake lock and a battery exemption are what keep the endpoint answering when something else wants the memory. - **Use all the cores.** The instinct to pin inference to the two big cores costs 40% of throughput on this SoC. ## Caveats - **A bare process is not protected from Android.** Mid-benchmark, with the 1B server resident and a 1.5B model loading beside it, the running server was killed by the low-memory killer. Nothing failed loudly — the port simply stopped answering. A mind host that has to stay up needs the foreground service, the wake lock and the battery exemption the app spec already calls for; they are load-bearing, not polish. - **Android 12 with a 2022-05-05 patch level, permanently.** LAN-only, no public address, no personal accounts; the trust model is the network, not the device. - **The battery is the wear item.** On charge permanently, in a case, on a 2019 cell. - **Memory pressure from the OS is real.** During the load test the model's pages were partly file-backed and reclaimable, so available memory never fell below 1.9 GB — but a running app with a resident model will see less headroom than a bare binary reading a mapped file. - **Context is cheap in memory and expensive in time.** 32 KB of cache per token, and 13–19 tokens a second to read a prompt, slower the longer it gets. The test server runs at 8192; conversations much past 4K spend more time preloading than answering. - **One device is not a table.** The revision below rests on a single 2019 phone. A 3 GB device and a 6 GB one measured the same way would say whether the difference is memory or the stripping — that is the next research item, and the method here is the one to repeat. - **The GPU cannot be used with llama.cpp's Snapdragon build.** That build links an OpenCL backend directly, and the backend fails to load here: `clCreateBufferWithProperties` is missing from the Adreno 615 driver — the phone ships OpenCL 2.0, and that entry point arrived in 3.0. The Snapdragon binaries therefore will not run at all on this device, GPU or no GPU. CPU only, and the generic `android-arm64` build is the one that works. ## Reference - [[Low Passage]] — the specification; the tender class is what produced this report. - [[Low Passage Research]] — the evidence behind the design, including the hardware floor this report revises. - [[Low Passage Mind Host]] — the Android app spec; this device is its first test target. - *House Network Map* — the LAN inventory, in the private vault, where this device's address and the Wi-Fi credentials live.