zerdovzad commited on
Commit
e8f45c1
Β·
verified Β·
1 Parent(s): 68c9b40

Upload 6 files

Browse files
nord_v4_700m-4.2/chat_v4.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD v4 β€” Interactive Chat v4.0 β•‘
4
+ β•‘ β•‘
5
+ β•‘ Commands: β•‘
6
+ β•‘ /stdp on|off β€” Toggle online learning β•‘
7
+ β•‘ /stats β€” Show zone & MoE statistics β•‘
8
+ β•‘ /memory β€” Show memory cortex state β•‘
9
+ β•‘ /reset β€” Clear working memory β•‘
10
+ β•‘ /expert β€” Show expert routing breakdown β•‘
11
+ β•‘ /tokens N β€” Set max response tokens (default: 200) β•‘
12
+ β•‘ /temp F β€” Set temperature (default: 0.85) β•‘
13
+ β•‘ /rep F β€” Set repetition penalty (default: 1.3) β•‘
14
+ β•‘ /live on|off β€” Toggle live spike visualization β•‘
15
+ β•‘ /quit β€” Exit β•‘
16
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sys
22
+ import time
23
+ import torch
24
+ import os
25
+ from pathlib import Path
26
+
27
+ from nord_core_700m import NordConfig, NordModel
28
+
29
+ # ── ANSI Colors ──
30
+ class C:
31
+ RESET = "\033[0m"
32
+ BOLD = "\033[1m"
33
+ DIM = "\033[2m"
34
+ CYAN = "\033[96m"
35
+ ORANGE = "\033[38;5;208m"
36
+ PURPLE = "\033[35m"
37
+ GREEN = "\033[92m"
38
+ BLUE = "\033[94m"
39
+ YELLOW = "\033[93m"
40
+ RED = "\033[91m"
41
+ WHITE = "\033[97m"
42
+ GREY = "\033[90m"
43
+
44
+ SPARK_CHARS = " β–‘β–’β–“β–ˆ"
45
+
46
+ def spike_bar(rate, width=20, color=C.CYAN, max_rate=0.4):
47
+ """Colored bar with adjustable scale. max_rate=0.4 means 40% rate fills full bar"""
48
+ normalized = min(rate / max(max_rate, 0.001), 1.0)
49
+ filled = int(normalized * width)
50
+ bar = ""
51
+ for i in range(width):
52
+ if i < filled:
53
+ frac = normalized * width - i
54
+ intensity = min(4, int(frac * 4))
55
+ bar += color + SPARK_CHARS[min(intensity + 1, 4)]
56
+ else:
57
+ bar += C.DIM + "Β·"
58
+ return bar + C.RESET
59
+
60
+
61
+ def render_live_spikes(stats, cfg):
62
+ spike_rates = stats.get("spike_rates", [])
63
+ if not spike_rates:
64
+ return
65
+
66
+ lines = []
67
+ lines.append(f" {C.GREY}{'─' * 56}{C.RESET}")
68
+
69
+ ns = cfg.sensory_layers + 1
70
+ if len(spike_rates) > 0:
71
+ avg_s = sum(spike_rates[:ns]) / max(ns, 1)
72
+ bar = spike_bar(avg_s, 20, C.CYAN)
73
+ lines.append(f" {C.CYAN}⚑ SEN{C.RESET} {bar} {C.CYAN}{avg_s*100:5.1f}%{C.RESET}")
74
+
75
+ na = cfg.association_layers
76
+ if len(spike_rates) > ns:
77
+ assoc_rates = spike_rates[ns:ns+na]
78
+ avg_a = sum(assoc_rates) / max(len(assoc_rates), 1) if assoc_rates else 0
79
+ bar = spike_bar(avg_a, 20, C.ORANGE)
80
+ lines.append(f" {C.ORANGE}⚑ ASC{C.RESET} {bar} {C.ORANGE}{avg_a*100:5.1f}%{C.RESET}")
81
+
82
+ mem_rate = stats.get("memory_spike_rate", 0)
83
+ if isinstance(mem_rate, torch.Tensor):
84
+ mem_rate = mem_rate.item()
85
+ bar = spike_bar(mem_rate * 0.3, 20, C.PURPLE)
86
+ lines.append(f" {C.PURPLE}⚑ MEM{C.RESET} {bar} {C.PURPLE}{mem_rate*100:5.1f}%{C.RESET}")
87
+
88
+ ne = cfg.executive_layers
89
+ offset = ns + na
90
+ if len(spike_rates) > offset:
91
+ exec_rates = spike_rates[offset:]
92
+ avg_e = sum(exec_rates) / max(len(exec_rates), 1) if exec_rates else 0
93
+ bar = spike_bar(avg_e, 20, C.GREEN)
94
+ lines.append(f" {C.GREEN}⚑ EXE{C.RESET} {bar} {C.GREEN}{avg_e*100:5.1f}%{C.RESET}")
95
+
96
+ sp = stats.get("sparsity", 0)
97
+ if isinstance(sp, torch.Tensor):
98
+ sp = sp.item()
99
+ sp_color = C.GREEN if sp > 0.85 else C.YELLOW if sp > 0.7 else C.RED
100
+ lines.append(f" {C.GREY} SPR{C.RESET} {sp_color}{sp*100:.0f}%{C.RESET} {C.DIM}neurons silent{C.RESET}")
101
+ lines.append(f" {C.GREY}{'─' * 56}{C.RESET}")
102
+
103
+ output = "\n".join(lines)
104
+ n_lines = len(lines)
105
+ sys.stdout.write(f"\033[{n_lines}A")
106
+ sys.stdout.write(output + "\n")
107
+ sys.stdout.flush()
108
+
109
+
110
+ def init_live_display(cfg):
111
+ for _ in range(7):
112
+ print()
113
+
114
+
115
+ def render_spike_panel(stats, cfg):
116
+ """Render a clean spike panel BELOW the generated text"""
117
+ spike_rates = stats.get("spike_rates", [])
118
+ if not spike_rates:
119
+ return
120
+
121
+ ns = cfg.sensory_layers + 1
122
+ na = cfg.association_layers
123
+
124
+ print(f" {C.GREY}β”Œ{'─' * 54}┐{C.RESET}")
125
+ print(f" {C.GREY}β”‚{C.RESET} {C.BOLD}Neural Activity{C.RESET}{' ' * 38}{C.GREY}β”‚{C.RESET}")
126
+ print(f" {C.GREY}β”œ{'─' * 54}─{C.RESET}")
127
+
128
+ # Sensory
129
+ if len(spike_rates) > 0:
130
+ avg_s = sum(spike_rates[:ns]) / max(ns, 1)
131
+ bar = spike_bar(avg_s, 25, C.CYAN)
132
+ print(f" {C.GREY}β”‚{C.RESET} {C.CYAN}⚑ Sensory {C.RESET} {bar} {C.CYAN}{avg_s*100:5.1f}%{C.RESET} {C.GREY}β”‚{C.RESET}")
133
+
134
+ # Association
135
+ if len(spike_rates) > ns:
136
+ assoc_rates = spike_rates[ns:ns+na]
137
+ avg_a = sum(assoc_rates) / max(len(assoc_rates), 1) if assoc_rates else 0
138
+ bar = spike_bar(avg_a, 25, C.ORANGE)
139
+ print(f" {C.GREY}β”‚{C.RESET} {C.ORANGE}⚑ Association{C.RESET} {bar} {C.ORANGE}{avg_a*100:5.1f}%{C.RESET} {C.GREY}β”‚{C.RESET}")
140
+
141
+ # Memory
142
+ mem_rate = stats.get("memory_spike_rate", 0)
143
+ if isinstance(mem_rate, torch.Tensor):
144
+ mem_rate = mem_rate.item()
145
+ bar = spike_bar(min(mem_rate, 1.0), 25, C.PURPLE)
146
+ print(f" {C.GREY}β”‚{C.RESET} {C.PURPLE}⚑ Memory {C.RESET} {bar} {C.PURPLE}{mem_rate*100:5.1f}%{C.RESET} {C.GREY}β”‚{C.RESET}")
147
+
148
+ # Executive
149
+ offset = ns + na
150
+ if len(spike_rates) > offset:
151
+ exec_rates = spike_rates[offset:]
152
+ avg_e = sum(exec_rates) / max(len(exec_rates), 1) if exec_rates else 0
153
+ bar = spike_bar(avg_e, 25, C.GREEN)
154
+ print(f" {C.GREY}β”‚{C.RESET} {C.GREEN}⚑ Executive {C.RESET} {bar} {C.GREEN}{avg_e*100:5.1f}%{C.RESET} {C.GREY}β”‚{C.RESET}")
155
+
156
+ # Sparsity
157
+ sp = stats.get("sparsity", 0)
158
+ if isinstance(sp, torch.Tensor):
159
+ sp = sp.item()
160
+ sp_color = C.GREEN if sp > 0.85 else C.YELLOW if sp > 0.7 else C.RED
161
+ silent = int(sp * 100)
162
+ active = 100 - silent
163
+ print(f" {C.GREY}β”œ{'─' * 54}─{C.RESET}")
164
+ print(f" {C.GREY}β”‚{C.RESET} {C.DIM}Sparsity:{C.RESET} {sp_color}{sp*100:.0f}%{C.RESET} silent {C.DIM}({active}% neurons active per token){C.RESET} {C.GREY}β”‚{C.RESET}")
165
+ print(f" {C.GREY}β””{'─' * 54}β”˜{C.RESET}")
166
+
167
+
168
+ def load_model(model_dir: str):
169
+ from transformers import AutoTokenizer
170
+
171
+ model_dir = Path(model_dir)
172
+
173
+ # ── Smart checkpoint search ──
174
+ # 1. If user gave a direct .pt file path
175
+ if model_dir.is_file() and model_dir.suffix == ".pt":
176
+ latest = model_dir
177
+ else:
178
+ latest = None
179
+
180
+ # 2. Search in the given directory
181
+ search_dirs = [model_dir]
182
+
183
+ # 3. Also search in current working directory (where the script is run from)
184
+ cwd = Path.cwd()
185
+ if cwd != model_dir:
186
+ search_dirs.append(cwd)
187
+
188
+ # 4. Also search in the script's own directory
189
+ script_dir = Path(__file__).resolve().parent
190
+ if script_dir != cwd and script_dir != model_dir:
191
+ search_dirs.append(script_dir)
192
+
193
+ # Search order: nord_v4_latest.pt, nord_v4_final.pt, step checkpoints, legacy names
194
+ checkpoint_names = [
195
+ "nord_v4_latest.pt",
196
+ "nord_v4_final.pt",
197
+ "nord_500m_latest.pt",
198
+ "nord_latest.pt",
199
+ ]
200
+
201
+ for search_dir in search_dirs:
202
+ if not search_dir.exists():
203
+ continue
204
+
205
+ # Try known names
206
+ for name in checkpoint_names:
207
+ p = search_dir / name
208
+ if p.exists():
209
+ latest = p
210
+ break
211
+
212
+ # Try step checkpoints
213
+ if latest is None:
214
+ ckpts = sorted(search_dir.glob("nord_v4_step_*.pt"))
215
+ if ckpts:
216
+ latest = ckpts[-1]
217
+
218
+ # Try any .pt file
219
+ if latest is None:
220
+ all_pt = sorted(search_dir.glob("*.pt"))
221
+ if all_pt:
222
+ latest = all_pt[-1]
223
+
224
+ if latest is not None:
225
+ break
226
+
227
+ if latest is None:
228
+ print(f" {C.RED}[βœ—] No checkpoint found!{C.RESET}")
229
+ print(f" {C.DIM}Searched in:{C.RESET}")
230
+ for d in search_dirs:
231
+ exists = "βœ“" if d.exists() else "βœ—"
232
+ print(f" [{exists}] {d}")
233
+ print(f"\n {C.DIM}Place your .pt file in the same folder as chat.py{C.RESET}")
234
+ print(f" {C.DIM}Or give the full path: /path/to/nord_v4_latest.pt{C.RESET}")
235
+ sys.exit(1)
236
+
237
+ print(f" [*] Loading: {latest.name}")
238
+ ckpt = torch.load(latest, map_location="cpu", weights_only=False)
239
+
240
+ saved_cfg = ckpt.get("config", {})
241
+ cfg = NordConfig(
242
+ device="cuda" if torch.cuda.is_available() else "cpu",
243
+ dtype=torch.float16,
244
+ )
245
+ for k, v in saved_cfg.items():
246
+ if hasattr(cfg, k):
247
+ setattr(cfg, k, v)
248
+
249
+ tokenizer = AutoTokenizer.from_pretrained(cfg.tokenizer_id, trust_remote_code=True)
250
+ if tokenizer.pad_token is None:
251
+ tokenizer.pad_token = tokenizer.eos_token
252
+ if cfg.vocab_size < tokenizer.vocab_size:
253
+ cfg.vocab_size = tokenizer.vocab_size
254
+
255
+ model = NordModel(cfg)
256
+ state = ckpt["model_state_dict"]
257
+ filtered = {k: v for k, v in state.items()
258
+ if "_v_mem_state" not in k and "_i_syn_state" not in k}
259
+ model.load_state_dict(filtered, strict=False)
260
+ model = model.to(cfg.device)
261
+ model.eval()
262
+
263
+ total = sum(p.numel() for p in model.parameters())
264
+ print(f" {C.GREEN}[βœ“]{C.RESET} Nord v4 loaded ({total/1e6:.1f}M params)")
265
+ print(f" {C.GREEN}[βœ“]{C.RESET} {model.count_params()}")
266
+
267
+ return model, tokenizer, cfg
268
+
269
+
270
+ @torch.no_grad()
271
+ def generate_streaming(model, tokenizer, cfg, prompt: str,
272
+ max_tokens: int = 200, temperature: float = 0.85,
273
+ top_p: float = 0.9, repetition_penalty: float = 1.3,
274
+ enable_stdp: bool = False, live_spikes: bool = False):
275
+
276
+ input_ids = tokenizer(
277
+ prompt, return_tensors="pt",
278
+ max_length=cfg.max_seq_len, truncation=True,
279
+ ).input_ids.to(cfg.device)
280
+
281
+ model.reset_state()
282
+
283
+ generated = input_ids.clone()
284
+ all_stats = {}
285
+ token_count = 0
286
+
287
+ t_start = time.time()
288
+
289
+ sys.stdout.write(f" {C.BOLD}Nord:{C.RESET} ")
290
+ sys.stdout.flush()
291
+
292
+ for i in range(max_tokens):
293
+ context = generated[:, -cfg.max_seq_len:]
294
+
295
+ if torch.cuda.is_available():
296
+ with torch.amp.autocast(device_type="cuda", dtype=torch.float16,
297
+ enabled=(cfg.dtype == torch.float16)):
298
+ logits, stats = model(context, enable_stdp=enable_stdp)
299
+ else:
300
+ logits, stats = model(context, enable_stdp=enable_stdp)
301
+
302
+ next_logits = logits[:, -1, :].float()
303
+
304
+ if repetition_penalty != 1.0:
305
+ for token_id in generated[0].unique():
306
+ next_logits[0, token_id] /= repetition_penalty
307
+
308
+ next_logits = next_logits / max(temperature, 0.01)
309
+
310
+ probs = torch.softmax(next_logits, dim=-1)
311
+ sorted_probs, sorted_idx = torch.sort(probs, descending=True)
312
+ cumsum = sorted_probs.cumsum(dim=-1)
313
+ mask = cumsum - sorted_probs > top_p
314
+ sorted_probs[mask] = 0
315
+ sorted_probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True)
316
+
317
+ token = sorted_idx[0, torch.multinomial(sorted_probs[0], 1)]
318
+ generated = torch.cat([generated, token.reshape(1, 1)], dim=1)
319
+ token_count += 1
320
+
321
+ if token.item() == tokenizer.eos_token_id:
322
+ break
323
+
324
+ # ── Stream token ──
325
+ decoded_token = tokenizer.decode([token.item()], skip_special_tokens=True)
326
+ sys.stdout.write(decoded_token)
327
+ sys.stdout.flush()
328
+
329
+ all_stats = stats
330
+
331
+ elapsed = time.time() - t_start
332
+ tps = token_count / elapsed if elapsed > 0 else 0
333
+
334
+ rep_score = 1.0
335
+ if token_count > 5:
336
+ out_ids = generated[0][input_ids.shape[1]:].tolist()
337
+ unique = len(set(out_ids))
338
+ rep_score = len(out_ids) / max(unique, 1)
339
+
340
+ sp = all_stats.get("sparsity", 0)
341
+ if isinstance(sp, torch.Tensor):
342
+ sp = sp.item()
343
+
344
+ print(f"\n {C.GREY}[{token_count} tok, {elapsed:.1f}s, {tps:.1f} tok/s "
345
+ f"[REP {rep_score:.1f}] [SPR {sp:.0%}]]{C.RESET}")
346
+
347
+ if live_spikes and all_stats:
348
+ render_spike_panel(all_stats, cfg)
349
+
350
+ return all_stats
351
+
352
+
353
+ def print_stats(stats: dict, cfg: NordConfig):
354
+ print(f"\n {C.GREY}{'─' * 50}{C.RESET}")
355
+ print(f" {C.BOLD}Zone Statistics:{C.RESET}")
356
+
357
+ spike_rates = stats.get("spike_rates", [])
358
+ if spike_rates:
359
+ print(f" {C.DIM}Encoder: {spike_rates[0]:.4f}{C.RESET}")
360
+ for i in range(min(cfg.sensory_layers, len(spike_rates)-1)):
361
+ rate = spike_rates[i+1]
362
+ bar = spike_bar(rate, 15, C.CYAN)
363
+ print(f" {C.CYAN}Sensory[{i}]:{C.RESET} {rate:.4f} {bar}")
364
+ offset = cfg.sensory_layers + 1
365
+ for i in range(cfg.association_layers):
366
+ if offset + i < len(spike_rates):
367
+ rate = spike_rates[offset+i]
368
+ bar = spike_bar(rate, 15, C.ORANGE)
369
+ print(f" {C.ORANGE}Assoc[{i}]:{C.RESET} {rate:.4f} {bar} {C.DIM}(MoE){C.RESET}")
370
+ offset += cfg.association_layers
371
+ for i in range(cfg.executive_layers):
372
+ if offset + i < len(spike_rates):
373
+ rate = spike_rates[offset+i]
374
+ bar = spike_bar(rate, 15, C.GREEN)
375
+ print(f" {C.GREEN}Exec[{i}]:{C.RESET} {rate:.4f} {bar}")
376
+
377
+ print(f"\n {C.BOLD}MoE Routing:{C.RESET}")
378
+ expert_loads = stats.get("expert_loads", None)
379
+ moe_entropy = stats.get("moe_route_entropy", None)
380
+
381
+ # Also check for entropy with assoc_ prefix
382
+ if moe_entropy is None:
383
+ for key in stats:
384
+ if "route_entropy" in key:
385
+ moe_entropy = stats[key]
386
+ break
387
+
388
+ if expert_loads is not None:
389
+ if isinstance(expert_loads, torch.Tensor):
390
+ expert_loads = expert_loads.detach().cpu().tolist()
391
+ if isinstance(expert_loads, float):
392
+ expert_loads = [expert_loads]
393
+ for e, load in enumerate(expert_loads):
394
+ pct = load if isinstance(load, float) else float(load)
395
+ bar = spike_bar(pct, 30, C.YELLOW, max_rate=0.5)
396
+ print(f" Expert {e}: {pct:.2%} {bar}")
397
+ else:
398
+ found = False
399
+ # Search with ALL possible key patterns including assoc_ prefix
400
+ for e in range(cfg.n_experts):
401
+ load = None
402
+ for key_pattern in [
403
+ f"expert_{e}_load",
404
+ f"expert_load_{e}",
405
+ f"moe_expert_{e}",
406
+ ]:
407
+ # Direct match
408
+ if key_pattern in stats:
409
+ load = stats[key_pattern]
410
+ break
411
+ # Prefixed match (assoc_0_expert_0_load, etc.)
412
+ for k, v in stats.items():
413
+ if key_pattern in k:
414
+ load = v
415
+ break
416
+ if load is not None:
417
+ break
418
+
419
+ if load is not None:
420
+ found = True
421
+ if isinstance(load, torch.Tensor): load = load.item()
422
+ bar = spike_bar(load, 30, C.YELLOW, max_rate=0.5)
423
+ print(f" Expert {e}: {load:.2%} {bar}")
424
+
425
+ if not found:
426
+ # Last resort: scan all stats keys for anything with "expert" and "load"
427
+ expert_data = {k: v for k, v in stats.items() if "expert" in k and "load" in k}
428
+ if expert_data:
429
+ found = True
430
+ for k, v in sorted(expert_data.items()):
431
+ if isinstance(v, torch.Tensor): v = v.item()
432
+ bar = spike_bar(v, 30, C.YELLOW, max_rate=0.5)
433
+ name = k.split("_expert_")[-1] if "_expert_" in k else k
434
+ print(f" {name}: {v:.2%} {bar}")
435
+
436
+ if not found:
437
+ moe_lb = stats.get("moe_lb_loss", None)
438
+ if moe_lb is None:
439
+ for k, v in stats.items():
440
+ if "load_balance" in k or "moe_lb" in k:
441
+ moe_lb = v
442
+ break
443
+ if moe_lb is not None:
444
+ if isinstance(moe_lb, torch.Tensor): moe_lb = moe_lb.item()
445
+ print(f" {C.DIM}Load balance loss: {moe_lb:.4f}{C.RESET}")
446
+ print(f" {C.DIM}Per-expert loads not in top-level stats.{C.RESET}")
447
+ print(f" {C.DIM}They exist as assoc_N_expert_N_load β€” fixing...{C.RESET}")
448
+
449
+ if moe_entropy is not None:
450
+ if isinstance(moe_entropy, torch.Tensor): moe_entropy = moe_entropy.item()
451
+ print(f" Entropy: {moe_entropy:.3f}")
452
+
453
+ mem_rate = stats.get("memory_spike_rate", None)
454
+ if mem_rate is not None:
455
+ if isinstance(mem_rate, torch.Tensor): mem_rate = mem_rate.item()
456
+ gate = stats.get("gate_activity", 0)
457
+ mix = stats.get("memory_mix", 0)
458
+ if isinstance(gate, torch.Tensor): gate = gate.item()
459
+ if isinstance(mix, torch.Tensor): mix = mix.item()
460
+ bar = spike_bar(mem_rate * 0.3, 15, C.PURPLE)
461
+ print(f"\n {C.BOLD}Memory Cortex:{C.RESET}")
462
+ print(f" {C.PURPLE}Spike rate:{C.RESET} {mem_rate:.4f} {bar}")
463
+ print(f" {C.PURPLE}Gate:{C.RESET} {gate:.4f}")
464
+ print(f" {C.PURPLE}Mix weight:{C.RESET} {mix:.4f}")
465
+
466
+ sparsity = stats.get("sparsity", 0)
467
+ if isinstance(sparsity, torch.Tensor): sparsity = sparsity.item()
468
+ sp_color = C.GREEN if sparsity > 0.85 else C.YELLOW if sparsity > 0.7 else C.RED
469
+ print(f"\n Overall Sparsity: {sp_color}{sparsity:.1%}{C.RESET}")
470
+ print(f" {C.GREY}{'─' * 50}{C.RESET}")
471
+
472
+
473
+ def main():
474
+ os.system('clear' if os.name != 'nt' else 'cls')
475
+
476
+ print(f"""
477
+ {C.CYAN}╔══════════════════════════════════════════════════════════╗{C.RESET}
478
+ {C.CYAN}β•‘{C.RESET} {C.BOLD}⚑ PROJECT NORD v4.2 β€” Brain-Inspired SNN Chat{C.RESET} {C.CYAN}β•‘{C.RESET}
479
+ {C.CYAN}β•‘{C.RESET} {C.DIM}618M params β”‚ Spike-driven β”‚ Zonal architecture{C.RESET} {C.CYAN}β•‘{C.RESET}
480
+ {C.CYAN}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•{C.RESET}
481
+ """)
482
+
483
+ default_dir = "nord_v4_700m"
484
+ print(f" Model directory?")
485
+ print(f" {C.DIM}(Enter = {default_dir}){C.RESET}")
486
+ model_input = input(" Path: ").strip()
487
+ model_dir = model_input if model_input else default_dir
488
+
489
+ model, tokenizer, cfg = load_model(model_dir)
490
+
491
+ stdp_enabled = False
492
+ live_spikes = False
493
+ max_tokens = 200
494
+ temperature = 0.85
495
+ top_p = 0.9
496
+ rep_penalty = 1.3
497
+ last_stats = {}
498
+
499
+ print(f"\n {C.DIM}Type /help for commands{C.RESET}")
500
+ print(f" {C.GREY}{'─' * 50}{C.RESET}\n")
501
+
502
+ while True:
503
+ try:
504
+ user = input(f" {C.BOLD}You:{C.RESET} ").strip()
505
+ except (EOFError, KeyboardInterrupt):
506
+ print(f"\n {C.DIM}Goodbye!{C.RESET}")
507
+ break
508
+
509
+ if not user:
510
+ continue
511
+
512
+ cmd = user.lower().split()
513
+
514
+ if cmd[0] == "/quit":
515
+ break
516
+ elif cmd[0] == "/help":
517
+ print(f"""
518
+ {C.BOLD}Commands:{C.RESET}
519
+ {C.CYAN}/tokens N{C.RESET} β€” Max response tokens (current: {max_tokens})
520
+ {C.CYAN}/temp F{C.RESET} β€” Temperature (current: {temperature})
521
+ {C.CYAN}/rep F{C.RESET} β€” Repetition penalty (current: {rep_penalty})
522
+ {C.CYAN}/stdp on|off{C.RESET} β€” Toggle online learning ({C.GREEN if stdp_enabled else C.RED}{'ON' if stdp_enabled else 'OFF'}{C.RESET})
523
+ {C.CYAN}/live on|off{C.RESET} β€” Live spike visualization ({C.GREEN if live_spikes else C.RED}{'ON' if live_spikes else 'OFF'}{C.RESET})
524
+ {C.CYAN}/stats{C.RESET} β€” Zone & MoE statistics
525
+ {C.CYAN}/memory{C.RESET} β€” Memory cortex state
526
+ {C.CYAN}/expert{C.RESET} β€” Expert routing breakdown
527
+ {C.CYAN}/reset{C.RESET} β€” Clear working memory
528
+ {C.CYAN}/quit{C.RESET} β€” Exit""")
529
+ continue
530
+ elif cmd[0] == "/tokens":
531
+ if len(cmd) > 1:
532
+ try:
533
+ max_tokens = int(cmd[1])
534
+ print(f" {C.GREEN}[βœ“]{C.RESET} Max tokens: {max_tokens}")
535
+ except ValueError:
536
+ print(f" {C.RED}[βœ—]{C.RESET} Usage: /tokens 300")
537
+ else:
538
+ print(f" Max tokens: {max_tokens}")
539
+ continue
540
+ elif cmd[0] == "/temp":
541
+ if len(cmd) > 1:
542
+ try:
543
+ temperature = float(cmd[1])
544
+ print(f" {C.GREEN}[βœ“]{C.RESET} Temperature: {temperature}")
545
+ except ValueError:
546
+ print(f" {C.RED}[βœ—]{C.RESET} Usage: /temp 0.7")
547
+ else:
548
+ print(f" Temperature: {temperature}")
549
+ continue
550
+ elif cmd[0] == "/rep":
551
+ if len(cmd) > 1:
552
+ try:
553
+ rep_penalty = float(cmd[1])
554
+ print(f" {C.GREEN}[βœ“]{C.RESET} Repetition penalty: {rep_penalty}")
555
+ except ValueError:
556
+ print(f" {C.RED}[βœ—]{C.RESET} Usage: /rep 1.3")
557
+ else:
558
+ print(f" Repetition penalty: {rep_penalty}")
559
+ continue
560
+ elif cmd[0] == "/stdp":
561
+ if len(cmd) > 1 and cmd[1] == "on":
562
+ stdp_enabled = True
563
+ print(f" {C.GREEN}[βš™] STDP enabled{C.RESET}")
564
+ elif len(cmd) > 1 and cmd[1] == "off":
565
+ stdp_enabled = False
566
+ print(f" {C.YELLOW}[βš™] STDP disabled{C.RESET}")
567
+ else:
568
+ print(f" STDP: {'ON' if stdp_enabled else 'OFF'}")
569
+ continue
570
+ elif cmd[0] == "/live":
571
+ if len(cmd) > 1 and cmd[1] == "on":
572
+ live_spikes = True
573
+ print(f" {C.GREEN}[βš™] Live spike visualization ON{C.RESET}")
574
+ elif len(cmd) > 1 and cmd[1] == "off":
575
+ live_spikes = False
576
+ print(f" {C.YELLOW}[βš™] Live spike visualization OFF{C.RESET}")
577
+ else:
578
+ print(f" Live spikes: {'ON' if live_spikes else 'OFF'}")
579
+ continue
580
+ elif cmd[0] == "/stats":
581
+ print_stats(last_stats, cfg)
582
+ continue
583
+ elif cmd[0] == "/memory":
584
+ mem_rate = last_stats.get("memory_spike_rate", "N/A")
585
+ gate = last_stats.get("gate_activity", "N/A")
586
+ mix = last_stats.get("memory_mix", "N/A")
587
+ if isinstance(mem_rate, torch.Tensor): mem_rate = f"{mem_rate.item():.4f}"
588
+ if isinstance(gate, torch.Tensor): gate = f"{gate.item():.4f}"
589
+ if isinstance(mix, torch.Tensor): mix = f"{mix.item():.4f}"
590
+ print(f" {C.PURPLE}Memory:{C.RESET} rate={mem_rate}, gate={gate}, mix={mix}")
591
+ continue
592
+ elif cmd[0] == "/expert":
593
+ # Search all stats keys for expert load data
594
+ expert_data = {k: v for k, v in last_stats.items() if "expert" in k and "load" in k}
595
+ if expert_data:
596
+ for k, v in sorted(expert_data.items()):
597
+ if isinstance(v, torch.Tensor): v = v.item()
598
+ bar = spike_bar(v, 30, C.YELLOW, max_rate=0.5)
599
+ # Clean up key name for display
600
+ display_name = k.replace("assoc_", "A").replace("_load", "")
601
+ print(f" {display_name}: {v:.2%} {bar}")
602
+ else:
603
+ print(f" {C.DIM}No expert load data in stats{C.RESET}")
604
+ moe_keys = [k for k in last_stats.keys() if "moe" in k or "expert" in k]
605
+ if moe_keys:
606
+ print(f" {C.DIM}Related keys: {moe_keys}{C.RESET}")
607
+ continue
608
+ elif cmd[0] == "/reset":
609
+ model.reset_state()
610
+ print(f" {C.GREEN}[βš™] Working memory cleared{C.RESET}")
611
+ continue
612
+
613
+ last_stats = generate_streaming(
614
+ model, tokenizer, cfg, user,
615
+ max_tokens=max_tokens,
616
+ temperature=temperature,
617
+ top_p=top_p,
618
+ repetition_penalty=rep_penalty,
619
+ enable_stdp=stdp_enabled,
620
+ live_spikes=live_spikes,
621
+ )
622
+
623
+
624
+ if __name__ == "__main__":
625
+ main()
nord_v4_700m-4.2/download_data.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD β€” ЗавантаТСння датасСтів β•‘
4
+ β•‘ β•‘
5
+ β•‘ ΠŸΡ€ΠΎΡΡ‚ΠΎ запусти: python download_data.py β•‘
6
+ β•‘ β•‘
7
+ β•‘ ДатасСти для Ρ€Ρ–Π·Π½ΠΈΡ… Ρ„Π°Π· навчання: β•‘
8
+ β•‘ 1. FineWeb-Edu β€” Π·Π°Π³Π°Π»ΡŒΠ½Ρ– освітні тСксти (Π±Π°Π·Π°) β•‘
9
+ β•‘ 2. OpenWebMath β€” ΠΌΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΠ° Ρ– reasoning β•‘
10
+ β•‘ 3. The Stack v2 β€” ΠΊΠΎΠ΄ (Python, JS, C++ Ρ‚Π° Ρ–Π½ΡˆΡ–) β•‘
11
+ β•‘ 4. peS2o β€” Π½Π°ΡƒΠΊΠΎΠ²Ρ– статті β•‘
12
+ β•‘ 5. OpenHermes 2.5 β€” інструкції (chat/assistant Ρ„ΠΎΡ€ΠΌΠ°Ρ‚) β•‘
13
+ β•‘ 6. SlimPajama β€” Ρ€Ρ–Π·Π½ΠΎΠΌΠ°Π½Ρ–Ρ‚Π½ΠΈΠΉ Π²Π΅Π±-тСкст β•‘
14
+ β•‘ 7. Wikipedia β€” Π΅Π½Ρ†ΠΈΠΊΠ»ΠΎΠΏΠ΅Π΄ΠΈΡ‡Π½Ρ– знання β•‘
15
+ β•‘ 8. Cosmopedia β€” синтСтичні ΠΏΡ–Π΄Ρ€ΡƒΡ‡Π½ΠΈΠΊΠΈ β•‘
16
+ β•‘ β•‘
17
+ β•‘ ΠŸΠΎΡ‚Ρ€Ρ–Π±Π½ΠΎ: pip install datasets tqdm β•‘
18
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
19
+ """
20
+
21
+ import json, os, sys, time
22
+
23
+ DATASETS = {
24
+ "1": {
25
+ "name": "FineWeb-Edu (освітні тСксти)",
26
+ "desc": "Високоякісні освітні тСксти. НайкращС для Π±Π°Π·ΠΎΠ²ΠΎΠ³ΠΎ навчання.",
27
+ "hf_id": "HuggingFaceFW/fineweb-edu", "hf_name": "sample-10BT",
28
+ "split": "train", "field": "text", "gb": 40,
29
+ "phase": "Π€Π°Π·Π° 1 β€” Π‘Π°Π·ΠΎΠ²Π° ΠΌΠΎΠ²Π°",
30
+ },
31
+ "2": {
32
+ "name": "OpenWebMath (ΠΌΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΠ°)",
33
+ "desc": "ΠœΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΠ°: Ρ„ΠΎΡ€ΠΌΡƒΠ»ΠΈ, Π΄ΠΎΠΊΠ°Π·ΠΈ, Π·Π°Π΄Π°Ρ‡Ρ–. Для reasoning.",
34
+ "hf_id": "open-web-math/open-web-math", "hf_name": None,
35
+ "split": "train", "field": "text", "gb": 15,
36
+ "phase": "Π€Π°Π·Π° 2 β€” Reasoning Ρ– ΠΌΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΠ°",
37
+ },
38
+ "3": {
39
+ "name": "StarCoder Data (ΠΊΠΎΠ΄)",
40
+ "desc": "Код Π½Π° Python, JS, C++, Java Ρ‚Π° Ρ–Π½ΡˆΠΈΡ… ΠΌΠΎΠ²Π°Ρ….",
41
+ "hf_id": "bigcode/starcoderdata", "hf_name": None,
42
+ "split": "train", "field": "content", "gb": 20,
43
+ "phase": "Π€Π°Π·Π° 3 β€” ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΡƒΠ²Π°Π½Π½Ρ",
44
+ },
45
+ "4": {
46
+ "name": "peS2o (Π½Π°ΡƒΠΊΠΎΠ²Ρ– статті)",
47
+ "desc": "Наукові papers Π²Ρ–Π΄ Semantic Scholar.",
48
+ "hf_id": "allenai/peS2o", "hf_name": "v2",
49
+ "split": "train", "field": "text", "gb": 20,
50
+ "phase": "Π€Π°Π·Π° 4 β€” Наукові тСксти",
51
+ },
52
+ "5": {
53
+ "name": "OpenHermes 2.5 (інструкції)",
54
+ "desc": "Chat/Assistant Ρ„ΠΎΡ€ΠΌΠ°Ρ‚. ΠŸΠ΅Ρ€Π΅Ρ‚Π²ΠΎΡ€ΡŽΡ” base model Π² chat bot.",
55
+ "hf_id": "teknium/OpenHermes-2.5", "hf_name": None,
56
+ "split": "train", "field": "conversations", "gb": 2,
57
+ "phase": "Π€Π°Π·Π° 5 β€” Інструкції (chat)", "is_chat": True,
58
+ },
59
+ "6": {
60
+ "name": "SlimPajama (Ρ€Ρ–Π·Π½ΠΎΠΌΠ°Π½Ρ–Ρ‚Π½ΠΈΠΉ тСкст)",
61
+ "desc": "Π—ΠΌΡ–ΡˆΠ°Π½ΠΈΠΉ: Π²Π΅Π±, ΠΊΠ½ΠΈΠ³ΠΈ, Wikipedia, GitHub.",
62
+ "hf_id": "cerebras/SlimPajama-627B", "hf_name": None,
63
+ "split": "train", "field": "text", "gb": 30,
64
+ "phase": "ΠΠ»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π° β€” Π Ρ–Π·Π½ΠΎΠΌΠ°Π½Ρ–Ρ‚Π½ΠΈΠΉ тСкст",
65
+ },
66
+ "7": {
67
+ "name": "Wikipedia (СнциклопСдія)",
68
+ "desc": "Вся Π°Π½Π³Π»Ρ–ΠΉΡΡŒΠΊΠ° Wikipedia. Чисті Ρ„Π°ΠΊΡ‚ΠΈ.",
69
+ "hf_id": "wikimedia/wikipedia", "hf_name": "20231101.en",
70
+ "split": "train", "field": "text", "gb": 6,
71
+ "phase": "Π”ΠΎΠ΄Π°Ρ‚ΠΎΠΊ β€” Π•Π½Ρ†ΠΈΠΊΠ»ΠΎΠΏΠ΅Π΄ΠΈΡ‡Π½Ρ– знання",
72
+ },
73
+ "8": {
74
+ "name": "Cosmopedia (синтСтичні ΠΏΡ–Π΄Ρ€ΡƒΡ‡Π½ΠΈΠΊΠΈ)",
75
+ "desc": "AI-Π·Π³Π΅Π½Π΅Ρ€ΠΎΠ²Π°Π½Ρ– освітні тСксти Ρƒ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ– ΠΏΡ–Π΄Ρ€ΡƒΡ‡Π½ΠΈΠΊΡ–Π².",
76
+ "hf_id": "HuggingFaceTB/cosmopedia", "hf_name": None,
77
+ "split": "train", "field": "text", "gb": 15,
78
+ "phase": "Π”ΠΎΠ΄Π°Ρ‚ΠΎΠΊ β€” Π‘ΠΈΠ½Ρ‚Π΅Ρ‚ΠΈΡ‡Π½Ρ– освітні тСксти",
79
+ },
80
+ }
81
+
82
+ def fmt(b):
83
+ for u in ["B","KB","MB","GB","TB"]:
84
+ if b < 1024: return f"{b:.1f} {u}"
85
+ b /= 1024
86
+ return f"{b:.1f} PB"
87
+
88
+ def format_chat(convs):
89
+ if isinstance(convs, str): return convs
90
+ parts = []
91
+ for m in convs:
92
+ role = m.get("from", m.get("role", "user"))
93
+ text = m.get("value", m.get("content", ""))
94
+ if role in ("system","human","user"): parts.append(f"User: {text}")
95
+ elif role in ("gpt","assistant"): parts.append(f"Assistant: {text}")
96
+ return "\n".join(parts)
97
+
98
+ def download_one(ds, save_dir, target_gb=None):
99
+ if target_gb is None: target_gb = ds["gb"]
100
+ target_bytes = int(target_gb * (1024**3))
101
+ safe = ds["hf_id"].split("/")[-1].replace("-","_").lower()
102
+ path = os.path.join(save_dir, f"{safe}.jsonl")
103
+
104
+ print(f"\n {'═'*55}")
105
+ print(f" πŸ“¦ {ds['name']}")
106
+ print(f" πŸ“ {path}")
107
+ print(f" 🎯 {target_gb:.0f} GB")
108
+ print(f" {'═'*55}")
109
+
110
+ os.makedirs(save_dir, exist_ok=True)
111
+ written = 0; count = 0; mode = "w"
112
+
113
+ if os.path.exists(path):
114
+ sz = os.path.getsize(path)
115
+ if sz >= target_bytes:
116
+ print(f" [βœ“] Π’ΠΆΠ΅ Ρ”! ({fmt(sz)})"); return path
117
+ if sz > 0:
118
+ written = sz
119
+ with open(path,"r",encoding="utf-8") as f: count = sum(1 for _ in f)
120
+ mode = "a"
121
+ print(f" [*] ΠŸΡ€ΠΎΠ΄ΠΎΠ²ΠΆΡƒΡ”ΠΌΠΎ Π· {fmt(written)} ({count:,} Π·Ρ€Π°Π·ΠΊΡ–Π²)")
122
+
123
+ print(f" [*] ΠŸΡ–Π΄ΠΊΠ»ΡŽΡ‡Π°Ρ”ΠΌΠΎΡΡ Π΄ΠΎ HuggingFace...")
124
+ try:
125
+ from datasets import load_dataset
126
+ except ImportError:
127
+ print(" [βœ—] pip install datasets"); return None
128
+
129
+ kw = {"path": ds["hf_id"], "split": ds["split"], "streaming": True}
130
+ if ds.get("hf_name"): kw["name"] = ds["hf_name"]
131
+
132
+ try:
133
+ data = load_dataset(**kw)
134
+ except Exception as e:
135
+ print(f" [βœ—] Помилка: {e}"); return None
136
+
137
+ it = iter(data)
138
+ is_chat = ds.get("is_chat", False)
139
+ field = ds["field"]
140
+
141
+ if count > 0:
142
+ print(f" [*] ΠŸΡ€ΠΎΠΏΡƒΡΠΊΠ°Ρ”ΠΌΠΎ {count:,} Π·Ρ€Π°Π·ΠΊΡ–Π²...")
143
+ for _ in range(count):
144
+ try: next(it)
145
+ except StopIteration: break
146
+
147
+ print(f" [*] Записуємо... (Ctrl+C = ΠΏΠ°ΡƒΠ·Π°)")
148
+ t0 = time.time(); lp = t0; start_b = written
149
+
150
+ try:
151
+ with open(path, mode, encoding="utf-8") as f:
152
+ for sample in it:
153
+ if is_chat:
154
+ text = format_chat(sample.get(field, []))
155
+ else:
156
+ text = sample.get(field, "")
157
+ if not text or len(text) < 50: continue
158
+
159
+ line = json.dumps({"text": text}, ensure_ascii=False) + "\n"
160
+ lb = len(line.encode("utf-8"))
161
+ f.write(line); written += lb; count += 1
162
+
163
+ now = time.time()
164
+ if now - lp >= 2.0:
165
+ el = now - t0
166
+ spd = (written - start_b) / el if el > 0 else 0
167
+ pct = written / target_bytes * 100
168
+ fl = int(30 * min(pct,100) / 100)
169
+ bar = "β–ˆ"*fl + "β–‘"*(30-fl)
170
+ eta = (target_bytes - written) / spd if spd > 0 else 0
171
+ es = f"{eta/60:.0f}Ρ…Π²" if eta < 3600 else f"{eta/3600:.1f}Π³ΠΎΠ΄"
172
+ print(f"\r [{bar}] {pct:.1f}% {fmt(written)}/{fmt(target_bytes)} {count:,} Π·Ρ€. {fmt(int(spd))}/s ETA {es} ", end="", flush=True)
173
+ lp = now
174
+ if count % 10000 == 0: f.flush()
175
+
176
+ if written >= target_bytes: break
177
+
178
+ except KeyboardInterrupt:
179
+ print(f"\n [⏸] ΠŸΠ°ΡƒΠ·Π°: {fmt(written)} ({count:,} Π·Ρ€.)")
180
+ return path
181
+
182
+ el = time.time() - t0
183
+ print(f"\n [βœ“] {ds['name']}: {fmt(written)} | {count:,} Π·Ρ€. | {el/60:.0f}Ρ…Π²")
184
+ return path
185
+
186
+ def main():
187
+ print("=" * 60)
188
+ print(" PROJECT NORD β€” ЗавантаТСння датасСтів")
189
+ print("=" * 60)
190
+ print()
191
+ print(" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”")
192
+ for k, ds in DATASETS.items():
193
+ print(f" β”‚ [{k}] {ds['name']:<45}β”‚")
194
+ print(f" β”‚ {ds['phase']:<41} ~{ds['gb']:>2}GB β”‚")
195
+ print(f" β”‚ β”‚")
196
+ print(f" β”‚ [A] Π—Π°Π²Π°Π½Ρ‚Π°ΠΆΠΈΡ‚ΠΈ Π’Π‘Π• (Π€Π°Π·ΠΈ 1-5) β”‚")
197
+ print(f" β”‚ [M] ΠšΡ–Π»ΡŒΠΊΠ° (Ρ‡Π΅Ρ€Π΅Π· ΠΊΠΎΠΌΡƒ: 1,2,5) β”‚")
198
+ print(f" β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜")
199
+ print()
200
+
201
+ choice = input(" Π’ΠΈΠ±Π΅Ρ€ΠΈ: ").strip().upper()
202
+
203
+ default_dir = os.path.join(os.sep, "nord_dataset")
204
+ print(f"\n Папка? (Enter = {default_dir})")
205
+ di = input(" Папка: ").strip()
206
+ save_dir = di if di else default_dir
207
+
208
+ if choice == "A":
209
+ for k in ["1","2","4","5","7"]: download_one(DATASETS[k], save_dir)
210
+ elif choice == "M":
211
+ nums = input(" НомСри (Ρ‡Π΅Ρ€Π΅Π· ΠΊΠΎΠΌΡƒ): ").strip()
212
+ for k in [x.strip() for x in nums.split(",")]:
213
+ if k in DATASETS: download_one(DATASETS[k], save_dir)
214
+ else: print(f" [!] НСвідомий: {k}")
215
+ elif choice in DATASETS:
216
+ ds = DATASETS[choice]
217
+ print(f"\n {ds['desc']}")
218
+ print(f" Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΠΎΠ²Π°Π½ΠΎ: {ds['gb']}GB")
219
+ print(f" Π‘ΠΊΡ–Π»ΡŒΠΊΠΈ GB? (Enter = {ds['gb']})")
220
+ si = input(" GB: ").strip()
221
+ gb = float(si) if si else ds["gb"]
222
+ download_one(ds, save_dir, gb)
223
+ else:
224
+ print(f" [!] НСвідомий Π²ΠΈΠ±Ρ–Ρ€: {choice}"); return
225
+
226
+ print(f"\n {'═'*55}")
227
+ print(f" [βœ“] Π“ΠžΠ’ΠžΠ’Πž! ДатасСти Π²: {save_dir}")
228
+ print(f" {'═'*55}")
229
+ print(f"\n Π―ΠΊ Ρ‚Ρ€Π΅Π½ΡƒΠ²Π°Ρ‚ΠΈ:")
230
+ print(f" Π‘Π°Π·ΠΎΠ²Π΅: python train_nord_700m.py --dataset {save_dir}/fineweb_edu.jsonl")
231
+ print(f" ΠœΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΠ°: python train_nord_700m.py --dataset {save_dir}/open_web_math.jsonl --continued")
232
+ print(f" Код: python train_nord_700m.py --dataset {save_dir}/starcoderdata.jsonl --continued")
233
+ print(f" Наука: python train_nord_700m.py --dataset {save_dir}/pes2o.jsonl --continued")
234
+ print(f" Chat: python train_nord_700m.py --dataset {save_dir}/openhermes_2.5.jsonl --continued")
235
+ print()
236
+
237
+ if __name__ == "__main__":
238
+ main()
nord_v4_700m-4.2/fast_tokenize.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD β€” Fast LMDB Tokenizer β•‘
4
+ β•‘ β•‘
5
+ β•‘ Usage: β•‘
6
+ β•‘ python build_lmdb.py (interactive) β•‘
7
+ β•‘ python build_lmdb.py --src data.jsonl (auto) β•‘
8
+ β•‘ python build_lmdb.py --src data.jsonl --dst out_lmdb --seq 512 β•‘
9
+ β•‘ β•‘
10
+ β•‘ Batch tokenization with progress bar and resume support β•‘
11
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
12
+ """
13
+
14
+ import argparse, json, struct, time, os, sys
15
+
16
+ def build_lmdb(src, dst, seq_len=512, batch_size=1024):
17
+ import lmdb
18
+ import numpy as np
19
+ from transformers import AutoTokenizer
20
+
21
+ print("=" * 60, flush=True)
22
+ print(" PROJECT NORD β€” Fast LMDB Tokenizer", flush=True)
23
+ print("=" * 60, flush=True)
24
+ print(f" Source: {src}", flush=True)
25
+ print(f" Output: {dst}", flush=True)
26
+ print(f" Seq len: {seq_len}", flush=True)
27
+ print(f" Batch: {batch_size}", flush=True)
28
+ print(flush=True)
29
+
30
+ # ── Check if already exists ──
31
+ if os.path.exists(dst):
32
+ try:
33
+ env = lmdb.open(dst, readonly=True, lock=False)
34
+ with env.begin(write=False) as txn:
35
+ existing = struct.unpack("<Q", txn.get(b"__len__"))[0]
36
+ existing_tok = struct.unpack("<Q", txn.get(b"__total_tokens__"))[0]
37
+ env.close()
38
+ print(f" [!] LMDB already exists: {existing:,} samples, {existing_tok/1e6:.0f}M tokens", flush=True)
39
+ print(f" Overwrite? (y/n, Enter = n)")
40
+ choice = input(" > ").strip().lower()
41
+ if choice not in ("y", "yes"):
42
+ print(" [*] Skipped.", flush=True)
43
+ return dst
44
+ import shutil
45
+ shutil.rmtree(dst)
46
+ print(" [*] Deleted old LMDB.", flush=True)
47
+ except:
48
+ pass
49
+
50
+ # ── Init tokenizer ──
51
+ print(" [*] Loading tokenizer...", flush=True)
52
+ tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B")
53
+ if tok.pad_token is None:
54
+ tok.pad_token = tok.eos_token
55
+ PAD_ID = tok.pad_token_id
56
+ print(f" [βœ“] Tokenizer ready (vocab={tok.vocab_size:,})", flush=True)
57
+
58
+ # ── Read all texts ──
59
+ print(f"\n [1/3] Reading JSONL into memory...", flush=True)
60
+ t0 = time.time()
61
+ texts = []
62
+ with open(src, "r", encoding="utf-8") as f:
63
+ for i, line in enumerate(f):
64
+ if i % 1_000_000 == 0 and i > 0:
65
+ print(f" read {i:,} lines...", flush=True)
66
+ line = line.strip()
67
+ if not line:
68
+ continue
69
+ try:
70
+ obj = json.loads(line)
71
+ except:
72
+ continue
73
+ text = obj.get("text") or obj.get("content") or obj.get("passage", "")
74
+ if len(text) >= 30:
75
+ texts.append(text)
76
+ print(f" {len(texts):,} valid texts in {time.time()-t0:.0f}s", flush=True)
77
+
78
+ if not texts:
79
+ print(" [βœ—] No valid texts found!", flush=True)
80
+ return None
81
+
82
+ # ── Batch tokenize ──
83
+ print(f"\n [2/3] Batch tokenizing {len(texts):,} texts (batch={batch_size})...", flush=True)
84
+ t1 = time.time()
85
+
86
+ os.makedirs(os.path.dirname(dst) if os.path.dirname(dst) else ".", exist_ok=True)
87
+ env = lmdb.open(dst, map_size=80 * (1024**3))
88
+ txn = env.begin(write=True)
89
+
90
+ count = 0
91
+ total_tok = 0
92
+ total_batches = (len(texts) + batch_size - 1) // batch_size
93
+
94
+ for batch_idx in range(0, len(texts), batch_size):
95
+ batch = texts[batch_idx : batch_idx + batch_size]
96
+ batch_num = batch_idx // batch_size + 1
97
+
98
+ enc = tok(
99
+ batch,
100
+ max_length=seq_len,
101
+ truncation=True,
102
+ padding="max_length",
103
+ return_tensors="np",
104
+ return_attention_mask=False,
105
+ )
106
+ ids_np = enc.input_ids.astype(np.int32)
107
+
108
+ for j in range(ids_np.shape[0]):
109
+ row = ids_np[j]
110
+ non_pad = int(np.sum(row != PAD_ID))
111
+ if non_pad < 10:
112
+ continue
113
+ txn.put(f"sample_{count:010d}".encode(), row.tobytes())
114
+ count += 1
115
+ total_tok += non_pad
116
+
117
+ # Progress
118
+ if batch_num % 100 == 0 or batch_num == total_batches:
119
+ elapsed = time.time() - t1
120
+ pct = batch_num / total_batches * 100
121
+ eta = (elapsed / batch_num) * (total_batches - batch_num)
122
+ speed = count / elapsed if elapsed > 0 else 0
123
+ bar_len = 30
124
+ filled = int(bar_len * pct / 100)
125
+ bar = "β–ˆ" * filled + "β–‘" * (bar_len - filled)
126
+ print(
127
+ f" [{bar}] {pct:5.1f}% | "
128
+ f"{count:,} samples | {total_tok/1e6:.0f}M tok | "
129
+ f"{speed:.0f} doc/s | ETA {eta:.0f}s",
130
+ flush=True,
131
+ )
132
+
133
+ # Commit every 500k
134
+ if count % 500_000 < batch_size and count >= 500_000:
135
+ txn.commit()
136
+ txn = env.begin(write=True)
137
+
138
+ # Save metadata
139
+ txn.put(b"__len__", struct.pack("<Q", count))
140
+ txn.put(b"__total_tokens__", struct.pack("<Q", total_tok))
141
+ txn.commit()
142
+ env.close()
143
+
144
+ elapsed = time.time() - t1
145
+ print(f"\n [3/3] Done!", flush=True)
146
+ print(f" {'═' * 50}", flush=True)
147
+ print(f" Samples: {count:,}", flush=True)
148
+ print(f" Tokens: {total_tok:,} ({total_tok/1e6:.0f}M)", flush=True)
149
+ print(f" Time: {elapsed:.0f}s ({elapsed/60:.1f} min)", flush=True)
150
+ print(f" Speed: {count/elapsed:.0f} doc/s", flush=True)
151
+ print(f" {'═' * 50}", flush=True)
152
+ print(f"\n Π’Π΅ΠΏΠ΅Ρ€ Ρ‚Ρ€Π΅Π½ΡƒΠΉ:", flush=True)
153
+ print(f" python train_nord_700m.py --dataset {src}", flush=True)
154
+ print(flush=True)
155
+ return dst
156
+
157
+
158
+ def main():
159
+ parser = argparse.ArgumentParser(description="Nord LMDB Tokenizer")
160
+ parser.add_argument("--src", type=str, default=None, help="Source JSONL file")
161
+ parser.add_argument("--dst", type=str, default=None, help="Output LMDB directory")
162
+ parser.add_argument("--seq", type=int, default=512, help="Max sequence length (default: 512)")
163
+ parser.add_argument("--batch", type=int, default=1024, help="Batch size (default: 1024)")
164
+ args = parser.parse_args()
165
+
166
+ # Interactive mode if no args
167
+ if args.src is None:
168
+ print("=" * 60)
169
+ print(" PROJECT NORD β€” Fast LMDB Tokenizer")
170
+ print("=" * 60)
171
+ print()
172
+ print(" Шлях Π΄ΠΎ JSONL датасСту?")
173
+ print(" (Π½Π°ΠΏΡ€ΠΈΠΊΠ»Π°Π΄: /nord_dataset/train_data.jsonl)")
174
+ args.src = input(" Source: ").strip()
175
+ if not args.src:
176
+ print(" [βœ—] ΠŸΠΎΡ‚Ρ€Ρ–Π±Π½ΠΎ Π²ΠΊΠ°Π·Π°Ρ‚ΠΈ ΡˆΠ»ΡΡ…!", flush=True)
177
+ sys.exit(1)
178
+
179
+ if not os.path.exists(args.src):
180
+ print(f" [βœ—] Π€Π°ΠΉΠ» Π½Π΅ Π·Π½Π°ΠΉΠ΄Π΅Π½ΠΎ: {args.src}", flush=True)
181
+ sys.exit(1)
182
+
183
+ if args.dst is None:
184
+ # Auto: same path but _lmdb suffix
185
+ args.dst = args.src.replace(".jsonl", "") + "_lmdb"
186
+ print(f"\n Output LMDB? (Enter = {args.dst})")
187
+ user_dst = input(" Output: ").strip()
188
+ if user_dst:
189
+ args.dst = user_dst
190
+
191
+ print(f"\n Sequence length? (Enter = {args.seq})")
192
+ seq_input = input(" Seq: ").strip()
193
+ if seq_input:
194
+ args.seq = int(seq_input)
195
+
196
+ print()
197
+ build_lmdb(args.src, args.dst, args.seq, args.batch)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
nord_v4_700m-4.2/nord_core_700m.py ADDED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD β€” Core Engine v4.2 (700M) β•‘
4
+ β•‘ Spiking Neural Network LLM with Brain-Inspired Architecture β•‘
5
+ β•‘ β•‘
6
+ β•‘ v4.1 CRITICAL FIXES (from code review): β•‘
7
+ β•‘ FIX A: Vectorized MoE dispatch β€” no Python loops over experts β•‘
8
+ β•‘ FIX B: Temporal attention memory β€” multi-head read over ALL timesteps β•‘
9
+ β•‘ FIX C: Differentiable spike loss β€” proper gradient flow β•‘
10
+ β•‘ FIX D: LIF stability β€” clamped tau/threshold, warmup freeze β•‘
11
+ β•‘ FIX E: Temporal mixing in attention (no naive T*Dh flattening) β•‘
12
+ β•‘ FIX F: STDP isolation β€” only executive zone, bounded magnitude β•‘
13
+ β•‘ FIX G: MoE load balancing loss β€” prevents expert collapse β•‘
14
+ β•‘ FIX H: Gradient checkpointing support β€” VRAM control β•‘
15
+ β•‘ FIX I: Fused LIF operations β€” reduced kernel launch overhead β•‘
16
+ β•‘ FIX J: Realistic training estimates in docs β•‘
17
+ β•‘ β•‘
18
+ β•‘ v4.2 FIXES (from 13K step training analysis): β•‘
19
+ β•‘ FIX K: Block outputs spike-only β€” clamp negative before spike_ts β•‘
20
+ β•‘ FIX L: Stronger spike regulator β€” adaptive weight, per-layer targeting β•‘
21
+ β•‘ FIX M: Executive clamp floor=0 β€” prevent negative spike propagation β•‘
22
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
23
+ """
24
+
25
+ from __future__ import annotations
26
+ import math, torch, torch.nn as nn, torch.nn.functional as F
27
+ from torch import Tensor
28
+ from torch.utils.checkpoint import checkpoint as grad_checkpoint
29
+ from dataclasses import dataclass
30
+ from typing import Dict, Tuple, Optional, List
31
+
32
+ # ═══════════════════════════════════════════════════════════════════════════════
33
+ # Β§0 CONFIG
34
+ # ═══════════════════════════════════════════════════════════════════════════════
35
+ @dataclass
36
+ class NordConfig:
37
+ tokenizer_id:str="meta-llama/Llama-3.2-1B"
38
+ # ── 700M Architecture ──
39
+ vocab_size:int=128_256; d_model:int=1536; n_heads:int=24; n_layers:int=10
40
+ d_ff:int=4096; max_seq_len:int=512
41
+ T:int=8; T_slow:int=2; persistent_mem:bool=True
42
+ # LIF β€” FIX D: constrained ranges
43
+ tau_mem:float=0.9; tau_mem_min:float=0.8; tau_mem_max:float=0.98
44
+ tau_syn:float=0.50; v_threshold:float=0.12
45
+ v_thresh_min:float=0.05; v_thresh_max:float=0.5
46
+ v_reset:float=-0.1; refractory_t:int=2; threshold_lr:float=0.01
47
+ lif_freeze_steps:int=1000
48
+ n_clusters:int=128; cascade_radius:int=3; cascade_gain:float=0.8
49
+ # STDP β€” FIX F: bounded
50
+ stdp_a_plus:float=0.005; stdp_a_minus:float=0.005
51
+ stdp_tau_plus:float=20.0; stdp_tau_minus:float=20.0
52
+ stdp_w_max:float=0.5; stdp_w_min:float=-0.15
53
+ stdp_reward_scale:float=1.0; stdp_layers:Optional[List[str]]=None
54
+ resonance_top_k:int=64; clamp_floor:float=-0.1; surrogate_alpha:float=4.0
55
+ rope_theta:float=10000.0
56
+ # MoE β€” FIX A+G
57
+ n_experts:int=4; top_k_experts:int=2; moe_capacity_factor:float=1.25
58
+ moe_load_balance_weight:float=0.01; moe_route_temperature:float=1.0
59
+ # Spike loss β€” FIX L
60
+ target_spike_rate:float=0.03; spike_loss_weight:float=0.5
61
+ # Zones: 3 sensory + 3 association(MoE) + 4 executive = 10
62
+ sensory_layers:int=3; association_layers:int=3; executive_layers:int=4
63
+ # Memory β€” FIX B
64
+ memory_tau_mem:float=0.99; memory_size:int=256
65
+ memory_gate_threshold:float=0.3; memory_n_read_heads:int=8
66
+ # FIX H
67
+ gradient_checkpointing:bool=False
68
+ # Training
69
+ batch_size:int=1; grad_accum:int=32; lr:float=2e-4; min_lr:float=1e-5
70
+ weight_decay:float=0.01; warmup_steps:int=1000; max_steps:int=50_000
71
+ save_every:int=1000; log_every:int=10; max_grad_norm:float=1.0
72
+ dtype:torch.dtype=torch.float16; device:str="cuda"
73
+ @property
74
+ def T_total(self)->int: return self.T+self.T_slow
75
+ @property
76
+ def n_layers_total(self)->int: return self.sensory_layers+self.association_layers+self.executive_layers
77
+ def __post_init__(self):
78
+ if self.stdp_layers is None:
79
+ self.stdp_layers=[f"executive_{i}" for i in range(self.executive_layers)]
80
+
81
+ # ═══════════════════════════════════════════════════════════════════════════════
82
+ # Β§1 SURROGATE GRADIENT
83
+ # ═══════════════════════════════════════════════════════════════════════════════
84
+ class ATanSurrogate(torch.autograd.Function):
85
+ alpha=2.0
86
+ @staticmethod
87
+ def forward(ctx,membrane:Tensor,threshold:Tensor)->Tensor:
88
+ ctx.save_for_backward(membrane,threshold)
89
+ return(membrane>=threshold).to(membrane.dtype)
90
+ @staticmethod
91
+ def backward(ctx,grad_output:Tensor)->Tuple[Tensor,Tensor]:
92
+ membrane,threshold=ctx.saved_tensors
93
+ x=(membrane.float()-threshold.float())
94
+ grad=ATanSurrogate.alpha/(2.0*math.pi*(1.0+(ATanSurrogate.alpha*x)**2))
95
+ grad_v=(grad_output.float()*grad).to(membrane.dtype)
96
+ return grad_v,-grad_v
97
+
98
+ def spike_fn(v:Tensor,th:Tensor,alpha:float=2.0)->Tensor:
99
+ ATanSurrogate.alpha=alpha; return ATanSurrogate.apply(v,th)
100
+
101
+ # ═══════════════════════════════════════════════════════════════════════════════
102
+ # Β§2 ASSOCIATIVE LIF β€” FIX D: Stability + FIX I: Fused ops
103
+ # ═══════════════════════════════════════════════════════════════════════════════
104
+ class AssociativeLIF(nn.Module):
105
+ def __init__(self,d:int,cfg:NordConfig,persistent:bool=False,
106
+ tau_mem_override:Optional[float]=None):
107
+ super().__init__()
108
+ self.cfg=cfg; self.d=d; self.persistent=persistent
109
+ self.threshold_raw=nn.Parameter(torch.full((d,),cfg.v_threshold))
110
+ tau_mem=tau_mem_override if tau_mem_override is not None else cfg.tau_mem
111
+ self.beta_mem_raw=nn.Parameter(torch.tensor(math.log(tau_mem/(1-tau_mem+1e-6))))
112
+ self.beta_syn_raw=nn.Parameter(torch.tensor(math.log(cfg.tau_syn/(1-cfg.tau_syn+1e-6))))
113
+ nc=cfg.n_clusters
114
+ self.register_buffer("cluster_ids",torch.arange(d)%nc)
115
+ r=cfg.cascade_radius; idx=torch.arange(nc)
116
+ iw=torch.zeros(nc,nc)
117
+ for offset in range(-r,r+1):
118
+ if offset!=0: iw[idx,(idx+offset)%nc]=1.0-abs(offset)/(r+1)
119
+ self.neighbor_weights=nn.Parameter(iw)
120
+ self.cluster_gain=nn.Parameter(torch.full((nc,),cfg.cascade_gain))
121
+ if persistent:
122
+ self.register_buffer("_v_mem_state",torch.zeros(1,d))
123
+ self.register_buffer("_i_syn_state",torch.zeros(1,d))
124
+ self.register_buffer("_firing_rate_ema",torch.full((d,),cfg.target_spike_rate))
125
+ self.register_buffer("_step_counter",torch.tensor(0,dtype=torch.long))
126
+
127
+ @property
128
+ def threshold(self)->Tensor:
129
+ return self.threshold_raw.clamp(self.cfg.v_thresh_min,self.cfg.v_thresh_max)
130
+ @property
131
+ def beta_mem(self)->Tensor:
132
+ return torch.sigmoid(self.beta_mem_raw).clamp(self.cfg.tau_mem_min,self.cfg.tau_mem_max)
133
+ @property
134
+ def beta_syn(self)->Tensor: return torch.sigmoid(self.beta_syn_raw)
135
+
136
+ def _cascade_amplify(self,spikes:Tensor)->Tensor:
137
+ B,D=spikes.shape; nc=self.cfg.n_clusters
138
+ cid=self.cluster_ids.unsqueeze(0).expand(B,-1)
139
+ cf=torch.zeros(B,nc,device=spikes.device,dtype=spikes.dtype)
140
+ cf.scatter_add_(1,cid,spikes); cf=cf/max(D//nc,1)
141
+ W=torch.sigmoid(self.neighbor_weights)
142
+ ns=(W.to(cf.dtype)@cf.T).T*self.cluster_gain.to(cf.dtype).unsqueeze(0)
143
+ return ns.gather(1,cid)
144
+
145
+ def reset_state(self):
146
+ if self.persistent: self._v_mem_state.zero_(); self._i_syn_state.zero_()
147
+
148
+ def forward(self,current_in:Tensor)->Tuple[Tensor,Tensor]:
149
+ T,B,D=current_in.shape; device=current_in.device; dtype=current_in.dtype
150
+ bm=self.beta_mem; bs=self.beta_syn; thresh=self.threshold
151
+ if self.persistent and self._v_mem_state.shape[0]==B:
152
+ v_mem=self._v_mem_state.clone(); i_syn=self._i_syn_state.clone()
153
+ else:
154
+ v_mem=torch.zeros(B,D,device=device,dtype=dtype)
155
+ i_syn=torch.zeros(B,D,device=device,dtype=dtype)
156
+ if self.persistent:
157
+ self._v_mem_state=torch.zeros(B,D,device=device,dtype=dtype)
158
+ self._i_syn_state=torch.zeros(B,D,device=device,dtype=dtype)
159
+ refrac=torch.zeros(B,D,device=device,dtype=torch.int32)
160
+ spikes_out=[]; v_trace=[]
161
+ refractory_val=torch.full_like(v_mem,self.cfg.v_reset)
162
+ ref_t=self.cfg.refractory_t; alpha=self.cfg.surrogate_alpha
163
+ for t in range(T):
164
+ i_syn=bs*i_syn+current_in[t]
165
+ rmask=(refrac>0)
166
+ new_v=bm*v_mem+(1.0-bm)*i_syn
167
+ v_mem=torch.where(rmask,refractory_val,new_v)
168
+ s=spike_fn(v_mem,thresh,alpha)
169
+ if s.sum()>0: i_syn=i_syn+self._cascade_amplify(s)
170
+ v_mem=v_mem-s*thresh.detach()
171
+ refrac=torch.where(s.bool(),torch.full_like(refrac,ref_t),(refrac-1).clamp(min=0))
172
+ spikes_out.append(s); v_trace.append(v_mem)
173
+ if self.persistent:
174
+ self._v_mem_state=v_mem.detach(); self._i_syn_state=i_syn.detach()
175
+ ss=torch.stack(spikes_out)
176
+ with torch.no_grad():
177
+ self._firing_rate_ema.lerp_(ss.mean(dim=(0,1)),0.01)
178
+ self._step_counter+=1
179
+ return ss,torch.stack(v_trace)
180
+
181
+ # ═══════════════════════════════════════════════════════════════════════════════
182
+ # Β§3 TEMPORAL ENCODER
183
+ # ═══════════════════════════════════════════════════════════════════════════════
184
+ class TemporalSpikeEncoder(nn.Module):
185
+ def __init__(self,cfg:NordConfig):
186
+ super().__init__(); self.cfg=cfg; D=cfg.d_model
187
+ self.embed=nn.Embedding(cfg.vocab_size,D)
188
+ nn.init.kaiming_uniform_(self.embed.weight,a=math.sqrt(5))
189
+ self.temporal_proj=nn.Linear(D,D,bias=False)
190
+ self.drive_scale=nn.Parameter(torch.tensor(25.0))
191
+ self.fast_basis=nn.Parameter(torch.randn(cfg.T,D)*0.02)
192
+ self.slow_basis=nn.Parameter(torch.randn(cfg.T_slow,D)*0.02)
193
+ self.slow_scale=nn.Parameter(torch.tensor(8.0))
194
+ def forward(self,token_ids:Tensor)->Tensor:
195
+ B,S=token_ids.shape; D=self.cfg.d_model
196
+ x=self.temporal_proj(self.embed(token_ids)).reshape(B*S,D)
197
+ fast=torch.sigmoid(self.fast_basis).unsqueeze(1)*x.unsqueeze(0)*self.drive_scale
198
+ slow=torch.sigmoid(self.slow_basis).unsqueeze(1)*x.unsqueeze(0)*self.slow_scale
199
+ return torch.cat([fast,slow],dim=0)
200
+
201
+ # ═══════════════════════════════════════════════════════════════════════════════
202
+ # Β§4 RoPE
203
+ # ═══════════════════════════════════════════════════════════════════════════════
204
+ class RotaryPositionEmbedding(nn.Module):
205
+ def __init__(self,dim:int,max_seq_len:int=2048,theta:float=10000.0):
206
+ super().__init__()
207
+ inv_freq=1.0/(theta**(torch.arange(0,dim,2).float()/dim))
208
+ self.register_buffer("inv_freq",inv_freq)
209
+ t=torch.arange(max_seq_len).float(); freqs=torch.outer(t,inv_freq)
210
+ self.register_buffer("cos_cached",freqs.cos())
211
+ self.register_buffer("sin_cached",freqs.sin())
212
+ def forward(self,x:Tensor,seq_len:int)->Tuple[Tensor,Tensor]:
213
+ return self.cos_cached[:seq_len].to(x.dtype),self.sin_cached[:seq_len].to(x.dtype)
214
+
215
+ def apply_rope(x:Tensor,cos:Tensor,sin:Tensor)->Tensor:
216
+ d=cos.shape[-1]; x1=x[...,:d]; x2=x[...,d:2*d]
217
+ c=cos.unsqueeze(0).unsqueeze(0); s=sin.unsqueeze(0).unsqueeze(0)
218
+ rot=torch.cat([x1*c-x2*s,x1*s+x2*c],dim=-1)
219
+ return torch.cat([rot,x[...,2*d:]],dim=-1) if x.shape[-1]>2*d else rot
220
+
221
+ # ═══════════════════════════════════════════════════════════════════════════════
222
+ # Β§5 SYNAPTIC RESONANCE β€” FIX E: Temporal mixing (not flattening)
223
+ # ═══════════════════════════════════════════════════════════════════════════════
224
+ class SpikingSynapticResonance(nn.Module):
225
+ def __init__(self,cfg:NordConfig):
226
+ super().__init__(); self.cfg=cfg
227
+ self.n_heads=cfg.n_heads; self.d_head=cfg.d_model//cfg.n_heads
228
+ self.top_k=cfg.resonance_top_k; D=cfg.d_model; T_t=cfg.T_total
229
+ self.W_q=nn.Linear(D,D,bias=False); self.W_k=nn.Linear(D,D,bias=False)
230
+ self.W_v=nn.Linear(D,D,bias=False); self.W_o=nn.Linear(D,D,bias=False)
231
+ self.lif_q=AssociativeLIF(D,cfg); self.lif_k=AssociativeLIF(D,cfg)
232
+ self.resonance_temp=nn.Parameter(torch.tensor(1.0/math.sqrt(self.d_head)))
233
+ # FIX E: Learned temporal mixing weights (not concatenation)
234
+ self.temporal_mix_q=nn.Parameter(torch.ones(T_t)/T_t)
235
+ self.temporal_mix_k=nn.Parameter(torch.ones(T_t)/T_t)
236
+ self.rope=RotaryPositionEmbedding(self.d_head,cfg.max_seq_len,cfg.rope_theta)
237
+
238
+ def forward(self,x_spikes:Tensor)->Tensor:
239
+ T_t,B,S,D=x_spikes.shape; H=self.n_heads; Dh=self.d_head
240
+ xf=x_spikes.reshape(T_t*B*S,D)
241
+ qc=self.W_q(xf).reshape(T_t,B*S,D)
242
+ kc=self.W_k(xf).reshape(T_t,B*S,D)
243
+ vr=self.W_v(xf).reshape(T_t,B,S,D)
244
+ qs,_=self.lif_q(qc); ks,_=self.lif_k(kc)
245
+ qs=qs.reshape(T_t,B,S,H,Dh); ks=ks.reshape(T_t,B,S,H,Dh)
246
+ # FIX E: Weighted sum over time, preserves spike timing semantics
247
+ twq=F.softmax(self.temporal_mix_q,dim=0).reshape(T_t,1,1,1,1)
248
+ twk=F.softmax(self.temporal_mix_k,dim=0).reshape(T_t,1,1,1,1)
249
+ qm=(qs*twq).sum(0).permute(0,2,1,3) # (B,H,S,Dh)
250
+ km=(ks*twk).sum(0).permute(0,2,1,3)
251
+ cos,sin=self.rope(qm,S)
252
+ qm=apply_rope(qm,cos,sin); km=apply_rope(km,cos,sin)
253
+ res=torch.matmul(qm,km.transpose(-2,-1))*self.resonance_temp
254
+ cmask=torch.triu(torch.ones(S,S,device=x_spikes.device,dtype=torch.bool),diagonal=1)
255
+ res.masked_fill_(cmask.unsqueeze(0).unsqueeze(0),float("-inf"))
256
+ K=min(self.top_k,S)
257
+ if K<S:
258
+ tv,ti=torch.topk(res,K,dim=-1)
259
+ sr=torch.full_like(res,float("-inf")); sr.scatter_(-1,ti,tv); res=sr
260
+ attn=F.softmax(res.float(),dim=-1).to(res.dtype)
261
+ vm=vr.mean(dim=0).reshape(B,S,H,Dh).permute(0,2,1,3)
262
+ ctx=torch.matmul(attn,vm).permute(0,2,1,3).reshape(B,S,D)
263
+ return self.W_o(ctx).unsqueeze(0).expand(T_t,-1,-1,-1)
264
+
265
+ # ═══════════════════════════════════════════════════════════════════════════════
266
+ # Β§6 SPIKE-DRIVEN MoE β€” FIX A: Vectorized + FIX G: Load Balance
267
+ # ═══════════════════════════════════════════════════════════════════════════════
268
+ class SpikingExpertGroup(nn.Module):
269
+ """FIX A: Memory-efficient expert dispatch using per-expert Linear + masking.
270
+ Instead of bmm with (N,ef,D) tensors, we loop over experts (not tokens).
271
+ With 4 experts this is 4 iterations β€” much better than 2048-token bmm."""
272
+ def __init__(self,cfg:NordConfig):
273
+ super().__init__()
274
+ self.n_experts=cfg.n_experts; self.expert_ff=cfg.d_ff//cfg.n_experts
275
+ D=cfg.d_model; ef=self.expert_ff
276
+ # Standard Linear layers per expert β€” memory efficient
277
+ self.up=nn.ModuleList([nn.Linear(D,ef,bias=False) for _ in range(cfg.n_experts)])
278
+ self.down=nn.ModuleList([nn.Linear(ef,D,bias=False) for _ in range(cfg.n_experts)])
279
+ self.lif1=AssociativeLIF(ef,cfg); self.lif2=AssociativeLIF(D,cfg)
280
+
281
+ def forward(self,x:Tensor,expert_indices:Tensor,expert_weights:Tensor)->Tensor:
282
+ """x:(T,N,D), expert_indices:(N,top_k), expert_weights:(N,top_k)"""
283
+ T,N,D=x.shape; top_k=expert_indices.shape[1]
284
+ output=torch.zeros_like(x)
285
+ # Loop over experts (4 iterations), not tokens (2048)
286
+ for e in range(self.n_experts):
287
+ # Find which tokens use this expert and with what weight
288
+ mask=torch.zeros(N,device=x.device,dtype=x.dtype)
289
+ for k in range(top_k):
290
+ is_e=(expert_indices[:,k]==e).to(x.dtype)
291
+ mask=mask+is_e*expert_weights[:,k]
292
+ if mask.sum()==0: continue
293
+ # Which tokens actually route here
294
+ active=(mask>0)
295
+ if not active.any(): continue
296
+ # Extract active tokens across all timesteps
297
+ active_x=x[:,active,:] # (T, n_active, D)
298
+ Ta,Na,Da=active_x.shape
299
+ # Up projection + LIF
300
+ h=self.up[e](active_x.reshape(Ta*Na,Da)).reshape(Ta,Na,-1)
301
+ h,_=self.lif1(h)
302
+ # Down projection + LIF
303
+ o=self.down[e](h.reshape(Ta*Na,-1)).reshape(Ta,Na,Da)
304
+ o,_=self.lif2(o)
305
+ # Weighted scatter back
306
+ w=mask[active].unsqueeze(0).unsqueeze(-1) # (1,n_active,1)
307
+ output[:,active,:]+=o*w
308
+ return output
309
+
310
+ class SpikeDrivenMoE(nn.Module):
311
+ def __init__(self,cfg:NordConfig):
312
+ super().__init__(); self.cfg=cfg
313
+ self.n_experts=cfg.n_experts; self.top_k=cfg.top_k_experts
314
+ self.clusters_per_expert=cfg.n_clusters//cfg.n_experts
315
+ self.expert_group=SpikingExpertGroup(cfg)
316
+ self.route_lif=AssociativeLIF(cfg.d_model,cfg)
317
+ self.expert_bias=nn.Parameter(torch.zeros(cfg.n_experts))
318
+ self.register_buffer("expert_counts_ema",torch.ones(cfg.n_experts)/cfg.n_experts)
319
+
320
+ def _compute_expert_scores(self,spikes:Tensor)->Tensor:
321
+ fr=spikes.mean(dim=0); N,D=fr.shape; nc=self.cfg.n_clusters
322
+ cid=torch.arange(D,device=fr.device)%nc
323
+ cr=torch.zeros(N,nc,device=fr.device,dtype=fr.dtype)
324
+ cr.scatter_add_(1,cid.unsqueeze(0).expand(N,-1),fr)
325
+ cr=cr/max(D//nc,1)
326
+ es=cr.reshape(N,self.n_experts,self.clusters_per_expert).mean(dim=-1)
327
+ es=es/max(self.cfg.moe_route_temperature,0.01)
328
+ return es+self.expert_bias.to(es.dtype)
329
+
330
+ def _load_balance_loss(self,scores:Tensor,top_idx:Tensor)->Tensor:
331
+ N=scores.shape[0]
332
+ ef=torch.zeros(self.n_experts,device=scores.device)
333
+ for e in range(self.n_experts):
334
+ ef[e]=(top_idx==e).float().sum()/(N*self.top_k)
335
+ rp=F.softmax(scores,dim=-1).mean(dim=0)
336
+ loss=self.n_experts*(ef*rp).sum()
337
+ with torch.no_grad(): self.expert_counts_ema.lerp_(ef,0.01)
338
+ return loss
339
+
340
+ def forward(self,x:Tensor)->Tuple[Tensor,Dict]:
341
+ T,B,S,D=x.shape; N=B*S
342
+ xf=x.reshape(T,N,D); rs,_=self.route_lif(xf)
343
+ es=self._compute_expert_scores(rs)
344
+ ts,ti=torch.topk(es,self.top_k,dim=-1)
345
+ tw=F.softmax(ts.float(),dim=-1).to(x.dtype)
346
+ output=self.expert_group(xf,ti,tw).reshape(T,B,S,D)
347
+ lb=self._load_balance_loss(es,ti)
348
+ stats={"moe_route_entropy":-(F.softmax(es,dim=-1)*F.log_softmax(es+1e-8,dim=-1)).sum(-1).mean().item(),
349
+ "moe_load_balance_loss":lb}
350
+ with torch.no_grad():
351
+ for e in range(self.n_experts): stats[f"expert_{e}_load"]=self.expert_counts_ema[e].item()
352
+ return output,stats
353
+
354
+ # ═══════════════════════════════════════════════════════════════════════════════
355
+ # Β§7 MEMORY CORTEX β€” FIX B: Temporal attention readout
356
+ # ═══════════════════════════════════════════════════════════════════════════════
357
+ class MemoryCortex(nn.Module):
358
+ def __init__(self,cfg:NordConfig):
359
+ super().__init__(); self.cfg=cfg; D=cfg.d_model; M=cfg.memory_size
360
+ self.to_memory=nn.Linear(D,M,bias=False)
361
+ self.from_memory=nn.Linear(M,D,bias=False)
362
+ self.memory_lif=AssociativeLIF(M,cfg,persistent=True,tau_mem_override=cfg.memory_tau_mem)
363
+ self.gate_lif=AssociativeLIF(M,cfg)
364
+ self.gate_proj=nn.Linear(D,M,bias=False)
365
+ self.gate_threshold=nn.Parameter(torch.tensor(cfg.memory_gate_threshold))
366
+ # FIX B: Multi-head temporal attention for memory readout
367
+ H=cfg.memory_n_read_heads; hd=M//H
368
+ self.n_read_heads=H
369
+ self.read_query=nn.Parameter(torch.randn(H,hd)*0.02)
370
+ self.read_key_proj=nn.Linear(M,M,bias=False)
371
+ self.read_scale=1.0/math.sqrt(hd)
372
+ self.mem_norm=nn.LayerNorm(D)
373
+ self.memory_mix=nn.Parameter(torch.tensor(0.1))
374
+
375
+ def reset_state(self): self.memory_lif.reset_state()
376
+
377
+ def forward(self,x:Tensor)->Tuple[Tensor,Dict[str,float]]:
378
+ T,B,S,D=x.shape; M=self.cfg.memory_size; N=B*S; H=self.n_read_heads; hd=M//H
379
+ xf=x.reshape(T,N,D)
380
+ mi=self.to_memory(xf.reshape(T*N,D)).reshape(T,N,M)
381
+ ms,mv=self.memory_lif(mi)
382
+ gi=self.gate_proj(xf.reshape(T*N,D)).reshape(T,N,M)
383
+ gs,_=self.gate_lif(gi)
384
+ gate_sig=gs.mean(dim=0)
385
+ gate_mask=torch.sigmoid((gate_sig-self.gate_threshold)*10.0)
386
+ # FIX B: Temporal attention over ALL timesteps
387
+ mvh=mv.reshape(T,N,H,hd)
388
+ mk=self.read_key_proj(mv.reshape(T*N,M)).reshape(T,N,H,hd)
389
+ q=self.read_query.unsqueeze(0).unsqueeze(0) # (1,1,H,hd)
390
+ attn_s=(q*mk).sum(-1)*self.read_scale # (T,N,H)
391
+ attn_w=F.softmax(attn_s.float(),dim=0).to(mv.dtype) # (T,N,H)
392
+ mem_read=(mvh*attn_w.unsqueeze(-1)).sum(0).reshape(N,M) # (N,M)
393
+ mem_read=mem_read*gate_mask
394
+ mem_out=self.mem_norm(self.from_memory(mem_read).float()).to(x.dtype)
395
+ mix=torch.sigmoid(self.memory_mix)
396
+ x_e=x+mix*mem_out.reshape(1,B,S,D).expand_as(x)
397
+ stats={"memory_spike_rate":ms.mean().item(),"gate_activity":gate_sig.mean().item(),
398
+ "memory_mix":mix.item(),
399
+ "memory_attn_entropy":-(attn_w.float()*(attn_w.float()+1e-8).log()).sum(0).mean().item()}
400
+ return x_e,stats
401
+
402
+ # ═══════════════════════════════════════════════════════════════════════════════
403
+ # Β§8 BLOCKS β€” FIX H: Gradient checkpointing
404
+ # ═══════════════════════════════════════════════════════════════════════════════
405
+ class SpikingFeedForward(nn.Module):
406
+ def __init__(self,cfg:NordConfig):
407
+ super().__init__()
408
+ self.up=nn.Linear(cfg.d_model,cfg.d_ff,bias=False)
409
+ self.down=nn.Linear(cfg.d_ff,cfg.d_model,bias=False)
410
+ self.lif1=AssociativeLIF(cfg.d_ff,cfg); self.lif2=AssociativeLIF(cfg.d_model,cfg)
411
+ def forward(self,x:Tensor)->Tensor:
412
+ T,B,S,D=x.shape
413
+ h=self.up(x.reshape(T*B*S,D)).reshape(T,B*S,-1); h,_=self.lif1(h)
414
+ h=self.down(h.reshape(T*B*S,-1)).reshape(T,B*S,D); h,_=self.lif2(h)
415
+ return h.reshape(T,B,S,D)
416
+
417
+ class LeakyClamp(nn.Module):
418
+ def __init__(self,d:int,floor_init:float=-0.1,leak_init:float=0.1,force_nonneg:bool=False):
419
+ super().__init__()
420
+ # FIX M: force_nonneg=True for executive blocks β€” no negative spikes
421
+ self.force_nonneg=force_nonneg
422
+ if force_nonneg:
423
+ floor_init=0.0
424
+ self.floor=nn.Parameter(torch.full((d,),floor_init))
425
+ self.leak_raw=nn.Parameter(torch.full((d,),math.log(leak_init/(1-leak_init+1e-6))))
426
+ @property
427
+ def leak(self)->Tensor: return torch.sigmoid(self.leak_raw)
428
+ def forward(self,x:Tensor)->Tensor:
429
+ if self.force_nonneg:
430
+ # Executive: no negative values allowed
431
+ return F.relu(x)
432
+ return torch.where(x>=0,x,(self.leak*x).clamp(min=self.floor))
433
+
434
+ class NordBlock(nn.Module):
435
+ def __init__(self,cfg:NordConfig,layer_idx:int=0,use_moe:bool=False,zone:str="sensory"):
436
+ super().__init__(); D=cfg.d_model; self.use_moe=use_moe; self.zone=zone
437
+ self.layer_idx=layer_idx; self.use_checkpoint=cfg.gradient_checkpointing
438
+ self.norm1=nn.LayerNorm(D); self.norm2=nn.LayerNorm(D)
439
+ self.resonance=SpikingSynapticResonance(cfg)
440
+ if use_moe: self.moe=SpikeDrivenMoE(cfg)
441
+ else: self.ffn=SpikingFeedForward(cfg)
442
+ sc=0.1/max(cfg.n_layers_total,1)
443
+ self.gamma_attn=nn.Parameter(torch.full((D,),sc))
444
+ self.gamma_ffn=nn.Parameter(torch.full((D,),sc))
445
+ # FIX M: Executive blocks force non-negative output
446
+ self.clamp=LeakyClamp(D,floor_init=cfg.clamp_floor,
447
+ force_nonneg=(zone=="executive"))
448
+ @staticmethod
449
+ def _sn(nl:nn.LayerNorm,x:Tensor)->Tensor:
450
+ od=x.dtype
451
+ return F.layer_norm(x.float(),nl.normalized_shape,
452
+ nl.weight.float() if nl.weight is not None else None,
453
+ nl.bias.float() if nl.bias is not None else None,nl.eps).to(od)
454
+ def _forward_inner(self,x:Tensor)->Tuple[Tensor,Dict]:
455
+ stats={}
456
+ x=x+self.gamma_attn*self.resonance(self._sn(self.norm1,x))
457
+ xn=self._sn(self.norm2,x)
458
+ if self.use_moe: fo,ms=self.moe(xn); stats.update(ms)
459
+ else: fo=self.ffn(xn)
460
+ return self.clamp(x+self.gamma_ffn*fo),stats
461
+ def forward(self,x:Tensor)->Tuple[Tensor,Dict]:
462
+ if self.use_checkpoint and self.training:
463
+ x=grad_checkpoint(lambda inp:self._forward_inner(inp)[0],x,use_reentrant=False)
464
+ return x,{}
465
+ return self._forward_inner(x)
466
+
467
+ # ═══════════════════════════════════════════════════════════════════════════════
468
+ # Β§9 SPIKE REGULATOR β€” FIX C: Differentiable
469
+ # ═══════════════════════════════════════════════════════════════════════════════
470
+ class AuxiliarySpikeRegulator(nn.Module):
471
+ """FIX L: Adaptive spike regulator.
472
+ - Stronger weight (0.5 default)
473
+ - Extra penalty when any layer drops below min_rate (anti-death)
474
+ - Asymmetric: penalizes too-low firing 3x more than too-high"""
475
+ def __init__(self,cfg:NordConfig):
476
+ super().__init__(); self.target=cfg.target_spike_rate
477
+ self.weight=cfg.spike_loss_weight
478
+ self.min_rate=0.01 # absolute minimum β€” below this = dead layer
479
+ def forward(self,spike_tensors:List[Tensor])->Tensor:
480
+ if not spike_tensors: return torch.tensor(0.0)
481
+ loss=torch.tensor(0.0,device=spike_tensors[0].device,dtype=torch.float32)
482
+ for s in spike_tensors:
483
+ # FIX K: Only count non-negative values as spikes
484
+ rate=s.float().clamp(min=0).mean()
485
+ diff=self.target-rate
486
+ # Asymmetric: penalize too-low firing 3x more
487
+ if diff>0:
488
+ loss=loss+3.0*diff**2
489
+ else:
490
+ loss=loss+diff**2
491
+ # Anti-death penalty: heavy penalty if rate < min_rate
492
+ if rate<self.min_rate:
493
+ loss=loss+10.0*(self.min_rate-rate)**2
494
+ return self.weight*loss/len(spike_tensors)
495
+
496
+ # ═══════════════════════════════════════════════════════════════════════════════
497
+ # Β§10 STDP β€” FIX F: Bounded + Isolated
498
+ # ═══════════════════════════════════════════════════════════════════════════════
499
+ class STDPEngine:
500
+ def __init__(self,cfg:NordConfig):
501
+ self.cfg=cfg; self.a_plus=cfg.stdp_a_plus; self.a_minus=cfg.stdp_a_minus
502
+ self.tau_plus=cfg.stdp_tau_plus; self.tau_minus=cfg.stdp_tau_minus
503
+ self.w_max=cfg.stdp_w_max; self.w_min=cfg.stdp_w_min
504
+ self.reward_scale=cfg.stdp_reward_scale
505
+ self.allowed=set(cfg.stdp_layers or [])
506
+ self._loss_ema=10.0; self._ema_decay=0.99; self.max_update_norm=0.01
507
+
508
+ def update_reward(self,cl:float): self._loss_ema=self._ema_decay*self._loss_ema+(1-self._ema_decay)*cl
509
+ def _compute_reward(self,cl:float)->float:
510
+ return float(torch.sigmoid(torch.tensor((self._loss_ema-cl)*self.reward_scale)).item())
511
+ def is_allowed(self,name:str)->bool: return name in self.allowed
512
+
513
+ @torch.no_grad()
514
+ def compute_stdp_update(self,pre:Tensor,post:Tensor)->Tensor:
515
+ T=pre.shape[0]; d=pre.device
516
+ tp=torch.zeros_like(pre[0]); tpo=torch.zeros_like(post[0])
517
+ dp=math.exp(-1.0/self.tau_plus); dm=math.exp(-1.0/self.tau_minus)
518
+ dW=torch.zeros(post.shape[1],pre.shape[1],device=d,dtype=pre.dtype)
519
+ for t in range(T):
520
+ tp=tp*dp+pre[t]; tpo=tpo*dm+post[t]
521
+ if post[t].any(): dW+=self.a_plus*torch.outer(post[t],tp)
522
+ if pre[t].any(): dW-=self.a_minus*torch.outer(tpo,pre[t])
523
+ n=dW.norm()
524
+ if n>self.max_update_norm: dW=dW*(self.max_update_norm/n)
525
+ return dW
526
+
527
+ @torch.no_grad()
528
+ def apply_to_layer(self,layer:nn.Linear,pre:Tensor,post:Tensor,
529
+ cl:Optional[float]=None,name:str=""):
530
+ if name and not self.is_allowed(name): return
531
+ if pre.dim()==3: pre=pre.mean(dim=1)
532
+ if post.dim()==3: post=post.mean(dim=1)
533
+ dW=self.compute_stdp_update(pre,post)
534
+ if cl is not None:
535
+ r=self._compute_reward(cl); dW=dW*(2.0*r-1.0); self.update_reward(cl)
536
+ o,i=layer.weight.shape; dW=dW[:o,:i]
537
+ layer.weight.data=(layer.weight.data+dW).clamp(self.w_min,self.w_max)
538
+
539
+ # ═══════════════════════════════════════════════════════════════════════════════
540
+ # Β§11 NORD MODEL v4.1
541
+ # ═══════════════════════════════════════════════════════════════════════════════
542
+ class NordModel(nn.Module):
543
+ def __init__(self,cfg:NordConfig):
544
+ super().__init__(); self.cfg=cfg
545
+ self.encoder=TemporalSpikeEncoder(cfg)
546
+ self.input_lif=AssociativeLIF(cfg.d_model,cfg,persistent=cfg.persistent_mem)
547
+ self.sensory_blocks=nn.ModuleList([NordBlock(cfg,i,False,zone="sensory") for i in range(cfg.sensory_layers)])
548
+ self.association_blocks=nn.ModuleList([NordBlock(cfg,cfg.sensory_layers+i,True,zone="association") for i in range(cfg.association_layers)])
549
+ self.memory_cortex=MemoryCortex(cfg)
550
+ self.executive_blocks=nn.ModuleList([NordBlock(cfg,cfg.sensory_layers+cfg.association_layers+i,False,zone="executive") for i in range(cfg.executive_layers)])
551
+ self.readout_lif=AssociativeLIF(cfg.d_model,cfg,persistent=cfg.persistent_mem)
552
+ self.readout_ema_raw=nn.Parameter(torch.tensor(1.4))
553
+ self.readout_norm=nn.LayerNorm(cfg.d_model)
554
+ self.lm_head=nn.Linear(cfg.d_model,cfg.vocab_size,bias=False)
555
+ self.stdp=STDPEngine(cfg); self._last_loss=None
556
+ self.spike_regulator=AuxiliarySpikeRegulator(cfg)
557
+
558
+ @property
559
+ def readout_ema_decay(self)->Tensor: return torch.sigmoid(self.readout_ema_raw)
560
+ def reset_state(self):
561
+ self.input_lif.reset_state(); self.readout_lif.reset_state()
562
+ self.memory_cortex.reset_state()
563
+
564
+ def forward(self,token_ids:Tensor,enable_stdp:bool=False)->Tuple[Tensor,Dict]:
565
+ B,S=token_ids.shape; T_t=self.cfg.T_total; D=self.cfg.d_model
566
+ cur=self.encoder(token_ids); isp,_=self.input_lif(cur)
567
+ isp=isp.reshape(T_t,B,S,D)
568
+ spike_ts=[isp]; stats={}; moe_lb=torch.tensor(0.0,device=token_ids.device)
569
+
570
+ x=isp
571
+ for i,bl in enumerate(self.sensory_blocks):
572
+ x,bs=bl(x); spike_ts.append(x)
573
+ for k,v in bs.items(): stats[f"sensory_{i}_{k}"]=v
574
+
575
+ for i,bl in enumerate(self.association_blocks):
576
+ x,bs=bl(x); spike_ts.append(x)
577
+ lb=bs.pop("moe_load_balance_loss",None)
578
+ if lb is not None: moe_lb=moe_lb+lb
579
+ for k,v in bs.items(): stats[f"assoc_{i}_{k}"]=v
580
+
581
+ x,ms=self.memory_cortex(x); stats.update(ms)
582
+
583
+ for i,bl in enumerate(self.executive_blocks):
584
+ x,bs=bl(x); spike_ts.append(x)
585
+ for k,v in bs.items(): stats[f"exec_{i}_{k}"]=v
586
+
587
+ xf=x.reshape(T_t,B*S,D); rsp,vm=self.readout_lif(xf)
588
+ a=self.readout_ema_decay
589
+ ema=torch.zeros(B*S,D,device=x.device,dtype=vm.dtype)
590
+ for t in range(T_t): ema=a*ema+(1-a)*vm[t]
591
+ vs=ema.reshape(B,S,D)
592
+ sm=rsp.mean(dim=0).reshape(B,S,D)
593
+ ro=vs+sm
594
+ xn=F.layer_norm(ro.float(),self.readout_norm.normalized_shape,
595
+ self.readout_norm.weight.float() if self.readout_norm.weight is not None else None,
596
+ self.readout_norm.bias.float() if self.readout_norm.bias is not None else None,
597
+ self.readout_norm.eps).to(ro.dtype)
598
+ logits=self.lm_head(xn)
599
+
600
+ out_rate=rsp.detach().mean().item()
601
+ # FIX K: clamp negatives β€” spike rates cannot be negative
602
+ sr=[s.detach().clamp(min=0).mean().item() for s in spike_ts]
603
+
604
+ # Convert ALL stats to tensors for DataParallel gather compatibility
605
+ dev = token_ids.device
606
+ tensor_stats = {}
607
+ tensor_stats["sparsity"] = torch.tensor(1.0 - out_rate, device=dev)
608
+ tensor_stats["avg_spike_rate"] = torch.tensor(sum(sr)/len(sr), device=dev)
609
+ tensor_stats["spike_loss"] = self.spike_regulator(spike_ts)
610
+ tensor_stats["moe_lb_loss"] = moe_lb
611
+ # Pack spike_rates as a single tensor
612
+ tensor_stats["spike_rates_tensor"] = torch.tensor(sr, device=dev)
613
+ # Convert any float stats from blocks/memory to tensors
614
+ for k, v in stats.items():
615
+ if isinstance(v, (int, float)):
616
+ tensor_stats[k] = torch.tensor(v, device=dev)
617
+ elif isinstance(v, torch.Tensor):
618
+ tensor_stats[k] = v.to(dev) if v.device != dev else v
619
+ # skip lists and other non-tensor types
620
+ return logits, tensor_stats
621
+
622
+ def set_last_loss(self,l:float): self._last_loss=l
623
+ def count_params(self)->str:
624
+ total=sum(p.numel() for p in self.parameters())
625
+ train=sum(p.numel() for p in self.parameters() if p.requires_grad)
626
+ se=sum(p.numel() for n,p in self.named_parameters() if 'sensory' in n)
627
+ a=sum(p.numel() for n,p in self.named_parameters() if 'association' in n)
628
+ m=sum(p.numel() for n,p in self.named_parameters() if 'memory' in n)
629
+ e=sum(p.numel() for n,p in self.named_parameters() if 'executive' in n)
630
+ return(f"Total: {total/1e6:.1f}M | Trainable: {train/1e6:.1f}M\n"
631
+ f" Sensory: {se/1e6:.1f}M ({self.cfg.sensory_layers} blocks)\n"
632
+ f" Association: {a/1e6:.1f}M ({self.cfg.association_layers} blocks, MoE)\n"
633
+ f" Memory: {m/1e6:.1f}M\n"
634
+ f" Executive: {e/1e6:.1f}M ({self.cfg.executive_layers} blocks)")
nord_v4_700m-4.2/train_nord_700m.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD v4.2 β€” Training Script (700M) β•‘
4
+ β•‘ β•‘
5
+ β•‘ Usage: β•‘
6
+ β•‘ python train_nord_700m.py β•‘
7
+ β•‘ β•‘
8
+ β•‘ v4.2 (700M) β€” Scaling test on L40/A100 β•‘
9
+ β•‘ - Auxiliary spike loss (homeostatic regulation) β•‘
10
+ β•‘ - MoE routing stats (expert load, entropy) β•‘
11
+ β•‘ - Memory cortex monitoring β•‘
12
+ β•‘ - Zone-aware logging (sensory/association/executive) β•‘
13
+ β•‘ - Combined loss: L_total = L_CE + Ξ»_spike * L_spike + Ξ»_lb * L_lbβ•‘
14
+ β•‘ β•‘
15
+ β•‘ Hardware: β•‘
16
+ β•‘ - RTX 5070 (8GB) β€” batch=2, ~3GB VRAM β•‘
17
+ β•‘ - RTX 3090/4090 (24GB) β€” batch=4 β•‘
18
+ β•‘ - A100/L40 (48-80GB) β€” batch=8-16 β•‘
19
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import math
26
+ import os
27
+ import shutil
28
+ import struct
29
+ import sys
30
+ import time
31
+ from pathlib import Path
32
+ from typing import Optional
33
+
34
+ import torch
35
+ import torch.nn.functional as F
36
+ import torch.distributed as dist
37
+ from torch.amp import autocast
38
+ from torch.utils.data import Dataset, DataLoader
39
+ from torch.nn.parallel import DataParallel
40
+
41
+ # Use local nord_core_700m (fix for gradient checkpoint + MoE metadata mismatch)
42
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
43
+ from nord_core_700m import NordConfig, NordModel
44
+
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # TOKENIZER
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ class NordTokenizer:
51
+ def __init__(self, cfg: NordConfig):
52
+ from transformers import AutoTokenizer
53
+
54
+ print(f" [*] Loading Llama-3.2 tokenizer...", flush=True)
55
+ self.tokenizer = AutoTokenizer.from_pretrained(
56
+ cfg.tokenizer_id, trust_remote_code=True,
57
+ )
58
+ if self.tokenizer.pad_token is None:
59
+ self.tokenizer.pad_token = self.tokenizer.eos_token
60
+ self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
61
+
62
+ self.max_len = cfg.max_seq_len
63
+ self.vocab_size = self.tokenizer.vocab_size
64
+ if cfg.vocab_size < self.vocab_size:
65
+ cfg.vocab_size = self.vocab_size
66
+
67
+ print(f" [βœ“] Tokenizer ready (vocab={self.vocab_size:,})", flush=True)
68
+
69
+ def encode(self, text: str) -> torch.Tensor:
70
+ enc = self.tokenizer(
71
+ text, return_tensors="pt",
72
+ max_length=self.max_len, truncation=True, padding="max_length",
73
+ )
74
+ return enc.input_ids
75
+
76
+ def decode(self, ids) -> str:
77
+ return self.tokenizer.decode(ids, skip_special_tokens=True)
78
+
79
+ @property
80
+ def pad_id(self) -> int:
81
+ return self.tokenizer.pad_token_id
82
+
83
+
84
+ # ─────────────────────────────────────────────────────────────────────────────
85
+ # LMDB DATASET
86
+ # ─────────────────────────────────────────────────────────────────────────────
87
+
88
+ class LMDBDataset(Dataset):
89
+ def __init__(self, db_path: str, max_seq_len: int):
90
+ import lmdb
91
+ self.db_path = db_path
92
+ self.max_seq_len = max_seq_len
93
+ self._env = None
94
+
95
+ env = lmdb.open(db_path, readonly=True, lock=False, readahead=False, meminit=False)
96
+ with env.begin(write=False) as txn:
97
+ raw = txn.get(b"__len__")
98
+ self.length = struct.unpack("<Q", raw)[0]
99
+ env.close()
100
+ print(f" [βœ“] LMDB: {self.length:,} samples", flush=True)
101
+
102
+ def _get_env(self):
103
+ if self._env is None:
104
+ import lmdb
105
+ self._env = lmdb.open(
106
+ self.db_path, readonly=True, lock=False,
107
+ readahead=True, meminit=False, max_readers=64,
108
+ )
109
+ return self._env
110
+
111
+ def __len__(self): return self.length
112
+
113
+ def __getitem__(self, idx):
114
+ env = self._get_env()
115
+ with env.begin(write=False) as txn:
116
+ raw = txn.get(f"sample_{idx:010d}".encode())
117
+ ids = torch.frombuffer(bytearray(raw), dtype=torch.int32).long()
118
+ S = self.max_seq_len
119
+ return ids[:S] if ids.shape[0] >= S else F.pad(ids, (0, S - ids.shape[0]))
120
+
121
+
122
+ def build_lmdb(jsonl_path: str, db_path: str, tokenizer: NordTokenizer,
123
+ max_seq_len: int, map_size_gb: float = 80.0):
124
+ import lmdb
125
+ import numpy as np
126
+
127
+ print(f"\n [*] Building LMDB database (fast batch mode)...", flush=True)
128
+ print(f" Source: {jsonl_path}", flush=True)
129
+ print(f" Target: {db_path}", flush=True)
130
+
131
+ # Read all texts into memory
132
+ print(f" [*] Reading JSONL into memory...", flush=True)
133
+ t0 = time.time()
134
+ texts = []
135
+ with open(jsonl_path, "r", encoding="utf-8") as f:
136
+ for i, line in enumerate(f):
137
+ if i % 1_000_000 == 0 and i > 0:
138
+ print(f" read {i:,} lines...", flush=True)
139
+ line = line.strip()
140
+ if not line:
141
+ continue
142
+ try:
143
+ obj = json.loads(line)
144
+ except json.JSONDecodeError:
145
+ continue
146
+ text = obj.get("text") or obj.get("content") or obj.get("passage", "")
147
+ if len(text) >= 30:
148
+ texts.append(text)
149
+
150
+ print(f" {len(texts):,} valid texts in {time.time()-t0:.0f}s", flush=True)
151
+
152
+ # Batch tokenize
153
+ print(f" [*] Batch tokenizing...", flush=True)
154
+ t1 = time.time()
155
+ BATCH = 1024
156
+ PAD_ID = tokenizer.pad_id
157
+
158
+ env = lmdb.open(db_path, map_size=int(map_size_gb * (1024**3)))
159
+ txn = env.begin(write=True)
160
+ count = 0
161
+ total_tokens = 0
162
+ total_batches = (len(texts) + BATCH - 1) // BATCH
163
+
164
+ for batch_idx in range(0, len(texts), BATCH):
165
+ batch = texts[batch_idx : batch_idx + BATCH]
166
+ batch_num = batch_idx // BATCH + 1
167
+
168
+ enc = tokenizer.tokenizer(
169
+ batch, max_length=max_seq_len, truncation=True,
170
+ padding="max_length", return_tensors="np",
171
+ return_attention_mask=False,
172
+ )
173
+ ids_np = enc.input_ids.astype(np.int32)
174
+
175
+ for j in range(ids_np.shape[0]):
176
+ row = ids_np[j]
177
+ non_pad = int(np.sum(row != PAD_ID))
178
+ if non_pad < 10:
179
+ continue
180
+ txn.put(f"sample_{count:010d}".encode(), row.tobytes())
181
+ count += 1
182
+ total_tokens += non_pad
183
+
184
+ if batch_num % 100 == 0 or batch_num == total_batches:
185
+ elapsed = time.time() - t1
186
+ pct = batch_num / total_batches * 100
187
+ eta = (elapsed / batch_num) * (total_batches - batch_num)
188
+ print(
189
+ f" [{pct:5.1f}%] {count:,} samples | "
190
+ f"{total_tokens/1e6:.0f}M tok | ETA {eta:.0f}s",
191
+ flush=True,
192
+ )
193
+
194
+ if count % 500_000 < BATCH and count >= 500_000:
195
+ txn.commit()
196
+ txn = env.begin(write=True)
197
+
198
+ txn.put(b"__len__", struct.pack("<Q", count))
199
+ txn.put(b"__total_tokens__", struct.pack("<Q", total_tokens))
200
+ txn.commit()
201
+ env.close()
202
+
203
+ elapsed = time.time() - t1
204
+ print(f"\n [βœ“] LMDB ready!", flush=True)
205
+ print(f" Samples: {count:,}", flush=True)
206
+ print(f" Tokens: {total_tokens:,} ({total_tokens/1e6:.1f}M)", flush=True)
207
+ print(f" Time: {elapsed:.0f}s ({elapsed/60:.1f} min)", flush=True)
208
+
209
+
210
+ # ─────────────────────────────────────────────────────────────────────────────
211
+ # LR SCHEDULE
212
+ # ─────────────────────────────────────────────────────────────────────────────
213
+
214
+ def get_lr(step: int, cfg: NordConfig) -> float:
215
+ """Warmup β†’ cosine decay to min_lr.
216
+
217
+ Phase 1 (0 β†’ warmup_steps): linear warmup from 0 β†’ lr
218
+ Phase 2 (warmup_steps β†’ max_steps): cosine decay from lr β†’ min_lr
219
+ """
220
+ if step < cfg.warmup_steps:
221
+ return cfg.lr * (step + 1) / cfg.warmup_steps
222
+
223
+ # Cosine decay phase
224
+ decay_steps = cfg.max_steps - cfg.warmup_steps
225
+ progress = (step - cfg.warmup_steps) / max(decay_steps, 1)
226
+ progress = min(progress, 1.0) # clamp at 1.0
227
+
228
+ # Cosine annealing: lr β†’ min_lr
229
+ cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
230
+ return cfg.min_lr + (cfg.lr - cfg.min_lr) * cosine
231
+
232
+
233
+ # ─────────────────────��───────────────────────────────────────────────────────
234
+ # CHECKPOINT MANAGER
235
+ # ─────────────────────────────────────────────────────────────────────────────
236
+
237
+ class CheckpointManager:
238
+ def __init__(self, save_dir: str, keep_last: int = 5):
239
+ self.save_dir = Path(save_dir)
240
+ self.save_dir.mkdir(parents=True, exist_ok=True)
241
+ self.keep_last = keep_last
242
+
243
+ def save(self, model, optimizer, scaler, step, loss, cfg):
244
+ path = self.save_dir / f"nord_v4_step_{step:07d}.pt"
245
+ # Handle DataParallel: save inner model
246
+ model_to_save = model.module if hasattr(model, 'module') else model
247
+ torch.save({
248
+ "step": step, "loss": loss,
249
+ "version": "v4.1",
250
+ "model_state_dict": model_to_save.state_dict(),
251
+ "optimizer_state_dict": optimizer.state_dict(),
252
+ "scaler_state_dict": scaler.state_dict(),
253
+ "config": {k: v for k, v in cfg.__dict__.items()
254
+ if not k.startswith("_") and k != "dtype"},
255
+ }, path)
256
+
257
+ latest = self.save_dir / "nord_v4_latest.pt"
258
+ if latest.exists():
259
+ latest.unlink()
260
+ shutil.copy2(path, latest)
261
+
262
+ ckpts = sorted(self.save_dir.glob("nord_v4_step_*.pt"),
263
+ key=lambda p: p.stat().st_mtime)
264
+ for old in ckpts[:max(0, len(ckpts) - self.keep_last)]:
265
+ old.unlink()
266
+
267
+ print(f" [πŸ’Ύ] Saved: {path.name} (loss={loss:.4f})", flush=True)
268
+
269
+ def load(self, model, optimizer, scaler, device) -> int:
270
+ latest = self.save_dir / "nord_v4_latest.pt"
271
+ if not latest.exists():
272
+ ckpts = sorted(self.save_dir.glob("nord_v4_step_*.pt"))
273
+ latest = ckpts[-1] if ckpts else None
274
+ if latest is None:
275
+ return 0
276
+
277
+ print(f" [*] Resuming from: {latest.name}", flush=True)
278
+ ckpt = torch.load(latest, map_location=device, weights_only=False)
279
+ # Handle DataParallel: load into inner model
280
+ model_to_load = model.module if hasattr(model, 'module') else model
281
+ # Filter out persistent LIF state buffers β€” they resize with batch
282
+ state = ckpt["model_state_dict"]
283
+ filtered = {k: v for k, v in state.items()
284
+ if "_v_mem_state" not in k and "_i_syn_state" not in k}
285
+ model_to_load.load_state_dict(filtered, strict=False)
286
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
287
+ scaler.load_state_dict(ckpt["scaler_state_dict"])
288
+ step = ckpt["step"]
289
+ print(f" [βœ“] Resumed at step {step:,} (loss={ckpt.get('loss', '?')})", flush=True)
290
+ return step
291
+
292
+ def save_final(self, model, cfg):
293
+ path = self.save_dir / "nord_v4_final.pt"
294
+ model_to_save = model.module if hasattr(model, 'module') else model
295
+ torch.save({
296
+ "version": "v4.1",
297
+ "model_state_dict": model_to_save.state_dict(),
298
+ "config": {k: v for k, v in cfg.__dict__.items()
299
+ if not k.startswith("_") and k != "dtype"},
300
+ }, path)
301
+ print(f" [⭐] Final model: {path}", flush=True)
302
+ return path
303
+
304
+
305
+ # ─────────────────────────────────────────────────────────────────────────────
306
+ # TRAINING
307
+ # ─────────────────────────────────────────────────────────────────────────────
308
+
309
+ # ─────────────────────────────────────────────────────────────────────────────
310
+ # TRAINING
311
+ # ─────────────────────────────────────────────────────────────────────────────
312
+
313
+ def train(dataset_path: str, model_dir: str):
314
+ # ── Config β€” v4.2 (700M) ──
315
+ cfg = NordConfig(
316
+ device="cuda" if torch.cuda.is_available() else "cpu",
317
+ dtype=torch.float16,
318
+
319
+ d_model=1536,
320
+ n_heads=24,
321
+ d_ff=4096,
322
+ n_clusters=128,
323
+ max_seq_len=192, # 23.5GB Π±Π΅Π· checkpoint: 384 OOM β†’ 192 Π²ΠΌΡ–Ρ‰Π°Ρ”Ρ‚ΡŒΡΡ
324
+
325
+ sensory_layers=3,
326
+ association_layers=3,
327
+ executive_layers=4,
328
+
329
+ T=8,
330
+ T_slow=2,
331
+ persistent_mem=False, # FIX для Π³Ρ€Π°Π΄Ρ–Ρ”Π½Ρ‚Ρ–Π²
332
+
333
+ n_experts=4,
334
+ top_k_experts=2,
335
+
336
+ memory_size=256,
337
+ memory_tau_mem=0.99,
338
+ memory_n_read_heads=8,
339
+
340
+ target_spike_rate=0.03,
341
+ spike_loss_weight=0.5,
342
+
343
+ v_threshold=0.12,
344
+ tau_mem=0.9,
345
+ lif_freeze_steps=1000,
346
+
347
+ gradient_checkpointing=False, # OFF: MoE+SNN нСсумісні Π· checkpoint (219 vs 220 metadata)
348
+
349
+ batch_size=1,
350
+ grad_accum=64,
351
+ lr=2e-4,
352
+ warmup_steps=1000,
353
+ max_steps=50_000,
354
+ save_every=1000,
355
+ log_every=10,
356
+ )
357
+
358
+
359
+ print(flush=True)
360
+ print("═" * 60, flush=True)
361
+ print(" PROJECT NORD v4.2 β€” 700M SNN Training", flush=True)
362
+ print("═" * 60, flush=True)
363
+
364
+ if torch.cuda.is_available():
365
+ n_gpus = torch.cuda.device_count()
366
+ print(f" GPU: {torch.cuda.get_device_name()}", flush=True)
367
+ vram = torch.cuda.get_device_properties(0).total_memory / (1024**3)
368
+ print(f" VRAM: {vram:.1f} GB" + (f" Γ— {n_gpus} GPUs" if n_gpus > 1 else ""), flush=True)
369
+
370
+ # Auto-adjust batch size for 700M SNN
371
+ if vram >= 80:
372
+ cfg.batch_size = 8
373
+ cfg.grad_accum = 4
374
+ print(f" [Auto] batch=8, accum=4 (A100 80GB)", flush=True)
375
+ elif vram >= 40:
376
+ cfg.batch_size = 4
377
+ cfg.grad_accum = 8
378
+ print(f" [Auto] batch=4, accum=8 (L40 48GB)", flush=True)
379
+ elif vram >= 20:
380
+ cfg.batch_size = 1
381
+ cfg.grad_accum = 32
382
+ print(f" [Auto] batch=1, accum=32 (24GB VRAM)", flush=True)
383
+ else:
384
+ print(f" [ERROR] 700M model needs minimum 24GB VRAM!", flush=True)
385
+ sys.exit(1)
386
+ else:
387
+ print(" CPU mode (not recommended!)", flush=True)
388
+
389
+ print(f" Architecture: d={cfg.d_model}, heads={cfg.n_heads}, clusters={cfg.n_clusters}", flush=True)
390
+ print(f" Zones: Sensory({cfg.sensory_layers}) β†’ Association({cfg.association_layers},MoE) β†’ Memory β†’ Executive({cfg.executive_layers})", flush=True)
391
+ print(f" MoE: {cfg.n_experts} experts, top-{cfg.top_k_experts}", flush=True)
392
+ print(f" Memory: {cfg.memory_size} neurons (Ο„={cfg.memory_tau_mem})", flush=True)
393
+ print(f" Spike target: {cfg.target_spike_rate:.0%} firing rate (Ξ»={cfg.spike_loss_weight})", flush=True)
394
+ print(f" Effective batch: {cfg.batch_size} Γ— {cfg.grad_accum} = {cfg.batch_size * cfg.grad_accum}", flush=True)
395
+ print(f" LR: {cfg.lr} β†’ {cfg.min_lr} (cosine decay, {cfg.warmup_steps} warmup)", flush=True)
396
+ print(f" Max steps: {cfg.max_steps:,}", flush=True)
397
+ print(f" Dataset: {dataset_path}", flush=True)
398
+ print(f" Model dir: {model_dir}", flush=True)
399
+ print(flush=True)
400
+
401
+ # ── Tokenizer ──
402
+ tokenizer = NordTokenizer(cfg)
403
+
404
+ # ── LMDB ──
405
+ db_path = str(Path(dataset_path).with_suffix("")) + "_lmdb"
406
+ if not Path(db_path).exists():
407
+ build_lmdb(dataset_path, db_path, tokenizer, cfg.max_seq_len)
408
+
409
+ dataset = LMDBDataset(db_path, cfg.max_seq_len)
410
+ dataloader = DataLoader(
411
+ dataset, batch_size=cfg.batch_size, shuffle=True,
412
+ num_workers=2, pin_memory=True, drop_last=True, persistent_workers=True,
413
+ )
414
+
415
+ # ── Model ──
416
+ print(f"\n [*] Building Nord v4 model...", flush=True)
417
+ model = NordModel(cfg).to(cfg.device)
418
+ print(f" [βœ“] {model.count_params()}", flush=True)
419
+
420
+ # ── Multi-GPU support ──
421
+ n_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 0
422
+ if n_gpus > 1:
423
+ print(f" [⚑] {n_gpus} GPUs detected! Using DataParallel", flush=True)
424
+ for i in range(n_gpus):
425
+ name = torch.cuda.get_device_name(i)
426
+ vram_i = torch.cuda.get_device_properties(i).total_memory / (1024**3)
427
+ print(f" GPU {i}: {name} ({vram_i:.1f} GB)", flush=True)
428
+ model = DataParallel(model)
429
+ # Scale batch size by number of GPUs
430
+ cfg.batch_size = cfg.batch_size * n_gpus
431
+ cfg.grad_accum = max(1, cfg.grad_accum // n_gpus)
432
+ print(f" [Auto] Scaled: batch={cfg.batch_size}, accum={cfg.grad_accum} "
433
+ f"(effective={cfg.batch_size * cfg.grad_accum})", flush=True)
434
+
435
+ # ── Gradient checkpointing for OOM prevention ──
436
+ if cfg.gradient_checkpointing:
437
+ print(f" [*] Gradient checkpointing: ON (saves VRAM)", flush=True)
438
+
439
+ if torch.cuda.is_available():
440
+ allocated = torch.cuda.memory_allocated() / (1024**3)
441
+ print(f" [*] Model VRAM: {allocated:.2f} GB", flush=True)
442
+
443
+ # ── Optimizer ──
444
+ optimizer = torch.optim.AdamW(
445
+ model.parameters(), lr=cfg.lr,
446
+ weight_decay=cfg.weight_decay, betas=(0.9, 0.95),
447
+ )
448
+ scaler = torch.amp.GradScaler("cuda", enabled=(cfg.dtype == torch.float16))
449
+
450
+ # ── Checkpoints ──
451
+ ckpt_mgr = CheckpointManager(model_dir)
452
+ start_step = ckpt_mgr.load(model, optimizer, scaler, cfg.device)
453
+
454
+ # ── Training loop ──
455
+ model.train()
456
+ data_iter = iter(dataloader)
457
+ running_loss = 0.0
458
+ running_spike_loss = 0.0
459
+ tokens_seen = 0
460
+ t_start = time.time()
461
+
462
+ print(f"\n {'─' * 55}", flush=True)
463
+ print(f" Starting from step {start_step:,} | {len(dataset):,} samples", flush=True)
464
+ print(f" Ctrl+C = stop (model will be saved!)", flush=True)
465
+ print(f" {'─' * 55}\n", flush=True)
466
+
467
+ try:
468
+ for step in range(start_step, cfg.max_steps):
469
+ accum_loss = 0.0
470
+ accum_spike_loss = 0.0
471
+ stats = {}
472
+
473
+ for _ in range(cfg.grad_accum):
474
+ try:
475
+ input_ids = next(data_iter)
476
+ except StopIteration:
477
+ data_iter = iter(dataloader)
478
+ input_ids = next(data_iter)
479
+
480
+ input_ids = input_ids.to(cfg.device, non_blocking=True)
481
+
482
+ with autocast(device_type="cuda", dtype=torch.float16,
483
+ enabled=(cfg.dtype == torch.float16)):
484
+ logits, stats = model(input_ids)
485
+
486
+ shift_logits = logits[:, :-1, :].contiguous()
487
+ shift_labels = input_ids[:, 1:].contiguous()
488
+
489
+ # Main loss: cross entropy
490
+ ce_loss = F.cross_entropy(
491
+ shift_logits.reshape(-1, cfg.vocab_size),
492
+ shift_labels.reshape(-1),
493
+ ignore_index=tokenizer.pad_id,
494
+ )
495
+
496
+ # v4.1: Auxiliary spike loss
497
+ spike_loss = stats.get("spike_loss", torch.tensor(0.0))
498
+ if isinstance(spike_loss, torch.Tensor):
499
+ spike_loss = spike_loss.to(ce_loss.device)
500
+ else:
501
+ spike_loss = torch.tensor(0.0, device=ce_loss.device)
502
+
503
+ # v4.1: MoE load balance loss
504
+ moe_lb_loss = stats.get("moe_lb_loss", torch.tensor(0.0))
505
+ if isinstance(moe_lb_loss, torch.Tensor):
506
+ moe_lb_loss = moe_lb_loss.to(ce_loss.device)
507
+ else:
508
+ moe_lb_loss = torch.tensor(0.0, device=ce_loss.device)
509
+
510
+ # Combined loss: CE + spike homeostasis + MoE load balance
511
+ loss = (ce_loss + spike_loss + 0.01 * moe_lb_loss) / cfg.grad_accum
512
+
513
+ scaler.scale(loss).backward()
514
+ accum_loss += ce_loss.item() / cfg.grad_accum
515
+ accum_spike_loss += spike_loss.item() / cfg.grad_accum
516
+ tokens_seen += input_ids.numel()
517
+
518
+ scaler.unscale_(optimizer)
519
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.max_grad_norm)
520
+ scaler.step(optimizer)
521
+ scaler.update()
522
+ optimizer.zero_grad(set_to_none=True)
523
+
524
+ # LR schedule
525
+ lr = get_lr(step, cfg)
526
+ for pg in optimizer.param_groups:
527
+ pg["lr"] = lr
528
+
529
+ running_loss += accum_loss
530
+ running_spike_loss += accum_spike_loss
531
+
532
+ if step % cfg.log_every == 0 and step > start_step:
533
+ avg = running_loss / cfg.log_every
534
+ avg_spike = running_spike_loss / cfg.log_every
535
+ elapsed = time.time() - t_start
536
+ tps = tokens_seen / elapsed / 1000 if elapsed > 0 else 0
537
+ sp = stats.get("sparsity", 0)
538
+
539
+ # VRAM monitoring
540
+ vram_used = ""
541
+ if torch.cuda.is_available():
542
+ vram_gb = torch.cuda.memory_allocated() / (1024**3)
543
+ vram_used = f" | VRAM {vram_gb:.1f}G"
544
+
545
+ # MoE routing info
546
+ moe_info = ""
547
+ entropy = stats.get("moe_route_entropy", None)
548
+ if entropy is not None:
549
+ moe_info = f" | MoE H={entropy:.2f}"
550
+
551
+ # Memory info
552
+ mem_info = ""
553
+ mem_rate = stats.get("memory_spike_rate", None)
554
+ if mem_rate is not None:
555
+ mem_info = f" | mem={mem_rate:.3f}"
556
+
557
+ print(
558
+ f" step {step:>7,} β”‚ "
559
+ f"loss {avg:.4f} β”‚ "
560
+ f"spike_L {avg_spike:.4f} β”‚ "
561
+ f"lr {lr:.1e} β”‚ "
562
+ f"grad {grad_norm:.1f} β”‚ "
563
+ f"sparsity {sp:.0%} β”‚ "
564
+ f"{tps:.1f}k tok/s"
565
+ f"{moe_info}{mem_info}{vram_used}",
566
+ flush=True,
567
+ )
568
+ running_loss = 0.0
569
+ running_spike_loss = 0.0
570
+
571
+ # Detailed stats every 100 steps
572
+ if step % 100 == 0 and step > start_step:
573
+ print(f" {'Β·' * 50}", flush=True)
574
+ # Spike rates per zone
575
+ spike_rates = stats.get("spike_rates", [])
576
+ if spike_rates:
577
+ s_rates = spike_rates[:cfg.sensory_layers + 1]
578
+ a_rates = spike_rates[cfg.sensory_layers + 1:
579
+ cfg.sensory_layers + 1 + cfg.association_layers]
580
+ e_rates = spike_rates[cfg.sensory_layers + 1 + cfg.association_layers:]
581
+
582
+ print(f" Sensory spike rates: {[f'{r:.4f}' for r in s_rates]}", flush=True)
583
+ print(f" Association spike rates: {[f'{r:.4f}' for r in a_rates]}", flush=True)
584
+ print(f" Executive spike rates: {[f'{r:.4f}' for r in e_rates]}", flush=True)
585
+
586
+ # Expert load balance
587
+ loads = [stats.get(f"expert_{e}_load", 0) for e in range(cfg.n_experts)]
588
+ if any(l > 0 for l in loads):
589
+ print(f" Expert loads: {[f'{l:.2f}' for l in loads]}", flush=True)
590
+
591
+ # Memory stats
592
+ gate = stats.get("gate_activity", None)
593
+ mix = stats.get("memory_mix", None)
594
+ if gate is not None:
595
+ print(f" Memory gate={gate:.4f} mix={mix:.4f}", flush=True)
596
+
597
+ print(f" {'Β·' * 50}", flush=True)
598
+
599
+ if step > 0 and step % cfg.save_every == 0:
600
+ ckpt_mgr.save(model, optimizer, scaler, step, accum_loss, cfg)
601
+
602
+ except KeyboardInterrupt:
603
+ print(f"\n\n [⏸] Stopped at step {step:,}", flush=True)
604
+ ckpt_mgr.save(model, optimizer, scaler, step, accum_loss, cfg)
605
+ print(f" To resume β€” just run the script again.", flush=True)
606
+
607
+ ckpt_mgr.save_final(model, cfg)
608
+
609
+ print(f"\n {'═' * 55}", flush=True)
610
+ print(f" Training complete!", flush=True)
611
+ print(f" Model saved in: {model_dir}", flush=True)
612
+ print(f" {'═' * 55}", flush=True)
613
+
614
+
615
+ # ─────────────────────────────────────────────────────────────────────────────
616
+ # ENTRY POINT
617
+ # ─────────────────────────────────────────────────────────────────────────────
618
+
619
+ def main():
620
+ print("=" * 60, flush=True)
621
+ print(" PROJECT NORD v4 β€” Brain-Inspired SNN Training", flush=True)
622
+ print("=" * 60, flush=True)
623
+
624
+ default_data = "train_data.jsonl"
625
+ print(f"\n Dataset path? (JSONL file)", flush=True)
626
+ print(f" (Enter = {default_data})", flush=True)
627
+ data_input = input(" Dataset: ").strip()
628
+ dataset_path = data_input if data_input else default_data
629
+
630
+ if not Path(dataset_path).exists():
631
+ print(f"\n [βœ—] File not found: {dataset_path}", flush=True)
632
+ sys.exit(1)
633
+
634
+ default_model = "nord_v4_700m"
635
+ print(f"\n Model save directory?", flush=True)
636
+ print(f" (Enter = {default_model})", flush=True)
637
+ model_input = input(" Model dir: ").strip()
638
+ model_dir = model_input if model_input else default_model
639
+
640
+ train(dataset_path, model_dir)
641
+
642
+
643
+ if __name__ == "__main__":
644
+ main()
nord_v4_700m-4.2/train_nord_tpu_700m.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════╗
3
+ β•‘ PROJECT NORD v4.2 β€” Training Script (700M) β•‘
4
+ β•‘ β•‘
5
+ β•‘ Usage: β•‘
6
+ β•‘ CUDA: python train_nord_700m.py β•‘
7
+ β•‘ TPU: python train_nord_700m.py --tpu β•‘
8
+ β•‘ β•‘
9
+ β•‘ v4.2 (700M) β€” Supports CUDA GPU and Google Cloud TPU β•‘
10
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
11
+ """
12
+
13
+ from __future__ import annotations
14
+ import argparse, json, math, os, shutil, struct, sys, time
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+ from torch.utils.data import Dataset, DataLoader
21
+
22
+ # ── Backend globals ──
23
+ USE_TPU = False
24
+ xm = None
25
+
26
+ def detect_backend(force_tpu=False):
27
+ global USE_TPU, xm
28
+ if force_tpu:
29
+ try:
30
+ import torch_xla.core.xla_model as _xm
31
+ xm = _xm; USE_TPU = True
32
+ print(" [βœ“] TPU backend: torch_xla loaded", flush=True); return
33
+ except ImportError:
34
+ print(" [!] --tpu but torch_xla not found, fallback CUDA", flush=True)
35
+ if torch.cuda.is_available():
36
+ print(f" [βœ“] CUDA backend: {torch.cuda.get_device_name()}", flush=True)
37
+ else:
38
+ try:
39
+ import torch_xla.core.xla_model as _xm
40
+ xm = _xm; USE_TPU = True
41
+ print(" [βœ“] TPU backend (auto-detected)", flush=True)
42
+ except ImportError:
43
+ print(" [!] CPU mode (very slow!)", flush=True)
44
+
45
+ def get_device():
46
+ if USE_TPU: return xm.xla_device()
47
+ if torch.cuda.is_available(): return torch.device("cuda")
48
+ return torch.device("cpu")
49
+
50
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
51
+ from nord_core_700m import NordConfig, NordModel
52
+
53
+ # ── Tokenizer ──
54
+ class NordTokenizer:
55
+ def __init__(self, cfg):
56
+ from transformers import AutoTokenizer
57
+ print(f" [*] Loading Llama-3.2 tokenizer...", flush=True)
58
+ self.tokenizer = AutoTokenizer.from_pretrained(cfg.tokenizer_id, trust_remote_code=True)
59
+ if self.tokenizer.pad_token is None:
60
+ self.tokenizer.pad_token = self.tokenizer.eos_token
61
+ self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
62
+ self.max_len = cfg.max_seq_len
63
+ self.vocab_size = self.tokenizer.vocab_size
64
+ if cfg.vocab_size < self.vocab_size: cfg.vocab_size = self.vocab_size
65
+ print(f" [βœ“] Tokenizer ready (vocab={self.vocab_size:,})", flush=True)
66
+ def encode(self, text):
67
+ return self.tokenizer(text, return_tensors="pt", max_length=self.max_len, truncation=True, padding="max_length").input_ids
68
+ def decode(self, ids): return self.tokenizer.decode(ids, skip_special_tokens=True)
69
+ @property
70
+ def pad_id(self): return self.tokenizer.pad_token_id
71
+
72
+ # ── LMDB Dataset ──
73
+ class LMDBDataset(Dataset):
74
+ def __init__(self, db_path, max_seq_len):
75
+ import lmdb
76
+ self.db_path = db_path; self.max_seq_len = max_seq_len; self._env = None
77
+ env = lmdb.open(db_path, readonly=True, lock=False, readahead=False, meminit=False)
78
+ with env.begin(write=False) as txn: self.length = struct.unpack("<Q", txn.get(b"__len__"))[0]
79
+ env.close()
80
+ print(f" [βœ“] LMDB: {self.length:,} samples", flush=True)
81
+ def _get_env(self):
82
+ if self._env is None:
83
+ import lmdb
84
+ self._env = lmdb.open(self.db_path, readonly=True, lock=False, readahead=True, meminit=False, max_readers=64)
85
+ return self._env
86
+ def __len__(self): return self.length
87
+ def __getitem__(self, idx):
88
+ env = self._get_env()
89
+ with env.begin(write=False) as txn: raw = txn.get(f"sample_{idx:010d}".encode())
90
+ ids = torch.frombuffer(bytearray(raw), dtype=torch.int32).long()
91
+ S = self.max_seq_len
92
+ return ids[:S] if ids.shape[0] >= S else F.pad(ids, (0, S - ids.shape[0]))
93
+
94
+ def build_lmdb(jsonl_path, db_path, tokenizer, max_seq_len, map_size_gb=80.0):
95
+ import lmdb, numpy as np
96
+ print(f"\n [*] Building LMDB...", flush=True)
97
+ t0 = time.time(); texts = []
98
+ with open(jsonl_path, "r", encoding="utf-8") as f:
99
+ for i, line in enumerate(f):
100
+ if i % 1_000_000 == 0 and i > 0: print(f" read {i:,} lines...", flush=True)
101
+ line = line.strip()
102
+ if not line: continue
103
+ try: obj = json.loads(line)
104
+ except: continue
105
+ text = obj.get("text") or obj.get("content") or obj.get("passage", "")
106
+ if len(text) >= 30: texts.append(text)
107
+ print(f" {len(texts):,} texts in {time.time()-t0:.0f}s", flush=True)
108
+ t1 = time.time(); BATCH = 1024; PAD_ID = tokenizer.pad_id
109
+ env = lmdb.open(db_path, map_size=int(map_size_gb * (1024**3))); txn = env.begin(write=True)
110
+ count = 0; total_tokens = 0; total_batches = (len(texts) + BATCH - 1) // BATCH
111
+ for batch_idx in range(0, len(texts), BATCH):
112
+ batch = texts[batch_idx:batch_idx+BATCH]; batch_num = batch_idx // BATCH + 1
113
+ enc = tokenizer.tokenizer(batch, max_length=max_seq_len, truncation=True, padding="max_length", return_tensors="np", return_attention_mask=False)
114
+ ids_np = enc.input_ids.astype(np.int32)
115
+ for j in range(ids_np.shape[0]):
116
+ row = ids_np[j]; non_pad = int(np.sum(row != PAD_ID))
117
+ if non_pad < 10: continue
118
+ txn.put(f"sample_{count:010d}".encode(), row.tobytes()); count += 1; total_tokens += non_pad
119
+ if batch_num % 100 == 0 or batch_num == total_batches:
120
+ pct = batch_num / total_batches * 100
121
+ print(f" [{pct:5.1f}%] {count:,} samples | {total_tokens/1e6:.0f}M tok", flush=True)
122
+ if count % 500_000 < BATCH and count >= 500_000: txn.commit(); txn = env.begin(write=True)
123
+ txn.put(b"__len__", struct.pack("<Q", count)); txn.put(b"__total_tokens__", struct.pack("<Q", total_tokens))
124
+ txn.commit(); env.close()
125
+ print(f" [βœ“] LMDB: {count:,} samples, {total_tokens/1e6:.1f}M tokens in {time.time()-t1:.0f}s", flush=True)
126
+
127
+ # ── LR Schedule ──
128
+ def get_lr(step, cfg):
129
+ if step < cfg.warmup_steps: return cfg.lr * (step + 1) / cfg.warmup_steps
130
+ progress = min((step - cfg.warmup_steps) / max(cfg.max_steps - cfg.warmup_steps, 1), 1.0)
131
+ return cfg.min_lr + (cfg.lr - cfg.min_lr) * 0.5 * (1.0 + math.cos(math.pi * progress))
132
+
133
+ # ── Checkpoint Manager ──
134
+ class CheckpointManager:
135
+ def __init__(self, save_dir, keep_last=5):
136
+ self.save_dir = Path(save_dir); self.save_dir.mkdir(parents=True, exist_ok=True); self.keep_last = keep_last
137
+
138
+ def save(self, model, optimizer, step, loss, cfg, scaler=None):
139
+ path = self.save_dir / f"nord_v4_step_{step:07d}.pt"
140
+ m = model.module if hasattr(model, 'module') else model
141
+ d = {"step": step, "loss": loss, "version": "v4.2", "model_state_dict": m.state_dict(),
142
+ "optimizer_state_dict": optimizer.state_dict(),
143
+ "config": {k: v for k, v in cfg.__dict__.items() if not k.startswith("_") and k != "dtype"}}
144
+ if scaler: d["scaler_state_dict"] = scaler.state_dict()
145
+ if USE_TPU: xm.save(d, str(path))
146
+ else: torch.save(d, path)
147
+ latest = self.save_dir / "nord_v4_latest.pt"
148
+ if latest.exists(): latest.unlink()
149
+ shutil.copy2(path, latest)
150
+ ckpts = sorted(self.save_dir.glob("nord_v4_step_*.pt"), key=lambda p: p.stat().st_mtime)
151
+ for old in ckpts[:max(0, len(ckpts) - self.keep_last)]: old.unlink()
152
+ print(f" [πŸ’Ύ] Saved: {path.name} (loss={loss:.4f})", flush=True)
153
+
154
+ def load(self, model, optimizer, device, scaler=None):
155
+ latest = self.save_dir / "nord_v4_latest.pt"
156
+ if not latest.exists():
157
+ ckpts = sorted(self.save_dir.glob("nord_v4_step_*.pt"))
158
+ latest = ckpts[-1] if ckpts else None
159
+ if latest is None: return 0
160
+ print(f" [*] Resuming from: {latest.name}", flush=True)
161
+ ckpt = torch.load(latest, map_location="cpu", weights_only=False)
162
+ m = model.module if hasattr(model, 'module') else model
163
+ filtered = {k: v for k, v in ckpt["model_state_dict"].items() if "_v_mem_state" not in k and "_i_syn_state" not in k}
164
+ m.load_state_dict(filtered, strict=False)
165
+ optimizer.load_state_dict(ckpt["optimizer_state_dict"])
166
+ if scaler and "scaler_state_dict" in ckpt: scaler.load_state_dict(ckpt["scaler_state_dict"])
167
+ print(f" [βœ“] Resumed at step {ckpt['step']:,} (loss={ckpt.get('loss', '?')})", flush=True)
168
+ return ckpt["step"]
169
+
170
+ def save_final(self, model, cfg):
171
+ path = self.save_dir / "nord_v4_final.pt"
172
+ m = model.module if hasattr(model, 'module') else model
173
+ d = {"version": "v4.2", "model_state_dict": m.state_dict(),
174
+ "config": {k: v for k, v in cfg.__dict__.items() if not k.startswith("_") and k != "dtype"}}
175
+ if USE_TPU: xm.save(d, str(path))
176
+ else: torch.save(d, path)
177
+ print(f" [⭐] Final model: {path}", flush=True)
178
+
179
+ # ── Training ──
180
+ def train(dataset_path, model_dir, lr_override=None, continued=False):
181
+ device = get_device()
182
+
183
+ # Determine LR: continued pretraining uses lower LR
184
+ base_lr = 2e-4
185
+ if continued:
186
+ base_lr = 5e-5
187
+ print(" [*] Continued pretraining mode: LR=5e-5, warmup=200", flush=True)
188
+ if lr_override is not None:
189
+ base_lr = lr_override
190
+ print(f" [*] LR override: {base_lr}", flush=True)
191
+
192
+ warmup = 200 if continued else 1000
193
+
194
+ cfg = NordConfig(
195
+ device=str(device), dtype=torch.bfloat16 if USE_TPU else torch.float16,
196
+ d_model=1536, n_heads=24, d_ff=4096, n_clusters=128, max_seq_len=192,
197
+ sensory_layers=3, association_layers=3, executive_layers=4,
198
+ T=8, T_slow=2, persistent_mem=False,
199
+ n_experts=4, top_k_experts=2,
200
+ memory_size=256, memory_tau_mem=0.99, memory_n_read_heads=8,
201
+ target_spike_rate=0.03, spike_loss_weight=0.5,
202
+ v_threshold=0.12, tau_mem=0.9, lif_freeze_steps=1000,
203
+ gradient_checkpointing=False,
204
+ batch_size=1, grad_accum=64, lr=base_lr, min_lr=1e-5,
205
+ warmup_steps=warmup, max_steps=50_000,
206
+ save_every=1000, log_every=10,
207
+ )
208
+
209
+ print(flush=True); print("═" * 60, flush=True)
210
+ print(" PROJECT NORD v4.2 β€” 700M SNN Training", flush=True); print("═" * 60, flush=True)
211
+
212
+ # ── Auto-adjust batch size ──
213
+ if USE_TPU:
214
+ print(f" Device: TPU ({device})", flush=True)
215
+ print(f" Precision: bfloat16 (native)", flush=True)
216
+ cfg.batch_size = 8; cfg.grad_accum = 4
217
+ print(f" [Auto] batch=8, accum=4 (TPU)", flush=True)
218
+ elif torch.cuda.is_available():
219
+ vram = torch.cuda.get_device_properties(0).total_memory / (1024**3)
220
+ n_gpus = torch.cuda.device_count()
221
+ total_vram = vram * n_gpus
222
+ print(f" GPU: {torch.cuda.get_device_name()} ({vram:.1f}GB)" + (f" Γ— {n_gpus} = {total_vram:.1f}GB total" if n_gpus > 1 else ""), flush=True)
223
+ if vram < 16:
224
+ print(" [ERROR] Need β‰₯16GB VRAM per GPU!", flush=True); sys.exit(1)
225
+
226
+ print(f" Arch: d={cfg.d_model}, h={cfg.n_heads}, ff={cfg.d_ff}", flush=True)
227
+ print(f" Zones: S({cfg.sensory_layers})→A({cfg.association_layers},MoE)→M→E({cfg.executive_layers})", flush=True)
228
+
229
+ tokenizer = NordTokenizer(cfg)
230
+ db_path = str(Path(dataset_path).with_suffix("")) + "_lmdb"
231
+ if not Path(db_path).exists(): build_lmdb(dataset_path, db_path, tokenizer, cfg.max_seq_len)
232
+ dataset = LMDBDataset(db_path, cfg.max_seq_len)
233
+
234
+ print(f"\n [*] Building Nord v4 model...", flush=True)
235
+ model = NordModel(cfg).to(device)
236
+ print(f" [βœ“] {model.count_params()}", flush=True)
237
+
238
+ # Multi-GPU (CUDA only)
239
+ n_gpus = 1
240
+ if not USE_TPU and torch.cuda.is_available() and torch.cuda.device_count() > 1:
241
+ from torch.nn.parallel import DataParallel
242
+ n_gpus = torch.cuda.device_count()
243
+ print(f" [⚑] {n_gpus} GPUs β†’ DataParallel", flush=True)
244
+ model = DataParallel(model)
245
+
246
+ # ── Smart VRAM auto-tuning: probe batch sizes to fill 85% VRAM ──
247
+ if not USE_TPU and torch.cuda.is_available():
248
+ TARGET_VRAM_PCT = 0.85 # Fill 85% of VRAM
249
+ EFF_BATCH_TARGET = 32 # Target effective batch size
250
+
251
+ vram_total = torch.cuda.get_device_properties(0).total_memory
252
+ vram_after_model = torch.cuda.memory_allocated()
253
+ vram_free = vram_total - vram_after_model
254
+ print(f"\n [*] Smart VRAM auto-tuning...", flush=True)
255
+ print(f" Total VRAM (per GPU): {vram_total/(1024**3):.1f}GB", flush=True)
256
+ print(f" Model + optimizer: {vram_after_model/(1024**3):.1f}GB", flush=True)
257
+ print(f" Available: {vram_free/(1024**3):.1f}GB", flush=True)
258
+ print(f" Target fill: {TARGET_VRAM_PCT:.0%}", flush=True)
259
+
260
+ # Probe increasing batch sizes with a dummy forward+backward
261
+ best_batch = 1
262
+ test_seq_len = cfg.max_seq_len
263
+ model.train()
264
+
265
+ # Create temporary optimizer for probing
266
+ temp_optim = torch.optim.AdamW(model.parameters(), lr=1e-4)
267
+ temp_scaler = torch.amp.GradScaler("cuda", enabled=(cfg.dtype == torch.float16))
268
+
269
+ for test_batch in [1, 2, 3, 4, 6, 8, 10, 12, 16]:
270
+ # For DataParallel, total batch = test_batch, split across GPUs
271
+ # Each GPU gets test_batch // n_gpus, need at least 1 per GPU
272
+ per_gpu = test_batch // n_gpus if n_gpus > 1 else test_batch
273
+ if per_gpu < 1: continue
274
+
275
+ torch.cuda.empty_cache()
276
+ torch.cuda.reset_peak_memory_stats()
277
+
278
+ try:
279
+ dummy_ids = torch.randint(0, 1000, (test_batch, test_seq_len), device=device)
280
+ with torch.amp.autocast(device_type="cuda", dtype=torch.float16, enabled=(cfg.dtype == torch.float16)):
281
+ logits, stats = model(dummy_ids)
282
+ loss = logits[:, :-1, :].contiguous().reshape(-1, cfg.vocab_size).mean()
283
+ temp_scaler.scale(loss).backward()
284
+ temp_scaler.unscale_(temp_optim)
285
+ temp_scaler.step(temp_optim)
286
+ temp_scaler.update()
287
+ temp_optim.zero_grad(set_to_none=True)
288
+
289
+ peak = torch.cuda.max_memory_allocated()
290
+ pct = peak / vram_total
291
+ print(f" batch={test_batch:>2} β†’ peak {peak/(1024**3):.1f}GB ({pct:.0%})", flush=True)
292
+
293
+ if pct <= TARGET_VRAM_PCT:
294
+ best_batch = test_batch
295
+ else:
296
+ # Exceeded target, stop probing
297
+ break
298
+ except RuntimeError as e:
299
+ if "out of memory" in str(e).lower():
300
+ torch.cuda.empty_cache()
301
+ print(f" batch={test_batch:>2} β†’ OOM!", flush=True)
302
+ break
303
+ else:
304
+ raise
305
+
306
+ # Clean up probe state
307
+ del temp_optim, temp_scaler
308
+ torch.cuda.empty_cache()
309
+
310
+ # Re-initialize model weights since probe corrupted them
311
+ model_to_reinit = model.module if hasattr(model, 'module') else model
312
+ model_to_reinit.__init__(cfg)
313
+ model_to_reinit.to(device)
314
+ if hasattr(model, 'module'):
315
+ # Re-wrap in DataParallel
316
+ model = DataParallel(model_to_reinit)
317
+
318
+ cfg.batch_size = best_batch
319
+ cfg.grad_accum = max(1, EFF_BATCH_TARGET // best_batch)
320
+ eff = cfg.batch_size * cfg.grad_accum
321
+
322
+ print(f"\n [βœ“] Auto-tuned: batch={cfg.batch_size}, accum={cfg.grad_accum}, effective={eff}", flush=True)
323
+ print(f" VRAM utilization: ~{TARGET_VRAM_PCT:.0%} target", flush=True)
324
+
325
+ # Rebuild dataloader with tuned batch size
326
+ if USE_TPU:
327
+ dataloader = DataLoader(dataset, batch_size=cfg.batch_size, shuffle=True, num_workers=4, drop_last=True)
328
+ else:
329
+ dataloader = DataLoader(dataset, batch_size=cfg.batch_size, shuffle=True, num_workers=2, pin_memory=True, drop_last=True, persistent_workers=True)
330
+
331
+ print(f" Eff batch: {cfg.batch_size}Γ—{cfg.grad_accum}={cfg.batch_size*cfg.grad_accum}", flush=True)
332
+ print(f" LR: {cfg.lr}β†’{cfg.min_lr} (cosine, {cfg.warmup_steps} warmup)", flush=True)
333
+
334
+ optimizer = torch.optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay, betas=(0.9, 0.95))
335
+ scaler = None
336
+ if not USE_TPU and cfg.dtype == torch.float16:
337
+ scaler = torch.amp.GradScaler("cuda", enabled=True)
338
+
339
+ ckpt_mgr = CheckpointManager(model_dir)
340
+ start_step = ckpt_mgr.load(model, optimizer, device, scaler)
341
+
342
+ model.train(); data_iter = iter(dataloader)
343
+ running_loss = 0.0; running_spike_loss = 0.0; tokens_seen = 0; t_start = time.time()
344
+
345
+ print(f"\n {'─'*55}", flush=True)
346
+ print(f" Start step {start_step:,} | {len(dataset):,} samples | {'TPU' if USE_TPU else 'CUDA'}", flush=True)
347
+ print(f" Ctrl+C = save & stop", flush=True)
348
+ print(f" {'─'*55}\n", flush=True)
349
+
350
+ try:
351
+ for step in range(start_step, cfg.max_steps):
352
+ accum_loss = 0.0; accum_spike_loss = 0.0; stats = {}
353
+ for _ in range(cfg.grad_accum):
354
+ try: input_ids = next(data_iter)
355
+ except StopIteration: data_iter = iter(dataloader); input_ids = next(data_iter)
356
+ input_ids = input_ids.to(device)
357
+
358
+ if USE_TPU:
359
+ with torch.autocast(device_type="xla", dtype=torch.bfloat16):
360
+ logits, stats = model(input_ids)
361
+ ce_loss = F.cross_entropy(logits[:, :-1, :].contiguous().reshape(-1, cfg.vocab_size),
362
+ input_ids[:, 1:].contiguous().reshape(-1), ignore_index=tokenizer.pad_id)
363
+ spike_loss = stats.get("spike_loss", torch.tensor(0.0, device=device))
364
+ if isinstance(spike_loss, torch.Tensor):
365
+ if spike_loss.dim() > 0: spike_loss = spike_loss.mean()
366
+ else:
367
+ spike_loss = torch.tensor(float(spike_loss), device=device)
368
+ moe_lb = stats.get("moe_lb_loss", torch.tensor(0.0, device=device))
369
+ if isinstance(moe_lb, torch.Tensor):
370
+ if moe_lb.dim() > 0: moe_lb = moe_lb.mean()
371
+ else:
372
+ moe_lb = torch.tensor(float(moe_lb), device=device)
373
+ loss = (ce_loss + spike_loss + 0.01 * moe_lb) / cfg.grad_accum
374
+ loss.backward()
375
+ else:
376
+ with torch.amp.autocast(device_type="cuda", dtype=torch.float16, enabled=(cfg.dtype == torch.float16)):
377
+ logits, stats = model(input_ids)
378
+ ce_loss = F.cross_entropy(logits[:, :-1, :].contiguous().reshape(-1, cfg.vocab_size),
379
+ input_ids[:, 1:].contiguous().reshape(-1), ignore_index=tokenizer.pad_id)
380
+ spike_loss = stats.get("spike_loss", torch.tensor(0.0, device=device))
381
+ if isinstance(spike_loss, torch.Tensor):
382
+ if spike_loss.dim() > 0: spike_loss = spike_loss.mean()
383
+ else:
384
+ spike_loss = torch.tensor(float(spike_loss), device=device)
385
+ moe_lb = stats.get("moe_lb_loss", torch.tensor(0.0, device=device))
386
+ if isinstance(moe_lb, torch.Tensor):
387
+ if moe_lb.dim() > 0: moe_lb = moe_lb.mean()
388
+ else:
389
+ moe_lb = torch.tensor(float(moe_lb), device=device)
390
+ loss = (ce_loss + spike_loss + 0.01 * moe_lb) / cfg.grad_accum
391
+ scaler.scale(loss).backward()
392
+
393
+ accum_loss += ce_loss.item() / cfg.grad_accum
394
+ sp_item = spike_loss.item() if isinstance(spike_loss, torch.Tensor) else float(spike_loss)
395
+ accum_spike_loss += sp_item / cfg.grad_accum
396
+ tokens_seen += input_ids.numel()
397
+
398
+ # Optimizer step
399
+ if USE_TPU:
400
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.max_grad_norm)
401
+ xm.optimizer_step(optimizer)
402
+ optimizer.zero_grad(set_to_none=True)
403
+ else:
404
+ scaler.unscale_(optimizer)
405
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.max_grad_norm)
406
+ scaler.step(optimizer); scaler.update()
407
+ optimizer.zero_grad(set_to_none=True)
408
+
409
+ lr = get_lr(step, cfg)
410
+ for pg in optimizer.param_groups: pg["lr"] = lr
411
+ running_loss += accum_loss; running_spike_loss += accum_spike_loss
412
+
413
+ # Logging
414
+ if step % cfg.log_every == 0 and step > start_step:
415
+ avg = running_loss / cfg.log_every; avg_sp = running_spike_loss / cfg.log_every
416
+ tps = tokens_seen / (time.time() - t_start) / 1000
417
+ # Handle both tensor and float stats (DataParallel returns averaged tensors)
418
+ sp = stats.get("sparsity", 0)
419
+ if isinstance(sp, torch.Tensor): sp = sp.mean().item()
420
+ mem_r = stats.get("memory_spike_rate", None)
421
+ if isinstance(mem_r, torch.Tensor): mem_r = mem_r.mean().item()
422
+ mem_s = f" | mem={mem_r:.3f}" if mem_r is not None else ""
423
+ gn = grad_norm.item() if isinstance(grad_norm, torch.Tensor) else grad_norm
424
+ dev = " | TPU" if USE_TPU else (f" | VRAM {torch.cuda.memory_allocated()/(1024**3):.1f}G" if torch.cuda.is_available() else "")
425
+ print(f" step {step:>7,} β”‚ loss {avg:.4f} β”‚ spike_L {avg_sp:.4f} β”‚ lr {lr:.1e} β”‚ grad {gn:.1f} β”‚ sparsity {sp:.0%} β”‚ {tps:.1f}k tok/s{mem_s}{dev}", flush=True)
426
+ running_loss = 0.0; running_spike_loss = 0.0
427
+
428
+ if step % 100 == 0 and step > start_step:
429
+ print(f" {'Β·'*50}", flush=True)
430
+ # Handle spike_rates as tensor (DataParallel) or list
431
+ sr = stats.get("spike_rates_tensor", stats.get("spike_rates", []))
432
+ if isinstance(sr, torch.Tensor):
433
+ sr = sr.float()
434
+ if sr.dim() > 1: sr = sr.mean(dim=0) # average across DataParallel replicas
435
+ sr = sr.tolist()
436
+ if sr:
437
+ ns = cfg.sensory_layers + 1; na = cfg.association_layers
438
+ print(f" Sensory spike rates: {[f'{r:.4f}' for r in sr[:ns]]}", flush=True)
439
+ print(f" Association spike rates: {[f'{r:.4f}' for r in sr[ns:ns+na]]}", flush=True)
440
+ print(f" Executive spike rates: {[f'{r:.4f}' for r in sr[ns+na:]]}", flush=True)
441
+ gate = stats.get("gate_activity"); mix = stats.get("memory_mix")
442
+ if isinstance(gate, torch.Tensor): gate = gate.mean().item()
443
+ if isinstance(mix, torch.Tensor): mix = mix.mean().item()
444
+ if gate is not None: print(f" Memory gate={gate:.4f} mix={mix:.4f}", flush=True)
445
+ print(f" {'Β·'*50}", flush=True)
446
+
447
+ if step > 0 and step % cfg.save_every == 0:
448
+ ckpt_mgr.save(model, optimizer, step, accum_loss, cfg, scaler)
449
+
450
+ except KeyboardInterrupt:
451
+ print(f"\n\n [⏸] Stopped at step {step:,}", flush=True)
452
+ ckpt_mgr.save(model, optimizer, step, accum_loss, cfg, scaler)
453
+
454
+ ckpt_mgr.save_final(model, cfg)
455
+ print(f"\n {'═'*55}\n Training complete! Model: {model_dir}\n {'═'*55}", flush=True)
456
+
457
+ def main():
458
+ parser = argparse.ArgumentParser()
459
+ parser.add_argument("--tpu", action="store_true", help="Force TPU backend")
460
+ parser.add_argument("--dataset", type=str, default=None)
461
+ parser.add_argument("--model_dir", type=str, default=None)
462
+ parser.add_argument("--lr", type=float, default=None, help="Override learning rate (e.g. 5e-5 for continued pretraining)")
463
+ parser.add_argument("--continued", action="store_true", help="Continued pretraining mode: auto LR=5e-5, shorter warmup")
464
+ args = parser.parse_args()
465
+
466
+ print("=" * 60, flush=True)
467
+ print(" PROJECT NORD v4.2 β€” Brain-Inspired SNN Training", flush=True)
468
+ print("=" * 60, flush=True)
469
+ detect_backend(force_tpu=args.tpu)
470
+
471
+ if args.dataset: dataset_path = args.dataset
472
+ else:
473
+ d = "train_data.jsonl"
474
+ print(f"\n Dataset? (Enter = {d})", flush=True)
475
+ inp = input(" Dataset: ").strip(); dataset_path = inp if inp else d
476
+ if not Path(dataset_path).exists(): print(f" [βœ—] Not found: {dataset_path}", flush=True); sys.exit(1)
477
+
478
+ if args.model_dir: model_dir = args.model_dir
479
+ else:
480
+ d = "nord_v4_700m"
481
+ print(f"\n Model dir? (Enter = {d})", flush=True)
482
+ inp = input(" Model dir: ").strip(); model_dir = inp if inp else d
483
+
484
+ train(dataset_path, model_dir, lr_override=args.lr, continued=args.continued)
485
+
486
+ if __name__ == "__main__":
487
+ main()