<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pingredsai</title>
    <description>The latest articles on DEV Community by Pingredsai (@pingredsai).</description>
    <link>https://dev.to/pingredsai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4173320%2Fbc0be47e-2369-458c-8a6a-164ba4b40430.png</url>
      <title>DEV Community: Pingredsai</title>
      <link>https://dev.to/pingredsai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pingredsai"/>
    <language>en</language>
    <item>
      <title>LocalRAG Desktop: a Docker-free local document Q&amp;A app for Windows</title>
      <dc:creator>Pingredsai</dc:creator>
      <pubDate>Wed, 21 Oct 2026 01:52:12 +0000</pubDate>
      <link>https://dev.to/pingredsai/localrag-desktop-a-docker-free-local-document-qa-app-for-windows-1n4g</link>
      <guid>https://dev.to/pingredsai/localrag-desktop-a-docker-free-local-document-qa-app-for-windows-1n4g</guid>
      <description>&lt;p&gt;Most local RAG demos still start with the same obstacle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;localrag-ollama ollama pull ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine for developers. It is not fine for a lawyer, accountant, or small-business owner who just wants to search their own documents.&lt;/p&gt;

&lt;p&gt;So I rebuilt the user-facing setup around a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can a Windows user run a private document Q&amp;amp;A system without installing Docker, WSL, or Python?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This beta is my answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;LocalRAG Desktop is a local document knowledge base:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upload text-based PDF, Word, TXT, or Markdown files&lt;/li&gt;
&lt;li&gt;parse and chunk documents locally&lt;/li&gt;
&lt;li&gt;create embeddings with a local Chinese embedding model&lt;/li&gt;
&lt;li&gt;retrieve relevant passages from Qdrant&lt;/li&gt;
&lt;li&gt;generate an answer with a local Qwen model&lt;/li&gt;
&lt;li&gt;show the source file and chunk used for the answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything stays on the local machine. The services bind to &lt;code&gt;127.0.0.1&lt;/code&gt; only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in this beta
&lt;/h2&gt;

&lt;p&gt;The earlier version was a Docker Compose stack. The Windows beta now uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;native Windows Qdrant&lt;/li&gt;
&lt;li&gt;native llama.cpp CPU runtime&lt;/li&gt;
&lt;li&gt;Qwen2.5 1.5B for generation&lt;/li&gt;
&lt;li&gt;bge-small-zh-v1.5 for embeddings&lt;/li&gt;
&lt;li&gt;a packaged FastAPI backend&lt;/li&gt;
&lt;li&gt;a native Windows launcher&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user flow is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download ZIP
→ verify SHA256
→ extract to a data drive
→ double-click the launcher
→ wait for the local services
→ use the local web UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The launcher performs environment checks, starts the services, waits for health checks, and opens the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I did not just wrap Docker
&lt;/h2&gt;

&lt;p&gt;A wrapper can hide Docker commands, but it cannot remove the Docker installation, WSL2 requirement, virtualization settings, admin permissions, and large image downloads.&lt;/p&gt;

&lt;p&gt;For a technical user, Docker is convenient. For a non-technical user, it is a second product they have to install before they can try the first one.&lt;/p&gt;

&lt;p&gt;The goal here was to remove that second product entirely. A real one-click deployment is not a script that hides Docker commands. It is a setup where the user never needs Docker in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows launcher
├── Qdrant native
├── llama.cpp generation server
├── llama.cpp embedding server
├── packaged FastAPI backend
├── local web UI
└── local data directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security-related choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;services bind to &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the local API uses a random session token for non-health endpoints&lt;/li&gt;
&lt;li&gt;CORS is restricted to local origins&lt;/li&gt;
&lt;li&gt;diagnostics exclude documents, models, vector data, and access tokens&lt;/li&gt;
&lt;li&gt;no default telemetry&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I verified
&lt;/h2&gt;

&lt;p&gt;On the development machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;native Qdrant and llama.cpp started successfully&lt;/li&gt;
&lt;li&gt;a sample document was indexed into 3 chunks&lt;/li&gt;
&lt;li&gt;a policy question returned the correct answer with a source&lt;/li&gt;
&lt;li&gt;streaming answers worked&lt;/li&gt;
&lt;li&gt;the native stack continued working after Docker containers were stopped&lt;/li&gt;
&lt;li&gt;the packaged backend ran without system Python&lt;/li&gt;
&lt;li&gt;no-token API requests returned &lt;code&gt;401&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is still a beta. It has not yet been tested on a wide matrix of clean Windows machines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download and try it
&lt;/h2&gt;

&lt;p&gt;Release page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Pingredsai/local-rag/releases/tag/v0.3.0-beta.1" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-rag/releases/tag/v0.3.0-beta.1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Direct ZIP:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Pingredsai/local-rag/releases/download/v0.3.0-beta.1/LocalRAG-Beta-v0.3.zip" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-rag/releases/download/v0.3.0-beta.1/LocalRAG-Beta-v0.3.zip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SHA256:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f35d424fedd98e9f8b143e4a72c6dfd2ee350c03c3338582449c2c479a287035
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows 10 22H2 / Windows 11 x64 only&lt;/li&gt;
&lt;li&gt;8GB RAM recommended for the included 1.5B model&lt;/li&gt;
&lt;li&gt;text-based PDFs only; OCR is not included&lt;/li&gt;
&lt;li&gt;CPU inference in this beta&lt;/li&gt;
&lt;li&gt;single knowledge base&lt;/li&gt;
&lt;li&gt;no multi-user permissions yet&lt;/li&gt;
&lt;li&gt;the beta is not code signed yet, so Windows may show an “Unknown publisher” or SmartScreen warning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ZIP contains a Windows prompt guide and a diagnostic tool. Please do not disable Defender, SmartScreen, or Application Control to run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback I am looking for
&lt;/h2&gt;

&lt;p&gt;I am mainly interested in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installation/startup failures&lt;/li&gt;
&lt;li&gt;unclear Windows prompts&lt;/li&gt;
&lt;li&gt;first-answer latency&lt;/li&gt;
&lt;li&gt;document types that fail to parse&lt;/li&gt;
&lt;li&gt;whether source references are clear enough&lt;/li&gt;
&lt;li&gt;whether the setup feels simple enough for a non-technical user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you test it, please include your Windows version, RAM, the step where you got stuck, and the diagnostic ZIP generated by the launcher.&lt;/p&gt;

&lt;p&gt;Contact: &lt;a href="mailto:siquhjh@163.com"&gt;siquhjh@163.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will keep the technical limitations and test results honest as the project evolves.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Self-Hosted RAG Stack That Runs Without Cloud AI APIs</title>
      <dc:creator>Pingredsai</dc:creator>
      <pubDate>Fri, 16 Oct 2026 02:58:56 +0000</pubDate>
      <link>https://dev.to/pingredsai/i-built-a-self-hosted-rag-stack-that-runs-without-cloud-ai-apis-2278</link>
      <guid>https://dev.to/pingredsai/i-built-a-self-hosted-rag-stack-that-runs-without-cloud-ai-apis-2278</guid>
      <description>&lt;p&gt;Many teams want AI to answer questions from internal documents, but they cannot send those documents to a cloud service.&lt;/p&gt;

&lt;p&gt;Legal teams have confidential case files. Clinics have patient data. Finance teams have client working papers. Manufacturers have proprietary process documentation. Some organizations operate on isolated networks.&lt;/p&gt;

&lt;p&gt;For these teams, a cloud-first RAG stack is not an option.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;LocalRAG&lt;/strong&gt; to verify a simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A document question-answering system should be able to run inside a controlled environment, use local models, and return source references for every answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The project is open source and the current &lt;code&gt;v0.1.0&lt;/code&gt; release has been verified end to end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/Pingredsai/local-rag" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-rag&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/Pingredsai/local-rag/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-rag/releases/tag/v0.1.0&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What LocalRAG does
&lt;/h2&gt;

&lt;p&gt;LocalRAG allows a user to upload documents and ask questions against them.&lt;/p&gt;

&lt;p&gt;The verified flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload a PDF, DOCX, TXT, or Markdown document.&lt;/li&gt;
&lt;li&gt;Parse and split the document into chunks.&lt;/li&gt;
&lt;li&gt;Generate embeddings with a local &lt;code&gt;bge-m3&lt;/code&gt; model.&lt;/li&gt;
&lt;li&gt;Store vectors in Qdrant.&lt;/li&gt;
&lt;li&gt;Embed the user's question.&lt;/li&gt;
&lt;li&gt;Retrieve the most relevant chunks.&lt;/li&gt;
&lt;li&gt;Generate an answer with a local LLM.&lt;/li&gt;
&lt;li&gt;Return the answer with source references.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application does not call a cloud AI API at runtime.&lt;/p&gt;

&lt;p&gt;Initial installation still requires network access to pull Docker images and Ollama models. After that, the stack can run inside a local or air-gapped environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;LocalRAG uses three main containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser UI
    |
    v
FastAPI Backend
  - document parsing
  - chunking
  - RAG orchestration
  - REST API and static frontend
    |
    +--&amp;gt; Qdrant
    |      vector storage and search
    |
    +--&amp;gt; Ollama
           bge-m3 embeddings
           local LLM generation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why these components?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Easy to inspect, modify, and deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector database&lt;/td&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;One container, clean API, persistent storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedding runtime&lt;/td&gt;
&lt;td&gt;Ollama + bge-m3&lt;/td&gt;
&lt;td&gt;Local, multilingual, avoids a separate Python model stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation runtime&lt;/td&gt;
&lt;td&gt;Ollama + Qwen2.5&lt;/td&gt;
&lt;td&gt;Easy model switching from 1.5B to larger models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Plain HTML/CSS/JS&lt;/td&gt;
&lt;td&gt;No frontend build step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Docker Compose&lt;/td&gt;
&lt;td&gt;Reproducible and easy to run locally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I deliberately did not build a new model runtime, vector database, or agent engine.&lt;/p&gt;

&lt;p&gt;The interesting engineering problem is not reinventing Ollama. It is making the complete workflow reproducible, observable, and usable in a private deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verified test
&lt;/h2&gt;

&lt;p&gt;I tested the current release with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker Desktop 4.95.0&lt;/li&gt;
&lt;li&gt;Ollama 0.11.3&lt;/li&gt;
&lt;li&gt;Qdrant&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bge-m3&lt;/code&gt; for embeddings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qwen2.5:3b&lt;/code&gt; for generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test document was a fictional employee handbook.&lt;/p&gt;

&lt;p&gt;The system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Indexed it into three chunks.&lt;/li&gt;
&lt;li&gt;Received the question: “How many days of annual leave do full-time employees receive?”&lt;/li&gt;
&lt;li&gt;Retrieved the relevant policy text.&lt;/li&gt;
&lt;li&gt;Returned “15 days.”&lt;/li&gt;
&lt;li&gt;Displayed the source document and similarity score in the UI.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A health check returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"qdrant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"llm_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen2.5:3b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"embed_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bge-m3"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository includes screenshots and a 42-second demo video.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Pingredsai/local-rag.git
&lt;span class="nb"&gt;cd &lt;/span&gt;local-rag
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

docker &lt;span class="nb"&gt;exec &lt;/span&gt;localrag-ollama ollama pull qwen2.5:3b
docker &lt;span class="nb"&gt;exec &lt;/span&gt;localrag-ollama ollama pull bge-m3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Four problems I ran into
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The official Ollama image was too large
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;latest&lt;/code&gt; Docker image is convenient, but the unpacked image can approach 9 GB. In a slow network environment, that is a serious deployment problem.&lt;/p&gt;

&lt;p&gt;I pinned the project to a smaller compatible Ollama release and documented the deployment tradeoff.&lt;/p&gt;

&lt;p&gt;This is a reminder that runtime size and download behavior are part of the product experience, not just infrastructure details.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A slow Docker registry is not the same as a broken Docker installation
&lt;/h3&gt;

&lt;p&gt;Docker Hub was extremely slow in my environment. The solution was a combination of a mirror, resumable pulls, and local model import in some cases.&lt;/p&gt;

&lt;p&gt;I did not hard-code a third-party mirror into the project. Instead, the deployment notes describe the approach so operators can replace it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The original streaming response did not show sources
&lt;/h3&gt;

&lt;p&gt;The first implementation returned text from the streaming endpoint, while sources were only available from the non-streaming endpoint.&lt;/p&gt;

&lt;p&gt;That made the UI look like a normal chatbot. The source references, which are essential in a business system, were invisible.&lt;/p&gt;

&lt;p&gt;I changed the streaming endpoint to return source metadata in a response header. The frontend reads the header, streams the answer, and renders the source cards when the answer completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A 1.5B model was not reliable enough for the demo
&lt;/h3&gt;

&lt;p&gt;The smaller model worked on short context but produced inconsistent responses on slightly longer documents.&lt;/p&gt;

&lt;p&gt;I switched the demo to &lt;code&gt;qwen2.5:3b&lt;/code&gt; and tightened the prompt and generation options.&lt;/p&gt;

&lt;p&gt;Local AI is not automatically better just because it is smaller. Model size must match the hardware, use case, and required answer quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current limitations
&lt;/h2&gt;

&lt;p&gt;The first version is intentionally narrow.&lt;/p&gt;

&lt;p&gt;It does not include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OCR for scanned PDFs&lt;/li&gt;
&lt;li&gt;Multi-tenant permissions&lt;/li&gt;
&lt;li&gt;Multiple isolated knowledge bases&lt;/li&gt;
&lt;li&gt;Audit logging&lt;/li&gt;
&lt;li&gt;A reranker&lt;/li&gt;
&lt;li&gt;Visual agent orchestration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of &lt;code&gt;v0.1.0&lt;/code&gt; is to make the core path dependable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload -&amp;gt; index -&amp;gt; retrieve -&amp;gt; answer -&amp;gt; show sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would rather state those boundaries clearly than imply that the project is already a production system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I plan to improve next
&lt;/h2&gt;

&lt;p&gt;The next work is focused on the existing engineering path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improve PDF and DOCX parsing&lt;/li&gt;
&lt;li&gt;Evaluate and refine chunking strategies&lt;/li&gt;
&lt;li&gt;Improve retrieval quality and testing&lt;/li&gt;
&lt;li&gt;Polish streaming answers and source rendering&lt;/li&gt;
&lt;li&gt;Add clearer checks for ports, models, and data directories&lt;/li&gt;
&lt;li&gt;Document backup, restore, and upgrade paths&lt;/li&gt;
&lt;li&gt;Continue reducing deployment friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before adding more modules, I want the core path to be dependable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upload -&amp;gt; index -&amp;gt; retrieve -&amp;gt; answer -&amp;gt; show sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Feedback and contributions
&lt;/h2&gt;

&lt;p&gt;LocalRAG is an open-source project under active development.&lt;/p&gt;

&lt;p&gt;If you work with local inference, RAG, or private deployment, feedback is welcome through GitHub issues and pull requests.&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Pingredsai/local-rag" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-rag&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most useful reports are reproducible bugs, deployment issues, retrieval quality observations, and documentation improvements.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>selfhosted</category>
      <category>python</category>
    </item>
    <item>
      <title>Is Your Phone's LLM 37x Too Slow? Check This One Compiler Flag</title>
      <dc:creator>Pingredsai</dc:creator>
      <pubDate>Mon, 12 Oct 2026 02:14:50 +0000</pubDate>
      <link>https://dev.to/pingredsai/is-your-phones-llm-37x-too-slow-check-this-one-compiler-flag-3mcc</link>
      <guid>https://dev.to/pingredsai/is-your-phones-llm-37x-too-slow-check-this-one-compiler-flag-3mcc</guid>
      <description>&lt;h1&gt;
  
  
  Is Your Phone's LLM 37x Too Slow? Check This One Compiler Flag
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Part 3 of a series.&lt;/p&gt;

&lt;p&gt;Part 1: &lt;a href="https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645"&gt;found the missing &lt;code&gt;-march&lt;/code&gt; (2.5x faster)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Part 2: &lt;a href="https://dev.to/pingredsai/on-device-inference-debugging-part-2-threads-on-big-cores-cpu-at-full-clock-still-slow-6hm"&gt;ruled out scheduling and frequency scaling (negative results).&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: found the real culprit — &lt;code&gt;-O0&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The result first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you build llama.cpp for Android with a Gradle debug variant, you get an &lt;code&gt;-O0&lt;/code&gt; binary. It is 37x slower than Release.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Debug (-O0)&lt;/th&gt;
&lt;th&gt;Release (-O3)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;0.5 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18.5 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First token&lt;/td&gt;
&lt;td&gt;1863 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;36x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefill, 43 tokens&lt;/td&gt;
&lt;td&gt;42 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.556 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model load&lt;/td&gt;
&lt;td&gt;24 s&lt;/td&gt;
&lt;td&gt;3.9 s&lt;/td&gt;
&lt;td&gt;6x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Device: Pixel 4 (Snapdragon 855, 2019). Model: Qwen2.5-1.5B-Instruct Q4_K_M.&lt;br&gt;
&lt;strong&gt;Nothing changed except two build config lines.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. How I missed something this basic
&lt;/h2&gt;

&lt;p&gt;What I had already checked (parts 1 and 2):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing &lt;code&gt;-march&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Real, fixed — 2.5x on prefill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads on little cores&lt;/td&gt;
&lt;td&gt;❌ Ruled out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenMP not threading&lt;/td&gt;
&lt;td&gt;❌ Ruled out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mmap pages evicted&lt;/td&gt;
&lt;td&gt;❌ Ruled out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU frequency throttling&lt;/td&gt;
&lt;td&gt;⚠️ Real, but only ~20%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Optimisation level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴 &lt;strong&gt;Never checked&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I spent days on &lt;em&gt;how the CPU was running&lt;/em&gt;, and never checked &lt;em&gt;whether the compiled code was optimised at all&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Worse: I had disassembled the binary and confirmed &lt;code&gt;sdot&lt;/code&gt; (the ARM dotprod instruction) was present, and concluded "the build is fine".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sdot&lt;/code&gt; only tells you the instruction set. It says nothing about the optimisation level. At &lt;code&gt;-O0&lt;/code&gt; the compiler still emits &lt;code&gt;sdot&lt;/code&gt; — it just does nothing else.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. What turned it around
&lt;/h2&gt;

&lt;p&gt;A reader on DEV left this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build type. A Gradle debug variant hands CMake &lt;code&gt;CMAKE_BUILD_TYPE=Debug&lt;/code&gt; unless you override it, and ggml at &lt;code&gt;-O0&lt;/code&gt; with asserts on is 20-50x slower than release.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sdot&lt;/code&gt; showing up in the disassembly does not tell you the optimisation level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;-DCMAKE_BUILD_TYPE=Release&lt;/code&gt; to that arguments list, or benchmark the release variant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I would bet the build type.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He called it exactly.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Verifying takes one command
&lt;/h2&gt;

&lt;p&gt;Look at what the build system actually passes to the compiler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find app/.cxx &lt;span class="nt"&gt;-name&lt;/span&gt; build.ninja

&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'\-O[0-3s]'&lt;/span&gt; build.ninja | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(no output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a single &lt;code&gt;-O&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;CMAKE_BUILD_TYPE:STRING=Debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So ggml, llama.cpp and my own JNI layer were all compiled with &lt;strong&gt;zero optimisation&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Change 1: force optimisation in &lt;code&gt;CMakeLists.txt&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This must come before &lt;code&gt;add_subdirectory(llama.cpp)&lt;/code&gt;&lt;/strong&gt;, otherwise it won't affect ggml.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_C_FLAGS_DEBUG   &lt;span class="s2"&gt;"-O3 -g"&lt;/span&gt; CACHE STRING &lt;span class="s2"&gt;""&lt;/span&gt; FORCE&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_CXX_FLAGS_DEBUG &lt;span class="s2"&gt;"-O3 -g"&lt;/span&gt; CACHE STRING &lt;span class="s2"&gt;""&lt;/span&gt; FORCE&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_C_FLAGS_RELWITHDEBINFO   &lt;span class="s2"&gt;"-O3 -g -DNDEBUG"&lt;/span&gt; CACHE STRING &lt;span class="s2"&gt;""&lt;/span&gt; FORCE&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;CMAKE_CXX_FLAGS_RELWITHDEBINFO &lt;span class="s2"&gt;"-O3 -g -DNDEBUG"&lt;/span&gt; CACHE STRING &lt;span class="s2"&gt;""&lt;/span&gt; FORCE&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Change 2: tell Gradle to use Release
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;externalNativeBuild&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;cmake&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"-DUSE_LLAMA_CPP=ON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"-DCMAKE_BUILD_TYPE=Release"&lt;/span&gt;    &lt;span class="c1"&gt;// key&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both, because AGP can override what you pass via Gradle, while &lt;code&gt;FORCE&lt;/code&gt; in CMakeLists wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Verify the fix actually landed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Don't trust the flag you passed. Check what happened.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 1: build.ninja
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\-O3'&lt;/span&gt; build.ninja
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'\-DNDEBUG'&lt;/span&gt; build.ninja
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-O3 appears 82 times
-DNDEBUG present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check 2: binary size (the fastest smell test)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Debug (-O0)&lt;/th&gt;
&lt;th&gt;Release (-O3)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libggml-cpu.so&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.92 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.95 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;libllama.so&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7427 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3404 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A large drop in size means optimisation actually happened&lt;/strong&gt; — inlining, dead code elimination and constant folding all shrink the binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is my favourite quick check: you don't need to disassemble anything, just look at the file size.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Why it's this dramatic
&lt;/h2&gt;

&lt;p&gt;What &lt;code&gt;-O0&lt;/code&gt; turns off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimisation&lt;/th&gt;
&lt;th&gt;-O0&lt;/th&gt;
&lt;th&gt;-O3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Function inlining&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ on hot paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop unrolling&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-vectorisation&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register allocation&lt;/td&gt;
&lt;td&gt;conservative&lt;/td&gt;
&lt;td&gt;aggressive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dead code elimination&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asserts&lt;/td&gt;
&lt;td&gt;kept&lt;/td&gt;
&lt;td&gt;removed (&lt;code&gt;-DNDEBUG&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For LLM inference the killers are &lt;strong&gt;auto-vectorisation&lt;/strong&gt; and &lt;strong&gt;inlining&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Quantised matrix multiplication — 99% of the compute — is made of tight loops. At &lt;code&gt;-O0&lt;/code&gt; those loops are &lt;strong&gt;not vectorised&lt;/strong&gt;, so every element is computed and fetched individually. Tens of times slower is expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That explains every anomaly we saw:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU at full clock ✅
threads on big cores ✅
dotprod instructions present ✅
utilization: 1% ❌
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CPU was running full speed on &lt;strong&gt;unoptimised element-by-element code&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. How to avoid this
&lt;/h2&gt;

&lt;h3&gt;
  
  
  If you're building llama.cpp for Android
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# after building, check the optimisation flags&lt;/span&gt;
find app/.cxx &lt;span class="nt"&gt;-name&lt;/span&gt; build.ninja &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'\-O3'&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;

&lt;span class="c"&gt;# or just look at the binary size&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; app/build/intermediates/cxx/&lt;span class="k"&gt;*&lt;/span&gt;/obj/arm64-v8a/&lt;span class="k"&gt;*&lt;/span&gt;.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;libggml-cpu.so&lt;/code&gt; around 1 MB → optimised. Around 3-4 MB → probably &lt;code&gt;-O0&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A general debugging order
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Optimisation level (-O0/-O1/-O2/-O3)     &amp;lt;- start here
2. Architecture flags (-march / -mcpu)
3. Thread count and affinity
4. Memory and IO
5. The algorithm itself
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;I worked through step 5 before coming back to step 1.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The more basic something is, the easier it is to skip.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We checked CPU architecture, thread scheduling, frequency, bandwidth — and never checked whether the code was optimised. Because "basic things don't look like the problem".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "The instruction exists" ≠ "the build is right".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instruction set and optimisation level are different axes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Binary size is an underrated signal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A big size change is the simplest evidence that optimisation happened&lt;/strong&gt;, and it's much faster than reading disassembly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. One good comment can beat days of solo debugging.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That reader spotted it in a sentence. &lt;strong&gt;And he could only do that because I published the negative results&lt;/strong&gt; — if I'd only written about the successes, he wouldn't have known where I was stuck.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Where it stands now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;18.5 tok/s, fully offline, on a 2019 phone.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 20-character sentence finishes in about a second&lt;/li&gt;
&lt;li&gt;First token in 52 ms — essentially no perceptible delay&lt;/li&gt;
&lt;li&gt;1.3 GB RSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For a five-year-old phone, that's genuinely usable.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1: &lt;a href="https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645"&gt;Why Your Phone Runs LLMs 80x Slower Than It Should&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 2: &lt;a href="https://dev.to/pingredsai/on-device-inference-debugging-part-2-threads-on-big-cores-cpu-at-full-clock-still-slow-6hm"&gt;Threads on Big Cores, CPU at Full Clock — Still Slow&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 3: this post&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Code: &lt;a href="https://github.com/Pingredsai/local-ai-android" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-ai-android&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>llama</category>
      <category>performance</category>
      <category>cpp</category>
    </item>
    <item>
      <title>On-Device Inference Debugging (Part 2): Threads on Big Cores, CPU at Full Clock — Still Slow</title>
      <dc:creator>Pingredsai</dc:creator>
      <pubDate>Sat, 10 Oct 2026 11:02:50 +0000</pubDate>
      <link>https://dev.to/pingredsai/on-device-inference-debugging-part-2-threads-on-big-cores-cpu-at-full-clock-still-slow-6hm</link>
      <guid>https://dev.to/pingredsai/on-device-inference-debugging-part-2-threads-on-big-cores-cpu-at-full-clock-still-slow-6hm</guid>
      <description>&lt;h1&gt;
  
  
  On-Device Inference Debugging (Part 2): Threads on Big Cores, CPU at Full Clock — Still Slow
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Part 1: &lt;a href="https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645"&gt;&lt;em&gt;Why Your Phone Runs LLMs 80x Slower Than It Should&lt;/em&gt;&lt;/a&gt;&lt;br&gt;
This part focuses on &lt;strong&gt;thread scheduling&lt;/strong&gt; and &lt;strong&gt;CPU frequency scaling&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Where part 1 left off
&lt;/h2&gt;

&lt;p&gt;In part 1 I benchmarked Qwen2.5-1.5B on a Pixel 4 (Snapdragon 855) and measured &lt;strong&gt;0.5 tok/s&lt;/strong&gt; — 60-120x off the theoretical limits for that SoC.&lt;/p&gt;

&lt;p&gt;That investigation found one real bug: &lt;strong&gt;llama.cpp cross-compilation passes no &lt;code&gt;-march&lt;/code&gt; by default&lt;/strong&gt;, so quantized matmul falls back to a slow path. Fixing it cut prefill from 114 s to 45 s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But throughput was still 0.5 tok/s.&lt;/strong&gt; The biggest hole was filled; the problem remained.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. A reader points in a new direction
&lt;/h2&gt;

&lt;p&gt;After publishing on DEV, a reader offered two traps that &lt;em&gt;look&lt;/em&gt; like a slow model but aren't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scheduling:&lt;/strong&gt; a llama.cpp server whose child processes were parked on efficiency cores under load — a 3-second transcription became 37 seconds. Same binary, same flags; the only difference was who launched it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage:&lt;/strong&gt; running off a USB stick, the whole model is re-read at every start — 2.6 GB on a 34 MB/s drive means over a minute before the first token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the line that stung:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The number that looks like "the model is slow" is almost never the model.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both point the same way: maybe the problem isn't &lt;em&gt;what&lt;/em&gt; is being computed, but &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how fast&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Know your silicon first: 1+3+4
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in&lt;/span&gt; /sys/devices/system/cpu/cpu[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt;/cpufreq/cpuinfo_max_freq&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cores&lt;/th&gt;
&lt;th&gt;Max freq&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cpu0–3&lt;/td&gt;
&lt;td&gt;1.79 GHz&lt;/td&gt;
&lt;td&gt;A55 little&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpu4–6&lt;/td&gt;
&lt;td&gt;2.42 GHz&lt;/td&gt;
&lt;td&gt;A76 mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpu7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.84 GHz&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A76 big&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Classic 1+3+4. If inference threads land on cpu0–3, you lose 3-4x immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Test 1 — which core are the threads actually on?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method: read /proc, no tooling required
&lt;/h3&gt;

&lt;p&gt;Field 39 of &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/&amp;lt;tid&amp;gt;/stat&lt;/code&gt; is &lt;strong&gt;the CPU the thread is currently running on&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pidof com.example.localai&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;t &lt;span class="k"&gt;in&lt;/span&gt; /proc/&lt;span class="nv"&gt;$pid&lt;/span&gt;/task/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;tid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;comm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;/comm&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $39}'&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;/stat&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;ticks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $14+$15}'&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;/stat&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tid&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$comm&lt;/span&gt;&lt;span class="s2"&gt; cpu=&lt;/span&gt;&lt;span class="nv"&gt;$cpu&lt;/span&gt;&lt;span class="s2"&gt; ticks=&lt;/span&gt;&lt;span class="nv"&gt;$ticks&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sample repeatedly&lt;/strong&gt; — a single snapshot tells you nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result: all on big cores. Scheduling is not the problem.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main thread 9556:  cpu5 -&amp;gt; cpu5 -&amp;gt; cpu5 -&amp;gt; cpu3 -&amp;gt; cpu5 -&amp;gt; cpu4
19524 openmp_worker  cpu6
19525 openmp_worker  cpu7   &amp;lt;- big core
19526 openmp_worker  cpu5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;All four inference threads stayed on cpu4–7. None touched the little cores.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also confirmed OpenMP is genuinely working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;19524 openmp_worker   5607 ticks
19525 openmp_worker   5606 ticks
19526 openmp_worker   5602 ticks
9556  DefaultDispatch 12132 ticks (main)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;n_threads=4&lt;/code&gt; → 3 workers + 1 main thread. Exactly as configured.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verdict: "threads parked on efficiency cores" does not happen on this device. Ruled out.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Test 2 — is the CPU being throttled?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The symptom: frequency swinging wildly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cpu4:  710 ~ 1920 MHz   (max 2419)   &amp;lt;- as low as 29%
cpu5: 1612 ~ 2419 MHz   (max 2419)
cpu6:  710 ~ 2419 MHz   (max 2419)
cpu7: 1612 ~ 2016 MHz   (max 2841)   &amp;lt;- never above 71%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First rule out thermals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys battery | &lt;span class="nb"&gt;grep &lt;/span&gt;temperature
&lt;span class="c"&gt;# temperature: 339  -&amp;gt;  33.9 C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;33.9 °C. Not thermal.&lt;/p&gt;

&lt;p&gt;The culprit is &lt;strong&gt;&lt;code&gt;schedutil&lt;/code&gt;&lt;/strong&gt;, Android's default governor. It scales frequency based on utilization estimates — and LLM inference is &lt;em&gt;bursty&lt;/em&gt;: small work units, back to back. The estimate lags, the frequency hunts, and you get exactly this oscillation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decisive experiment: pin everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# root required; SELinux Enforcing blocks a direct write, so chmod first&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in &lt;/span&gt;0 1 2 3 4 5 6 7&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;g&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/sys/devices/system/cpu/cpu&lt;span class="nv"&gt;$c&lt;/span&gt;/cpufreq/scaling_governor
  &lt;span class="nb"&gt;chmod &lt;/span&gt;666 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$g&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo &lt;/span&gt;performance &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$g&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed pinned: cpu0-3 @1785, cpu4-6 @2419, cpu7 @2841 MHz.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result: only 20% faster
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;schedutil&lt;/th&gt;
&lt;th&gt;performance&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;0.5 tok/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+20%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First token&lt;/td&gt;
&lt;td&gt;1863 ms&lt;/td&gt;
&lt;td&gt;1619 ms&lt;/td&gt;
&lt;td&gt;−13%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;20%.&lt;/strong&gt; Throttling is real, but it is not the dominant factor.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Updated elimination table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hypothesis&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Missing ARM optimization (&lt;code&gt;-march&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Real, fixed&lt;/strong&gt; — 2.5x on prefill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads scheduled onto little cores&lt;/td&gt;
&lt;td&gt;❌ Ruled out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenMP not actually threading&lt;/td&gt;
&lt;td&gt;❌ Ruled out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mmap pages evicted&lt;/td&gt;
&lt;td&gt;❌ Ruled out (&lt;code&gt;LLAMA_LOAD_MODE_MLOCK&lt;/code&gt; changed nothing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU frequency throttling&lt;/td&gt;
&lt;td&gt;⚠️ Real, but only ~20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  7. What's left
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU at full clock (2419/2841 MHz)
threads on big cores (cpu4-7)
dotprod instructions present (verified by disassembly)
        |
        v
Utilization of peak compute capacity: ~1%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per token: 1.5B params x 2 = 3 GFLOPs
measured: 1.67 s per token
effective: 1.8 GFLOPS
peak: ~182 GFLOPS (4x A76 at full clock)
utilization: 1%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per token reads all weights = 1.06 GB
1.06 GB / 1.67 s = 635 MB/s
available bandwidth: 17-34 GB/s
utilization: 2-4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Neither the CPU nor the memory bus is anywhere near saturated. It's just slow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The remaining explanation points inside &lt;strong&gt;ggml's quantized kernels on this platform&lt;/strong&gt; — memory access patterns, cache behaviour, or OpenMP barrier overhead on small work units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can't fix that from configuration. It stays on the list as the next thing to dig into.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Why this post is about method, not answers
&lt;/h2&gt;

&lt;p&gt;The investigation produced no final answer — but the &lt;em&gt;process&lt;/em&gt; is reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero-dependency diagnostics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $39}'&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/task/&amp;lt;tid&amp;gt;/stat      &lt;span class="c"&gt;# current CPU per thread&lt;/span&gt;
&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $14+$15}'&lt;/span&gt; /proc/&amp;lt;pid&amp;gt;/task/&amp;lt;tid&amp;gt;/stat  &lt;span class="c"&gt;# cumulative CPU time per thread&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/devices/system/cpu/cpu&lt;span class="k"&gt;*&lt;/span&gt;/cpufreq/scaling_cur_freq   &lt;span class="c"&gt;# live frequency&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/devices/system/cpu/cpu&lt;span class="k"&gt;*&lt;/span&gt;/cpufreq/scaling_governor   &lt;span class="c"&gt;# governor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Works on any Android device. No extra tooling.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Negative results are results
&lt;/h3&gt;

&lt;p&gt;Ruling out A, B and C &lt;em&gt;is&lt;/em&gt; progress. It shrinks the search space from "anything" to "inside ggml's kernels".&lt;/p&gt;

&lt;p&gt;If I only published the successful fix, you'd think this was a clean path. Real engineering is mostly wrong hypotheses and the value is eliminating them quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute the ceiling first
&lt;/h3&gt;

&lt;p&gt;Most people say "my model is slow" without quantifying it. Work out the memory-bandwidth ceiling and the compute ceiling, then measure. &lt;strong&gt;Knowing the gap is 60-120x tells you the problem isn't "needs tuning" — it's "something isn't running at all".&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Restore your environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Restored to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU governor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;schedutil&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELinux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Enforcing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stay-awake flag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;screen timeout&lt;/td&gt;
&lt;td&gt;60000 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;temp scripts&lt;/td&gt;
&lt;td&gt;deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Don't leave side effects on a test device — especially SELinux.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Next
&lt;/h2&gt;

&lt;p&gt;Next up: &lt;strong&gt;ggml quantized kernel efficiency on ARM&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which code path does Q4_K_M take?&lt;/li&gt;
&lt;li&gt;What's the cache miss rate?&lt;/li&gt;
&lt;li&gt;How much does the OpenMP barrier cost per generated token?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've dug into this, I'd love to hear it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1: &lt;a href="https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645"&gt;Why Your Phone Runs LLMs 80x Slower Than It Should&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Part 2: this post&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Code: &lt;a href="https://github.com/Pingredsai/local-ai-android" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-ai-android&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>llm</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Why Your Phone Runs LLMs 80x Slower Than It Should (And What I Found)</title>
      <dc:creator>Pingredsai</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:45:01 +0000</pubDate>
      <link>https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645</link>
      <guid>https://dev.to/pingredsai/why-your-phone-runs-llms-80x-slower-than-it-should-and-what-i-found-2645</guid>
      <description>&lt;h1&gt;
  
  
  Why Your Phone Runs LLMs 80x Slower Than It Should (A Debugging Log)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Tags: on-device inference / llama.cpp / Android / performance&lt;br&gt;
Status: draft&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I ran &lt;strong&gt;Qwen2.5-1.5B-Instruct Q4_K_M&lt;/strong&gt; fully offline on a &lt;strong&gt;Google Pixel 4&lt;/strong&gt; (Snapdragon 855, 2019).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First token&lt;/td&gt;
&lt;td&gt;1.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;1.3-2.0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;402% (4 threads saturated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Theoretical limits say this device should do &lt;strong&gt;30-60 tok/s&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;It is 60-120x slower.&lt;/strong&gt; And the CPU is already maxed out.&lt;/p&gt;

&lt;p&gt;Here is the full debugging log — including three mistakes I made.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Do the math first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory bandwidth bound:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model size      1.06 GB
SD855 bandwidth ~34 GB/s
one token reads all weights once
-&amp;gt; theoretical ceiling ~32 tok/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Compute bound:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.5B params x 2 FLOPs = 3 GFLOPs per token
SD855 CPU peak ~182 GFLOPS
-&amp;gt; theoretical ceiling ~60 tok/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both ceilings land in the 30-60 tok/s range. &lt;strong&gt;Measured: 0.5.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Knowing the gap (60-120x) tells you what to suspect.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Suspect 1: thread count? (ruled out)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell top &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="nt"&gt;-o&lt;/span&gt; PID,%CPU,%MEM,ARGS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID    %CPU   %MEM   ARGS
17834  402%   24.3%  com.example.localai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;402%&lt;/strong&gt; — four threads fully saturated. Not a threading problem. Ruled out.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Suspect 2: missing ARM optimization? (HIT)
&lt;/h2&gt;

&lt;p&gt;This is the valuable one.&lt;/p&gt;

&lt;p&gt;llama.cpp does its heavy lifting in &lt;code&gt;ggml&lt;/code&gt;. From &lt;code&gt;ggml/src/ggml-cpu/CMakeLists.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;GGML_NATIVE&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# probe host CPU, add -mcpu=native&lt;/span&gt;
    ...
&lt;span class="nb"&gt;else&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;GGML_CPU_ARM_ARCH&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;APPEND ARCH_FLAGS -march=&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GGML_CPU_ARM_ARCH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- key&lt;/span&gt;
    &lt;span class="nb"&gt;elseif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;GGML_CPU_ALL_VARIANTS&lt;span class="p"&gt;)&lt;/span&gt;
        ...
    &lt;span class="c1"&gt;# neither set -&amp;gt; NO -march at all&lt;/span&gt;
&lt;span class="nb"&gt;endif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When cross-compiling for Android:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GGML_NATIVE&lt;/code&gt; is &lt;code&gt;OFF&lt;/code&gt; (can't probe the target)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GGML_CPU_ARM_ARCH&lt;/code&gt; defaults to &lt;strong&gt;empty&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Result: &lt;strong&gt;no &lt;code&gt;-march&lt;/code&gt; is passed at all&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compiler falls back to the &lt;strong&gt;baseline ARMv8-A&lt;/strong&gt; — no dotprod, no fp16.&lt;/p&gt;

&lt;p&gt;Quantized matrix multiplication (99% of LLM compute) degrades to a slow path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"-DUSE_LLAMA_CPP=ON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: prefill went from 114 s to 45 s — 2.5x faster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Better. But not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the optimization actually landed
&lt;/h3&gt;

&lt;p&gt;Don't trust the flag. Disassemble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llvm-objdump &lt;span class="nt"&gt;-d&lt;/span&gt; libggml-cpu.so | &lt;span class="nb"&gt;grep &lt;/span&gt;sdot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight armasm"&gt;&lt;code&gt;&lt;span class="nl"&gt;18f558&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;&lt;span class="nb"&gt;e829420&lt;/span&gt;    &lt;span class="nv"&gt;sdot&lt;/span&gt;   &lt;span class="nv"&gt;v0&lt;/span&gt;&lt;span class="mf"&gt;.4&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="mf"&gt;.16&lt;/span&gt;&lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;v2&lt;/span&gt;&lt;span class="mf"&gt;.16&lt;/span&gt;&lt;span class="nv"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;sdot&lt;/code&gt; is there.&lt;/strong&gt; dotprod is active. Ruled out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Still 0.5 tok/s.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Suspect 3: mmap pages getting evicted? (ruled out)
&lt;/h2&gt;

&lt;p&gt;Android reclaims mmap'ed pages under memory pressure. Then every weight read falls back to flash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flash read ~1.5 GB/s
1.06 GB / 1.5 GB/s ~ 700 ms/token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Right order of magnitude for the 2 s/token we saw.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I forced the model to stay resident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;mparams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLAMA_LOAD_MODE_MLOCK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result: still 0.5 tok/s. Ruled out.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Three mistakes I made (worth avoiding)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: assuming &lt;code&gt;llama_batch_get_one&lt;/code&gt; pins position to 0
&lt;/h3&gt;

&lt;p&gt;I claimed: &lt;em&gt;"&lt;code&gt;llama_batch_get_one()&lt;/code&gt; sets pos to 0, so every token recomputes the whole sequence."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I hand-rolled &lt;code&gt;llama_batch_init()&lt;/code&gt; with explicit positions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually happened:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reading the header: &lt;code&gt;llama_batch_get_one&lt;/code&gt;'s &lt;code&gt;pos&lt;/code&gt; is &lt;strong&gt;&lt;code&gt;nullptr&lt;/code&gt;&lt;/strong&gt; — llama.cpp assigns positions automatically. &lt;strong&gt;Not 0.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;My hand-rolled version &lt;strong&gt;hung the prefill&lt;/strong&gt; (43 tokens, never returned).&lt;/li&gt;
&lt;li&gt;Reverting to &lt;code&gt;llama_batch_get_one&lt;/code&gt; fixed it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Lesson: for perf issues, suspect compiler flags and memory access first — not a detail you "remember".&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: using a removed API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;llama_kv_cache_clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g_ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// removed in current llama.cpp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;llama_memory_clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llama_get_memory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g_ctx&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Mistake 3: guessing struct field names
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;mparams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_mmap&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// fields no longer exist&lt;/span&gt;
&lt;span class="n"&gt;mparams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use_mlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merged into one enum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;mparams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLAMA_LOAD_MODE_MLOCK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common lesson: llama.cpp's API moves fast. Always read &lt;code&gt;include/llama.h&lt;/code&gt;. Never write from memory.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Where it landed
&lt;/h2&gt;

&lt;p&gt;After ruling out three suspects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 GFLOPs per token
2 s per token measured
-&amp;gt; ~0.8% of peak CPU utilization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it is not a bandwidth wall (~500 MB/s measured vs ~34 GB/s available).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So compute efficiency itself is the problem&lt;/strong&gt; — beyond "wrong config". Could be Android CPU scheduling, OpenMP synchronization overhead, or platform-specific behavior in llama.cpp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is not a one-day fix. And it should not block shipping.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What I did about it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship it and state the number honestly&lt;/strong&gt; — README says "Pixel 4: 0.5 tok/s"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain the limitation&lt;/strong&gt; — old SoC + large model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the community for more data&lt;/strong&gt; — Snapdragon 8 Gen 2/3 results welcome&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move performance work to v2&lt;/strong&gt; instead of blocking v1&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. Transferable lessons
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Check compiler flags first&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-march&lt;/code&gt; / dotprod issues are the #1 cause of slow on-device inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU saturated ≠ efficient&lt;/td&gt;
&lt;td&gt;402% usage can mean you're running the slowest implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute theoretical limits&lt;/td&gt;
&lt;td&gt;The gap tells you what to suspect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify with disassembly&lt;/td&gt;
&lt;td&gt;Flags set ≠ instructions generated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read the header, don't recall it&lt;/td&gt;
&lt;td&gt;llama.cpp API churns fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set a stop-loss for the project&lt;/td&gt;
&lt;td&gt;Perf work must not block shipping forever&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Appendix: environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Google Pixel 4 (Snapdragon 855, 6GB, Android 13)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Qwen2.5-1.5B-Instruct-Q4_K_M.gguf (1065 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;commit 8a1a9b5 (2026-10-09)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NDK&lt;/td&gt;
&lt;td&gt;30.0.16248370&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMake&lt;/td&gt;
&lt;td&gt;4.1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build flags&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16&lt;/code&gt;, &lt;code&gt;-DUSE_LLAMA_CPP=ON&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;n_threads=4&lt;/code&gt;, &lt;code&gt;n_ctx=2048&lt;/code&gt;, &lt;code&gt;load_mode=MLOCK&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;Full implementation (Kotlin + JNI + llama.cpp + Jetpack Compose), MIT licensed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Pingredsai/local-ai-android" rel="noopener noreferrer"&gt;https://github.com/Pingredsai/local-ai-android&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running the same model on newer hardware? Send me your numbers — that is exactly what this project needs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>llm</category>
      <category>performance</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
