Quazim0t0 commited on
Commit
6e3b98a
·
verified ·
1 Parent(s): 54981c8

Upload 88 files

Browse files
README.md CHANGED
@@ -1,13 +1,168 @@
1
  ---
2
- title: ModuleMind
3
- emoji: 🌖
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.16.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "Quazim0t0's 🍄 Thousand Token Wood Entry"
3
+ emoji: 🍄
4
+ colorFrom: green
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.15.2
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  ---
12
 
13
+ # ⚔️ Modular Mind: Boss Fight
14
+
15
+ A mini **Dark-Souls-style** duel where the boss is controlled by a **Modular Mind** —
16
+ a handful of tiny neural *specialists* that communicate through a **shared latent**
17
+ (a `RecursiveLink` bridge) and a *coordinator* that reads the latent to choose the
18
+ boss's next move. **The brain was trained by self-play reinforcement learning** — its
19
+ tactics emerged from playing thousands of duels, nothing is scripted.
20
+
21
+ You play the **Fire Knight**. Defeat the **Demon Slime**.
22
+
23
+ ## How the Modular Mind works
24
+
25
+ This is the [ModularMind-on-V2](https://github.com/your-username/ModularMind) concept at *specialist scale*: instead of one
26
+ monolithic policy, six small networks each handle one concern and talk to each other
27
+ through a latent channel.
28
+
29
+ ```
30
+ game state ─▶ ┌──────────────┐ each specialist emits a latent
31
+ │ Aggressor │──┐ (LatentProjection) and, if it OWNS
32
+ │ Stalker │──┤ an action, a "drive" for that action
33
+ │ Survivor │──┤
34
+ │ Baiter │──┤ ┌───────────────┐ ┌─────────────┐
35
+ │ Punisher (M) │──┼─ sum ─▶│ RecursiveLink │─────▶│ Coordinator │─▶ action
36
+ │ Enrage (M) │──┘ │ ReGLU+residual│ shared│ read-out │
37
+ └──────────────┘ └───────────────┘ latent└─────────────┘
38
+ ```
39
+
40
+ - **Four action-owning specialists** push their move's score directly:
41
+ **Aggressor → CLEAVE**, **Stalker → APPROACH**, **Survivor → RETREAT**, **Baiter → IDLE**.
42
+ - **Two modulators (M)** — **Punisher** ("the player is open!") and **Enrage**
43
+ ("we're low on HP — go berserk") — **own no action**. Their *only* way to affect the
44
+ fight is the latent they write into the shared `RecursiveLink`, which the coordinator
45
+ turns into modulation. So training has to *learn to use the latent channel* — the whole
46
+ point of the architecture.
47
+
48
+ The right-hand panel shows all of this live: each specialist's activity, the shared
49
+ latent bridge, and the coordinator's modulation, for every decision the boss makes.
50
+
51
+ ## What emerged from training
52
+
53
+ Trained on a reward that values *dealing damage* and *pressuring in range* over
54
+ playing it safe (landing a cleave ≫ whiffing, and stalling / staying out of range is
55
+ penalised), the boss learned an **aggressive pressure** style:
56
+
57
+ - **closes the distance** when you're far or at mid-range,
58
+ - **cleaves on contact** — once you're in range and it's off cooldown it commits to a
59
+ lunging swing essentially every time (verified: in-range attack-rate ≈ **0.8–1.0**),
60
+ - **retreats only when it can't swing** (mid-cooldown) to reset spacing,
61
+ - **blocks your punish** — a **Defender** specialist raises a guard (negating ~90% of
62
+ your melee) when you swing at it and it can't cleave back,
63
+ - **punishes your recovery** and gets **even more aggressive at low HP** — the Enrage
64
+ modulator raises CLEAVE through the shared latent.
65
+
66
+ It reaches a **~55–65% win rate** against a near-optimal scripted dodger (avg reward
67
+ +8, up from −12 before the reward was tuned for aggression). Against a human it's a
68
+ fair, readable fight: **dodge the red telegraph, then punish the recovery.**
69
+
70
+ > The earlier version of the brain learned a degenerate "space forever, never commit"
71
+ > policy that *technically* won but barely attacked — so the trainer now selects the
72
+ > checkpoint on **win-rate + in-range attack-rate**, and a non-attacking policy can no
73
+ > longer be saved. (`behavior()` in `train.py` measures this directly.)
74
+
75
+ ## It learns from your fights (online finetuning)
76
+
77
+ The model is tiny, so a gradient step is microseconds — the boss finetunes from real
78
+ play **on the free CPU**. Each HARD-tier fight is logged (state, action, HP per boss
79
+ decision) and POSTed to `/learn`; the server rebuilds per-decision rewards (damage
80
+ dealt − taken, + kill / − death), computes REINFORCE returns, and takes **one Adam
81
+ step** ([`mm_grad.py`](mm_grad.py), numpy backprop verified against PyTorch to ~1e-8).
82
+ A frozen copy of the sim-trained weights anchors the update so it can't drift into
83
+ nonsense; the adapted weights feed straight back into the live boss.
84
+
85
+ - **On by default, in-memory.** Set `MM_ONLINE=0` to disable.
86
+ - **Persistent across restarts:** add Space secrets `HF_TOKEN` (write) and
87
+ `MM_DATASET_REPO` (e.g. `you/boss-fight-online`) and the adapted weights are pushed
88
+ to / pulled from that Dataset. Only HARD-tier fights train (keeps the data on-policy).
89
+
90
+ ## Difficulty = the trained brain's decision-noise
91
+
92
+ The difficulty selector doesn't change the boss's stats — it runs the **same trained
93
+ Modular Mind at a different *mistake-rate*** (`explore` = probability of a random legal
94
+ action). vs a near-optimal scripted dodger:
95
+
96
+ | Tier | Mistake-rate | Boss win-rate | Feel |
97
+ |------|------|------|------|
98
+ | **Easy** | 50% | ~0.35 | erratic, leaves big openings — beatable |
99
+ | **Normal** | 22% | ~0.65 | competent pressure, occasional slip |
100
+ | **Hard** | 4% | ~0.95 | closes in, blocks your punish, relentless |
101
+
102
+ The browser sends the chosen tier with every decision; the server routes it through
103
+ `modular_mind.decide` with that tier's mistake-rate. *(Originally Easy/Normal were
104
+ genuinely-undertrained checkpoints — but once the boss learned to BLOCK it dominates
105
+ the sim almost immediately, so there's no longer a weak checkpoint; decision-noise is
106
+ the controllable, honest dial. `train.py` still snapshots checkpoints if you want them.)*
107
+
108
+ ## Controls
109
+
110
+ | Key | Action |
111
+ |-----|--------|
112
+ | ← → | Move |
113
+ | Space | Roll / dodge (i-frames, costs stamina) |
114
+ | J | Attack |
115
+ | K (hold) | **Block** — cuts incoming damage to 20% and drains stamina; if stamina hits 0 your guard **breaks** into a stagger |
116
+ | M / 🔊 | Mute / unmute music + SFX |
117
+
118
+ Background music and combat SFX play during the fight (a random track per fight,
119
+ plus per-action sound effects). Audio is served statically from `audio/`.
120
+
121
+ You begin each fight with a one-time **Aegis shield** (a cyan bubble + the *Aegis*
122
+ HUD bar): for the first few seconds it absorbs **all** damage so you can learn the
123
+ controls. Once it fades it does **not** come back.
124
+
125
+ > Click **Enter the Fog**, then click the game once so it has keyboard focus.
126
+
127
+ ## Architecture / files
128
+
129
+ | File | Role |
130
+ |------|------|
131
+ | `app.py` | Gradio Space: serves the game, exposes the trained brain at `/decide` |
132
+ | `modular_mind.py` | **numpy** inference of the trained Modular Mind (no torch at runtime) |
133
+ | `mm_torch.py` | the trainable Modular Mind (specialists + RecursiveLink + coordinator) |
134
+ | `train.py` | self-play **REINFORCE** trainer → `mm_weights.npz` |
135
+ | `mm_grad.py` | pure-numpy forward+backward (REINFORCE gradient), verified vs torch — the online learner |
136
+ | `online.py` | finetunes the HARD brain from real player fights; optional HF-Dataset persistence |
137
+ | `duel_sim.py` | the headless duel simulator (the RL environment) |
138
+ | `features.py` | shared feature/action definitions (single source of truth) |
139
+ | `web/` | the HTML5 canvas game (60fps render; calls `/decide` at decision points) |
140
+ | `audio/` | background music (`mp3/`) and sound effects (`sfx/`), served statically |
141
+ | `serve_local.py` | run the whole thing locally **without gradio** (stdlib + numpy) |
142
+
143
+ The model is **tiny** (~4.5k parameters) and inference is pure numpy, so the Space
144
+ needs only `gradio` + `numpy` and starts instantly.
145
+
146
+ ## Run / retrain locally
147
+
148
+ ```bash
149
+ pip install -r requirements.txt # gradio + numpy (runtime)
150
+ python app.py # the Space, locally
151
+ # or, with no gradio at all:
152
+ python serve_local.py # http://localhost:7861
153
+
154
+ # retrain the boss (needs torch):
155
+ pip install torch
156
+ python train.py # -> mm_weights.npz (+ train_log.json)
157
+ ```
158
+
159
+ ## Credits
160
+
161
+ - **Sprites:** *Fire Knight* and *Demon Slime* free asset packs by **LuizMelo**
162
+ (itch.io). Please check their licenses for your own use.
163
+ - **Audio:** background music tracks (`audio/mp3`) and combat SFX (`audio/sfx`).
164
+ Check the licenses of your audio packs before publishing.
165
+ - **Brain:** the *Modular Mind* concept (latent-communicating specialists via
166
+ `RecursiveLink`), trained here at specialist scale by self-play RL.
167
+
168
+ Built for a HuggingFace hackathon.
agents/__pycache__/panel.cpython-313.pyc ADDED
Binary file (23.1 kB). View file
 
agents/modmind/__pycache__/config.cpython-313.pyc ADDED
Binary file (6.17 kB). View file
 
agents/modmind/__pycache__/model.cpython-313.pyc ADDED
Binary file (59.9 kB). View file
 
agents/modmind/__pycache__/moe_gradio.cpython-313.pyc ADDED
Binary file (33.7 kB). View file
 
agents/modmind/__pycache__/registry.cpython-313.pyc ADDED
Binary file (2.41 kB). View file
 
agents/modmind/__pycache__/specialist_presets.cpython-313.pyc ADDED
Binary file (3.11 kB). View file
 
agents/modmind/__pycache__/spike_tokenizer.cpython-313.pyc ADDED
Binary file (6.16 kB). View file
 
agents/modmind/language/checkpoints/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:632f4dc7cd328200cc798b9ae707e48643fc8bdcb6f11c817d61cec1b154496d
3
+ size 186170504
agents/modmind/links/language__from__reasoning.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:79240c1ed535d6afae2559f020616812b06290e96c93ecf19a09395360cb328c
3
+ size 188927842
agents/modmind/moe_gradio.py CHANGED
@@ -53,8 +53,12 @@ KEY_CHARS = string.ascii_letters + string.digits # must match train_link.py
53
 
54
 
55
  def _latest_ckpt(domain: str):
56
- cks = sorted(glob.glob(str(CKPT_ROOT / domain / "checkpoints" / "step_*.pt")))
57
- return cks[-1] if cks else None
 
 
 
 
58
 
59
 
60
  class SpikeWhaleMoE:
@@ -79,11 +83,20 @@ class SpikeWhaleMoE:
79
  continue
80
  cfg = specialist_config(domain)
81
  m = SpikeWhaleLM(cfg).to(self.device).eval()
82
- st = torch.load(ck, map_location=self.device, weights_only=False)
83
- m.load_state_dict(st["model_state"])
 
 
 
 
 
 
 
 
 
84
  self.models[slot] = m
85
  self.toks[slot] = SpikeTokenizer(vocab_file=str(tok_path))
86
- self.steps[slot] = int(st.get("step", 0))
87
  self._mtime[slot] = mt
88
  self._load_links()
89
  return list(self.models)
@@ -91,6 +104,30 @@ class SpikeWhaleMoE:
91
  def available(self):
92
  return list(self.models)
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def _load_links(self):
95
  """Load a TRAINED RecursiveLink bridge (train_link.py output) from
96
  links/<asker>__from__<consultant>.pt and overlay its bridge-trained injection path
@@ -99,33 +136,47 @@ class SpikeWhaleMoE:
99
  self.trained_link = None
100
  self.bridge_asker = None
101
  self.link_meta = None
 
102
  for asker_domain, consultant_domain in LINKS:
103
  a, c = DOMAIN2SLOT.get(asker_domain), DOMAIN2SLOT.get(consultant_domain)
104
  if a not in self.models or c not in self.models:
105
  continue
106
- lp = CKPT_ROOT / "links" / f"{asker_domain}__from__{consultant_domain}.pt"
107
- if not lp.exists():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  continue
109
- st = torch.load(lp, map_location=self.device, weights_only=False)
110
  link = RecursiveLink(d_latent=D_LATENT).to(self.device).eval()
111
- link.load_state_dict(st["link"])
 
112
  try:
113
- self.models[a].model.latent_inject.load_state_dict(st["asker_latent_inject"])
114
  except Exception:
115
  pass
116
- # FULL fine-tuned asker, if this bridge was saved with the updated train_link.py.
117
- # Without it we can show latent *influence* (consult) but not reproduce key-recall.
118
- if "asker_state" in st:
119
  ba = SpikeWhaleLM(specialist_config(asker_domain)).to(self.device).eval()
120
- ba.load_state_dict(st["asker_state"])
121
  self.bridge_asker = ba
122
  self.trained_link = link
123
- self.link_meta = {
124
- "asker": a, "consultant": c,
125
- "key_len": int(st.get("key_len", 6)), "prompt": st.get("prompt", "KEY> "),
126
- "with_latent": float(st.get("with_latent", float("nan"))),
127
- "without_latent": float(st.get("without_latent", float("nan"))),
128
- }
129
  return # one bridge is enough for the panel demo
130
 
131
  def key_recall_available(self):
@@ -326,15 +377,14 @@ class SpikeWhaleMoE:
326
  merged[k] = lsd[k].clone() # integer/bool buffers: copy as-is
327
  m = SpikeWhaleLM(specialist_config("language")).to(self.device).eval()
328
  m.load_state_dict(merged)
329
- # make it bridge-ready: re-load the bridge-trained injection weights straight from the
330
- # link file (self-contained). The injection only fires when we pass inject_latent.
331
- try:
332
- lp = CKPT_ROOT / "links" / "language__from__reasoning.pt"
333
- if lp.exists():
334
- st = torch.load(lp, map_location=self.device, weights_only=False)
335
- m.model.latent_inject.load_state_dict(st["asker_latent_inject"])
336
- except Exception:
337
- pass
338
  self._merge_cache = (alpha, m)
339
  return m
340
 
 
53
 
54
 
55
  def _latest_ckpt(domain: str):
56
+ base = CKPT_ROOT / domain / "checkpoints"
57
+ sft = base / "model.safetensors"
58
+ if sft.exists():
59
+ return str(sft) # slim fp16 safetensors (preferred)
60
+ cks = sorted(glob.glob(str(base / "step_*.pt")))
61
+ return cks[-1] if cks else None # fall back to a full .pt checkpoint
62
 
63
 
64
  class SpikeWhaleMoE:
 
83
  continue
84
  cfg = specialist_config(domain)
85
  m = SpikeWhaleLM(cfg).to(self.device).eval()
86
+ if ck.endswith(".safetensors"):
87
+ from safetensors.torch import load_file
88
+ from safetensors import safe_open
89
+ sd = load_file(ck, device=self.device)
90
+ sd = {k: (v.float() if v.is_floating_point() else v) for k, v in sd.items()}
91
+ m.load_state_dict(sd)
92
+ with safe_open(ck, framework="pt") as f:
93
+ step = int((f.metadata() or {}).get("step", 0))
94
+ else:
95
+ st = torch.load(ck, map_location=self.device, weights_only=False)
96
+ m.load_state_dict(st["model_state"]); step = int(st.get("step", 0))
97
  self.models[slot] = m
98
  self.toks[slot] = SpikeTokenizer(vocab_file=str(tok_path))
99
+ self.steps[slot] = step
100
  self._mtime[slot] = mt
101
  self._load_links()
102
  return list(self.models)
 
104
  def available(self):
105
  return list(self.models)
106
 
107
+ def ensure_device(self, device):
108
+ """Move all loaded models/links to `device`. On ZeroGPU this is called inside the
109
+ @spaces.GPU function so CUDA is only touched there, never at import/startup.
110
+ .to(device) is a no-op when already there, so it's safe to call on every request."""
111
+ for k in list(self.models):
112
+ self.models[k] = self.models[k].to(device)
113
+ if self.link is not None:
114
+ self.link = self.link.to(device)
115
+ if self.trained_link is not None:
116
+ self.trained_link = self.trained_link.to(device)
117
+ if self.bridge_asker is not None:
118
+ self.bridge_asker = self.bridge_asker.to(device)
119
+ cache = getattr(self, "_merge_cache", None)
120
+ if cache is not None:
121
+ self._merge_cache = (cache[0], cache[1].to(device))
122
+ self.device = device
123
+
124
+ def to_gpu_if_available(self):
125
+ try:
126
+ if torch.cuda.is_available():
127
+ self.ensure_device("cuda")
128
+ except Exception:
129
+ pass
130
+
131
  def _load_links(self):
132
  """Load a TRAINED RecursiveLink bridge (train_link.py output) from
133
  links/<asker>__from__<consultant>.pt and overlay its bridge-trained injection path
 
136
  self.trained_link = None
137
  self.bridge_asker = None
138
  self.link_meta = None
139
+ self._bridge_ali = None
140
  for asker_domain, consultant_domain in LINKS:
141
  a, c = DOMAIN2SLOT.get(asker_domain), DOMAIN2SLOT.get(consultant_domain)
142
  if a not in self.models or c not in self.models:
143
  continue
144
+ base = CKPT_ROOT / "links" / f"{asker_domain}__from__{consultant_domain}"
145
+ sft, pt = Path(str(base) + ".safetensors"), Path(str(base) + ".pt")
146
+ if sft.exists():
147
+ from safetensors.torch import load_file
148
+ from safetensors import safe_open
149
+ t = load_file(str(sft), device=self.device)
150
+ t = {k: (v.float() if v.is_floating_point() else v) for k, v in t.items()}
151
+ with safe_open(str(sft), framework="pt") as f:
152
+ md = f.metadata() or {}
153
+ link_sd = {k[5:]: v for k, v in t.items() if k.startswith("link.")}
154
+ ali_sd = {k[4:]: v for k, v in t.items() if k.startswith("ali.")}
155
+ asker_sd = {k[6:]: v for k, v in t.items() if k.startswith("asker.")} or None
156
+ key_len = int(md.get("key_len", 6)); prompt = md.get("prompt", "KEY> ")
157
+ wl = float(md.get("with_latent", "nan")); nl = float(md.get("without_latent", "nan"))
158
+ elif pt.exists():
159
+ st = torch.load(str(pt), map_location=self.device, weights_only=False)
160
+ link_sd = st["link"]; ali_sd = st["asker_latent_inject"]; asker_sd = st.get("asker_state")
161
+ key_len = int(st.get("key_len", 6)); prompt = st.get("prompt", "KEY> ")
162
+ wl = float(st.get("with_latent", float("nan"))); nl = float(st.get("without_latent", float("nan")))
163
+ else:
164
  continue
 
165
  link = RecursiveLink(d_latent=D_LATENT).to(self.device).eval()
166
+ link.load_state_dict(link_sd)
167
+ self._bridge_ali = ali_sd
168
  try:
169
+ self.models[a].model.latent_inject.load_state_dict(ali_sd)
170
  except Exception:
171
  pass
172
+ # FULL fine-tuned asker (if present) -> lets us reproduce key-recall live.
173
+ if asker_sd:
 
174
  ba = SpikeWhaleLM(specialist_config(asker_domain)).to(self.device).eval()
175
+ ba.load_state_dict(asker_sd)
176
  self.bridge_asker = ba
177
  self.trained_link = link
178
+ self.link_meta = {"asker": a, "consultant": c, "key_len": key_len, "prompt": prompt,
179
+ "with_latent": wl, "without_latent": nl}
 
 
 
 
180
  return # one bridge is enough for the panel demo
181
 
182
  def key_recall_available(self):
 
377
  merged[k] = lsd[k].clone() # integer/bool buffers: copy as-is
378
  m = SpikeWhaleLM(specialist_config("language")).to(self.device).eval()
379
  m.load_state_dict(merged)
380
+ # make it bridge-ready: overlay the bridge-trained injection weights (loaded by
381
+ # _load_links, format-agnostic). The injection only fires when we pass inject_latent.
382
+ ali = getattr(self, "_bridge_ali", None)
383
+ if ali is not None:
384
+ try:
385
+ m.model.latent_inject.load_state_dict(ali)
386
+ except Exception:
387
+ pass
 
388
  self._merge_cache = (alpha, m)
389
  return m
390
 
agents/modmind/reasoning/checkpoints/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:352ed238052385a248d19808a8492b283915ac5d42719c3801f46a4191605d1a
3
+ size 186170504
agents/panel.py CHANGED
@@ -1,292 +1,312 @@
1
- """
2
- panel.py -- the Gradio section for the bottom of the boss app: a live demo of the
3
- Modular-Mind mixture-of-experts.
4
-
5
- For the SpikeWhale backend it leads with the *latent bridge* (the real result) and
6
- organizes the three demos into tabs. Every handler is a generator that yields an
7
- instant "loading/generating" message first, so the first run never looks frozen while
8
- the ~80M models lazy-load. Hot-reloads checkpoints.
9
- """
10
- from __future__ import annotations
11
-
12
- import os
13
- import sys
14
-
15
- import gradio as gr
16
-
17
- EMOJI = {"language": "📖 Language", "math": "➗ Math", "tool": "🛠️ Tool-use"}
18
- DEVICE = os.environ.get("MM_AGENTS_DEVICE", "cpu")
19
- # Self-contained SpikeWhale bundle that ships next to this file (agents/modmind/: the 80M
20
- # specialists + bridge + inference code). If it's present we default to the SpikeWhale backend
21
- # so the HuggingFace Space "just works" with no env config. Env vars still override.
22
- _BUNDLED_MODMIND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modmind")
23
- _DEFAULT_BACKEND = "spikewhale" if os.path.isdir(_BUNDLED_MODMIND) else "bytegpt"
24
- _SPIKEWHALE = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower() in ("spikewhale", "modmind")
25
- _WARMED = {"done": False} # so the "loading the models" notice only shows on the first run
26
-
27
- _FOOTER = (
28
- "Two ~80M dense specialists — 📖 Language (FineWeb-Edu) and ➗ Math (FineMath) — sharing a "
29
- "16k length-max tokenizer. A coordinator routes by bits-per-byte, and a trained RecursiveLink "
30
- "lets them communicate in latent space (proven in the Bridge tab). Hot-reloads checkpoints."
31
- if _SPIKEWHALE else
32
- "Three byte-level ~10M specialists, streamed-trained on FineWeb-Edu / FineMath / "
33
- "glaive-function-calling. Tiny + early-trained, so generations are rough — the routing "
34
- "(which expert is most confident) is the point. It hot-reloads as training continues."
35
- )
36
-
37
-
38
- def _get_moe():
39
- """Pick the MoE backend. Defaults to the bundled SpikeWhale 80M specialists
40
- (agents/modmind/) when present, else the byte-level ByteGPT experts. MM_MOE_BACKEND
41
- and MODMIND_DIR override."""
42
- backend = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower()
43
- if backend in ("spikewhale", "modmind"):
44
- mm_dir = os.environ.get("MODMIND_DIR", _BUNDLED_MODMIND)
45
- if mm_dir and mm_dir not in sys.path:
46
- sys.path.insert(0, mm_dir) # front: ModMind's model.py wins over agents/model.py
47
- from moe_gradio import get_moe
48
- return get_moe
49
- from orchestrator import get_moe
50
- return get_moe
51
-
52
-
53
- def _loading_notice(action="Generating"):
54
- """First-run popup + in-place message so nothing ever looks frozen."""
55
- if not _WARMED["done"]:
56
- gr.Info("First run — loading the models (~20–40s on CPU). After this, it's quick.")
57
- return (f"### ⏳ Loading the models + {action.lower()}…\n"
58
- "*First run loads the ~80M specialists into memory — this can take ~20–40s on CPU. "
59
- "Every run after this is fast.*")
60
- return f"### ⏳ {action}…"
61
-
62
-
63
- def _bar(frac, n=18):
64
- f = max(0.0, min(1.0, frac))
65
- return "█" * round(f * n) + "·" * (n - round(f * n))
66
-
67
-
68
- def moe_run(query, max_new):
69
- yield _loading_notice("Routing & generating")
70
- moe = _get_moe()(DEVICE)
71
- if not moe.available():
72
- if _SPIKEWHALE:
73
- yield ("### ⏳ No SpikeWhale experts found\nSet `MODMIND_DIR` to your ModMind folder "
74
- "and make sure `<domain>/checkpoints/step_*.pt` exist (the panel hot-reloads them).")
75
- else:
76
- yield ("### ⏳ No experts trained yet\nRun `python agents/train.py --expert language` "
77
- "(and `math`, `tool`).")
78
- return
79
- q = (query or "").strip() or "The"
80
- r = moe.run(q, max_new=int(max_new))
81
- _WARMED["done"] = True
82
- w = r["weights"]; bits = r["bits_per_byte"]; steps = r["steps"]
83
- rows = "\n".join(
84
- f"| {EMOJI.get(n, n)} | {steps.get(n,0):,} | {bits[n]:.2f} | `{_bar(w[n])}` {w[n]*100:4.1f}% |"
85
- + (" ⬅ **routed**" if n == r["winner"] else "")
86
- for n in w
87
- )
88
- lat = r["shared_latent"][:16]
89
- spark = "".join("▁▂▃▄▅▆▇█"[min(7, int((abs(v)) / (max(1e-3, max(abs(x) for x in lat))) * 7))] for v in lat)
90
- yield f"""### Routed to {EMOJI.get(r['winner'], r['winner'])}
91
- The expert with the **lowest bits/byte** (most fluent on your text) wins the route.
92
-
93
- | expert | train steps | bits/byte | routing weight |
94
- |---|---|---|---|
95
- {rows}
96
-
97
- **{EMOJI.get(r['winner'], r['winner'])} continues your prompt:**
98
- > {q}**{r['generation']}**
99
-
100
- **Fused latent** (both experts' output latents combined — a glimpse of the shared bus): `{spark}`
101
-
102
- <sub>{_FOOTER}</sub>"""
103
-
104
-
105
- def moe_key_recall(n):
106
- """THE PROOF: a random key shown only to the consultant; the asker reproduces it from the
107
- latent alone (with) vs ablated (without)."""
108
- yield _loading_notice("Running the proof")
109
- moe = _get_moe()(DEVICE)
110
- if not getattr(moe, "key_recall_available", lambda: False)():
111
- yield ("### 🔑 Bridge unavailable\nNeeds the **SpikeWhale** backend and a trained "
112
- "`links/<asker>__from__<consultant>.pt` saved with the full asker.")
113
- return
114
- meta = moe.consult_meta()
115
- a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
116
- wr = moe.key_recall(n=int(n), ablate=False)
117
- ar = moe.key_recall(n=int(n), ablate=True)
118
- _WARMED["done"] = True
119
- rows = "\n".join(f"| `{k}` | `{rec}` | {'✅' if ok else '❌'} |" for k, rec, ok in wr["examples"])
120
- return_ok = wr['acc'] * 100
121
- return_no = ar['acc'] * 100
122
- yield f"""### 🔑 {a} read {c}'s mind — {return_ok:.0f}% with the latent, {return_no:.0f}% without
123
-
124
- A random key is shown **only to {c}**. {a} never sees it — yet by reading {c}'s latent through the
125
- trained RecursiveLink, it reproduces the key:
126
-
127
- | secret key (only {c} saw it) | {a} recovered it | |
128
- |---|---|---|
129
- {rows}
130
-
131
- ## ✅ With the latent: {return_ok:.0f}% → ❌ Cut the latent: {return_no:.0f}%
132
-
133
- <sub>That collapse to chance when the latent is removed is the whole point: the information is genuinely
134
- crossing the latent bridge between two models that were trained **separately, on different data**.
135
- This is the result — routing and generation are the supporting act.</sub>"""
136
-
137
-
138
- def moe_consult(query, max_new):
139
- """Latent-influence demo: inject the consultant's latent into the asker (WITH vs WITHOUT).
140
- The bridge was trained on key-recall, so the latent *steers* the output rather than answering."""
141
- yield _loading_notice("Injecting latent")
142
- moe = _get_moe()(DEVICE)
143
- if not getattr(moe, "consult_available", lambda: False)():
144
- yield ("### 🔗 Latent bridge unavailable\nNeeds the **SpikeWhale** backend and a trained bridge.")
145
- return
146
- q = (query or "").strip() or "natural selection"
147
- meta = moe.consult_meta()
148
- a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
149
- with_gen = moe.consult(q, max_new=int(max_new), ablate=False)
150
- abl_gen = moe.consult(q, max_new=int(max_new), ablate=True)
151
- _WARMED["done"] = True
152
- wl, nl = meta["with_latent"], meta["without_latent"]
153
- yield f"""### 🔗 Latent influence — {c}'s latent injected into {a}
154
-
155
- **WITH {c}'s latent (through the trained RecursiveLink):**
156
- > {q}**{with_gen}**
157
-
158
- **WITHOUT it (latent ablated to zero):**
159
- > {q}**{abl_gen}**
160
-
161
- <sub>This shows the latent's raw *effect* on generation, not a Q&A answer. The bridge was trained on
162
- key-recall (ablation with={wl:.3f} vs without={nl:.4f}), so the injected latent pushes the asker toward
163
- that learned behavior — which is why WITH and WITHOUT differ so sharply. The clean proof the latent
164
- carries real information is the **Bridge** tab.</sub>"""
165
-
166
-
167
- def moe_combine(query, max_new, blend, consult):
168
- """Two blends compared at the same mix ratio: a real WEIGHT-MERGE (one merged model) vs an
169
- OUTPUT-BLEND (two models run separately, distributions averaged)."""
170
- yield _loading_notice("Building merge + blending")
171
- moe = _get_moe()(DEVICE)
172
- if not getattr(moe, "merge_available", lambda: False)():
173
- yield "### 🧬 Needs both specialists loaded."
174
- return
175
- q = (query or "").strip() or "The water cycle works by"
176
- a = float(blend)
177
- merged_gen = moe.merge_generate(q, alpha=a, max_new=int(max_new), consult=bool(consult))
178
- blend_gen = moe.combine(q, max_new=int(max_new), blend=a, consult=bool(consult))
179
- _WARMED["done"] = True
180
- mix = f"{int(round((1-a)*100))}% 📖 Language / {int(round(a*100))}% ➗ Math"
181
- extra = " · +Reasoning's latent (consult)" if consult else ""
182
- yield f"""### 🧬 MoE Modular Minds — two ways to blend · {mix}{extra}
183
-
184
- **① Weight merge** — a *single* model whose weights are `(1-α)·Language + α·Math` — one network, one forward pass:
185
- > {q}**{merged_gen}**
186
-
187
- **② Output blend** — both models run separately, their next-token distributions averaged each step:
188
- > {q}**{blend_gen}**
189
-
190
- <sub>Same mix ratio, two different mechanisms. **Weight merge** fuses the actual parameters into one model
191
- (only possible because they're the identical dense architecture); **output blend** is an inference-time
192
- ensemble of two separate models (only possible because they share the 16k tokenizer). Tick *consult* to also
193
- route Reasoning's latent into each through the trained bridge. Exploratory — generations are rough at this scale.</sub>"""
194
-
195
-
196
- HERO = """# 🧩 Modular Mind — two specialists that talk in latent space
197
- **Two ~80M models trained completely separately** — 📖 **Language** on FineWeb-Edu, ➗ **Math** on
198
- FineMath — that never saw each other's data. A coordinator **routes** your query to the right one,
199
- and a trained **RecursiveLink** lets them **communicate through latent space**: Language can read
200
- information straight out of Math's "mind." The **🔑 Bridge** tab proves it.
201
-
202
- > ℹ️ *These specialists were trained only to demonstrate a **verifiable result** — clean routing and a
203
- > provable latent-bridge ablation — **not** for production-quality output. The generated text is
204
- > intentionally rough at this scale; the mechanism is the point.*"""
205
-
206
- BRIDGE_INTRO = """### The proof: two independent models, one latent channel
207
- A random secret key is shown **only to ➗ Math**. 📖 Language never sees it — but by reading Math's
208
- latent through the trained RecursiveLink, it **reproduces the key**. Zero out the latent and it
209
- collapses to chance. That gap *is* the result: real information crossing between two models that
210
- were trained on different data and never met. **Hit the button.**"""
211
-
212
- INTRO_BYTE = """## 🧩 Experiment — Modular Mind as a Mixture of Experts
213
- Three tiny ~10M byte-level specialists (language, math, tool-use), each streamed-trained on its own
214
- dataset. A coordinator **routes** your query to whichever expert is most fluent (perplexity-based MoE)
215
- and fuses their latents through a **RecursiveLink**. Try a math problem vs. a sentence."""
216
-
217
-
218
- def _routing_block():
219
- with gr.Row():
220
- q = gr.Textbox(label="Your prompt", value="Solve for x: 2x + 3 = 11",
221
- scale=4, placeholder="a sentence or a math problem…")
222
- n = gr.Slider(40, 300, value=80, step=20, label="generate tokens", scale=1)
223
- btn = gr.Button("🧭 Route & generate", variant="primary")
224
- out = gr.Markdown()
225
- btn.click(moe_run, [q, n], out)
226
- gr.Examples(examples=[["The theory of evolution explains", 80],
227
- ["Compute the derivative of x^2 + 3x", 80],
228
- ["The history of the Roman Empire began", 80]],
229
- inputs=[q, n])
230
-
231
-
232
- def build_moe_panel():
233
- """Create the MoE demo components inside the current gr.Blocks context."""
234
- if not _SPIKEWHALE:
235
- with gr.Accordion("🧩 Experiment: Modular Mind = Mixture of Experts (3 specialists)", open=False):
236
- gr.Markdown(INTRO_BYTE)
237
- _routing_block()
238
- return
239
-
240
- with gr.Accordion("🧩 Modular Mind — independent specialists communicating in latent space", open=True):
241
- gr.Markdown(HERO)
242
- with gr.Tabs():
243
- # The headline result, FIRST.
244
- with gr.Tab("🔑 The latent bridge — the proof"):
245
- gr.Markdown(BRIDGE_INTRO)
246
- with gr.Row():
247
- kn = gr.Slider(4, 16, value=8, step=1, label="keys to test", scale=3)
248
- kbtn = gr.Button("🔑 Run the proof", variant="primary", scale=1)
249
- kout = gr.Markdown()
250
- kbtn.click(moe_key_recall, [kn], kout)
251
-
252
- # Routing — the supporting act.
253
- with gr.Tab("🧭 Routing & generation"):
254
- gr.Markdown("Type a math problem vs. a sentence and watch the **route flip** — each "
255
- "expert is most fluent (lowest bits/byte) on its own domain.")
256
- _routing_block()
257
-
258
- # Latent influence — honest "with vs without".
259
- with gr.Tab("🔗 Latent influence"):
260
- gr.Markdown(
261
- "Inject ➗ Math's latent into 📖 Language through the **trained RecursiveLink** and "
262
- "see generation **with vs without** it. The bridge was trained on key-recall, so the "
263
- "latent *steers* the output (it doesn't answer the question) — the point is how "
264
- "strongly it changes generation. The clean proof is the **Bridge** tab.")
265
- with gr.Row():
266
- cq = gr.Textbox(label="Text to encode (Math's latent → injected into Language)",
267
- value="natural selection", scale=4)
268
- cn = gr.Slider(40, 200, value=80, step=20, label="generate tokens", scale=1)
269
- cbtn = gr.Button("🔗 Show latent influence (with vs without)", variant="secondary")
270
- cout = gr.Markdown()
271
- cbtn.click(moe_consult, [cq, cn], cout)
272
-
273
- # MoE Modular Minds — TWO ways to blend the specialists, compared side by side.
274
- with gr.Tab("🧬 MoE Modular Minds"):
275
- gr.Markdown(
276
- "**Two ways to blend the two specialists**, shown side by side at the same mix ratio:\n"
277
- "- **① Weight merge** — fuse the *parameters* into one model `(1-α)·Language + α·Math` "
278
- "(works because they're the identical dense architecture).\n"
279
- "- **② Output blend** — run both models separately and average their next-token "
280
- "distributions (works because they share the 16k tokenizer).\n\n"
281
- "Slide the mix, and tick *consult* to also route Reasoning's latent into each through the "
282
- "trained bridge.")
283
- with gr.Row():
284
- mq = gr.Textbox(label="Prompt", value="The water cycle works by", scale=4)
285
- mn = gr.Slider(40, 160, value=70, step=10, label="generate tokens", scale=1)
286
- with gr.Row():
287
- mblend = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
288
- label="mix α: 0 = 📖 Language ⟷ 1 = ➗ Math", scale=3)
289
- mconsult = gr.Checkbox(value=False, label="consult (inject Reasoning's latent)", scale=1)
290
- mbtn = gr.Button("🧬 Blend both ways (weight-merge vs output-blend)", variant="primary")
291
- mout = gr.Markdown()
292
- mbtn.click(moe_combine, [mq, mn, mblend, mconsult], mout)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ panel.py -- the Gradio section for the bottom of the boss app: a live demo of the
3
+ Modular-Mind mixture-of-experts.
4
+
5
+ For the SpikeWhale backend it leads with the *latent bridge* (the real result) and
6
+ organizes the three demos into tabs. Every handler is a generator that yields an
7
+ instant "loading/generating" message first, so the first run never looks frozen while
8
+ the ~80M models lazy-load. Hot-reloads checkpoints.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import sys
14
+
15
+ import gradio as gr
16
+
17
+ # ZeroGPU: @spaces.GPU allocates a GPU only for the decorated call (CUDA is never touched at
18
+ # import/startup). Falls back to a no-op decorator when `spaces` isn't installed (local / plain CPU).
19
+ try:
20
+ import spaces
21
+ _gpu = spaces.GPU
22
+ except Exception:
23
+ def _gpu(fn=None, **kw):
24
+ return fn if callable(fn) else (lambda f: f)
25
+
26
+
27
+ def _to_gpu(moe):
28
+ if hasattr(moe, "to_gpu_if_available"):
29
+ moe.to_gpu_if_available()
30
+ return moe
31
+
32
+
33
+ EMOJI = {"language": "📖 Language", "math": "➗ Math", "tool": "🛠️ Tool-use"}
34
+ DEVICE = os.environ.get("MM_AGENTS_DEVICE", "cpu")
35
+ # Self-contained SpikeWhale bundle that ships next to this file (agents/modmind/: the 80M
36
+ # specialists + bridge + inference code). If it's present we default to the SpikeWhale backend
37
+ # so the HuggingFace Space "just works" with no env config. Env vars still override.
38
+ _BUNDLED_MODMIND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modmind")
39
+ _DEFAULT_BACKEND = "spikewhale" if os.path.isdir(_BUNDLED_MODMIND) else "bytegpt"
40
+ _SPIKEWHALE = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower() in ("spikewhale", "modmind")
41
+ _WARMED = {"done": False} # so the "loading the models" notice only shows on the first run
42
+
43
+ _FOOTER = (
44
+ "Two ~80M dense specialists — 📖 Language (FineWeb-Edu) and ➗ Math (FineMath) — sharing a "
45
+ "16k length-max tokenizer. A coordinator routes by bits-per-byte, and a trained RecursiveLink "
46
+ "lets them communicate in latent space (proven in the Bridge tab). Hot-reloads checkpoints."
47
+ if _SPIKEWHALE else
48
+ "Three byte-level ~10M specialists, streamed-trained on FineWeb-Edu / FineMath / "
49
+ "glaive-function-calling. Tiny + early-trained, so generations are rough — the routing "
50
+ "(which expert is most confident) is the point. It hot-reloads as training continues."
51
+ )
52
+
53
+
54
+ def _get_moe():
55
+ """Pick the MoE backend. Defaults to the bundled SpikeWhale 80M specialists
56
+ (agents/modmind/) when present, else the byte-level ByteGPT experts. MM_MOE_BACKEND
57
+ and MODMIND_DIR override."""
58
+ backend = os.environ.get("MM_MOE_BACKEND", _DEFAULT_BACKEND).lower()
59
+ if backend in ("spikewhale", "modmind"):
60
+ mm_dir = os.environ.get("MODMIND_DIR", _BUNDLED_MODMIND)
61
+ if mm_dir and mm_dir not in sys.path:
62
+ sys.path.insert(0, mm_dir) # front: ModMind's model.py wins over agents/model.py
63
+ from moe_gradio import get_moe
64
+ return get_moe
65
+ from orchestrator import get_moe
66
+ return get_moe
67
+
68
+
69
+ def _loading_notice(action="Generating"):
70
+ """First-run popup + in-place message so nothing ever looks frozen."""
71
+ if not _WARMED["done"]:
72
+ gr.Info("First run — loading the models (~20–40s on CPU). After this, it's quick.")
73
+ return (f"### ⏳ Loading the models + {action.lower()}…\n"
74
+ "*First run loads the ~80M specialists into memory — this can take ~20–40s on CPU. "
75
+ "Every run after this is fast.*")
76
+ return f"### ⏳ {action}…"
77
+
78
+
79
+ def _bar(frac, n=18):
80
+ f = max(0.0, min(1.0, frac))
81
+ return "█" * round(f * n) + "·" * (n - round(f * n))
82
+
83
+
84
+ @_gpu(duration=120)
85
+ def moe_run(query, max_new):
86
+ yield _loading_notice("Routing & generating")
87
+ moe = _to_gpu(_get_moe()(DEVICE))
88
+ if not moe.available():
89
+ if _SPIKEWHALE:
90
+ yield ("### ⏳ No SpikeWhale experts found\nSet `MODMIND_DIR` to your ModMind folder "
91
+ "and make sure `<domain>/checkpoints/step_*.pt` exist (the panel hot-reloads them).")
92
+ else:
93
+ yield ("### ⏳ No experts trained yet\nRun `python agents/train.py --expert language` "
94
+ "(and `math`, `tool`).")
95
+ return
96
+ q = (query or "").strip() or "The"
97
+ r = moe.run(q, max_new=int(max_new))
98
+ _WARMED["done"] = True
99
+ w = r["weights"]; bits = r["bits_per_byte"]; steps = r["steps"]
100
+ rows = "\n".join(
101
+ f"| {EMOJI.get(n, n)} | {steps.get(n,0):,} | {bits[n]:.2f} | `{_bar(w[n])}` {w[n]*100:4.1f}% |"
102
+ + (" ⬅ **routed**" if n == r["winner"] else "")
103
+ for n in w
104
+ )
105
+ lat = r["shared_latent"][:16]
106
+ spark = "".join("▁▂▃▄▅▆▇█"[min(7, int((abs(v)) / (max(1e-3, max(abs(x) for x in lat))) * 7))] for v in lat)
107
+ yield f"""### Routed to {EMOJI.get(r['winner'], r['winner'])}
108
+ The expert with the **lowest bits/byte** (most fluent on your text) wins the route.
109
+
110
+ | expert | train steps | bits/byte | routing weight |
111
+ |---|---|---|---|
112
+ {rows}
113
+
114
+ **{EMOJI.get(r['winner'], r['winner'])} continues your prompt:**
115
+ > {q}**{r['generation']}**
116
+
117
+ **Fused latent** (both experts' output latents combined — a glimpse of the shared bus): `{spark}`
118
+
119
+ <sub>{_FOOTER}</sub>"""
120
+
121
+
122
+ @_gpu(duration=120)
123
+ def moe_key_recall(n):
124
+ """THE PROOF: a random key shown only to the consultant; the asker reproduces it from the
125
+ latent alone (with) vs ablated (without)."""
126
+ yield _loading_notice("Running the proof")
127
+ moe = _to_gpu(_get_moe()(DEVICE))
128
+ if not getattr(moe, "key_recall_available", lambda: False)():
129
+ yield ("### 🔑 Bridge unavailable\nNeeds the **SpikeWhale** backend and a trained "
130
+ "`links/<asker>__from__<consultant>.pt` saved with the full asker.")
131
+ return
132
+ meta = moe.consult_meta()
133
+ a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
134
+ wr = moe.key_recall(n=int(n), ablate=False)
135
+ ar = moe.key_recall(n=int(n), ablate=True)
136
+ _WARMED["done"] = True
137
+ rows = "\n".join(f"| `{k}` | `{rec}` | {'✅' if ok else '❌'} |" for k, rec, ok in wr["examples"])
138
+ return_ok = wr['acc'] * 100
139
+ return_no = ar['acc'] * 100
140
+ yield f"""### 🔑 {a} read {c}'s mind — {return_ok:.0f}% with the latent, {return_no:.0f}% without
141
+
142
+ A random key is shown **only to {c}**. {a} never sees it — yet by reading {c}'s latent through the
143
+ trained RecursiveLink, it reproduces the key:
144
+
145
+ | secret key (only {c} saw it) | {a} recovered it | |
146
+ |---|---|---|
147
+ {rows}
148
+
149
+ ## ✅ With the latent: {return_ok:.0f}% → ❌ Cut the latent: {return_no:.0f}%
150
+
151
+ <sub>That collapse to chance when the latent is removed is the whole point: the information is genuinely
152
+ crossing the latent bridge between two models that were trained **separately, on different data**.
153
+ This is the result — routing and generation are the supporting act.</sub>"""
154
+
155
+
156
+ @_gpu(duration=120)
157
+ def moe_consult(query, max_new):
158
+ """Latent-influence demo: inject the consultant's latent into the asker (WITH vs WITHOUT).
159
+ The bridge was trained on key-recall, so the latent *steers* the output rather than answering."""
160
+ yield _loading_notice("Injecting latent")
161
+ moe = _to_gpu(_get_moe()(DEVICE))
162
+ if not getattr(moe, "consult_available", lambda: False)():
163
+ yield ("### 🔗 Latent bridge unavailable\nNeeds the **SpikeWhale** backend and a trained bridge.")
164
+ return
165
+ q = (query or "").strip() or "natural selection"
166
+ meta = moe.consult_meta()
167
+ a = EMOJI.get(meta["asker"], meta["asker"]); c = EMOJI.get(meta["consultant"], meta["consultant"])
168
+ with_gen = moe.consult(q, max_new=int(max_new), ablate=False)
169
+ abl_gen = moe.consult(q, max_new=int(max_new), ablate=True)
170
+ _WARMED["done"] = True
171
+ wl, nl = meta["with_latent"], meta["without_latent"]
172
+ yield f"""### 🔗 Latent influence — {c}'s latent injected into {a}
173
+
174
+ **WITH {c}'s latent (through the trained RecursiveLink):**
175
+ > {q}**{with_gen}**
176
+
177
+ **WITHOUT it (latent ablated to zero):**
178
+ > {q}**{abl_gen}**
179
+
180
+ <sub>This shows the latent's raw *effect* on generation, not a Q&A answer. The bridge was trained on
181
+ key-recall (ablation with={wl:.3f} vs without={nl:.4f}), so the injected latent pushes the asker toward
182
+ that learned behavior — which is why WITH and WITHOUT differ so sharply. The clean proof the latent
183
+ carries real information is the **Bridge** tab.</sub>"""
184
+
185
+
186
+ @_gpu(duration=120)
187
+ def moe_combine(query, max_new, blend, consult):
188
+ """Two blends compared at the same mix ratio: a real WEIGHT-MERGE (one merged model) vs an
189
+ OUTPUT-BLEND (two models run separately, distributions averaged)."""
190
+ yield _loading_notice("Building merge + blending")
191
+ moe = _to_gpu(_get_moe()(DEVICE))
192
+ if not getattr(moe, "merge_available", lambda: False)():
193
+ yield "### 🧬 Needs both specialists loaded."
194
+ return
195
+ q = (query or "").strip() or "The water cycle works by"
196
+ a = float(blend)
197
+ merged_gen = moe.merge_generate(q, alpha=a, max_new=int(max_new), consult=bool(consult))
198
+ blend_gen = moe.combine(q, max_new=int(max_new), blend=a, consult=bool(consult))
199
+ _WARMED["done"] = True
200
+ mix = f"{int(round((1-a)*100))}% 📖 Language / {int(round(a*100))}% ➗ Math"
201
+ extra = " · +Reasoning's latent (consult)" if consult else ""
202
+ yield f"""### 🧬 MoE Modular Minds — two ways to blend · {mix}{extra}
203
+
204
+ **① Weight merge** — a *single* model whose weights are `(1-α)·Language + α·Math` — one network, one forward pass:
205
+ > {q}**{merged_gen}**
206
+
207
+ **② Output blend** — both models run separately, their next-token distributions averaged each step:
208
+ > {q}**{blend_gen}**
209
+
210
+ <sub>Same mix ratio, two different mechanisms. **Weight merge** fuses the actual parameters into one model
211
+ (only possible because they're the identical dense architecture); **output blend** is an inference-time
212
+ ensemble of two separate models (only possible because they share the 16k tokenizer). Tick *consult* to also
213
+ route Reasoning's latent into each through the trained bridge. Exploratory — generations are rough at this scale.</sub>"""
214
+
215
+
216
+ HERO = """# 🧩 Modular Mind — two specialists that talk in latent space
217
+ **Two ~80M models trained completely separately** — 📖 **Language** on FineWeb-Edu, ➗ **Math** on
218
+ FineMath — that never saw each other's data. A coordinator **routes** your query to the right one,
219
+ and a trained **RecursiveLink** lets them **communicate through latent space**: Language can read
220
+ information straight out of Math's "mind." The **🔑 Bridge** tab proves it.
221
+
222
+ > ℹ️ *These specialists were trained only to demonstrate a **verifiable result** — clean routing and a
223
+ > provable latent-bridge ablation — **not** for production-quality output. The generated text is
224
+ > intentionally rough at this scale; the mechanism is the point.*"""
225
+
226
+ BRIDGE_INTRO = """### The proof: two independent models, one latent channel
227
+ A random secret key is shown **only to ➗ Math**. 📖 Language never sees it — but by reading Math's
228
+ latent through the trained RecursiveLink, it **reproduces the key**. Zero out the latent and it
229
+ collapses to chance. That gap *is* the result: real information crossing between two models that
230
+ were trained on different data and never met. **Hit the button.**"""
231
+
232
+ INTRO_BYTE = """## 🧩 Experiment — Modular Mind as a Mixture of Experts
233
+ Three tiny ~10M byte-level specialists (language, math, tool-use), each streamed-trained on its own
234
+ dataset. A coordinator **routes** your query to whichever expert is most fluent (perplexity-based MoE)
235
+ and fuses their latents through a **RecursiveLink**. Try a math problem vs. a sentence."""
236
+
237
+
238
+ def _routing_block():
239
+ with gr.Row():
240
+ q = gr.Textbox(label="Your prompt", value="Solve for x: 2x + 3 = 11",
241
+ scale=4, placeholder="a sentence or a math problem…")
242
+ n = gr.Slider(40, 300, value=80, step=20, label="generate tokens", scale=1)
243
+ btn = gr.Button("🧭 Route & generate", variant="primary")
244
+ out = gr.Markdown()
245
+ btn.click(moe_run, [q, n], out)
246
+ gr.Examples(examples=[["The theory of evolution explains", 80],
247
+ ["Compute the derivative of x^2 + 3x", 80],
248
+ ["The history of the Roman Empire began", 80]],
249
+ inputs=[q, n])
250
+
251
+
252
+ def build_moe_panel():
253
+ """Create the MoE demo components inside the current gr.Blocks context."""
254
+ if not _SPIKEWHALE:
255
+ with gr.Accordion("🧩 Experiment: Modular Mind = Mixture of Experts (3 specialists)", open=False):
256
+ gr.Markdown(INTRO_BYTE)
257
+ _routing_block()
258
+ return
259
+
260
+ with gr.Accordion("🧩 Modular Mind — independent specialists communicating in latent space", open=True):
261
+ gr.Markdown(HERO)
262
+ with gr.Tabs():
263
+ # The headline result, FIRST.
264
+ with gr.Tab("🔑 The latent bridge — the proof"):
265
+ gr.Markdown(BRIDGE_INTRO)
266
+ with gr.Row():
267
+ kn = gr.Slider(4, 16, value=8, step=1, label="keys to test", scale=3)
268
+ kbtn = gr.Button("🔑 Run the proof", variant="primary", scale=1)
269
+ kout = gr.Markdown()
270
+ kbtn.click(moe_key_recall, [kn], kout)
271
+
272
+ # Routing — the supporting act.
273
+ with gr.Tab("🧭 Routing & generation"):
274
+ gr.Markdown("Type a math problem vs. a sentence and watch the **route flip** — each "
275
+ "expert is most fluent (lowest bits/byte) on its own domain.")
276
+ _routing_block()
277
+
278
+ # Latent influence — honest "with vs without".
279
+ with gr.Tab("🔗 Latent influence"):
280
+ gr.Markdown(
281
+ "Inject ➗ Math's latent into 📖 Language through the **trained RecursiveLink** and "
282
+ "see generation **with vs without** it. The bridge was trained on key-recall, so the "
283
+ "latent *steers* the output (it doesn't answer the question) — the point is how "
284
+ "strongly it changes generation. The clean proof is the **Bridge** tab.")
285
+ with gr.Row():
286
+ cq = gr.Textbox(label="Text to encode (Math's latent → injected into Language)",
287
+ value="natural selection", scale=4)
288
+ cn = gr.Slider(40, 200, value=80, step=20, label="generate tokens", scale=1)
289
+ cbtn = gr.Button("🔗 Show latent influence (with vs without)", variant="secondary")
290
+ cout = gr.Markdown()
291
+ cbtn.click(moe_consult, [cq, cn], cout)
292
+
293
+ # MoE Modular Minds — TWO ways to blend the specialists, compared side by side.
294
+ with gr.Tab("🧬 MoE Modular Minds"):
295
+ gr.Markdown(
296
+ "**Two ways to blend the two specialists**, shown side by side at the same mix ratio:\n"
297
+ "- **① Weight merge** — fuse the *parameters* into one model `(1-α)·Language + α·Math` "
298
+ "(works because they're the identical dense architecture).\n"
299
+ "- **② Output blend** — run both models separately and average their next-token "
300
+ "distributions (works because they share the 16k tokenizer).\n\n"
301
+ "Slide the mix, and tick *consult* to also route Reasoning's latent into each through the "
302
+ "trained bridge.")
303
+ with gr.Row():
304
+ mq = gr.Textbox(label="Prompt", value="The water cycle works by", scale=4)
305
+ mn = gr.Slider(40, 160, value=70, step=10, label="generate tokens", scale=1)
306
+ with gr.Row():
307
+ mblend = gr.Slider(0.0, 1.0, value=0.5, step=0.1,
308
+ label="mix α: 0 = 📖 Language ⟷ 1 = ➗ Math", scale=3)
309
+ mconsult = gr.Checkbox(value=False, label="consult (inject Reasoning's latent)", scale=1)
310
+ mbtn = gr.Button("🧬 Blend both ways (weight-merge vs output-blend)", variant="primary")
311
+ mout = gr.Markdown()
312
+ mbtn.click(moe_combine, [mq, mn, mblend, mconsult], mout)
piano/__pycache__/piano_mind.cpython-313.pyc ADDED
Binary file (14.6 kB). View file
 
piano/__pycache__/poly_mind.cpython-313.pyc ADDED
Binary file (15.7 kB). View file
 
piano/poly_mind.py CHANGED
@@ -111,12 +111,23 @@ def train_and_save(notes=os.path.join(HERE, "poly_notes.json"),
111
 
112
 
113
  class PolyPlayer:
114
- def __init__(self, weights=os.path.join(HERE, "poly_weights.pt")):
115
- ck = torch.load(weights, map_location="cpu")
116
- self.n, self.K, self.fps = ck["n_tokens"], ck["K"], ck["fps"]
117
- self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
118
- self.seed = ck["seed"]
119
- self.model = PolyMind(self.n); self.model.load_state_dict(ck["state"]); self.model.eval()
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  @torch.no_grad()
122
  def next_frame(self, history, thresh=0.45, maxn=4):
 
111
 
112
 
113
  class PolyPlayer:
114
+ def __init__(self, weights=None):
115
+ sft = os.path.join(HERE, "poly_weights.safetensors")
116
+ if weights is None and os.path.exists(sft):
117
+ from safetensors.torch import load_file
118
+ from safetensors import safe_open
119
+ state = load_file(sft)
120
+ with safe_open(sft, framework="pt") as f:
121
+ md = f.metadata() or {}
122
+ self.n = int(md["n_tokens"]); self.K = int(md["K"]); self.fps = int(md["fps"])
123
+ self.tok2midi = {int(k): int(v) for k, v in json.loads(md["tok2midi"]).items()}
124
+ self.seed = json.loads(md["seed"])
125
+ else:
126
+ ck = torch.load(weights or os.path.join(HERE, "poly_weights.pt"), map_location="cpu")
127
+ self.n, self.K, self.fps = ck["n_tokens"], ck["K"], ck["fps"]
128
+ self.tok2midi = {int(k): int(v) for k, v in ck["tok2midi"].items()}
129
+ self.seed = ck["seed"]; state = ck["state"]
130
+ self.model = PolyMind(self.n); self.model.load_state_dict(state); self.model.eval()
131
 
132
  @torch.no_grad()
133
  def next_frame(self, history, thresh=0.45, maxn=4):
piano/poly_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3dc98a525a7c2fee19b354f6128ee21d5c8dfa4089d8b4836982b274ac5ef41d
3
+ size 1169060
requirements.txt CHANGED
@@ -1,4 +1,6 @@
1
- gradio==6.16.0 # must match the version pinned by the HuggingFace Spaces base image; mismatched pin breaks the Space build
2
  numpy>=1.24
3
- torch>=2.2 # the bottom "Mixture of Experts" experiment (lazy-loaded)
4
  transformers>=4.40 # SpikeWhale config/model + SpikeTokenizer (agents/modmind/)
 
 
 
1
+ gradio==6.15.2
2
  numpy>=1.24
3
+ torch>=2.2 # the bottom "Mixture of Experts" experiment + the piano model
4
  transformers>=4.40 # SpikeWhale config/model + SpikeTokenizer (agents/modmind/)
5
+ safetensors>=0.4 # all model weights ship as slim fp16 .safetensors
6
+ spaces # ZeroGPU: @spaces.GPU decorator for the 80M MoE inference