Eclipse-Senpai commited on
Commit
309aac0
·
verified ·
1 Parent(s): b8cf586

min-spark-preview — Meiosis looped hybrid demo (ZeroGPU)

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Binaries live in the HF Space repo, not git (CLAUDE.md: no binaries).
2
+ assets/meiosis.safetensors
3
+ assets/tokenizer.json
README.md CHANGED
@@ -1,13 +1,54 @@
1
  ---
2
- title: Min Spark Preview
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
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: min-spark · Meiosis preview
3
+ emoji: 🧬
4
+ colorFrom: yellow
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: "5.50.0"
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
  pinned: false
11
+ license: apache-2.0
12
+ tags:
13
+ - text-generation
14
+ - language-model
15
+ - research
16
+ short_description: A 5.76M looped-hybrid LM — pick the effort and prompt it.
17
  ---
18
 
19
+ # min-spark · Meiosis preview
20
+
21
+ A research demo for **Meiosis** — PICO release 01, a sub-10M-parameter
22
+ decoder-only **looped-hybrid** language model trained from scratch on ~10B
23
+ tokens of filtered fineweb-edu + finemath.
24
+
25
+ The one mechanic: a single weight-shared body block runs **K times per token**.
26
+ This Space lets you set that effort (K = 2, 3, or 4) and prompt the model on a
27
+ free CPU — ~20 tokens/sec, sized for showing the mechanic, not for throughput.
28
+
29
+ ## K-split
30
+
31
+ The loop-count trade-off is real (measured on the lm-eval harness):
32
+
33
+ - **K = 2** — favors commonsense tasks (PIQA, HellaSwag)
34
+ - **K = 3** — favors grammar (BLiMP), the default
35
+ - **K = 4** — deeper grammar passes; diminishing returns on CPU
36
+
37
+ ## What's in here
38
+
39
+ - `assets/meiosis.safetensors` — the decay-p09 release candidate (23 MB, fp32)
40
+ - `assets/tokenizer.json` — byte-level BPE, vocab 4096 (ADR-0010)
41
+ - `assets/meiosis.py` — the model definition (torch-only, vendored)
42
+ - `loader.py` — model + tokenizer load, generation (mirrors PICO's `infer.py`)
43
+ - `app.py` — the Gradio interface
44
+
45
+ ## Provenance
46
+
47
+ The preview squeeze gate (2026-07-27) tested whether weight-space averaging of
48
+ late trunk pins (SWA / EMA / Model Stock over p05–p09 + decay-p09) beat the
49
+ final decay-p09 checkpoint on reserved-val NLL. It did **not** — averaging across
50
+ mixed WSD phases regressed val perplexity — so the Space ships decay-p09 as-is.
51
+ See `PICO/results/post_train/preview_winner.json`.
52
+
53
+ PICO is a monthly series of cheap, fully-trained-and-evaluated small models.
54
+ Source: [eclipse-senpai/PICO](https://github.com/eclipse-senpai/PICO).
__pycache__/app.cpython-311.pyc ADDED
Binary file (14.4 kB). View file
 
__pycache__/loader.cpython-311.pyc ADDED
Binary file (4.48 kB). View file
 
app.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """min-spark-preview — a research demo for Meiosis, PICO release 01.
2
+
3
+ A 5.76M-parameter looped-hybrid language model. The signature mechanic: one
4
+ weight-shared body block runs `K` times per token (the "effort"). More passes
5
+ sharpen grammar; fewer favor commonsense. The Space makes the loop visible.
6
+
7
+ Runs on ZeroGPU (free for the creator; visitors consume their own quota). The
8
+ model is tiny so cold-start weight streaming is near-instant.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import spaces # MUST precede any torch / CUDA-touching import (ZeroGPU hijack)
13
+ import torch
14
+ import gradio as gr
15
+
16
+ from loader import load_model, load_tokenizer, generate
17
+
18
+ # ── load at module scope, .to("cuda") eagerly so the hijack packs weights ────
19
+ # "cuda" as a STRING (never an int device id) — ZeroGPU re-allocs device ids.
20
+ print("Loading Meiosis on ZeroGPU ...")
21
+ DEVICE = "cuda"
22
+ MODEL = load_model(DEVICE)
23
+ TOKENIZER = load_tokenizer()
24
+ _PARAMS = sum(p.numel() for p in MODEL.parameters())
25
+ print(f" {round(_PARAMS/1e6, 2)}M params ready on {DEVICE}")
26
+
27
+ EFFORTS = [
28
+ {"id": "low", "k": 2, "name": "Low", "tag": "commonsense"},
29
+ {"id": "med", "k": 3, "name": "Medium", "tag": "grammar", "default": True},
30
+ {"id": "high", "k": 4, "name": "High", "tag": "deeper grammar"},
31
+ ]
32
+ EFFORT_K = {e["id"]: e["k"] for e in EFFORTS}
33
+ _MAX_K = max(e["k"] for e in EFFORTS)
34
+
35
+
36
+ # ── the effort rail (signature) ─────────────────────────────────────────────
37
+ def effort_html(selected_id: str = "med") -> str:
38
+ cards = []
39
+ for e in EFFORTS:
40
+ sel = "selected" if e["id"] == selected_id else ""
41
+ dots = "".join(
42
+ f'<span class="dot {"lit" if i < e["k"] else ""}"></span>'
43
+ for i in range(_MAX_K)
44
+ )
45
+ cards.append(f"""
46
+ <button class="effort-card {sel}" data-id="{e['id']}" type="button">
47
+ <span class="eff-label">{e['name']}</span>
48
+ <span class="eff-k">K={e['k']}</span>
49
+ <span class="eff-dots">{dots}</span>
50
+ <span class="eff-tag">{e['tag']}</span>
51
+ </button>""")
52
+ return f'<div id="effort-rail" max-k="{_MAX_K}">{"".join(cards)}</div>'
53
+
54
+
55
+ # On load: attach click listeners to the cards; track selection in window.__effort.
56
+ ATTACH_JS = """
57
+ () => {
58
+ window.__effort = "med";
59
+ const rail = document.getElementById('effort-rail');
60
+ if (!rail) return;
61
+ rail.querySelectorAll('.effort-card').forEach(btn => {
62
+ btn.addEventListener('click', () => {
63
+ rail.querySelectorAll('.effort-card').forEach(b => b.classList.remove('selected'));
64
+ btn.classList.add('selected');
65
+ window.__effort = btn.dataset.id;
66
+ });
67
+ });
68
+ }
69
+ """
70
+
71
+ # Generate-click preamble: override the effort slot with the JS-tracked selection.
72
+ READ_EFFORT_JS = """
73
+ (prompt, eff, mx, t, k) => [prompt, window.__effort || eff || "med", mx, t, k]
74
+ """
75
+
76
+
77
+ def _estimate_duration(prompt, effort, max_new, temperature, top_k):
78
+ # Tiny model on an RTX PRO 6000: cold-start weight stream (~1-2s) + ~0.5s
79
+ # per 128 tokens. Declare the realistic worst case; cap polite.
80
+ return min(60, 8 + int(max_new) * 0.1)
81
+
82
+
83
+ @spaces.GPU(duration=_estimate_duration)
84
+ def run_generate(prompt: str, effort: str, max_new: int,
85
+ temperature: float, top_k: int):
86
+ """Generate text from a prompt. `effort` sets the loop count K (2/3/4):
87
+ more passes sharpen grammar, fewer favor commonsense. Streams token-by-token."""
88
+ picked = EFFORT_K.get(effort or "med", 3)
89
+ if not str(prompt).strip():
90
+ yield ("<div class='outbox' id='specimen'></div>"
91
+ "<div class='status'><span class='empty'>Type a prompt to start.</span></div>")
92
+ return
93
+ outs = []
94
+ last_count = 0
95
+ last_tps = 0.0
96
+ for text, count, tps in generate(
97
+ MODEL, TOKENIZER, prompt, loops=picked, max_new=int(max_new),
98
+ temperature=float(temperature), top_k=int(top_k), device=DEVICE):
99
+ outs.append(text)
100
+ last_tps, last_count = tps, count
101
+ status = (f"<div class='status'>"
102
+ f"<span class='k-chip'>K={picked}</span>"
103
+ f"<span class='stat'>{last_count} tok</span>"
104
+ f"<span class='stat'>{last_tps:.1f} tok/s</span>"
105
+ f"<span class='stat'>ZeroGPU</span></div>")
106
+ yield f"<div class='outbox' id='specimen'>{''.join(outs)}</div>", status
107
+ status = (f"<div class='status'>"
108
+ f"<span class='k-chip'>K={picked}</span>"
109
+ f"<span class='stat'>{last_count} tok</span>"
110
+ f"<span class='stat'>{last_tps:.1f} tok/s</span>"
111
+ f"<span class='done'>done</span></div>")
112
+ yield f"<div class='outbox' id='specimen'>{''.join(outs)}</div>", status
113
+
114
+
115
+ CSS = """
116
+ @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
117
+
118
+ :root{
119
+ --bg:#13131A; --surface:#1C1C26; --surface2:#22222E;
120
+ --hair:#2A2A38; --text:#E8E6DF; --muted:#8A8A98;
121
+ --amber:#E8A547; --amber-dim:#8a6a2e; --teal:#4FB3A0;
122
+ --r:10px;
123
+ }
124
+ *{box-sizing:border-box;}
125
+ body, .gradio-container, .gradio-container * { background:var(--bg) !important; color:var(--text) !important; font-family:'Inter',system-ui,sans-serif !important; }
126
+ .gradio-container{ max-width:980px !important; padding:28px 20px 48px !important; }
127
+
128
+ .head{ display:flex; align-items:baseline; gap:16px; padding-bottom:6px; flex-wrap:wrap; }
129
+ .breed{ font-family:'Fraunces',serif; font-weight:300; font-size:30px; line-height:1; letter-spacing:-0.01em; color:var(--text); }
130
+ .breed em{ font-style:italic; font-weight:500; color:var(--amber); }
131
+ .kicker{ font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:0.22em; text-transform:uppercase; color:var(--muted); border-bottom:1px solid var(--hair); padding-bottom:8px; width:100%; margin-top:4px; }
132
+ .lede{ font-family:'Fraunces',serif; font-weight:300; font-size:15.5px; line-height:1.5; color:#b7b6ad; max-width:62ch; margin:14px 0 4px; }
133
+ .lede b{ font-weight:500; color:var(--teal); }
134
+
135
+ .panel-h{ font-family:'IBM Plex Mono',monospace; font-size:10.5px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted); margin:22px 0 10px; }
136
+ #effort-rail{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; }
137
+ .effort-card{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r); padding:12px 14px 12px; cursor:pointer; display:grid; grid-template-columns:auto 1fr auto; grid-template-rows:auto auto; align-items:center; gap:2px 10px; transition:border-color .15s, background .15s; text-align:left; }
138
+ .effort-card:hover{ border-color:#3a3a4a; }
139
+ .effort-card.selected{ background:var(--surface2); border-color:var(--amber); box-shadow:0 0 0 1px var(--amber) inset; }
140
+ .eff-label{ font-family:'Inter'; font-weight:600; font-size:14px; color:var(--text); }
141
+ .effort-card.selected .eff-label{ color:var(--amber); }
142
+ .eff-k{ grid-column:2; font-family:'IBM Plex Mono',monospace; font-size:12px; color:var(--muted); }
143
+ .effort-card.selected .eff-k{ color:#c9a05a; }
144
+ .eff-dots{ grid-column:1 / span 3; grid-row:2; display:flex; gap:4px; margin-top:6px; }
145
+ .dot{ width:7px; height:7px; border-radius:50%; background:#33333f; transition:background .15s, box-shadow .15s; }
146
+ .effort-card .dot.lit{ background:#4a4a3a; }
147
+ .effort-card.selected .dot.lit{ background:var(--amber); box-shadow:0 0 5px rgba(232,165,71,.55); }
148
+ .eff-tag{ grid-column:3; grid-row:1; font-family:'Inter'; font-size:10.5px; color:#6f6f7d; text-transform:lowercase; }
149
+ .effort-card.selected .eff-tag{ color:var(--amber-dim); }
150
+
151
+ textarea#prompt, .gradio-container textarea{ background:var(--surface) !important; border:1px solid var(--hair) !important; border-radius:var(--r) !important; color:var(--text) !important; font-family:'Fraunces',serif !important; font-weight:300 !important; font-size:16px !important; line-height:1.5 !important; }
152
+ textarea#prompt:focus{ border-color:var(--amber) !important; }
153
+ .label-wrap label, .gradio-container .form label{ color:#b7b6ad !important; font-size:11.5px !important; letter-spacing:0.04em !important; }
154
+
155
+ button#gen, button.primary{ background:var(--amber) !important; color:#1a1409 !important; border:none !important; border-radius:var(--r) !important; font-weight:600 !important; font-size:14px !important; }
156
+ button#gen:hover{ filter:brightness(1.08); }
157
+
158
+ .outbox{ background:var(--surface); border:1px solid var(--hair); border-radius:var(--r); padding:18px 20px; min-height:140px; font-family:'Fraunces',serif; font-weight:300; font-size:17px; line-height:1.55; color:var(--text); white-space:pre-wrap; word-break:break-word; }
159
+ .empty{ color:#55556a; font-style:italic; font-family:'Fraunces',serif; }
160
+
161
+ .status{ font-family:'IBM Plex Mono',monospace; font-size:11.5px; display:flex; gap:12px; align-items:center; margin-top:10px; flex-wrap:wrap; }
162
+ .stat{ color:var(--muted); }
163
+ .k-chip{ background:rgba(232,165,71,.12); color:var(--amber); border:1px solid rgba(232,165,71,.35); border-radius:5px; padding:1px 7px; font-weight:500; }
164
+ .done{ color:var(--teal); }
165
+
166
+ input[type=range]{ accent-color:var(--amber); }
167
+
168
+ .foot{ margin-top:28px; border-top:1px solid var(--hair); padding-top:12px; font-family:'IBM Plex Mono',monospace; font-size:10.5px; color:#5d5d6c; display:flex; justify-content:space-between; flex-wrap:wrap; gap:8px; }
169
+ .foot a{ color:var(--muted); text-decoration:none; border-bottom:1px dotted var(--hair); }
170
+ .foot a:hover{ color:var(--text); }
171
+ """
172
+
173
+ HEADER_HTML = """
174
+ <div class="head">
175
+ <div class="breed">min&nbsp;·spark <em>preview</em></div>
176
+ <div class="kicker">PICO release 01 · Meiosis · looped hybrid · 5.76M params · ZeroGPU</div>
177
+ </div>
178
+ <div class="lede">
179
+ A research demo of a small language model trained from scratch on ten billion tokens.
180
+ Its one mechanic: a single weight-shared block runs <b>K times per token</b> — that is the
181
+ &ldquo;effort.&rdquo; More passes sharpen grammar; fewer favor commonsense. Pick a pass count
182
+ and prompt it.
183
+ </div>
184
+ """
185
+
186
+ FOOTER_HTML = """
187
+ <div class="foot">
188
+ <span>ZeroGPU · generations stop early on &lt;eos&gt; · visitors use their own quota</span>
189
+ <span><a href="https://github.com/eclipse-senpai/PICO" target="_blank" rel="noopener">PICO on GitHub</a></span>
190
+ </div>
191
+ """
192
+
193
+ with gr.Blocks(css=CSS, title="min-spark · Meiosis preview") as demo:
194
+ gr.HTML(HEADER_HTML)
195
+
196
+ gr.HTML('<div class="panel-h">Effort — loops per token</div>')
197
+ effort_view = gr.HTML(effort_html("med"))
198
+
199
+ gr.HTML('<div class="panel-h">Prompt</div>')
200
+ prompt = gr.Textbox(value="", placeholder="Once upon a time, the very small model",
201
+ elem_id="prompt", lines=2, show_label=False)
202
+
203
+ with gr.Row():
204
+ max_new = gr.Slider(8, 256, value=128, step=8, label="Max tokens")
205
+ temperature = gr.Slider(0.1, 1.6, value=0.8, step=0.05, label="Temperature")
206
+ top_k = gr.Slider(0, 200, value=50, step=5, label="Top-k (0 = off)")
207
+
208
+ gen_btn = gr.Button("Generate", elem_id="gen", variant="primary")
209
+
210
+ out = gr.HTML(value='<div class="outbox" id="specimen"></div>')
211
+ status = gr.HTML(value='<div class="status"></div>')
212
+
213
+ gr.HTML(FOOTER_HTML)
214
+
215
+ effort_state = gr.State("med") # default; overridden by READ_EFFORT_JS preamble
216
+
217
+ demo.load(fn=None, js=ATTACH_JS)
218
+ gen_btn.click(
219
+ fn=run_generate,
220
+ js=READ_EFFORT_JS,
221
+ inputs=[prompt, effort_state, max_new, temperature, top_k],
222
+ outputs=[out, status],
223
+ )
224
+
225
+ if __name__ == "__main__":
226
+ demo.queue().launch(mcp_server=True)
assets/meiosis.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Meiosis: PICO release 1 (2026-07). Tied-embedding looped decoder-only LM.
2
+
3
+ Spec: research/2026-07-first-release/final-spec.md (approved 2026-07-02).
4
+ embed -> prelude x1 -> [body of `body_blocks` distinct blocks xK loops,
5
+ per-loop LoRA + loop embed, Deep Delta vdim1 residuals] -> coda x1
6
+ -> RMSNorm -> tied unembed. Attention is MHA by default, GQA when
7
+ `n_kv_heads` < `n_heads` (2026-07-05 overhaul knobs, ADR-0009).
8
+ """
9
+
10
+ import math
11
+ from dataclasses import dataclass
12
+
13
+ import torch
14
+ from torch import Tensor, nn
15
+ from torch.nn import functional
16
+
17
+ EMBED_STD = 0.02
18
+ LOOP_EMBED_STD = 0.02
19
+
20
+
21
+ @dataclass
22
+ class MeiosisConfig:
23
+ # defaults = release shape per ADR-0009 (B'-GQA overhaul, 2026-07-05):
24
+ # 3-block GQA body x3 loops, vocab 4096, ~5.76M total under the <6M cap
25
+ vocab_size: int = 4096
26
+ dim: int = 288
27
+ n_heads: int = 6
28
+ n_kv_heads: int | None = 2 # None -> MHA (= n_heads)
29
+ ffn_hidden: int = 768
30
+ prelude_layers: int = 1
31
+ coda_layers: int = 1
32
+ body_blocks: int = 3 # distinct blocks in the loop body
33
+ max_loops: int = 4
34
+ train_loops: int = 3
35
+ lora_rank: int = 16
36
+ rope_base: float = 10000.0
37
+ max_seq_len: int = 512
38
+ ddl_beta_init: float = 1.0
39
+ # rms_norm backward amplifies grads by 1/sqrt(eps_rms) when k_in ~ 0 — which is
40
+ # exactly the zero-init state. 1e-5 gave a 1.7e6x amplifier (1e5-magnitude grad
41
+ # spikes; > fp16 max at ANY loss scale — the 2026-07-06 fp16 divergence, ADR-0012).
42
+ # 1e-2 caps it at 1.7e3: fp16-safe, and identical bf16 training curves.
43
+ ddl_k_eps: float = 1e-2
44
+ ddl_v_sigmoid_scale: float = 4.0
45
+ # intra-document attention (ADR-0019): tokens attend only within their own
46
+ # EOS-delimited document. None = plain causal (pre-mask checkpoints).
47
+ doc_mask_eos: int | None = 2
48
+
49
+ @property
50
+ def head_dim(self) -> int:
51
+ return self.dim // self.n_heads
52
+
53
+ @property
54
+ def kv_heads(self) -> int:
55
+ return self.n_kv_heads if self.n_kv_heads is not None else self.n_heads
56
+
57
+ @property
58
+ def qkv_dim(self) -> int:
59
+ return self.dim + 2 * self.kv_heads * self.head_dim
60
+
61
+
62
+ def build_rope_cache(config: MeiosisConfig, length: int) -> tuple[Tensor, Tensor]:
63
+ positions = torch.arange(length, dtype=torch.float32)
64
+ inv_freq = 1.0 / (
65
+ config.rope_base
66
+ ** (torch.arange(0, config.head_dim, 2, dtype=torch.float32) / config.head_dim)
67
+ )
68
+ angles = torch.outer(positions, inv_freq)
69
+ return torch.cos(angles), torch.sin(angles)
70
+
71
+
72
+ def apply_rope(x: Tensor, cos: Tensor, sin: Tensor) -> Tensor:
73
+ x_even, x_odd = x[..., 0::2], x[..., 1::2]
74
+ rotated_even = x_even * cos - x_odd * sin
75
+ rotated_odd = x_even * sin + x_odd * cos
76
+ return torch.stack((rotated_even, rotated_odd), dim=-1).flatten(-2)
77
+
78
+
79
+ def build_doc_mask(tokens: Tensor, eos_id: int) -> Tensor:
80
+ """(B,T) tokens -> (B,1,T,T) bool, True where attention is allowed:
81
+ causal AND same document. Exclusive EOS scan, so an EOS token is the
82
+ last token of its document (FSX-1 convention)."""
83
+ is_eos = tokens == eos_id
84
+ doc_id = torch.cumsum(is_eos, dim=1) - is_eos.to(torch.long)
85
+ same = doc_id.unsqueeze(2) == doc_id.unsqueeze(1)
86
+ causal = torch.ones(
87
+ tokens.shape[1], tokens.shape[1], dtype=torch.bool, device=tokens.device
88
+ ).tril()
89
+ return (same & causal).unsqueeze(1)
90
+
91
+
92
+ class SwiGlu(nn.Module):
93
+ def __init__(self, dim: int, hidden: int) -> None:
94
+ super().__init__()
95
+ self.gate_up = nn.Linear(dim, 2 * hidden, bias=False)
96
+ self.down = nn.Linear(hidden, dim, bias=False)
97
+
98
+ def forward(self, x: Tensor) -> Tensor:
99
+ gate, up = self.gate_up(x).chunk(2, dim=-1)
100
+ return self.down(functional.silu(gate) * up)
101
+
102
+
103
+ class Attention(nn.Module):
104
+ """MHA, or GQA when kv_heads < n_heads (KV repeated to full head count)."""
105
+
106
+ def __init__(self, config: MeiosisConfig) -> None:
107
+ super().__init__()
108
+ self.n_heads = config.n_heads
109
+ self.kv_heads = config.kv_heads
110
+ self.head_dim = config.head_dim
111
+ self.qkv = nn.Linear(config.dim, config.qkv_dim, bias=False)
112
+ self.out = nn.Linear(config.dim, config.dim, bias=False)
113
+
114
+ def forward(
115
+ self,
116
+ x: Tensor,
117
+ cos: Tensor,
118
+ sin: Tensor,
119
+ qkv_delta: Tensor | None = None,
120
+ attn_mask: Tensor | None = None,
121
+ ) -> Tensor:
122
+ batch, seq_len, dim = x.shape
123
+ kv_dim = self.kv_heads * self.head_dim
124
+ qkv = self.qkv(x)
125
+ if qkv_delta is not None:
126
+ qkv = qkv + qkv_delta
127
+ q, k, v = qkv.split([dim, kv_dim, kv_dim], dim=-1)
128
+ q = q.view(batch, seq_len, self.n_heads, self.head_dim).transpose(1, 2)
129
+ k = k.view(batch, seq_len, self.kv_heads, self.head_dim).transpose(1, 2)
130
+ v = v.view(batch, seq_len, self.kv_heads, self.head_dim).transpose(1, 2)
131
+ q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
132
+ if self.kv_heads != self.n_heads:
133
+ k = k.repeat_interleave(self.n_heads // self.kv_heads, dim=1)
134
+ v = v.repeat_interleave(self.n_heads // self.kv_heads, dim=1)
135
+ if attn_mask is not None:
136
+ attended = functional.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
137
+ else:
138
+ attended = functional.scaled_dot_product_attention(q, k, v, is_causal=True)
139
+ return self.out(attended.transpose(1, 2).reshape(batch, seq_len, dim))
140
+
141
+
142
+ class DeepDeltaResidual(nn.Module):
143
+ """DDL vdim1 (arXiv 2601.00417): x <- x + beta * (v - k^T x) * k.
144
+
145
+ k is the sublayer output (rms-normed), beta in [0,2] gates between
146
+ identity / projection / reflection, v is a learned scalar target.
147
+ Replaces the plain additive residual in the looped block only.
148
+ """
149
+
150
+ def __init__(self, config: MeiosisConfig) -> None:
151
+ super().__init__()
152
+ self.k_eps = config.ddl_k_eps
153
+ self.v_sigmoid_scale = config.ddl_v_sigmoid_scale
154
+ self.beta = nn.Linear(config.dim, 1, bias=True)
155
+ self.v_proj = nn.Linear(config.dim, 1, bias=True)
156
+ beta_p = min(max(config.ddl_beta_init, 0.0), 2.0) / 2.0
157
+ beta_p = min(max(beta_p, 1e-6), 1.0 - 1e-6)
158
+ with torch.no_grad():
159
+ self.beta.bias.fill_(math.log(beta_p) - math.log(1.0 - beta_p))
160
+
161
+ def forward(self, x: Tensor, *, k_in: Tensor, context: Tensor) -> Tensor:
162
+ k_dim = k_in.size(-1)
163
+ eps_rms = (self.k_eps * self.k_eps) / k_dim
164
+ k_rms = functional.rms_norm(k_in, [k_dim], eps=eps_rms)
165
+ k_scale = 1.0 / math.sqrt(k_dim)
166
+ beta = 2.0 * torch.sigmoid(self.beta(context).float())
167
+ proj = torch.sum(k_rms * x, dim=-1, keepdim=True, dtype=torch.float32) * k_scale
168
+ v = torch.sigmoid(self.v_proj(x).float()) * self.v_sigmoid_scale
169
+ delta = ((beta * (v - proj)) * k_scale).to(dtype=x.dtype)
170
+ return x + delta * k_rms
171
+
172
+
173
+ class Block(nn.Module):
174
+ """Pre-norm block with plain additive residuals (prelude/coda)."""
175
+
176
+ def __init__(self, config: MeiosisConfig) -> None:
177
+ super().__init__()
178
+ self.attn_norm = nn.RMSNorm(config.dim)
179
+ self.attn = Attention(config)
180
+ self.ffn_norm = nn.RMSNorm(config.dim)
181
+ self.ffn = SwiGlu(config.dim, config.ffn_hidden)
182
+
183
+ def forward(
184
+ self, x: Tensor, cos: Tensor, sin: Tensor, attn_mask: Tensor | None = None
185
+ ) -> Tensor:
186
+ x = x + self.attn(self.attn_norm(x), cos, sin, attn_mask=attn_mask)
187
+ return x + self.ffn(self.ffn_norm(x))
188
+
189
+
190
+ class LoopedBlock(nn.Module):
191
+ """Shared block with Deep Delta residuals; run K times with per-loop LoRA."""
192
+
193
+ def __init__(self, config: MeiosisConfig) -> None:
194
+ super().__init__()
195
+ self.attn_norm = nn.RMSNorm(config.dim)
196
+ self.attn = Attention(config)
197
+ self.ddl_attn = DeepDeltaResidual(config)
198
+ self.ffn_norm = nn.RMSNorm(config.dim)
199
+ self.ffn = SwiGlu(config.dim, config.ffn_hidden)
200
+ self.ddl_ffn = DeepDeltaResidual(config)
201
+
202
+ def forward(
203
+ self,
204
+ x: Tensor,
205
+ cos: Tensor,
206
+ sin: Tensor,
207
+ qkv_delta: Tensor,
208
+ loop_emb: Tensor,
209
+ attn_mask: Tensor | None = None,
210
+ ) -> Tensor:
211
+ # loop_emb conditions the sublayer inputs only — it is not carried in
212
+ # the residual stream, so the block stays exactly identity at init
213
+ # (zero-init out-projections -> k=0 -> DDL no-op) for any loop count.
214
+ x_norm = self.attn_norm(x + loop_emb)
215
+ x = self.ddl_attn(
216
+ x,
217
+ k_in=self.attn(x_norm, cos, sin, qkv_delta, attn_mask=attn_mask),
218
+ context=x_norm,
219
+ )
220
+ x_norm = self.ffn_norm(x + loop_emb)
221
+ return self.ddl_ffn(x, k_in=self.ffn(x_norm), context=x_norm)
222
+
223
+
224
+ class LoopLora(nn.Module):
225
+ def __init__(self, config: MeiosisConfig) -> None:
226
+ super().__init__()
227
+ self.down = nn.ModuleList(
228
+ nn.Linear(config.dim, config.lora_rank, bias=False)
229
+ for _ in range(config.max_loops)
230
+ )
231
+ self.up = nn.ModuleList(
232
+ nn.Linear(config.lora_rank, config.qkv_dim, bias=False)
233
+ for _ in range(config.max_loops)
234
+ )
235
+ for up in self.up:
236
+ nn.init.zeros_(up.weight)
237
+
238
+ def forward(self, x: Tensor, loop_index: int) -> Tensor:
239
+ clamped = min(loop_index, len(self.down) - 1)
240
+ return self.up[clamped](self.down[clamped](x))
241
+
242
+
243
+ class Meiosis(nn.Module):
244
+ def __init__(self, config: MeiosisConfig) -> None:
245
+ super().__init__()
246
+ self.config = config
247
+ self.embed = nn.Embedding(config.vocab_size, config.dim)
248
+ self.prelude = nn.ModuleList(Block(config) for _ in range(config.prelude_layers))
249
+ self.body = nn.ModuleList(LoopedBlock(config) for _ in range(config.body_blocks))
250
+ self.loop_lora = nn.ModuleList(LoopLora(config) for _ in range(config.body_blocks))
251
+ self.loop_embed = nn.Embedding(config.max_loops, config.dim)
252
+ self.coda = nn.ModuleList(Block(config) for _ in range(config.coda_layers))
253
+ self.final_norm = nn.RMSNorm(config.dim)
254
+ cos, sin = build_rope_cache(config, config.max_seq_len)
255
+ self.register_buffer("rope_cos", cos, persistent=False)
256
+ self.register_buffer("rope_sin", sin, persistent=False)
257
+ self.register_buffer("last_loop_rms", torch.zeros(config.max_loops), persistent=False)
258
+
259
+ def forward(
260
+ self,
261
+ tokens: Tensor,
262
+ loops: int | None = None,
263
+ return_hidden: bool = False,
264
+ collect_loop_rms: bool = False,
265
+ attn_mask: Tensor | None = None,
266
+ ) -> Tensor | tuple[Tensor, Tensor]:
267
+ loop_count = loops if loops is not None else self.config.train_loops
268
+ seq_len = tokens.shape[1]
269
+ if seq_len > self.config.max_seq_len:
270
+ raise ValueError(f"seq_len {seq_len} > max {self.config.max_seq_len}")
271
+ x = self.embed(tokens)
272
+ if attn_mask is None and self.config.doc_mask_eos is not None:
273
+ attn_mask = build_doc_mask(tokens, self.config.doc_mask_eos)
274
+ device_type = tokens.device.type
275
+ compute_dtype = (
276
+ torch.get_autocast_dtype(device_type)
277
+ if torch.is_autocast_enabled(device_type)
278
+ else x.dtype
279
+ )
280
+ cos = self.rope_cos[:seq_len].to(compute_dtype)
281
+ sin = self.rope_sin[:seq_len].to(compute_dtype)
282
+ for block in self.prelude:
283
+ x = block(x, cos, sin, attn_mask=attn_mask)
284
+ rms_per_loop = []
285
+ for i in range(loop_count):
286
+ clamped = min(i, self.config.max_loops - 1)
287
+ loop_emb = self.loop_embed.weight[clamped]
288
+ for block, lora in zip(self.body, self.loop_lora):
289
+ x = block(x, cos, sin, lora(x + loop_emb, i), loop_emb, attn_mask=attn_mask)
290
+ rms = x.float().pow(2).mean().sqrt()
291
+ self.last_loop_rms[clamped] = rms.detach()
292
+ if collect_loop_rms:
293
+ rms_per_loop.append(rms)
294
+ for block in self.coda:
295
+ x = block(x, cos, sin, attn_mask=attn_mask)
296
+ x = self.final_norm(x)
297
+ out = x if return_hidden else functional.linear(x, self.embed.weight)
298
+ if collect_loop_rms:
299
+ return out, torch.stack(rms_per_loop)
300
+ return out
301
+
302
+
303
+ def init_meiosis(model: Meiosis) -> None:
304
+ """Mandatory MythosMini-validated init. Never mu-center the tied embedding."""
305
+ with torch.no_grad():
306
+ model.embed.weight.normal_(mean=0.0, std=EMBED_STD)
307
+ model.loop_embed.weight.normal_(mean=0.0, std=LOOP_EMBED_STD)
308
+ for block in [*model.prelude, *model.body, *model.coda]:
309
+ nn.init.zeros_(block.attn.out.weight)
310
+ nn.init.zeros_(block.ffn.down.weight)
311
+
312
+
313
+ def count_parameters(model: nn.Module) -> int:
314
+ return sum(p.numel() for p in model.parameters())
315
+
316
+
317
+ def muon_param_split(model: Meiosis) -> tuple[list[nn.Parameter], list[nn.Parameter]]:
318
+ """Explicit Muon/aux split (ADR-0005). Muon gets the block and LoRA
319
+ matrices; the tied embedding, loop embeddings, norm gains, and 1-row DDL
320
+ heads stay on NAdamW. Listed explicitly - no shape heuristics, so a
321
+ rank-8 pilot LoRA cannot silently fall out of the Muon group.
322
+ """
323
+ muon: list[nn.Parameter] = []
324
+ for block in [*model.prelude, *model.body, *model.coda]:
325
+ muon += [
326
+ block.attn.qkv.weight,
327
+ block.attn.out.weight,
328
+ block.ffn.gate_up.weight,
329
+ block.ffn.down.weight,
330
+ ]
331
+ for lora in model.loop_lora:
332
+ muon += [linear.weight for linear in [*lora.down, *lora.up]]
333
+ muon_ids = {id(p) for p in muon}
334
+ aux = [p for p in model.parameters() if id(p) not in muon_ids]
335
+ return muon, aux
loader.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Meiosis model loader + byte-BPE tokenizer for the min-spark-preview Space.
2
+
3
+ ZeroGPU: model loads at module scope with .to("cuda") (string, never an int) so
4
+ the `spaces` hijack packs weights to disk for the GPU worker. Generation runs
5
+ inside @spaces.GPU (decorated in app.py). Vendored paths so the Space has zero
6
+ dependency on the PICO repo layout.
7
+ """
8
+ from __future__ import annotations
9
+ from pathlib import Path
10
+ import torch
11
+
12
+ _ASSETS = Path(__file__).parent / "assets"
13
+ _TOK = _ASSETS / "tokenizer.json"
14
+ _CKPT = _ASSETS / "meiosis.safetensors"
15
+
16
+ # Keep `meiosis.py` importable: it is torch-only and self-contained here.
17
+ import sys
18
+ if str(_ASSETS) not in sys.path:
19
+ sys.path.insert(0, str(_ASSETS))
20
+
21
+ from meiosis import Meiosis, MeiosisConfig
22
+
23
+ # tokenizer.json is a raw HF `tokenizers` artifact (no PreTrainedTokenizerFast
24
+ # wrapper, per ADR-0010) — load it with the `tokenizers` library directly.
25
+ from tokenizers import Tokenizer
26
+
27
+ EOS_ID = 2 # PICO specials: <pad>=0, <bos>=1, <eos>=2 (ADR-0010)
28
+
29
+
30
+ def load_tokenizer():
31
+ return Tokenizer.from_file(str(_TOK))
32
+
33
+
34
+ def load_model(device: str = "cpu") -> Meiosis:
35
+ from safetensors.torch import load_file
36
+ model = Meiosis(MeiosisConfig())
37
+ state = load_file(str(_CKPT))
38
+ # strict=False: safetensors may lack non-persistent buffers (rope, loop_rms)
39
+ model.load_state_dict(state, strict=False)
40
+ model.to(device).eval()
41
+ return model
42
+
43
+
44
+ @torch.no_grad()
45
+ def generate(model, tokenizer, prompt: str, *, loops: int, max_new: int,
46
+ temperature: float, top_k: int, device: str):
47
+ """Token-by-token sampling. Yields (text_so_far, token_count, tok_per_s).
48
+ Mirrors infer.py: EOS prefix, top-k + temperature, stop on EOS. Runs on the
49
+ GPU worker when invoked under @spaces.GPU; returns only CPU-safe Python
50
+ objects (str/int/float) so nothing CUDA crosses the pickle boundary."""
51
+ import time
52
+ ids = [EOS_ID] + tokenizer.encode(prompt).ids
53
+ out_text = ""
54
+ t0 = None
55
+ count = 0
56
+ for _ in range(max_new):
57
+ ctx = ids[-model.config.max_seq_len:]
58
+ x = torch.tensor([ctx], device=device)
59
+ logits = model(x, loops=loops)
60
+ if t0 is None:
61
+ t0 = time.perf_counter()
62
+ next_logits = logits[0, -1] / max(temperature, 1e-6)
63
+ if top_k > 0:
64
+ topk_vals, _ = torch.topk(next_logits, min(top_k, next_logits.shape[-1]))
65
+ next_logits[next_logits < topk_vals[-1]] = float("-inf")
66
+ probs = torch.softmax(next_logits, dim=-1)
67
+ next_id = int(torch.multinomial(probs, 1).item())
68
+ if next_id == EOS_ID:
69
+ break
70
+ ids.append(next_id)
71
+ out_text += tokenizer.decode([next_id])
72
+ count += 1
73
+ elapsed = time.perf_counter() - t0
74
+ yield out_text, count, (count / elapsed if elapsed > 0 else 0.0)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # gradio, spaces, huggingface_hub are preinstalled by the Gradio SDK base image —
2
+ # do NOT list them (pinning breaks the runtime). torch is also preinstalled on
3
+ # ZeroGPU; leave it unpinned (the runtime pins one of 2.8/2.9.1/2.10/2.11).
4
+ safetensors>=0.4.1
5
+ tokenizers>=0.21