llaa33219 commited on
Commit
c047c1e
·
verified ·
1 Parent(s): 51549a9

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +49 -7
  2. app.py +316 -0
  3. requirements.txt +3 -0
  4. src/__init__.py +0 -0
  5. src/model.py +526 -0
  6. src/tokenizer.py +116 -0
README.md CHANGED
@@ -1,13 +1,55 @@
1
  ---
2
- title: MicroMixer 2
3
- emoji: 🏆
4
- colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.18.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: MicroMixer-2 Discord Demo
3
+ emoji: 🎛️
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 4.44.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ short_description: Play with the four attention-free MicroMixer-2 checkpoints.
12
  ---
13
 
14
+ # MicroMixer-2 Discord-dialogue Playground
15
+
16
+ Interactive Gradio demo for the four attention-free, MLP-only language
17
+ models in [`llaa33219/micromixer-2`](https://huggingface.co/collections/llaa33219/micromixer-2).
18
+
19
+ | Variant | Max context | Params |
20
+ | --- | --- | --- |
21
+ | 100K | 64 | ~125K |
22
+ | 300K | 128 | ~431K |
23
+ | 500K | 128 | ~779K |
24
+ | 1M | 4096| ~1.02M |
25
+
26
+ All checkpoints are byte-level (vocabulary = 256) and were trained on
27
+ [mookiezi/Discord-Dialogues](https://huggingface.co/datasets/mookiezi/Discord-Dialogues),
28
+ so prompts in `User:` / `Assistant:` format tend to work best.
29
+
30
+ ## How it works
31
+
32
+ - The first time you pick a model, `app.py` downloads its `model.pt` from
33
+ the Hub via `huggingface_hub.hf_hub_download` and caches it.
34
+ - The architecture (`src/model.py`) and the byte-level tokenizer
35
+ (`src/tokenizer.py`) are vendored from
36
+ [llaa33219/MicroMixer-2](https://github.com/llaa33219/MicroMixer-2).
37
+ - `MicroMixerConfig` is rebuilt from the exact hyperparameters listed on
38
+ each model card, so `load_state_dict` matches the published checkpoints.
39
+
40
+ ## Files
41
+
42
+ ```
43
+ .
44
+ ├── app.py # Gradio entry point
45
+ ├── requirements.txt
46
+ ├── src/
47
+ │ ├── __init__.py
48
+ │ ├── model.py # MicroMixer-2 V4 architecture
49
+ │ └── tokenizer.py # ByteTokenizer (vocab=256)
50
+ └── README.md
51
+ ```
52
+
53
+ ## License
54
+
55
+ Apache 2.0, in line with the upstream project.
app.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Space app for the MicroMixer-2 Discord-dialogue model family.
2
+
3
+ The four checkpoints in the llaa33219/micromixer-2 collection are all
4
+ MLP-Mixer based byte-level language models. They differ only in size,
5
+ max sequence length, and a couple of regularisation knobs (DropPath,
6
+ label smoothing) - everything else shares the same architecture and
7
+ the same ByteTokenizer.
8
+
9
+ This app exposes a Gradio demo that:
10
+ * lets the user pick one of the four checkpoints,
11
+ * downloads `model.pt` from the Hub on first use and caches it,
12
+ * generates Discord-style replies from a prompt.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from pathlib import Path
19
+ from typing import Dict, Tuple
20
+
21
+ import gradio as gr
22
+ import torch
23
+ from huggingface_hub import hf_hub_download
24
+
25
+ from src.model import MicroMixer, MicroMixerConfig
26
+ from src.tokenizer import ByteTokenizer
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Model registry
31
+ # ---------------------------------------------------------------------------
32
+ # Each entry maps a UI label to:
33
+ # (hf_repo_id, MicroMixerConfig kwargs matching the card)
34
+ #
35
+ # Numbers were taken straight from the model cards on the Hub
36
+ # (llaa33219/MicroMixer-2-*-discord-dialogues).
37
+ MODEL_REGISTRY: Dict[str, Tuple[str, dict]] = {
38
+ "100K (max 64 tok, ~125K params)": (
39
+ "llaa33219/MicroMixer-2-100K-discord-dialogues",
40
+ dict(
41
+ max_seq_len=64,
42
+ hidden_dim=84,
43
+ hyper_hidden_dim=48,
44
+ channel_mlp_dim=128,
45
+ num_layers=3,
46
+ dropout=0.1,
47
+ drop_path=0.0,
48
+ label_smoothing=0.0,
49
+ ),
50
+ ),
51
+ "300K (max 128 tok, ~431K params)": (
52
+ "llaa33219/MicroMixer-2-300K-discord-dialogues",
53
+ dict(
54
+ max_seq_len=128,
55
+ hidden_dim=128,
56
+ hyper_hidden_dim=64,
57
+ channel_mlp_dim=288,
58
+ num_layers=4,
59
+ dropout=0.1,
60
+ drop_path=0.05,
61
+ label_smoothing=0.05,
62
+ ),
63
+ ),
64
+ "500K (max 128 tok, ~779K params)": (
65
+ "llaa33219/MicroMixer-2-500K-discord-dialogues",
66
+ dict(
67
+ max_seq_len=128,
68
+ hidden_dim=176,
69
+ hyper_hidden_dim=88,
70
+ channel_mlp_dim=384,
71
+ num_layers=4,
72
+ dropout=0.1,
73
+ drop_path=0.1,
74
+ label_smoothing=0.05,
75
+ ),
76
+ ),
77
+ "1M (max 4096 tok, ~1.02M params)": (
78
+ "llaa33219/MicroMixer-2-1M-discord-dialogues",
79
+ dict(
80
+ max_seq_len=4096,
81
+ hidden_dim=168,
82
+ hyper_hidden_dim=84,
83
+ channel_mlp_dim=448,
84
+ num_layers=5,
85
+ dropout=0.1,
86
+ drop_path=0.1,
87
+ label_smoothing=0.1,
88
+ ),
89
+ ),
90
+ }
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Cached model loader
95
+ # ---------------------------------------------------------------------------
96
+ class ModelCache:
97
+ """Lazily downloads, builds, and caches the four MicroMixer checkpoints."""
98
+
99
+ def __init__(self) -> None:
100
+ self._cache: Dict[str, Tuple[MicroMixer, "torch.device"]] = {}
101
+ self._tokenizer = ByteTokenizer()
102
+ # Prefer an explicit env override, then the Spaces persistent volume
103
+ # (/data, only present when the Space opted in to persistent storage),
104
+ # then huggingface_hub's default cache. Falling back gracefully means
105
+ # the demo still works on a stock CPU Space.
106
+ env_dir = os.environ.get("MICROMIXER_CACHE")
107
+ if env_dir:
108
+ self._cache_dir = Path(env_dir)
109
+ elif Path("/data").is_dir():
110
+ self._cache_dir = Path("/data")
111
+ else:
112
+ self._cache_dir = None # let hf_hub_download use its default
113
+ if self._cache_dir is not None:
114
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
115
+ self._device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
116
+
117
+ @property
118
+ def device(self) -> "torch.device":
119
+ return self._device
120
+
121
+ @property
122
+ def tokenizer(self) -> ByteTokenizer:
123
+ return self._tokenizer
124
+
125
+ def get(self, label: str) -> MicroMixer:
126
+ if label not in self._cache:
127
+ self._cache[label] = self._load(label)
128
+ return self._cache[label][0]
129
+
130
+ def _load(self, label: str) -> Tuple[MicroMixer, "torch.device"]:
131
+ repo_id, cfg_kwargs = MODEL_REGISTRY[label]
132
+ config = MicroMixerConfig(**cfg_kwargs)
133
+
134
+ # 1. Download the .pt file (cached locally between Space restarts).
135
+ download_kwargs = {"repo_id": repo_id, "filename": "model.pt"}
136
+ if self._cache_dir is not None:
137
+ download_kwargs["cache_dir"] = str(self._cache_dir)
138
+ ckpt_path = hf_hub_download(**download_kwargs)
139
+
140
+ # 2. Build the model and load weights.
141
+ model = MicroMixer(config)
142
+ state = torch.load(ckpt_path, map_location=self._device, weights_only=False)
143
+ # Checkpoints on the Hub store the weights under "model_state_dict";
144
+ # be defensive in case a future upload drops the wrapper key.
145
+ if isinstance(state, dict) and "model_state_dict" in state:
146
+ state = state["model_state_dict"]
147
+ model.load_state_dict(state)
148
+
149
+ model.to(self._device)
150
+ model.eval()
151
+ return model, self._device
152
+
153
+
154
+ CACHE = ModelCache()
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Generation
159
+ # ---------------------------------------------------------------------------
160
+ def _resolve_max_new_tokens(prompt: str, max_seq_len: int) -> int:
161
+ """Cap generation so the running window never exceeds the model's context."""
162
+ prompt_len = len(CACHE.tokenizer.encode(prompt))
163
+ # Keep at least a 1-token safety margin for the seed token.
164
+ budget = max_seq_len - prompt_len - 1
165
+ return max(1, budget)
166
+
167
+
168
+ def generate(
169
+ model_label: str,
170
+ prompt: str,
171
+ max_new_tokens: int,
172
+ temperature: float,
173
+ top_k: int,
174
+ ) -> str:
175
+ if not prompt:
176
+ return "⚠️ Prompt is empty - type something first."
177
+
178
+ try:
179
+ model = CACHE.get(model_label)
180
+ except Exception as exc: # pragma: no cover - surfaced to the UI
181
+ return f"❌ Failed to load `{model_label}`:\n```\n{exc}\n```"
182
+
183
+ cfg = MODEL_REGISTRY[model_label][1]
184
+ max_seq_len = cfg["max_seq_len"]
185
+ hard_cap = _resolve_max_new_tokens(prompt, max_seq_len)
186
+ max_new_tokens = int(min(max_new_tokens, hard_cap))
187
+
188
+ input_ids = CACHE.tokenizer.encode(prompt)
189
+ input_tensor = torch.tensor([input_ids], dtype=torch.long, device=CACHE.device)
190
+
191
+ with torch.no_grad():
192
+ output_ids = model.generate(
193
+ input_tensor,
194
+ max_new_tokens=max_new_tokens,
195
+ temperature=temperature,
196
+ top_k=int(top_k),
197
+ )
198
+
199
+ full = CACHE.tokenizer.decode(output_ids[0].cpu().tolist())
200
+ return full
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Gradio UI
205
+ # ---------------------------------------------------------------------------
206
+ DEFAULT_PROMPT = "User: Hello! How are you today?\nAssistant:"
207
+
208
+ EXAMPLES = [
209
+ ["User: What games do you play?\nAssistant:"],
210
+ ["User: Tell me a joke about programming.\nAssistant:"],
211
+ ["User: I'm bored, what should I do?\nAssistant:"],
212
+ ["User: Good morning!\nAssistant:"],
213
+ ["User: Do you like pizza?\nAssistant:"],
214
+ ]
215
+
216
+
217
+ def build_demo() -> gr.Blocks:
218
+ # NOTE: keep kwarg names compatible with Gradio 4.x / 5.x / 6.x.
219
+ # `theme` was moved from Blocks() to launch() in Gradio 6, so we
220
+ # hand the theme to launch() and leave Blocks() vanilla.
221
+ with gr.Blocks(
222
+ title="MicroMixer-2 Discord Demo",
223
+ ) as demo:
224
+ gr.Markdown(
225
+ """
226
+ # 🎛️ MicroMixer-2 Discord-dialogue Playground
227
+
228
+ Try the four attention-free, MLP-only language models from
229
+ [`llaa33219/micromixer-2`](https://huggingface.co/collections/llaa33219/micromixer-2).
230
+ All checkpoints are byte-level (vocab = 256) and were trained on
231
+ [mookiezi/Discord-Dialogues](https://huggingface.co/datasets/mookiezi/Discord-Dialogues),
232
+ so prompting with a `User:` / `Assistant:` turn works best.
233
+
234
+ | Variant | Max context | Params |
235
+ | --- | --- | --- |
236
+ | 100K | 64 | ~125K |
237
+ | 300K | 128 | ~431K |
238
+ | 500K | 128 | ~779K |
239
+ | 1M | 4096| ~1.02M |
240
+ """
241
+ )
242
+
243
+ with gr.Row():
244
+ with gr.Column(scale=1):
245
+ model_dd = gr.Dropdown(
246
+ choices=list(MODEL_REGISTRY.keys()),
247
+ value="1M (max 4096 tok, ~1.02M params)",
248
+ label="Model",
249
+ info="The 1M model is the strongest but slowest.",
250
+ )
251
+ prompt_tb = gr.Textbox(
252
+ label="Prompt",
253
+ value=DEFAULT_PROMPT,
254
+ lines=4,
255
+ placeholder="User: ...\nAssistant:",
256
+ )
257
+ with gr.Accordion("Sampling settings", open=True):
258
+ max_new = gr.Slider(
259
+ minimum=8, maximum=512, value=128, step=8,
260
+ label="max_new_tokens",
261
+ )
262
+ temperature = gr.Slider(
263
+ minimum=0.1, maximum=2.0, value=0.8, step=0.05,
264
+ label="temperature",
265
+ )
266
+ top_k = gr.Slider(
267
+ minimum=0, maximum=200, value=40, step=1,
268
+ label="top_k (0 = off)",
269
+ )
270
+ run_btn = gr.Button("Generate", variant="primary")
271
+ with gr.Column(scale=2):
272
+ output = gr.Textbox(
273
+ label="Output",
274
+ lines=18,
275
+ interactive=False,
276
+ )
277
+
278
+ gr.Examples(
279
+ examples=EXAMPLES,
280
+ inputs=[prompt_tb],
281
+ label="Prompt examples (User/Assistant format)",
282
+ )
283
+
284
+ gr.Markdown(
285
+ """
286
+ ---
287
+ ### Notes
288
+ * First run for a given model downloads `model.pt` from the Hub
289
+ (one-time, then cached). All four checkpoints together are < 20 MB.
290
+ * The 100K/300K/500K models cap context at 64–128 bytes, so the
291
+ UI clamps `max_new_tokens` automatically.
292
+ * Runs on CPU by default; a CUDA GPU will be used automatically
293
+ if the Space has one.
294
+ * Source: [github.com/llaa33219/MicroMixer-2](https://github.com/llaa33219/MicroMixer-2)
295
+ """
296
+ )
297
+
298
+ run_btn.click(
299
+ fn=generate,
300
+ inputs=[model_dd, prompt_tb, max_new, temperature, top_k],
301
+ outputs=output,
302
+ )
303
+
304
+ return demo
305
+
306
+
307
+ if __name__ == "__main__":
308
+ demo = build_demo()
309
+ demo.queue(max_size=8).launch(
310
+ server_name="0.0.0.0",
311
+ server_port=7860,
312
+ theme=gr.themes.Soft(),
313
+ )
314
+ else:
315
+ # When imported (e.g. by Spaces that wrap `app.py`).
316
+ demo = build_demo()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ torch>=2.0
2
+ gradio>=4.0
3
+ huggingface_hub>=0.20
src/__init__.py ADDED
File without changes
src/model.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MicroMixer-2 V4: MLP-Mixer architecture optimized for language models.
2
+
3
+ V4 innovations based on research:
4
+ - DropPath (stochastic depth): Regularization via random residual skipping
5
+ - FourierMixing: Parameter-free FFT token mixing (FNet-inspired)
6
+ - Padding-aware loss: Ignore padding tokens in cross-entropy
7
+ - Label smoothing: Regularize overconfident predictions
8
+ - Increased depth: 6-12 layers for larger models
9
+ - HyperMixing (ACL 2023): O(S) token mixing via hypernetwork
10
+ - RoPE: Rotary position embedding for length generalization
11
+ - Standard MLP: Better knowledge capacity than GatedMLP
12
+ """
13
+
14
+ import math
15
+ from dataclasses import dataclass, field
16
+ from enum import Enum, auto
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+
22
+
23
+ class TokenMixerType(Enum):
24
+ HYPER = auto() # HyperMixing: O(S) via hypernetwork
25
+ FOURIER = auto() # FourierMixing: O(S log S) via FFT, zero params
26
+
27
+
28
+ class DropPath(nn.Module):
29
+ """Stochastic Depth (DropPath) per sample.
30
+
31
+ Randomly drops entire residual branches during training.
32
+ Linear schedule: drop probability increases with layer depth.
33
+ """
34
+
35
+ def __init__(self, drop_prob: float = 0.0):
36
+ super().__init__()
37
+ self.drop_prob = drop_prob
38
+
39
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
40
+ if not self.training or self.drop_prob == 0.0:
41
+ return x
42
+
43
+ keep_prob = 1 - self.drop_prob
44
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1)
45
+ random_tensor = torch.rand(shape, dtype=x.dtype, device=x.device)
46
+ random_tensor = torch.floor(random_tensor + keep_prob)
47
+ output = x / keep_prob * random_tensor
48
+ return output
49
+
50
+
51
+ class MlpBlock(nn.Module):
52
+ """Standard 2-layer MLP with GELU activation."""
53
+
54
+ def __init__(self, in_dim: int, hidden_dim: int, dropout: float = 0.1):
55
+ super().__init__()
56
+ self.fc1 = nn.Linear(in_dim, hidden_dim)
57
+ self.fc2 = nn.Linear(hidden_dim, in_dim)
58
+ self.dropout = nn.Dropout(dropout)
59
+
60
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
61
+ x = self.fc1(x)
62
+ x = F.gelu(x)
63
+ x = self.dropout(x)
64
+ x = self.fc2(x)
65
+ x = self.dropout(x)
66
+ return x
67
+
68
+
69
+ class RotaryPositionEmbedding(nn.Module):
70
+ """Rotary Position Embedding (RoPE) for length generalization."""
71
+
72
+ def __init__(self, dim: int, max_seq_len: int = 512):
73
+ super().__init__()
74
+ self.dim = dim
75
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
76
+ self.register_buffer("inv_freq", inv_freq)
77
+
78
+ def forward(self, x: torch.Tensor, seq_len: int) -> torch.Tensor:
79
+ t = torch.arange(seq_len, device=x.device).type_as(self.inv_freq)
80
+ freqs = torch.einsum("i,j->ij", t, self.inv_freq)
81
+ emb = torch.cat((freqs, freqs), dim=-1)
82
+ cos, sin = emb.cos(), emb.sin()
83
+ cos = cos.unsqueeze(0)
84
+ sin = sin.unsqueeze(0)
85
+
86
+ x_rot = x[..., : self.dim]
87
+ x_rest = x[..., self.dim :] if x.shape[-1] > self.dim else None
88
+
89
+ x1, x2 = x_rot[..., ::2], x_rot[..., 1::2]
90
+ rotated = torch.stack([-x2, x1], dim=-1).flatten(-2)
91
+ x_rotated = x_rot * cos + rotated * sin
92
+
93
+ if x_rest is not None:
94
+ return torch.cat([x_rotated, x_rest], dim=-1)
95
+ return x_rotated
96
+
97
+
98
+ class HyperMixing(nn.Module):
99
+ """HyperMixing: O(S) token mixing via cumulative-mean hypernetwork.
100
+
101
+ Based on HyperMixer (ACL 2023). Uses running statistics to generate
102
+ mixing weights dynamically.
103
+ """
104
+
105
+ def __init__(self, hidden_dim: int, hyper_hidden_dim: int, dropout: float = 0.1):
106
+ super().__init__()
107
+ self.hidden_dim = hidden_dim
108
+ self.hyper = nn.Sequential(
109
+ nn.Linear(hidden_dim, hyper_hidden_dim),
110
+ nn.GELU(),
111
+ nn.Linear(hyper_hidden_dim, hidden_dim * 2),
112
+ )
113
+ self.dropout = nn.Dropout(dropout)
114
+
115
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
116
+ B, S, H = x.shape
117
+
118
+ # Cumulative mean for causal context
119
+ cumsum = torch.cumsum(x, dim=1)
120
+ counts = torch.arange(1, S + 1, device=x.device).view(1, S, 1).float()
121
+ pooled = cumsum / counts
122
+
123
+ # Hypernetwork generates affine transform weights
124
+ weights = self.hyper(pooled)
125
+ w1, w2 = weights.chunk(2, dim=-1)
126
+
127
+ # Affine mixing: scale + shift
128
+ x = x * w1 + w2
129
+ return self.dropout(x)
130
+
131
+
132
+ class FourierMixing(nn.Module):
133
+ """FourierMixing: Parameter-free token mixing via FFT.
134
+
135
+ Based on FNet (NAACL 2022). Replaces attention with 2D FFT.
136
+ - Zero learnable parameters for token mixing
137
+ - O(S log S) complexity
138
+ - 80% faster than attention on GPUs
139
+
140
+ Causal property: FFT mixes all positions, so we apply
141
+ cumulative masking to maintain autoregressive property.
142
+ """
143
+
144
+ def __init__(self, hidden_dim: int, dropout: float = 0.1):
145
+ super().__init__()
146
+ self.dropout = nn.Dropout(dropout)
147
+
148
+ # Learnable scaling for output (optional, helps stability)
149
+ self.scale = nn.Parameter(torch.ones(1) * 0.1)
150
+
151
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
152
+ B, S, H = x.shape
153
+
154
+ # Apply FFT along sequence dimension (dim=1)
155
+ # Real-valued FFT preserves real output
156
+ x_fft = torch.fft.fft(x, dim=1).real
157
+
158
+ # Apply causal masking: each position only sees itself and prior
159
+ # Use cumulative sum to enforce causality
160
+ mask = torch.triu(torch.ones(S, S, device=x.device)).bool()
161
+ # More efficient: use cumulative mean like HyperMixing
162
+ cumsum = torch.cumsum(x_fft, dim=1)
163
+ counts = torch.arange(1, S + 1, device=x.device).view(1, S, 1).float()
164
+ x_causal = cumsum / counts
165
+
166
+ # Blend original FFT output with causal version
167
+ x = x_fft * (1 - self.scale) + x_causal * self.scale
168
+
169
+ return self.dropout(x)
170
+
171
+
172
+ class MicroMixerLayer(nn.Module):
173
+ """Single MicroMixer layer with DropPath regularization.
174
+
175
+ Architecture:
176
+ 1. LayerNorm -> Token Mixing -> DropPath -> Residual
177
+ 2. LayerNorm -> Channel Mixing (MLP) -> DropPath -> Residual
178
+ """
179
+
180
+ def __init__(
181
+ self,
182
+ hidden_dim: int,
183
+ hyper_hidden_dim: int,
184
+ channel_mlp_dim: int,
185
+ dropout: float = 0.1,
186
+ drop_path: float = 0.0,
187
+ mixer_type: TokenMixerType = TokenMixerType.HYPER,
188
+ ):
189
+ super().__init__()
190
+ self.norm1 = nn.LayerNorm(hidden_dim)
191
+ self.norm2 = nn.LayerNorm(hidden_dim)
192
+
193
+ if mixer_type == TokenMixerType.HYPER:
194
+ self.token_mixer = HyperMixing(hidden_dim, hyper_hidden_dim, dropout)
195
+ elif mixer_type == TokenMixerType.FOURIER:
196
+ self.token_mixer = FourierMixing(hidden_dim, dropout)
197
+ else:
198
+ raise ValueError(f"Unknown mixer type: {mixer_type}")
199
+
200
+ self.channel_mlp = MlpBlock(hidden_dim, channel_mlp_dim, dropout)
201
+ self.drop_path = DropPath(drop_path) if drop_path > 0 else nn.Identity()
202
+
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
204
+ # Token mixing with stochastic depth
205
+ residual = x
206
+ x = self.norm1(x)
207
+ x = self.token_mixer(x)
208
+ x = residual + self.drop_path(x)
209
+
210
+ # Channel mixing with stochastic depth
211
+ residual = x
212
+ x = self.norm2(x)
213
+ x = self.channel_mlp(x)
214
+ x = residual + self.drop_path(x)
215
+
216
+ return x
217
+
218
+
219
+ @dataclass
220
+ class MicroMixerConfig:
221
+ """Configuration for MicroMixer-2 V4.
222
+
223
+ Attributes:
224
+ vocab_size: Vocabulary size (256 for byte-level).
225
+ max_seq_len: Maximum sequence length.
226
+ hidden_dim: Hidden dimension for embeddings and mixer layers.
227
+ hyper_hidden_dim: Hidden dimension for HyperMixing hypernetwork.
228
+ channel_mlp_dim: Inner dimension of channel-mixing MLP.
229
+ num_layers: Number of mixer layers.
230
+ dropout: Dropout probability.
231
+ drop_path: DropPath probability (0 = disabled).
232
+ label_smoothing: Label smoothing for cross-entropy (0 = disabled).
233
+ tie_weights: Tie input/output embeddings.
234
+ mixer_type: Token mixing strategy (HYPER or FOURIER).
235
+ pad_token_id: Padding token ID for masked loss.
236
+ """
237
+
238
+ vocab_size: int = 256
239
+ max_seq_len: int = 128
240
+ hidden_dim: int = 128
241
+ hyper_hidden_dim: int = 64
242
+ channel_mlp_dim: int = 256
243
+ num_layers: int = 2
244
+ dropout: float = 0.1
245
+ drop_path: float = 0.0
246
+ label_smoothing: float = 0.0
247
+ tie_weights: bool = True
248
+ mixer_type: TokenMixerType = TokenMixerType.HYPER
249
+ pad_token_id: int = 0
250
+
251
+
252
+ class MicroMixer(nn.Module):
253
+ """MicroMixer-2 V4: MLP-Mixer language model with research-backed innovations.
254
+
255
+ V4 improvements:
256
+ - DropPath: Stochastic depth regularization
257
+ - FourierMixing: Optional parameter-free FFT mixing
258
+ - Padding-aware loss: Ignores padding tokens
259
+ - Label smoothing: Regularizes overconfident predictions
260
+ - Increased depth: Up to 12 layers for larger models
261
+ """
262
+
263
+ def __init__(self, config: MicroMixerConfig):
264
+ super().__init__()
265
+ self.config = config
266
+ self.vocab_size = config.vocab_size
267
+ self.max_seq_len = config.max_seq_len
268
+ self.hidden_dim = config.hidden_dim
269
+ self.pad_token_id = config.pad_token_id
270
+
271
+ self.token_embedding = nn.Embedding(config.vocab_size, config.hidden_dim)
272
+ self.rope = RotaryPositionEmbedding(config.hidden_dim, config.max_seq_len)
273
+ self.dropout = nn.Dropout(config.dropout)
274
+
275
+ # DropPath: linear schedule (increases with depth)
276
+ dpr = [x.item() for x in torch.linspace(0, config.drop_path, config.num_layers)]
277
+
278
+ self.mixer_layers = nn.ModuleList([
279
+ MicroMixerLayer(
280
+ config.hidden_dim,
281
+ config.hyper_hidden_dim,
282
+ config.channel_mlp_dim,
283
+ config.dropout,
284
+ drop_path=dpr[i],
285
+ mixer_type=config.mixer_type,
286
+ )
287
+ for i in range(config.num_layers)
288
+ ])
289
+
290
+ self.layer_norm = nn.LayerNorm(config.hidden_dim)
291
+ self.lm_head = nn.Linear(config.hidden_dim, config.vocab_size, bias=False)
292
+
293
+ if config.tie_weights:
294
+ self.lm_head.weight = self.token_embedding.weight
295
+
296
+ self.apply(self._init_weights)
297
+
298
+ def _init_weights(self, module: nn.Module):
299
+ if isinstance(module, nn.Linear):
300
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
301
+ if getattr(module, "bias", None) is not None:
302
+ torch.nn.init.zeros_(module.bias)
303
+ elif isinstance(module, nn.Embedding):
304
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
305
+ elif isinstance(module, nn.LayerNorm):
306
+ torch.nn.init.ones_(module.weight)
307
+ torch.nn.init.zeros_(module.bias)
308
+
309
+ def forward(
310
+ self,
311
+ input_ids: torch.Tensor,
312
+ targets: torch.Tensor | None = None,
313
+ attention_mask: torch.Tensor | None = None,
314
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
315
+ B, S = input_ids.shape
316
+
317
+ if S > self.max_seq_len:
318
+ input_ids = input_ids[:, -self.max_seq_len :]
319
+ S = self.max_seq_len
320
+ if targets is not None:
321
+ targets = targets[:, -self.max_seq_len :]
322
+ if attention_mask is not None:
323
+ attention_mask = attention_mask[:, -self.max_seq_len :]
324
+
325
+ token_emb = self.token_embedding(input_ids)
326
+ x = self.rope(token_emb, S)
327
+ x = self.dropout(x)
328
+
329
+ for layer in self.mixer_layers:
330
+ x = layer(x)
331
+
332
+ x = self.layer_norm(x)
333
+ logits = self.lm_head(x)
334
+
335
+ if targets is not None:
336
+ # Flatten for cross-entropy
337
+ logits_flat = logits.view(-1, self.vocab_size)
338
+ targets_flat = targets.view(-1)
339
+
340
+ # Build ignore_mask: padding tokens AND positions after padding
341
+ if attention_mask is not None:
342
+ # Shift mask: predict token AFTER seeing context
343
+ # mask[i] = 1 means token i is real, so predicting i+1 is valid
344
+ shifted_mask = torch.ones_like(attention_mask)
345
+ shifted_mask[:, 1:] = attention_mask[:, :-1]
346
+ ignore_mask = (shifted_mask.view(-1) == 0)
347
+ pad_indices = ignore_mask.nonzero(as_tuple=True)[0]
348
+ else:
349
+ # No mask provided: only ignore explicit pad tokens in targets
350
+ pad_indices = (targets_flat == self.pad_token_id).nonzero(as_tuple=True)[0]
351
+
352
+ # Compute loss with label smoothing and padding ignore
353
+ loss = F.cross_entropy(
354
+ logits_flat,
355
+ targets_flat,
356
+ ignore_index=self.pad_token_id if len(pad_indices) > 0 else -100,
357
+ label_smoothing=self.config.label_smoothing,
358
+ )
359
+ return logits, loss
360
+
361
+ return logits
362
+
363
+ @torch.no_grad()
364
+ def generate(
365
+ self,
366
+ input_ids: torch.Tensor,
367
+ max_new_tokens: int,
368
+ temperature: float = 1.0,
369
+ top_k: int | None = None,
370
+ ) -> torch.Tensor:
371
+ """Autoregressive text generation."""
372
+ self.eval()
373
+ device = next(self.parameters()).device
374
+ input_ids = input_ids.to(device)
375
+
376
+ for _ in range(max_new_tokens):
377
+ logits = self(input_ids)
378
+ logits = logits[:, -1, :]
379
+
380
+ if temperature == 0.0:
381
+ next_token = torch.argmax(logits, dim=-1, keepdim=True)
382
+ else:
383
+ logits = logits / temperature
384
+
385
+ if top_k is not None:
386
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
387
+ logits[logits < v[:, [-1]]] = -float("Inf")
388
+
389
+ probs = F.softmax(logits, dim=-1)
390
+ next_token = torch.multinomial(probs, num_samples=1)
391
+
392
+ input_ids = torch.cat([input_ids, next_token], dim=1)
393
+
394
+ return input_ids
395
+
396
+
397
+ def count_parameters(model: nn.Module) -> int:
398
+ """Count total trainable parameters."""
399
+ return sum(p.numel() for p in model.parameters() if p.requires_grad)
400
+
401
+
402
+
403
+
404
+ def micromixer_100k() -> MicroMixerConfig:
405
+ """~100K parameter model for testing/experimentation."""
406
+ return MicroMixerConfig(
407
+ max_seq_len=64,
408
+ hidden_dim=84,
409
+ hyper_hidden_dim=48,
410
+ channel_mlp_dim=128,
411
+ num_layers=3,
412
+ dropout=0.1,
413
+ drop_path=0.0,
414
+ label_smoothing=0.0,
415
+ )
416
+
417
+
418
+ def micromixer_300k() -> MicroMixerConfig:
419
+ """~300K parameter model for small-scale experiments."""
420
+ return MicroMixerConfig(
421
+ max_seq_len=128,
422
+ hidden_dim=128,
423
+ hyper_hidden_dim=64,
424
+ channel_mlp_dim=288,
425
+ num_layers=4,
426
+ dropout=0.1,
427
+ drop_path=0.05,
428
+ label_smoothing=0.05,
429
+ )
430
+
431
+
432
+ def micromixer_500k() -> MicroMixerConfig:
433
+ """~500K parameter model for medium-scale experiments."""
434
+ return MicroMixerConfig(
435
+ max_seq_len=128,
436
+ hidden_dim=176,
437
+ hyper_hidden_dim=88,
438
+ channel_mlp_dim=384,
439
+ num_layers=4,
440
+ dropout=0.1,
441
+ drop_path=0.1,
442
+ label_smoothing=0.05,
443
+ )
444
+
445
+
446
+ def micromixer_1m() -> MicroMixerConfig:
447
+ """~1M parameter model for standard experiments."""
448
+ return MicroMixerConfig(
449
+ max_seq_len=256,
450
+ hidden_dim=168,
451
+ hyper_hidden_dim=84,
452
+ channel_mlp_dim=448,
453
+ num_layers=5,
454
+ dropout=0.1,
455
+ drop_path=0.1,
456
+ label_smoothing=0.1,
457
+ )
458
+
459
+
460
+ def micromixer_1m_long(max_seq_len: int = 4096) -> MicroMixerConfig:
461
+ """~1M parameter model with extended context length."""
462
+ return MicroMixerConfig(
463
+ max_seq_len=max_seq_len,
464
+ hidden_dim=168,
465
+ hyper_hidden_dim=84,
466
+ channel_mlp_dim=448,
467
+ num_layers=5,
468
+ dropout=0.1,
469
+ drop_path=0.1,
470
+ label_smoothing=0.1,
471
+ )
472
+
473
+
474
+ def micromixer_1m_fourier() -> MicroMixerConfig:
475
+ """~1M parameter model with FourierMixing (parameter-free token mixing)."""
476
+ return MicroMixerConfig(
477
+ max_seq_len=256,
478
+ hidden_dim=168,
479
+ hyper_hidden_dim=84,
480
+ channel_mlp_dim=448,
481
+ num_layers=5,
482
+ dropout=0.1,
483
+ drop_path=0.1,
484
+ label_smoothing=0.1,
485
+ mixer_type=TokenMixerType.FOURIER,
486
+ )
487
+
488
+
489
+ if __name__ == "__main__":
490
+ print("Testing MicroMixer-2 V4...")
491
+
492
+ for name, config_fn in [
493
+ ("100k", micromixer_100k),
494
+ ("300k", micromixer_300k),
495
+ ("500k", micromixer_500k),
496
+ ("1M", micromixer_1m),
497
+ ("1M-fourier", micromixer_1m_fourier),
498
+ ]:
499
+ config = config_fn()
500
+ model = MicroMixer(config)
501
+ params = count_parameters(model)
502
+ print(f" {name}: {params:,} parameters, {config.num_layers} layers")
503
+
504
+ batch_size = 2
505
+ seq_len = min(32, config.max_seq_len)
506
+ input_ids = torch.randint(0, config.vocab_size, (batch_size, seq_len))
507
+
508
+ logits = model(input_ids)
509
+ assert logits.shape == (batch_size, seq_len, config.vocab_size)
510
+
511
+ targets = torch.randint(0, config.vocab_size, (batch_size, seq_len))
512
+ logits, loss = model(input_ids, targets)
513
+ assert logits.shape == (batch_size, seq_len, config.vocab_size)
514
+ assert loss.dim() == 0
515
+
516
+ prompt = input_ids[:, :4]
517
+ gen_ids = model.generate(prompt, max_new_tokens=8, temperature=0.8, top_k=10)
518
+ assert gen_ids.shape == (batch_size, 12)
519
+
520
+ prefix = torch.randint(0, config.vocab_size, (1, 5))
521
+ extra = torch.randint(0, config.vocab_size, (1, 3))
522
+ logits_prefix = model(prefix)[:, -1, :]
523
+ logits_extended = model(torch.cat([prefix, extra], dim=1))[:, 4, :]
524
+ assert torch.allclose(logits_prefix, logits_extended, atol=1e-5)
525
+
526
+ print("All V4 tests passed!")
src/tokenizer.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Byte-level tokenizer for MicroMixer-1 language model."""
2
+
3
+
4
+ class ByteTokenizer:
5
+ """Byte-level tokenizer with 256 vocabulary size.
6
+
7
+ Reserves byte values 0, 1, 2 for special tokens:
8
+ - 0: pad_token
9
+ - 1: bos_token (beginning of sequence)
10
+ - 2: eos_token (end of sequence)
11
+
12
+ All other byte values (3-255) represent raw UTF-8 bytes.
13
+ """
14
+
15
+ def __init__(self):
16
+ self.vocab_size = 256
17
+ # Special tokens (using byte values that are rare in text)
18
+ self.pad_token_id = 0
19
+ self.bos_token_id = 1 # Beginning of sequence
20
+ self.eos_token_id = 2 # End of sequence
21
+
22
+ def encode(self, text: str) -> list[int]:
23
+ """Encode string to list of byte IDs.
24
+
25
+ Args:
26
+ text: Input string to encode
27
+
28
+ Returns:
29
+ List of byte IDs with BOS token at start and EOS token at end
30
+ """
31
+ if not text:
32
+ # Empty string: just return BOS + EOS
33
+ return [self.bos_token_id, self.eos_token_id]
34
+
35
+ # Convert text to UTF-8 bytes
36
+ text_bytes = text.encode("utf-8")
37
+
38
+ # Map each byte to its integer value (0-255)
39
+ byte_ids = [b for b in text_bytes]
40
+
41
+ # Add BOS at start, EOS at end
42
+ return [self.bos_token_id] + byte_ids + [self.eos_token_id]
43
+
44
+ def decode(self, ids: list[int]) -> str:
45
+ """Decode list of byte IDs back to string.
46
+
47
+ Args:
48
+ ids: List of byte IDs
49
+
50
+ Returns:
51
+ Decoded string
52
+ """
53
+ if not ids:
54
+ return ""
55
+
56
+ # Filter out special tokens (pad, bos, eos)
57
+ byte_values = [
58
+ b for b in ids
59
+ if b not in (self.pad_token_id, self.bos_token_id, self.eos_token_id)
60
+ ]
61
+
62
+ if not byte_values:
63
+ return ""
64
+
65
+ # Convert byte values back to bytes object
66
+ byte_data = bytes(byte_values)
67
+
68
+ # Decode UTF-8 bytes to string, handle errors gracefully
69
+ return byte_data.decode("utf-8", errors="replace")
70
+
71
+ def encode_batch(
72
+ self, texts: list[str], max_length: int = None, padding: bool = True
73
+ ) -> dict:
74
+ """Encode a batch of texts with optional padding.
75
+
76
+ Args:
77
+ texts: List of strings to encode
78
+ max_length: Maximum sequence length (truncation if specified)
79
+ padding: Whether to pad sequences to longest in batch
80
+
81
+ Returns:
82
+ Dict with 'input_ids' (list of lists) and 'attention_mask' (list of lists)
83
+ """
84
+ # Encode each text
85
+ encoded = [self.encode(text) for text in texts]
86
+
87
+ # Get sequence lengths before padding/truncation
88
+ lengths = [len(ids) for ids in encoded]
89
+
90
+ # Truncate if max_length specified
91
+ if max_length is not None:
92
+ encoded = [ids[:max_length] for ids in encoded]
93
+
94
+ # Pad to longest sequence if padding=True
95
+ if padding and encoded:
96
+ max_seq_len = max(len(ids) for ids in encoded)
97
+ pad_token_id = self.pad_token_id
98
+
99
+ padded = []
100
+ attention_masks = []
101
+ for ids in encoded:
102
+ pad_len = max_seq_len - len(ids)
103
+ padded.append(ids + [pad_token_id] * pad_len)
104
+ attention_masks.append([1] * len(ids) + [0] * pad_len)
105
+
106
+ return {
107
+ "input_ids": padded,
108
+ "attention_mask": attention_masks,
109
+ }
110
+
111
+ # No padding
112
+ attention_masks = [[1] * len(ids) for ids in encoded]
113
+ return {
114
+ "input_ids": encoded,
115
+ "attention_mask": attention_masks,
116
+ }