Resilient in-room R1 mesh — design for shitty devices that brick constantly
Target: throw N (cheap, flaky) Rabbit R1s in a room; they self-organize into a working distributed-inference mesh and keep answering while devices brick, drop WiFi, and rejoin. No node is load-bearing. No central coordinator in the data path.
This builds on the proven pieces: per-node memory sharding (partial loader, cos=1.0),
the /embed → /forward(X-Layer-Range) → /lm-head wire across real nodes, and the
monster-guard qspec quality gate (the golden star).
Hard assumptions
- Devices brick at random — mid-idle, mid-forward, mid-decode. Constantly.
- WiFi is lossy / partitions. BTLE for discovery is flaky too.
- In one room — low latency, decent bandwidth, but unreliable links.
- A model is sharded into stages (contiguous layer ranges) that each fit a node's
~2.4 GB RAM budget (see
R1_MESH_SCALING_PLAN.md).
Principles
- No load-bearing node. Every stage
[a,b)is held by ≥2 replicas. The mesh survives any single brick and degrades gracefully as more die (down to 1 replica/stage = still works, just no headroom). - Stateless hops. A forward is a pure function of
(residual, start_pos, layer_range). Any replica of a stage can serve any hop → failover = re-send to another replica, no shared state required. - Coordinator-free routing. Each node knows the live replicas of the next stage (from gossip) and routes to one; on timeout/error it retries an alternate replica. The caller hits any stage-0 replica; the answer bubbles back.
- Self-healing membership. Nodes gossip
{node_id, model_id, stage, ip:port, health, load, epoch}. Missed heartbeats → evicted from routing. New nodes → assigned an under-replicated stage (RAM-aware) → admitted, then serve. - Admission-gated joins. A joining node must pass the monster-guard
qspecon its shard before it's added to a stage pool — a corrupt/flaky/lying node can't poison the mesh. (Reuses the golden-star gate per stage.) - Fail fast, not hang. If a stage has zero live replicas, the request errors with
stage K unavailableimmediately rather than hanging — the caller can retry once the mesh re-forms.
KV / decode resilience
The forward uses a per-position KV cache that lives on whichever replica processed the prior tokens. If that replica bricks mid-decode, its KV is gone. Two modes:
- STATELESS (max resilience, default for the experiment): every token re-prefills through a freshly-routed path (no cross-token KV reuse). Brick-proof — any token can be served by any live path. Slower (re-reads the prompt each token) but never wedges.
- STICKY+re-prefill (faster): a sequence pins to a path and reuses KV; on a hop failure the coordinator-node re-prefills the prompt on a new path and resumes. Faster steady state, with a recovery cost on each brick.
Start STATELESS; add STICKY+re-prefill once the membership layer is solid.
Components (build order)
/chainwith a next-stage replica POOL + failover (MESH FIRST — building now).fat-station --stage a..b --next url1,url2,..../chain: do my stage, then try the next-stage replicas in randomized order until one returns; bubble the result back. Static pool now; gossip fills it later. This is the resilience primitive — it already survives bricks given a populated pool.- Liveness + gossip registry. Periodic heartbeat over WiFi (UDP gossip) + a BTLE
beacon advertising
{node_id, model, stage, ip}. Each node maintains a live per-stage replica view; evict on missed heartbeats. (Discovery + "find each other".) - RAM-aware stage assignment / ring election. On join, a node picks the most under-replicated stage it can fit; ephemeral elector breaks ties; no runtime coordinator.
- qspec admission per stage. A joiner proves its shard (argmax gate) before serving.
- Boot service + equip. fat-station as a boot service bound to the WiFi IP; the
mass-unknotflashing/setup script provisions binary + shard + manifest at equip time (plug → equip → release).
Failure modes → behavior
| failure | behavior |
|---|---|
| node bricks idle | evicted on missed heartbeat; routing skips it |
| node bricks mid-forward | hop times out → caller-node retries another replica |
| stage loses all replicas | request fails fast (stage K unavailable) |
| WiFi partition | nodes re-discover + re-admit on reconnect |
| new node joins | gossip → under-replicated stage → qspec admit → serves |
| corrupt/lying node | fails qspec → never admitted |
In-room experiment (the demo)
N R1s in a room on WiFi. Power them on → they gossip, self-assign replicated stages, admit via qspec, and serve. Run sustained inference (a steady token stream) while you physically unplug/brick random devices — the mesh re-routes around them and keeps answering; plug them back and throughput recovers. The headline: quality-gated distributed inference that survives a room of unreliable $200 devices.
Status
Proven: sharding, the wire, the qspec gate, correct inference (bowl + qwen3 fixes).
Component 1 DONE (gnosis
3b0f53ae):/chainserver-side peer relay +--nextnext-stage replica pool + failover (3 s connect-timeout). Verified: entry→exit relay argmax 12095; with a dead replica first in the pool, 3/3 fail over to the live one. Caller hits only the entry node; hops are peer-driven.Full stack proven at 7B (2026-05-24):
/chainrelay over a memory-sharded 7B — mistral-7b split into two 3.6 GB shards (each node loads only its half via the partial loader), caller→entry(shard 0-16)→exit(shard 16-32)+lm-head → 'Paris' @ 13.225. And sharded == monolithic for mistral-7b: residualcos=1.0, same argmax. So peer-relay + per-node memory sharding + correct inference compose end-to-end on a real 7B.SOVEREIGN R1↔R1 MESH OVER WIFI — PROVEN (2026-05-24): two R1s on WiFi ("Hawk", 10.0.0.199 + 10.0.0.240). R1#1 (entry, layers 0-12) relays the residual directly to R1#2 (exit, layers 12-24) over WiFi — no Mac/coordinator in the data path; R1#2's log shows the relayed
/chainhop. End to end: caller(WiFi) → R1#1(WiFi) → R1#2(WiFi) → argmax 12095 @ 16.82, 766 ms warm, zero USB in the path. Two untethered $200 devices doing quality-correct distributed inference peer-to-peer. The transport-agnostic/chainlit up on WiFi unchanged.Component 2 DONE (gnosis
00c8c134+ac508c5d): UDP gossip membership + liveness (src/mesh_gossip.rs).--gossip-port/--peers/--node-id. Each node advertises its stage + data endpoint;/chainauto-populates its failover pool from LIVE discovered peers (static--nextas fallback);/mesh/peersfor observability. Direct heartbeats refresh liveness, relayed lists are discovery-only. Verified over localhost: entry auto-discovers 2 exit replicas →/chainroutes via the discovered pool (12095, no static next); brick a replica → evicted within the 8 s window →/chainself-heals to the survivor (3/3). Gossip rides houseflow_frame::FlowFrames (the aeon wire), VENT-tagged, on a dedicated mesh stream 2010 — distinct from 2002 (the Thoth/pneuma conscious stream) so control traffic never collides with consciousness; one const retargets to 2002.Component 2 VERIFIED OVER REAL WIFI (2026-05-24): two R1s on "Hawk" (10.0.0.199 + .240), no static
--next. They discover each other via WiFi gossip (2010 FlowFrames) →/chainauto-routes via the discovered pool → 12095; each yields the 2002 brain digest (/mesh/hive: nodes=2, stages [[0,12,1],[12,24,1]], covered 0-24, status "healthy (no redundancy)"). A self-organizing, self-aware untethered mesh. (Restart gotcha: a backgroundedadb -s X shell 'pkill; nohup…'can silently fail to replace the station — verify the new process via its log.)Component 5 (equip) DONE (gnosis through
43000911):scripts/r1/equip.sh(per-node) +scripts/r1/mass-equip.sh(whole fleet). Catch-and-release: plug in, run mass-equip, unplug. Each node gets: idempotent binary push; root-aware ops (su 0 sh -cwhen adb shell isn't root — R1s vary); a kernel wakelock (/sys/power/wake_lock) + stay-awake so a RELEASED node on battery doesn't deep-doze and suspend fat-station; a setsid-detached start that survives USB unplug; a bulletproof restart (root-pkill + wait-until-gone, so a stale station can't hold the port) with a node-id verify (role-only checks miss stale stations); a pushed boot-rejoin hook (host-built thenadb push, no nested-quote breakage). mass-equip deals devices round-robin across stages so redundancy grows with the fleet.equip.sh --apkalso does the post-flash OS prep itself (launcher, WebView provider, lockscreen off, provisioned), so no external setup script is needed. Verified: onemass-equip.sh-> r1-0 entry + r1-1 exit, both wakelocks held, mesh forms (/chain 12095). Reboot-survival is the in-app node (see R1_INAPP_NODE.md).Catch-and-release runbook: flash the device with the mtkclient toolkit in
packages/aeon-r1-unknot(the "unknot" flasher) ->scripts/r1/equip.sh --apk(one command: APK + OS prep + self-election conf + launch) -> unplug (keeps serving on WiFi via wakelock+setsid; re-joins on every boot). Inspect:curl <node>:8090/mesh/hive.Component 4 (qspec admission) DONE (gnosis through
c15a09db): a joining node can't be routed to until it passes the golden-star PARIS gate. Registry gains anadmittedflag + opt-inadmission_required; an entry/both node with--admitprobes each discovered next-stage peer (embed + my layers -> peer/forward+/lm-head-> argmax) and admits ONLY onargmax == --admit-expect(default 12095).next_stage_poolreturns only admitted peers; admission survives heartbeats. Verified on localhost: good exit -> ADMITTED,/chain12095; wrong-expect (bad-node proxy) -> REJECTED, pool empty,/chain503.--admitthreads through equip.sh + mass-equip.sh, somass-equip.sh --admitgives a qspec-gated fleet. 6 unit tests.Component 3 (RAM-aware stage election): DONE + WIRED (gnosis
9be8c515+ the elector): the election is a pure fnrecommend_stage_from(peers, model_id, stages, capacity_mb, target_replicas)(therecommend_stagemethod delegates to it) — counts live replicas per stage, recommends the most under-replicated stage belowtarget_replicas, capacity tie-break. 11 lib tests green; kept offPeerInfo(no ripple into the concurrently-owned fat-station.rs). WIRED at SHARD-ASSIGNMENT time, not fat-station startup: a node's stage is tied to the shard it holds on disk, so a rebooted node must resume its shard — it can't re-elect a stage whose shard it lacks. So the host toolmesh-elect(src/bin/mesh-elect.rs) runs during provisioning: it pulls a live seed's/mesh/peers, reads the joining device's free RAM, and prints the stage to take.mass-equip.shcalls it per device (device 0 seeds as entry; each later device elects vs the live mesh), falling back to round-robin if the seed is unreachable. VERIFIED LIVE: against the running R1 seed (10.0.0.199) a new 2.4 GB node is told12..24(the under-replicated stage), exit 0. CAVEAT: the capacity tie-break uses a ~1 GB/layer heuristic tuned for ~7 B shards; for the equal-size 12-layer stages we run today the under-replication signal dominates and the tie-break is moot — refinemb-per-layerwhen running heterogeneous big-model shards.5b (reboot survival): DONE + PROVEN ON HARDWARE (aeon-shell
e18659b; seedocs/R1_INAPP_NODE.md). Thesuboot hook was architecturally dead (this GSI forbids apps from usingsuor exec'ing/databinaries). The working mechanism: ship fat-station aslibfatstation.soin the APK'sjniLibsand have the Aeon Shell launcher exec it from its ownnativeLibraryDir(allowed foruntrusted_app) on every boot, reading/data/local/tmp/mesh-node.conf(role/layers/peers/...) and the world-readable knot in place; holds an appPARTIAL_WAKE_LOCK(no root) and auto-detects the wlan0 IP (DHCP-safe). VERIFIED on R1919109A4H16000691813: app reads the knot, binds the LAN port, serves correct inference (argmax 12095 @16.82); rebooted the entry node and it rejoined autonomously in ~12 s,/chain-> 12095, zero manual intervention. The mesh node now lives in the device's own OS surface, not as an external root-managed binary. REMAINING (polish, not blocking): reproducible APK packaging of the binary +extractNativeLibs,equip.shwritingmesh-node.conf, remove the dead Rustsuhook.Research line: protocol69 /
teleportfor mesh (docs/R1_MESH_PROTOCOL69.md): FOIL-over-RF backing (a) RF discovery beacon as a WiFi-partition-tolerant liveness channel feeding stream-2010 gossip, and (b) cache-key teleport handoff — a pre-flight/.aeon/teleport-admitprobe before each/chainforward that skips recompute on cache-warm hops.- (a) RF-gossip bridge — BUILT + VERIFIED (everything but the SDR front-end):
src/rf_beacon.rs(Protocol69RfBeacon, fail-closedto_peer_info/to_gossip_frame)bin/rf-gossip-bridge(selftest/file sources;rtlsdra gated stub) + themesh_gossippeer_heartbeat_frame/decode_heartbeat_frameinjection API. Live test: a phantom beacon entered a running station's/mesh/peers; a forged envelope (expectedProjection:68) was dropped fail-closed. Standalone — no fat-station.rs change. REMAINING: the RTL-SDR demod front-end, gated on the R1 USB-OTG probe.
- (b) cache-key teleport handoff — BUILT + VERIFIED (opt-in
--teleport-cache): each/chainhop derives a prompt hash (fnv1a_64_tokens, propagated asX-Prefix-Hash), and on an identical PREFILL prefix replays its cached forward output (protocol69-gated,geodesicLength:0) instead of recomputing — a server-side guard inhandle_chain(no residual round-trips back to the caller). Bit-exact (store/replay of the real residual; shape-checked), prefill-only (start_pos==0). Verified on a 2-node local chain: run1 543 ms argmax 12095 @16.82; run2 (both hops teleport) 17 ms argmax 12095 @16.82 BIT-IDENTICAL — 32.6x. Off by default = zero risk to the proven path. REMAINING: cache TTL/eviction (grows unbounded today).
- (a) RF-gossip bridge — BUILT + VERIFIED (everything but the SDR front-end):
Fit-aware election -> data-parallel "both" lanes (2026-05-24):
elect_stage_from_gossipnow checks whether the model fits the node's RAM (model size <= 60% of MemAvailable). When it does — qwen-0.5b is 373 MB on ~2.7 GB-free nodes — the node electsrole=bothand serves the WHOLE model (layers 0-24) as one lane instead of taking a pipeline stage. This dissolves the entry-bound bottleneck: the old split made stage-0 the cap (one entry feeding many exits); now N nodes = **N independent full-model lanes**, the path to >=Nx throughput (cf. the cannon +MeshBatchSpeedup.lean). Only a model too big for one node falls back to the0..12/12..24split election.Gossip node health (jam awareness) — DONE (2026-05-24): every node advertises a compact
NodeStatus(free/total RAM, 1-min load, in-flight request count, battery, temp, derivedjammed) on the gossip heartbeat (PeerInfo.status,#[serde(default)]so it's wire-compatible with pre-status peers). A router drop-guard counts DATA-plane requests in flight (survives handler panics — the truest jam signal); a 1.5 s sampler refreshes the advertised health so each heartbeat is fresh.next_stage_poolroutes AROUNDjammedpeers (falling back to them only if every replica of the next stage is jammed).GET /statusexposes the same snapshot for a direct poll. So the whole mesh — routing, election, the 2002 brain — sees who is saturated without polling each node. (In-app nodes: battery/temp read "unknown" under SELinux; RAM/load/in-flight are live.)FLEET REFRESH — 5 nodes live, all
both(2026-05-24): refreshed all live R1s to the gossip-NodeStatus build (USB, one at a time,equip.sh --tcpip --apk), each re-electing torole=both. Node ids are now hardware-hash (r1-<bitwise hash of ANDROID_ID>; seeR1_FLEET.md). Power config flipped to screen-sleeps-but-node-stays-active (short screen timeout + WifiLock + no-doze; saves heat/throttle) and adb-over-wifi (:5555) is enabled so released nodes stay manageable. Verified: all 5/statushealthy,role=both, gossip-status propagating peer-to-peer (a refreshed node sees another's livefree_mb).Next: the RTL-SDR front-end + on-R1 NESDR USB-OTG probe (RF path); flash the 6th R1; the live >=5x throughput benchmark (point aeon
wallat the 5bothlanes); persistent adb-over-wifi (persist.adb.tcp.port, survives reboot — needs root); teleport cache TTL/eviction for production.
Benchmarks
Reproduce: scripts/r1/mesh-bench.py --entries <ip1>,<ip2> (latency cold/warm + sequential
vs concurrent throughput). ALWAYS label cold vs warm AND power state (battery-released
devices throttle vs plugged-in).
2026-05-24 — 4 nodes, fully redundant (entry x2, exit x2), all battery-released on WiFi (qwen-0.5b, stage plan 0..12/12..24, 5-token prefill -> logits, all argmax 12095):
| metric | value |
|---|---|
| latency cold (first /chain) | 3073 ms |
| latency warm, r1-0 x8 | mean 3801 (min 3120, max 4620) ms |
| latency warm, r1-3 x8 | mean 4481 (min 3727, max 5228) ms |
| throughput sequential (16 req) | 0.24 req/s |
| throughput concurrent (2 pipelines) | 0.76 req/s |
| concurrency speedup | 3.23x |
- Battery-released latency is ~4-5x the earlier plugged ~766 ms 2-R1 figure — MediaTek throttles clocks off-charger (the wakelock keeps it awake, not fast). Keep the room's R1s charging if latency matters.
- The 3.23x exceeds the 2x you'd expect from two entry->exit pipelines alone, because
each node also BATCHES concurrent requests (
NATIVE_BATCH_CAPACITY=8), amortizing the per-request fixed cost. Formalized:gnosis-math/Gnosis/MeshBatchSpeedup.lean(pipeline parallelism P + per-node batching with positive fixed overhead -> speedup > P).
2026-05-24 — 5 nodes, all both (full-model data-parallel lanes), battery-released
(the fit-aware-election topology; mesh-bench.py --entries <5 ips> --n 20, all argmax 12095):
| metric | value |
|---|---|
| latency cold (first /chain) | 3128 ms |
| latency warm, per node x4 | mean ~1.6-1.9 s (min 1495, max 2530) ms |
| throughput sequential (20 req) | 0.59 req/s |
| throughput concurrent (5 lanes) | 3.36 req/s |
| concurrency speedup | 5.66x |
- 5.66x clears the >=5x target — 5 independent full-model lanes plus per-node batching.
Switching from the entry/exit split to fit-aware
bothremoved the single-entry cap (the old 4-node split topped out at 3.23x); now the speedup scales with node count again. - Warm latency roughly HALVED vs the split pipeline (~1.7 s vs ~3.8-4.5 s, both
battery-released) because a
bothnode serves the whole model locally — no WiFi relay hop in the data path. Off-charger MediaTek throttle still dominates absolute latency; keep the room charging for speed.