Maggio33 commited on
Commit
d17e08e
·
verified ·
1 Parent(s): d056906

Slayer1B 1.00 weights: step 1430 (ckpt sha 7d092d4d), exported 1:1 from the trainer checkpoint

Browse files
Files changed (6) hide show
  1. SHA256SUMS +5 -0
  2. config.json +28 -0
  3. load_slayer.py +38 -0
  4. model.safetensors +3 -0
  5. tokenizer.json +0 -0
  6. train_gpt_ref_r23.py +1321 -0
SHA256SUMS ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ 55445c12307a780e841b234bc381865a1ec9b301dc9e5a76c85fff1e8130a111 config.json
2
+ 53a04c726bebbf8d8a25ff1479ab4f70c3003022675d87332d130139cca1d020 load_slayer.py
3
+ 1c97c0527e1cff6baf3a07539a1947469b476769891399c505854824ebd7a0e6 model.safetensors
4
+ 0640d3bd3674d7a6e59c540d945a2ad275a12e1bcaeb6007c37a90751f9815dd tokenizer.json
5
+ db36c150e95c184e82b5f84c3ee783fd5fd2c62e960a438177f543dc3eba1d0b train_gpt_ref_r23.py
config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "attn_bias": 0,
3
+ "block": 4096,
4
+ "ckpt_sha256": "7d092d4d0bf838da1115a5369bf0a6729fe4c170e0d5ad303873f7320d6c1866",
5
+ "data_seed": 20261023,
6
+ "ffn": "swiglu",
7
+ "ffn_mult": 2.667,
8
+ "logit_cap": 0.0,
9
+ "n_embd": 2048,
10
+ "n_head": 16,
11
+ "n_layer": 20,
12
+ "n_params": 1073912851,
13
+ "norm": "rmsnorm",
14
+ "norm_eps": 1e-06,
15
+ "pos": "rope",
16
+ "qk_norm": true,
17
+ "rope_theta": 500000.0,
18
+ "step": 1430,
19
+ "tied_weights": {
20
+ "tok.weight": "head.weight"
21
+ },
22
+ "tokenizer_sha256": "0640d3bd3674d7a6e59c540d945a2ad275a12e1bcaeb6007c37a90751f9815dd",
23
+ "trainer": "train_gpt_ref_r23.py",
24
+ "trainer_sha256": "db36c150e95c184e82b5f84c3ee783fd5fd2c62e960a438177f543dc3eba1d0b",
25
+ "value_residual": true,
26
+ "vocab": 32768,
27
+ "z_loss": 0.0
28
+ }
load_slayer.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load Slayer weights (model.safetensors + config.json) into the GPT class of train_gpt_ref_r23.py.
2
+
3
+ Usage:
4
+ import load_slayer
5
+ model = load_slayer.load("path/to/Slayer1B-1.00") # float32, eval mode, on CPU
6
+ logits, _ = model(token_ids) # token_ids: LongTensor [batch, seq], seq <= config["block"]
7
+ Tokenizer: tokenizers.Tokenizer.from_file("tokenizer.json"); id 0 is end-of-text.
8
+ """
9
+ import importlib.util
10
+ import json
11
+ import os
12
+ from types import SimpleNamespace
13
+
14
+ from safetensors.torch import load_file
15
+
16
+
17
+ def _trainer(path):
18
+ spec = importlib.util.spec_from_file_location("train_gpt_ref_r23", os.path.join(path, "train_gpt_ref_r23.py"))
19
+ mod = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(mod)
21
+ return mod
22
+
23
+
24
+ def build(cfg, path):
25
+ """Empty model with the architecture from config (no weights loaded)."""
26
+ c = SimpleNamespace(**cfg)
27
+ return _trainer(path).GPT(c.vocab, c.n_layer, c.n_embd, c.n_head, c.block, c)
28
+
29
+
30
+ def load(path, device="cpu"):
31
+ with open(os.path.join(path, "config.json"), encoding="utf-8") as f:
32
+ cfg = json.load(f)
33
+ model = build(cfg, path)
34
+ sd = load_file(os.path.join(path, "model.safetensors"))
35
+ for dst, src in cfg.get("tied_weights", {}).items():
36
+ sd.setdefault(src, sd[dst])
37
+ model.load_state_dict(sd, strict=True)
38
+ return model.to(device).float().eval()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c97c0527e1cff6baf3a07539a1947469b476769891399c505854824ebd7a0e6
3
+ size 4295670676
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
train_gpt_ref_r23.py ADDED
@@ -0,0 +1,1321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # NOTE (GoLLeM-v5 leaderboard repo): this is a GENERAL nanoGPT-style causal transformer;
3
+ # vocab/dtype are CLI-parameterized. The crown/Path-B leaderboard checkpoints were trained in
4
+ # BPE-12k mode: --vocab 12288 --dtype uint16 (NOT the byte-level default below). The header
5
+ # doc-comment reflects the file origin as a standard-GPT control vs experimental BDH; the
6
+ # leaderboard models are the STANDARD transformer in BPE mode and do NOT use BDH.
7
+ # -*- coding: utf-8 -*-
8
+ """
9
+ Referencyjny ZWYKLY transformer (byte-level nanoGPT-style) ~25M — apples-to-apples vs BDH-25M.
10
+ Ta sama data (train.bin/val.bin uint8), ten sam scale (~25M), ten sam byte-level (vocab256).
11
+ Rozni sie TYLKO architektura (standard causal transformer vs BDH fast-weights) -> czysta referencja.
12
+
13
+ CLI mirror train_bdh.py. GPU ROCm/CUDA bf16, cosine+warmup+clip, ckpt/resume, logging.
14
+ Autor: Hart (N-02).
15
+
16
+ Smoke throughput (bez danych PII, syntetyczny bufor): --synthetic --steps 60
17
+ Realny: --data-dir . --run-id gpt25m_run1 --steps 30000
18
+ r8 (Monter): wiele GPU przez torchrun (DDP). --batch = batch GLOBALNY; kazdy rank bierze swoje wiersze z tego samego
19
+ batcha (te same starty okien co przebieg 1-GPU), gradienty usredniane przez DDP, wiec krok = krok 1-GPU z tym samym
20
+ --batch (do numeryki). Logi, metryki, zdarzenia i ckpt pisze tylko rank 0. Bez torchrun (WORLD_SIZE=1) = r7 bez zmian.
21
+ torchrun --standalone --nproc-per-node 4 train_gpt_ref_r8.py ... --batch 64
22
+ r11 (Monter, 10.10, Wzrost etap 1b: dlugie okna): dwie opcje tylko na pamiec, domyslnie WYLACZONE (bez nich = r10 bit w bit):
23
+ --act-ckpt przeliczanie aktywacji po bloku w treningu (torch.utils.checkpoint, use_reentrant=False);
24
+ --loss-chunk N strata liczona kawalkami po N pozycji (glowica + CE w checkpoint), bez pelnych logitow [B,T,V];
25
+ forward zwraca wtedy logits=None, a strata = ta sama srednia po B*T (i ten sam z-loss).
26
+ Okno i baza RoPE z CLI dzialaja z --init-from bez zmian (RoPE bez parametrow, --pos rope nie ma pos-embeddingu).
27
+ r12 (Monter, 10.10, Slayer9-Eff): --loop K = wspoldzielenie wag miedzy warstwami (looped transformer, pomysl Kacpra 18:30Z):
28
+ n_layer unikalnych blokow przechodzi sie K razy (glebokosc liczenia n_layer*K, parametry n_layer). Tablica liczy parametry
29
+ modelu, wiec K nie zmienia rozmiaru. Blok 0 w kazdym przejsciu na nowo ustala v0 (value-residual). K w config ckpt ("loop");
30
+ --init-from sprawdza zgodnosc K. Domyslnie K=1 = r11 bit w bit (config ckpt bez klucza "loop").
31
+ r13 (Hart, 11.10, kampania 2 Slayer9, Latarnik 11:40Z): hybryda GatedDeltaNet2 (flash-linear-attention 0.5.2) + uwaga.
32
+ --mixer-layout STR litera na blok: A = uwaga jak r12, G = fla.layers.gdn2.GatedDeltaNet2 (hidden d, head_dim
33
+ --gdn-head-dim, num_heads d/head_dim, expand_v 1, short conv 4, mode chunk) w miejsce uwagi; FFN,
34
+ normy i reszta bloku bez zmian. Domyslnie brak = same A = r12 bit w bit (config bez nowych kluczy).
35
+ Value-residual: v0 ustala PIERWSZY blok A (w r12 blok 0). Wagi G inicjalizuje fla (bez _init GPT).
36
+ Optymalizator: w blokach G Muon tylko q/k/v/o_proj; reszta G (konwolucje, bramki, A_log, dt_bias,
37
+ normy) -> AdamW (karta kampanii 2: czesc „budowy Q-ARCH”, ustalona przed biegiem).
38
+ --attn-bias 0|1 bias w qkv/proj uwagi (domyslnie 1 jak r12; Qielbas M2 = 0).
39
+ --rope-style interleaved (r12) | half (rotate-half jak Qielbas M2; baza z --rope-theta).
40
+ Import fla tylko przy G w ukladzie (PYTHONPATH z fla-lib), wiec same A dzialaja bez fla.
41
+ r14 (Hart, 11.10, kampania 3 Slayer9: KD w pretreningu, Arek 15:56Z): destylacja od nauczyciela na zywo, domyslnie WYLACZONA
42
+ (bez --kd-teacher = r13 bit w bit).
43
+ --kd-teacher CKPT --kd-teacher-sha HEX64 ckpt nauczyciela (r12/r13, ten sam trener i vocab co uczen; sha pelne, inaczej
44
+ STOP). Nauczyciel budowany z config ckpt, eval, bez gradientu, forward w tym samym autocast co uczen
45
+ (bf16), w kazdym kroku na tym samym batchu. Budowa nauczyciela nie zuzywa globalnego RNG (fork_rng).
46
+ --kd-alpha A strata = (1-A)*CE + A*T^2*KL(p_naucz || p_ucz), obie srednie po B*T pozycjach; KL po pelnym vocab,
47
+ w fp32, z logitow /T (uczen po --logit-cap, jak w CE). Z --loss-chunk liczone tymi samymi kawalkami.
48
+ --kd-temp T temperatura (domyslnie 1.0).
49
+ Walidacja (VAL loss) = sama CE jak w r13. W logu kroku dodatkowo ce (sama CE). W ckpt klucz "kd" (poza "config",
50
+ wiec valport/--init-from bez zmian).
51
+ r15 (Hart, 11.10, Arek 21:33Z „eksperymenty do skutku”, przeglad arXiv #4): faktoryzowane osadzenia, domyslnie WYLACZONE
52
+ (bez --emb-dim = r14 bit w bit).
53
+ --emb-dim E tok = Embedding(V, E) -> emb_up Linear(E, d); glowica: emb_down Linear(d, E) -> head Linear(E, V)
54
+ zwiazana z tok (te same wagi V x E). P osadzen V*E + 2*E*d zamiast V*d. emb_up/emb_down: init
55
+ N(0, 1/E) i N(0, 1/d) (skala strumienia jak bez faktoryzacji), optymalizator jak macierze ukryte (Muon).
56
+ W config ckpt "emb_dim" tylko przy E > 0.
57
+ r16 (Hart, 11.10, kolejka nocna pkt 4 Latarnika: biegi ~V1 po 1 na karte): akumulacja gradientu, domyslnie WYLACZONA
58
+ (bez --grad-accum = r15 bit w bit).
59
+ --grad-accum K wiersze ranku (batch / world) dzielone na K rownych, ciaglych mikrobatchy; backward (strata_j / K),
60
+ pod DDP no_sync poza ostatnim. Te same wiersze co bez akumulacji (sampler globalny), wiec krok =
61
+ krok bez akumulacji z dokladnoscia do kolejnosci sumowania. Strata logu = srednia mikrobatchy.
62
+ r17 (Hart, 12.10, karta Q-REPRO Mierniczego a92e717c: replika przepisu Qielbas M2): opcje optymalizatora z ich
63
+ `pretraining-report.json`, wszystkie domyslnie WYLACZONE (bez nich = r16 bit w bit).
64
+ --muon-scale match_rms_adamw skala kroku Muon jak torch.optim.Muon(adjust_lr_fn="match_rms_adamw"):
65
+ 0.2 * sqrt(max(wiersze, kolumny)) zamiast max(1, wiersze/kolumny)^0.5; z --muon-lr = --lr
66
+ (ich „peak lr=0.001” wspolne dla Muon i AdamW).
67
+ --muon-wd W decoupled weight decay na parametrach Muon (p *= 1 - lr*W, lr przed skalowaniem, jak torch);
68
+ domyslnie 0 (jak dotad: WD tylko na AdamW-aux).
69
+ --tf32 0 TF32 wylaczone w matmul/cudnn (ich „TF32 disabled”); domyslnie 1 (jak dotad).
70
+ Harmonogram cosine -> min-lr, warmup, swiezy optymalizator przy --init-from: bez zmian (juz byly).
71
+ r18 (Hart, 13.10, ramie TC-arch po krzywej rozmiaru). Fragmenty XSA, partial RoPE, ReLU^2 i zero-init ADAPTED FROM
72
+ kurcontko/Tiny-Chick-10M@60d32c4d tinylm_arch.py (sha256 4876824d...), Apache License 2.0
73
+ (https://www.apache.org/licenses/LICENSE-2.0); zmiany: wpiecie w Block/GPT r17, flagi CLI, walidacja, config ckpt.
74
+ wszystkie domyslnie WYLACZONE (bez nich = r17 bit w bit, config ckpt bez nowych kluczy):
75
+ --xsa 1 po uwadze z wyjscia kazdej glowy usuwana skladowa wzdluz jej (znormalizowanego) wektora value tokenu:
76
+ y -= (y . v^) v^ (v = to, co weszlo do SDPA, czyli po value-residual).
77
+ --rope-dims N RoPE tylko na pierwszych N wymiarach glowy (parzyste, 0 < N < head_dim; czestotliwosci liczone z N),
78
+ reszta wymiarow bez pozycji; 0 = wszystkie (jak r17). Styl interleaved/half bez zmian, w obrebie N.
79
+ --ffn relu2 MLP down(relu(up(x))^2) bez biasu, hidden = round(--ffn-mult * d) (Tiny Chick: mult 4); gelu/swiglu bez zmian.
80
+ --zero-init 1 po inicjalizacji zerowane wagi wyjscia uwagi (proj, bloki A) i wyjscia MLP (down / ostatni Linear, kazdy blok);
81
+ bloki G zostaja z inicjalizacja fla.
82
+ --init-from i nauczyciel KD: config ckpt bez kluczy r18 = wartosci domyslne; rozjazd xsa/rope_dims/ffn z argumentami = STOP.
83
+ r19 (Hart, 13.10, karta TC-REPLIKA): backend --arch tinylm = model z ORYGINALNEGO pliku kurcontko/Tiny-Chick-10M@60d32c4d
84
+ tinylm_arch.py (sha256 4876824de8c697a6..., Apache License 2.0, uzyty bez zmian, ladowany z --tinylm-file i sprawdzany po
85
+ sha; NOTICE: "derived from kurcontko/Tiny-Chick-10M"). Bez --arch tinylm = r18 bit w bit (config ckpt bez nowych kluczy).
86
+ Z --arch tinylm: GPT trzyma TinyLM jako self.tl (klucze state_dict "tl.<ich nazwa>"); forward zwraca (logity, CE) jak GPT.
87
+ TinyConfig z argumentow: vocab, n_layer, n_embd, n_head, --kv-heads, --ffn relu2|swiglu, --ffn-mult, --qk-norm,
88
+ --rope-dims, --rope-theta, --xsa, --zero-init, --norm-eps, --block. Optymalizator Muon: tl.emb -> AdamW (jak tok/head),
89
+ skalarny gain QK-norm (ndim 3) -> AdamW, reszta 2D -> Muon. Niezgodne z tinylm (STOP): --loop > 1, --mixer-layout,
90
+ --emb-dim, --value-residual, --kd-teacher, --act-ckpt, --loss-chunk, --logit-cap, --z-loss, --pos learned, --rope-style
91
+ interleaved, --norm layernorm, --attn-bias 1.
92
+ r20 (Hart, 13.10): torch.load ckpt przy --resume i --init-from z weights_only=True (uwaga Wartownika do r18 l. 832:
93
+ weights_only=False wykonuje dowolny pickle). Ckpt trenera (model / opt / step / config / queue) laduje sie tak bez zmian
94
+ (sprawdzone na ba223162). Zapis ckpt po ostatnim kroku biegu także bez --queue, gdy end_step % ckpt_every != 0
95
+ (r18/r19: tylko z --queue; CPT k9 13.10 skonczyl z ckpt 1140 zamiast 1145). Poza tym r20 = r19 bit w bit.
96
+ r21 (Monter dla Harta, 14.10, Axolote maggi-uxz6, przepis_axolote_v0 2.2): --train-only REGEX, domyslnie WYLACZONE (bez
97
+ flagi = r20 bit w bit, ckpt bez nowych kluczy). Parametry, ktorych nazwa nie pasuje do REGEX (re.search), maja
98
+ requires_grad False i nie trafiaja do optymalizatora (muon juz je pomijal, adamw filtruje), wiec zostaja bit w bit.
99
+ Tylko z --init-from; regex bez trafien albo chwytajacy osadzenia / tied head = STOP. Wzor: ft_arc11.py --train-only.
100
+ W ckpt klucz "train_only" {regex, n_trainable} poza "config" (jak "kd"), wiec --init-from i valport bez zmian.
101
+ r22 (Hart, 17.10, maggi-v38t / karta maggi-jyoy): --imha-t T, domyslnie 0 = WYLACZONE (r21 bit w bit, ckpt bez nowych kluczy).
102
+ T >= 1: podwarstwa uwagi blokow A = IMHA (Divakar i Priyadarshini, TAI 2(1) 2026, doi 10.53941/tai.2026.100014, Algorytm 1):
103
+ h0 = ln1(x); T iteracji tymi samymi qkv / proj: A = softmax(QK^T/sqrt(dk) + M) jawnie (fp32), z = AV; entropia wiersza
104
+ per glowa H_i znormalizowana przez log(i+1) (H_0 = 0); w_i = exp(-H_i/tau) / srednia prefiksu exp(-H_k/tau), k <= i;
105
+ z_i *= w_i; z = proj([z_h]); g_i = srednia prefiksu z; h += sigmoid(W_a[z;g]) * relu(W_f[z;g]); wyjscie x += h_T - h0.
106
+ Nowe parametry na blok: imha_a, imha_f (Linear 2d -> d, bez biasu: w artykule W in R^{2d x d}) i imha_log_tau
107
+ (tau = exp(log_tau) > 0, init tau = 1; artykul: "learnable temperature per layer, initialised at 1").
108
+ Decyzje poza artykulem (nasz blok ma wiecej niz MHA z artykulu): qk-norm i RoPE stosowane w kazdej iteracji jak w r21;
109
+ value residual: v0 ustala blok pierwszy w iteracji 0, kolejne iteracje i bloki mieszaja v z v0 jak w r21.
110
+ STOP: --imha-t z --xsa, --mixer-layout (bloki G), --arch tinylm. Atrybut testowy cfg.imha_plain (tylko test_r22.py):
111
+ entropia, kontekst globalny i bramka wylaczone -> przy T = 1 ma byc rowne SDPA r21 (pozytywna kontrola z artykulu, 3.3.7).
112
+ Koszt: jawna macierz uwagi B x H x N x N zamiast SDPA (wolniej i wiecej pamieci) -> rownowazenie kosztu z tok/s z pomiaru.
113
+ r23 (Hart, 20.10, karta dzien dowodow 400M, Mierniczy 19:26Z): --kv-heads dla --arch gpt (GQA), domyslnie 0 = r22 bit w bit
114
+ (te same nazwy/ksztalty wag, ten sam RNG inicjalizacji, te same wyjscia, config ckpt bez nowych kluczy; K = n_head = jak 0).
115
+ K > 0: w blokach A qkv = jeden Linear d -> d + 2*K*hd (bias wg --attn-bias, Muon jak dotad, bo 2D); q ma n_head glow,
116
+ k i v po K glow. QK-norm i RoPE na k z K glowami, value residual (v0, vr_lambda) na v z K glowami, potem k, v powielone
117
+ repeat_interleave na n_head glow (grupa = n_head / K kolejnych glow q) przed SDPA; XSA na v po powieleniu (per glowa q).
118
+ Bloki G bez zmian. W config ckpt "kv_heads" tylko przy K > 0; --init-from sprawdza kv_heads (brak klucza = 0), nauczyciel
119
+ KD czyta go z configu. STOP: n_head % K != 0, K > n_head, K < 0, --kv-heads z --imha-t.
120
+ --data-seed S (karta dowodów v0.1 pkt 2, Mierniczy 20:56Z): ziarno kolejności danych (generator prefetchera i samplery
121
+ epoch / queue) osobno od --seed (inicjalizacja modelu); domyślnie brak = --seed, czyli r22 bit w bit. Ramiona R × 3
122
+ różnią się --seed przy stałym --data-seed. W config ckpt "data_seed" tylko, gdy podane; --resume z innym = STOP.
123
+ """
124
+ import argparse
125
+ import hashlib
126
+ import json
127
+ import math
128
+ import os
129
+ import re
130
+ import time
131
+ import queue
132
+ import threading
133
+ from contextlib import nullcontext
134
+
135
+ import numpy as np
136
+ import torch
137
+ import torch.distributed as dist
138
+ import torch.nn as nn
139
+ import torch.nn.functional as F
140
+ from torch.utils.checkpoint import checkpoint
141
+ from torch.nn.parallel import DistributedDataParallel as DDP
142
+
143
+
144
+ def get_args():
145
+ p = argparse.ArgumentParser()
146
+ p.add_argument("--data-dir", default=".")
147
+ p.add_argument("--out-dir", default=None)
148
+ p.add_argument("--run-id", default="gpt25m_run1")
149
+ p.add_argument("--steps", type=int, default=30000)
150
+ p.add_argument("--batch", type=int, default=32)
151
+ p.add_argument("--block", type=int, default=256)
152
+ p.add_argument("--n-layer", type=int, default=8)
153
+ p.add_argument("--n-embd", type=int, default=512)
154
+ p.add_argument("--n-head", type=int, default=8)
155
+ p.add_argument("--lr", type=float, default=6e-4)
156
+ p.add_argument("--min-lr", type=float, default=6e-5)
157
+ p.add_argument("--warmup", type=int, default=200)
158
+ p.add_argument("--schedule", choices=["cosine", "wsd"], default="cosine",
159
+ help="cosine (default) | wsd (warmup-stable-decay: constant peak then 1-sqrt decay, JugnuLM ARC-closer)")
160
+ p.add_argument("--decay-frac", type=float, default=0.2,
161
+ help="WSD: fraction of steps in the final decay phase (default 0.2)")
162
+ p.add_argument("--rewarm-steps", type=int, default=0,
163
+ help="przy --resume: liniowe przejscie LR od wartosci z ckpt do harmonogramu przez N krokow (0=off)")
164
+ p.add_argument("--wd", type=float, default=0.1)
165
+ p.add_argument("--grad-clip", type=float, default=1.0)
166
+ p.add_argument("--log-every", type=int, default=50)
167
+ p.add_argument("--eval-every", type=int, default=500)
168
+ p.add_argument("--eval-iters", type=int, default=50)
169
+ p.add_argument("--ckpt-every", type=int, default=1000)
170
+ p.add_argument("--seed", type=int, default=1337)
171
+ p.add_argument("--data-seed", type=int, default=None,
172
+ help="r23: ziarno kolejności danych (domyślnie = --seed); --seed zostaje ziarnem inicjalizacji modelu")
173
+ p.add_argument("--sampler", choices=["random", "epoch"], default="random",
174
+ help="random (r5: okna ze zwracaniem, ~75%% korpusu przy 1.4 ep.) | epoch (r6: kazde okno raz na epoke, "
175
+ "per-epoka permutacja + losowe przesuniecie siatki; resume dokladny bez fast-forwardu)")
176
+ p.add_argument("--resume", action="store_true")
177
+ p.add_argument("--init-from", default=None,
178
+ help="r10 (Wzrost): start z wag innego ckpt (np. z grow_depth.py), krok 0 i swiezy optymalizator; "
179
+ "ignorowane, gdy --resume wznawia wlasny ckpt.pt tego biegu")
180
+ p.add_argument("--mixer-layout", default=None,
181
+ help="r13: litera na blok (A = uwaga, G = GatedDeltaNet2), dlugosc = --n-layer; brak = same A (r12)")
182
+ p.add_argument("--gdn-head-dim", type=int, default=64, help="r13: head_dim GatedDeltaNet2")
183
+ p.add_argument("--attn-bias", type=int, choices=(0, 1), default=1, help="r13: bias w qkv/proj uwagi (1 = r12)")
184
+ p.add_argument("--rope-style", choices=("interleaved", "half"), default="interleaved",
185
+ help="r13: interleaved (r12) | half (rotate-half)")
186
+ p.add_argument("--data-offset-steps", type=int, default=0,
187
+ help="r10 (Wzrost, tylko --sampler epoch): dane od kroku N strumienia, np. dalszy ciag po ckpt "
188
+ "mniejszego modelu (ten sam batch i seed), bez powtarzania widzianych okien")
189
+ p.add_argument("--data-dir-2", default=None,
190
+ help="r10 (Wzrost, tylko --sampler epoch): od kroku strumienia --switch-step okna z DATA_DIR_2/train.bin "
191
+ "(np. swieze dokumenty rozlaczne z base), wlasna permutacja epokowa liczona od --switch-step")
192
+ p.add_argument("--switch-step", type=int, default=0, help="krok strumienia przejscia na --data-dir-2")
193
+ p.add_argument("--batch-sha", default=None,
194
+ help="r10 (Wzrost): bez treningu; sha256 paczek (x+y) w podanych krokach biegu, np. 0,24413,24414, "
195
+ "z tego samego Prefetchera co trening (kontrola strumienia), potem koniec")
196
+ p.add_argument("--synthetic", action="store_true", help="smoke throughput na losowym uint8 (bez danych)")
197
+ p.add_argument("--vocab", type=int, default=256)
198
+ p.add_argument("--dtype", default="uint8", help="bin dtype: uint8 (byte) | uint16 (BPE)")
199
+ p.add_argument("--optimizer", choices=["adamw", "muon"], default="adamw",
200
+ help="adamw (default, backward-compat) | muon (Newton-Schulz ortho dla 2D-weights + AdamW dla reszty)")
201
+ p.add_argument("--muon-lr", type=float, default=0.02,
202
+ help="peak LR dla Muon (macierzowe params); AdamW-aux uzywa --lr. Muon skalowany ta sama cosine-schedule co AdamW przez lr_mult=muon_lr/lr")
203
+ p.add_argument("--norm", choices=["layernorm", "rmsnorm"], default="layernorm",
204
+ help="layernorm (default, backward-compat) | rmsnorm (Qwen3-style, fp32-compute)")
205
+ p.add_argument("--norm-eps", type=float, default=1e-6)
206
+ p.add_argument("--pos", choices=["learned", "rope"], default="learned",
207
+ help="learned (default) | rope (parameter-free RoPE na q,k; usuwa learned pos-embedding)")
208
+ p.add_argument("--rope-theta", type=float, default=100000.0,
209
+ help="RoPE theta; top-3-board (JugnuLM/GPT-X2) uzywaja 100000 (nie-default-10K)")
210
+ p.add_argument("--ffn", choices=["gelu", "swiglu", "relu2"], default="gelu",
211
+ help="gelu (default 4x MLP) | swiglu (Qwen3 gated-MLP, hidden=--ffn-mult*d) | relu2 (r18: down(relu(up)^2), bez biasu)")
212
+ p.add_argument("--ffn-mult", type=float, default=2.667,
213
+ help="mnoznik hidden dla swiglu (~param-parity z 4x-gelu przy 8/3)")
214
+ p.add_argument("--value-residual", action="store_true",
215
+ help="ResFormer value-residuals: v_l += lambda_l*v0 (lambda init 0), ARC-targeted")
216
+ p.add_argument("--qk-norm", action="store_true",
217
+ help="Qwen3 QK-Norm: RMSNorm per-head na Q,K przed-attention (stabilnosc z Muon/high-LR)")
218
+ p.add_argument("--events-jsonl", default=None,
219
+ help="jesli podane: emituj events.jsonl (fabryka-track sidecar-format: update/evaluation/checkpoint/end)")
220
+ p.add_argument("--compile", action="store_true",
221
+ help="torch.compile model (2-3x throughput; state_dict zapisywany bez _orig_mod prefix via raw_model)")
222
+ p.add_argument("--logit-cap", type=float, default=0.0,
223
+ help="Gemma-style logit soft-cap: logits=cap*tanh(logits/cap); 0=off (prob. 30)")
224
+ p.add_argument("--z-loss", type=float, default=0.0,
225
+ help="z-loss coef: +coef*mean(logsumexp(logits)^2), stabilizuje logity; 0=off (prob. 1e-4)")
226
+ p.add_argument("--act-ckpt", action="store_true",
227
+ help="r11: przeliczanie aktywacji po bloku w treningu (pamiec ~ wejscia blokow zamiast wszystkich aktywacji)")
228
+ p.add_argument("--loss-chunk", type=int, default=0,
229
+ help="r11: strata kawalkami po N pozycji, bez pelnych logitow [B,T,V]; 0 = off (r10)")
230
+ p.add_argument("--loop", type=int, default=1,
231
+ help="r12: ile razy przejsc przez n_layer wspoldzielonych blokow (1 = bez wspoldzielenia, jak r11)")
232
+ p.add_argument("--queue", default=None,
233
+ help="r7: manifest kolejki shardow (JSON: steps_per_shard, shards{id:{lo,hi,sha256,source}}, rotations[[id..]..]). "
234
+ "Slot j = kroki [j*S,(j+1)*S) na shardzie schedule[j]; schedule = rotacje po kolei. Bez flagi = r6 bit w bit.")
235
+ p.add_argument("--queue-one-slot", action="store_true",
236
+ help="r7: trenuj tylko do konca biezacego slotu i zapisz ckpt (krok nadzorcy kolejki decyduje o nastepnym)")
237
+ p.add_argument("--kd-teacher", default=None, help="r14: ckpt nauczyciela KD (r12/r13); bez = r13 bit w bit")
238
+ p.add_argument("--kd-teacher-sha", default=None, help="r14: pelne sha256 ckpt nauczyciela (wymagane z --kd-teacher)")
239
+ p.add_argument("--kd-alpha", type=float, default=0.0, help="r14: waga KL w (1-A)*CE + A*T^2*KL; 0 < A <= 1 z nauczycielem")
240
+ p.add_argument("--kd-temp", type=float, default=1.0, help="r14: temperatura KD")
241
+ p.add_argument("--emb-dim", type=int, default=0, help="r15: faktoryzacja osadzen V x E + E x d (0 = wylaczone, r14)")
242
+ p.add_argument("--grad-accum", type=int, default=1, help="r16: mikrobatche na krok (1 = wylaczone, r15)")
243
+ p.add_argument("--muon-scale", choices=["sqrt_ratio", "match_rms_adamw"], default="sqrt_ratio",
244
+ help="r17: skala kroku Muon (sqrt_ratio = jak dotad; match_rms_adamw = torch.optim.Muon)")
245
+ p.add_argument("--muon-wd", type=float, default=0.0, help="r17: decoupled WD na parametrach Muon (0 = jak dotad)")
246
+ p.add_argument("--tf32", type=int, choices=[0, 1], default=1, help="r17: 0 = TF32 wylaczone (jak Qielbas M2)")
247
+ p.add_argument("--xsa", type=int, choices=[0, 1], default=0, help="r18: 1 = z wyjscia glowy usuwana skladowa wzdluz wlasnego v")
248
+ p.add_argument("--rope-dims", type=int, default=0, help="r18: RoPE na pierwszych N wymiarach glowy (0 = wszystkie, r17)")
249
+ p.add_argument("--zero-init", type=int, choices=[0, 1], default=0, help="r18: 1 = zerowe wagi proj uwagi i wyjscia MLP")
250
+ p.add_argument("--arch", choices=["gpt", "tinylm"], default="gpt", help="r19: tinylm = model z pliku Tiny Chick (--tinylm-file)")
251
+ p.add_argument("--tinylm-file", default=None, help="r19: tinylm_arch.py z kurcontko/Tiny-Chick-10M@60d32c4d (sha 4876824d...)")
252
+ p.add_argument("--kv-heads", type=int, default=0,
253
+ help="r19 tinylm / r23 gpt: liczba glow KV (GQA, dzielnik --n-head); 0 = --n-head (MHA)")
254
+ p.add_argument("--train-only", default=None,
255
+ help="r21: regex nazw uczonych parametrow; reszta requires_grad False i poza optymalizatorem; tylko z --init-from")
256
+ p.add_argument("--imha-t", type=int, default=0,
257
+ help="r22: IMHA z T iteracjami w blokach uwagi (0 = zwykla uwaga, r21 bit w bit); artykul: T = 2")
258
+ return p.parse_args()
259
+
260
+
261
+ # ---- Muon (Keller Jordan) --------------------------------------------------
262
+ # Ref: https://github.com/KellerJordan/Muon (modded-nanogpt). Muon = momentum
263
+ # SGD, ale update ortogonalizowany przez ~5 krokow iteracji Newtona-Schulza
264
+ # (przyblizona ortogonalizacja macierzy gradientu). Stosowany TYLKO do
265
+ # macierzowych ukrytych wag (ndim>=2: qkv/proj/mlp). Embeddingi (tok/pos), head
266
+ # (tied), LayerNorm-gains i biasy ida do zwyklego AdamW.
267
+ def zeropower_via_newtonschulz5(G, steps=5):
268
+ """Ortogonalizacja macierzy G przez quintic Newton-Schulz (bf16). Zwraca
269
+ macierz ~ U V^T z SVD(G)=U S V^T. Wspolczynniki (a,b,c) z impl. Kellera."""
270
+ assert G.ndim == 2
271
+ a, b, c = (3.4445, -4.7750, 2.0315)
272
+ X = G.bfloat16()
273
+ transposed = G.size(0) > G.size(1)
274
+ if transposed:
275
+ X = X.T
276
+ X = X / (X.norm() + 1e-7)
277
+ for _ in range(steps):
278
+ A = X @ X.T
279
+ B = b * A + c * (A @ A)
280
+ X = a * X + B @ X
281
+ if transposed:
282
+ X = X.T
283
+ return X
284
+
285
+
286
+ class Muon(torch.optim.Optimizer):
287
+ """Momentum-SGD z ortogonalizowanym update. weight_decay domyslnie 0 (Muon-params
288
+ czysto; WD trzymamy na AdamW-aux). lr_mult pozwala petli lr-schedule skalowac
289
+ Muon proporcjonalnie do AdamW."""
290
+ def __init__(self, params, lr=0.02, lr_mult=1.0, momentum=0.95, nesterov=True,
291
+ ns_steps=5, weight_decay=0.0, scale_mode="sqrt_ratio"):
292
+ defaults = dict(lr=lr, lr_mult=lr_mult, momentum=momentum, nesterov=nesterov,
293
+ ns_steps=ns_steps, weight_decay=weight_decay, scale_mode=scale_mode)
294
+ super().__init__(params, defaults)
295
+
296
+ @torch.no_grad()
297
+ def step(self, closure=None):
298
+ loss = None
299
+ if closure is not None:
300
+ with torch.enable_grad():
301
+ loss = closure()
302
+ for group in self.param_groups:
303
+ lr = group["lr"]; momentum = group["momentum"]; wd = group["weight_decay"]
304
+ for p in group["params"]:
305
+ g = p.grad
306
+ if g is None:
307
+ continue
308
+ if g.ndim > 2:
309
+ g = g.reshape(g.size(0), -1)
310
+ state = self.state[p]
311
+ if "momentum_buffer" not in state:
312
+ state["momentum_buffer"] = torch.zeros_like(g)
313
+ buf = state["momentum_buffer"]
314
+ buf.mul_(momentum).add_(g)
315
+ g = g.add(buf, alpha=momentum) if group["nesterov"] else buf
316
+ u = zeropower_via_newtonschulz5(g, steps=group["ns_steps"])
317
+ if wd != 0:
318
+ p.mul_(1 - lr * wd)
319
+ # scale ~ sqrt(fan_out/fan_in): zrownuje RMS update niezaleznie od ksztaltu
320
+ # r17 match_rms_adamw: 0.2*sqrt(max(A,B)) jak torch.optim.Muon (RMS kroku ~ AdamW)
321
+ if group.get("scale_mode", "sqrt_ratio") == "match_rms_adamw":
322
+ scale = 0.2 * max(g.size(0), g.size(1)) ** 0.5
323
+ else:
324
+ scale = max(1.0, p.size(0) / p.size(1)) ** 0.5
325
+ p.add_(u.reshape(p.shape).to(p.dtype), alpha=-lr * scale)
326
+ return loss
327
+
328
+
329
+ class MuonWithAuxAdam:
330
+ """Kontener: Muon dla macierzowych ukrytych wag + AdamW dla reszty. Wystawia
331
+ param_groups/step/zero_grad/state_dict tak, by petla treningowa dzialala bez zmian."""
332
+ def __init__(self, muon, adamw):
333
+ self.muon = muon
334
+ self.adamw = adamw
335
+
336
+ @property
337
+ def param_groups(self):
338
+ return self.muon.param_groups + self.adamw.param_groups
339
+
340
+ @property
341
+ def state(self):
342
+ return {**self.muon.state, **self.adamw.state}
343
+
344
+ def step(self, closure=None):
345
+ self.muon.step()
346
+ self.adamw.step()
347
+
348
+ def zero_grad(self, set_to_none=True):
349
+ self.muon.zero_grad(set_to_none=set_to_none)
350
+ self.adamw.zero_grad(set_to_none=set_to_none)
351
+
352
+ def state_dict(self):
353
+ return {"muon": self.muon.state_dict(), "adamw": self.adamw.state_dict()}
354
+
355
+ def load_state_dict(self, sd):
356
+ self.muon.load_state_dict(sd["muon"])
357
+ self.adamw.load_state_dict(sd["adamw"])
358
+
359
+
360
+ def build_optimizer(a, model):
361
+ """--optimizer adamw -> DOKLADNIE poprzedni AdamW (backward-compat).
362
+ --optimizer muon -> Muon(2D-hidden) + AdamW(embeddingi/head/norm/bias)."""
363
+ if a.optimizer == "adamw":
364
+ return torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=a.lr, weight_decay=a.wd,
365
+ betas=(0.9, 0.95)) # r21: bez zamrozonych (--train-only)
366
+ muon_params, adamw_params, seen = [], [], set()
367
+ for name, p in model.named_parameters():
368
+ if not p.requires_grad or id(p) in seen:
369
+ continue
370
+ seen.add(id(p))
371
+ is_embed_or_head = name.startswith(("tok.", "pos.", "head.", "tl.emb.")) # r19: osadzenia TinyLM (tied head) jak tok
372
+ tl_gain = name.startswith("tl.") and name.endswith(".attn.gain") # r19: skalar QK-norm na glowe [H,1,1] -> AdamW
373
+ # r13: w blokach G do Muon tylko kwadratowe projekcje q/k/v/o; konwolucje (3D), bramki niskiego rzedu,
374
+ # b/w_proj, A_log, dt_bias, normy -> AdamW. Bez blokow G warunek jest pusty (r12 bit w bit).
375
+ gdn_aux = ".gdn." in name and not name.endswith(("q_proj.weight", "k_proj.weight", "v_proj.weight", "o_proj.weight"))
376
+ if p.ndim >= 2 and not is_embed_or_head and not gdn_aux and not tl_gain:
377
+ muon_params.append(p)
378
+ else:
379
+ adamw_params.append(p)
380
+ lr_mult = a.muon_lr / a.lr if a.lr > 0 else 1.0
381
+ muon = Muon(muon_params, lr=a.muon_lr, lr_mult=lr_mult, weight_decay=getattr(a, "muon_wd", 0.0),
382
+ scale_mode=getattr(a, "muon_scale", "sqrt_ratio"))
383
+ adamw = torch.optim.AdamW(adamw_params, lr=a.lr, weight_decay=a.wd, betas=(0.9, 0.95))
384
+ return MuonWithAuxAdam(muon, adamw)
385
+
386
+
387
+ class RMSNorm(nn.Module):
388
+ """Qwen3-style RMSNorm (fp32-compute dla stabilnosci). 1D weight -> AdamW w split-Muon."""
389
+ def __init__(self, d, eps=1e-6):
390
+ super().__init__()
391
+ self.weight = nn.Parameter(torch.ones(d))
392
+ self.eps = eps
393
+
394
+ def forward(self, x):
395
+ return x * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + self.eps).to(x.dtype) * self.weight
396
+
397
+
398
+ def make_norm(d, cfg):
399
+ return RMSNorm(d, cfg.norm_eps) if cfg.norm == "rmsnorm" else nn.LayerNorm(d)
400
+
401
+
402
+ def apply_rope(x, base=100000.0, style="interleaved", nrot=0):
403
+ """Parameter-free RoPE na [B,H,T,D] (interleaved-conv, port z qwen_model.py). Train==eval
404
+ MUSZA uzywac tej samej konwencji (self-contained eval -> spojne).
405
+ r13: style="half" = rotate-half (pary i, i+D/2), jak Qielbas M2; "interleaved" = r12 bez zmian.
406
+ r18: nrot (0 < nrot < D) = obrot tylko pierwszych nrot wymiarow (czestotliwosci z nrot), reszta bez zmian; 0 = calosc."""
407
+ if 0 < nrot < x.size(-1):
408
+ return torch.cat([apply_rope(x[..., :nrot], base, style), x[..., nrot:]], dim=-1)
409
+ _, _, T, dim = x.shape
410
+ pos = torch.arange(T, device=x.device, dtype=torch.float32)
411
+ freq = 1.0 / (base ** (torch.arange(0, dim, 2, device=x.device, dtype=torch.float32) / dim))
412
+ ang = torch.outer(pos, freq)
413
+ if style == "half":
414
+ ang = torch.cat([ang, ang], dim=-1)
415
+ cos, sin = ang.cos().to(x.dtype)[None, None], ang.sin().to(x.dtype)[None, None]
416
+ a, b = x.chunk(2, dim=-1)
417
+ return x * cos + torch.cat([-b, a], dim=-1) * sin
418
+ cos, sin = ang.cos().to(x.dtype)[None, None], ang.sin().to(x.dtype)[None, None]
419
+ even, odd = x[..., ::2], x[..., 1::2]
420
+ return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
421
+
422
+
423
+ class SwiGLU(nn.Module):
424
+ """Qwen3 gated-MLP: down(silu(gate(x))*up(x)). 3x 2D bez-bias -> wszystkie do Muon."""
425
+ def __init__(self, d, hidden):
426
+ super().__init__()
427
+ self.gate = nn.Linear(d, hidden, bias=False)
428
+ self.up = nn.Linear(d, hidden, bias=False)
429
+ self.down = nn.Linear(hidden, d, bias=False)
430
+
431
+ def forward(self, x):
432
+ return self.down(F.silu(self.gate(x)) * self.up(x))
433
+
434
+
435
+ class ReLU2(nn.Module):
436
+ """r18 (Tiny Chick): down(relu(up(x))^2), 2x 2D bez biasu -> Muon."""
437
+ def __init__(self, d, hidden):
438
+ super().__init__()
439
+ self.up = nn.Linear(d, hidden, bias=False)
440
+ self.down = nn.Linear(hidden, d, bias=False)
441
+
442
+ def forward(self, x):
443
+ return self.down(F.relu(self.up(x)).square())
444
+
445
+
446
+ class Block(nn.Module):
447
+ def __init__(self, d, nh, block, cfg, is_first=False, mixer="A", layer_idx=0):
448
+ super().__init__()
449
+ self.ln1 = make_norm(d, cfg)
450
+ self.ln2 = make_norm(d, cfg)
451
+ self.mixer = mixer
452
+ if mixer == "G": # r13: GatedDeltaNet2 w miejsce uwagi (fla 0.5.2); inicjalizacja fla, nie _init GPT
453
+ from fla.layers.gdn2 import GatedDeltaNet2
454
+ hd = getattr(cfg, "gdn_head_dim", 64)
455
+ if d % hd:
456
+ raise SystemExit(f"STOP: n_embd {d} nie dzieli sie przez --gdn-head-dim {hd}")
457
+ self.gdn = GatedDeltaNet2(hidden_size=d, head_dim=hd, num_heads=d // hd, expand_v=1.0,
458
+ use_short_conv=True, mode="chunk", layer_idx=layer_idx)
459
+ for sm in self.gdn.modules():
460
+ sm._r13_fla_init = True
461
+ else:
462
+ bias = bool(getattr(cfg, "attn_bias", 1))
463
+ kvh = int(getattr(cfg, "kv_heads", 0) or 0) # r23: GQA (0 albo n_head = MHA, qkv d -> 3d jak r22)
464
+ self.nkv = kvh if 0 < kvh < nh else nh
465
+ self.kvd = self.nkv * (d // nh)
466
+ self.qkv = nn.Linear(d, d + 2 * self.kvd, bias=bias)
467
+ self.proj = nn.Linear(d, d, bias=bias)
468
+ if cfg.ffn == "swiglu":
469
+ self.mlp = SwiGLU(d, int(round(cfg.ffn_mult * d)))
470
+ elif cfg.ffn == "relu2":
471
+ self.mlp = ReLU2(d, int(round(cfg.ffn_mult * d)))
472
+ else:
473
+ self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
474
+ self.nh = nh
475
+ self.d = d
476
+ self.cfg = cfg
477
+ self.is_first = is_first
478
+ if mixer == "A" and cfg.value_residual and not is_first:
479
+ self.vr_lambda = nn.Parameter(torch.zeros(1))
480
+ if mixer == "A" and cfg.qk_norm:
481
+ hd = d // nh
482
+ self.q_norm = RMSNorm(hd, cfg.norm_eps)
483
+ self.k_norm = RMSNorm(hd, cfg.norm_eps)
484
+ self.imha_t = int(getattr(cfg, "imha_t", 0) or 0) if mixer == "A" else 0
485
+ if self.imha_t and self.nkv != nh:
486
+ raise SystemExit("STOP --imha-t: niezgodne z --kv-heads (GQA)")
487
+ if self.imha_t: # r22: bramka i kandydat IMHA (W_a, W_f in R^{2d x d}) + temperatura per warstwa
488
+ self.imha_a = nn.Linear(2 * d, d, bias=False)
489
+ self.imha_f = nn.Linear(2 * d, d, bias=False)
490
+ self.imha_log_tau = nn.Parameter(torch.zeros(1))
491
+
492
+ def _qkv(self, h, v0, set_v0):
493
+ B, T, D = h.size()
494
+ q, k, v = self.qkv(h).split(self.d, dim=2)
495
+ hd = D // self.nh
496
+ q = q.view(B, T, self.nh, hd).transpose(1, 2)
497
+ k = k.view(B, T, self.nh, hd).transpose(1, 2)
498
+ v = v.view(B, T, self.nh, hd).transpose(1, 2)
499
+ if self.cfg.qk_norm:
500
+ q = self.q_norm(q)
501
+ k = self.k_norm(k)
502
+ if self.cfg.pos == "rope":
503
+ style = getattr(self.cfg, "rope_style", "interleaved")
504
+ nrot = int(getattr(self.cfg, "rope_dims", 0) or 0)
505
+ q = apply_rope(q, self.cfg.rope_theta, style, nrot)
506
+ k = apply_rope(k, self.cfg.rope_theta, style, nrot)
507
+ if self.cfg.value_residual:
508
+ if self.is_first: # blok 0 nie ma vr_lambda (r21): v0 = v z iteracji t = 0, w t >= 1 v bez mieszania
509
+ if set_v0:
510
+ v0 = v
511
+ else:
512
+ v = v + self.vr_lambda * v0
513
+ return q, k, v, v0
514
+
515
+ def _imha(self, x, v0):
516
+ """r22: IMHA (Algorytm 1); zwraca (h_T - h_0, v0). Wszystkie sumy po k <= i (prefiks), wiec przyczynowo."""
517
+ B, T, D = x.size()
518
+ hd = D // self.nh
519
+ h0 = self.ln1(x)
520
+ h = h0
521
+ pos = torch.arange(T, device=x.device)
522
+ cnt = (pos + 1).to(torch.float32) # i + 1
523
+ logn = torch.log(cnt).clamp_min(1e-12) # log(i+1); i = 0 -> H_0 = 0 nizej
524
+ mask = torch.ones(T, T, device=x.device, dtype=torch.bool).tril()
525
+ plain = bool(getattr(self.cfg, "imha_plain", False))
526
+ tau = self.imha_log_tau.exp()
527
+ for t in range(self.imha_t):
528
+ q, k, v, v0 = self._qkv(h, v0, set_v0=(t == 0))
529
+ s = (q.float() @ k.float().transpose(-2, -1)) * (hd ** -0.5)
530
+ A = torch.softmax(s.masked_fill(~mask, float("-inf")), dim=-1) # [B,H,T,T] fp32
531
+ z = (A.to(v.dtype) @ v) # [B,H,T,hd]
532
+ if not plain:
533
+ ent = -(A * torch.log(A.clamp_min(1e-30))).sum(-1) # 0 log 0 = 0 (A = 0 -> 0 * log(1e-30) = 0)
534
+ hn = torch.where(pos == 0, torch.zeros_like(ent), ent / logn)
535
+ e = torch.exp(-hn / tau)
536
+ w = e / (e.cumsum(-1) / cnt) # srednia prefiksu, E[w] ~ 1
537
+ z = z * w.unsqueeze(-1).to(z.dtype)
538
+ z = self.proj(z.transpose(1, 2).contiguous().view(B, T, D))
539
+ if plain:
540
+ h = h + z
541
+ continue
542
+ g = z.cumsum(1) / cnt.view(1, T, 1).to(z.dtype)
543
+ u = torch.cat([z, g], dim=-1)
544
+ h = h + torch.sigmoid(self.imha_a(u)) * F.relu(self.imha_f(u))
545
+ return h - h0, v0
546
+
547
+ def forward(self, x, v0=None):
548
+ B, T, D = x.size()
549
+ if self.imha_t:
550
+ dx, v0 = self._imha(x, v0)
551
+ x = x + dx
552
+ x = x + self.mlp(self.ln2(x))
553
+ return x, v0
554
+ h = self.ln1(x)
555
+ if self.mixer == "G":
556
+ x = x + self.gdn(h, use_cache=False)[0]
557
+ x = x + self.mlp(self.ln2(x))
558
+ return x, v0
559
+ q, k, v = self.qkv(h).split([self.d, self.kvd, self.kvd], dim=2)
560
+ hd = D // self.nh
561
+ q = q.view(B, T, self.nh, hd).transpose(1, 2)
562
+ k = k.view(B, T, self.nkv, hd).transpose(1, 2)
563
+ v = v.view(B, T, self.nkv, hd).transpose(1, 2)
564
+ if self.cfg.qk_norm:
565
+ q = self.q_norm(q)
566
+ k = self.k_norm(k)
567
+ if self.cfg.pos == "rope":
568
+ style = getattr(self.cfg, "rope_style", "interleaved")
569
+ nrot = int(getattr(self.cfg, "rope_dims", 0) or 0)
570
+ q = apply_rope(q, self.cfg.rope_theta, style, nrot)
571
+ k = apply_rope(k, self.cfg.rope_theta, style, nrot)
572
+ if self.cfg.value_residual:
573
+ if self.is_first:
574
+ v0 = v
575
+ else:
576
+ v = v + self.vr_lambda * v0
577
+ if self.nkv != self.nh: # r23: GQA, glowy KV powielone na grupy glow q (po RoPE / QK-norm / value-residual)
578
+ k = k.repeat_interleave(self.nh // self.nkv, dim=1)
579
+ v = v.repeat_interleave(self.nh // self.nkv, dim=1)
580
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
581
+ if getattr(self.cfg, "xsa", 0): # r18: bez skladowej wzdluz wlasnego v tokenu (per glowa)
582
+ vn = F.normalize(v, dim=-1)
583
+ y = y - (y * vn).sum(-1, keepdim=True) * vn
584
+ y = y.transpose(1, 2).contiguous().view(B, T, D)
585
+ x = x + self.proj(y)
586
+ x = x + self.mlp(self.ln2(x))
587
+ return x, v0
588
+
589
+
590
+ TINYLM_SHA = "4876824de8c697a657e5f58351c216ffb84cb90d6a2a53ecd94933f7ddcc4cfa" # r19: Tiny-Chick-10M@60d32c4d tinylm_arch.py
591
+
592
+
593
+ def load_tinylm_module(path):
594
+ """r19: oryginalny tinylm_arch.py (Apache-2.0, kurcontko/Tiny-Chick-10M@60d32c4d) z pliku przypietego sha.
595
+ Bez --tinylm-file: tinylm_arch.py obok tego trenera (np. eval, ktory odtwarza model z configu ckpt)."""
596
+ import importlib.util
597
+ path = path or os.path.join(os.path.dirname(os.path.abspath(__file__)), "tinylm_arch.py")
598
+ if not os.path.exists(path):
599
+ raise SystemExit(f"STOP --arch tinylm: brak {path}")
600
+ with open(path, "rb") as f:
601
+ h = hashlib.sha256(f.read()).hexdigest()
602
+ if h != TINYLM_SHA:
603
+ raise SystemExit(f"STOP --arch tinylm: sha {path} = {h[:8]} != {TINYLM_SHA[:8]}")
604
+ spec = importlib.util.spec_from_file_location("tinylm_arch_tc", path)
605
+ mod = importlib.util.module_from_spec(spec)
606
+ spec.loader.exec_module(mod)
607
+ return mod
608
+
609
+
610
+ class GPT(nn.Module):
611
+ def __init__(self, vocab, n_layer, n_embd, n_head, block, cfg):
612
+ super().__init__()
613
+ self.cfg = cfg
614
+ if getattr(cfg, "arch", "gpt") == "tinylm": # r19: caly model = TinyLM z pliku Tiny Chick (klucze "tl.*")
615
+ tlm = load_tinylm_module(getattr(cfg, "tinylm_file", None))
616
+ self.tl = tlm.TinyLM(tlm.TinyConfig(
617
+ vocab_size=vocab, d=n_embd, layers=n_layer, heads=n_head,
618
+ kv_heads=int(getattr(cfg, "kv_heads", 0) or 0) or n_head, mlp_mult=cfg.ffn_mult, act=cfg.ffn,
619
+ qk_norm=bool(cfg.qk_norm), qk_gain=1.0, rope_dims=int(getattr(cfg, "rope_dims", 0) or 0),
620
+ xsa=bool(getattr(cfg, "xsa", 0)), zero_init=bool(getattr(cfg, "zero_init", 0)),
621
+ max_position_embeddings=block, rope_theta=cfg.rope_theta, eps=cfg.norm_eps))
622
+ self.block = block
623
+ return
624
+ E = int(getattr(cfg, "emb_dim", 0) or 0) # r15: faktoryzowane osadzenia (0 = jak r14)
625
+ self.emb_dim = E
626
+ self.tok = nn.Embedding(vocab, E if E else n_embd)
627
+ self.use_rope = cfg.pos == "rope"
628
+ if not self.use_rope:
629
+ self.pos = nn.Embedding(block, n_embd)
630
+ layout = getattr(cfg, "mixer_layout", None) or "A" * n_layer
631
+ if len(layout) != n_layer or set(layout) - {"A", "G"}:
632
+ raise SystemExit(f"STOP: --mixer-layout {layout!r}: dlugosc {n_layer}, tylko litery A/G")
633
+ nrot = int(getattr(cfg, "rope_dims", 0) or 0)
634
+ kvh = int(getattr(cfg, "kv_heads", 0) or 0) # r23: GQA dla blokow A
635
+ if kvh < 0 or kvh > n_head or (kvh and n_head % kvh):
636
+ raise SystemExit(f"STOP: --kv-heads {kvh}: 0 (MHA) albo dzielnik n_head {n_head}")
637
+ if nrot and (nrot % 2 or not 0 < nrot < n_embd // n_head or cfg.pos != "rope"):
638
+ raise SystemExit(f"STOP: --rope-dims {nrot}: parzyste, 0 < N < head_dim {n_embd // n_head}, tylko z --pos rope")
639
+ first_a = layout.find("A") # value-residual: v0 z pierwszego bloku A (r12: blok 0)
640
+ self.blocks = nn.ModuleList([Block(n_embd, n_head, block, cfg, is_first=(i == first_a), mixer=layout[i], layer_idx=i)
641
+ for i in range(n_layer)])
642
+ self.lnf = make_norm(n_embd, cfg)
643
+ self.head = nn.Linear(E if E else n_embd, vocab, bias=False)
644
+ self.head.weight = self.tok.weight # tie
645
+ if E:
646
+ self.emb_up = nn.Linear(E, n_embd, bias=False)
647
+ self.emb_down = nn.Linear(n_embd, E, bias=False)
648
+ self.block = block
649
+ self.apply(self._init)
650
+ if getattr(cfg, "zero_init", 0): # r18: wyjscia galezi rezydualnych od zera (bloki G: proj fla bez zmian)
651
+ for b in self.blocks:
652
+ if b.mixer == "A":
653
+ nn.init.zeros_(b.proj.weight)
654
+ nn.init.zeros_(b.mlp.down.weight if hasattr(b.mlp, "down") else b.mlp[2].weight)
655
+ if E:
656
+ nn.init.normal_(self.emb_up.weight, 0.0, E ** -0.5)
657
+ nn.init.normal_(self.emb_down.weight, 0.0, n_embd ** -0.5)
658
+
659
+ def _head(self, h):
660
+ return self.head(self.emb_down(h)) if self.emb_dim else self.head(h)
661
+
662
+ def _init(self, m):
663
+ if getattr(m, "_r13_fla_init", False): # r13: moduly GatedDeltaNet2 zostaja z inicjalizacja fla
664
+ return
665
+ if isinstance(m, nn.Linear):
666
+ nn.init.normal_(m.weight, 0.0, 0.02)
667
+ if m.bias is not None:
668
+ nn.init.zeros_(m.bias)
669
+ elif isinstance(m, nn.Embedding):
670
+ nn.init.normal_(m.weight, 0.0, 0.02)
671
+
672
+ def _loss_sum(self, h, y, t=None):
673
+ """r11: suma CE (+ z-loss) dla kawalka pozycji; h po lnf [B,t,D], y [B,t]. Ta sama arytmetyka co sciezka r10.
674
+ r14: t = logity nauczyciela tego kawalka [B,t,V] -> (suma straty mieszanej KD, suma samej CE)."""
675
+ logits = self._head(h)
676
+ cap = getattr(self.cfg, "logit_cap", 0.0)
677
+ if cap and cap > 0:
678
+ logits = cap * torch.tanh(logits / cap)
679
+ flat = logits.view(-1, logits.size(-1))
680
+ s = F.cross_entropy(flat, y.reshape(-1), reduction="sum")
681
+ zc = getattr(self.cfg, "z_loss", 0.0)
682
+ if zc and zc > 0:
683
+ lse = torch.logsumexp(flat, dim=-1)
684
+ s = s + zc * (lse * lse).sum()
685
+ if t is None:
686
+ return s
687
+ return self._kd_mix(s, flat, t, 1), s
688
+
689
+ def _kd_mix(self, ce, flat, t, div):
690
+ """r14: (1-A)*ce + A*T^2*KL(p_naucz || p_ucz); KL sumowane po pozycjach kawalka / div (1 = suma, N = srednia).
691
+ fp32, pelny vocab; flat = logity ucznia [N,V] (po logit_cap), t = logity nauczyciela [...,V]."""
692
+ A, T = self.cfg.kd_alpha, self.cfg.kd_temp
693
+ ls = F.log_softmax(flat.float() / T, dim=-1)
694
+ lt = F.log_softmax(t.reshape(-1, t.size(-1)).float() / T, dim=-1)
695
+ kl = (lt.exp() * (lt - ls)).sum() / div
696
+ return (1.0 - A) * ce + (A * T * T) * kl
697
+
698
+ def forward(self, idx, targets=None, teacher_logits=None):
699
+ if getattr(self.cfg, "arch", "gpt") == "tinylm": # r19: ich forward (logity z tied emb), CE w fp32 jak u nich
700
+ logits = self.tl(idx).logits
701
+ if targets is None:
702
+ return logits, None
703
+ return logits, F.cross_entropy(logits.reshape(-1, logits.size(-1)).float(), targets.reshape(-1))
704
+ B, T = idx.size()
705
+ x = self.tok(idx)
706
+ if self.emb_dim:
707
+ x = self.emb_up(x)
708
+ if not self.use_rope:
709
+ pos = torch.arange(T, device=idx.device)
710
+ x = x + self.pos(pos)[None]
711
+ v0 = None
712
+ act_ckpt = self.training and getattr(self.cfg, "act_ckpt", False)
713
+ for _ in range(getattr(self.cfg, "loop", 1)):
714
+ for b in self.blocks:
715
+ x, v0 = checkpoint(b, x, v0, use_reentrant=False) if act_ckpt else b(x, v0)
716
+ h = self.lnf(x)
717
+ n = getattr(self.cfg, "loss_chunk", 0)
718
+ if targets is not None and n and n < T:
719
+ if teacher_logits is None:
720
+ s = sum(checkpoint(self._loss_sum, h[:, i:i + n], targets[:, i:i + n], use_reentrant=False)
721
+ for i in range(0, T, n))
722
+ return None, s / (B * T)
723
+ parts = [checkpoint(self._loss_sum, h[:, i:i + n], targets[:, i:i + n], teacher_logits[:, i:i + n],
724
+ use_reentrant=False) for i in range(0, T, n)]
725
+ return None, sum(p[0] for p in parts) / (B * T), sum(p[1] for p in parts).detach() / (B * T)
726
+ logits = self._head(h)
727
+ cap = getattr(self.cfg, "logit_cap", 0.0)
728
+ if cap and cap > 0:
729
+ logits = cap * torch.tanh(logits / cap)
730
+ loss = None
731
+ if targets is not None:
732
+ flat = logits.view(-1, logits.size(-1))
733
+ loss = F.cross_entropy(flat, targets.view(-1))
734
+ zc = getattr(self.cfg, "z_loss", 0.0)
735
+ if zc and zc > 0:
736
+ lse = torch.logsumexp(flat, dim=-1)
737
+ loss = loss + zc * (lse * lse).mean()
738
+ if teacher_logits is not None:
739
+ return logits, self._kd_mix(loss, flat, teacher_logits, flat.size(0)), loss.detach()
740
+ return logits, loss
741
+
742
+
743
+ def main():
744
+ a = get_args()
745
+ world = int(os.environ.get("WORLD_SIZE", "1"))
746
+ rank = int(os.environ.get("RANK", "0"))
747
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
748
+ ddp = world > 1
749
+ if ddp:
750
+ if a.batch % world:
751
+ raise SystemExit(f"STOP: --batch {a.batch} (globalny) nie dzieli sie przez liczbe procesow {world}")
752
+ if torch.cuda.is_available():
753
+ torch.cuda.set_device(local_rank)
754
+ dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
755
+ if a.grad_accum < 1 or (a.batch // world) % a.grad_accum:
756
+ raise SystemExit(f"STOP: --grad-accum {a.grad_accum}: wiersze ranku {a.batch // world} nie dziela sie na K mikrobatchy")
757
+ master = rank == 0
758
+ rows = slice(rank * (a.batch // world), (rank + 1) * (a.batch // world)) # wiersze globalnego batcha tego ranku
759
+ out_dir = a.out_dir or os.path.join(a.data_dir, "runs", a.run_id)
760
+ os.makedirs(out_dir, exist_ok=True)
761
+ log_path = os.path.join(out_dir, "train.log")
762
+ metrics_path = os.path.join(out_dir, "metrics.jsonl")
763
+ ckpt_path = os.path.join(out_dir, "ckpt.pt")
764
+
765
+ def log(msg):
766
+ if not master:
767
+ return
768
+ line = f"[{time.strftime('%H:%M:%S')}] {msg}"
769
+ print(line, flush=True)
770
+ with open(log_path, "a", encoding="utf-8") as f:
771
+ f.write(line + "\n")
772
+
773
+ events_path = a.events_jsonl
774
+
775
+ def emit(kind, step, metrics=None, **extra):
776
+ if not events_path or not master:
777
+ return
778
+ rec = {"kind": kind, "updates": int(step), "tokens": int(step) * a.block * a.batch}
779
+ if metrics:
780
+ rec["metrics"] = metrics
781
+ rec.update(extra)
782
+ with open(events_path, "a", encoding="utf-8") as f:
783
+ f.write(json.dumps(rec) + "\n")
784
+
785
+ torch.manual_seed(a.seed)
786
+ torch.backends.cuda.matmul.allow_tf32 = bool(a.tf32) # r17: --tf32 0 jak Qielbas M2 (domyslnie 1 = jak dotad)
787
+ torch.backends.cudnn.allow_tf32 = bool(a.tf32)
788
+ device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu")
789
+ use_bf16 = device.type == "cuda" and torch.cuda.is_bf16_supported()
790
+ ptdtype = torch.bfloat16 if use_bf16 else torch.float32
791
+ ctx = torch.amp.autocast(device_type=device.type, dtype=ptdtype) if device.type == "cuda" else nullcontext()
792
+ log(f"device={device} bf16={use_bf16} dev={torch.cuda.get_device_name(local_rank) if device.type=='cuda' else 'cpu'}"
793
+ f" world={world}")
794
+
795
+ if a.synthetic:
796
+ rng = np.random.default_rng(a.seed)
797
+ train_data = rng.integers(0, 256, size=8_000_000, dtype=np.uint8)
798
+ val_data = train_data[:200_000]
799
+ log("SYNTHETIC uint8 (smoke throughput, zero danych PII)")
800
+ else:
801
+ train_data = np.memmap(os.path.join(a.data_dir, "train.bin"), dtype=np.dtype(a.dtype), mode="r")
802
+ val_data = np.memmap(os.path.join(a.data_dir, "val.bin"), dtype=np.dtype(a.dtype), mode="r")
803
+ train2_data = (np.memmap(os.path.join(a.data_dir_2, "train.bin"), dtype=np.dtype(a.dtype), mode="r")
804
+ if a.data_dir_2 else None)
805
+ log(f"dane: train={len(train_data):,}B block={a.block} batch={a.batch} tok/step={a.block*a.batch:,}")
806
+
807
+ class EpochSampler:
808
+ """Losowanie BEZ zwracania: epoka e = permutacja okien na siatce co `block` tokenow, przesunietej o losowe
809
+ offset_e w [0, block) (kolejne epoki widza inne granice okien). Krok s bierze okna s*batch..(s+1)*batch-1
810
+ z ciaglego strumienia epok, wiec start od dowolnego kroku (resume) daje dokladnie ten sam ciag co run ciagly.
811
+ Epoka e ma wlasny RNG ziarna (seed, e): najpierw offset, potem permutacja; niezalezna od innych epok."""
812
+ def __init__(self, n_tokens, block, batch, seed):
813
+ self.n, self.block, self.batch, self.seed = n_tokens, block, batch, seed
814
+ self._perm_e, self._perm = None, None
815
+
816
+ def _meta(self, e):
817
+ rng = np.random.default_rng([self.seed, e])
818
+ off = int(rng.integers(self.block))
819
+ return rng, off, (self.n - 1 - off) // self.block # okno = block+1 tokenow od off + k*block
820
+
821
+ def _perm_of(self, e):
822
+ if self._perm_e != e: # trzymamy tylko biezaca epoke (~70 MB przy 9.4B tok / 1024)
823
+ rng, _, nwin = self._meta(e)
824
+ self._perm_e, self._perm = e, rng.permutation(nwin)
825
+ return self._perm
826
+
827
+ def starts(self, step):
828
+ g, need, out, e = step * self.batch, self.batch, [], 0
829
+ while g >= self._meta(e)[2]: # epoki maja rozna dlugosc (offset), wiec liczymy po kolei
830
+ g -= self._meta(e)[2]; e += 1
831
+ while need:
832
+ _, off, nwin = self._meta(e)
833
+ take = min(need, nwin - g)
834
+ out.append(off + self._perm_of(e)[g:g + take].astype(np.int64) * self.block)
835
+ need -= take; g = 0; e += 1
836
+ return np.concatenate(out)
837
+
838
+ class QueueSampler:
839
+ """r7: pula jako kolejka shardow (zakresy tokenow [lo,hi) w train.bin, wyrownane do dokumentow).
840
+ schedule = rotacje manifestu po kolei; slot j = kroki [j*S,(j+1)*S) na shardzie schedule[j]. W slocie kazde okno
841
+ uzyte najwyzej raz: siatka co `block` od lo+off, off i permutacja z RNG (seed, j, id shardu). Starty okien zaleza
842
+ tylko od (manifest, seed, krok), wiec resume z ckpt na granicy slotu jest dokladny, a usuniecie shardu ze slotu j
843
+ nie zmienia krokow < j*S. prefix_sha(krok) = hash slotow juz uzytych; zapisywany w ckpt i sprawdzany przy resume."""
844
+ def __init__(self, manifest, n_tokens, block, batch, seed):
845
+ self.S, self.block, self.batch, self.seed = int(manifest["steps_per_shard"]), block, batch, seed
846
+ self.shards = manifest["shards"]
847
+ self.schedule = [sid for rot in manifest["rotations"] for sid in rot]
848
+ for sid in self.schedule:
849
+ sh = self.shards[sid]
850
+ if not 0 <= sh["lo"] < sh["hi"] <= n_tokens:
851
+ raise SystemExit(f"STOP kolejka: shard {sid} [{sh['lo']},{sh['hi']}) poza train.bin ({n_tokens})")
852
+ if (sh["hi"] - sh["lo"] - block) // block < self.S * batch:
853
+ raise SystemExit(f"STOP kolejka: shard {sid} ma za malo okien na {self.S} krokow x {batch}")
854
+ used = sorted((self.shards[s]["lo"], self.shards[s]["hi"], s) for s in dict.fromkeys(self.schedule))
855
+ for (_, hi0, s0), (lo1, _, s1) in zip(used, used[1:]):
856
+ if lo1 < hi0:
857
+ raise SystemExit(f"STOP kolejka: shardy {s0} i {s1} nakladaja sie w train.bin (dane uzyte dwa razy)")
858
+ self._cache = (None, None)
859
+
860
+ def _slot(self, j):
861
+ if self._cache[0] != j:
862
+ sid = self.schedule[j]
863
+ sh = self.shards[sid]
864
+ idn = int(hashlib.sha256(sid.encode()).hexdigest()[:8], 16)
865
+ rng = np.random.default_rng([self.seed, j, idn])
866
+ off = int(rng.integers(self.block))
867
+ nwin = (sh["hi"] - sh["lo"] - 1 - off) // self.block
868
+ perm = rng.permutation(nwin)[: self.S * self.batch].astype(np.int64)
869
+ self._cache = (j, (sh["lo"] + off, perm))
870
+ return self._cache[1]
871
+
872
+ def starts(self, step):
873
+ j, k = divmod(step, self.S)
874
+ if j >= len(self.schedule):
875
+ raise SystemExit(f"STOP kolejka: krok {step} poza harmonogramem ({len(self.schedule)} slotow x {self.S})")
876
+ base, perm = self._slot(j)
877
+ return base + perm[k * self.batch:(k + 1) * self.batch] * self.block
878
+
879
+ def prefix_sha(self, step):
880
+ used = -(-step // self.S) # sloty, ktorych kroki juz sie zaczely
881
+ spec = [self.S, self.seed, self.block, self.batch,
882
+ [[sid, self.shards[sid]["lo"], self.shards[sid]["hi"], self.shards[sid]["sha256"]] for sid in self.schedule[:used]]]
883
+ return hashlib.sha256(json.dumps(spec).encode()).hexdigest()
884
+
885
+ def _make_batch_cpu(split, generator=None, ix=None, shard=False):
886
+ """Wektoryzowane budowanie batcha: JEDEN numpy fancy-index zamiast
887
+ python-loop per-item. sliding_window_view daje strided-view (N-block, block+1)
888
+ BEZ kopiowania; windows[ix] materializuje tylko wybrane wiersze naraz.
889
+ Zwraca (x,y) long CPU (pinned jesli cuda). Rozklad batchy IDENTYCZNY jak
890
+ stary torch.stack-loop: x=data[i:i+block], y=data[i+1:i+1+block]. ix: gotowe starty okien (sampler epoch).
891
+ shard=True (trening pod DDP): starty losowane dla CALEGO globalnego batcha jak w 1-GPU, rank bierze swoje wiersze."""
892
+ data = train_data if split == "train" else (train2_data if split == "train2" else val_data)
893
+ if ix is None:
894
+ ix = torch.randint(len(data) - a.block - 1, (a.batch,), generator=generator)
895
+ if shard:
896
+ ix = ix[rows]
897
+ # (N-block, block+1) view; jeden fancy-index kopiuje wybrane okna
898
+ windows = np.lib.stride_tricks.sliding_window_view(data, a.block + 1)
899
+ sel = windows[ix.numpy()] # (batch, block+1) materialized
900
+ x = torch.from_numpy(sel[:, :-1].astype(np.int64)) # astype -> contiguous copy
901
+ y = torch.from_numpy(sel[:, 1:].astype(np.int64))
902
+ if device.type == "cuda":
903
+ x = x.pin_memory(); y = y.pin_memory()
904
+ return x, y
905
+
906
+ def _to_device(x, y):
907
+ if device.type == "cuda":
908
+ return x.to(device, non_blocking=True), y.to(device, non_blocking=True)
909
+ return x.to(device), y.to(device)
910
+
911
+ def get_batch(split, generator=None):
912
+ return _to_device(*_make_batch_cpu(split, generator))
913
+
914
+ class Prefetcher:
915
+ """Async double-buffer: 1 background-thread buduje NASTEPNY batch na CPU
916
+ (pinned) podczas gdy GPU liczy biezacy. queue depth=2. Konsument robi
917
+ .next() -> H2D-copy (non_blocking) w watku glownym. Watek uzywa wlasnego
918
+ torch.Generator (seeded), wiec ciag train-batchy jest deterministyczny i
919
+ NIEZALEZNY od timingu watku oraz od RNG val-loopa (dystrybucja bez zmian).
920
+ sampler (EpochSampler) zamiast generatora: starty okien z kroku, liczonego od start_step."""
921
+ def __init__(self, split, generator, depth=2, sampler=None, start_step=0):
922
+ self.split = split
923
+ self.gen = generator
924
+ self.sampler, self.step = sampler, start_step
925
+ self.q = queue.Queue(maxsize=depth)
926
+ self._stop = threading.Event()
927
+ self.t = threading.Thread(target=self._worker, daemon=True)
928
+ self.t.start()
929
+
930
+ def _worker(self):
931
+ while not self._stop.is_set():
932
+ try:
933
+ if self.sampler is not None and sampler2 is not None and self.step >= a.switch_step:
934
+ item = _make_batch_cpu("train2", ix=torch.from_numpy(sampler2.starts(self.step - a.switch_step)),
935
+ shard=True)
936
+ self.step += 1
937
+ elif self.sampler is not None:
938
+ item = _make_batch_cpu(self.split, ix=torch.from_numpy(self.sampler.starts(self.step)), shard=True)
939
+ self.step += 1
940
+ else:
941
+ item = _make_batch_cpu(self.split, self.gen, shard=True)
942
+ except Exception as e: # przekaz blad do konsumenta
943
+ self.q.put(e)
944
+ return
945
+ while not self._stop.is_set():
946
+ try:
947
+ self.q.put(item, timeout=0.5)
948
+ break
949
+ except queue.Full:
950
+ continue
951
+
952
+ def next(self):
953
+ item = self.q.get()
954
+ if isinstance(item, Exception):
955
+ raise item
956
+ return _to_device(*item)
957
+
958
+ def close(self):
959
+ self._stop.set()
960
+ # opróżnij kolejke zeby watek nie zawisl na put()
961
+ try:
962
+ self.q.get_nowait()
963
+ except queue.Empty:
964
+ pass
965
+
966
+ if a.arch == "tinylm": # r19: tylko kombinacje, ktore TinyLM odtwarza; reszta = STOP zamiast cichego pominiecia
967
+ bad = [n for n, v in (("--loop > 1", a.loop != 1), ("--mixer-layout", a.mixer_layout), ("--emb-dim", a.emb_dim),
968
+ ("--value-residual", a.value_residual), ("--kd-teacher", a.kd_teacher),
969
+ ("--act-ckpt", a.act_ckpt), ("--loss-chunk", a.loss_chunk), ("--logit-cap", a.logit_cap),
970
+ ("--z-loss", a.z_loss), ("--pos != rope", a.pos != "rope"),
971
+ ("--rope-style != half", a.rope_style != "half"), ("--norm != rmsnorm", a.norm != "rmsnorm"),
972
+ ("--attn-bias != 0", a.attn_bias != 0), ("--ffn gelu", a.ffn == "gelu"),
973
+ ("--init-from", a.init_from)) if v]
974
+ if bad:
975
+ raise SystemExit(f"STOP --arch tinylm: niezgodne {bad}")
976
+ if a.n_embd % a.n_head or a.n_head % (a.kv_heads or a.n_head):
977
+ raise SystemExit(f"STOP --arch tinylm: n_embd {a.n_embd} / n_head {a.n_head} / kv_heads {a.kv_heads}")
978
+ elif a.tinylm_file:
979
+ raise SystemExit("STOP: --tinylm-file tylko z --arch tinylm")
980
+ elif a.kv_heads == a.n_head: # r23: K = n_head to zwykla MHA -> jak 0 (wagi, config ckpt i wyjscia = r22)
981
+ a.kv_heads = 0
982
+ raw_model = GPT(a.vocab, a.n_layer, a.n_embd, a.n_head, a.block, a).to(device)
983
+ nparam = sum(p.numel() for p in raw_model.parameters())
984
+ log(f"model GPT-ref: {nparam/1e6:.1f}M param (L{a.n_layer} d{a.n_embd} h{a.n_head}"
985
+ + (f" kv{a.kv_heads}" if a.arch == "gpt" and a.kv_heads else "") + ")")
986
+ if a.arch == "tinylm":
987
+ log(f"r19 arch=tinylm (Tiny-Chick-10M@60d32c4d tinylm_arch.py {TINYLM_SHA[:8]}): P={nparam} "
988
+ f"kv_heads={a.kv_heads or a.n_head} ffn={a.ffn} x{a.ffn_mult} qk_norm={a.qk_norm} rope_dims={a.rope_dims} "
989
+ f"xsa={a.xsa} zero_init={a.zero_init} eps={a.norm_eps} theta={a.rope_theta}")
990
+ n_train = nparam
991
+ if a.train_only: # r21: zamrozenie wszystkiego poza dopasowanymi nazwami (wzor ft_arc11.py)
992
+ if not a.init_from:
993
+ raise SystemExit("STOP --train-only: wymaga --init-from")
994
+ if a.arch == "tinylm" or a.loop != 1 or a.mixer_layout or a.kd_teacher or a.compile or ddp:
995
+ raise SystemExit("STOP --train-only: kombinacja nietestowana (tinylm / loop / mixer-layout / kd / compile / DDP)")
996
+ rx = re.compile(a.train_only)
997
+ for name, p in raw_model.named_parameters(): # tied head/tok wystepuje raz, jako tok.weight
998
+ p.requires_grad_(bool(rx.search(name)))
999
+ n_train = sum(p.numel() for p in raw_model.parameters() if p.requires_grad)
1000
+ if n_train == 0:
1001
+ raise SystemExit(f"STOP --train-only: {a.train_only!r} nie pasuje do zadnego parametru")
1002
+ if raw_model.tok.weight.requires_grad:
1003
+ raise SystemExit(f"STOP --train-only: {a.train_only!r} chwyta osadzenia / tied head")
1004
+ log(f"r21 train-only {a.train_only!r}: uczone {n_train} z {nparam}")
1005
+ opt = build_optimizer(a, raw_model)
1006
+ log(f"optimizer={a.optimizer}" + (f" muon_lr={a.muon_lr} (mult={a.muon_lr/a.lr:.1f}x)" if a.optimizer == "muon" else ""))
1007
+ if a.act_ckpt or a.loss_chunk:
1008
+ log(f"r11 pamiec: act_ckpt={a.act_ckpt} loss_chunk={a.loss_chunk}")
1009
+ if a.imha_t < 0:
1010
+ raise SystemExit("STOP: --imha-t >= 0")
1011
+ if a.imha_t:
1012
+ bad = [n for n, v in (("--xsa", a.xsa), ("--mixer-layout", a.mixer_layout), ("--arch tinylm", a.arch == "tinylm")) if v]
1013
+ if bad:
1014
+ raise SystemExit(f"STOP --imha-t: kombinacja nietestowana {bad}")
1015
+ log(f"r22 IMHA: T={a.imha_t} w {a.n_layer} blokach (jawna macierz uwagi, entropia + bramka); P={nparam}")
1016
+ if a.loop < 1:
1017
+ raise SystemExit("STOP: --loop >= 1")
1018
+ if a.loop > 1:
1019
+ log(f"r12 loop={a.loop}: {a.n_layer} wspoldzielonych blokow x {a.loop} = glebokosc {a.n_layer * a.loop}")
1020
+ if a.mixer_layout or a.attn_bias != 1 or a.rope_style != "interleaved":
1021
+ log(f"r13: mixer_layout={a.mixer_layout or 'A' * a.n_layer} gdn_head_dim={a.gdn_head_dim} "
1022
+ f"attn_bias={a.attn_bias} rope_style={a.rope_style}")
1023
+ if a.xsa or a.rope_dims or a.zero_init or a.ffn == "relu2":
1024
+ log(f"r18: xsa={a.xsa} rope_dims={a.rope_dims or 'wszystkie'} zero_init={a.zero_init} ffn={a.ffn} ffn_mult={a.ffn_mult}")
1025
+ # DDP: identyczne wagi startowe na kazdym ranku (ten sam seed) + broadcast z rank 0 w konstruktorze DDP.
1026
+ # broadcast_buffers=False: forward walidacji (bez backward) nie robi kolektywow.
1027
+ model = DDP(raw_model, device_ids=[local_rank] if device.type == "cuda" else None,
1028
+ broadcast_buffers=False) if ddp else raw_model
1029
+ model = torch.compile(model) if a.compile else model
1030
+ if a.compile:
1031
+ log("torch.compile enabled")
1032
+
1033
+ start_step = 0
1034
+ if a.resume and os.path.exists(ckpt_path):
1035
+ ck = torch.load(ckpt_path, map_location=device, weights_only=True) # r20
1036
+ raw_model.load_state_dict(ck["model"]); opt.load_state_dict(ck["opt"]); start_step = ck["step"]
1037
+ if ck.get("config", {}).get("data_seed") != a.data_seed: # r23: ta sama kolejność danych po wznowieniu
1038
+ raise SystemExit(f"STOP --resume: data_seed w ckpt {ck.get('config', {}).get('data_seed')} != {a.data_seed}")
1039
+ log(f"RESUME @ {start_step}")
1040
+ if a.init_from and start_step == 0:
1041
+ ck0 = torch.load(a.init_from, map_location=device, weights_only=True) # r20 (r18/r19: False)
1042
+ c0 = ck0.get("config", {})
1043
+ for key in ("vocab", "n_layer", "n_embd", "n_head"):
1044
+ if c0.get(key) is not None and c0[key] != getattr(a, key):
1045
+ raise SystemExit(f"STOP --init-from: {key}={c0[key]} w ckpt != {getattr(a, key)} w argumentach")
1046
+ if c0.get("loop", 1) != a.loop:
1047
+ raise SystemExit(f"STOP --init-from: loop={c0.get('loop', 1)} w ckpt != {a.loop} w argumentach")
1048
+ for key, dflt in (("xsa", 0), ("rope_dims", 0), ("ffn", "gelu"), ("imha_t", 0), ("kv_heads", 0)): # r18: klucze bez wag tez musza sie zgadzac; r22: imha_t; r23: kv_heads
1049
+ if c0.get(key, dflt) != getattr(a, key):
1050
+ raise SystemExit(f"STOP --init-from: {key}={c0.get(key, dflt)} w ckpt != {getattr(a, key)} w argumentach")
1051
+ raw_model.load_state_dict(ck0["model"], strict=True)
1052
+ log(f"INIT-FROM {a.init_from} (wagi, krok 0, swiezy optymalizator; grown_from={ck0.get('grown_from')})")
1053
+ del ck0
1054
+ teacher = None
1055
+ if a.kd_teacher: # r14: nauczyciel KD (po budowie/wznowieniu ucznia, wiec jego wagi startowe bez zmian)
1056
+ if not 0.0 <= a.kd_alpha <= 1.0 or a.kd_temp <= 0:
1057
+ raise SystemExit(f"STOP --kd: alpha {a.kd_alpha} poza [0,1] albo temp {a.kd_temp} <= 0")
1058
+ _h = hashlib.sha256()
1059
+ with open(a.kd_teacher, "rb") as _f:
1060
+ for _chunk in iter(lambda: _f.read(1 << 20), b""):
1061
+ _h.update(_chunk)
1062
+ _tsha = _h.hexdigest()
1063
+ if _tsha != (a.kd_teacher_sha or "").lower():
1064
+ raise SystemExit(f"STOP --kd-teacher: sha {_tsha} != --kd-teacher-sha")
1065
+ ckt = torch.load(a.kd_teacher, map_location="cpu", weights_only=True)
1066
+ ct = ckt["config"]
1067
+ if ct["vocab"] != a.vocab or ct["block"] < a.block:
1068
+ raise SystemExit(f"STOP --kd-teacher: vocab {ct['vocab']} / block {ct['block']} nauczyciela "
1069
+ f"vs vocab {a.vocab} / block {a.block} ucznia")
1070
+ import types
1071
+ with torch.random.fork_rng(devices=[]): # budowa (init) nauczyciela nie zmienia globalnego RNG ucznia
1072
+ teacher = GPT(ct["vocab"], ct["n_layer"], ct["n_embd"], ct["n_head"], ct["block"], types.SimpleNamespace(**ct))
1073
+ teacher.load_state_dict({k.removeprefix("_orig_mod."): v for k, v in ckt["model"].items()}, strict=True)
1074
+ teacher = teacher.to(device).eval().requires_grad_(False)
1075
+ log(f"r14 KD: nauczyciel {a.kd_teacher} sha {_tsha[:8]} krok {ckt.get('step')} "
1076
+ f"{sum(p.numel() for p in teacher.parameters())/1e6:.1f}M (L{ct['n_layer']} d{ct['n_embd']} "
1077
+ f"{ct.get('mixer_layout') or 'A' * ct['n_layer']}), alpha {a.kd_alpha} T {a.kd_temp}")
1078
+ del ckt
1079
+ teacher_fwd = torch.compile(teacher) if a.compile else teacher
1080
+ elif a.kd_alpha or a.kd_teacher_sha:
1081
+ raise SystemExit("STOP: --kd-alpha/--kd-teacher-sha bez --kd-teacher")
1082
+ if a.data_offset_steps and a.sampler != "epoch":
1083
+ raise SystemExit("STOP: --data-offset-steps wymaga --sampler epoch")
1084
+
1085
+ def _lr_sched(s):
1086
+ if s < a.warmup:
1087
+ return a.lr * (s + 1) / a.warmup
1088
+ if s >= a.steps:
1089
+ return a.min_lr
1090
+ if a.schedule == "wsd":
1091
+ # warmup-stable-decay (JugnuLM/MiniCPM): constant peak, then 1-sqrt decay in last decay-frac.
1092
+ decay_start = int(a.steps * (1.0 - a.decay_frac))
1093
+ if s < decay_start:
1094
+ return a.lr
1095
+ d = (s - decay_start) / max(1, a.steps - decay_start) # 0->1 over decay phase
1096
+ return a.min_lr + (a.lr - a.min_lr) * (1.0 - d ** 0.5) # 1-sqrt: fast early decay
1097
+ r = (s - a.warmup) / max(1, a.steps - a.warmup)
1098
+ return a.min_lr + 0.5 * (a.lr - a.min_lr) * (1 + math.cos(math.pi * r))
1099
+
1100
+ # --rewarm-steps: galaz z ckpt (np. cosine @320k -> wsd) nie skacze od razu na nowy harmonogram,
1101
+ # tylko przechodzi liniowo od LR zapisanego w ckpt (param_groups po opt.load_state_dict).
1102
+ rewarm_from = None
1103
+ if start_step and a.rewarm_steps > 0:
1104
+ _g0 = opt.param_groups[0]
1105
+ rewarm_from = _g0["lr"] / _g0.get("lr_mult", 1.0)
1106
+ log(f"rewarm: LR {rewarm_from:.3e} -> harmonogram przez {a.rewarm_steps} krokow od {start_step}")
1107
+
1108
+ def lr_at(s):
1109
+ base = _lr_sched(s)
1110
+ if rewarm_from is not None and s < start_step + a.rewarm_steps:
1111
+ f = (s - start_step + 1) / a.rewarm_steps
1112
+ return rewarm_from + (base - rewarm_from) * f
1113
+ return base
1114
+
1115
+ @torch.no_grad()
1116
+ def eval_val():
1117
+ model.eval()
1118
+ ls = []
1119
+ for _ in range(a.eval_iters):
1120
+ xb, yb = get_batch("val")
1121
+ with ctx:
1122
+ _, loss = model(xb, yb)
1123
+ ls.append(loss.item())
1124
+ model.train()
1125
+ return sum(ls) / len(ls)
1126
+
1127
+ model.train()
1128
+ log(f"START gpt-ref: steps={a.steps} (od {start_step}) lr={a.lr}->{a.min_lr}")
1129
+ # dedykowany seeded generator dla train-prefetchera (determinizm niezalezny
1130
+ # od RNG val-loopa i timingu watku; ta sama dystrybucja co global-RNG)
1131
+ train_gen = torch.Generator()
1132
+ ds = a.seed if a.data_seed is None else a.data_seed # r23: kolejność danych osobno od inicjalizacji
1133
+ train_gen.manual_seed(ds)
1134
+ sampler = None
1135
+ sampler2 = None
1136
+ if a.queue:
1137
+ if a.synthetic:
1138
+ raise SystemExit("STOP: --queue wymaga prawdziwego train.bin (bez --synthetic)")
1139
+ with open(a.queue, "rb") as f:
1140
+ _qraw = f.read()
1141
+ sampler = QueueSampler(json.loads(_qraw), len(train_data), a.block, a.batch, ds)
1142
+ if start_step:
1143
+ _ckq = ck.get("queue") or {}
1144
+ if _ckq.get("prefix_sha") != sampler.prefix_sha(start_step):
1145
+ raise SystemExit(f"STOP kolejka: sloty uzyte do kroku {start_step} w manifescie != zapisane w ckpt "
1146
+ "(resume nie bylby dokladny)")
1147
+ log(f"sampler queue: manifest {hashlib.sha256(_qraw).hexdigest()[:8]}, {len(sampler.schedule)} slotow x "
1148
+ f"{sampler.S} krokow, start od kroku {start_step} = slot {start_step // sampler.S}")
1149
+ elif a.sampler == "epoch" and not a.synthetic:
1150
+ sampler = EpochSampler(len(train_data), a.block, a.batch, ds)
1151
+ _, _off0, _nwin0 = sampler._meta(0)
1152
+ log(f"sampler epoch: {_nwin0:,} okien/epoke (offset0 {_off0}), start od kroku {start_step} "
1153
+ f"= epoka {start_step * a.batch / _nwin0:.3f}")
1154
+ if a.data_dir_2:
1155
+ if a.switch_step <= 0 or _nwin0 // a.batch < a.switch_step:
1156
+ raise SystemExit(f"STOP --data-dir-2: --switch-step {a.switch_step} poza epoka 0 train.bin "
1157
+ f"({_nwin0 // a.batch} krokow)")
1158
+ sampler2 = EpochSampler(len(train2_data), a.block, a.batch, ds)
1159
+ _nwin2 = sampler2._meta(0)[2]
1160
+ log(f"data-dir-2: {a.data_dir_2} {len(train2_data):,} tok, {_nwin2:,} okien/epoke = {_nwin2 // a.batch} krokow, "
1161
+ f"od kroku strumienia {a.switch_step}")
1162
+ elif a.data_dir_2:
1163
+ raise SystemExit("STOP: --data-dir-2 wymaga --sampler epoch")
1164
+ elif start_step and not a.synthetic:
1165
+ # --resume: przewin generator o start_step batchy, zeby wznowiony run losowal DOKLADNIE te okna,
1166
+ # ktore run ciagly losowalby od kroku start_step (bez tego resume powtarzal okna z krokow 0..N).
1167
+ # Kazdy krok zuzywa 1 x torch.randint(len-block-1, (batch,)); 320k krokow ~1 s CPU.
1168
+ _n = len(train_data) - a.block - 1
1169
+ for _ in range(start_step):
1170
+ torch.randint(_n, (a.batch,), generator=train_gen)
1171
+ log(f"train RNG fast-forward o {start_step} krokow (resume)")
1172
+ if a.data_offset_steps:
1173
+ log(f"data-offset: dane od kroku strumienia {start_step + a.data_offset_steps} (krok biegu {start_step} + {a.data_offset_steps})")
1174
+ prefetcher = Prefetcher("train", train_gen, sampler=sampler, start_step=start_step + a.data_offset_steps)
1175
+ end_step = a.steps
1176
+ if a.queue and a.queue_one_slot:
1177
+ end_step = min(a.steps, (start_step // sampler.S + 1) * sampler.S)
1178
+ if sampler2 is not None:
1179
+ _last = a.data_offset_steps + end_step - 1 # ostatni krok strumienia tego biegu
1180
+ if _last - a.switch_step >= sampler2._meta(0)[2] // a.batch:
1181
+ raise SystemExit(f"STOP --data-dir-2: krok strumienia {_last} wychodzi poza 1 epoke DATA_DIR_2 (powtorzenie)")
1182
+
1183
+ if a.batch_sha:
1184
+ _want = sorted({int(s) for s in a.batch_sha.split(",")})
1185
+ if _want[0] < start_step or _want[-1] >= end_step:
1186
+ raise SystemExit(f"STOP --batch-sha: kroki poza [{start_step}, {end_step})")
1187
+ for s in range(start_step, _want[-1] + 1):
1188
+ xb, yb = prefetcher.next()
1189
+ if s in _want:
1190
+ _h = hashlib.sha256(xb.cpu().numpy().tobytes() + yb.cpu().numpy().tobytes()).hexdigest()
1191
+ log(f"BATCH-SHA krok biegu {s} = krok strumienia {s + a.data_offset_steps}: {_h}")
1192
+ prefetcher.close()
1193
+ log("BATCH-SHA DONE")
1194
+ return
1195
+
1196
+ def save_ckpt(done):
1197
+ if master:
1198
+ state = {"model": raw_model.state_dict(), "opt": opt.state_dict(), "step": done,
1199
+ "config": {"vocab": a.vocab, "n_layer": a.n_layer, "n_embd": a.n_embd,
1200
+ "n_head": a.n_head, "block": a.block, "norm": a.norm,
1201
+ "norm_eps": a.norm_eps, "pos": a.pos, "rope_theta": a.rope_theta,
1202
+ "ffn": a.ffn, "ffn_mult": a.ffn_mult, "value_residual": a.value_residual,
1203
+ "qk_norm": a.qk_norm, "logit_cap": a.logit_cap, "z_loss": a.z_loss,
1204
+ **({"loop": a.loop} if a.loop > 1 else {}),
1205
+ **({"data_seed": a.data_seed} if a.data_seed is not None else {}),
1206
+ **({"mixer_layout": a.mixer_layout, "gdn_head_dim": a.gdn_head_dim} if a.mixer_layout else {}),
1207
+ **({"attn_bias": a.attn_bias} if a.attn_bias != 1 else {}),
1208
+ **({"rope_style": a.rope_style} if a.rope_style != "interleaved" else {}),
1209
+ **({"emb_dim": a.emb_dim} if a.emb_dim else {}),
1210
+ **({"xsa": a.xsa} if a.xsa else {}),
1211
+ **({"rope_dims": a.rope_dims} if a.rope_dims else {}),
1212
+ **({"zero_init": a.zero_init} if a.zero_init else {}),
1213
+ **({"imha_t": a.imha_t} if a.imha_t else {}),
1214
+ **({"kv_heads": a.kv_heads} if a.arch == "gpt" and a.kv_heads else {}),
1215
+ **({"arch": "tinylm", "kv_heads": a.kv_heads or a.n_head, "tinylm_sha256": TINYLM_SHA}
1216
+ if a.arch == "tinylm" else {})}}
1217
+ if a.queue:
1218
+ state["queue"] = {"prefix_sha": sampler.prefix_sha(done), "slots_used": -(-done // sampler.S)}
1219
+ if teacher is not None:
1220
+ state["kd"] = {"teacher": a.kd_teacher, "teacher_sha256": _tsha, "alpha": a.kd_alpha, "temp": a.kd_temp}
1221
+ if a.train_only: # r21: poza "config", wiec --init-from i valport bez zmian
1222
+ state["train_only"] = {"regex": a.train_only, "n_trainable": n_train}
1223
+ torch.save(state, ckpt_path + ".tmp")
1224
+ os.replace(ckpt_path + ".tmp", ckpt_path) # crash w trakcie zapisu nie zostawia ucietego ckpt.pt
1225
+ log(f"ckpt @ {done}")
1226
+ if a.events_jsonl:
1227
+ _h = hashlib.sha256()
1228
+ with open(ckpt_path, "rb") as _cf:
1229
+ for _chunk in iter(lambda: _cf.read(1 << 20), b""):
1230
+ _h.update(_chunk)
1231
+ emit("checkpoint", done, sha256=_h.hexdigest())
1232
+ if ddp:
1233
+ dist.barrier() # inne ranki nie ida dalej (ani nie koncza), zanim rank 0 zapisze ckpt
1234
+
1235
+ def finish():
1236
+ prefetcher.close()
1237
+ if ddp:
1238
+ dist.destroy_process_group()
1239
+
1240
+ # r9 (Monter, 06.10): strata sumowana na urządzeniu (float64), .item() tylko przy logu. r8 robił loss.item() co krok,
1241
+ # czyli synchronizację GPU->CPU co krok, przez co CPU nie mógł kolejkować następnego kroku. Obliczenia modelu,
1242
+ # kolejność batchy i aktualizacje wag są identyczne z r8; zmienia się tylko moment odczytu straty do logu.
1243
+ t0 = time.time(); running = torch.zeros((), dtype=torch.float64, device=device)
1244
+ running_ce = torch.zeros((), dtype=torch.float64, device=device)
1245
+ for step in range(start_step, end_step):
1246
+ lr = lr_at(step)
1247
+ for g in opt.param_groups:
1248
+ g["lr"] = lr * g.get("lr_mult", 1.0)
1249
+ xb, yb = prefetcher.next()
1250
+ if a.grad_accum == 1:
1251
+ with ctx:
1252
+ if teacher is None:
1253
+ _, loss = model(xb, yb)
1254
+ else:
1255
+ with torch.no_grad():
1256
+ tl = teacher_fwd(xb)[0]
1257
+ _, loss, ce = model(xb, yb, tl)
1258
+ del tl
1259
+ running_ce += ce.to(torch.float64)
1260
+ loss.backward()
1261
+ else: # r16: K mikrobatchy z wierszy ranku, gradient = srednia (strata_j / K); DDP synchronizuje tylko ostatni
1262
+ K = a.grad_accum; m = xb.size(0) // K
1263
+ loss = torch.zeros((), dtype=torch.float32, device=device)
1264
+ for j in range(K):
1265
+ xj, yj = xb[j * m:(j + 1) * m], yb[j * m:(j + 1) * m]
1266
+ with (model.no_sync() if ddp and j < K - 1 else nullcontext()):
1267
+ with ctx:
1268
+ if teacher is None:
1269
+ _, lj = model(xj, yj)
1270
+ else:
1271
+ with torch.no_grad():
1272
+ tl = teacher_fwd(xj)[0]
1273
+ _, lj, ce = model(xj, yj, tl)
1274
+ del tl
1275
+ running_ce += ce.to(torch.float64) / K
1276
+ (lj / K).backward()
1277
+ loss += lj.detach().float() / K
1278
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), a.grad_clip) if a.grad_clip > 0 else 0.0
1279
+ opt.step(); opt.zero_grad(set_to_none=True)
1280
+ running += loss.detach().to(torch.float64)
1281
+ if (step + 1) % a.log_every == 0:
1282
+ if ddp: # strata logowana = srednia po wszystkich wierszach globalnego batcha (jak w 1-GPU)
1283
+ dist.all_reduce(running)
1284
+ running = running / world
1285
+ if teacher is not None:
1286
+ dist.all_reduce(running_ce)
1287
+ running_ce = running_ce / world
1288
+ running = running.item()
1289
+ kd_ce = running_ce.item() / a.log_every if teacher is not None else None
1290
+ dt = time.time() - t0
1291
+ tok_s = a.log_every * a.block * a.batch / dt
1292
+ mem = torch.cuda.max_memory_allocated(device)/1e9 if device.type == "cuda" else 0.0
1293
+ log(f"step {step+1}/{a.steps} loss {running/a.log_every:.4f} lr {lr:.2e} gnorm {float(gn):.2f} {tok_s:,.0f} tok/s peakVRAM {mem:.1f}GB"
1294
+ + (f" ce {kd_ce:.4f}" if kd_ce is not None else ""))
1295
+ if master:
1296
+ with open(metrics_path, "a", encoding="utf-8") as f:
1297
+ f.write(json.dumps({"step": step+1, "loss": running/a.log_every, "lr": lr, "tok_s": tok_s,
1298
+ **({"ce": kd_ce} if kd_ce is not None else {})}) + "\n")
1299
+ emit("update", step + 1, metrics={"loss": running / a.log_every, "tokens_per_second": tok_s,
1300
+ "gradient_norm": float(gn), "learning_rate": lr})
1301
+ running = torch.zeros((), dtype=torch.float64, device=device); t0 = time.time()
1302
+ running_ce = torch.zeros((), dtype=torch.float64, device=device)
1303
+ if (step + 1) % a.eval_every == 0:
1304
+ vloss = eval_val()
1305
+ log(f" >> VAL loss {vloss:.4f} @ {step+1}")
1306
+ emit("evaluation", step + 1, metrics={"loss": vloss})
1307
+ if (step + 1) % a.ckpt_every == 0 and not a.synthetic:
1308
+ save_ckpt(step + 1)
1309
+ if end_step % a.ckpt_every and (a.queue or not a.synthetic):
1310
+ save_ckpt(end_step) # r20: zawsze ckpt po ostatnim kroku poza rytmem ckpt_every (r18/r19: tylko z --queue; k9 13.10: 1140 zamiast 1145)
1311
+ if end_step < a.steps:
1312
+ log(f"SLOT DONE @ {end_step}")
1313
+ finish()
1314
+ return
1315
+ log("DONE")
1316
+ emit("end", a.steps, status="completed")
1317
+ finish()
1318
+
1319
+
1320
+ if __name__ == "__main__":
1321
+ main()