Companion page to Brady Dibble's talk at SeaGL 2026, Friday 2026-10-23.
Every command and number needed to repeat the tests is here, along with the findings the 42-minute talk had no room for. Tests ran on three 2018 mini PCs, one 2024 laptop and one 2021 phone, between 2026-10-20 and 2026-10-22.
Disclosure: I work at CIQ as Director of Product. CIQ did not fund or review these tests.
This page was written with AI assistance.
See the slides, with both demo clips playing, or download the PowerPoint (18 MB).
Never run a model on your own computer? Start here: one install and one command.
You need a computer with 8 GB of memory and an internet connection for the first download. No graphics card is needed. Every command below was run exactly as written on a 2018 mini PC (Intel i5-8500T, 8 GB, no GPU, Rocky Linux 10) with Ollama 0.40.10, on 2026-10-23.
Ollama is a free program that downloads models and runs them on your computer. On Linux:
curl -fsSL https://ollama.com/install.sh | sh
It installs in about 30 seconds, starts itself in the background and says the API is available at 127.0.0.1:11434. A warning that no GPU was found is normal on a computer without a graphics card. If the installer says it needs zstd, install the zstd package with your package manager and run it again.
On a Mac or Windows PC, download Ollama from ollama.com/download instead. Those two were not tested here.
ollama run qwen3.5:4b --think=false "Write one shell command that counts the .log files under ./data, recursively."
The first time, this downloads the model, a 3.3 GB file (49 seconds on a fast connection). Then it answers in a few seconds:
find ./data -name "*.log" | wc -l
On the mini PC that took 3.6 seconds, at about 6 tokens per second (a token is about three quarters of a word).
--think=false matters on small computers. This model thinks out loud by default: for the same one-line answer it wrote 2,226 tokens of reasoning first, about 1,700 words, and took 6 minutes 26 seconds. Leave thinking on for hard questions and off for quick ones.
curl http://localhost:11434/v1/chat/completions -d '{"model":"qwen3.5:4b","reasoning_effort":"none","messages":[{"role":"user","content":"Say OK and nothing else."}]}'
The reply has the same shape as OpenAI's API, with "content":"OK" inside. Any tool that can talk to OpenAI can use your own model instead: point it at http://localhost:11434/v1.
A model only writes text. A harness turns that text into actions: it reads files, runs commands and checks the result. One command installs OpenCode, a free coding agent, and starts it with your model:
ollama launch opencode --model qwen3.5:4b --yes
On the mini PC, OpenCode started with the local model already selected. In a separate test it finished a one-line task ("list the three largest files in this directory") in a few minutes. That is slow, and it all ran on one small machine. Small models do best on short jobs with a clear result you can check.
With 16 GB or more, or a graphics card, try the larger qwen3.5:9b. On the 8 GB mini PC it fit in memory but wrote half a token per second, too slow to use. Section 3, Getting started, covers llama.cpp and the rest of the tested setup.
To remove the model later: ollama rm qwen3.5:4b. To stop Ollama running in the background on Linux: sudo systemctl stop ollama.
Governments use "sovereign AI" for national models and datacenters. The same idea works for one person or one team. Sovereign AI, as used here, means four things are yours:
A model is a file of numbers. llama.cpp is a free program that loads that file and answers over the same kind of API that Anthropic and OpenAI offer. A harness (pi, OpenCode, Claude Code) is the program that sends your request to the model, runs the commands the model writes, and sends the results back. Using a cloud model for one hard task can still be a sovereign choice when you make it on purpose.
The seven sections below give the reasons to run your own models, then what you give up.
A prompt sent to Claude or ChatGPT, and every file attached to it, is processed on the provider's servers. A local model processes the same prompt in the memory of the machine that runs it. In every local test on this page, prompts crossed only one home network, from the machine running the agent to the machine running the model.
Code, notes and documents pasted into a hosted model leave your control and fall under the provider's terms. Consumer chat plans can use your conversations for training unless you opt out, and you trust the provider with work you have not published. Paid fine-tunes are exposed too. OpenAI's deprecation notice of 2026-04-22 also removes fine-tuned models on 2026-10-23, ft-gpt-3.5-turbo among them, so a fine-tune a customer paid to train stops answering on that date. A model file on your own disk has no shutdown date: the 2023 Llama 2 7B file used below (4.08 GB, sha256 08a5566d61d7cb6b420c3e4387a39e0078e1f2fe5f055f3a03887385304d4bfa) ran on a 2021 phone in October 2026.
Running the model locally does not make an agent safe. Simon Willison calls the risk the lethal trifecta: an agent that can read private data, also reads untrusted text, and can send data out can be tricked into leaking that data. Removing any one of the three closes the path. The software supply chain carries risk too. LiteLLM 1.82.7 and 1.82.8, published to PyPI on 2026-03-24, carried a credential stealer; the web-search test below used 1.82.6, installed from a hash-checked wheel. The model card for nl2sh-1.5b, a small shell-command model, reports that 11.0% of its outputs on adversarial prompts would destroy data.
The same three abilities are what make an agent useful, so the fix is to split the work. Give each job only two of the three, and ask before anything leaves the machine. An agent that reads the web gets no secrets. An agent that holds secrets reads no outside text. Anything that sends data out, such as an email, a push or a web request, waits for your yes. For risky jobs, run the agent in a sandbox on a throwaway copy of your files; a bad command can still delete what is inside, so keep nothing there you would miss.
When a vendor runs every layer for you, you never learn how the layers work, and leaving gets harder every month. Running locally shows how much a tool depends on its vendor's servers. Claude Code completed 3 of 3 shell tasks against a local model. Its web search stopped working: llama-server offers no search tool, and the local model answered a question about llama.cpp's latest release with an invented tag, "v0.2.2, 2024-06-12". Claude Code also sent a 15,680-token system prompt with its request. For Qwen3.5-2B, reading that prompt takes at least 23 minutes on the 2021 phone, 3 minutes on a 2018 mini PC, 51 seconds on the 2024 laptop's CPU and 17 seconds on its built-in GPU. These are floors: they stretch reading speeds measured on 512 to 2,861-token prompts to a 15,680-token one, and longer prompts read slower (on the phone, a 16K context cut reading speed about 16%). The measured speeds: 11.2 tokens per second on the phone, about 85 on the mini PC for a real 2,861-token agent prompt, 308 on the laptop CPU and 914 on its GPU.
Hosted models charge per token, and prices can rise once you depend on them. Subscription plans also change: lower limits, or features moved to a higher tier. A local model costs hardware you may already own, plus electricity. The Claude comparison below (246 shell tasks, 150 homelab questions, 12 desktop tasks and pilot runs) cost $0.74 at API prices, as reported by Claude Code; the runs used a subscription. The local hardware dates from 2018 to 2024, and its electricity was not metered in these tests. Buying new memory got expensive: a Reddit comment put 96 GB of laptop DDR5 at $200 in October 2025, and a 2026-08-18 post reported 128 GB of DDR5 at $3,399.
A hosted model can change or disappear under a working setup. OpenAI's deprecations page lists 12 API model snapshots, among them gpt-4-turbo, o1 and o3-mini, with a shutdown date of 2026-10-23, each with a suggested substitute. On 2026-10-14 OpenAI announced that GPT-4o, GPT-4o mini, GPT-4.1 and GPT-4.1 mini leave the API on 2027-01-14. A prompt tuned for one model has to be retested on its substitute. A local model file stays byte-identical until you replace it. In the shell tests, both local models passed the same tasks in all 3 runs.
A hosted service can be down, slow or rate-limited. During the Claude comparison on 2026-10-22, Claude Code hit usage limits more than once, and the affected batches were rerun. The local runs waited only on the model. A model on your own machine keeps answering when the internet connection is down.
Small local models are weaker. On 25 questions about a homelab, each given the 5 best-matching passages of the homelab's notes, Claude Sonnet 5.5 answered 13 to 14 correctly and a 7-billion-parameter local model answered 10 to 11. Small machines are slow at reading long prompts. The local models did best on jobs with a checkable result: shell commands, fixes on a running system, and data extraction. Local is not free: it costs hardware, power, and your time to patch systems, update drivers and swap models.
Five devices ran models. None was bought for these tests.
| Device | Year | Processor | Graphics | Memory | Memory bandwidth | Storage, network |
|---|---|---|---|---|---|---|
| Mini PC 1, 2, 3: Lenovo ThinkCentre M920q | 2018 | Intel Core i5-8500T, 6 cores, 6 threads | Intel UHD 630, built in | 8 GB DDR4-2666, one stick (single channel) | 21 GB/s | NVMe SSD (Samsung PM981 in mini PC 2), 1 Gb Ethernet |
| Laptop: Lenovo IdeaPad Slim 5 16AHP9 | 2024 | AMD Ryzen 5 8645HS, 6 cores, 12 threads | AMD Radeon 760M, built in, shares system memory | 16 GB LPDDR5x | 102 GB/s | NVMe SSD, Wi-Fi 6 |
| Phone: Samsung Galaxy S21 Ultra (SM-G998U) | 2021 | Qualcomm Snapdragon 888, 8 cores | Adreno 660 (unused, see section 6) | 12 GB | not measured | Built-in flash, USB to a computer for adb |
The mini PCs and the laptop ran Linux. The phone ran stock Android with no root; llama.cpp ran from /data/local/tmp over adb. Two other machines supported the tests without serving a model: a Linux desktop that ran the router and the test scripts, and a workstation that ran the Claude comparison.
The three mini PCs boot over the network from one shared image with Warewulf, an open-source tool for provisioning cluster nodes. Disclosure: CIQ, where I work, sells support for Warewulf; these machines run the free open-source release. Warewulf was not benchmarked. It only puts the operating system on the machines.
Terms used below. A token is about three quarters of a word. Reading speed is how fast a machine processes your prompt (llama.cpp calls it prompt processing, pp). Writing speed is how fast it produces the reply (generation, tg). People read roughly 5 to 8 tokens per second (an estimate, not measured here). A parameter is one number inside a model; "4B" means 4 billion. A MoE (mixture of experts) model uses only part of its parameters for each token: Granite 4.0 H Tiny has 6.9B parameters and uses about 1B per token. Q4_K_M and similar names describe quantization, storing each parameter in about 4 to 5 bits instead of 16; gpt-oss-20b has 20.9B parameters and its file is 12.11 GB.
The tested path, three installs. A runtime loads the model, the model writes text, and a harness runs the tools the model asks for.
--jinja and --reasoning off (section 3.2).http://127.0.0.1:8080/v1 (section 3.9).On the 2024 laptop, Qwen3.5-4B inside pi fixed a real bug from a one-sentence request in 5 of 5 takes (section 3.9). For short shell jobs, Granite 4.0 H Tiny scored 39 of 41 and writes about twice as fast; it needs --jinja and --reasoning-format none. With Granite, pi, OpenCode and Claude Code each finished 3 of 3 shell tasks, but on the coding bug Granite passed 0 of 13 takes. Ollama installs a runtime and a model in one step and is the easiest start; it is built on llama.cpp and was not benchmarked here. Bigger hardware: Qwen3.6-35B-A3B on a 12 to 32 GB GPU (section 9 has reported speeds); on a 16 GB laptop it streams from the SSD and long prompts take minutes. A phone: Qwen3.5-2B or Qwen3.5-0.8B (section 3.6). The agent you already use can also do the setup: "Set up llama.cpp, Qwen3.5-4B and pi on this machine."
Anyone can upload a model. Download from the model maker or a known quantizer such as Unsloth, and do not run remote code from a stranger's repository.
All local runs used llama.cpp. Release b11784 (commit 493253fe5, published 2026-10-21) produced most numbers on this page. Its prebuilt Linux downloads include a CPU build and a Vulkan build.
Prebuilt, for the laptop and any x86-64 Linux machine:
# CPU-only build
curl -LO https://github.com/ggml-org/llama.cpp/releases/download/b11784/llama-b11784-bin-ubuntu-x64.tar.gz
# Vulkan build, for a built-in AMD or Intel GPU
curl -LO https://github.com/ggml-org/llama.cpp/releases/download/b11784/llama-b11784-bin-ubuntu-vulkan-x64.tar.gz
mkdir llama-vulkan
tar -xzf llama-b11784-bin-ubuntu-vulkan-x64.tar.gz -C llama-vulkan
find llama-vulkan -name llama-server # cd into the folder this prints
From source. The first mini PC runs used a source build of commit dd002b9 (GCC 11.5, CPU backend, AVX2); b11784 is the tag to check out now:
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
git checkout b11784
cmake -B build
cmake --build build --config Release -j 6
ls build/bin/llama-server build/bin/llama-bench
This is the server behind the pi bug fix in section 3.9 (Qwen3.5-4B Q4_K_M, 32,768-token context, thinking off):
./llama-server -m Qwen3.5-4B-Q4_K_M.gguf -c 32768 --reasoning off --jinja -t 8 --port 8080
This is the server used for the Claude Code test (Granite 4.0 H Tiny, laptop CPU, 32,768-token context):
./llama-server -m granite-4.0-h-tiny-Q4_K_M.gguf -c 32768 --reasoning-format none --jinja --port 8916
The Granite file is 4.25 GB on Hugging Face. llama-server can download it on first start with -hf; this form was not the one timed in the tests:
./llama-server -hf unsloth/granite-4.0-h-tiny-GGUF:Q4_K_M -c 32768 --reasoning-format none --jinja --port 8916
Notes from the tests:
--reasoning-format none was required for Granite 4.0 H Tiny. Without it, Granite answered agent requests in prose and called no tools.--jinja makes llama-server use the model's own chat template, which Granite needs for tool calls. Release b11784 turns it on by default; older builds may not, so every command on this page spells it out.--reasoning off on the server, or send "chat_template_kwargs": {"enable_thinking": false} in each request. Before that change, the first Qwen3.5-2B test runs returned empty answers and scored 0.--host with a LAN address and --api-key with a key the clients must send.--cache-prompt). Section 5.5 shows what it saves.Measure a machine the way the tables below were measured:
./llama-bench -m Qwen3.5-2B-Q4_K_M.gguf -p 512 -n 128 -r 3 -t 6
-p 512 reads a 512-token prompt, -n 128 writes 128 tokens, -r 3 repeats 3 times, and -t sets CPU threads (6 on the mini PCs, 12 on the laptop).
llama-server answers Anthropic's Messages API (/v1/messages), so Claude Code can use it with two environment variables. Tested with Claude Code 2.1.273, Granite 4.0 H Tiny on the laptop CPU, 3 of 3 shell tasks completed with real tool calls:
ANTHROPIC_BASE_URL=http://127.0.0.1:8916 ANTHROPIC_API_KEY=dummy-key \
claude -p --permission-mode acceptEdits --allowedTools Bash \
"Count the lines in notes.txt and write the number to count.txt"
The key value is ignored by llama-server. Claude Code features that run on Anthropic's servers, such as web search, do not work this way; section 3.8 covers the workaround that was tested. pi and OpenCode (section 3.9) talk to llama-server directly.
The Radeon 760M runs llama.cpp's Vulkan build through the Mesa RADV driver. Install the driver and check that Vulkan sees the GPU:
# Fedora and Enterprise Linux family
sudo dnf install mesa-vulkan-drivers vulkan-loader vulkan-tools
# Debian and Ubuntu
sudo apt install mesa-vulkan-drivers libvulkan1 vulkan-tools
vulkaninfo --summary
Then run the Vulkan build with all layers on the GPU. The log line should name the GPU (on this laptop: RADV PHOENIX):
./llama-bench -m Qwen3.5-2B-Q4_K_M.gguf -ngl 99 -p 512 -n 128 -r 3 -t 12
./llama-server -m Qwen3.5-4B-Q4_K_M.gguf -ngl 99 -c 8192 --reasoning off --jinja --port 8092
For a CPU comparison on the identical binary, copy the folder and delete libggml-vulkan.so from the copy; llama.cpp then reports the CPU backend. Run only one llama-server per built-in GPU: two at once crashed with a Vulkan "device lost" error.
llama.cpp can map the model file from the SSD and read the parts it needs. On an 8 GB machine, turn off weight repacking, or llama.cpp tries to copy the weights into RAM (a 9.7 GB buffer for gpt-oss-20b) and fails to load:
# benchmark (llama-bench spells the flag --repack 0)
./llama-bench -m gpt-oss-20b-MXFP4.gguf --repack 0 -lm mmap -p 128 -n 32 -r 3 -t 6
# server (llama-server spells it --no-repack)
./llama-server -m gpt-oss-120b-MXFP4.gguf --no-repack -lm mmap -t 6 -c 8192 --jinja --port 8080
Close other programs first: free RAM becomes page cache for the model file. Section 5.2 shows two mini PCs differing 2 times on the same file for reasons the tests did not settle.
llama.cpp's prebuilt Android binaries link OpenCL 3.0, and the S21 Ultra's driver is older, so they did not run. A CPU-only build made with the Android NDK did. The recipe below is the one in llama.cpp's docs/android.md; the exact flags of the tested build were not recorded.
# on a Linux machine with the Android NDK installed
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 \
-DGGML_NATIVE=OFF -DGGML_OPENMP=OFF -DGGML_LLAMAFILE=OFF -DLLAMA_OPENSSL=OFF \
-B build-android
cmake --build build-android --config Release -j
cmake --install build-android --prefix ./android-install --config Release
# copy to the phone (USB debugging on) and start a server with 3 threads
adb shell mkdir -p /data/local/tmp/llama
adb push android-install /data/local/tmp/llama/
adb push Qwen3.5-0.8B-Q8_0.gguf /data/local/tmp/llama/
adb shell "cd /data/local/tmp/llama && LD_LIBRARY_PATH=android-install/lib android-install/bin/llama-server -m Qwen3.5-0.8B-Q8_0.gguf -t 3 -c 8192 --jinja --port 8091"
# on the computer: reach the phone's server at 127.0.0.1:8094
adb forward tcp:8094 tcp:8091
On the Snapdragon 888, 3 threads beat 4 because the phone throttled less. For Qwen3.5-0.8B, the 8-bit file (Q8_0) was the fastest of 4 quantizations at 3 threads (21.8 tokens per second writing, against 18.8 to 20.3); an 8,192-token context cost no speed, and 16,384 cost about 16% of reading speed. For Llama 2 models, pass the official chat template with --chat-template-file; with the default fallback template, Llama 2's output was incoherent.
Termux is a Linux environment for Android, installed from F-Droid or its GitHub releases. Termux ships a llama-cpp package; its build recipe pins llama.cpp v0.7.0.
pkg update
pkg install llama-cpp
llama-server -m Qwen3.5-0.8B-Q8_0.gguf -t 3 -c 8192 --jinja --port 8091
On Samsung phones, set Termux to unrestricted battery use, or Android may put it to sleep. Watch the battery temperature: the 10-minute test in section 5.1 ended at 40.5 °C, below its 42 °C pause point.
llama-swap is a router. Each machine runs its own model under its own name, and clients send every request to the router's single address. Version v265 was tested (binary sha256 cb1f9a94be05afa129d3c17868f1dd2ca710b919333c7b868239dc31575dcfa1), from github.com/mostlygeek/llama-swap/releases. This config follows llama-swap's remote-upstream pattern: cmd: sleep keeps a placeholder process alive, proxy points at the remote llama-server, and checkEndpoint: none skips the local health check.
# llama-swap.yaml (addresses replaced with placeholders)
listen: 127.0.0.1:8890
models:
minipc-granite-tiny:
cmd: sleep 999999
proxy: http://127.0.0.1:18918 # SSH tunnel to the mini PC's llama-server on :8918
checkEndpoint: none
laptop-granite-tiny:
cmd: sleep 999999
proxy: http://LAPTOP_IP:8916 # laptop llama-server started with --host LAPTOP_IP
checkEndpoint: none
phone-qwen08b:
cmd: sleep 999999
proxy: http://127.0.0.1:8094 # adb forward tcp:8094 tcp:8091
checkEndpoint: none
# on the mini PC
./llama-server -m granite-4.0-h-tiny-Q4_K_M.gguf -t 6 --reasoning-format none --jinja --port 8918
# on the router machine
ssh -N -L 18918:127.0.0.1:8918 user@MINI_PC_IP &
adb forward tcp:8094 tcp:8091
./llama-swap -config llama-swap.yaml -listen 127.0.0.1:8890
# any OpenAI-style client
curl http://127.0.0.1:8890/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"minipc-granite-tiny","messages":[{"role":"user","content":"hi"}]}'
llama-swap does not retry on another machine. In 1 failover trial, a request to the stopped laptop's model failed at once with HTTP 502; the same request sent with the mini PC's model name answered 0.7 seconds after the laptop's server was stopped. The client has to pick the other name.
SearXNG is a self-hosted search engine. LiteLLM (version 1.82.6) sat between the client and llama-server, caught the model's search tool call, ran the search on SearXNG and returned the results to the model. Install the pinned version from a checked wheel:
pip download litellm==1.82.6 --no-deps -d wheels
sha256sum wheels/litellm-1.82.6-*.whl
# expect 164a3ef3e19f309e3cabc199bef3d2045212712fefdfa25fc7f75884a5b5b205
pip install wheels/litellm-1.82.6-*.whl
# litellm-websearch.yaml (keys removed, address replaced)
model_list:
- model_name: granite-tiny-local
litellm_params:
model: openai/granite-4.0-h-tiny
api_base: http://127.0.0.1:8916/v1
api_key: dummy-key # llama-server needs a key header; the value is ignored
litellm_settings:
callbacks: websearch_interception
websearch_interception:
search_provider: searxng
searxng_url: http://SEARXNG_HOST:PORT
Three details from the test. In 1.82.6 the interception callback had to be registered in Python, and the SearXNG connector read api_base from the router's search tool list and ignored searxng_url:
litellm.callbacks = [WebSearchInterceptionLogger(enabled_providers=["openai"])]
SEARCH_TOOLS = [{"search_tool_name": "searxng-search",
"litellm_params": {"search_provider": "searxng",
"model": "searxng/search",
"api_base": "http://SEARXNG_HOST:PORT"}}]
The tool offered to the model must be named exactly litellm_web_search:
TOOLS = [{"type": "function", "function": {"name": "litellm_web_search",
"description": "Search the web",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}}}}]
Results, 3 runs with Granite 4.0 H Tiny: 2 runs returned answers grounded in real search results, and 1 returned nothing. The answers cited the llama.cpp GitHub repository instead of the release tag the question asked for. Only SearXNG's GitHub engine answered; its Brave, DuckDuckGo and Startpage engines were rate-limited on that instance. The test called LiteLLM through its OpenAI-style interface. A full Claude Code session routed through LiteLLM was not recorded.
pi and OpenCode are open-source coding agents that talk to any OpenAI-compatible server, so they need no translator in front of llama-server. Both were installed from scratch on the laptop on 2026-10-22. For the shell tasks below, both pointed at this Granite server (CPU-only build, 8 threads); the bug fix used the Qwen3.5-4B server from section 3.2 on the same port:
./llama-server -m granite-4.0-h-tiny-Q4_K_M.gguf -c 32768 --reasoning-format none --jinja --port 8080 -t 8
pi needs Node.js 22.19 or later (the test used 22.23.1). The npm package is @earendil-works/pi-coding-agent; older guides name @mariozechner/pi-coding-agent.
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
# if npm's global prefix is owned by root, install under your home instead
npm install -g --ignore-scripts --prefix ~/.local @earendil-works/pi-coding-agent
# the binary is then ~/.local/bin/pi
Config file: ~/.pi/agent/models.json. The agent directory is ~/.pi/agent, not ~/.pi.
{
"$schema": "https://pi.dev/schemas/models.schema.json",
"providers": {
"llamacpp": {
"name": "llama.cpp (local)",
"baseUrl": "http://127.0.0.1:8080/v1",
"apiKey": "dummy-key",
"api": "openai-completions",
"models": [
{
"type": "chat",
"id": "granite",
"name": "Granite 4.0 H Tiny Q4_K_M",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 32768,
"maxTokens": 4096
},
{
"type": "chat",
"id": "qwen3.5-4b",
"name": "Qwen3.5-4B Q4_K_M",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 32768,
"maxTokens": 4096
}
]
}
}
}
llama-server answers with whichever model it loaded, so the entry you pick should match the server you started. The qwen3.5-4b entry follows the same pattern as the tested Granite entry; its exact file from the bug-fix takes was not saved.
pi --list-models granite # expect: llamacpp granite 32.8K 4.1K
pi --print --provider llamacpp --model granite "<task>"
# interactive, as in the bug-fix takes
pi --provider llamacpp --model qwen3.5-4b
The installer is a script piped to bash; read it first, or use the npm package opencode-ai.
curl -fsSL https://opencode.ai/install | bash # binary: ~/.opencode/bin/opencode
# or
npm i -g opencode-ai
Config file: ~/.config/opencode/opencode.json.
{
"$schema": "https://opencode.ai/config.json",
"model": "llamacpp/granite",
"provider": {
"llamacpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama.cpp (local)",
"options": {
"baseURL": "http://127.0.0.1:8080/v1",
"apiKey": "dummy-key"
},
"models": {
"granite": {
"name": "Granite 4.0 H Tiny Q4_K_M",
"tool_call": true,
"limit": { "context": 32768, "output": 4096 }
}
}
}
}
}
opencode models | grep llamacpp # expect: llamacpp/granite
opencode run -m llamacpp/granite "<task>"
| Task | pi 1.4.0 | OpenCode 1.19.4 | Claude Code 2.1.273 (section 3.3) |
|---|---|---|---|
| Count the lines of a.txt into count.txt | pass, 2.6 s, bash | pass, 4.4 s, bash | pass, bash |
| Sum the numbers in nums2.txt into sum.txt | pass, 2.8 s, bash | pass, 3.7 s, bash | pass, bash |
| Sort a.txt into sorted.txt | pass, 2.1 s, bash | pass, 3.5 s, read and edit tools | pass, bash |
| Score | 3 of 3 | 3 of 3 | 3 of 3 |
A small repository held logstats.py, a log summarizer, and test_logstats.py. It counted server errors with if status > 500:, so a 500 response was missed and pytest reported 2 failed, 2 passed. The request was typed into interactive pi 1.4.0 the way a person would type it: "the tests are failing, can you figure out why and fix it?" No file names, no steps.
Qwen3.5-4B read the files pi showed it, found the off-by-one and wrote if status >= 500:. pi applied the edit with its edit tool and ran pytest: 4 passed. The pass rule was checked outside pi: pytest reported 4 passed and git showed only logstats.py changed. The model decides what to change; the harness reads the files, applies the edit and runs the tests.
| Model | Prompts tried | Takes that fixed it | Notes |
|---|---|---|---|
| Qwen3.5-4B Q4_K_M | the one sentence above | 5 of 5 | Same one-line fix every take. Working time 30.6 to 34.7 s; one take read the git history first and took 94.7 s. Writing 23.4 to 25.2 tokens per second. |
| Granite 4.0 H Tiny Q4_K_M | the sentence above, a version naming the test file, and a step-by-step version | 0 of 13 | Also 0 of 9 unrecorded probes. It asked for the code to be pasted, guessed files that do not exist, explained without editing, and once edited the test to match the bug. |
Pick the model for the job. Granite scored 39 of 41 on short shell commands and failed this coding job every time; Qwen3.5-4B, trained harder on code, fixed it every time. With a step-by-step prompt and a two-file FizzBuzz repository, Granite did fix a bug once in --print mode, after earlier attempts guessed a wrong file path and once edited toward the broken behavior.
Problems hit during setup:
~/.local/share/opencode/opencode.db left by an older OpenCode install caused it. Moving that file aside fixed it.<output_of_wc_command>. Prompts that said to execute an exact command worked.--reasoning-format none and --jinja on the server.A model is a file of numbers behind an API: text in, text out. It never starts anything. Something has to send it a prompt: you typing, a cron job, or an event such as a new email or a failed build. The harness is that something. It builds the prompt, gives the model its tools, runs the commands the model asks for, feeds the results back and loops. A check decides when the job is done.
Picks after these tests: OpenCode, pi and Hermes Agent. The harness also changes cost. A 2026 ICML paper by Vats and Golev ran the same models through three open harnesses: pass rates were within 8 points, and tokens per solved task differed by up to 40 times.
The task: backport one upstream Linux kernel fix onto an older kernel tree, graded the same way every run. The benchmark ran 136 runs across models, providers and harnesses. Its repository is not public.
| Model, where it ran | Harness | Passed |
|---|---|---|
| Kimi K3, DigitalOcean | OpenCode | 11 of 15 |
| Kimi K3, DigitalOcean | Claude Code | 0 of 3 |
| Kimi K3, DigitalOcean | Codex | 0 of 3 |
| GLM 5.3, through llm-relay, a self-hosted gateway | OpenCode, patched build | 20 of 20 |
| GLM 5.3, another provider | Codex | 0 of 3 |
Why, from the benchmark's own write-up:
The same Kimi K3 weights on the same servers passed 11 of 15 runs in one harness and 0 of 3 in each of two others.
These fixes came from three months of serving open models to a team: in a locked-down OpenCode build, and in llm-relay, the gateway I run at work. Most people should not have to make them; they are why the choice of harness and gateway matters.
| Problem | What happened | Fix |
|---|---|---|
| Loops | One prompt sent a model into 1,300 identical chunks. vLLM defaults the repetition penalty to 1.0; Ollama defaults it to 1.1, and the same weights finished in 28 seconds there. | The gateway caps output and sets a penalty floor. It also translates the setting's name: llama.cpp calls it repeat_penalty and silently ignored the other name. |
| Thinking | The same floor made GLM 5.3 reason 5 to 10 times longer and return empty answers. Its official setting is 1.0. | The floor became a per-model setting. Reasoning and answer share one token budget, so the gateway reserves part of it for the answer. |
| Retries | Some models still looped. | The gateway throws the attempt away, nudges the model and retries inside the same stream, with a small temperature floor; at temperature 0 a retry repeats the same loop. |
| Tool calls | A Qwen3 server ran the tool-call parser for the Qwen3 Coder family. Plain Qwen3 writes a different format, so every call was dropped and the reply looked like a clean stop. | The matching parser. |
| Templates | The harness inserts reminder messages mid-conversation. Two Qwen chat templates allow exactly one system message, at the start, so every agent conversation failed with HTTP 400. | The gateway merges the system messages. |
| Context | OpenCode's own summary request overflowed the context window on long sessions. | A patched OpenCode build that caps it. |
| Model limits | The harness assumed every model could call tools and showed one context size for all of them. | The gateway publishes real limits per model. |
A small Python agent with one tool, run_command, ran inside an Omarchy desktop virtual machine and sent its requests to Granite 4.0 H Tiny on a 2018 mini PC. A separate check looked at the machine after every job.
key=value to mode=fast in a config file, it wrote key=fast and reported success. The check caught it. In an earlier take with a vaguer prompt, it looped on the config until it hit its turn limit.| Lesson | Evidence from these tests |
|---|---|
| Pick the model for the job | Qwen3.5-4B fixed the coding bug in 5 of 5 takes (section 3.9). Granite failed it in 13 of 13 but scored 39 of 41 on shell commands (section 5.3). |
| Prompt precisely | The desktop jobs went 5 of 5 with exact prompts and 2 of 3 with casual ones (section 4.4). |
| One job at a time | Small steps with a clear finish keep a small model on track and keep the prompt short. Each desktop job took about 6 seconds. |
| Check the result | Check what changed on the machine, not what the model says. The desktop agent reported a config fix it got wrong. Asked for mini PC 2's address, Granite gave mini PC 3's in all 3 runs (section 5.7). |
| Serve it right | Chat templates, reasoning flags and sampling settings live in the runtime and the gateway. Granite needs --jinja and --reasoning-format none or it answers in prose and runs nothing; Qwen3.5-4B needs thinking off for quick tool use. |
A job you repeat: ask the agent you already trust to write a small harness for that one job and that one model. Give it a fixed prompt, only the tools the job needs, and checks that fail loudly when the result is wrong. The desktop agent in section 4.4 is that kind of harness: one tool, and a check after every job.
All speeds are tokens per second. "Reps" is the number of repetitions behind each number. Where two sessions disagree, both appear.
| Device and backend | llama.cpp build, threads | Model (Q4_K_M unless noted) | Reading | Writing | Reps |
|---|---|---|---|---|---|
| Mini PC 2, CPU | dd002b9, 6 | Qwen3.5-2B (1.9B) | 138.7 | 13.2 | 3 |
| Mini PC 2, CPU | dd002b9, 6 | Granite 4.0 H Tiny (6.9B, about 1B active) | 105.4 | 15.3 | 3 |
| Mini PC 2, CPU | dd002b9, 6 | Qwen3.5-4B (4.2B) | 54.8 | 6.0 | 3 |
| Mini PC 3, CPU | e36df60, 6 | Granite 4.0 H Tiny | 100.7 | 15.1 | 1 |
| Mini PC 2, Intel UHD 630 (Vulkan) | b11784 | Qwen3.5-2B | 35.6 | 7.2 | 2 |
| Laptop, CPU | dd002b9, 6 | Qwen3.5-2B | 214.5 | 33.3 | 3 |
| Laptop, CPU | dd002b9, 6 | Granite 4.0 H Tiny | 193.8 | 36.7 | 3 |
| Laptop, CPU | dd002b9, 6 | Qwen3.5-4B | 83.7 | 15.0 | 3 |
| Laptop, CPU | b11784, 12 | Qwen3.5-2B | 308.3 | 32.2 | 3 |
| Laptop, CPU | b11784, 12 | Granite 4.0 H Tiny | 233.7 | 36.9 | 3 |
| Laptop, CPU | b11784, 12 | Qwen3.5-4B | 106.6 | 13.8 | 3 |
| Laptop, Radeon 760M (Vulkan) | b11784 (same binary) | Qwen3.5-2B | 914.4 | 56.0 | 3 |
| Laptop, Radeon 760M (Vulkan) | b11784 (same binary) | Granite 4.0 H Tiny | 632.3 | 52.4 | 3 |
| Laptop, Radeon 760M (Vulkan) | b11784 (same binary) | Qwen3.5-4B | 347.1 | 26.5 | 3 |
| Phone, CPU | b11784 NDK, 3 | Qwen3.5-0.8B Q8_0 (0.81 GB) | 51.0 | 21.8 | not recorded |
| Phone, CPU | b11784 NDK, 3 | Qwen3.5-0.8B Q4_0 (0.56 GB) | 40.6 | 20.3 | not recorded |
| Phone, CPU | b11784 NDK, 3 | Qwen3.5-2B | 12.5 | 7.8 | not recorded |
| Phone, CPU | b11784 NDK, 4 | Gemma 4 E2B | 12.1 | 7.7 | not recorded |
| Phone, CPU (earlier session) | b11784 NDK, 4 | Qwen3.5-2B | 12.2 | 4.8 | 3 |
| Phone, CPU (earlier session) | b11784 NDK, 4 | Gemma 4 E2B | 9.4 | 5.3 | 3 |
What the table shows:
| Device (RAM) | Engine | Model, file size | Reading | Writing | First token | Runs |
|---|---|---|---|---|---|---|
| Mini PC 3 (8 GB) | llama.cpp b11784, mmap | gpt-oss-20b MXFP4, 12.11 GB | 20.95 | 6.06 | n/a | 3 reps; a second pass gave 6.05; whether the page cache was emptied between passes was not recorded |
| Mini PC 2 (8 GB) | llama.cpp dd002b9, mmap | gpt-oss-20b MXFP4, 12.11 GB | 11.6 to 12.4 | 2.93 to 2.94 | n/a | 2 sessions, 1 and 2 reps |
| Mini PC 3 (8 GB) | llama.cpp e36df60, llama-bench | gpt-oss-120b MXFP4, 63.39 GB | 4.13 | 0.23 | n/a | 3 reps |
| Mini PC 3 (8 GB) | llama.cpp e36df60, llama-server | gpt-oss-120b MXFP4, 63.39 GB | 1.29 | 0.42 | 63.0 s | 1 request |
| Laptop (16 GB) | llama.cpp b11784, mmap | Qwen3.6-35B-A3B UD-Q3_K_XL, 16.85 GB | 45.97 | 13.80 | n/a | 3 reps |
| Laptop (16 GB) | llama.cpp b11784, mmap | Qwen3.6-35B-A3B UD-Q4_K_XL, 22.85 GB | n/a | 3.85 to 6.13 (median 5.65) | 1.3 to 57.8 s | 6 requests |
| Laptop (16 GB) | Colibri v2.0.0, CPU | Qwen3.6-35B-A3B int4, 23.1 GB | n/a | about 2.7 | 30.2 s total for a 100-token prompt, 436.3 s for 2,000 | 1 per prompt size |
| Laptop (16 GB) | Colibri v2.0.0, CPU | DeepSeek V4 Flash REAP 150B (pruned from the 284B model), 85 GB | n/a | about 2.4 | 28.5 s cold, 26.4 s warm | cold and warm, same answers |
| Phone (12 GB) | llama.cpp b11784 NDK, mmap | Qwen3.6-35B-A3B UD-Q3_K_XL, 16.9 GB | n/a | 2.02 (100-token prompt), 1.36 (2,000) | 28.8 s, 329 s | 1 per prompt size |
| Model | Ran on | Shell tasks, of 41 | Homelab questions, of 25 (3 runs) | Median seconds per task |
|---|---|---|---|---|
| Claude Sonnet 5.5 | Anthropic | 39 to 41 (middle 40), 5 runs | 13 to 14 (middle 14) | 2.6 shell, 2.9 questions |
| Granite 4.0 H Tiny | Mini PC 3 | 39, 39, 39 | 10, 10, 11 | not recorded |
| Qwen3.5-2B | Mini PC 3 | 33, 33, 33 (laptop: 33, 1 run) | not run | not recorded |
awk '{m=$1} END {print m}', which prints the last number. On another task it garbled the quoting of a printf command.printf x\ny\n); bash removes unquoted backslashes, so the output was xnyn. Granite and Claude quoted the escapes and passed. Qwen also piped grep -c into wc -l, used sed -i where printed output was asked for, and gave paste a flag it does not have.| Model | Released | Parameters | File | Peak RAM serving | Reading / writing | Shell tasks, of 41 |
|---|---|---|---|---|---|---|
| Llama 2 7B Chat (Meta) | July 2023 | 6.74B | 4.08 GB | 6.11 GB | 2.69 / 1.59 | 3 |
| Qwen3.5-2B | 2026 | 1.88B | 1.28 GB | 1.51 GB | 11.24 / 4.42 | 34 |
| Qwen3.5-0.8B | 2026 | 0.75B | 0.53 GB | 0.78 GB | 31.29 / 15.42 | 30 |
The 2026 Qwen3.5-2B has 3.6 times fewer parameters than Llama 2 7B, a 3.2 times smaller file, writes 2.8 times faster, and passed 34 shell tasks against 3. Llama 2 passed only the 3 tasks whose result is a file (append, make a directory, move a file). Most of its replies were prose ("Sure! Here is the shell command...") or backtick-wrapped commands instead of the bare command the task asked for. Probes with Llama 2's official prompt format returned sensible commands, so the low score reflects the model, not a broken setup.
Agents resend a long block of instructions with every turn. llama-server keeps the part of the prompt it has already read and skips it next time.
| Turn | Prompt tokens | Already cached | Wall time |
|---|---|---|---|
| First turn | 2,861 | 0 | 33.45 s |
| Same prompt again | 2,861 | 2,857 | 10.71 s |
| Next turn, with 545 new tokens of tool output | 3,402 | 2,857 | 15.63 s |
The cached turn is almost all writing: 128 tokens at 12 tokens per second take about 10.7 seconds. Without caching, every agent turn on this mini PC would cost about 23 seconds of reading before the first word.
llama.cpp's RPC mode splits one model across machines. The pool was mini PC 1 (head, 8 GB), mini PC 2 (worker, 8 GB, 1 Gb Ethernet) and the laptop (worker, 16 GB, Wi-Fi 6). It served Qwen3.6-35B-A3B UD-Q4_K_XL, a 22.85 GB file that fits no single member's RAM. The laptop alone ran the same file from its SSD.
| Setup | Writing, tokens/s | First token, 4,000-token prompt | First token, 2,000-token prompt, cold | Requests |
|---|---|---|---|---|
| RPC pool, 3 machines | 3.27 to 4.47 (median 3.92) | 48.3 s | 75 to 78 s | 9 |
| Laptop alone, from SSD | 3.85 to 6.13 (median 5.65) | 56.6 and 57.8 s | 48.8 s | 6 |
The pool gave room: it ran a model no member could hold in RAM. It did not give speed: every token crossed the network between machines, and the laptop alone wrote about 1.4 times faster. The pool read a long prompt somewhat faster than the laptop's first pass. Section 6 lists what broke.
Splitting by layers, as llama.cpp's RPC mode does, sends every token through every machine in turn. Splitting within each layer needs machines to trade results many times per token, which takes RDMA-class links, where one machine writes straight into another's memory. Fast links change the result. Jeff Geerling ran Kimi K2 Thinking (1 trillion parameters, 32B active) on four Mac Studios linked by Thunderbolt 5, using RDMA added in macOS 26.2 in December 2025. Exo 1.0 over RDMA wrote 28.3 tokens per second, against 16.4 for llama.cpp over TCP on the same four machines (his post; not reproduced here).
Granite 4.0 H Tiny on mini PC 3 answered 25 questions about a homelab's hardware and network. A keyword search fed it the 5 most relevant chunks of the homelab's notes, with an instruction to copy the answer exactly. An answer passed when it contained the expected string. Scores: 10, 10 and 11 of 25 in 3 runs.
The 15 misses in run 1, by kind:
Which chunks the model saw was not recorded, so the split between retrieval misses and model misses is unknown. With the same retrieval, Claude Sonnet 5.5 answered 13 to 14 of the same 25.
The same model, Granite 4.0 H Tiny, ran at five quantizations on mini PC 2 (8 GB), each scored on the 41 shell tasks.
| Quantization | File | Reading | Writing | Shell tasks, of 41 |
|---|---|---|---|---|
| Q8_0 | 7.39 GB | 81.8 | 10.1 | 38 |
| Q6_K | 5.73 GB | 97.3 | 12.3 | 38 |
| Q4_K_M | 4.25 GB | 97.8 | 15.2 | 39 |
| Q3_K_M | 3.39 GB | 98.9 | 17.6 | 38 |
| Q2_K | 2.64 GB | 95.1 | 19.8 | 37 |
A 2025 study (arXiv 2505.02214, Table 4) quantized Qwen3 models of several sizes and scored them on MMLU, a 57-subject multiple-choice exam with four answers per question, so 25% is a random guess.
| Bits per weight | Qwen3 0.6B, MMLU % correct | Qwen3 32B, MMLU % correct |
|---|---|---|
| 16 | 47.1 | 81.2 |
| 8 | 47.0 | 81.3 |
| 4 | 42.1 | 78.0 |
| 3 | 25.0 | 54.9 |
At 4 bits the small model keeps 89% of its score and the big one 96%. At 3 bits the 0.6B model falls to a random guess, 25%, while the 32B still scores 55%. The study cuts every layer to the same bits, which is harsher than GGUF k-quants such as Q3_K_M and Q2_K that keep key layers at higher precision. Rule of thumb: start with Q4_K_M, go lower only to fit in memory, and test your own tasks.
Colibri streams model weights from the SSD. On the laptop, its Qwen3.6-35B-A3B (int4, 23.1 GB) wrote about 2.7 tokens per second and took 436.3 seconds to finish a 2,000-token prompt. llama.cpp with a 16.85 GB file of the same model wrote 13.80 tokens per second, about 5 times faster. The files differ in quantization and size, so the comparison is not exact. Colibri's Vulkan mode did not help: with the 760M sharing system memory, its planner found 873 MiB free against a 1.00 GiB reserve and placed 0 of 40 layers on the GPU. Colibri did run an 85 GB model that llama.cpp was not tried on, at about 2.4 tokens per second.
llama-bench over the RPC pool hung for more than 35 minutes at 0% CPU and was killed; the speeds in section 5.6 come from the server API instead. The head node crashed twice: once mid-answer on a network send failure, once at load when a worker had exited. A third worker is a known deadlock case (llama.cpp issue 28487), so the phone stayed out of the pool. llama.cpp's own RPC README calls the backend "fragile and insecure" and says never to run it on an open network.
Its built-in chat template rejected the system messages Claude Code inserts mid-conversation. With --chat-template chatml it ran, but wrote code blocks instead of tool calls, so Claude Code executed nothing. Granite 4.0 H Tiny worked once --reasoning-format none was set. Reddit tests point the same way: one 21-model comparison found a model's tool-call score rose from 0.640 to 0.880 after its parser was fixed.
Mini PC 2's Intel UHD 630, through Vulkan, read at 35.6 and wrote at 7.2 tokens per second on Qwen3.5-2B (2 reps). The same mini PC's CPU read at 138.7 and wrote at 13.2 (3 reps, an earlier build). A built-in GPU helps only when it is recent: the 2024 laptop's 760M was 3 times faster than its CPU at reading.
-jf flag, which constrains output to a JSON schema, was not used in that run.--no-repack on llama-server, or --repack 0 on llama-bench, fixed it.--chat-template-file.dd002b9 (2026-10-20): built from source with GCC 11.5, CPU backend, AVX2. First round of mini PC and laptop CPU benchmarks.b11784 (commit 493253fe5, 2026-10-21): prebuilt Linux CPU and Vulkan downloads, and a CPU-only Android NDK build. Most numbers on this page.e36df60: an older binary already on mini PC 3, used for gpt-oss-120b and one Granite benchmark.e0db083), llama-swap v265 (d1e743f), LiteLLM 1.82.6.llama-bench -p 512 -n 128 -r 3. Threads: 6 on the mini PCs, 12 on the laptop CPU, 3 or 4 on the phone as noted. The quantization runs in section 5.8 added --repack 0 -lm mmap.llama-bench --repack 0 -lm mmap -p 128 -n 32 -r 3 -t 6, two passes per model. Whether the page cache was emptied between passes was not recorded. One later pass with a recorded cache drop, on an older build, measured Qwen3.6-35B-A3B at 2.50 tokens per second cold against 3.51 warm.libggml-vulkan.so removed for the CPU pass. A pass where the "CPU" numbers matched the GPU numbers was thrown out.hostname run on the grading machine, and the other key matches its input file. The first scorer also kept only the first line of each reply; the fixed scorer strips Markdown fences and keeps multi-line commands. Every saved answer was rescored with the fixed keys and scorer, without rerunning a model.claude -p with tools disabled. Claude Haiku 5.5 was also run, but its scorer kept only the first line of each reply and the full replies were not saved, so its scores are left out.An AI agent (Hermes Agent) ran the experiments overnight from written plans and logged every run as one line of JSON. The overnight log holds 817 records: 793 completed, 10 failed and 7 voided. Later runs (big models, Claude, the 2023 model, the RPC pool) were logged the same way. No model ran on the machine that orchestrated the tests. The results were then audited against the log: numbers with conflicting runs appear as ranges or are left out.
Posts about small hardware and posts about agents both grew as a share of r/LocalLLaMA and r/LocalLLM. Small-hardware posts went from 14.1% in October 2025 to 21.7% in October 2026. Agent posts went from 14.1% to 28.0%, after a peak of 30.5% in March 2026.
| Month | Posts | Small hardware | Agents |
|---|---|---|---|
| 2025-10 | 3,583 | 14.1% | 14.1% |
| 2025-11 | 3,823 | 13.6% | 18.3% |
| 2025-12 | 4,228 | 13.5% | 19.4% |
| 2026-01 | 4,632 | 13.6% | 23.4% |
| 2026-02 | 6,330 | 15.2% | 28.3% |
| 2026-03 | 8,608 | 14.9% | 30.5% |
| 2026-04 | 10,514 | 13.8% | 28.6% |
| 2026-05 | 5,726 | 16.0% | 29.2% |
| 2026-06 | 5,544 | 14.6% | 26.4% |
| 2026-07 | 5,743 | 15.8% | 27.0% |
| 2026-08 | 7,557 | 17.1% | 25.9% |
| 2026-09 | 6,517 | 18.2% | 27.4% |
| 2026-10 (1 to 19) | 4,422 | 21.7% | 28.0% |
Method: keyword matching on each post's title and body. A post counts once per category when any keyword matches. Matching counts mentions, not topics: a post that says "laptop" or "budget" once counts as small hardware, and a post that says "agent" once counts as agents. The script below holds both keyword lists; it reads posts-LocalLLaMA.jsonl and posts-LocalLLM.jsonl (one Arctic Shift post object per line) from a reddit2 folder next to it.
"""Monthly share of r/LocalLLaMA and r/LocalLLM posts about small hardware and about agents.
Input: the Arctic Shift full listings in research/reddit2/posts-*.jsonl (2025-10-01 to 2026-10-19).
Small hardware = the phones, sbc_edge and minipc_laptop_cpu theme patterns from the harvest script.
Agents = the AGENT pattern below. A post counts when its title or body matches.
Usage: python3 -I reddit-trend.py
"""
import datetime
import json
import os
import re
from collections import Counter
BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "reddit2")
SMALL = [
# phones
r"\bphones?\b", r"\bandroid\b", r"\biphone", r"\bios\b", "termux", "pocketpal", r"\bmlc\b", "executorch", "litert",
"mediapipe", "edge gallery", "snapdragon", "exynos", "dimensity", r"tensor g\d", "adreno", r"\bmali\b", "hexagon",
"smartphone", r"\btablet", r"\bipad", r"\bmnn\b", "chatterui", "layla", "smolchat", "phonon", r"\bgalaxy s\d",
# single-board computers and edge devices
"raspberry", r"\brpi\b", r"\bpi ?[45]\b", "orange ?pi", "rk3588", "rockchip", r"\bnpu\b", "jetson", r"\borin\b",
"hailo", r"\bcoral\b", r"\bsbc\b", "radxa", "embedded", r"\bedge devices?\b", "esp32", "microcontroller",
# mini PCs, laptops, CPU-only
r"mini ?pcs?\b", r"\bn1[05]0\b", r"\bnuc\b", "thinkcentre", "optiplex", "elitedesk", r"old laptop", r"\blaptops?\b",
"chromebook", "steam deck", r"\bigpu\b", r"integrated (gpu|graphics)", r"\b[78][689]0m\b", "lunar lake",
"meteor lake", r"cpu[- ]only", r"\bno gpu\b", r"without (a )?gpu", r"\bddr[45]\b", "memory bandwidth",
"dual[- ]channel", r"\bpotato\b", r"low[- ]end", r"\bbudget\b", r"old hardware", r"e-?waste",
]
SMALL_RE = [re.compile(p, re.I) for p in SMALL]
AGENT = re.compile(r"\bagent(s|ic)?\b|\bharness|opencode|claude code|tool[- ]?call|\bmcp\b|hermes agent|\bpi agent"
r"|openhands|\bgoose\b|\baider\b|\bcline\b", re.I)
def main():
total, small, agents = Counter(), Counter(), Counter()
for sub in ("LocalLLaMA", "LocalLLM"):
with open(os.path.join(BASE, f"posts-{sub}.jsonl")) as f:
for line in f:
try:
post = json.loads(line)
except ValueError:
continue
month = datetime.datetime.fromtimestamp(int(post["created_utc"]), datetime.timezone.utc).strftime("%Y-%m")
text = (post.get("title") or "") + "\n" + (post.get("selftext") or "")
total[month] += 1
small[month] += any(r.search(text) for r in SMALL_RE)
agents[month] += bool(AGENT.search(text))
for month in sorted(total):
n = total[month]
print(f"{month} posts {n:6d} small hardware {100 * small[month] / n:5.1f}% agents {100 * agents[month] / n:5.1f}%")
if __name__ == "__main__":
main()
A review of 85,900 posts from r/LocalLLaMA, r/LocalLLM, r/ollama, r/LocalAIServers and r/RockchipNPU (2025-10-01 to 2026-10-19) shaped these tests. Reddit numbers are self-reported unless marked checked.
Other people's numbers, not measured on the hardware above. Several use tuned builds.
Brady Dibble is Director of Product at CIQ. He oversees Rocky Linux from CIQ and runs CIQ's self-hosted AI fleet. In the 30 days to 2026-10-22 he sent 105.6 billion tokens through those self-hosted models, most of it agents re-reading their own context: 94.4% of input tokens came from the prompt cache. Nothing here is a CIQ product pitch. Every local tool on this page is open source or free to use.
Contact: [email protected] · linkedin.com/in/bradydibble · bradydibble.com