Text Generation
PyTorch
Safetensors
Polish
English
polish
causal-lm
from-scratch
Maggio33 commited on
Commit
a78f425
·
verified ·
1 Parent(s): 3add7eb

Slayer1B 1.07: converted weights of the final checkpoint (step 22327)

Browse files
Files changed (7) hide show
  1. README.md +22 -4
  2. SHA256SUMS +5 -0
  3. config.json +28 -0
  4. load_slayer.py +38 -0
  5. model.safetensors +3 -0
  6. tokenizer.json +0 -0
  7. train_gpt_ref_r23.py +1321 -0
README.md CHANGED
@@ -20,10 +20,10 @@ Decoder-only language model, 1 073 912 851 parameters, Polish-dominant. **Slayer
20
  Slayer1B 1.06** from its final checkpoint (step 13848) on 8 891 005 707 training tokens (about 7.0 B new and 1.9 B
21
  replayed from the 1.06 data), at the same constant learning rate and global batch.
22
 
23
- **Status: training complete at step 22327 (6 November 2026).** All 15 evaluated checkpoints of the run, from the 1.06
24
- starting point (13848) to the final checkpoint (22327), are in the training-time evaluation table below. A 1.07 model release (converted weights) has not been
25
- published in this repository yet. Native training checkpoints (raw PyTorch training state `ckpt_N.pt`, not a
26
- ready-to-use model) are published file by file in
27
  [SlayerLab/Slayer1B-1.07-checkpoints](https://huggingface.co/SlayerLab/Slayer1B-1.07-checkpoints), with sha256 sums.
28
 
29
  - Previous version: [SlayerLab/Slayer1B-1.06](https://huggingface.co/SlayerLab/Slayer1B-1.06) (step 13848,
@@ -111,3 +111,21 @@ were scanned for 13-gram overlap with the test items of the evaluation suite bef
111
  hit were removed. Exact matches of short answers (below 13 words) were counted but not removed, and one evaluation set
112
  (EWoK, distributed as a password-protected archive) was not part of the scan. The Slayer 1.06 core includes the source
113
  passage corpora of PolQA and PoQuAD; passages with a 13-gram hit were removed and the rest remain in the replay.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  Slayer1B 1.06** from its final checkpoint (step 13848) on 8 891 005 707 training tokens (about 7.0 B new and 1.9 B
21
  replayed from the 1.06 data), at the same constant learning rate and global batch.
22
 
23
+ **Status: training complete at step 22327 (6 November 2026). This repository contains the converted weights of the
24
+ final checkpoint (step 22327)**; see Files below. All 15 evaluated checkpoints of the run, from the 1.06 starting point
25
+ (13848) to the final checkpoint (22327), are in the training-time evaluation table below. Native training checkpoints
26
+ (raw PyTorch training state `ckpt_N.pt`, not a ready-to-use model) are published file by file in
27
  [SlayerLab/Slayer1B-1.07-checkpoints](https://huggingface.co/SlayerLab/Slayer1B-1.07-checkpoints), with sha256 sums.
28
 
29
  - Previous version: [SlayerLab/Slayer1B-1.06](https://huggingface.co/SlayerLab/Slayer1B-1.06) (step 13848,
 
111
  hit were removed. Exact matches of short answers (below 13 words) were counted but not removed, and one evaluation set
112
  (EWoK, distributed as a password-protected archive) was not part of the scan. The Slayer 1.06 core includes the source
113
  passage corpora of PolQA and PoQuAD; passages with a 13-gram hit were removed and the rest remain in the replay.
114
+
115
+ ## Files
116
+
117
+ `model.safetensors` (weights, float32), `config.json`, `tokenizer.json`, `load_slayer.py` (loader),
118
+ `train_gpt_ref_r23.py` and `SHA256SUMS`. The `config.json` records the sha256 of the source checkpoint `ckpt_22327.pt`
119
+ (`48cdb5eb16b69e0ee9a7a2ed72895ff47c0ba57aca0f4516b1f7e9cba286685c`, also published in the checkpoints repository).
120
+ The file layout and the loader are the same as in [SlayerLab/Slayer1B-1.06](https://huggingface.co/SlayerLab/Slayer1B-1.06):
121
+
122
+ ```python
123
+ import load_slayer
124
+ from tokenizers import Tokenizer
125
+
126
+ model = load_slayer.load(".") # float32, eval mode, on CPU
127
+ tok = Tokenizer.from_file("tokenizer.json") # id 0 is end-of-text
128
+ ```
129
+
130
+ `train_gpt_ref_r23.py` is the model definition (`GPT` class) imported by `load_slayer.py`; `trainer_sha256` in
131
+ `config.json` refers to this file. The architecture is unchanged from 1.00–1.06.
SHA256SUMS ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ 9e2062501a87d0b87d7ef75ba8bc5484f3b1afdf42081e727a74dc02f5a4afce config.json
2
+ 53a04c726bebbf8d8a25ff1479ab4f70c3003022675d87332d130139cca1d020 load_slayer.py
3
+ d826b4a11bf4eaccee10fe43ec19a4f6d515dfc5f3e5c859adb631f1a0b76ad5 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": "48cdb5eb16b69e0ee9a7a2ed72895ff47c0ba57aca0f4516b1f7e9cba286685c",
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": 22327,
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:d826b4a11bf4eaccee10fe43ec19a4f6d515dfc5f3e5c859adb631f1a0b76ad5
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()