Maggio33 commited on
Commit
0fcd392
·
verified ·
1 Parent(s): c989b7c

Slayer1B 1.09: card, data card and training configuration (in preparation)

Browse files
Files changed (5) hide show
  1. DATA_CARD.md +60 -0
  2. README.md +67 -0
  3. TRAINING_CONFIGURATION.md +79 -0
  4. train_gpt_ref_r24_2.py +1431 -0
  5. uruchom_109.sh +209 -0
DATA_CARD.md ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ pretty_name: Slayer1B 1.09 proposed training-data composition
3
+ language:
4
+ - pl
5
+ - en
6
+ task_categories:
7
+ - text-generation
8
+ tags:
9
+ - pretraining
10
+ - planning-stage
11
+ ---
12
+
13
+ # Slayer1B 1.09 — data plan
14
+
15
+ **Arkadiusz Słota, Anna Sieczka, Piotr Styła / Fabryka AI (SlayerLab)**
16
+
17
+ **Planning-stage documentation, 9 November 2026; decisions through 19:11.** The first rebalanced reference shard is prepared and byte-approved; the full 100B corpus is not prepared and no 1.09 training is claimed. Goal: a 1B model, 100B TRAIN positions, approximately 20–30% Polish, 72 hours and USD 400; these are not achieved results.
18
+
19
+ ## Target, units and exposure
20
+
21
+ The full-corpus target is **100,000,000,000 native Slayer v6 TRAIN positions**, including document-ending EOS 0. The current nominal allocation is **21,370,000,000 PL / 78,630,000,000 other**, within the permitted approximate **20–30% Polish** range. These are targets, not retained or consumed counts. Source buckets do not prove document language. GPT-2, Llama, cl100k proxy, word and estimated v6 units cannot substitute for measured native positions.
22
+
23
+ Provisional schedule: 95,000 updates × 1,048,576 positions = **99,614,720,000**, not 100B. Nineteen 5,000-update slots process **5,242,880,000 positions each** from input shards targeted at 5.3–5.4B. An owner-approved schedule must resolve the 100B mismatch.
24
+
25
+ The selected plan is random-weight initialization with the existing tokenizer, not continuation from trained 1.07 weights; no launch is claimed. Rebalanced s01 v2 has a prepared reference of **5,316,374,064 positions**: **4,180,222,380 English-source** and **1,136,151,684 Polish-source** positions, approximately 21.37% PL. These describe input capacity, not the 5,242,880,000 positions processed by its planned slot. Exact consumed language shares are unavailable.
26
+
27
+ Earlier models' material is reused as data, not weights. Used documents are recorded for exclusion from later shards, with exact token-sequence deduplication between inputs. This is not a guarantee of semantic uniqueness or absence of near-duplicates.
28
+
29
+ ## Proposed source catalog
30
+
31
+ The 18:19 EN allocation supersedes the earlier category proposal: **38B FineWeb-Edu, 10B StackExchange, 8B peS2o, 3B arXiv, 3B PubMed, 6B Wikimedia, 6B FineMath, 3.63B FinePDFs-Edu, 1B OER/books = 78.63B**. Dedicated code is zero; StackExchange code/QA is not a 12B pure-code corpus. Final domain shares require actual document classification after filtering.
32
+
33
+ Linked public cards describe upstream material, not approval of every document:
34
+
35
+ | Candidate | Published unit / selection condition |
36
+ |---|---|
37
+ | [GoLLeM PL](https://huggingface.co/datasets/SlayerLab/gollem-corpus-16b-pl), `17f9d37a` | Principal PL candidate: 16.58B published V32k tokens, including 14.62B HPLT web; not measured v6. Per-record `license`/`is_share_alike`; curated rights and web TDM checked separately |
38
+ | [FinePDFs-Edu](https://huggingface.co/datasets/HuggingFaceFW/finepdfs-edu), `9cfabe21`, PL/EN | PL pool approximately 8.3B v6 is an estimate, not retained size; ODC-By/Common Crawl terms; deduplicate against Logika |
39
+ | [PL Logika](https://huggingface.co/datasets/SlayerLab/slayer-pl-logika-2b), `e24c8b71` | Published 1,911,918,958 native positions including EOS; earlier filtered reference approximately 1.63B, not a new 1.09 measurement; mixed terms/provenance |
40
+ | [English Technical](https://huggingface.co/datasets/SlayerLab/english-technical-1b), `3b833d36`; [1.07 replay](https://huggingface.co/datasets/SlayerLab/slayer-1.07-pretrain), `6a337bbf` | Published technical TRAIN 899,001,323 native tokens before EOS; replay 8,891,005,707 EOS-inclusive positions; not s01 retained totals |
41
+ | Common Pile [peS2o](https://huggingface.co/datasets/common-pile/peS2o_filtered), [PubMed](https://huggingface.co/datasets/common-pile/pubmed_filtered), [arXiv](https://huggingface.co/datasets/common-pile/arxiv_papers_filtered) | Published text sizes: 182.6/147.1/19 UTF-8 GB; per-document open-license allowlist, not blanket permission |
42
+ | Common Pile [StackExchange](https://huggingface.co/datasets/common-pile/stackexchange_filtered), [Wikimedia](https://huggingface.co/datasets/common-pile/wikimedia_filtered) | 89.7/57.4 UTF-8 GB; preserve attribution and applicable CC BY-SA terms |
43
+ | Common Pile [DOAB](https://huggingface.co/datasets/common-pile/doab_filtered), [PressBooks](https://huggingface.co/datasets/common-pile/pressbooks_filtered), [LibreTexts](https://huggingface.co/datasets/common-pile/libretexts_filtered), [OERCommons](https://huggingface.co/datasets/common-pile/oercommons_filtered) | 12/0.6/3.6/0.05 UTF-8 GB; per-document licensing; exclude LibreTexts GFDL material |
44
+ | [FineMath](https://huggingface.co/datasets/HuggingFaceTB/finemath), [FineWeb-Edu](https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu) | FineMath-4+ reports 9.6B Llama tokens; FineWeb-Edu samples use GPT-2; ODC-By plus Common Crawl terms |
45
+
46
+ PL reserves include FineWeb2-HQ-Plus, Polish Dynaword (6.65B cl100k, not v6) and 1.07 replay; overlap and post-filter yields remain unmeasured. No conversion establishes 21.37B retained PL. peS2o's card says `metadata.license`, while the inventory identifies `metadata.oa_license`; use the actual pinned schema.
47
+
48
+ ## Admission, isolation and byte approval
49
+
50
+ Use a per-document allowlist: CC0/public domain, eligible CC BY/CC BY-SA, ODC-By, explicitly approved permissive terms or separately reviewed HPLT TDM-web. Unknown/missing, NC, ND, GPL, AGPL and GFDL are out. No The Stack or frontier-model-generated text. Preserve creators, licenses, URLs and modification notices. HPLT compilation terms, ODC database rights and TDM do not grant blanket redistribution rights.
51
+
52
+ For new material reserve 1% of documents per group **before TRAIN/sharding**, grouping source-book identities; this is not 1% of tokens. Preserve existing evaluation partitions. Apply exact/near-deduplication and 13-gram plus 8-gram-template decontamination with positive controls, including GSM8K/MATH/ARC/panels.
53
+
54
+ **Legacy s01 exception:** approved 1.07 bins may train before scanning against later held-outs. Before 1.09 evaluations, scan decoded bins; exclude hit items for both compared models, leave bins unchanged and report removals and resulting denominators. Do not describe all inputs as pre-decontaminated.
55
+
56
+ GoLLeM reports LLMzSzŁ variants for 1,662/18,820 items (8.83%); exclude affected items if evaluated. Its card conflicts about PoQuAD exclusion versus partition inclusion: do not use PoQuAD without decontamination. The separate 1.07 replay retains its reported near-deduplication and EWoK omissions; these do not disappear with random-weight initialization.
57
+
58
+ Whole-document PII DROP changes coverage; document-drop fraction is not token-loss fraction. Independent residual checks, post-filter document/native-token counts, deduplication losses and final pins remain pending. Identifiers, extraction damage, bias and incorrect reasoning may survive.
59
+
60
+ Conditional content/code GREEN is **not byte GREEN**. Each tranche needs configuration, manifest, byte-derived SHA approval and queue-prefix/resume identity. Rebalanced s01 v2 received byte approval with TRAIN SHA256 `2ab70797649947e0c14b8067161ed8a2541585428ca52c5ac8c9a744669bbb06`; that does not approve all 100B. Tokenizer v6 SHA256: `0640d3bd3674d7a6e59c540d945a2ad275a12e1bcaeb6007c37a90751f9815dd`. Publication separately needs rights, attribution and egress GREEN. Full-corpus manifests, final pins and aggregate language audit remain pending.
README.md ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - pl
4
+ - en
5
+ library_name: pytorch
6
+ pipeline_tag: text-generation
7
+ tags:
8
+ - polish
9
+ - causal-lm
10
+ - pretraining
11
+ - planning
12
+ ---
13
+
14
+ # Slayer1B 1.09
15
+
16
+ **Arkadiusz Słota, Anna Sieczka, Piotr Styła / Fabryka AI (SlayerLab)**
17
+
18
+ **Planning-stage card, 9 November 2026, reflecting decisions through 19:11 UTC.** No trained 1.09 checkpoint, actual initialization, measured throughput, evaluation scores or public run URL is established. This is not a released model or an inference-ready artifact.
19
+
20
+ ## Objective and starting point
21
+
22
+ The objective is a roughly 1B-parameter causal language model trained on **100 billion native v6 token positions, including document EOS 0, within 72 hours and USD 400 total**. The requested balance is approximate: **20–30% Polish**, with the remainder mainly English. The current selected recipe remains near **21.37% / 78.63%**; a later adjustment requires an explicit shard decision. These are targets, not completed exposure. Base-model training takes priority over instruction tuning.
23
+
24
+ The selected plan is **random-weight, from-scratch initialization** with the existing v6 tokenizer, not continuation from trained 1.07 weights. Initialization has not yet been observed. Reusing a tokenizer does not mean inheriting trained model weights.
25
+
26
+ The unchanged architecture is specified as **1,073,912,851 parameters**, 20 layers, width 2,048, 16 heads, SwiGLU, RMSNorm, RoPE base 500,000, QK normalization and value residuals. The reused v6 tokenizer has vocabulary 32,768 and pin `0640d3bd3674d7a6e59c540d945a2ad275a12e1bcaeb6007c37a90751f9815dd`.
27
+
28
+ Architecture and tokenizer lineage: [SlayerLab/Slayer1B-1.07](https://huggingface.co/SlayerLab/Slayer1B-1.07/blob/a090b54e658fb57dc5166c1fcc90cc6bf5c80106/README.md), revision `a090b54e658fb57dc5166c1fcc90cc6bf5c80106`. This identifies the predecessor, not a weight initialization or 1.09 checkpoint.
29
+
30
+ The [training configuration](TRAINING_CONFIGURATION.md) identifies exact code digests and separates the saved, actually used 1.08 flags from this selected 1.09 recipe.
31
+
32
+ ## Proposed recipe, not launch evidence
33
+
34
+ | Setting | Proposal |
35
+ |---|---|
36
+ | Trainer / optimizer | Existing r24.2; Muon with AdamW; fresh optimizer |
37
+ | Batch / context | 256 × 4,096 = 1,048,576 positions per update |
38
+ | Layout | Eight GPU ranks; microbatch 8 × accumulation 4; activation checkpointing off |
39
+ | Budget | 95,000 updates; nineteen slots of 5,000 |
40
+ | WSD | 2,000 warmup updates; constant LR 0.0006, Muon 0.02; final 20% uses 1−sqrt decay to nonzero LR floor 0.00006 |
41
+ | Checkpoints | Every 500 updates and retained slot-end milestones |
42
+
43
+ **95,000 × 1,048,576 = 99,614,720,000 positions (99.61472B), not 100B.** Each 5,000-update slot processes 5.24288B positions; planned input shards are larger, about 5.3–5.4B. The 385,280,000-position shortfall needs an approved step/batch policy; no correction is selected here.
44
+
45
+ Queue mode uses append-only shards, manifest ranges and SHA-256 identities. Resume must preserve existing prefixes and earlier steps; new byte ranges must match tranche approval before use. Reported CPU smoke matched slotwise and continuous training bit-for-bit, including two-process DDP. The launcher received GREEN; this is not a production GPU run or approval of unverified new shards.
46
+
47
+ ## Data and exposure
48
+
49
+ The latest English source allocation replaces the earlier web/code/category proposal: educational web is combined with science, wiki, QA, mathematics and books. No dedicated code corpus is currently selected. Post-filter counts and source-language assignments remain necessary; see [the data card](DATA_CARD.md).
50
+
51
+ The selected recipe currently targets **21.37% PL / 78.63% other** in each shard rather than alternating language-heavy slots. The user allows an approximate 20–30% Polish range; this is not a requirement to rebuild already-approved input or silently change domains.
52
+
53
+ New material requires per-document licensing, PII filtering, contamination scans and grouped held-outs before sharding. A 1% document holdout is not necessarily 1% of tokens. Legacy inputs have a separate pre-evaluation contamination condition. The selected pipeline records used documents and excludes them from later shards; exact deduplication does not establish semantic uniqueness. Prepared bucket shares do not establish the language share of consumed windows.
54
+
55
+ ## Evaluation and acceptance
56
+
57
+ Compare fixed **FINAL**, not a retrospectively selected milestone, with 1.07 on matching hardware: Polish H bits/byte, grouped held-outs, mathematics, ARC-Easy, BLiMP, ARC-Easy-PL and MultiBLiMP-PL. Final preregistration details await approval.
58
+
59
+ Proposed final guards inherit 1.08 thresholds: H increase at most 0.005 bits/byte; each of the four panels at least −1 percentage point versus 1.07. Separately, milestone H increase above 0.02 or Polish-panel losses exceeding 2 points are **operator alarms, not automatic stops or final guards**.
60
+
61
+ Comparison is descriptive: no replay-only control is budgeted, and initialization, exposure and schedule differ. No causal dataset benefit or multi-seed significance can be claimed.
62
+
63
+ ## Cost, licensing and limitations
64
+
65
+ USD 400 authorizes the **total session**, including preparation, restarts, evaluation and interruptions, not just steady-state training. Funding is reported ready by the account owner; actual expense and 1.09 speed remain unmeasured. Meeting the deadline is unverified.
66
+
67
+ 1.07 declares Apache-2.0; this does not assign a license to future 1.09 weights or source texts. Release and publication rights require separate approval. Privacy filters are heuristic; generated text may be inaccurate, biased, offensive or personal. No reasoning quality, instruction-following ability or professional reliability has been demonstrated for 1.09.
TRAINING_CONFIGURATION.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Which training code and flags?
2
+
3
+ Fabryka AI / SlayerLab
4
+
5
+ **1.08 is a completed continued-pretraining experiment; 1.09 is the separate from-scratch 100B-target run.** A launch attempt is not proof of completed training. This record distinguishes saved 1.08 arguments from the selected 1.09 recipe.
6
+
7
+ ## Code identities
8
+
9
+ | Component | Identity |
10
+ |---|---|
11
+ | Actual training implementation, both recipes | `train_gpt_ref_r24_2.py`, SHA256 `46f83b51fcec18ffc3273d80fb9eab29e182c52d8667800dab27b80f7ff18afc` |
12
+ | First 1.09 launch attempt | `uruchom_109.sh`, SHA256 `dfee11522be49c938f169ca6992cf181f86a3c4e0d26fd5b5fa3646535dc890f` |
13
+ | Selected restart launcher v3 | `uruchom_109.sh`, SHA256 `c0a2699d1a2d7ce4cddd873ad92ff5ab5bb0dbcecc6e47d6bce01ee9b848895c` |
14
+ | Saved, actually used 1.08 argument file | SHA256 `d283bcb836e9bb89d5bf7dfa60487055086cf04620ad5c5dd36fe3435f2766a9` |
15
+
16
+ The older public `train_gpt_ref_r23.py` is **not the identity of this training implementation**. The inference-compatible architecture code packaged with a weight release and the executable used to train it are distinct artifacts; filenames alone do not establish identity.
17
+
18
+ The recipes use one eight-GPU B200 node, PyTorch 2.8/CUDA 12.8 and DDP. They are not a four-RTX-4090 configuration. Compute uses bf16 on the verified hardware; `--dtype uint16` below describes **stored token IDs**, not model arithmetic.
19
+
20
+ ## Shared explicit learning arguments
21
+
22
+ These arguments occur in the saved 1.08 invocation and the selected 1.09 launcher:
23
+
24
+ ```text
25
+ --batch 256 --grad-accum 4 --block 4096 --dtype uint16 --vocab 32768
26
+ --n-layer 20 --n-embd 2048 --n-head 16
27
+ --norm rmsnorm --norm-eps 1e-6 --pos rope --rope-theta 500000
28
+ --ffn swiglu --ffn-mult 2.667 --value-residual --qk-norm
29
+ --attn-bias 0 --logit-cap 0 --z-loss 0
30
+ --optimizer muon --lr 6e-4 --min-lr 6e-5 --muon-lr 0.02
31
+ --wd 0.1 --grad-clip 1.0 --schedule wsd --rewarm-steps 0
32
+ --seed 1 --loss-chunk 1024 --compile --log-every 10
33
+ ```
34
+
35
+ Global batch is 256 sequences: eight ranks × microbatch 8 × accumulation 4. Activation checkpointing is disabled. Each update processes 1,048,576 token positions. Muon handles matrix parameters; AdamW handles the remaining parameter groups.
36
+
37
+ ## Differences that change the experiment
38
+
39
+ | Setting | 1.08: saved arguments / completed run | 1.09: selected recipe, not completion |
40
+ |---|---|---|
41
+ | Weight initialization | `--init-from` the public 1.07 weights, fresh optimizer | `START=zero`; no `--init-from` |
42
+ | Total updates | `--steps 1709` | `--steps 95000` by default |
43
+ | Corresponding processed positions | 1,792,016,384 | 99,614,720,000 if all nominal updates complete, **not exactly 100B** |
44
+ | Sampler | `--sampler epoch` | `--queue` manifest; default slotwise operation adds `--queue-one-slot` |
45
+ | Data seed | `--data-seed 20261109` | `--data-seed 20261110` |
46
+ | Warmup | `--warmup 300` | `--warmup 2000` for random initialization |
47
+ | Final WSD decay | `--decay-frac 0.25` | `--decay-frac 0.2`; final 20%, nominally from update 76,000 |
48
+ | Checkpoint frequency | `--ckpt-every 250` | `--ckpt-every 500` plus slot-end milestones |
49
+ | In-training validation | `--eval-every 1000000 --eval-iters 1`; no periodic validation within 1,709 updates | `--eval-every 1000 --eval-iters 20` |
50
+ | Queue slot | Not used | `S=5000`, nineteen nominal slots |
51
+
52
+ Both argument sets contain `--resume` for automatic recovery. That flag does **not** mean 1.08 inherited the 1.07 optimizer, nor that a fresh 1.09 launch resumed trained weights. The launcher checks whether a checkpoint actually exists; its preflight log must establish the real starting state.
53
+
54
+ Output directories, run IDs, data directories, the queue manifest and `--events-jsonl` are operational arguments, not omitted learning choices. For 1.08 they identify `slayer1b_108_r1`; 1.09 uses a separate output history. Do not share output directories between experiments.
55
+
56
+ ## Selected 1.09 operator invocation
57
+
58
+ On the allocated, verified node, with the pinned trainer and launcher already installed and an approved data directory:
59
+
60
+ ```bash
61
+ START=zero GPU=B200 S=5000 KROKI=95000 NCCL_P2P_DISABLE=0 \
62
+ D=/workspace/dane109 O=/workspace/runs/109 \
63
+ PY=/workspace/venv/bin/python TORCHRUN=/workspace/venv/bin/torchrun \
64
+ bash /workspace/kod/uruchom_109.sh
65
+ ```
66
+
67
+ **Set `S=5000` explicitly:** the selected launcher retains a different historical default. Its manifest must agree. Use a fresh environment without access tokens; do not bypass hardware, checksum or preflight guards. H200 support in launcher v3 is a capability, **not evidence that this experiment ran on H200**. A hardware change requires its own numerical control and truthful run metadata.
68
+
69
+ The directory contains `train.bin`, `val.bin` and `man.json`. Add approved shard bytes **before** publishing the matching manifest ranges and SHA256 values. Keep the existing prefix and sampler history; do not replace an epoch-sampled file mid-run. `CIAGLE=1` is allowed only once every required slot is present. Otherwise each invocation runs one slot, saves its endpoint and waits for the next approved input.
70
+
71
+ Recovery uses the same output directory and arguments. Full optimizer state remains in `ckpt.pt`; v3 retains weights-only milestone files separately and can reclaim already-consumed shard ranges without changing offsets. Audit then relies on approved input digests and reconstruction recipes, not a claim that reclaimed bytes can still be reread.
72
+
73
+ Launcher v3 permits a total-step adjustment only before both the old and new decay boundaries. This technical capability is **not permission to report a shorter run as completed 100B training**: any changed length must be authorized, recorded and converted to actual processed positions.
74
+
75
+ ## What this does not claim
76
+
77
+ The 1.08 argument file is direct execution evidence. The 1.09 launcher is a selected, reviewed recipe; runtime logs, checkpoints and final counters establish what actually happened. Steady interval throughput is not end-to-end billed throughput, especially when checkpoint and validation schedules differ.
78
+
79
+ Exact execution also requires the approved data bytes, manifest and filtering artifacts. The full 100B corpus is being assembled; public code inspection alone does not supply those data or protected evaluation material. No held-out questions, gold answers, generated evaluation outputs or private row-exclusion lists belong in this public bundle. See the [model card](README.md) and [data card](DATA_CARD.md) for status, source rights and limitations.
train_gpt_ref_r24_2.py ADDED
@@ -0,0 +1,1431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ r24 (Hart, 23.10, Slayer1B 1.02, Arek 21:04Z): łańcuch strumieni danych po --data-dir-2. --data-dirs-next D3[,D4...] i
124
+ --switch-steps-next S3[,S4...] (kroki strumienia, rosnące, > --switch-step): od kroku strumienia S_k okna z D_k/train.bin,
125
+ własna permutacja epokowa (EpochSampler, to samo --data-seed) liczona od S_k — dokładnie jak --data-dir-2 od
126
+ --switch-step. Każdy odcinek musi zmieścić się w 1 epoce swojego train.bin (inaczej STOP: powtórzenie okien).
127
+ Bez nowych flag = r23 bit w bit (ta sama ścieżka kodu dla kroków < S3, więc wznowienie biegu r23 z tym samym
128
+ --data-dir-2/--switch-step daje te same batche do kroku S3 − 1; sprawdzane --batch-sha przed biegiem).
129
+ --ckpt-at K1[,K2...]: dodatkowe ckpt po krokach K poza rytmem --ckpt-every (reguła Arka: ckpt dokładnie w
130
+ decay_start = int(0.8*T), ostatni stan stałego lr). Bez flagi = r23.
131
+ r24.1 (Hart, 24.10, 1.03 na 4×4090, OOM przy --resume na 24 GB, Monter 09:12Z): po wczytaniu ckpt usuwane są z `ck` klucze
132
+ "model" i "opt" (wcześniej cały ckpt, 8,86 GB, zostawał na karcie do końca biegu) + empty_cache. Wartości, kolejność danych
133
+ i matematyka bez zmian; bez --resume = r24 bit w bit.
134
+ r24.2 (Hart, 27.10): opt-in --global-batch-sha ZERO po rzeczywistym opt.step():
135
+ SHA256 wszystkich256 wierszy x/y, w kolejności rank → microstep → row. Zbierane są
136
+ wyłącznie32-bajtowe skróty wierszy. Bez flagi brak kopiowania danych ani kolektywu.
137
+ """
138
+ import argparse
139
+ import hashlib
140
+ import json
141
+ import math
142
+ import os
143
+ import re
144
+ import struct
145
+ import time
146
+ import queue
147
+ import threading
148
+ from contextlib import nullcontext
149
+
150
+ import numpy as np
151
+ import torch
152
+ import torch.distributed as dist
153
+ import torch.nn as nn
154
+ import torch.nn.functional as F
155
+ from torch.utils.checkpoint import checkpoint
156
+ from torch.nn.parallel import DistributedDataParallel as DDP
157
+
158
+
159
+ def get_args():
160
+ p = argparse.ArgumentParser()
161
+ p.add_argument("--data-dir", default=".")
162
+ p.add_argument("--out-dir", default=None)
163
+ p.add_argument("--run-id", default="gpt25m_run1")
164
+ p.add_argument("--steps", type=int, default=30000)
165
+ p.add_argument("--batch", type=int, default=32)
166
+ p.add_argument("--block", type=int, default=256)
167
+ p.add_argument("--n-layer", type=int, default=8)
168
+ p.add_argument("--n-embd", type=int, default=512)
169
+ p.add_argument("--n-head", type=int, default=8)
170
+ p.add_argument("--lr", type=float, default=6e-4)
171
+ p.add_argument("--min-lr", type=float, default=6e-5)
172
+ p.add_argument("--warmup", type=int, default=200)
173
+ p.add_argument("--schedule", choices=["cosine", "wsd"], default="cosine",
174
+ help="cosine (default) | wsd (warmup-stable-decay: constant peak then 1-sqrt decay, JugnuLM ARC-closer)")
175
+ p.add_argument("--decay-frac", type=float, default=0.2,
176
+ help="WSD: fraction of steps in the final decay phase (default 0.2)")
177
+ p.add_argument("--rewarm-steps", type=int, default=0,
178
+ help="przy --resume: liniowe przejscie LR od wartosci z ckpt do harmonogramu przez N krokow (0=off)")
179
+ p.add_argument("--wd", type=float, default=0.1)
180
+ p.add_argument("--grad-clip", type=float, default=1.0)
181
+ p.add_argument("--log-every", type=int, default=50)
182
+ p.add_argument("--eval-every", type=int, default=500)
183
+ p.add_argument("--eval-iters", type=int, default=50)
184
+ p.add_argument("--ckpt-every", type=int, default=1000)
185
+ p.add_argument("--ckpt-at", default=None,
186
+ help="r24: dodatkowe kroki ckpt poza rytmem --ckpt-every, po przecinku (np. decay_start = int(0.8*T))")
187
+ p.add_argument("--seed", type=int, default=1337)
188
+ p.add_argument("--data-seed", type=int, default=None,
189
+ help="r23: ziarno kolejności danych (domyślnie = --seed); --seed zostaje ziarnem inicjalizacji modelu")
190
+ p.add_argument("--sampler", choices=["random", "epoch"], default="random",
191
+ help="random (r5: okna ze zwracaniem, ~75%% korpusu przy 1.4 ep.) | epoch (r6: kazde okno raz na epoke, "
192
+ "per-epoka permutacja + losowe przesuniecie siatki; resume dokladny bez fast-forwardu)")
193
+ p.add_argument("--resume", action="store_true")
194
+ p.add_argument("--init-from", default=None,
195
+ help="r10 (Wzrost): start z wag innego ckpt (np. z grow_depth.py), krok 0 i swiezy optymalizator; "
196
+ "ignorowane, gdy --resume wznawia wlasny ckpt.pt tego biegu")
197
+ p.add_argument("--mixer-layout", default=None,
198
+ help="r13: litera na blok (A = uwaga, G = GatedDeltaNet2), dlugosc = --n-layer; brak = same A (r12)")
199
+ p.add_argument("--gdn-head-dim", type=int, default=64, help="r13: head_dim GatedDeltaNet2")
200
+ p.add_argument("--attn-bias", type=int, choices=(0, 1), default=1, help="r13: bias w qkv/proj uwagi (1 = r12)")
201
+ p.add_argument("--rope-style", choices=("interleaved", "half"), default="interleaved",
202
+ help="r13: interleaved (r12) | half (rotate-half)")
203
+ p.add_argument("--data-offset-steps", type=int, default=0,
204
+ help="r10 (Wzrost, tylko --sampler epoch): dane od kroku N strumienia, np. dalszy ciag po ckpt "
205
+ "mniejszego modelu (ten sam batch i seed), bez powtarzania widzianych okien")
206
+ p.add_argument("--data-dir-2", default=None,
207
+ help="r10 (Wzrost, tylko --sampler epoch): od kroku strumienia --switch-step okna z DATA_DIR_2/train.bin "
208
+ "(np. swieze dokumenty rozlaczne z base), wlasna permutacja epokowa liczona od --switch-step")
209
+ p.add_argument("--switch-step", type=int, default=0, help="krok strumienia przejscia na --data-dir-2")
210
+ p.add_argument("--data-dirs-next", default=None,
211
+ help="r24: kolejne strumienie po --data-dir-2, lista katalogow po przecinku (kazdy z train.bin)")
212
+ p.add_argument("--switch-steps-next", default=None,
213
+ help="r24: kroki strumienia przejscia na kolejne --data-dirs-next, rosnace, > --switch-step")
214
+ p.add_argument("--batch-sha", default=None,
215
+ help="r10 (Wzrost): bez treningu; sha256 paczek (x+y) w podanych krokach biegu, np. 0,24413,24414, "
216
+ "z tego samego Prefetchera co trening (kontrola strumienia), potem koniec")
217
+ p.add_argument("--global-batch-sha", type=int, default=None, metavar="ZERO_STEP",
218
+ help="po prawdziwej aktualizacji ZERO_STEP: v1 sha256 wszystkich 256 wierszy x/y; "
219
+ "opcjonalny dowod niezalezny od liczby rankow, nie tryb --batch-sha")
220
+ p.add_argument("--synthetic", action="store_true", help="smoke throughput na losowym uint8 (bez danych)")
221
+ p.add_argument("--vocab", type=int, default=256)
222
+ p.add_argument("--dtype", default="uint8", help="bin dtype: uint8 (byte) | uint16 (BPE)")
223
+ p.add_argument("--optimizer", choices=["adamw", "muon"], default="adamw",
224
+ help="adamw (default, backward-compat) | muon (Newton-Schulz ortho dla 2D-weights + AdamW dla reszty)")
225
+ p.add_argument("--muon-lr", type=float, default=0.02,
226
+ 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")
227
+ p.add_argument("--norm", choices=["layernorm", "rmsnorm"], default="layernorm",
228
+ help="layernorm (default, backward-compat) | rmsnorm (Qwen3-style, fp32-compute)")
229
+ p.add_argument("--norm-eps", type=float, default=1e-6)
230
+ p.add_argument("--pos", choices=["learned", "rope"], default="learned",
231
+ help="learned (default) | rope (parameter-free RoPE na q,k; usuwa learned pos-embedding)")
232
+ p.add_argument("--rope-theta", type=float, default=100000.0,
233
+ help="RoPE theta; top-3-board (JugnuLM/GPT-X2) uzywaja 100000 (nie-default-10K)")
234
+ p.add_argument("--ffn", choices=["gelu", "swiglu", "relu2"], default="gelu",
235
+ help="gelu (default 4x MLP) | swiglu (Qwen3 gated-MLP, hidden=--ffn-mult*d) | relu2 (r18: down(relu(up)^2), bez biasu)")
236
+ p.add_argument("--ffn-mult", type=float, default=2.667,
237
+ help="mnoznik hidden dla swiglu (~param-parity z 4x-gelu przy 8/3)")
238
+ p.add_argument("--value-residual", action="store_true",
239
+ help="ResFormer value-residuals: v_l += lambda_l*v0 (lambda init 0), ARC-targeted")
240
+ p.add_argument("--qk-norm", action="store_true",
241
+ help="Qwen3 QK-Norm: RMSNorm per-head na Q,K przed-attention (stabilnosc z Muon/high-LR)")
242
+ p.add_argument("--events-jsonl", default=None,
243
+ help="jesli podane: emituj events.jsonl (fabryka-track sidecar-format: update/evaluation/checkpoint/end)")
244
+ p.add_argument("--compile", action="store_true",
245
+ help="torch.compile model (2-3x throughput; state_dict zapisywany bez _orig_mod prefix via raw_model)")
246
+ p.add_argument("--logit-cap", type=float, default=0.0,
247
+ help="Gemma-style logit soft-cap: logits=cap*tanh(logits/cap); 0=off (prob. 30)")
248
+ p.add_argument("--z-loss", type=float, default=0.0,
249
+ help="z-loss coef: +coef*mean(logsumexp(logits)^2), stabilizuje logity; 0=off (prob. 1e-4)")
250
+ p.add_argument("--act-ckpt", action="store_true",
251
+ help="r11: przeliczanie aktywacji po bloku w treningu (pamiec ~ wejscia blokow zamiast wszystkich aktywacji)")
252
+ p.add_argument("--loss-chunk", type=int, default=0,
253
+ help="r11: strata kawalkami po N pozycji, bez pelnych logitow [B,T,V]; 0 = off (r10)")
254
+ p.add_argument("--loop", type=int, default=1,
255
+ help="r12: ile razy przejsc przez n_layer wspoldzielonych blokow (1 = bez wspoldzielenia, jak r11)")
256
+ p.add_argument("--queue", default=None,
257
+ help="r7: manifest kolejki shardow (JSON: steps_per_shard, shards{id:{lo,hi,sha256,source}}, rotations[[id..]..]). "
258
+ "Slot j = kroki [j*S,(j+1)*S) na shardzie schedule[j]; schedule = rotacje po kolei. Bez flagi = r6 bit w bit.")
259
+ p.add_argument("--queue-one-slot", action="store_true",
260
+ help="r7: trenuj tylko do konca biezacego slotu i zapisz ckpt (krok nadzorcy kolejki decyduje o nastepnym)")
261
+ p.add_argument("--kd-teacher", default=None, help="r14: ckpt nauczyciela KD (r12/r13); bez = r13 bit w bit")
262
+ p.add_argument("--kd-teacher-sha", default=None, help="r14: pelne sha256 ckpt nauczyciela (wymagane z --kd-teacher)")
263
+ 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")
264
+ p.add_argument("--kd-temp", type=float, default=1.0, help="r14: temperatura KD")
265
+ p.add_argument("--emb-dim", type=int, default=0, help="r15: faktoryzacja osadzen V x E + E x d (0 = wylaczone, r14)")
266
+ p.add_argument("--grad-accum", type=int, default=1, help="r16: mikrobatche na krok (1 = wylaczone, r15)")
267
+ p.add_argument("--muon-scale", choices=["sqrt_ratio", "match_rms_adamw"], default="sqrt_ratio",
268
+ help="r17: skala kroku Muon (sqrt_ratio = jak dotad; match_rms_adamw = torch.optim.Muon)")
269
+ p.add_argument("--muon-wd", type=float, default=0.0, help="r17: decoupled WD na parametrach Muon (0 = jak dotad)")
270
+ p.add_argument("--tf32", type=int, choices=[0, 1], default=1, help="r17: 0 = TF32 wylaczone (jak Qielbas M2)")
271
+ p.add_argument("--xsa", type=int, choices=[0, 1], default=0, help="r18: 1 = z wyjscia glowy usuwana skladowa wzdluz wlasnego v")
272
+ p.add_argument("--rope-dims", type=int, default=0, help="r18: RoPE na pierwszych N wymiarach glowy (0 = wszystkie, r17)")
273
+ p.add_argument("--zero-init", type=int, choices=[0, 1], default=0, help="r18: 1 = zerowe wagi proj uwagi i wyjscia MLP")
274
+ p.add_argument("--arch", choices=["gpt", "tinylm"], default="gpt", help="r19: tinylm = model z pliku Tiny Chick (--tinylm-file)")
275
+ p.add_argument("--tinylm-file", default=None, help="r19: tinylm_arch.py z kurcontko/Tiny-Chick-10M@60d32c4d (sha 4876824d...)")
276
+ p.add_argument("--kv-heads", type=int, default=0,
277
+ help="r19 tinylm / r23 gpt: liczba glow KV (GQA, dzielnik --n-head); 0 = --n-head (MHA)")
278
+ p.add_argument("--train-only", default=None,
279
+ help="r21: regex nazw uczonych parametrow; reszta requires_grad False i poza optymalizatorem; tylko z --init-from")
280
+ p.add_argument("--imha-t", type=int, default=0,
281
+ help="r22: IMHA z T iteracjami w blokach uwagi (0 = zwykla uwaga, r21 bit w bit); artykul: T = 2")
282
+ return p.parse_args()
283
+
284
+
285
+ # ---- Muon (Keller Jordan) --------------------------------------------------
286
+ # Ref: https://github.com/KellerJordan/Muon (modded-nanogpt). Muon = momentum
287
+ # SGD, ale update ortogonalizowany przez ~5 krokow iteracji Newtona-Schulza
288
+ # (przyblizona ortogonalizacja macierzy gradientu). Stosowany TYLKO do
289
+ # macierzowych ukrytych wag (ndim>=2: qkv/proj/mlp). Embeddingi (tok/pos), head
290
+ # (tied), LayerNorm-gains i biasy ida do zwyklego AdamW.
291
+ def zeropower_via_newtonschulz5(G, steps=5):
292
+ """Ortogonalizacja macierzy G przez quintic Newton-Schulz (bf16). Zwraca
293
+ macierz ~ U V^T z SVD(G)=U S V^T. Wspolczynniki (a,b,c) z impl. Kellera."""
294
+ assert G.ndim == 2
295
+ a, b, c = (3.4445, -4.7750, 2.0315)
296
+ X = G.bfloat16()
297
+ transposed = G.size(0) > G.size(1)
298
+ if transposed:
299
+ X = X.T
300
+ X = X / (X.norm() + 1e-7)
301
+ for _ in range(steps):
302
+ A = X @ X.T
303
+ B = b * A + c * (A @ A)
304
+ X = a * X + B @ X
305
+ if transposed:
306
+ X = X.T
307
+ return X
308
+
309
+
310
+ class Muon(torch.optim.Optimizer):
311
+ """Momentum-SGD z ortogonalizowanym update. weight_decay domyslnie 0 (Muon-params
312
+ czysto; WD trzymamy na AdamW-aux). lr_mult pozwala petli lr-schedule skalowac
313
+ Muon proporcjonalnie do AdamW."""
314
+ def __init__(self, params, lr=0.02, lr_mult=1.0, momentum=0.95, nesterov=True,
315
+ ns_steps=5, weight_decay=0.0, scale_mode="sqrt_ratio"):
316
+ defaults = dict(lr=lr, lr_mult=lr_mult, momentum=momentum, nesterov=nesterov,
317
+ ns_steps=ns_steps, weight_decay=weight_decay, scale_mode=scale_mode)
318
+ super().__init__(params, defaults)
319
+
320
+ @torch.no_grad()
321
+ def step(self, closure=None):
322
+ loss = None
323
+ if closure is not None:
324
+ with torch.enable_grad():
325
+ loss = closure()
326
+ for group in self.param_groups:
327
+ lr = group["lr"]; momentum = group["momentum"]; wd = group["weight_decay"]
328
+ for p in group["params"]:
329
+ g = p.grad
330
+ if g is None:
331
+ continue
332
+ if g.ndim > 2:
333
+ g = g.reshape(g.size(0), -1)
334
+ state = self.state[p]
335
+ if "momentum_buffer" not in state:
336
+ state["momentum_buffer"] = torch.zeros_like(g)
337
+ buf = state["momentum_buffer"]
338
+ buf.mul_(momentum).add_(g)
339
+ g = g.add(buf, alpha=momentum) if group["nesterov"] else buf
340
+ u = zeropower_via_newtonschulz5(g, steps=group["ns_steps"])
341
+ if wd != 0:
342
+ p.mul_(1 - lr * wd)
343
+ # scale ~ sqrt(fan_out/fan_in): zrownuje RMS update niezaleznie od ksztaltu
344
+ # r17 match_rms_adamw: 0.2*sqrt(max(A,B)) jak torch.optim.Muon (RMS kroku ~ AdamW)
345
+ if group.get("scale_mode", "sqrt_ratio") == "match_rms_adamw":
346
+ scale = 0.2 * max(g.size(0), g.size(1)) ** 0.5
347
+ else:
348
+ scale = max(1.0, p.size(0) / p.size(1)) ** 0.5
349
+ p.add_(u.reshape(p.shape).to(p.dtype), alpha=-lr * scale)
350
+ return loss
351
+
352
+
353
+ class MuonWithAuxAdam:
354
+ """Kontener: Muon dla macierzowych ukrytych wag + AdamW dla reszty. Wystawia
355
+ param_groups/step/zero_grad/state_dict tak, by petla treningowa dzialala bez zmian."""
356
+ def __init__(self, muon, adamw):
357
+ self.muon = muon
358
+ self.adamw = adamw
359
+
360
+ @property
361
+ def param_groups(self):
362
+ return self.muon.param_groups + self.adamw.param_groups
363
+
364
+ @property
365
+ def state(self):
366
+ return {**self.muon.state, **self.adamw.state}
367
+
368
+ def step(self, closure=None):
369
+ self.muon.step()
370
+ self.adamw.step()
371
+
372
+ def zero_grad(self, set_to_none=True):
373
+ self.muon.zero_grad(set_to_none=set_to_none)
374
+ self.adamw.zero_grad(set_to_none=set_to_none)
375
+
376
+ def state_dict(self):
377
+ return {"muon": self.muon.state_dict(), "adamw": self.adamw.state_dict()}
378
+
379
+ def load_state_dict(self, sd):
380
+ self.muon.load_state_dict(sd["muon"])
381
+ self.adamw.load_state_dict(sd["adamw"])
382
+
383
+
384
+ def build_optimizer(a, model):
385
+ """--optimizer adamw -> DOKLADNIE poprzedni AdamW (backward-compat).
386
+ --optimizer muon -> Muon(2D-hidden) + AdamW(embeddingi/head/norm/bias)."""
387
+ if a.optimizer == "adamw":
388
+ return torch.optim.AdamW([p for p in model.parameters() if p.requires_grad], lr=a.lr, weight_decay=a.wd,
389
+ betas=(0.9, 0.95)) # r21: bez zamrozonych (--train-only)
390
+ muon_params, adamw_params, seen = [], [], set()
391
+ for name, p in model.named_parameters():
392
+ if not p.requires_grad or id(p) in seen:
393
+ continue
394
+ seen.add(id(p))
395
+ is_embed_or_head = name.startswith(("tok.", "pos.", "head.", "tl.emb.")) # r19: osadzenia TinyLM (tied head) jak tok
396
+ tl_gain = name.startswith("tl.") and name.endswith(".attn.gain") # r19: skalar QK-norm na glowe [H,1,1] -> AdamW
397
+ # r13: w blokach G do Muon tylko kwadratowe projekcje q/k/v/o; konwolucje (3D), bramki niskiego rzedu,
398
+ # b/w_proj, A_log, dt_bias, normy -> AdamW. Bez blokow G warunek jest pusty (r12 bit w bit).
399
+ gdn_aux = ".gdn." in name and not name.endswith(("q_proj.weight", "k_proj.weight", "v_proj.weight", "o_proj.weight"))
400
+ if p.ndim >= 2 and not is_embed_or_head and not gdn_aux and not tl_gain:
401
+ muon_params.append(p)
402
+ else:
403
+ adamw_params.append(p)
404
+ lr_mult = a.muon_lr / a.lr if a.lr > 0 else 1.0
405
+ muon = Muon(muon_params, lr=a.muon_lr, lr_mult=lr_mult, weight_decay=getattr(a, "muon_wd", 0.0),
406
+ scale_mode=getattr(a, "muon_scale", "sqrt_ratio"))
407
+ adamw = torch.optim.AdamW(adamw_params, lr=a.lr, weight_decay=a.wd, betas=(0.9, 0.95))
408
+ return MuonWithAuxAdam(muon, adamw)
409
+
410
+
411
+ class RMSNorm(nn.Module):
412
+ """Qwen3-style RMSNorm (fp32-compute dla stabilnosci). 1D weight -> AdamW w split-Muon."""
413
+ def __init__(self, d, eps=1e-6):
414
+ super().__init__()
415
+ self.weight = nn.Parameter(torch.ones(d))
416
+ self.eps = eps
417
+
418
+ def forward(self, x):
419
+ return x * torch.rsqrt(x.float().pow(2).mean(-1, keepdim=True) + self.eps).to(x.dtype) * self.weight
420
+
421
+
422
+ def make_norm(d, cfg):
423
+ return RMSNorm(d, cfg.norm_eps) if cfg.norm == "rmsnorm" else nn.LayerNorm(d)
424
+
425
+
426
+ def apply_rope(x, base=100000.0, style="interleaved", nrot=0):
427
+ """Parameter-free RoPE na [B,H,T,D] (interleaved-conv, port z qwen_model.py). Train==eval
428
+ MUSZA uzywac tej samej konwencji (self-contained eval -> spojne).
429
+ r13: style="half" = rotate-half (pary i, i+D/2), jak Qielbas M2; "interleaved" = r12 bez zmian.
430
+ r18: nrot (0 < nrot < D) = obrot tylko pierwszych nrot wymiarow (czestotliwosci z nrot), reszta bez zmian; 0 = calosc."""
431
+ if 0 < nrot < x.size(-1):
432
+ return torch.cat([apply_rope(x[..., :nrot], base, style), x[..., nrot:]], dim=-1)
433
+ _, _, T, dim = x.shape
434
+ pos = torch.arange(T, device=x.device, dtype=torch.float32)
435
+ freq = 1.0 / (base ** (torch.arange(0, dim, 2, device=x.device, dtype=torch.float32) / dim))
436
+ ang = torch.outer(pos, freq)
437
+ if style == "half":
438
+ ang = torch.cat([ang, ang], dim=-1)
439
+ cos, sin = ang.cos().to(x.dtype)[None, None], ang.sin().to(x.dtype)[None, None]
440
+ a, b = x.chunk(2, dim=-1)
441
+ return x * cos + torch.cat([-b, a], dim=-1) * sin
442
+ cos, sin = ang.cos().to(x.dtype)[None, None], ang.sin().to(x.dtype)[None, None]
443
+ even, odd = x[..., ::2], x[..., 1::2]
444
+ return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2)
445
+
446
+
447
+ class SwiGLU(nn.Module):
448
+ """Qwen3 gated-MLP: down(silu(gate(x))*up(x)). 3x 2D bez-bias -> wszystkie do Muon."""
449
+ def __init__(self, d, hidden):
450
+ super().__init__()
451
+ self.gate = nn.Linear(d, hidden, bias=False)
452
+ self.up = nn.Linear(d, hidden, bias=False)
453
+ self.down = nn.Linear(hidden, d, bias=False)
454
+
455
+ def forward(self, x):
456
+ return self.down(F.silu(self.gate(x)) * self.up(x))
457
+
458
+
459
+ class ReLU2(nn.Module):
460
+ """r18 (Tiny Chick): down(relu(up(x))^2), 2x 2D bez biasu -> Muon."""
461
+ def __init__(self, d, hidden):
462
+ super().__init__()
463
+ self.up = nn.Linear(d, hidden, bias=False)
464
+ self.down = nn.Linear(hidden, d, bias=False)
465
+
466
+ def forward(self, x):
467
+ return self.down(F.relu(self.up(x)).square())
468
+
469
+
470
+ class Block(nn.Module):
471
+ def __init__(self, d, nh, block, cfg, is_first=False, mixer="A", layer_idx=0):
472
+ super().__init__()
473
+ self.ln1 = make_norm(d, cfg)
474
+ self.ln2 = make_norm(d, cfg)
475
+ self.mixer = mixer
476
+ if mixer == "G": # r13: GatedDeltaNet2 w miejsce uwagi (fla 0.5.2); inicjalizacja fla, nie _init GPT
477
+ from fla.layers.gdn2 import GatedDeltaNet2
478
+ hd = getattr(cfg, "gdn_head_dim", 64)
479
+ if d % hd:
480
+ raise SystemExit(f"STOP: n_embd {d} nie dzieli sie przez --gdn-head-dim {hd}")
481
+ self.gdn = GatedDeltaNet2(hidden_size=d, head_dim=hd, num_heads=d // hd, expand_v=1.0,
482
+ use_short_conv=True, mode="chunk", layer_idx=layer_idx)
483
+ for sm in self.gdn.modules():
484
+ sm._r13_fla_init = True
485
+ else:
486
+ bias = bool(getattr(cfg, "attn_bias", 1))
487
+ kvh = int(getattr(cfg, "kv_heads", 0) or 0) # r23: GQA (0 albo n_head = MHA, qkv d -> 3d jak r22)
488
+ self.nkv = kvh if 0 < kvh < nh else nh
489
+ self.kvd = self.nkv * (d // nh)
490
+ self.qkv = nn.Linear(d, d + 2 * self.kvd, bias=bias)
491
+ self.proj = nn.Linear(d, d, bias=bias)
492
+ if cfg.ffn == "swiglu":
493
+ self.mlp = SwiGLU(d, int(round(cfg.ffn_mult * d)))
494
+ elif cfg.ffn == "relu2":
495
+ self.mlp = ReLU2(d, int(round(cfg.ffn_mult * d)))
496
+ else:
497
+ self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d))
498
+ self.nh = nh
499
+ self.d = d
500
+ self.cfg = cfg
501
+ self.is_first = is_first
502
+ if mixer == "A" and cfg.value_residual and not is_first:
503
+ self.vr_lambda = nn.Parameter(torch.zeros(1))
504
+ if mixer == "A" and cfg.qk_norm:
505
+ hd = d // nh
506
+ self.q_norm = RMSNorm(hd, cfg.norm_eps)
507
+ self.k_norm = RMSNorm(hd, cfg.norm_eps)
508
+ self.imha_t = int(getattr(cfg, "imha_t", 0) or 0) if mixer == "A" else 0
509
+ if self.imha_t and self.nkv != nh:
510
+ raise SystemExit("STOP --imha-t: niezgodne z --kv-heads (GQA)")
511
+ if self.imha_t: # r22: bramka i kandydat IMHA (W_a, W_f in R^{2d x d}) + temperatura per warstwa
512
+ self.imha_a = nn.Linear(2 * d, d, bias=False)
513
+ self.imha_f = nn.Linear(2 * d, d, bias=False)
514
+ self.imha_log_tau = nn.Parameter(torch.zeros(1))
515
+
516
+ def _qkv(self, h, v0, set_v0):
517
+ B, T, D = h.size()
518
+ q, k, v = self.qkv(h).split(self.d, dim=2)
519
+ hd = D // self.nh
520
+ q = q.view(B, T, self.nh, hd).transpose(1, 2)
521
+ k = k.view(B, T, self.nh, hd).transpose(1, 2)
522
+ v = v.view(B, T, self.nh, hd).transpose(1, 2)
523
+ if self.cfg.qk_norm:
524
+ q = self.q_norm(q)
525
+ k = self.k_norm(k)
526
+ if self.cfg.pos == "rope":
527
+ style = getattr(self.cfg, "rope_style", "interleaved")
528
+ nrot = int(getattr(self.cfg, "rope_dims", 0) or 0)
529
+ q = apply_rope(q, self.cfg.rope_theta, style, nrot)
530
+ k = apply_rope(k, self.cfg.rope_theta, style, nrot)
531
+ if self.cfg.value_residual:
532
+ if self.is_first: # blok 0 nie ma vr_lambda (r21): v0 = v z iteracji t = 0, w t >= 1 v bez mieszania
533
+ if set_v0:
534
+ v0 = v
535
+ else:
536
+ v = v + self.vr_lambda * v0
537
+ return q, k, v, v0
538
+
539
+ def _imha(self, x, v0):
540
+ """r22: IMHA (Algorytm 1); zwraca (h_T - h_0, v0). Wszystkie sumy po k <= i (prefiks), wiec przyczynowo."""
541
+ B, T, D = x.size()
542
+ hd = D // self.nh
543
+ h0 = self.ln1(x)
544
+ h = h0
545
+ pos = torch.arange(T, device=x.device)
546
+ cnt = (pos + 1).to(torch.float32) # i + 1
547
+ logn = torch.log(cnt).clamp_min(1e-12) # log(i+1); i = 0 -> H_0 = 0 nizej
548
+ mask = torch.ones(T, T, device=x.device, dtype=torch.bool).tril()
549
+ plain = bool(getattr(self.cfg, "imha_plain", False))
550
+ tau = self.imha_log_tau.exp()
551
+ for t in range(self.imha_t):
552
+ q, k, v, v0 = self._qkv(h, v0, set_v0=(t == 0))
553
+ s = (q.float() @ k.float().transpose(-2, -1)) * (hd ** -0.5)
554
+ A = torch.softmax(s.masked_fill(~mask, float("-inf")), dim=-1) # [B,H,T,T] fp32
555
+ z = (A.to(v.dtype) @ v) # [B,H,T,hd]
556
+ if not plain:
557
+ ent = -(A * torch.log(A.clamp_min(1e-30))).sum(-1) # 0 log 0 = 0 (A = 0 -> 0 * log(1e-30) = 0)
558
+ hn = torch.where(pos == 0, torch.zeros_like(ent), ent / logn)
559
+ e = torch.exp(-hn / tau)
560
+ w = e / (e.cumsum(-1) / cnt) # srednia prefiksu, E[w] ~ 1
561
+ z = z * w.unsqueeze(-1).to(z.dtype)
562
+ z = self.proj(z.transpose(1, 2).contiguous().view(B, T, D))
563
+ if plain:
564
+ h = h + z
565
+ continue
566
+ g = z.cumsum(1) / cnt.view(1, T, 1).to(z.dtype)
567
+ u = torch.cat([z, g], dim=-1)
568
+ h = h + torch.sigmoid(self.imha_a(u)) * F.relu(self.imha_f(u))
569
+ return h - h0, v0
570
+
571
+ def forward(self, x, v0=None):
572
+ B, T, D = x.size()
573
+ if self.imha_t:
574
+ dx, v0 = self._imha(x, v0)
575
+ x = x + dx
576
+ x = x + self.mlp(self.ln2(x))
577
+ return x, v0
578
+ h = self.ln1(x)
579
+ if self.mixer == "G":
580
+ x = x + self.gdn(h, use_cache=False)[0]
581
+ x = x + self.mlp(self.ln2(x))
582
+ return x, v0
583
+ q, k, v = self.qkv(h).split([self.d, self.kvd, self.kvd], dim=2)
584
+ hd = D // self.nh
585
+ q = q.view(B, T, self.nh, hd).transpose(1, 2)
586
+ k = k.view(B, T, self.nkv, hd).transpose(1, 2)
587
+ v = v.view(B, T, self.nkv, hd).transpose(1, 2)
588
+ if self.cfg.qk_norm:
589
+ q = self.q_norm(q)
590
+ k = self.k_norm(k)
591
+ if self.cfg.pos == "rope":
592
+ style = getattr(self.cfg, "rope_style", "interleaved")
593
+ nrot = int(getattr(self.cfg, "rope_dims", 0) or 0)
594
+ q = apply_rope(q, self.cfg.rope_theta, style, nrot)
595
+ k = apply_rope(k, self.cfg.rope_theta, style, nrot)
596
+ if self.cfg.value_residual:
597
+ if self.is_first:
598
+ v0 = v
599
+ else:
600
+ v = v + self.vr_lambda * v0
601
+ if self.nkv != self.nh: # r23: GQA, glowy KV powielone na grupy glow q (po RoPE / QK-norm / value-residual)
602
+ k = k.repeat_interleave(self.nh // self.nkv, dim=1)
603
+ v = v.repeat_interleave(self.nh // self.nkv, dim=1)
604
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
605
+ if getattr(self.cfg, "xsa", 0): # r18: bez skladowej wzdluz wlasnego v tokenu (per glowa)
606
+ vn = F.normalize(v, dim=-1)
607
+ y = y - (y * vn).sum(-1, keepdim=True) * vn
608
+ y = y.transpose(1, 2).contiguous().view(B, T, D)
609
+ x = x + self.proj(y)
610
+ x = x + self.mlp(self.ln2(x))
611
+ return x, v0
612
+
613
+
614
+ TINYLM_SHA = "4876824de8c697a657e5f58351c216ffb84cb90d6a2a53ecd94933f7ddcc4cfa" # r19: Tiny-Chick-10M@60d32c4d tinylm_arch.py
615
+
616
+
617
+ def load_tinylm_module(path):
618
+ """r19: oryginalny tinylm_arch.py (Apache-2.0, kurcontko/Tiny-Chick-10M@60d32c4d) z pliku przypietego sha.
619
+ Bez --tinylm-file: tinylm_arch.py obok tego trenera (np. eval, ktory odtwarza model z configu ckpt)."""
620
+ import importlib.util
621
+ path = path or os.path.join(os.path.dirname(os.path.abspath(__file__)), "tinylm_arch.py")
622
+ if not os.path.exists(path):
623
+ raise SystemExit(f"STOP --arch tinylm: brak {path}")
624
+ with open(path, "rb") as f:
625
+ h = hashlib.sha256(f.read()).hexdigest()
626
+ if h != TINYLM_SHA:
627
+ raise SystemExit(f"STOP --arch tinylm: sha {path} = {h[:8]} != {TINYLM_SHA[:8]}")
628
+ spec = importlib.util.spec_from_file_location("tinylm_arch_tc", path)
629
+ mod = importlib.util.module_from_spec(spec)
630
+ spec.loader.exec_module(mod)
631
+ return mod
632
+
633
+
634
+ class GPT(nn.Module):
635
+ def __init__(self, vocab, n_layer, n_embd, n_head, block, cfg):
636
+ super().__init__()
637
+ self.cfg = cfg
638
+ if getattr(cfg, "arch", "gpt") == "tinylm": # r19: caly model = TinyLM z pliku Tiny Chick (klucze "tl.*")
639
+ tlm = load_tinylm_module(getattr(cfg, "tinylm_file", None))
640
+ self.tl = tlm.TinyLM(tlm.TinyConfig(
641
+ vocab_size=vocab, d=n_embd, layers=n_layer, heads=n_head,
642
+ kv_heads=int(getattr(cfg, "kv_heads", 0) or 0) or n_head, mlp_mult=cfg.ffn_mult, act=cfg.ffn,
643
+ qk_norm=bool(cfg.qk_norm), qk_gain=1.0, rope_dims=int(getattr(cfg, "rope_dims", 0) or 0),
644
+ xsa=bool(getattr(cfg, "xsa", 0)), zero_init=bool(getattr(cfg, "zero_init", 0)),
645
+ max_position_embeddings=block, rope_theta=cfg.rope_theta, eps=cfg.norm_eps))
646
+ self.block = block
647
+ return
648
+ E = int(getattr(cfg, "emb_dim", 0) or 0) # r15: faktoryzowane osadzenia (0 = jak r14)
649
+ self.emb_dim = E
650
+ self.tok = nn.Embedding(vocab, E if E else n_embd)
651
+ self.use_rope = cfg.pos == "rope"
652
+ if not self.use_rope:
653
+ self.pos = nn.Embedding(block, n_embd)
654
+ layout = getattr(cfg, "mixer_layout", None) or "A" * n_layer
655
+ if len(layout) != n_layer or set(layout) - {"A", "G"}:
656
+ raise SystemExit(f"STOP: --mixer-layout {layout!r}: dlugosc {n_layer}, tylko litery A/G")
657
+ nrot = int(getattr(cfg, "rope_dims", 0) or 0)
658
+ kvh = int(getattr(cfg, "kv_heads", 0) or 0) # r23: GQA dla blokow A
659
+ if kvh < 0 or kvh > n_head or (kvh and n_head % kvh):
660
+ raise SystemExit(f"STOP: --kv-heads {kvh}: 0 (MHA) albo dzielnik n_head {n_head}")
661
+ if nrot and (nrot % 2 or not 0 < nrot < n_embd // n_head or cfg.pos != "rope"):
662
+ raise SystemExit(f"STOP: --rope-dims {nrot}: parzyste, 0 < N < head_dim {n_embd // n_head}, tylko z --pos rope")
663
+ first_a = layout.find("A") # value-residual: v0 z pierwszego bloku A (r12: blok 0)
664
+ self.blocks = nn.ModuleList([Block(n_embd, n_head, block, cfg, is_first=(i == first_a), mixer=layout[i], layer_idx=i)
665
+ for i in range(n_layer)])
666
+ self.lnf = make_norm(n_embd, cfg)
667
+ self.head = nn.Linear(E if E else n_embd, vocab, bias=False)
668
+ self.head.weight = self.tok.weight # tie
669
+ if E:
670
+ self.emb_up = nn.Linear(E, n_embd, bias=False)
671
+ self.emb_down = nn.Linear(n_embd, E, bias=False)
672
+ self.block = block
673
+ self.apply(self._init)
674
+ if getattr(cfg, "zero_init", 0): # r18: wyjscia galezi rezydualnych od zera (bloki G: proj fla bez zmian)
675
+ for b in self.blocks:
676
+ if b.mixer == "A":
677
+ nn.init.zeros_(b.proj.weight)
678
+ nn.init.zeros_(b.mlp.down.weight if hasattr(b.mlp, "down") else b.mlp[2].weight)
679
+ if E:
680
+ nn.init.normal_(self.emb_up.weight, 0.0, E ** -0.5)
681
+ nn.init.normal_(self.emb_down.weight, 0.0, n_embd ** -0.5)
682
+
683
+ def _head(self, h):
684
+ return self.head(self.emb_down(h)) if self.emb_dim else self.head(h)
685
+
686
+ def _init(self, m):
687
+ if getattr(m, "_r13_fla_init", False): # r13: moduly GatedDeltaNet2 zostaja z inicjalizacja fla
688
+ return
689
+ if isinstance(m, nn.Linear):
690
+ nn.init.normal_(m.weight, 0.0, 0.02)
691
+ if m.bias is not None:
692
+ nn.init.zeros_(m.bias)
693
+ elif isinstance(m, nn.Embedding):
694
+ nn.init.normal_(m.weight, 0.0, 0.02)
695
+
696
+ def _loss_sum(self, h, y, t=None):
697
+ """r11: suma CE (+ z-loss) dla kawalka pozycji; h po lnf [B,t,D], y [B,t]. Ta sama arytmetyka co sciezka r10.
698
+ r14: t = logity nauczyciela tego kawalka [B,t,V] -> (suma straty mieszanej KD, suma samej CE)."""
699
+ logits = self._head(h)
700
+ cap = getattr(self.cfg, "logit_cap", 0.0)
701
+ if cap and cap > 0:
702
+ logits = cap * torch.tanh(logits / cap)
703
+ flat = logits.view(-1, logits.size(-1))
704
+ s = F.cross_entropy(flat, y.reshape(-1), reduction="sum")
705
+ zc = getattr(self.cfg, "z_loss", 0.0)
706
+ if zc and zc > 0:
707
+ lse = torch.logsumexp(flat, dim=-1)
708
+ s = s + zc * (lse * lse).sum()
709
+ if t is None:
710
+ return s
711
+ return self._kd_mix(s, flat, t, 1), s
712
+
713
+ def _kd_mix(self, ce, flat, t, div):
714
+ """r14: (1-A)*ce + A*T^2*KL(p_naucz || p_ucz); KL sumowane po pozycjach kawalka / div (1 = suma, N = srednia).
715
+ fp32, pelny vocab; flat = logity ucznia [N,V] (po logit_cap), t = logity nauczyciela [...,V]."""
716
+ A, T = self.cfg.kd_alpha, self.cfg.kd_temp
717
+ ls = F.log_softmax(flat.float() / T, dim=-1)
718
+ lt = F.log_softmax(t.reshape(-1, t.size(-1)).float() / T, dim=-1)
719
+ kl = (lt.exp() * (lt - ls)).sum() / div
720
+ return (1.0 - A) * ce + (A * T * T) * kl
721
+
722
+ def forward(self, idx, targets=None, teacher_logits=None):
723
+ if getattr(self.cfg, "arch", "gpt") == "tinylm": # r19: ich forward (logity z tied emb), CE w fp32 jak u nich
724
+ logits = self.tl(idx).logits
725
+ if targets is None:
726
+ return logits, None
727
+ return logits, F.cross_entropy(logits.reshape(-1, logits.size(-1)).float(), targets.reshape(-1))
728
+ B, T = idx.size()
729
+ x = self.tok(idx)
730
+ if self.emb_dim:
731
+ x = self.emb_up(x)
732
+ if not self.use_rope:
733
+ pos = torch.arange(T, device=idx.device)
734
+ x = x + self.pos(pos)[None]
735
+ v0 = None
736
+ act_ckpt = self.training and getattr(self.cfg, "act_ckpt", False)
737
+ for _ in range(getattr(self.cfg, "loop", 1)):
738
+ for b in self.blocks:
739
+ x, v0 = checkpoint(b, x, v0, use_reentrant=False) if act_ckpt else b(x, v0)
740
+ h = self.lnf(x)
741
+ n = getattr(self.cfg, "loss_chunk", 0)
742
+ if targets is not None and n and n < T:
743
+ if teacher_logits is None:
744
+ s = sum(checkpoint(self._loss_sum, h[:, i:i + n], targets[:, i:i + n], use_reentrant=False)
745
+ for i in range(0, T, n))
746
+ return None, s / (B * T)
747
+ parts = [checkpoint(self._loss_sum, h[:, i:i + n], targets[:, i:i + n], teacher_logits[:, i:i + n],
748
+ use_reentrant=False) for i in range(0, T, n)]
749
+ return None, sum(p[0] for p in parts) / (B * T), sum(p[1] for p in parts).detach() / (B * T)
750
+ logits = self._head(h)
751
+ cap = getattr(self.cfg, "logit_cap", 0.0)
752
+ if cap and cap > 0:
753
+ logits = cap * torch.tanh(logits / cap)
754
+ loss = None
755
+ if targets is not None:
756
+ flat = logits.view(-1, logits.size(-1))
757
+ loss = F.cross_entropy(flat, targets.view(-1))
758
+ zc = getattr(self.cfg, "z_loss", 0.0)
759
+ if zc and zc > 0:
760
+ lse = torch.logsumexp(flat, dim=-1)
761
+ loss = loss + zc * (lse * lse).mean()
762
+ if teacher_logits is not None:
763
+ return logits, self._kd_mix(loss, flat, teacher_logits, flat.size(0)), loss.detach()
764
+ return logits, loss
765
+
766
+
767
+ def _global_batch_row_digests(x, y):
768
+ """Hash actual consumed rows, x then y, each token signed little-endian int64."""
769
+ if x.ndim != 2 or y.shape != x.shape:
770
+ raise ValueError("global batch proof requires matching two-dimensional x/y")
771
+ xc = x.detach().cpu().numpy().astype("<i8", copy=False)
772
+ yc = y.detach().cpu().numpy().astype("<i8", copy=False)
773
+ payload = bytearray()
774
+ for xr, yr in zip(xc, yc):
775
+ h = hashlib.sha256(xr.tobytes())
776
+ h.update(yr.tobytes())
777
+ payload.extend(h.digest())
778
+ return payload
779
+
780
+
781
+ def _global_batch_sha(x, y, block, world, rank):
782
+ """Only called for the selected real update, after opt.step().
783
+
784
+ EpochSampler draws global positions 0..255. _make_batch_cpu(shard=True)
785
+ selects [rank*(256/world):(rank+1)*(256/world)] BEFORE the optimizer loop
786
+ slices that rank into consecutive micros. Thus canonical draw order is
787
+ rank, micro, row (NOT micro, rank, row): global index
788
+ rank*(256/world) + micro*micro_rows + row. all_gather returns rank order.
789
+ Only fixed 32-byte row digests cross ranks; token arrays stay local.
790
+ """
791
+ if world < 1 or 256 % world or x.shape != (256 // world, block):
792
+ raise ValueError("global batch proof requires exactly 256 global rows")
793
+ payload = _global_batch_row_digests(x, y)
794
+ if world > 1:
795
+ local = torch.frombuffer(payload, dtype=torch.uint8).reshape(256 // world, 32).to(x.device)
796
+ gathered = [torch.empty_like(local) for _ in range(world)]
797
+ dist.all_gather(gathered, local)
798
+ if rank != 0:
799
+ return None
800
+ payload = b"".join(part.cpu().numpy().tobytes() for part in gathered)
801
+ if len(payload) != 256 * 32:
802
+ raise ValueError("global batch proof did not collect exactly 256 row digests")
803
+ h = hashlib.sha256(b"slayer-global-batch-v1\0" + struct.pack("<II", 256, block))
804
+ h.update(payload)
805
+ return h.hexdigest()
806
+
807
+
808
+ def main():
809
+ a = get_args()
810
+ world = int(os.environ.get("WORLD_SIZE", "1"))
811
+ rank = int(os.environ.get("RANK", "0"))
812
+ local_rank = int(os.environ.get("LOCAL_RANK", "0"))
813
+ ddp = world > 1
814
+ if ddp:
815
+ if a.batch % world:
816
+ raise SystemExit(f"STOP: --batch {a.batch} (globalny) nie dzieli sie przez liczbe procesow {world}")
817
+ if torch.cuda.is_available():
818
+ torch.cuda.set_device(local_rank)
819
+ dist.init_process_group("nccl" if torch.cuda.is_available() else "gloo")
820
+ if a.grad_accum < 1 or (a.batch // world) % a.grad_accum:
821
+ raise SystemExit(f"STOP: --grad-accum {a.grad_accum}: wiersze ranku {a.batch // world} nie dziela sie na K mikrobatchy")
822
+ master = rank == 0
823
+ rows = slice(rank * (a.batch // world), (rank + 1) * (a.batch // world)) # wiersze globalnego batcha tego ranku
824
+ out_dir = a.out_dir or os.path.join(a.data_dir, "runs", a.run_id)
825
+ os.makedirs(out_dir, exist_ok=True)
826
+ log_path = os.path.join(out_dir, "train.log")
827
+ metrics_path = os.path.join(out_dir, "metrics.jsonl")
828
+ ckpt_path = os.path.join(out_dir, "ckpt.pt")
829
+
830
+ def log(msg):
831
+ if not master:
832
+ return
833
+ line = f"[{time.strftime('%H:%M:%S')}] {msg}"
834
+ print(line, flush=True)
835
+ with open(log_path, "a", encoding="utf-8") as f:
836
+ f.write(line + "\n")
837
+
838
+ events_path = a.events_jsonl
839
+
840
+ def emit(kind, step, metrics=None, **extra):
841
+ if not events_path or not master:
842
+ return
843
+ rec = {"kind": kind, "updates": int(step), "tokens": int(step) * a.block * a.batch}
844
+ if metrics:
845
+ rec["metrics"] = metrics
846
+ rec.update(extra)
847
+ with open(events_path, "a", encoding="utf-8") as f:
848
+ f.write(json.dumps(rec) + "\n")
849
+
850
+ torch.manual_seed(a.seed)
851
+ torch.backends.cuda.matmul.allow_tf32 = bool(a.tf32) # r17: --tf32 0 jak Qielbas M2 (domyslnie 1 = jak dotad)
852
+ torch.backends.cudnn.allow_tf32 = bool(a.tf32)
853
+ device = torch.device(f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu")
854
+ use_bf16 = device.type == "cuda" and torch.cuda.is_bf16_supported()
855
+ ptdtype = torch.bfloat16 if use_bf16 else torch.float32
856
+ ctx = torch.amp.autocast(device_type=device.type, dtype=ptdtype) if device.type == "cuda" else nullcontext()
857
+ log(f"device={device} bf16={use_bf16} dev={torch.cuda.get_device_name(local_rank) if device.type=='cuda' else 'cpu'}"
858
+ f" world={world}")
859
+
860
+ if a.synthetic:
861
+ rng = np.random.default_rng(a.seed)
862
+ train_data = rng.integers(0, 256, size=8_000_000, dtype=np.uint8)
863
+ val_data = train_data[:200_000]
864
+ log("SYNTHETIC uint8 (smoke throughput, zero danych PII)")
865
+ else:
866
+ train_data = np.memmap(os.path.join(a.data_dir, "train.bin"), dtype=np.dtype(a.dtype), mode="r")
867
+ val_data = np.memmap(os.path.join(a.data_dir, "val.bin"), dtype=np.dtype(a.dtype), mode="r")
868
+ train2_data = (np.memmap(os.path.join(a.data_dir_2, "train.bin"), dtype=np.dtype(a.dtype), mode="r")
869
+ if a.data_dir_2 else None)
870
+ # r24: kolejne strumienie [(krok przejscia, nazwa splitu, memmap)], rosnaco po kroku przejscia
871
+ next_streams = []
872
+ if a.data_dirs_next or a.switch_steps_next:
873
+ _dirs = [d for d in (a.data_dirs_next or "").split(",") if d]
874
+ _sws = [int(s) for s in (a.switch_steps_next or "").split(",") if s]
875
+ if a.synthetic or not a.data_dir_2 or len(_dirs) != len(_sws) or not _dirs:
876
+ raise SystemExit("STOP --data-dirs-next: wymaga --data-dir-2 (bez --synthetic) i tylu --switch-steps-next "
877
+ "co katalogow")
878
+ if any(s <= p for p, s in zip([a.switch_step] + _sws, _sws)):
879
+ raise SystemExit(f"STOP --switch-steps-next: {_sws} musza rosnac i byc > --switch-step {a.switch_step}")
880
+ for k, (d, s) in enumerate(zip(_dirs, _sws), start=3):
881
+ next_streams.append((s, f"train{k}", np.memmap(os.path.join(d, "train.bin"), dtype=np.dtype(a.dtype),
882
+ mode="r")))
883
+ log(f"dane: train={len(train_data):,}B block={a.block} batch={a.batch} tok/step={a.block*a.batch:,}")
884
+
885
+ class EpochSampler:
886
+ """Losowanie BEZ zwracania: epoka e = permutacja okien na siatce co `block` tokenow, przesunietej o losowe
887
+ offset_e w [0, block) (kolejne epoki widza inne granice okien). Krok s bierze okna s*batch..(s+1)*batch-1
888
+ z ciaglego strumienia epok, wiec start od dowolnego kroku (resume) daje dokladnie ten sam ciag co run ciagly.
889
+ Epoka e ma wlasny RNG ziarna (seed, e): najpierw offset, potem permutacja; niezalezna od innych epok."""
890
+ def __init__(self, n_tokens, block, batch, seed):
891
+ self.n, self.block, self.batch, self.seed = n_tokens, block, batch, seed
892
+ self._perm_e, self._perm = None, None
893
+
894
+ def _meta(self, e):
895
+ rng = np.random.default_rng([self.seed, e])
896
+ off = int(rng.integers(self.block))
897
+ return rng, off, (self.n - 1 - off) // self.block # okno = block+1 tokenow od off + k*block
898
+
899
+ def _perm_of(self, e):
900
+ if self._perm_e != e: # trzymamy tylko biezaca epoke (~70 MB przy 9.4B tok / 1024)
901
+ rng, _, nwin = self._meta(e)
902
+ self._perm_e, self._perm = e, rng.permutation(nwin)
903
+ return self._perm
904
+
905
+ def starts(self, step):
906
+ g, need, out, e = step * self.batch, self.batch, [], 0
907
+ while g >= self._meta(e)[2]: # epoki maja rozna dlugosc (offset), wiec liczymy po kolei
908
+ g -= self._meta(e)[2]; e += 1
909
+ while need:
910
+ _, off, nwin = self._meta(e)
911
+ take = min(need, nwin - g)
912
+ out.append(off + self._perm_of(e)[g:g + take].astype(np.int64) * self.block)
913
+ need -= take; g = 0; e += 1
914
+ return np.concatenate(out)
915
+
916
+ class QueueSampler:
917
+ """r7: pula jako kolejka shardow (zakresy tokenow [lo,hi) w train.bin, wyrownane do dokumentow).
918
+ schedule = rotacje manifestu po kolei; slot j = kroki [j*S,(j+1)*S) na shardzie schedule[j]. W slocie kazde okno
919
+ uzyte najwyzej raz: siatka co `block` od lo+off, off i permutacja z RNG (seed, j, id shardu). Starty okien zaleza
920
+ tylko od (manifest, seed, krok), wiec resume z ckpt na granicy slotu jest dokladny, a usuniecie shardu ze slotu j
921
+ nie zmienia krokow < j*S. prefix_sha(krok) = hash slotow juz uzytych; zapisywany w ckpt i sprawdzany przy resume."""
922
+ def __init__(self, manifest, n_tokens, block, batch, seed):
923
+ self.S, self.block, self.batch, self.seed = int(manifest["steps_per_shard"]), block, batch, seed
924
+ self.shards = manifest["shards"]
925
+ self.schedule = [sid for rot in manifest["rotations"] for sid in rot]
926
+ for sid in self.schedule:
927
+ sh = self.shards[sid]
928
+ if not 0 <= sh["lo"] < sh["hi"] <= n_tokens:
929
+ raise SystemExit(f"STOP kolejka: shard {sid} [{sh['lo']},{sh['hi']}) poza train.bin ({n_tokens})")
930
+ if (sh["hi"] - sh["lo"] - block) // block < self.S * batch:
931
+ raise SystemExit(f"STOP kolejka: shard {sid} ma za malo okien na {self.S} krokow x {batch}")
932
+ used = sorted((self.shards[s]["lo"], self.shards[s]["hi"], s) for s in dict.fromkeys(self.schedule))
933
+ for (_, hi0, s0), (lo1, _, s1) in zip(used, used[1:]):
934
+ if lo1 < hi0:
935
+ raise SystemExit(f"STOP kolejka: shardy {s0} i {s1} nakladaja sie w train.bin (dane uzyte dwa razy)")
936
+ self._cache = (None, None)
937
+
938
+ def _slot(self, j):
939
+ if self._cache[0] != j:
940
+ sid = self.schedule[j]
941
+ sh = self.shards[sid]
942
+ idn = int(hashlib.sha256(sid.encode()).hexdigest()[:8], 16)
943
+ rng = np.random.default_rng([self.seed, j, idn])
944
+ off = int(rng.integers(self.block))
945
+ nwin = (sh["hi"] - sh["lo"] - 1 - off) // self.block
946
+ perm = rng.permutation(nwin)[: self.S * self.batch].astype(np.int64)
947
+ self._cache = (j, (sh["lo"] + off, perm))
948
+ return self._cache[1]
949
+
950
+ def starts(self, step):
951
+ j, k = divmod(step, self.S)
952
+ if j >= len(self.schedule):
953
+ raise SystemExit(f"STOP kolejka: krok {step} poza harmonogramem ({len(self.schedule)} slotow x {self.S})")
954
+ base, perm = self._slot(j)
955
+ return base + perm[k * self.batch:(k + 1) * self.batch] * self.block
956
+
957
+ def prefix_sha(self, step):
958
+ used = -(-step // self.S) # sloty, ktorych kroki juz sie zaczely
959
+ spec = [self.S, self.seed, self.block, self.batch,
960
+ [[sid, self.shards[sid]["lo"], self.shards[sid]["hi"], self.shards[sid]["sha256"]] for sid in self.schedule[:used]]]
961
+ return hashlib.sha256(json.dumps(spec).encode()).hexdigest()
962
+
963
+ def _make_batch_cpu(split, generator=None, ix=None, shard=False):
964
+ """Wektoryzowane budowanie batcha: JEDEN numpy fancy-index zamiast
965
+ python-loop per-item. sliding_window_view daje strided-view (N-block, block+1)
966
+ BEZ kopiowania; windows[ix] materializuje tylko wybrane wiersze naraz.
967
+ Zwraca (x,y) long CPU (pinned jesli cuda). Rozklad batchy IDENTYCZNY jak
968
+ stary torch.stack-loop: x=data[i:i+block], y=data[i+1:i+1+block]. ix: gotowe starty okien (sampler epoch).
969
+ shard=True (trening pod DDP): starty losowane dla CALEGO globalnego batcha jak w 1-GPU, rank bierze swoje wiersze."""
970
+ data = train_data if split == "train" else (train2_data if split == "train2" else
971
+ next(( m for _, n, m in next_streams if n == split), val_data))
972
+ if ix is None:
973
+ ix = torch.randint(len(data) - a.block - 1, (a.batch,), generator=generator)
974
+ if shard:
975
+ ix = ix[rows]
976
+ # (N-block, block+1) view; jeden fancy-index kopiuje wybrane okna
977
+ windows = np.lib.stride_tricks.sliding_window_view(data, a.block + 1)
978
+ sel = windows[ix.numpy()] # (batch, block+1) materialized
979
+ x = torch.from_numpy(sel[:, :-1].astype(np.int64)) # astype -> contiguous copy
980
+ y = torch.from_numpy(sel[:, 1:].astype(np.int64))
981
+ if device.type == "cuda":
982
+ x = x.pin_memory(); y = y.pin_memory()
983
+ return x, y
984
+
985
+ def _to_device(x, y):
986
+ if device.type == "cuda":
987
+ return x.to(device, non_blocking=True), y.to(device, non_blocking=True)
988
+ return x.to(device), y.to(device)
989
+
990
+ def get_batch(split, generator=None):
991
+ return _to_device(*_make_batch_cpu(split, generator))
992
+
993
+ class Prefetcher:
994
+ """Async double-buffer: 1 background-thread buduje NASTEPNY batch na CPU
995
+ (pinned) podczas gdy GPU liczy biezacy. queue depth=2. Konsument robi
996
+ .next() -> H2D-copy (non_blocking) w watku glownym. Watek uzywa wlasnego
997
+ torch.Generator (seeded), wiec ciag train-batchy jest deterministyczny i
998
+ NIEZALEZNY od timingu watku oraz od RNG val-loopa (dystrybucja bez zmian).
999
+ sampler (EpochSampler) zamiast generatora: starty okien z kroku, liczonego od start_step."""
1000
+ def __init__(self, split, generator, depth=2, sampler=None, start_step=0):
1001
+ self.split = split
1002
+ self.gen = generator
1003
+ self.sampler, self.step = sampler, start_step
1004
+ self.q = queue.Queue(maxsize=depth)
1005
+ self._stop = threading.Event()
1006
+ self.t = threading.Thread(target=self._worker, daemon=True)
1007
+ self.t.start()
1008
+
1009
+ def _worker(self):
1010
+ while not self._stop.is_set():
1011
+ try:
1012
+ _nx = next(((s, n, sm) for s, n, sm in reversed(samplers_next) if self.step >= s), None)
1013
+ if self.sampler is not None and _nx is not None: # r24
1014
+ item = _make_batch_cpu(_nx[1], ix=torch.from_numpy(_nx[2].starts(self.step - _nx[0])), shard=True)
1015
+ self.step += 1
1016
+ elif self.sampler is not None and sampler2 is not None and self.step >= a.switch_step:
1017
+ item = _make_batch_cpu("train2", ix=torch.from_numpy(sampler2.starts(self.step - a.switch_step)),
1018
+ shard=True)
1019
+ self.step += 1
1020
+ elif self.sampler is not None:
1021
+ item = _make_batch_cpu(self.split, ix=torch.from_numpy(self.sampler.starts(self.step)), shard=True)
1022
+ self.step += 1
1023
+ else:
1024
+ item = _make_batch_cpu(self.split, self.gen, shard=True)
1025
+ except Exception as e: # przekaz blad do konsumenta
1026
+ self.q.put(e)
1027
+ return
1028
+ while not self._stop.is_set():
1029
+ try:
1030
+ self.q.put(item, timeout=0.5)
1031
+ break
1032
+ except queue.Full:
1033
+ continue
1034
+
1035
+ def next(self):
1036
+ item = self.q.get()
1037
+ if isinstance(item, Exception):
1038
+ raise item
1039
+ return _to_device(*item)
1040
+
1041
+ def close(self):
1042
+ self._stop.set()
1043
+ # opróżnij kolejke zeby watek nie zawisl na put()
1044
+ try:
1045
+ self.q.get_nowait()
1046
+ except queue.Empty:
1047
+ pass
1048
+
1049
+ if a.arch == "tinylm": # r19: tylko kombinacje, ktore TinyLM odtwarza; reszta = STOP zamiast cichego pominiecia
1050
+ bad = [n for n, v in (("--loop > 1", a.loop != 1), ("--mixer-layout", a.mixer_layout), ("--emb-dim", a.emb_dim),
1051
+ ("--value-residual", a.value_residual), ("--kd-teacher", a.kd_teacher),
1052
+ ("--act-ckpt", a.act_ckpt), ("--loss-chunk", a.loss_chunk), ("--logit-cap", a.logit_cap),
1053
+ ("--z-loss", a.z_loss), ("--pos != rope", a.pos != "rope"),
1054
+ ("--rope-style != half", a.rope_style != "half"), ("--norm != rmsnorm", a.norm != "rmsnorm"),
1055
+ ("--attn-bias != 0", a.attn_bias != 0), ("--ffn gelu", a.ffn == "gelu"),
1056
+ ("--init-from", a.init_from)) if v]
1057
+ if bad:
1058
+ raise SystemExit(f"STOP --arch tinylm: niezgodne {bad}")
1059
+ if a.n_embd % a.n_head or a.n_head % (a.kv_heads or a.n_head):
1060
+ raise SystemExit(f"STOP --arch tinylm: n_embd {a.n_embd} / n_head {a.n_head} / kv_heads {a.kv_heads}")
1061
+ elif a.tinylm_file:
1062
+ raise SystemExit("STOP: --tinylm-file tylko z --arch tinylm")
1063
+ elif a.kv_heads == a.n_head: # r23: K = n_head to zwykla MHA -> jak 0 (wagi, config ckpt i wyjscia = r22)
1064
+ a.kv_heads = 0
1065
+ raw_model = GPT(a.vocab, a.n_layer, a.n_embd, a.n_head, a.block, a).to(device)
1066
+ nparam = sum(p.numel() for p in raw_model.parameters())
1067
+ log(f"model GPT-ref: {nparam/1e6:.1f}M param (L{a.n_layer} d{a.n_embd} h{a.n_head}"
1068
+ + (f" kv{a.kv_heads}" if a.arch == "gpt" and a.kv_heads else "") + ")")
1069
+ if a.arch == "tinylm":
1070
+ log(f"r19 arch=tinylm (Tiny-Chick-10M@60d32c4d tinylm_arch.py {TINYLM_SHA[:8]}): P={nparam} "
1071
+ 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} "
1072
+ f"xsa={a.xsa} zero_init={a.zero_init} eps={a.norm_eps} theta={a.rope_theta}")
1073
+ n_train = nparam
1074
+ if a.train_only: # r21: zamrozenie wszystkiego poza dopasowanymi nazwami (wzor ft_arc11.py)
1075
+ if not a.init_from:
1076
+ raise SystemExit("STOP --train-only: wymaga --init-from")
1077
+ if a.arch == "tinylm" or a.loop != 1 or a.mixer_layout or a.kd_teacher or a.compile or ddp:
1078
+ raise SystemExit("STOP --train-only: kombinacja nietestowana (tinylm / loop / mixer-layout / kd / compile / DDP)")
1079
+ rx = re.compile(a.train_only)
1080
+ for name, p in raw_model.named_parameters(): # tied head/tok wystepuje raz, jako tok.weight
1081
+ p.requires_grad_(bool(rx.search(name)))
1082
+ n_train = sum(p.numel() for p in raw_model.parameters() if p.requires_grad)
1083
+ if n_train == 0:
1084
+ raise SystemExit(f"STOP --train-only: {a.train_only!r} nie pasuje do zadnego parametru")
1085
+ if raw_model.tok.weight.requires_grad:
1086
+ raise SystemExit(f"STOP --train-only: {a.train_only!r} chwyta osadzenia / tied head")
1087
+ log(f"r21 train-only {a.train_only!r}: uczone {n_train} z {nparam}")
1088
+ opt = build_optimizer(a, raw_model)
1089
+ log(f"optimizer={a.optimizer}" + (f" muon_lr={a.muon_lr} (mult={a.muon_lr/a.lr:.1f}x)" if a.optimizer == "muon" else ""))
1090
+ if a.act_ckpt or a.loss_chunk:
1091
+ log(f"r11 pamiec: act_ckpt={a.act_ckpt} loss_chunk={a.loss_chunk}")
1092
+ if a.imha_t < 0:
1093
+ raise SystemExit("STOP: --imha-t >= 0")
1094
+ if a.imha_t:
1095
+ bad = [n for n, v in (("--xsa", a.xsa), ("--mixer-layout", a.mixer_layout), ("--arch tinylm", a.arch == "tinylm")) if v]
1096
+ if bad:
1097
+ raise SystemExit(f"STOP --imha-t: kombinacja nietestowana {bad}")
1098
+ log(f"r22 IMHA: T={a.imha_t} w {a.n_layer} blokach (jawna macierz uwagi, entropia + bramka); P={nparam}")
1099
+ if a.loop < 1:
1100
+ raise SystemExit("STOP: --loop >= 1")
1101
+ if a.loop > 1:
1102
+ log(f"r12 loop={a.loop}: {a.n_layer} wspoldzielonych blokow x {a.loop} = glebokosc {a.n_layer * a.loop}")
1103
+ if a.mixer_layout or a.attn_bias != 1 or a.rope_style != "interleaved":
1104
+ log(f"r13: mixer_layout={a.mixer_layout or 'A' * a.n_layer} gdn_head_dim={a.gdn_head_dim} "
1105
+ f"attn_bias={a.attn_bias} rope_style={a.rope_style}")
1106
+ if a.xsa or a.rope_dims or a.zero_init or a.ffn == "relu2":
1107
+ 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}")
1108
+ # DDP: identyczne wagi startowe na kazdym ranku (ten sam seed) + broadcast z rank 0 w konstruktorze DDP.
1109
+ # broadcast_buffers=False: forward walidacji (bez backward) nie robi kolektywow.
1110
+ model = DDP(raw_model, device_ids=[local_rank] if device.type == "cuda" else None,
1111
+ broadcast_buffers=False) if ddp else raw_model
1112
+ model = torch.compile(model) if a.compile else model
1113
+ if a.compile:
1114
+ log("torch.compile enabled")
1115
+
1116
+ start_step = 0
1117
+ if a.resume and os.path.exists(ckpt_path):
1118
+ ck = torch.load(ckpt_path, map_location=device, weights_only=True) # r20
1119
+ raw_model.load_state_dict(ck["model"]); opt.load_state_dict(ck["opt"]); start_step = ck["step"]
1120
+ if ck.get("config", {}).get("data_seed") != a.data_seed: # r23: ta sama kolejność danych po wznowieniu
1121
+ raise SystemExit(f"STOP --resume: data_seed w ckpt {ck.get('config', {}).get('data_seed')} != {a.data_seed}")
1122
+ log(f"RESUME @ {start_step}")
1123
+ # r24.1: tensory modelu/optymalizatora z ckpt nie zostają w pamięci karty (model skopiował swoje, optymalizator
1124
+ # trzyma potrzebny stan sam); zostają tylko małe klucze (config, queue, ...), z których korzysta dalszy kod.
1125
+ ck = {k: v for k, v in ck.items() if k not in ("model", "opt")}
1126
+ if device.type == "cuda":
1127
+ torch.cuda.empty_cache()
1128
+ if a.init_from and start_step == 0:
1129
+ ck0 = torch.load(a.init_from, map_location=device, weights_only=True) # r20 (r18/r19: False)
1130
+ c0 = ck0.get("config", {})
1131
+ for key in ("vocab", "n_layer", "n_embd", "n_head"):
1132
+ if c0.get(key) is not None and c0[key] != getattr(a, key):
1133
+ raise SystemExit(f"STOP --init-from: {key}={c0[key]} w ckpt != {getattr(a, key)} w argumentach")
1134
+ if c0.get("loop", 1) != a.loop:
1135
+ raise SystemExit(f"STOP --init-from: loop={c0.get('loop', 1)} w ckpt != {a.loop} w argumentach")
1136
+ 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
1137
+ if c0.get(key, dflt) != getattr(a, key):
1138
+ raise SystemExit(f"STOP --init-from: {key}={c0.get(key, dflt)} w ckpt != {getattr(a, key)} w argumentach")
1139
+ raw_model.load_state_dict(ck0["model"], strict=True)
1140
+ log(f"INIT-FROM {a.init_from} (wagi, krok 0, swiezy optymalizator; grown_from={ck0.get('grown_from')})")
1141
+ del ck0
1142
+ teacher = None
1143
+ if a.kd_teacher: # r14: nauczyciel KD (po budowie/wznowieniu ucznia, wiec jego wagi startowe bez zmian)
1144
+ if not 0.0 <= a.kd_alpha <= 1.0 or a.kd_temp <= 0:
1145
+ raise SystemExit(f"STOP --kd: alpha {a.kd_alpha} poza [0,1] albo temp {a.kd_temp} <= 0")
1146
+ _h = hashlib.sha256()
1147
+ with open(a.kd_teacher, "rb") as _f:
1148
+ for _chunk in iter(lambda: _f.read(1 << 20), b""):
1149
+ _h.update(_chunk)
1150
+ _tsha = _h.hexdigest()
1151
+ if _tsha != (a.kd_teacher_sha or "").lower():
1152
+ raise SystemExit(f"STOP --kd-teacher: sha {_tsha} != --kd-teacher-sha")
1153
+ ckt = torch.load(a.kd_teacher, map_location="cpu", weights_only=True)
1154
+ ct = ckt["config"]
1155
+ if ct["vocab"] != a.vocab or ct["block"] < a.block:
1156
+ raise SystemExit(f"STOP --kd-teacher: vocab {ct['vocab']} / block {ct['block']} nauczyciela "
1157
+ f"vs vocab {a.vocab} / block {a.block} ucznia")
1158
+ import types
1159
+ with torch.random.fork_rng(devices=[]): # budowa (init) nauczyciela nie zmienia globalnego RNG ucznia
1160
+ teacher = GPT(ct["vocab"], ct["n_layer"], ct["n_embd"], ct["n_head"], ct["block"], types.SimpleNamespace(**ct))
1161
+ teacher.load_state_dict({k.removeprefix("_orig_mod."): v for k, v in ckt["model"].items()}, strict=True)
1162
+ teacher = teacher.to(device).eval().requires_grad_(False)
1163
+ log(f"r14 KD: nauczyciel {a.kd_teacher} sha {_tsha[:8]} krok {ckt.get('step')} "
1164
+ f"{sum(p.numel() for p in teacher.parameters())/1e6:.1f}M (L{ct['n_layer']} d{ct['n_embd']} "
1165
+ f"{ct.get('mixer_layout') or 'A' * ct['n_layer']}), alpha {a.kd_alpha} T {a.kd_temp}")
1166
+ del ckt
1167
+ teacher_fwd = torch.compile(teacher) if a.compile else teacher
1168
+ elif a.kd_alpha or a.kd_teacher_sha:
1169
+ raise SystemExit("STOP: --kd-alpha/--kd-teacher-sha bez --kd-teacher")
1170
+ if a.data_offset_steps and a.sampler != "epoch":
1171
+ raise SystemExit("STOP: --data-offset-steps wymaga --sampler epoch")
1172
+
1173
+ def _lr_sched(s):
1174
+ if s < a.warmup:
1175
+ return a.lr * (s + 1) / a.warmup
1176
+ if s >= a.steps:
1177
+ return a.min_lr
1178
+ if a.schedule == "wsd":
1179
+ # warmup-stable-decay (JugnuLM/MiniCPM): constant peak, then 1-sqrt decay in last decay-frac.
1180
+ decay_start = int(a.steps * (1.0 - a.decay_frac))
1181
+ if s < decay_start:
1182
+ return a.lr
1183
+ d = (s - decay_start) / max(1, a.steps - decay_start) # 0->1 over decay phase
1184
+ return a.min_lr + (a.lr - a.min_lr) * (1.0 - d ** 0.5) # 1-sqrt: fast early decay
1185
+ r = (s - a.warmup) / max(1, a.steps - a.warmup)
1186
+ return a.min_lr + 0.5 * (a.lr - a.min_lr) * (1 + math.cos(math.pi * r))
1187
+
1188
+ # --rewarm-steps: galaz z ckpt (np. cosine @320k -> wsd) nie skacze od razu na nowy harmonogram,
1189
+ # tylko przechodzi liniowo od LR zapisanego w ckpt (param_groups po opt.load_state_dict).
1190
+ rewarm_from = None
1191
+ if start_step and a.rewarm_steps > 0:
1192
+ _g0 = opt.param_groups[0]
1193
+ rewarm_from = _g0["lr"] / _g0.get("lr_mult", 1.0)
1194
+ log(f"rewarm: LR {rewarm_from:.3e} -> harmonogram przez {a.rewarm_steps} krokow od {start_step}")
1195
+
1196
+ def lr_at(s):
1197
+ base = _lr_sched(s)
1198
+ if rewarm_from is not None and s < start_step + a.rewarm_steps:
1199
+ f = (s - start_step + 1) / a.rewarm_steps
1200
+ return rewarm_from + (base - rewarm_from) * f
1201
+ return base
1202
+
1203
+ @torch.no_grad()
1204
+ def eval_val():
1205
+ model.eval()
1206
+ ls = []
1207
+ for _ in range(a.eval_iters):
1208
+ xb, yb = get_batch("val")
1209
+ with ctx:
1210
+ _, loss = model(xb, yb)
1211
+ ls.append(loss.item())
1212
+ model.train()
1213
+ return sum(ls) / len(ls)
1214
+
1215
+ model.train()
1216
+ log(f"START gpt-ref: steps={a.steps} (od {start_step}) lr={a.lr}->{a.min_lr}")
1217
+ # dedykowany seeded generator dla train-prefetchera (determinizm niezalezny
1218
+ # od RNG val-loopa i timingu watku; ta sama dystrybucja co global-RNG)
1219
+ train_gen = torch.Generator()
1220
+ ds = a.seed if a.data_seed is None else a.data_seed # r23: kolejność danych osobno od inicjalizacji
1221
+ train_gen.manual_seed(ds)
1222
+ sampler = None
1223
+ sampler2 = None
1224
+ samplers_next = [] # r24: [(krok przejscia, split, EpochSampler)]
1225
+ if a.queue:
1226
+ if a.synthetic:
1227
+ raise SystemExit("STOP: --queue wymaga prawdziwego train.bin (bez --synthetic)")
1228
+ with open(a.queue, "rb") as f:
1229
+ _qraw = f.read()
1230
+ sampler = QueueSampler(json.loads(_qraw), len(train_data), a.block, a.batch, ds)
1231
+ if start_step:
1232
+ _ckq = ck.get("queue") or {}
1233
+ if _ckq.get("prefix_sha") != sampler.prefix_sha(start_step):
1234
+ raise SystemExit(f"STOP kolejka: sloty uzyte do kroku {start_step} w manifescie != zapisane w ckpt "
1235
+ "(resume nie bylby dokladny)")
1236
+ log(f"sampler queue: manifest {hashlib.sha256(_qraw).hexdigest()[:8]}, {len(sampler.schedule)} slotow x "
1237
+ f"{sampler.S} krokow, start od kroku {start_step} = slot {start_step // sampler.S}")
1238
+ elif a.sampler == "epoch" and not a.synthetic:
1239
+ sampler = EpochSampler(len(train_data), a.block, a.batch, ds)
1240
+ _, _off0, _nwin0 = sampler._meta(0)
1241
+ log(f"sampler epoch: {_nwin0:,} okien/epoke (offset0 {_off0}), start od kroku {start_step} "
1242
+ f"= epoka {start_step * a.batch / _nwin0:.3f}")
1243
+ if a.data_dir_2:
1244
+ if a.switch_step <= 0 or _nwin0 // a.batch < a.switch_step:
1245
+ raise SystemExit(f"STOP --data-dir-2: --switch-step {a.switch_step} poza epoka 0 train.bin "
1246
+ f"({_nwin0 // a.batch} krokow)")
1247
+ sampler2 = EpochSampler(len(train2_data), a.block, a.batch, ds)
1248
+ _nwin2 = sampler2._meta(0)[2]
1249
+ log(f"data-dir-2: {a.data_dir_2} {len(train2_data):,} tok, {_nwin2:,} okien/epoke = {_nwin2 // a.batch} krokow, "
1250
+ f"od kroku strumienia {a.switch_step}")
1251
+ for _sw, _nm, _mm in next_streams:
1252
+ samplers_next.append((_sw, _nm, EpochSampler(len(_mm), a.block, a.batch, ds)))
1253
+ _nw = samplers_next[-1][2]._meta(0)[2]
1254
+ log(f"r24 {_nm}: {len(_mm):,} tok, {_nw:,} okien/epoke = {_nw // a.batch} krokow, od kroku strumienia {_sw}")
1255
+ elif a.data_dir_2:
1256
+ raise SystemExit("STOP: --data-dir-2 wymaga --sampler epoch")
1257
+ elif start_step and not a.synthetic:
1258
+ # --resume: przewin generator o start_step batchy, zeby wznowiony run losowal DOKLADNIE te okna,
1259
+ # ktore run ciagly losowalby od kroku start_step (bez tego resume powtarzal okna z krokow 0..N).
1260
+ # Kazdy krok zuzywa 1 x torch.randint(len-block-1, (batch,)); 320k krokow ~1 s CPU.
1261
+ _n = len(train_data) - a.block - 1
1262
+ for _ in range(start_step):
1263
+ torch.randint(_n, (a.batch,), generator=train_gen)
1264
+ log(f"train RNG fast-forward o {start_step} krokow (resume)")
1265
+ if next_streams and not samplers_next:
1266
+ raise SystemExit("STOP --data-dirs-next: wymaga --sampler epoch")
1267
+ if a.data_offset_steps:
1268
+ log(f"data-offset: dane od kroku strumienia {start_step + a.data_offset_steps} (krok biegu {start_step} + {a.data_offset_steps})")
1269
+ prefetcher = Prefetcher("train", train_gen, sampler=sampler, start_step=start_step + a.data_offset_steps)
1270
+ end_step = a.steps
1271
+ if a.queue and a.queue_one_slot:
1272
+ end_step = min(a.steps, (start_step // sampler.S + 1) * sampler.S)
1273
+ if a.global_batch_sha is not None:
1274
+ if a.batch_sha or a.batch != 256 or not start_step <= a.global_batch_sha < end_step:
1275
+ raise SystemExit(f"STOP --global-batch-sha: wymaga batch256, prawdziwego treningu (bez --batch-sha) "
1276
+ f"i kroku w [{start_step}, {end_step})")
1277
+ if sampler2 is not None:
1278
+ _last = a.data_offset_steps + end_step - 1 # ostatni krok strumienia tego biegu
1279
+ # r24: kazdy odcinek [S_k, S_{k+1} - 1] (ostatni do _last) musi zmiescic sie w 1 epoce swojego train.bin
1280
+ _segs = [(a.switch_step, "DATA_DIR_2", sampler2)] + [(s, n, sm) for s, n, sm in samplers_next]
1281
+ for i, (_s, _n, _sm) in enumerate(_segs):
1282
+ _end = min(_last, _segs[i + 1][0] - 1) if i + 1 < len(_segs) else _last
1283
+ if _end - _s >= _sm._meta(0)[2] // a.batch:
1284
+ raise SystemExit(f"STOP {_n}: krok strumienia {_end} wychodzi poza 1 epoke (powtorzenie)")
1285
+
1286
+ if a.batch_sha:
1287
+ _want = sorted({int(s) for s in a.batch_sha.split(",")})
1288
+ if _want[0] < start_step or _want[-1] >= end_step:
1289
+ raise SystemExit(f"STOP --batch-sha: kroki poza [{start_step}, {end_step})")
1290
+ for s in range(start_step, _want[-1] + 1):
1291
+ xb, yb = prefetcher.next()
1292
+ if s in _want:
1293
+ _h = hashlib.sha256(xb.cpu().numpy().tobytes() + yb.cpu().numpy().tobytes()).hexdigest()
1294
+ log(f"BATCH-SHA krok biegu {s} = krok strumienia {s + a.data_offset_steps}: {_h}")
1295
+ prefetcher.close()
1296
+ log("BATCH-SHA DONE")
1297
+ return
1298
+
1299
+ ckpt_at = {int(s) for s in (a.ckpt_at or "").split(",") if s} # r24
1300
+
1301
+ def save_ckpt(done):
1302
+ if master:
1303
+ state = {"model": raw_model.state_dict(), "opt": opt.state_dict(), "step": done,
1304
+ "config": {"vocab": a.vocab, "n_layer": a.n_layer, "n_embd": a.n_embd,
1305
+ "n_head": a.n_head, "block": a.block, "norm": a.norm,
1306
+ "norm_eps": a.norm_eps, "pos": a.pos, "rope_theta": a.rope_theta,
1307
+ "ffn": a.ffn, "ffn_mult": a.ffn_mult, "value_residual": a.value_residual,
1308
+ "qk_norm": a.qk_norm, "logit_cap": a.logit_cap, "z_loss": a.z_loss,
1309
+ **({"loop": a.loop} if a.loop > 1 else {}),
1310
+ **({"data_seed": a.data_seed} if a.data_seed is not None else {}),
1311
+ **({"mixer_layout": a.mixer_layout, "gdn_head_dim": a.gdn_head_dim} if a.mixer_layout else {}),
1312
+ **({"attn_bias": a.attn_bias} if a.attn_bias != 1 else {}),
1313
+ **({"rope_style": a.rope_style} if a.rope_style != "interleaved" else {}),
1314
+ **({"emb_dim": a.emb_dim} if a.emb_dim else {}),
1315
+ **({"xsa": a.xsa} if a.xsa else {}),
1316
+ **({"rope_dims": a.rope_dims} if a.rope_dims else {}),
1317
+ **({"zero_init": a.zero_init} if a.zero_init else {}),
1318
+ **({"imha_t": a.imha_t} if a.imha_t else {}),
1319
+ **({"kv_heads": a.kv_heads} if a.arch == "gpt" and a.kv_heads else {}),
1320
+ **({"arch": "tinylm", "kv_heads": a.kv_heads or a.n_head, "tinylm_sha256": TINYLM_SHA}
1321
+ if a.arch == "tinylm" else {})}}
1322
+ if a.queue:
1323
+ state["queue"] = {"prefix_sha": sampler.prefix_sha(done), "slots_used": -(-done // sampler.S)}
1324
+ if teacher is not None:
1325
+ state["kd"] = {"teacher": a.kd_teacher, "teacher_sha256": _tsha, "alpha": a.kd_alpha, "temp": a.kd_temp}
1326
+ if a.train_only: # r21: poza "config", wiec --init-from i valport bez zmian
1327
+ state["train_only"] = {"regex": a.train_only, "n_trainable": n_train}
1328
+ torch.save(state, ckpt_path + ".tmp")
1329
+ os.replace(ckpt_path + ".tmp", ckpt_path) # crash w trakcie zapisu nie zostawia ucietego ckpt.pt
1330
+ log(f"ckpt @ {done}")
1331
+ if a.events_jsonl:
1332
+ _h = hashlib.sha256()
1333
+ with open(ckpt_path, "rb") as _cf:
1334
+ for _chunk in iter(lambda: _cf.read(1 << 20), b""):
1335
+ _h.update(_chunk)
1336
+ emit("checkpoint", done, sha256=_h.hexdigest())
1337
+ if ddp:
1338
+ dist.barrier() # inne ranki nie ida dalej (ani nie koncza), zanim rank 0 zapisze ckpt
1339
+
1340
+ def finish():
1341
+ prefetcher.close()
1342
+ if ddp:
1343
+ dist.destroy_process_group()
1344
+
1345
+ # r9 (Monter, 06.10): strata sumowana na urządzeniu (float64), .item() tylko przy logu. r8 robił loss.item() co krok,
1346
+ # czyli synchronizację GPU->CPU co krok, przez co CPU nie mógł kolejkować następnego kroku. Obliczenia modelu,
1347
+ # kolejność batchy i aktualizacje wag są identyczne z r8; zmienia się tylko moment odczytu straty do logu.
1348
+ t0 = time.time(); running = torch.zeros((), dtype=torch.float64, device=device)
1349
+ running_ce = torch.zeros((), dtype=torch.float64, device=device)
1350
+ for step in range(start_step, end_step):
1351
+ lr = lr_at(step)
1352
+ for g in opt.param_groups:
1353
+ g["lr"] = lr * g.get("lr_mult", 1.0)
1354
+ xb, yb = prefetcher.next()
1355
+ if a.grad_accum == 1:
1356
+ with ctx:
1357
+ if teacher is None:
1358
+ _, loss = model(xb, yb)
1359
+ else:
1360
+ with torch.no_grad():
1361
+ tl = teacher_fwd(xb)[0]
1362
+ _, loss, ce = model(xb, yb, tl)
1363
+ del tl
1364
+ running_ce += ce.to(torch.float64)
1365
+ loss.backward()
1366
+ else: # r16: K mikrobatchy z wierszy ranku, gradient = srednia (strata_j / K); DDP synchronizuje tylko ostatni
1367
+ K = a.grad_accum; m = xb.size(0) // K
1368
+ loss = torch.zeros((), dtype=torch.float32, device=device)
1369
+ for j in range(K):
1370
+ xj, yj = xb[j * m:(j + 1) * m], yb[j * m:(j + 1) * m]
1371
+ with (model.no_sync() if ddp and j < K - 1 else nullcontext()):
1372
+ with ctx:
1373
+ if teacher is None:
1374
+ _, lj = model(xj, yj)
1375
+ else:
1376
+ with torch.no_grad():
1377
+ tl = teacher_fwd(xj)[0]
1378
+ _, lj, ce = model(xj, yj, tl)
1379
+ del tl
1380
+ running_ce += ce.to(torch.float64) / K
1381
+ (lj / K).backward()
1382
+ loss += lj.detach().float() / K
1383
+ gn = torch.nn.utils.clip_grad_norm_(model.parameters(), a.grad_clip) if a.grad_clip > 0 else 0.0
1384
+ opt.step(); opt.zero_grad(set_to_none=True)
1385
+ if a.global_batch_sha is not None and step == a.global_batch_sha:
1386
+ # xb/yb are the exact tensors consumed above, including every accumulation slice.
1387
+ batch_digest = _global_batch_sha(xb, yb, a.block, world, rank)
1388
+ if master:
1389
+ log(f"GLOBAL-BATCH-SHA zero={step} native={step + 1} rows=256 sha256={batch_digest}")
1390
+ running += loss.detach().to(torch.float64)
1391
+ if (step + 1) % a.log_every == 0:
1392
+ if ddp: # strata logowana = srednia po wszystkich wierszach globalnego batcha (jak w 1-GPU)
1393
+ dist.all_reduce(running)
1394
+ running = running / world
1395
+ if teacher is not None:
1396
+ dist.all_reduce(running_ce)
1397
+ running_ce = running_ce / world
1398
+ running = running.item()
1399
+ kd_ce = running_ce.item() / a.log_every if teacher is not None else None
1400
+ dt = time.time() - t0
1401
+ tok_s = a.log_every * a.block * a.batch / dt
1402
+ mem = torch.cuda.max_memory_allocated(device)/1e9 if device.type == "cuda" else 0.0
1403
+ 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"
1404
+ + (f" ce {kd_ce:.4f}" if kd_ce is not None else ""))
1405
+ if master:
1406
+ with open(metrics_path, "a", encoding="utf-8") as f:
1407
+ f.write(json.dumps({"step": step+1, "loss": running/a.log_every, "lr": lr, "tok_s": tok_s,
1408
+ **({"ce": kd_ce} if kd_ce is not None else {})}) + "\n")
1409
+ emit("update", step + 1, metrics={"loss": running / a.log_every, "tokens_per_second": tok_s,
1410
+ "gradient_norm": float(gn), "learning_rate": lr})
1411
+ running = torch.zeros((), dtype=torch.float64, device=device); t0 = time.time()
1412
+ running_ce = torch.zeros((), dtype=torch.float64, device=device)
1413
+ if (step + 1) % a.eval_every == 0:
1414
+ vloss = eval_val()
1415
+ log(f" >> VAL loss {vloss:.4f} @ {step+1}")
1416
+ emit("evaluation", step + 1, metrics={"loss": vloss})
1417
+ if ((step + 1) % a.ckpt_every == 0 or (step + 1) in ckpt_at) and not a.synthetic:
1418
+ save_ckpt(step + 1)
1419
+ if end_step % a.ckpt_every and (a.queue or not a.synthetic):
1420
+ 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)
1421
+ if end_step < a.steps:
1422
+ log(f"SLOT DONE @ {end_step}")
1423
+ finish()
1424
+ return
1425
+ log("DONE")
1426
+ emit("end", a.steps, status="completed")
1427
+ finish()
1428
+
1429
+
1430
+ if __name__ == "__main__":
1431
+ main()
uruchom_109.sh ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # uruchom_109.sh (Hart, 09.11) — Slayer1B 1.09 (PLAN-1.09 Hart/1.09): 95 000 kroków × 1 mln tok, 8x B200, trener r24.2
3
+ # 46f83b51 BEZ ZMIAN w trybie kolejki shardów (r7: --queue + --queue-one-slot). Smoke 18:20 (stacja, tiny): bieg slot po
4
+ # slocie == bieg ciągły bit w bit (init/od zera, adamw/muon, DDP gloo); raport _tmp/Hart/smoke_kolejka_r24_2_raport.md.
5
+ #
6
+ # START=zero NCCL_P2P_DISABLE=0 bash uruchom_109.sh # każde wywołanie = następny slot (S kroków), potem SLOT DONE
7
+ # CIAGLE=1 START=zero NCCL_P2P_DISABLE=0 bash uruchom_109.sh # bez --queue-one-slot: tylko gdy manifest ma WSZYSTKIE sloty
8
+ #
9
+ # Wymagane env: START (zero = od zera | 107 = --init-from wag HF 1.07), NCCL_P2P_DISABLE (0|1).
10
+ # D = katalog z train.bin (shardy sklejone, uint16), val.bin, man.json {steps_per_shard, shards{id:{lo,hi,sha256,source}},
11
+ # rotations}. Kolejność dopisania shardu: NAJPIERW bajty na koniec train.bin (po GREEN W4 bajtów), POTEM manifest.
12
+ # sha256 shardu = sha256 bajtów train.bin [2·lo, 2·hi). Ograniczenia trenera (smoke): brak slotu w manifeście = trener
13
+ # wisi (wątek prefetchera) -> sprawdzane TUTAJ przed startem; bajty shardów trener nie sprawdza -> sprawdzane TUTAJ.
14
+ set -euo pipefail
15
+ unset HF_TOKEN HUGGING_FACE_HUB_TOKEN HUGGINGFACE_HUB_TOKEN PYTHONPATH
16
+ export HF_HUB_OFFLINE=1 HF_DATASETS_OFFLINE=1 CUDA_DEVICE_ORDER=PCI_BUS_ID OMP_NUM_THREADS=8 PYTHONUNBUFFERED=1 \
17
+ PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
18
+ die() { echo "STOP: $*" >&2; exit 2; }
19
+ : "${START:?START = zero|107}"; [[ $START = zero || $START = 107 ]] || die "START musi być zero albo 107"
20
+ : "${NCCL_P2P_DISABLE:?jawnie NCCL_P2P_DISABLE=0|1 (B200 z NVLink: 0)}"
21
+ CIAGLE=${CIAGLE:-0}; [[ $CIAGLE = 0 || $CIAGLE = 1 ]] || die "CIAGLE = 0|1"
22
+ # Typ kart (plan B przy braku spot B200, Latarnik 19:33): ta sama matematyka i te same argumenty trenera, inna arch.
23
+ GPU=${GPU:-B200}
24
+ case $GPU in B200) ARCH=sm_100;; H200) ARCH=sm_90;; *) die "GPU musi być B200 albo H200";; esac
25
+
26
+ WS=${WS:-/workspace}; T=${T:-$WS/kod/train_gpt_ref_r24_2.py}; HF=${HF:-$WS/hf107}; CONV=${CONV:-$WS/kod/hf_do_ckpt.py}
27
+ D=${D:-$WS/dane109}; O=${O:-$WS/runs/109}; INIT=${INIT:-$WS/init/init_107.pt}; MAN=$D/man.json
28
+ PY=${PY:-python3}; TORCHRUN=${TORCHRUN:-torchrun}; PORT=${PORT:-29551}
29
+ NGPU=8; BLOCK=4096; BATCH=256; VOCAB=32768; MICRO=8; GA=4 # pomiar 581 775 tok/s, bez act-ckpt
30
+ KROKI=${KROKI:-95000}; S=${S:-9500}; DECAY=0.2; DATA_SEED=20261110; CKPT_EVERY=500
31
+ if [[ $START = zero ]]; then WARMUP=2000; else WARMUP=1000; fi
32
+ SHA_TRENER=46f83b51fcec18ffc3273d80fb9eab29e182c52d8667800dab27b80f7ff18afc # Hart/imha/train_gpt_ref_r24_2.py
33
+ SHA_CONV=4990a1c2bc664417ea964a5235d702cad12bff0dc53ce08211ff9a84cbcfed64 # hf_do_ckpt.py (LF)
34
+ RUN_ID=slayer1b_109_$START
35
+
36
+ [[ $((MICRO * GA * NGPU)) = "$BATCH" ]] || die "MICRO×GA×NGPU != $BATCH"
37
+ [[ $((KROKI % S)) = 0 ]] || die "KROKI $KROKI nie jest wielokrotnością S $S"
38
+ [[ $(sha256sum "$T" | cut -c1-64) = "$SHA_TRENER" ]] || die "sha trenera != r24.2 46f83b51"
39
+ [[ -s $D/train.bin && -s $D/val.bin && -s $MAN ]] || die "brak $D/train.bin, val.bin albo man.json"
40
+ if env | grep -qiE '^[^=]*(hf|hugging)[^=]*token[^=]*='; then die "token HF w env"; fi
41
+ [[ ! -e ${HF_HOME:-$HOME/.cache/huggingface}/token ]] || die "plik tokenu HF na węźle"
42
+
43
+ # Krok startowy z ckpt (krok czytany z pliku, nie z nazwy); brak ckpt = krok 0.
44
+ K=0
45
+ if [[ -s $O/ckpt.pt ]]; then
46
+ K=$("$PY" -c "import torch,sys; print(int(torch.load(sys.argv[1], map_location='cpu', mmap=True, weights_only=True)['step']))" "$O/ckpt.pt")
47
+ fi
48
+ if [[ $K -ge $KROKI ]]; then
49
+ [[ ! -s $O/kroki.txt || $(cat "$O/kroki.txt") = "$KROKI" ]] || die "KROKI $KROKI <= krok ckpt $K przy biegu na $(cat "$O/kroki.txt") - skrócenie po fakcie niedozwolone"
50
+ echo "TRAIN-DONE 1.09 wcześniej (ckpt krok $K = $KROKI)"; exit 0
51
+ fi
52
+
53
+ echo "== manifest kolejki: sloty, zakresy, sha bajtów nowych shardów"
54
+ mkdir -p "$O"
55
+ PLAN=$("$PY" - "$MAN" "$D/train.bin" "$S" "$KROKI" "$K" "$O/zweryfikowane.txt" <<'PY'
56
+ import hashlib, json, os, sys
57
+ man_p, bin_p, S, kroki, k, ok_p = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]), int(sys.argv[5]), sys.argv[6]
58
+ m = json.load(open(man_p, encoding="utf-8"))
59
+ if int(m["steps_per_shard"]) != S:
60
+ sys.exit(f"STOP: steps_per_shard {m['steps_per_shard']} != S {S}")
61
+ sched = [s for rot in m["rotations"] for s in rot]
62
+ if len(sched) != len(set(sched)):
63
+ sys.exit("STOP: shard dwa razy w rotacjach (powtórka danych)")
64
+ if len(sched) * S > kroki:
65
+ sys.exit(f"STOP: {len(sched)} slotów × {S} > KROKI {kroki}")
66
+ n_tok = os.path.getsize(bin_p) // 2
67
+ done = set(open(ok_p).read().split("\n")) if os.path.exists(ok_p) else set()
68
+ for idx, sid in enumerate(sched):
69
+ sh = m["shards"][sid]
70
+ lo, hi, want = int(sh["lo"]), int(sh["hi"]), sh["sha256"].lower()
71
+ if not 0 <= lo < hi <= n_tok:
72
+ sys.exit(f"STOP: shard {sid} [{lo},{hi}) poza train.bin ({n_tok} tok) - najpierw bajty, potem manifest")
73
+ key = f"{sid} {lo} {hi} {want}"
74
+ if key in done or idx < k // S: # sloty < K/S zużyte (mogą być wycięte z dysku); metadane pilnuje prefix_sha
75
+ continue
76
+ h = hashlib.sha256()
77
+ with open(bin_p, "rb") as f:
78
+ f.seek(2 * lo); left = 2 * (hi - lo)
79
+ while left:
80
+ b = f.read(min(left, 1 << 24)); h.update(b); left -= len(b)
81
+ if h.hexdigest() != want:
82
+ sys.exit(f"STOP: shard {sid} bajty sha {h.hexdigest()[:12]} != manifest {want[:12]}")
83
+ with open(ok_p, "a") as f:
84
+ f.write(key + "\n")
85
+ print(f" shard {sid} [{lo},{hi}) sha {want[:12]} OK", file=sys.stderr)
86
+ slot = k // S
87
+ if slot >= len(sched):
88
+ sys.exit(f"STOP: brak shardu dla slotu {slot + 1} (manifest ma {len(sched)}) - trener by zawisł; dopisz shard")
89
+ print(len(sched))
90
+ PY
91
+ ) || die "manifest/shardy (wyżej)"
92
+ NSLOT=$PLAN
93
+ if [[ $CIAGLE = 1 ]]; then
94
+ [[ $((NSLOT * S)) -ge $KROKI ]] || die "CIAGLE=1 wymaga wszystkich slotów ($NSLOT × $S < $KROKI)"
95
+ E=$KROKI
96
+ else
97
+ E=$(( (K / S + 1) * S )); [[ $E -le $KROKI ]] || E=$KROKI
98
+ fi
99
+ echo "PLAN $RUN_ID: kroki $K -> $E z $KROKI (slot $((K / S + 1))/$((KROKI / S)), manifest $NSLOT slotów)"
100
+
101
+ echo "== GPU: $NGPU x $GPU, $ARCH"
102
+ [[ $(nvidia-smi --query-gpu=index --format=csv,noheader | wc -l) -ge $NGPU ]] || die "mniej niż $NGPU GPU"
103
+ for g in $(seq 0 $((NGPU - 1))); do
104
+ NAME=$(nvidia-smi -i "$g" --query-gpu=name --format=csv,noheader)
105
+ [[ $NAME = *"$GPU"* ]] || die "GPU $g to '$NAME', nie $GPU"
106
+ USED=$(nvidia-smi -i "$g" --query-gpu=memory.used --format=csv,noheader,nounits)
107
+ [[ $USED =~ ^[0-9]+$ && $USED -lt 1000 ]] || die "GPU $g zajęte: $USED MiB"
108
+ done
109
+ "$PY" -c "import torch,sys; sys.exit(0 if sys.argv[1] in torch.cuda.get_arch_list() else 3)" "$ARCH" || die "torch bez $ARCH"
110
+
111
+ if [[ $START = 107 && $K = 0 && ! -s $INIT ]]; then
112
+ [[ $(tr -d '\r' < "$CONV" | sha256sum | cut -c1-64) = "$SHA_CONV" ]] || die "sha hf_do_ckpt.py != 4990a1c2"
113
+ echo "== init: wagi HF 1.07 -> ckpt trenera (hf_do_ckpt.py, piny d826b4a1 / 9e206250 / 46f83b51)"
114
+ mkdir -p "$(dirname "$INIT")"; "$PY" "$CONV" --pkg "$HF" --trainer "$T" --out "$INIT"
115
+ fi
116
+
117
+ ARGS=(--data-dir "$D" --out-dir "$O" --run-id "$RUN_ID" --queue "$MAN" --resume --steps "$KROKI"
118
+ --batch "$BATCH" --grad-accum "$GA" --block "$BLOCK" --dtype uint16 --vocab "$VOCAB"
119
+ --n-layer 20 --n-embd 2048 --n-head 16 --norm rmsnorm --norm-eps 1e-6 --pos rope --rope-theta 500000
120
+ --ffn swiglu --ffn-mult 2.667 --value-residual --qk-norm --attn-bias 0 --logit-cap 0 --z-loss 0
121
+ --optimizer muon --lr 6e-4 --min-lr 6e-5 --muon-lr 0.02 --wd 0.1 --grad-clip 1.0
122
+ --schedule wsd --decay-frac "$DECAY" --warmup "$WARMUP" --rewarm-steps 0
123
+ --seed 1 --data-seed "$DATA_SEED" --loss-chunk 1024 --compile
124
+ --log-every 10 --eval-every 1000 --eval-iters 20 --ckpt-every "$CKPT_EVERY"
125
+ --events-jsonl "$O/events.jsonl")
126
+ if [[ $START = 107 ]]; then ARGS+=(--init-from "$INIT"); fi
127
+ # Te same argumenty przy każdym slocie. Wyjątek: KROKI (= --steps, długość całości) wolno zmienić TYLKO przed początkiem
128
+ # wygaszania wg starej i nowej długości - w fazie stałej WSD LR nie zależy od --steps, więc kroki < K są te same.
129
+ NOWE_ARGS=$(printf '%q ' "${ARGS[@]}")
130
+ if [[ -s $O/args.txt ]]; then
131
+ STARE_K=$(cat "$O/kroki.txt" 2>/dev/null || echo "$KROKI")
132
+ if [[ $STARE_K != "$KROKI" ]]; then
133
+ [[ "${NOWE_ARGS/--steps $KROKI /--steps $STARE_K }" = "$(cat "$O/args.txt")" ]] \
134
+ || die "zmiana KROKI $STARE_K -> $KROKI, ale inne argumenty też się różnią"
135
+ GRAN=$("$PY" -c "import sys; a,b,d=int(sys.argv[1]),int(sys.argv[2]),float(sys.argv[3]); print(min(int(a*(1-d)), int(b*(1-d))))" "$STARE_K" "$KROKI" "$DECAY")
136
+ [[ $K -lt $GRAN ]] || die "zmiana KROKI po początku wygaszania (krok $K >= $GRAN) - niedozwolona"
137
+ echo "ZMIANA KROKI $STARE_K -> $KROKI przy kroku $K (wygaszanie od $(( KROKI * 8 / 10 )))"
138
+ printf '%s' "$NOWE_ARGS" > "$O/args.txt"; echo "$KROKI" > "$O/kroki.txt"
139
+ fi
140
+ [[ "$NOWE_ARGS" = "$(cat "$O/args.txt")" ]] || die "argumenty inne niż przy pierwszym slocie ($O/args.txt)"
141
+ else
142
+ printf '%s' "$NOWE_ARGS" > "$O/args.txt"; echo "$KROKI" > "$O/kroki.txt"
143
+ fi
144
+ [[ $CIAGLE = 1 ]] || ARGS+=(--queue-one-slot)
145
+
146
+ echo "== torchrun $NGPU × r24.2 ($RUN_ID): kroki $K -> $E"
147
+ L0=0; [[ ! -f $O/train.log ]] || L0=$(wc -l < "$O/train.log")
148
+ nowe() { tail -n +$((L0 + 1)) "$O/train.log" 2>/dev/null; }
149
+ preflight() { # przed 1. linią kroku: world, P, kolejka od kroku K; K=0: bez RESUME (+INIT-FROM przy 107); K>0: RESUME @ K
150
+ local i zyje
151
+ for i in $(seq 1 120); do
152
+ zyje=1; kill -0 "$TPID" 2>/dev/null || zyje=0
153
+ if nowe | grep -qE '\] step [0-9]+/'; then
154
+ nowe | grep -qE " world=$NGPU\$" || { echo "pre-flight: brak world=$NGPU"; return 1; }
155
+ nowe | grep -qE 'model GPT-ref: 1073\.9M param' || { echo "pre-flight: P != 1073.9M"; return 1; }
156
+ nowe | grep -qE "sampler queue: manifest [0-9a-f]+, $NSLOT slotow x $S krokow, start od kroku $K " \
157
+ || { echo "pre-flight: kolejka nie od kroku $K / inny manifest"; return 1; }
158
+ if [[ $K = 0 ]]; then
159
+ ! nowe | grep -q 'RESUME @' || { echo "pre-flight: RESUME przy kroku 0"; return 1; }
160
+ if [[ $START = 107 ]]; then nowe | grep -q 'INIT-FROM ' || { echo "pre-flight: brak INIT-FROM"; return 1; }
161
+ else ! nowe | grep -q 'INIT-FROM ' || { echo "pre-flight: INIT-FROM przy START=zero"; return 1; }; fi
162
+ else
163
+ nowe | grep -qE "RESUME @ $K\$" || { echo "pre-flight: brak RESUME @ $K"; return 1; }
164
+ fi
165
+ echo "PRE-FLIGHT OK (krok $K)"; return 0
166
+ fi
167
+ [[ $zyje = 1 ]] || return 0
168
+ sleep 10
169
+ done
170
+ echo "pre-flight: 20 min bez linii kroku"; return 1
171
+ }
172
+ rc=0
173
+ "$TORCHRUN" --nnodes 1 --nproc-per-node "$NGPU" --rdzv-backend c10d --rdzv-endpoint "127.0.0.1:$PORT" \
174
+ "$T" "${ARGS[@]}" < /dev/null &
175
+ TPID=$!
176
+ if ! preflight; then
177
+ kill -TERM "$TPID" 2>/dev/null || true; sleep 30; kill -KILL "$TPID" 2>/dev/null || true
178
+ die "pre-flight nie przeszedł - trening zatrzymany ($O/train.log)"
179
+ fi
180
+ wait "$TPID" || rc=$?
181
+ [[ $rc = 0 ]] || die "trener rc=$rc (wywłaszczenie: to samo wywołanie wznowi z ckpt.pt)"
182
+ if [[ $E = "$KROKI" ]]; then nowe | grep -qE '\] DONE$' || die "brak DONE w nowych liniach"
183
+ else nowe | grep -qE "SLOT DONE @ $E\$" || die "brak 'SLOT DONE @ $E' w nowych liniach"; fi
184
+ KK=$("$PY" -c "import torch,sys; print(int(torch.load(sys.argv[1], map_location='cpu', mmap=True, weights_only=True)['step']))" "$O/ckpt.pt")
185
+ [[ $KK = "$E" ]] || die "ckpt.pt ma krok $KK != $E"
186
+ # Kamień = same wagi + config + krok (ok. 4,3 GB zamiast ok. 13 GB z optymalizatorem; ewaluacja i eksport HF czytają
187
+ # tylko "model"/"config"). Pełny stan do wznowienia to zawsze $O/ckpt.pt.
188
+ mkdir -p "$O/hist"
189
+ "$PY" - "$O/ckpt.pt" "$O/hist/wagi_$KK.pt" <<'PY'
190
+ import os, sys, torch
191
+ ck = torch.load(sys.argv[1], map_location="cpu", mmap=True, weights_only=True)
192
+ torch.save({"model": ck["model"], "config": ck["config"], "step": ck["step"]}, sys.argv[2] + ".tmp")
193
+ os.replace(sys.argv[2] + ".tmp", sys.argv[2])
194
+ PY
195
+ (cd "$O" && sha256sum "hist/wagi_$KK.pt" >> SHA256SUMS)
196
+ # Zużyte shardy (sloty < KK/S) trener już nigdy nie czyta: dziura w pliku (rozmiar i offsety bez zmian) zwalnia dysk.
197
+ # Ich sha zostały sprawdzone przed użyciem (zweryfikowane.txt), a lo/hi/sha w manifeście pilnuje prefix_sha w ckpt.
198
+ "$PY" - "$MAN" "$S" "$KK" <<'PY' | while read -r SID OFF LEN; do
199
+ import json, sys
200
+ m = json.load(open(sys.argv[1], encoding="utf-8")); S, kk = int(sys.argv[2]), int(sys.argv[3])
201
+ sched = [s for rot in m["rotations"] for s in rot]
202
+ for sid in sched[: kk // S]:
203
+ sh = m["shards"][sid]; print(sid, 2 * int(sh["lo"]), 2 * (int(sh["hi"]) - int(sh["lo"])))
204
+ PY
205
+ grep -qx "$SID" "$O/wyciete.txt" 2>/dev/null && continue
206
+ fallocate --punch-hole --keep-size -o "$OFF" -l "$LEN" "$D/train.bin" || die "punch-hole $SID nie powiódł się"
207
+ echo "$SID" >> "$O/wyciete.txt"; echo " shard $SID wycięty z dysku (zużyty)"
208
+ done
209
+ if [[ $KK = "$KROKI" ]]; then echo "TRAIN-DONE 1.09 $RUN_ID krok $KK"; else echo "SLOT-DONE 1.09 $RUN_ID krok $KK/$KROKI (kamień hist/wagi_$KK.pt)"; fi