SecludedCorner commited on
Commit
8182d87
·
verified ·
1 Parent(s): 01e34e6

bind2_0: release package

Browse files

Frozen snapshot. Uploaded as a single commit per batch.

release/.gitattributes ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Release packages are byte-frozen snapshots: MANIFEST.json records a SHA-256 per artifact,
2
+ # and for some packages a preregistration pins a subset of them as well. Any end-of-line
3
+ # conversion on checkout silently changes those bytes and breaks every one of those hashes
4
+ # on the cloner's machine while still verifying on ours (this repo has core.autocrlf=true).
5
+ #
6
+ # -text disables all eol conversion. Do not remove.
7
+ * -text
release/BUILD.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Building `bind2_0`
2
+
3
+ **Verdict: BLOCKED** — **Does not run as-is.** The blockers are listed below and are real, not cosmetic. A launcher works around what can be worked around without editing frozen files; the rest is stated honestly rather than papered over.
4
+
5
+ ## Dependencies
6
+
7
+ ```
8
+ torch==2.12.1
9
+ numpy==2.4.6
10
+ tokenizers==0.22.2
11
+ flash-linear-attention==0.5.1 # CUDA-only (Triton kernels)
12
+ # on Windows also: triton-windows==3.7.1.post27
13
+ ```
14
+
15
+ Python 3.11 is the version everything here was developed and smoke-tested against.
16
+
17
+ ## Known blockers and portability notes
18
+
19
+ - train_bind2_0_babylm.py:20 reads os.environ['BABYLM_WORK'] as a bare subscript at module scope -- an uncaught KeyError with no default. The shipped launcher works around this without editing the file.
20
+ - dev='cuda' is hardcoded at :30 with no CPU path.
21
+ - flash-linear-attention is a hard dependency and its kernels are CUDA-only, so this package cannot run on CPU or on non-NVIDIA hardware at all.
22
+
23
+ ## Launcher
24
+
25
+ `run_train.py` exists because this package's trainer hardcodes machine-specific inputs inside files that are frozen and must not be edited, so the launcher supplies them from outside instead.
26
+
27
+ **How it does that differs by package, so read `run_train.py`'s own docstring rather than this paragraph.** Some launchers import the frozen modules and rebind module-level constants before the entry point runs; at least one cannot, because its trainer reads an environment variable at import time and rebinding the attribute afterwards is already too late -- that launcher sets the variable first. Each launcher documents its own mechanism, and each was checked against the source it works around.
28
+
29
+ ## Inputs you must supply
30
+
31
+ - `alaya-strict-small/tokens_u16.bin` (~32.6 MB) — status: REFERENCED-TOO-LARGE
32
+
33
+ ## Verification
34
+
35
+ Every `.py` in `src/` is byte-identical to the working-tree file recorded in `MANIFEST.json` and compiles under Python 3.11 (`python -m compileall src`).
release/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yulin Yang (SecludedCorner)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
release/MANIFEST.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "package": "bind2_0",
3
+ "spec_title": "bind2_0 \u2014 delta-rule backbone with a forced bottleneck",
4
+ "generation": "bind2_0",
5
+ "buildable": "BLOCKED",
6
+ "entries": [
7
+ {
8
+ "source": "pilot-4060/train_bind2_0_babylm.py",
9
+ "kind": "code",
10
+ "sha256": "c325148dbecc8da56faf6f94b9494f1910cd437bc16d4c0b340eccdd279fb0c8",
11
+ "bytes": 4138,
12
+ "source_commit": "c3d39af06e8ac30ad3d8c930548dc03db4698ffb",
13
+ "status": "COPIED",
14
+ "dest": "src/train_bind2_0_babylm.py"
15
+ },
16
+ {
17
+ "source": "pilot-4060/modeling_bind2_0.py",
18
+ "kind": "code",
19
+ "sha256": "1a950720257d560e02833b4c2e6c5017ee03a7585be6204c1b726930fd80e6ed",
20
+ "bytes": 5047,
21
+ "source_commit": "c3d39af06e8ac30ad3d8c930548dc03db4698ffb",
22
+ "status": "COPIED",
23
+ "dest": "src/modeling_bind2_0.py",
24
+ "source_sha256": "12ea39f0b958ae7ad304bbf4cc7a71afc984fe37c5805c97151084348a47f899",
25
+ "scrubbed": "1 text rule(s); AST verified: structure identical"
26
+ },
27
+ {
28
+ "source": "alaya-strict-small/tokenizer.json",
29
+ "kind": "data",
30
+ "sha256": "6582b724cb5196ca72baf2a6b91c17be2631901e93feadce1fcd74c8d5e9089a",
31
+ "bytes": 1096025,
32
+ "source_commit": "c3d39af06e8ac30ad3d8c930548dc03db4698ffb",
33
+ "status": "COPIED",
34
+ "dest": "data/tokenizer.json"
35
+ },
36
+ {
37
+ "source": "alaya-strict-small/tokens_u16.bin",
38
+ "kind": "external",
39
+ "sha256": "bce1a7681c6dc323ad0166219d63e8af01ae55a8c07d74d9774432d23b440c4e",
40
+ "bytes": 32592654,
41
+ "source_commit": "(untracked)",
42
+ "status": "REFERENCED-TOO-LARGE"
43
+ }
44
+ ]
45
+ }
release/README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # bind2_0 — delta-rule backbone with a forced bottleneck
2
+
3
+ Gated-delta-rule backbone; the parameter-matching reference for the bind2_1 arms.
4
+
5
+ > Research artifact. Not a competition entry.
6
+
7
+ ## What this is
8
+
9
+ A **frozen snapshot** of the code for one model generation. It is not a living repository: it is never updated after release. Files shared with another generation are duplicated here on purpose, so that the code closure is complete and nothing has to be fetched from a sibling package.
10
+
11
+ **Self-contained in code, not in data.** Every module the code imports ships here. Large or non-redistributable inputs -- corpora, official evaluation sets, token streams -- do not, and are listed under "Inputs you must supply" in `BUILD.md` with how to obtain or regenerate each. Do not read the completeness of `src/` as a claim that the package runs with no further downloads.
12
+
13
+ ## Contents
14
+
15
+ | | count |
16
+ |---|---|
17
+ | source files (`src/`) | 2 |
18
+ | shipped data (`data/`) | 1 |
19
+ | externally-obtained inputs | 1 |
20
+
21
+ `MANIFEST.json` records, for every artifact, its original path in the working tree, its SHA-256, its size, and the git commit it came from. That is the traceability record: it is what lets you prove which state of the code produced a published result.
22
+
23
+ ## External inputs (not redistributed here)
24
+
25
+ - `alaya-strict-small/tokens_u16.bin` — regenerable from the bind1 package's tokenizer chain
26
+
27
+ ## Building and running
28
+
29
+ See `BUILD.md`. It states plainly what works, what does not, and why.
30
+
31
+ ## Availability of this package
32
+
33
+ If a model card, paper or dataset page points at this package, that pointer must resolve for whoever reads it. A public card that links private code makes a reproducibility promise it cannot keep — and a licence line on unreachable code claims a release that has not happened. Before publishing any pointer to this package, confirm the package is actually reachable by the audience that will read the pointer.
34
+
35
+ ## Licence
36
+
37
+ MIT (see `LICENSE`). Code only — model weights and evaluation artifacts are distributed separately and carry their own terms.
release/data/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
release/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch==2.12.1
2
+ numpy==2.4.6
3
+ tokenizers==0.22.2
4
+ flash-linear-attention==0.5.1 # CUDA-only (Triton kernels)
5
+ # on Windows also: triton-windows==3.7.1.post27
release/run_train.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Portable launcher for the bind2_0 trainer.
3
+
4
+ WHY THIS EXISTS
5
+ ---------------
6
+ `src/train_bind2_0_babylm.py` line 20 is:
7
+
8
+ WORK = os.environ["BABYLM_WORK"]
9
+
10
+ a bare subscript at module scope with no default, so merely IMPORTING the module raises an
11
+ uncaught KeyError when the variable is unset — the failure a newcomer hits first, with no message
12
+ explaining what to set. It is read at import time, not call time, so it must be set BEFORE the
13
+ import; rebinding the attribute afterwards is too late. This launcher sets it, checks the two
14
+ files it derives (tokenizer.json and tokens_u16.bin), and reports clearly if they are absent.
15
+
16
+ The trainer takes bare positional sys.argv (line 26), not argparse, so arguments are passed
17
+ through verbatim in order.
18
+
19
+ python run_train.py --work <dir> -- <positional args for train_bind2_0_babylm.py>
20
+ python run_train.py --work <dir> --check
21
+
22
+ HONEST LIMITATION, not worked around: line 30 hardcodes dev = "cuda" with no CPU path, and
23
+ modeling_bind2_0.py imports flash-linear-attention, whose kernels are CUDA-only. This package
24
+ cannot run on CPU or on non-NVIDIA hardware. A launcher cannot fix that; only editing the frozen
25
+ source could, and that is deliberately not done here.
26
+ """
27
+ from __future__ import annotations
28
+ import argparse
29
+ import os
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ HERE = Path(__file__).resolve().parent
34
+
35
+
36
+ def main() -> int:
37
+ ap = argparse.ArgumentParser(description=__doc__,
38
+ formatter_class=argparse.RawDescriptionHelpFormatter)
39
+ ap.add_argument("--work", default=os.environ.get("BABYLM_WORK", str(HERE / "data")),
40
+ help="directory holding tokenizer.json and tokens_u16.bin")
41
+ ap.add_argument("--check", action="store_true", help="resolve inputs and exit")
42
+ args, passthrough = ap.parse_known_args()
43
+ if passthrough and passthrough[0] == "--":
44
+ passthrough = passthrough[1:]
45
+
46
+ work = Path(args.work)
47
+ os.environ["BABYLM_WORK"] = str(work) # MUST precede the import
48
+ sys.path.insert(0, str(HERE / "src"))
49
+
50
+ need = {"tokenizer.json": work / "tokenizer.json", "tokens_u16.bin": work / "tokens_u16.bin"}
51
+ missing = [k for k, v in need.items() if not v.exists()]
52
+ if args.check or missing:
53
+ print(f" BABYLM_WORK = {work}")
54
+ for k, v in need.items():
55
+ print(f" {k:16} {'OK ' if v.exists() else 'MISSING'} {v}")
56
+ if missing:
57
+ print("\nERROR: tokens_u16.bin is not redistributed inside this package (it is large and "
58
+ "regenerable). Build it with the bind1 package's tokenizer chain "
59
+ "(train_tokenizer.py then make_tokens.py), or point --work at a directory that "
60
+ "already has it. See BUILD.md.", file=sys.stderr)
61
+ return 2
62
+ if args.check:
63
+ print("\nAll inputs resolve. Re-run without --check to train.")
64
+ return 0
65
+
66
+ import train_bind2_0_babylm as T
67
+ sys.argv = ["train_bind2_0_babylm.py"] + passthrough
68
+ T.main()
69
+ return 0
70
+
71
+
72
+ if __name__ == "__main__":
73
+ raise SystemExit(main())
release/src/modeling_bind2_0.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """modeling_bind2_0.py -- bind2_0 architecture: forced-bottleneck delta-overwrite recurrence
2
+ (2026-07-12).
3
+
4
+ d=384, L=12, 3:1 GatedDeltaNet:chunked-attn (9 GDN + 3 chunked-attn), ~24M (matched to bind1 23.9M).
5
+ - GDN layer (fla GatedDeltaNet): delta-rule state S_t = a_t S_{t-1}(I - b_t k k^T) + b_t v k^T,
6
+ o_t = S_t q_t. Per-token state UPDATE carried along the sequence = the "apply an operation to
7
+ update state" mechanism (the verb-dynamics bind1 lacks; bind1 only labels roles).
8
+ - CHUNKED attention (forced bottleneck, P0): strict intra-chunk, NON-overlapping mask
9
+ M[t,s]=1 iff floor(t/C)==floor(s/C) and s<=t. Cross-chunk info can flow ONLY through S.
10
+ - Unified readout: LM head on the residual stream after all blocks (GDN o_t already added to
11
+ residual) -> every position, no hard routing.
12
+ """
13
+ import math
14
+ import torch, torch.nn as nn, torch.nn.functional as F
15
+ from fla.layers import GatedDeltaNet
16
+
17
+ # --- self-contained generic primitives (bind2_0 is architecturally independent of bind1/mono;
18
+ # these are standard transformer building blocks, copied here so modeling_bind2_0.py stands alone) ---
19
+
20
+ def build_rope(T, D, device, base=10000.0):
21
+ inv = 1.0 / (base ** (torch.arange(0, D, 2, device=device).float() / D))
22
+ t = torch.arange(T, device=device).float()
23
+ f = torch.outer(t, inv)
24
+ emb = torch.cat([f, f], dim=-1)
25
+ return emb.cos(), emb.sin()
26
+
27
+
28
+ def rotate_half(x):
29
+ x1, x2 = x.chunk(2, dim=-1)
30
+ return torch.cat((-x2, x1), dim=-1)
31
+
32
+
33
+ def apply_rope(x, cos, sin):
34
+ cos = cos[None, None]; sin = sin[None, None]
35
+ return x * cos + rotate_half(x) * sin
36
+
37
+
38
+ class SwiGLU(nn.Module):
39
+ def __init__(self, d, h):
40
+ super().__init__()
41
+ self.w1 = nn.Linear(d, h, bias=False); self.w3 = nn.Linear(d, h, bias=False); self.w2 = nn.Linear(h, d, bias=False)
42
+
43
+ def forward(self, x):
44
+ return self.w2(F.silu(self.w1(x)) * self.w3(x))
45
+
46
+
47
+ def _hdim(d):
48
+ return ((int(8 / 3 * d) + 63) // 64) * 64
49
+
50
+
51
+ class ChunkedAttn(nn.Module):
52
+ """Forced bottleneck: causal attention restricted to within non-overlapping chunks of size C."""
53
+ def __init__(self, d, nh, chunk):
54
+ super().__init__()
55
+ self.nh = nh; self.hd = d // nh; self.chunk = chunk
56
+ self.qkv = nn.Linear(d, 3 * d, bias=False); self.o = nn.Linear(d, d, bias=False)
57
+
58
+ def forward(self, x, cos, sin):
59
+ B, T, D = x.shape
60
+ qkv = self.qkv(x).view(B, T, 3, self.nh, self.hd).permute(2, 0, 3, 1, 4)
61
+ q, k, v = qkv[0], qkv[1], qkv[2]
62
+ q = apply_rope(q, cos, sin); k = apply_rope(k, cos, sin)
63
+ idx = torch.arange(T, device=x.device)
64
+ same = (idx[:, None] // self.chunk) == (idx[None, :] // self.chunk)
65
+ causal = idx[:, None] >= idx[None, :]
66
+ keep = same & causal
67
+ mask = torch.zeros(T, T, device=x.device, dtype=q.dtype).masked_fill(~keep, float("-inf"))
68
+ o = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
69
+ return self.o(o.transpose(1, 2).reshape(B, T, D))
70
+
71
+
72
+ class GDNBlock(nn.Module):
73
+ def __init__(self, d, idx, mlp_hidden, gdn_heads=4, gdn_hd=72):
74
+ super().__init__()
75
+ self.n1 = nn.RMSNorm(d)
76
+ self.gdn = GatedDeltaNet(hidden_size=d, num_heads=gdn_heads, head_dim=gdn_hd, layer_idx=idx)
77
+ self.n2 = nn.RMSNorm(d); self.mlp = SwiGLU(d, mlp_hidden)
78
+
79
+ def forward(self, x):
80
+ m = self.gdn(self.n1(x))[0] # fla returns (output, attn, cache)
81
+ x = x + m
82
+ return x + self.mlp(self.n2(x))
83
+
84
+
85
+ class AttnBlock(nn.Module):
86
+ def __init__(self, d, nh, chunk, mlp_hidden):
87
+ super().__init__()
88
+ self.n1 = nn.RMSNorm(d); self.attn = ChunkedAttn(d, nh, chunk)
89
+ self.n2 = nn.RMSNorm(d); self.mlp = SwiGLU(d, mlp_hidden)
90
+
91
+ def forward(self, x, cos, sin):
92
+ x = x + self.attn(self.n1(x), cos, sin)
93
+ return x + self.mlp(self.n2(x))
94
+
95
+
96
+ class Bind2_0LM(nn.Module):
97
+ def __init__(self, vocab, d=384, depth=12, nh=6, chunk=32, mlp_hidden=576, gdn_heads=4, gdn_hd=72):
98
+ super().__init__()
99
+ self.emb = nn.Embedding(vocab, d)
100
+ self.kinds = ["attn" if (i + 1) % 4 == 0 else "gdn" for i in range(depth)] # 3:1 GDN:attn
101
+ self.blocks = nn.ModuleList([
102
+ GDNBlock(d, i, mlp_hidden, gdn_heads, gdn_hd) if k == "gdn" else AttnBlock(d, nh, chunk, mlp_hidden)
103
+ for i, k in enumerate(self.kinds)])
104
+ self.nf = nn.RMSNorm(d); self.head = nn.Linear(d, vocab, bias=False)
105
+ self.head.weight = self.emb.weight
106
+ self.d = d; self.nh = nh; self.chunk = chunk
107
+ nl = depth
108
+ for m in self.modules():
109
+ if isinstance(m, nn.Linear):
110
+ nn.init.normal_(m.weight, std=0.02)
111
+ nn.init.normal_(self.emb.weight, std=0.02)
112
+
113
+ def forward(self, ids):
114
+ cos, sin = build_rope(ids.shape[1], self.d // self.nh, ids.device)
115
+ h = self.emb(ids)
116
+ for blk, k in zip(self.blocks, self.kinds):
117
+ h = blk(h) if k == "gdn" else blk(h, cos, sin)
118
+ return self.head(self.nf(h))
release/src/train_bind2_0_babylm.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """train_bind2_0_babylm.py -- train bind2_0 on the REAL BabyLM strict-small corpus (2026-07-12),
2
+ matched to bind1/mono's regime (SEQ256, batch, 150M tokens, flat-chunk of tokens_u16.bin).
3
+
4
+ CONTAMINATION-SAFE: each SEQ-block is a separate batch element, so the GDN state S starts from 0
5
+ per block (no cross-block carry) -- same data regime as the stateless bind1/mono flat-chunk runs.
6
+ Eval/inference forwards each probe independently (S from 0), so items never influence each other.
7
+
8
+ Usage: python train_bind2_0_babylm.py DIM DEPTH CHUNK SEQ BATCH TARGET TAG
9
+ Reads BABYLM_WORK/tokens_u16.bin (the real 16.3M-token BabyLM corpus). Writes {TAG}_final.pt +
10
+ train_{TAG}.json (arch bind2_0) + grid ckpts at BABYLM_CKPT_MARKS.
11
+ """
12
+ import os, sys, time, json, random
13
+ import numpy as np
14
+ import torch, torch.nn.functional as F
15
+ from tokenizers import Tokenizer
16
+
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+ from modeling_bind2_0 import Bind2_0LM
19
+
20
+ WORK = os.environ["BABYLM_WORK"]
21
+ TOKJSON = os.path.join(WORK, "tokenizer.json")
22
+ TOKBIN = os.path.join(WORK, "tokens_u16.bin")
23
+
24
+
25
+ def main():
26
+ a = sys.argv
27
+ DIM, DEPTH, CHUNK = int(a[1]), int(a[2]), int(a[3])
28
+ SEQ, BATCH, TARGET, TAG = int(a[4]), int(a[5]), int(a[6]), a[7]
29
+ MLPH = int(a[8]) if len(a) > 8 else 576 # 832 => ~27.4M (BabyLM scale, matches mono); 576 => 23.9M (bind1 scale)
30
+ dev = "cuda"
31
+ SEED = int(os.environ.get("BABYLM_SEED", "0")); random.seed(SEED); torch.manual_seed(SEED); np.random.seed(SEED)
32
+ tok = Tokenizer.from_file(TOKJSON); V = tok.get_vocab_size()
33
+ toks = np.fromfile(TOKBIN, dtype=np.uint16).astype(np.int64); data = torch.from_numpy(toks)
34
+ nblk = (len(toks) - 1) // SEQ
35
+ assert nblk > 0
36
+
37
+ model = Bind2_0LM(V, DIM, DEPTH, 6, chunk=CHUNK, mlp_hidden=MLPH).to(dev).train()
38
+ P = sum(p.numel() for p in model.parameters())
39
+ print(f"bind2_0 params={P/1e6:.2f}M | REAL-BabyLM flat-chunk seq{SEQ}xb{BATCH} chunk{CHUNK} (S from 0 per block)", flush=True)
40
+ opt = torch.optim.AdamW(model.parameters(), lr=6e-4, betas=(0.9, 0.95), weight_decay=0.01)
41
+ g = torch.Generator().manual_seed(SEED)
42
+ torch.cuda.reset_peak_memory_stats(); t0 = time.time()
43
+ MARKS = sorted(int(float(m) * 1e6) for m in os.environ.get("BABYLM_CKPT_MARKS", "").split(",") if m.strip())
44
+ mi = 0; seen = 0; step = 0
45
+ while seen < TARGET:
46
+ idx = torch.randint(0, nblk, (BATCH,), generator=g)
47
+ x = torch.stack([data[i * SEQ:(i + 1) * SEQ + 1] for i in idx]).to(dev)
48
+ with torch.autocast("cuda", dtype=torch.bfloat16):
49
+ logits = model(x[:, :-1])
50
+ ce = F.cross_entropy(logits.reshape(-1, V).float(), x[:, 1:].reshape(-1))
51
+ opt.zero_grad(set_to_none=True); ce.backward()
52
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step()
53
+ step += 1; seen += BATCH * SEQ
54
+ while mi < len(MARKS) and seen >= MARKS[mi]:
55
+ os.makedirs(WORK + "/ckpt", exist_ok=True)
56
+ torch.save(model.state_dict(), WORK + f"/ckpt/{TAG}_g{MARKS[mi]/1e6:g}M.pt")
57
+ print(f" [ckpt-grid] {TAG}_g{MARKS[mi]/1e6:g}M.pt (seen={seen})", flush=True); mi += 1
58
+ if step == 1 or step % 200 == 0:
59
+ print(f"[{TAG}] st {step:6d} {seen/1e6:.1f}M ce {ce.item():.3f} ppl {np.exp(min(ce.item(),20)):.1f} | {round(seen/(time.time()-t0))} tok/s", flush=True)
60
+ dt = time.time() - t0
61
+ summ = {"tag": TAG, "arch": "bind2_0", "dim": DIM, "depth": DEPTH, "chunk": CHUNK, "mlp_hidden": MLPH,
62
+ "params_M": round(P / 1e6, 2), "vocab": V, "seq": SEQ, "batch": BATCH,
63
+ "peak_MiB": round(torch.cuda.max_memory_allocated() / 2**20), "tokens": seen,
64
+ "tok_s": round(seen / dt), "final_ce": round(ce.item(), 4), "ckpt_marks": os.environ.get("BABYLM_CKPT_MARKS", "")}
65
+ json.dump(summ, open(WORK + f"/train_{TAG}.json", "w", encoding="utf-8"), indent=2)
66
+ torch.save(model.state_dict(), WORK + f"/{TAG}_final.pt")
67
+ print(f"[{TAG}] DONE bind2_0 {P/1e6:.1f}M {seen/1e6:.1f}M ce {ce.item():.3f} ppl {np.exp(min(ce.item(),20)):.1f} {summ['tok_s']} tok/s peak {summ['peak_MiB']}MiB", flush=True)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()