Skip to content

Browser RAG UI, GPU/CPU tuning, tests, CI, and LAN serving - #1

Merged
ankamteja merged 5 commits into
mainfrom
feat/rag-web
Aug 20, 2026
Merged

Browser RAG UI, GPU/CPU tuning, tests, CI, and LAN serving#1
ankamteja merged 5 commits into
mainfrom
feat/rag-web

Conversation

@ankamteja

@ankamteja ankamteja commented Aug 20, 2026

Copy link
Copy Markdown
Owner

What this adds

Browser UI + OpenAI-compatible RAG endpoint (rag/bin/rag-web.py, port 8083).
The chat server on :8081 answers from model weights alone; this applies retrieval
first and serves both a chat page and a /v1/chat/completions endpoint that adds a
sources field. Bound LAN-wide behind the bearer token so other devices can use it;
set RAG_WEB_HOST=127.0.0.1 for loopback-only.

Shared core + tests (rag/bin/ragcore.py, tests/). Chunking, scoring, prompt
assembly extracted so the CLI and web paths agree. 59 tests run on a laptop / in CI
against stand-in model servers — no phone, no GGUF weights. GitHub Actions runs pytest
(py3.11–3.13), ruff, and shellcheck.

Performance — prompt eval 18 → 70 tok/s, generation 10 → 12 tok/s. Prompt
processing now runs on the Adreno GPU via Mesa's turnip Vulkan driver; generation runs
on the six pinned performance cores. A 605-token retrieval prompt starts answering in
8.9 s instead of 36 s. Full matrix and reproduction in bench/RESULTS.md.

Docs rewritten for a general audience, with measured numbers replacing estimates
and no concrete local network addresses.

Verification

  • python3 -m pytest tests/ -q → 59 passed
  • ruff check . → clean
  • Live-tested on device: GPU offload confirmed (70 tok/s prompt eval), browser UI and
    LAN auth verified over Wi-Fi.

The chat server on :8081 answers from model weights alone; retrieval only
happened inside rag-ask.py, so anything else pointed at the phone -- curl, a
browser, any OpenAI client -- silently got answers that had never seen the
notes. This moves retrieval behind an HTTP surface of its own.

rag-web.py serves a chat page and an OpenAI-compatible /v1/chat/completions on
:8083, both applying retrieval first, and holds the index in memory instead of
re-reading it per question. It binds loopback by default and is reached over
adb forward, so it is never exposed to the campus network; a bearer token is
required if it is ever bound to a routable address.

ragcore.py now holds the chunking, scoring and prompt assembly that rag-index,
rag-ask and rag-web all share, so the three cannot drift apart. Vectors are
kept as pre-normalized float32 arrays, which cuts the resident index from about
38 MB to 4.8 MB -- worth doing on a phone already near its RAM ceiling -- and
turns cosine similarity into a plain dot product.

Retrieved context is now bounded by a character budget rather than a fixed
chunk count. Time to first token is prompt-eval bound at roughly 20 tokens per
second, so prompt length is the wait; a fixed top_k let that wait vary with
whatever the chunker happened to produce.

Tests cover the whole path against stand-in embedding and chat servers, so they
run on a laptop and in CI with no phone and no model weights.
Prompt processing, not generation, is what you wait on before an answer starts,
and it was running at 17.9 tokens per second. Two things were leaving the phone
idle.

The GPU was never used. The handoff recorded "no GPU backend" as a property of
the phone, but it is a property of the installed build: the Adreno 830 is
reachable from unrooted Termux through Mesa's turnip Vulkan driver, and
llama.cpp ships a matching backend as a Termux package. Prompt eval goes to
70.2 tokens per second, so a 605-token RAG prompt starts answering after 8.9
seconds instead of 36.

The vendor OpenCL path is a dead end and worth recording as one: the Android
linker refuses an absolute path into /vendor/lib64 from an app namespace, and
the bare soname resolves to Termux's own ICD loader, so it can never find a
platform. Vulkan sidesteps this because turnip runs entirely in userspace.

The CPU is 6 performance cores plus 2 prime cores, and spreading 8 threads over
all of them is slower than pinning 6 threads to the matched ones -- generation
goes 9.1 to 12.2 tokens per second. The threads landing on the prime cores
finish early and idle, and the two cores left free absorb the OS and the GPU
driver. The chat server now takes cpu0-5 and the embedding server cpu6-7, which
also ends the contention of the two asking for 12 threads on an 8-core phone.

GGML_BACKEND_PATH is left deliberately unset, with a comment saying why:
pointing it at a single .so restricts ggml to that one backend and silently
drops the GPU, which cost an hour of chasing during this work.

--prio is dropped: raising thread priority needs root and only logged
"Operation not permitted" once per thread.

bench/RESULTS.md records the full matrix and bench/probe.py reproduces the
headline numbers against a running server.
…sured ones

The README claimed 15 tokens/sec generation and 33 prompt, both estimates that
turned out to be wrong in opposite directions once measured. It now carries the
numbers from bench/RESULTS.md and says which half runs on the GPU and why.

rag/README gains the part that was easiest to get wrong: :8081 answers from the
model's weights alone and :8083 is the one that applies retrieval, so anything
pointed at the chat server directly silently gets answers that never saw the
notes.
…neral audience

Two changes that landed together.

Serving: the browser UI and OpenAI-compatible endpoint were loopback-only,
reached over adb forward. They now bind LAN-wide at boot so other devices on the
same network can use them without a cable, which means every request except
/health must carry the bearer token. The page reads whether auth is required
from /health, prompts for the token when it is, remembers it per browser, sends
it on every request, and re-prompts on a 401. This is a deliberate exposure: the
endpoint reads out of a private notes corpus and the token travels in clear text
over HTTP, so RAG_WEB_HOST=127.0.0.1 returns it to loopback-only on an untrusted
network.

Docs: the public documentation no longer addresses a specific person or narrates
their plans, and no longer prints a concrete LAN address. Replaced the real IP
with a <phone-ip> placeholder, generalised the network context, corrected the
key-generation command to one that exists on Termux, and removed the claim that a
subnet size implies who can reach the service. Deployment-specific and planning
notes are kept out of the repository entirely.
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1499a1a-c3c2-4cb9-8739-cbc9406bd429


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

main removed GUIDE.md and updated README; this branch had edited GUIDE and the
README/rag-README. Resolved by dropping GUIDE.md as main intended, removing the
now-dangling links to it, and keeping the neutral, non-personal wording on both
sides of the rag-README conflict.
@ankamteja
ankamteja merged commit 317ca90 into main Aug 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant