Back to research

A 2B Language Model as One File on a 4 GB Raspberry Pi

Release

geist v0.8.2 ships Microsoft's BitNet b1.58 2B-4T as a single 1.2 GB executable for the Raspberry Pi 5 — model compiled in, zero-copy, offline after download. Measured on the 4 GB reference board, side by side with Microsoft's own bitnet.cpp, including what broke along the way.

Measured systems for practical intelligence - geisten

The most honest install instruction is the one with nothing to configure. As of v0.8.2, running a 2-billion-parameter language model on a Raspberry Pi 5 is three lines:

curl -L -o geist-bitnet https://github.com/geisten/geistlib/releases/latest/download/geist-bitnet-linux-arm64
chmod +x geist-bitnet
./geist-bitnet "The three largest moons of Jupiter are"

One file, 1.2 GB. It contains the engine — under 1 MB of compiled C23 — and the complete BitNet b1.58 2B-4T model. No Python, no model download step, no network access after the download. Run it with no arguments and you get a minimal REPL.

How a model fits into an executable

The weights are placed in the binary’s read-only data section at build time and loaded with geist_model_load_from_memory, which aliases them zero-copy. The kernel demand-pages the mapping exactly as it would an mmaped model file, so RAM cost does not change: peak resident memory measured 1.66 GB while generating, on a board with 4 GB. The pages are file-backed and evictable under pressure.

Two consequences worth stating plainly. First, cold start is storage-bound: the first run after boot took 14.3 s to the first token on our SD card, because 1.1 GiB of weights page in once. Every warm start after that: 0.6 s. Second, this only works with a linker that tolerates a >1 GB section — GNU ld does; Apple’s ld64 corrupted relocations and crashed (measured, reproducibly), which is why the single-file build exists for Linux/arm64 only.

Measured against Microsoft’s own runtime

Side by side on the same Pi 5: geist finishes 110 tokens in 7.1 s, bitnet.cpp needs 11.7 s

Same 4 GB board, byte-identical GGUF, both greedy at 4 threads, each take started below 57 °C with temperatures and commits visible in the frame, recorded sequentially so neither engine steals CPU from the other. This recording: 110 tokens in 7.1 s (15.5 tok/s end-to-end) against bitnet.cpp’s own timing line of 9.31 tok/s.

The frozen 10-repeat protocol says the same thing with error bars: 17.9 tok/s at short prompts, 15.0 at a 512-token context, spread under 1 %, and — new in the harness — energy from phase-separated PMIC probes: 0.44 J per decoded token, 0.20 J per prefill token. The full report is published as a gist, and make bench reproduces it on your hardware, competitor included.

What broke along the way

Findings we hit while producing this release, documented rather than hidden:

Limitations, openly

llama.cpp’s OpenBLAS prefill still leads on the Pi. The REPL is deliberately memory-less — there is no chat template, each line is an independent completion. And the single-file build covers Linux/arm64 only; macOS and x86 build from source or use the 2 MB CLI with any GGUF.

Everything is Apache-2.0: geisten/geistlib. The comparison methodology, more recordings, and the fairness controls are in docs/DEMOS.md.