The rullama apps — the consumer product family, all running Gemma 4 on your local GPU (with optional cloud providers). This repo holds three front-ends:
apps/web/— a browser-resident AI PWA (React + Vite + Tailwind + Workbox) that runs inference in the browser via WebGPU + WASM. Chat (text / vision / audio-input), a Knowledge tab (drop docs → embed → RAG), a Fine-tune tab (in-browser LoRA), and a Voice tab — your data never has to leave the device.apps/native/— the desktop + mobile app (.NET / Avalonia) with a Rustrust-coreC-ABI shim. Ships to the app stores.apps/cli/— the agentic CLI (BYOK providers, tools, MCP, sessions).
All three build on the rullama-framework
platform — the OSS inference engine (rullama-engine + the rullama-lora
trainer) and agent harness (rullama-* crates: agents, tools, memory, RAG,
providers, MCP). The engine + harness live in that sibling repo, not here.
One brand — rullama — across the stack, all at
rullama.com. Brainwires is the company / GitHub org, not a project name. See the canonical topology doc:rullama-framework/docs/ARCHITECTURE-engine-harness.md.
| App | Consumes the platform via |
|---|---|
apps/web/ |
the engine's wasm bundle at /pkg/rullama.js (built from rullama-framework/engine/rullama-lora), driven in a Worker; cloud via the BYOK /api/cloud/* proxy. |
apps/native/ |
the C-ABI rust-core shim linking rullama-engine + rullama-lora directly (P/Invoke — no HTTP, no wasm). |
apps/cli/ |
path-deps the rullama-framework harness crates (its own Cargo workspace). |
The dev-server can also host the engine over an OpenAI-compatible
/v1/chat/completions endpoint for any local client.
| Path | What it is |
|---|---|
apps/web/ |
The PWA (React + Vite + Tailwind + Workbox SW). Imports the engine wasm bundle from /pkg/rullama.js. |
apps/native/ |
Desktop + mobile app: app/ (.NET / Avalonia) + rust-core/ (C-ABI cdylib over the engine). Own build; excluded from the root workspace. |
apps/cli/ |
The agentic CLI — its own Cargo workspace (own Cargo.lock), path-deps ../../../rullama-framework. Excluded from the root workspace. |
services/dev-server/ |
The dev/serve HTTP server (Vite proxy, /api/blob, /api/models, /api/cloud/*, /pkg/*, cross-repo wasm-bundle watcher). Excluded from the workspace; run via --manifest-path. |
services/worker/ |
Cloudflare Worker — the production BYOK cloud proxy (deployed standalone via wrangler). |
xtask/ |
cargo dev + cargo docker:* dispatcher (std-only). The root workspace is just this. |
pkg/ |
The engine wasm bundle (built artifact, --out-name rullama; gitignored, at the repo root). |
The inference engine + LoRA trainer live in the
rullama-framework repo as
rullama-engine / rullama-lora (an isolated engine/ wasm32 sub-workspace).
The iOS bench harness moved there too (engine/tools/ios-bench).
- ✅
gemma4:e2btext inference on the desktop loads end-to-end and generates greedy output bit-identical to Ollama. (gemma4:e4bis shape-compatible — pull and try it.) - ✅
gemma4:e2btext inference on iPhone — full Q4_K_M model loaded into iPhone 16e (A18, 8 GB shared RAM) and streaming tokens at ~4.65 tok/s via a Dedicated Worker + sync OPFS path. Multimodal towers stay Mac-only for now; mobile picks the text-only loader (max_context=512). - ✅ Vision + audio multimodal on the desktop. ViT (16 blocks, 768
hidden) + Conformer (12 blocks, 1024 hidden) towers run on the same wgpu
device as the text path; soft tokens splice into the prompt via
<|image>/<|audio>sentinels. Validated bit-identical to Ollama on a fixed image and a 30-second pangram WAV. - ✅ Q4_K + Q6_K + F16 + F32 quants (the actual mix in
gemma4:e2bQ4_K_M). - ✅ Streaming load via HTTP byte-range requests or OPFS sync access
handles — the 7 GB GGUF never enters wasm linear memory in bulk. The
PWA writes to OPFS once via
FileSystemSyncAccessHandle.write()in a worker, and reads tile-by-tile during inference, so the wasm peak stays in the tens of MiB regardless of model size. - ✅ Multi-turn chat with system prompt, mid-generation Stop, persistent KV cache.
- ✅ Encoder chained + per-layer submits (M7 + M15) — one CommandEncoder spans each transformer layer, submitted incrementally so the GPU drains smoothly even on tight-RAM phones.
- ✅ In-browser LoRA fine-tuning (
rullama-lora, wasm + native). Backward kernels for matmul Q4_K / Q6_K, rmsnorm, rope, geglu, attention, cross-entropy; Adam optimizer over GPU buffers; rank-r LoRA on attention- FFN projections. 200-step overfit-one drops loss from ~17.7 → 0 on the
dev fixture. Trained adapters export as safetensors and load back into
the inference
ModelvialoadAdapter— no roundtrip through native. The PWA's Fine-tune tab drives all of this in the foreground tab.
- FFN projections. 200-step overfit-one drops loss from ~17.7 → 0 on the
dev fixture. Trained adapters export as safetensors and load back into
the inference
- ❌ MoE
gemma4:26b/gemma4:31b— out of scope. - ❌ Other architectures (llama, mistral, qwen, phi).
- 🛠️ Mobile multimodal — desktop multimodal works; the iPhone loader currently skips the vision/audio towers to fit in shared RAM. Lazy upload for those is a follow-up.
You need:
- Rust ≥ 1.91 +
wasm-pack(cargo install wasm-pack --locked --version 0.13.1) - A WebGPU-capable browser (Chrome 113+, Edge 113+, recent Firefox; iOS Safari 17.4+ for phones)
- Ollama installed locally with
gemma4:e2bpulled (ollama pull gemma4:e2b)
The engine bundle is built from a sibling rullama-framework engine checkout. With
../rullama-framework/engine present, cargo dev rebuilds it automatically
when engine source changes; otherwise build it once:
# Unified bundle — inference (`Model`) + training (`TrainingSession`) surfaces.
# Run inside the engine checkout. --out-name rullama keeps pkg/rullama.js stable.
# Override the location with RULLAMA_ENGINE_DIR.
cd ../rullama-framework/engine
wasm-pack build rullama-lora --target web --release \
--out-dir ../../rullama/pkg --out-name rullamaThis emits pkg/rullama.js + pkg/rullama_bg.wasm + TypeScript typings into
the app's pkg/. (Engine internals — kernels, GGUF, towers, parity — are
documented in the rullama-framework engine repo.)
The user-facing browser app lives in apps/web/ — a production-quality React + Vite
- Tailwind + Workbox chat PWA (service-worker offline shell, restart dialog on
deploys, attachment UI, conversation history in OPFS + SQLite via
rsqlite-wasm) built against the shared wasm bundle.
# React / Vite PWA — auto-runs the wasm bundle build via `pnpm dev`.
cd apps/web
pnpm install
pnpm dev # https://localhost:5173/The first load streams the ~7 GB blob from the local Ollama install (or an R2
mirror — see scripts/upload-models-to-r2.sh) through a Dedicated Worker that
owns a FileSystemSyncAccessHandle over OPFS. Bytes go network → sync handle
→ disk without ever pinning a Blob in the JS heap. Subsequent loads (within
the same Safari session) reuse the cached file.
The PWA is fully drivable from the Mac via Apple's safaridriver:
# One-time setup on the phone:
# Settings → Safari → Advanced → Remote Automation = on
# Web Inspector = on
# Feature Flags → WebGPU = on
# Then on the Mac:
safaridriver -p 4444 &
./apps/web/serve-iphone.sh # HTTPS serve reachable from the phone's Safari
./apps/web/test/iphone-test.sh # navigate → Load → chat → log perf/tmp/rullama-page.log collects beacon traces from the page ([chat],
[pe], [tg], [gen], [wkr], [rs]) so any regression in a phone
run leaves a server-side trail even after a WebContent crash.
A .NET 9 / Avalonia 11 app (apps/native/app/, Rullama.sln) over a Rust
C-ABI shim (apps/native/rust-core/, a cdylib/staticlib) that links the engine
crates directly — no HTTP, no wasm. Runs on macOS / Windows / Linux desktop
(builds with the plain dotnet SDK, no Xcode) plus an Android head; iOS needs
Xcode 16. The engine Model is !Send, so each handle owns one OS thread and
calls are marshalled to it inside Rust (Avalonia → RustCore P/Invoke → rust-core → rullama-engine on wgpu Metal/DX12/Vulkan). Chat, multimodal, tools, voice
(Kokoro TTS), in-app model downloads, LoRA fine-tuning, RAG, voice-cloning, and
ROME editing are wired. Shipped through the app stores.
cd apps/native
cargo build --manifest-path rust-core/Cargo.toml --release # build the C-ABI core
./scripts/build-rust.sh release # stage the native lib
dotnet run --project app/Rullama.ProbeSmoke # verify the P/Invoke path
dotnet run --project app/Rullama.Desktop # run the desktop apprust-core is excluded from the root workspace (it links wgpu); it path-deps
../../../../rullama-framework/engine/{rullama-engine,rullama-lora}. See
apps/native/README.md for the full toolchain + heads.
An AI agentic coding CLI (installed binary: rullama) — multi-agent
orchestrator/worker decomposition, a rich tool system (file/bash/git/web), an MCP
client, interactive / single-shot / batch / TUI modes, plan + task management, and
semantic conversation memory. BYOK: on first run it prompts you to pick a
provider (Ollama-local / OpenAI / Anthropic / …) — no hosted account.
cd apps/cli
cargo build --release
cargo install --path . # installs the `rullama` binary
rullama # first run: pick + configure a providerapps/cli/ is its own Cargo workspace (own Cargo.lock), excluded from the root and
path-depping the ../../../rullama-framework harness crates. The legacy Studio
remote-bridge (web control of CLI agents) is kept behind the off-by-default
remote-bridge feature. See apps/cli/README.md.
compose.yaml packages the built PWA + a model-blob HTTP service behind
nginx, designed to sit behind Cloudflare. The Cargo workspace ships
cargo docker:* aliases (dispatched through the xtask crate) so the
deploy loop doesn't need shell aliases:
| Alias | Effective command |
|---|---|
cargo docker:build |
docker compose build |
cargo docker:start |
docker compose up -d |
cargo docker:stop |
docker compose down |
cargo docker:restart |
docker compose build --no-cache then docker compose up -d --force-recreate |
cargo docker:logs |
docker compose logs -f --tail=200 |
cargo docker:ps |
docker compose ps |
First run compiles xtask (~1 s); subsequent invocations reuse the cached
binary. Add new tasks by appending a match arm in xtask/src/main.rs and
a corresponding line in .cargo/config.toml. The compose file's
OLLAMA_MODELS_DIR env var picks the host's model store; defaults to
/usr/share/ollama/.ollama/models.
The engine's native parity examples (greedy_parity, model_api,
chained_smoke, …) run the same code paths against host wgpu (Metal on macOS,
Vulkan on Linux) without a browser. They live with the engine, not in this
repo — run them from the sibling
rullama-framework engine
checkout against rullama-engine, e.g.:
cd ../rullama-framework/engine
cargo run -p rullama-engine --release --example greedy_parity -- \
~/.ollama/models/blobs/sha256-<digest> "Hi" 5See the engine repo's README for the full list.
rullama-lora runs LoRA SGD against the live wgpu kernels — no Burn, no
PyTorch, no separate runtime. Scope: rank-r LoRAs on
attn_q / attn_k / attn_v / attn_o and the FFN projections, Adam, global
L2 grad clipping, gradient accumulation, mixed precision, gradient
checkpointing. PerPosition CE is a single-forward variant with a ~C/2 speedup
vs. the multi-forward path.
In the browser: the unified wasm bundle (see Get the wasm bundle)
exposes TrainingSession to JS alongside Model. The Fine-tune tab in
apps/web/ drives a full session — dataset upload, hyperparam UI, live
loss chart, save adapter to OPFS as safetensors. The same Model that's
loaded for inference accepts the trained adapter via Model.loadAdapter(bytes)
(re-runs in the chat tab against the adapted weights).
Native: the native trainer examples (overfit_one, train_jsonl,
eval_adapter) live with the engine now — run them from the rullama-framework engine
repo against rullama-lora (e.g.
cargo run -p rullama-lora --release --example overfit_one -- <gguf>). See
the engine repo's README.
PWA (host page) ──┐
▼ postMessage RPC
┌──────────────────────────────────────────────────────────────────┐
│ inference-worker.js (Dedicated Worker) │
│ ▶ owns FileSystemSyncAccessHandle for the GGUF │
│ ▶ owns the wasm Model handle │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ wasm32 (Rust, the rullama-engine bundle) │ │
│ │ Model.loadFromOpfs(read_fn, total) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ GgufReader (header only, ~5 MB) │ │
│ │ │ │ │
│ │ │ TensorFetcher (OPFS sync read | HTTP Range)│
│ │ ▼ │ │
│ │ WeightCache ─────────▶ Forward / VisionForward / │ │
│ │ (lazy GPU upload, GpuAudioForward │ │
│ │ per-tile range fetch (per-layer encoder │ │
│ │ on big tensors) submits, GPU-resident │ │
│ │ KV cache) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ wgpu (WebGPU / Metal / Vulkan) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ WGSL kernels: matmul Q4_K/Q6_K/F16, rmsnorm, │ │
│ │ rmsnorm_per_row, rope_neox, attention (incl. │ │
│ │ HPD-f16 + block-local + subgroup variants), │ │
│ │ conv2d, geglu, softcap, residual_add, scale, │ │
│ │ top_k, quick_gelu, plus backward kernels for │ │
│ │ training (cross_entropy, rmsnorm, rope, geglu, │ │
│ │ attention dQ / dKV, matmul Q4_K / Q6_K, Adam) │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
▲ postMessage replies (tokens, errors)
PWA renders tokens, manages chat history, handles attachments.
The Worker move (M15) is what unblocked iPhone inference: iOS Safari only
exposes FileSystemSyncAccessHandle in Worker contexts, and the Worker
isolates inference from main-thread page-watchdog reapers.
The reference Go implementation lives in Ollama's tree under
model/models/gemma4/. Every op in the engine's reference/forward.rs
(CPU oracle), forward_chained.rs (production GPU forward),
multimodal/vision.rs, and multimodal/audio.rs corresponds 1:1 — see the
rullama-engine crate in the rullama-framework repo.
Measurements as of M15:
| Target | Steady-state tok/s (gen) | Notes |
|---|---|---|
| iPhone 16e (A18, iOS 26) | ~4.65 tok/s | text-only, max_context=512 |
| AMD Radeon Pro 555 (Mac) | ~1 tok/s (M7 baseline) | naive kernels, tiled matmul deferred |
The architectural foundation (chained encoder, GPU-resident KV cache, per-layer submits, per-tile range fetch from OPFS) is in place. Inference kernels are still naive matvec; reaching ≥10 tok/s on both Mac and phone needs tiled matmul + bind-group caching + kernel fusion (the M8 line on the roadmap).
The iPhone A18 advertises 1 GiB for both max_buffer_size and
max_storage_buffer_binding_size — four times the WebGPU spec floor — so
there's real headroom for fewer/larger weight buffers (currently 455 of
them resident, see M15 follow-ups).
Other capability notes captured during iPhone validation:
shader-f16✓ — packed FP16 MADs engage on A18.timestamp-query✓ — Pro 555 doesn't expose this; could wire GPU-side per-pass timing.subgroups✗ — A18 has SIMDgroup hardware but Safari's WebGPU doesn't surface WGSL subgroup ops yet. Vision attention falls through to the no-subgroup HPD-f16 kernel automatically.
Dual-licensed under either of:
- Apache License 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Contributions are accepted under the same dual-license terms.