SURELYNOTVAIN commited on
Commit
11e2c7c
·
verified ·
1 Parent(s): bedb850

Add MP-01 (9500) 100M base: weights, config, tokenizer, model code, model card and eval results

Browse files
Files changed (13) hide show
  1. README.md +108 -0
  2. config.json +24 -0
  3. eval_results.json +99 -0
  4. model.safetensors +3 -0
  5. tokenizer.json +0 -0
  6. vbq/__init__.py +7 -0
  7. vbq/chat.py +65 -0
  8. vbq/data.py +92 -0
  9. vbq/evals.py +88 -0
  10. vbq/fastce.py +117 -0
  11. vbq/model.py +357 -0
  12. vbq/optim.py +55 -0
  13. vbq/sources.py +171 -0
README.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: to-be-decided
4
+ language:
5
+ - en
6
+ library_name: vbq
7
+ pipeline_tag: text-generation
8
+ tags:
9
+ - base-model
10
+ - pretraining
11
+ - decoder-only
12
+ - small-language-model
13
+ ---
14
+
15
+ # MP-01 (9500) 100M base
16
+
17
+ MP-01 (9500) 100M base is a 100M-parameter decoder-only language model from Manprot, grown from a 75M base and pretrained further on new English data.
18
+ This is the **base model** (no chat or instruction tuning). It continues text; it does not follow instructions.
19
+
20
+ - **Author:** Surelynotvain (organisation: Manprot)
21
+ - **Checkpoint:** training step 9500 of 34341 (expansion run `vbq-100m-expand`)
22
+ - **Parameters:** 100.6M (tied input/output embeddings)
23
+
24
+ ## Architecture
25
+
26
+ | | |
27
+ |---|---|
28
+ | Layers | 18 |
29
+ | Model width (d_model) | 640 |
30
+ | Attention heads / KV heads | 10 / 5 (grouped-query attention, QK-norm) |
31
+ | MLP hidden | 1664 (SwiGLU) |
32
+ | Vocabulary | 32768 (byte-level BPE, `tokenizer.json`) |
33
+ | Context length | 2048 tokens (RoPE) |
34
+ | Normalisation | RMSNorm |
35
+
36
+ This is a custom architecture, not a `transformers` model. Load it with the `vbq/` code in this repository.
37
+
38
+ ## Training
39
+
40
+ - Started from a 75M-parameter base, grown to 18 layers, then continued on new English data.
41
+ - Optimizer: Muon. Peak learning rate 1.2e-3, batch 262,144 tokens, weight decay 0.1, z-loss 1e-4.
42
+ - Sequence length 1,024 tokens for the first part, 2,048 from step 24,038 (not reached yet).
43
+ - Data mixture of this stage (share of tokens): FineWeb-Edu 34%, Wikipedia 24%, Cosmopedia 18%,
44
+ DCLM 12%, FineMath 5%, Stack Exchange 3%, Python code 3%.
45
+ - Expansion steps completed when this checkpoint was saved: 9500 of 34341
46
+ (about 1.88B tokens in this run).
47
+
48
+ ## Evaluation
49
+
50
+ Scored on this repository's own harness (`scripts/eval_checkpoint.py`), zero-shot.
51
+
52
+ Benchmark tasks use 500 questions each (the same sample every time). With 500 questions per task, a
53
+ difference under about 2 points is within noise.
54
+
55
+ | Task | Score |
56
+ |---|---:|
57
+ | arc_challenge | 27.6 |
58
+ | arc_easy | 52.8 |
59
+ | hellaswag | 29.8 |
60
+ | openbookqa | 27.6 |
61
+ | piqa | 62.4 |
62
+ | sciq | 71.0 |
63
+ | winogrande | 51.6 |
64
+ | **Average (7 tasks)** | **46.11** |
65
+ | **BLiMP (67 paradigms, grammatical vs ungrammatical minimal pairs)** | **76.88** |
66
+
67
+ Validation loss (held-out text, nats per token): **2.7780** overall.
68
+
69
+ | Source | Val loss |
70
+ |---|---:|
71
+ | fineweb_edu | 2.941 |
72
+ | wikipedia | 2.714 |
73
+ | cosmopedia | 2.183 |
74
+ | dclm | 3.387 |
75
+ | stackexchange | 2.566 |
76
+ | finemath | 3.402 |
77
+ | code_python | 1.823 |
78
+
79
+ ## Limitations
80
+
81
+ - Base model: it has no instruction or chat tuning and can produce factually wrong or repetitive text.
82
+ - Small size (100M parameters) and small benchmarks; these numbers are indicative only.
83
+ - Trained on web and educational text that may contain bias and errors.
84
+ - The training data is not released with this model.
85
+
86
+ ## Loading
87
+
88
+ ```python
89
+ import torch
90
+ from tokenizers import Tokenizer
91
+ from vbq.model import VBQ, VBQConfig
92
+ from safetensors.torch import load_file
93
+ import json
94
+
95
+ cfg = VBQConfig(**json.load(open("config.json"))["model_cfg"])
96
+ model = VBQ(cfg)
97
+ model.load_state_dict(load_file("model.safetensors")) # the tied output weight is stored once
98
+ model.eval()
99
+ tok = Tokenizer.from_file("tokenizer.json")
100
+ ```
101
+
102
+ ## Repository contents
103
+
104
+ - `model.safetensors`: weights (bf16)
105
+ - `config.json`: architecture and training configuration
106
+ - `tokenizer.json`: tokenizer
107
+ - `vbq/`: model code
108
+ - `eval_results.json`: full evaluation output for this checkpoint
config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "vbq",
3
+ "architectures": [
4
+ "VBQ"
5
+ ],
6
+ "model_cfg": {
7
+ "vocab_size": 32768,
8
+ "d_model": 640,
9
+ "n_layers": 18,
10
+ "n_heads": 10,
11
+ "n_kv_heads": 5,
12
+ "mlp_hidden": 1664,
13
+ "max_seq_len": 2048,
14
+ "rope_theta": 10000.0,
15
+ "norm_eps": 1e-05,
16
+ "qk_norm": true,
17
+ "tie_embeddings": true,
18
+ "n_experts": 0,
19
+ "capacity_factor": 1.25
20
+ },
21
+ "train_step": 9500,
22
+ "total_steps": 34341,
23
+ "display_name": "MP-01 (9500) 100M base"
24
+ }
eval_results.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ckpt": "step_009500.pt",
3
+ "step": 9500,
4
+ "val": {
5
+ "fineweb_edu": 2.9408107101917267,
6
+ "wikipedia": 2.713771253824234,
7
+ "cosmopedia": 2.1831925809383392,
8
+ "dclm": 3.386944741010666,
9
+ "stackexchange": 2.5659963488578796,
10
+ "finemath": 3.4021530747413635,
11
+ "code_python": 1.8225234597921371,
12
+ "overall": 2.7780119873583318
13
+ },
14
+ "bench": {
15
+ "arc_challenge": 27.6,
16
+ "arc_easy": 52.8,
17
+ "hellaswag": 29.8,
18
+ "openbookqa": 27.6,
19
+ "piqa": 62.4,
20
+ "sciq": 71.0,
21
+ "winogrande": 51.6
22
+ },
23
+ "bench_avg": 46.114285714285714,
24
+ "blimp": {
25
+ "per_paradigm": {
26
+ "adjunct_island": 82.9,
27
+ "anaphor_gender_agreement": 92.3,
28
+ "anaphor_number_agreement": 97.3,
29
+ "animate_subject_passive": 75.9,
30
+ "animate_subject_trans": 83.8,
31
+ "causative": 75.4,
32
+ "complex_NP_island": 50.3,
33
+ "coordinate_structure_constraint_complex_left_branch": 53.5,
34
+ "coordinate_structure_constraint_object_extraction": 75.9,
35
+ "determiner_noun_agreement_1": 98.2,
36
+ "determiner_noun_agreement_2": 97.8,
37
+ "determiner_noun_agreement_irregular_1": 90.6,
38
+ "determiner_noun_agreement_irregular_2": 95.9,
39
+ "determiner_noun_agreement_with_adj_2": 93.3,
40
+ "determiner_noun_agreement_with_adj_irregular_1": 90.1,
41
+ "determiner_noun_agreement_with_adj_irregular_2": 89.2,
42
+ "determiner_noun_agreement_with_adjective_1": 93.8,
43
+ "distractor_agreement_relational_noun": 84.0,
44
+ "distractor_agreement_relative_clause": 77.5,
45
+ "drop_argument": 76.5,
46
+ "ellipsis_n_bar_1": 83.8,
47
+ "ellipsis_n_bar_2": 87.6,
48
+ "existential_there_object_raising": 79.5,
49
+ "existential_there_quantifiers_1": 97.3,
50
+ "existential_there_quantifiers_2": 23.3,
51
+ "existential_there_subject_raising": 85.5,
52
+ "expletive_it_object_raising": 78.4,
53
+ "inchoative": 63.4,
54
+ "intransitive": 73.4,
55
+ "irregular_past_participle_adjectives": 90.8,
56
+ "irregular_past_participle_verbs": 92.5,
57
+ "irregular_plural_subject_verb_agreement_1": 88.8,
58
+ "irregular_plural_subject_verb_agreement_2": 91.9,
59
+ "left_branch_island_echo_question": 45.0,
60
+ "left_branch_island_simple_question": 54.8,
61
+ "matrix_question_npi_licensor_present": 44.7,
62
+ "npi_present_1": 55.7,
63
+ "npi_present_2": 60.9,
64
+ "only_npi_licensor_present": 62.0,
65
+ "only_npi_scope": 47.4,
66
+ "passive_1": 88.4,
67
+ "passive_2": 86.0,
68
+ "principle_A_c_command": 63.5,
69
+ "principle_A_case_1": 100.0,
70
+ "principle_A_case_2": 95.5,
71
+ "principle_A_domain_1": 96.2,
72
+ "principle_A_domain_2": 74.7,
73
+ "principle_A_domain_3": 57.6,
74
+ "principle_A_reconstruction": 36.3,
75
+ "regular_plural_subject_verb_agreement_1": 92.4,
76
+ "regular_plural_subject_verb_agreement_2": 91.7,
77
+ "sentential_negation_npi_licensor_present": 96.8,
78
+ "sentential_negation_npi_scope": 68.9,
79
+ "sentential_subject_island": 30.2,
80
+ "superlative_quantifiers_1": 91.8,
81
+ "superlative_quantifiers_2": 80.3,
82
+ "tough_vs_raising_1": 64.3,
83
+ "tough_vs_raising_2": 86.2,
84
+ "transitive": 86.9,
85
+ "wh_island": 70.5,
86
+ "wh_questions_object_gap": 75.6,
87
+ "wh_questions_subject_gap": 90.7,
88
+ "wh_questions_subject_gap_long_distance": 81.2,
89
+ "wh_vs_that_no_gap": 97.1,
90
+ "wh_vs_that_no_gap_long_distance": 96.4,
91
+ "wh_vs_that_with_gap": 46.1,
92
+ "wh_vs_that_with_gap_long_distance": 24.8
93
+ },
94
+ "paradigm_avg": 76.88059701492537,
95
+ "overall": 76.88059701492537,
96
+ "pairs": 67000
97
+ },
98
+ "seconds": 124
99
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:621d2129dd34133fa4fcb623db7ff05d94755e0271eba4473e7625cdf8e84030
3
+ size 201265144
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
vbq/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """VBQ — a 75M-parameter decoder-only language model by Voussoir AI."""
2
+ import os
3
+
4
+ # Flash / mem-efficient SDPA kernels on RDNA3 are gated behind this flag in PyTorch's ROCm build.
5
+ # They are ~10x faster than the math fallback here, so enable them before torch is imported.
6
+ os.environ.setdefault("TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL", "1")
7
+ os.environ.setdefault("PYTORCH_HIP_ALLOC_CONF", "expandable_segments:True")
vbq/chat.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared ChatML tokens, assistant supervision and context budgeting."""
2
+ import os
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+ from tokenizers import Tokenizer
6
+
7
+ ROOT = Path(__file__).resolve().parent.parent
8
+ SPECIALS = ('<|endoftext|>', '<|pad|>', '<|im_start|>', '<|im_end|>', '<|system|>', '<|user|>', '<|assistant|>', '<|reserved|>')
9
+ STOP_IDS = {0, 3}
10
+
11
+
12
+ @lru_cache(maxsize=1)
13
+ def tokenizer():
14
+ return Tokenizer.from_file(str(ROOT / os.environ.get('VBQ_TOKENIZER', 'data/tokenizer/vbq-tokenizer.json')))
15
+
16
+
17
+ def render(messages, add_generation_prompt=False):
18
+ ids, mask = [], []
19
+ tok = tokenizer()
20
+ for m in messages:
21
+ role, content = m['role'], m['content']
22
+ if role not in ('system', 'user', 'assistant') or not isinstance(content, str):
23
+ raise ValueError('messages must contain system/user/assistant roles and string content')
24
+ if any(s in content for s in SPECIALS):
25
+ raise ValueError('message content cannot contain reserved tokens')
26
+ header = tok.encode(f'<|im_start|>{role}\n', add_special_tokens=False).ids
27
+ body = tok.encode(content, add_special_tokens=False).ids
28
+ tail = tok.encode('\n', add_special_tokens=False).ids
29
+ ids.extend(header + body + [3] + tail)
30
+ mask.extend([0] * len(header) + [int(role == 'assistant')] * (len(body) + 1) + [0] * len(tail))
31
+ if add_generation_prompt:
32
+ header = tok.encode('<|im_start|>assistant\n', add_special_tokens=False).ids
33
+ ids.extend(header); mask.extend([0] * len(header))
34
+ return ids, mask
35
+
36
+
37
+ def fit_history(messages, budget):
38
+ history = [dict(m) for m in messages]
39
+ if not history or history[-1]['role'] != 'user':
40
+ raise ValueError('chat must end with a user message')
41
+ if any(m['role'] == 'system' for m in history[1:]):
42
+ raise ValueError('system message must be first')
43
+ start = int(history[0]['role'] == 'system')
44
+ while True:
45
+ ids, _ = render(history, add_generation_prompt=True)
46
+ if len(ids) <= budget: return history, ids
47
+ if len(history) <= start + 1:
48
+ raise ValueError('system prompt and latest message exceed the context budget')
49
+ end = start + 1
50
+ while end < len(history) - 1 and history[end]['role'] != 'user': end += 1
51
+ del history[start:end]
52
+
53
+
54
+ def stream_reply(model, messages, max_new=256, temperature=0.7, top_p=0.9, top_k=50, rep_penalty=1.1):
55
+ import torch
56
+ _, ids = fit_history(messages, model.c.max_seq_len - max_new)
57
+ out, sent = [], ''
58
+ for t in model.generate_cached(torch.tensor([ids], device=next(model.parameters()).device), max_new_tokens=max_new,
59
+ temperature=temperature, top_p=top_p, top_k=top_k, repetition_penalty=rep_penalty, eos_id=STOP_IDS):
60
+ if t in STOP_IDS: break
61
+ out.append(t); text = tokenizer().decode(out)
62
+ if text.endswith('�'): continue
63
+ yield text[len(sent):]; sent = text
64
+ text = tokenizer().decode(out)
65
+ if len(text) > len(sent): yield text[len(sent):]
vbq/data.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic, resumable batch loader over the packed token corpus.
2
+
3
+ The corpus is one flat uint16 array in which every source already holds exactly its mixture share.
4
+ It is cut into 2048-token blocks and visited once in a fixed random order, so a single pass
5
+ reproduces the intended mix with no repetition. Step N always sees the same blocks (resume-safe).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import queue
11
+ import threading
12
+ from pathlib import Path
13
+
14
+ import numpy as np
15
+ import torch
16
+
17
+ BLOCK = 2048
18
+
19
+
20
+ class BlockLoader:
21
+ def __init__(self, data_dir: Path, seed: int = 1337, corpus_json: str = "corpus.json", train_file: str = "train.bin"):
22
+ self.dir = Path(data_dir)
23
+ self.meta = json.loads((self.dir / corpus_json).read_text())
24
+ self.mm = np.memmap(self.dir / train_file, dtype=np.uint16, mode="r")
25
+ self.n_blocks = (len(self.mm) - 1) // BLOCK
26
+ self.perm = np.random.default_rng(seed).permutation(self.n_blocks).astype(np.int64)
27
+
28
+ def batch(self, step: int, blocks_per_step: int, seq_len: int) -> torch.Tensor:
29
+ """Returns int64 [blocks_per_step * (BLOCK // seq_len), seq_len + 1]: inputs and shifted targets in one array."""
30
+ ids = self.perm[(np.arange(blocks_per_step) + step * blocks_per_step) % self.n_blocks]
31
+ out = np.empty((blocks_per_step, BLOCK // seq_len, seq_len + 1), dtype=np.int64)
32
+ for i, b in enumerate(ids):
33
+ w = self.mm[b * BLOCK : b * BLOCK + BLOCK + 1].astype(np.int64)
34
+ for j in range(BLOCK // seq_len):
35
+ out[i, j] = w[j * seq_len : j * seq_len + seq_len + 1]
36
+ return torch.from_numpy(out.reshape(-1, seq_len + 1))
37
+
38
+
39
+ class StagedLoader:
40
+ """first loader for steps < from_step, then a second corpus from its own beginning (opened when first needed)."""
41
+
42
+ def __init__(self, first: BlockLoader, from_step: int, second_dir: Path, seed: int):
43
+ self.first, self.from_step, self.second_dir, self.seed = first, from_step, Path(second_dir), seed
44
+ self.second, self.n_blocks, self.meta = None, first.n_blocks, first.meta
45
+
46
+ def ready(self) -> bool:
47
+ return (self.second_dir / "corpus.json").exists()
48
+
49
+ def batch(self, step: int, blocks_per_step: int, seq_len: int) -> torch.Tensor:
50
+ if step < self.from_step: return self.first.batch(step, blocks_per_step, seq_len)
51
+ if self.second is None: self.second = BlockLoader(self.second_dir, seed=self.seed)
52
+ return self.second.batch(step - self.from_step, blocks_per_step, seq_len)
53
+
54
+
55
+ class Prefetcher:
56
+ """Background thread that prepares upcoming steps' batches."""
57
+
58
+ def __init__(self, loader: BlockLoader, start_step: int, blocks_per_step: int, seq_len_fn, depth: int = 3):
59
+ self.q: queue.Queue = queue.Queue(maxsize=depth)
60
+ self.loader, self.bps, self.seq_len_fn = loader, blocks_per_step, seq_len_fn
61
+ self.step = start_step
62
+ self.stop = False
63
+ self.t = threading.Thread(target=self._run, daemon=True)
64
+ self.t.start()
65
+
66
+ def _run(self):
67
+ s = self.step
68
+ while not self.stop:
69
+ T = self.seq_len_fn(s)
70
+ b = self.loader.batch(s, self.bps, T).pin_memory()
71
+ self.q.put((s, T, b))
72
+ s += 1
73
+
74
+ def get(self):
75
+ return self.q.get()
76
+
77
+
78
+ class ValSet:
79
+ """Fixed held-out token windows, overall and per source."""
80
+
81
+ def __init__(self, data_dir: Path, seq_len: int = 2048, per_source_windows: int = 64):
82
+ meta = json.loads((Path(data_dir) / "corpus.json").read_text())
83
+ mm = np.memmap(Path(data_dir) / "val.bin", dtype=np.uint16, mode="r")
84
+ self.sets: dict[str, torch.Tensor] = {}
85
+ for name, s in meta["sources"].items():
86
+ n = (s["val_tokens"] - 1) // seq_len
87
+ if n <= 0:
88
+ continue
89
+ take = min(n, per_source_windows)
90
+ starts = np.linspace(0, n - 1, take).astype(int)
91
+ arr = np.stack([mm[s["val_start"] + i * seq_len : s["val_start"] + i * seq_len + seq_len + 1].astype(np.int64) for i in starts])
92
+ self.sets[name] = torch.from_numpy(arr)
vbq/evals.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Zero-shot multiple-choice evaluation (lm-evaluation-harness conventions) that fits next to a training job."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import random
6
+ import re
7
+ from pathlib import Path
8
+
9
+ import torch
10
+
11
+ EVAL_DIR = Path(__file__).resolve().parent.parent / "data" / "evals"
12
+ # chance level, for the dashboard
13
+ CHANCE = {"hellaswag": 25.0, "arc_easy": 25.0, "arc_challenge": 25.0, "piqa": 50.0, "sciq": 25.0, "openbookqa": 25.0, "winogrande": 50.0}
14
+ METRIC = {"hellaswag": "acc_norm", "arc_easy": "acc", "arc_challenge": "acc_norm", "piqa": "acc_norm", "sciq": "acc_norm", "openbookqa": "acc_norm", "winogrande": "acc"}
15
+
16
+
17
+ def load_tasks(limit: int | None = None, seed: int = 0) -> dict[str, list[dict]]:
18
+ tasks = {}
19
+ for f in sorted(EVAL_DIR.glob("*.json")):
20
+ items = json.loads(f.read_text())
21
+ if limit and len(items) > limit:
22
+ items = random.Random(seed).sample(items, limit)
23
+ tasks[f.stem] = items
24
+ return tasks
25
+
26
+
27
+ @torch.no_grad()
28
+ def loglik_batch(model, pairs, device, pad_id=1, max_ctx=480, bs=48):
29
+ """pairs: list of (ctx_ids, cont_ids). Returns (sum logprob, n_tokens) per pair."""
30
+ res = [None] * len(pairs)
31
+ order = sorted(range(len(pairs)), key=lambda i: len(pairs[i][0]) + len(pairs[i][1]))
32
+ w = model.out_weight
33
+ for s in range(0, len(order), bs):
34
+ idxs = order[s : s + bs]
35
+ seqs = []
36
+ for i in idxs:
37
+ c, k = pairs[i]
38
+ c = c[-max_ctx:]
39
+ seqs.append((c + k, len(c), len(k)))
40
+ L = max(len(x[0]) for x in seqs)
41
+ inp = torch.full((len(seqs), L), pad_id, dtype=torch.long)
42
+ for r, (ids, _, _) in enumerate(seqs):
43
+ inp[r, : len(ids)] = torch.tensor(ids)
44
+ inp = inp.to(device)
45
+ with torch.autocast(device.type, torch.bfloat16):
46
+ h = model.hidden(inp)
47
+ rows, cols, tgts = [], [], []
48
+ for r, (ids, nc, nk) in enumerate(seqs):
49
+ for j in range(nk):
50
+ rows.append(r); cols.append(nc + j - 1); tgts.append(ids[nc + j])
51
+ hs = h[torch.tensor(rows, device=device), torch.tensor(cols, device=device)]
52
+ logits = (hs @ w.to(hs.dtype).t()).float()
53
+ lp = torch.log_softmax(logits, -1).gather(1, torch.tensor(tgts, device=device)[:, None]).squeeze(1).cpu()
54
+ p = 0
55
+ for r, i in enumerate(idxs):
56
+ nk = seqs[r][2]
57
+ res[i] = (float(lp[p : p + nk].sum()), nk); p += nk
58
+ return res
59
+
60
+
61
+ def evaluate(model, tok, tasks: dict[str, list[dict]], device, pad_id=1, bs=48) -> dict[str, dict]:
62
+ model.eval()
63
+ out = {}
64
+ for name, items in tasks.items():
65
+ pairs, spans, labels, chs = [], [], [], []
66
+ for it in items:
67
+ start = len(pairs)
68
+ if "alt_ctx" in it: # WinoGrande partial scoring: same continuation, two candidate contexts
69
+ tail = tok.encode(it["choices"][0]).ids
70
+ pairs += [(tok.encode(it["ctx"]).ids, tail), (tok.encode(it["alt_ctx"]).ids, tail)]
71
+ labels.append(it["wino_label"]); chs.append([it["choices"][0]] * 2)
72
+ else:
73
+ cids = tok.encode(it["ctx"]).ids
74
+ for ch in it["choices"]:
75
+ pairs.append((cids, tok.encode(ch).ids or [0]))
76
+ labels.append(it["label"]); chs.append(it["choices"])
77
+ spans.append((start, len(pairs) - start))
78
+ ll = loglik_batch(model, pairs, device, pad_id=pad_id, bs=bs)
79
+ acc = accn = 0
80
+ for (st, n), lab, ch in zip(spans, labels, chs):
81
+ raw = [ll[st + k][0] for k in range(n)]
82
+ norm = [raw[k] / max(1, len(ch[k].encode())) for k in range(n)]
83
+ acc += int(max(range(n), key=raw.__getitem__) == lab)
84
+ accn += int(max(range(n), key=norm.__getitem__) == lab)
85
+ out[name] = {"acc": 100 * acc / len(items), "acc_norm": 100 * accn / len(items), "n": len(items)}
86
+ out[name]["score"] = out[name][METRIC.get(name, "acc")]
87
+ model.train()
88
+ return out
vbq/fastce.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fused cross-entropy: same maths as FusedLinearCE, but the per-chunk softmax/grad chain is one compiled kernel set.
2
+
3
+ On a 288 GB/s card the eager version streams ~5 GB of fp32 logits-sized tensors per chunk; here the bf16 logits are
4
+ read twice and the bf16 gradient written once.
5
+ """
6
+ import torch
7
+
8
+
9
+ def _chunk(logits, tgt, z_loss: float, inv_n: float):
10
+ lg = logits.float()
11
+ lse = torch.logsumexp(lg, dim=-1)
12
+ tl = lg.gather(1, tgt[:, None]).squeeze(1)
13
+ loss = (lse - tl).sum()
14
+ p = torch.exp(lg - lse[:, None])
15
+ if z_loss:
16
+ loss = loss + z_loss * (lse * lse).sum()
17
+ p = p * (1.0 + 2.0 * z_loss * lse[:, None])
18
+ cols = torch.arange(lg.shape[1], device=lg.device)[None, :]
19
+ p = torch.where(cols == tgt[:, None], p - 1.0, p)
20
+ return loss, (p * inv_n).to(torch.bfloat16)
21
+
22
+
23
+ chunk_compiled = torch.compile(_chunk, dynamic=False)
24
+
25
+
26
+ class FastLinearCE(torch.autograd.Function):
27
+ @staticmethod
28
+ def forward(ctx, h, w, targets, chunk: int, z_loss: float):
29
+ N = h.shape[0]
30
+ wb = w.to(h.dtype)
31
+ grad_h = torch.empty_like(h)
32
+ grad_w = torch.zeros_like(w, dtype=torch.float32)
33
+ total = torch.zeros((), device=h.device, dtype=torch.float32)
34
+ for i in range(0, N, chunk):
35
+ hc, tc = h[i : i + chunk], targets[i : i + chunk]
36
+ loss_i, pb = chunk_compiled(hc @ wb.t(), tc, z_loss, 1.0 / N)
37
+ total += loss_i
38
+ grad_h[i : i + chunk] = pb @ wb
39
+ grad_w.add_((pb.t() @ hc).float())
40
+ ctx.save_for_backward(grad_h, grad_w)
41
+ return total / N
42
+
43
+ @staticmethod
44
+ def backward(ctx, g):
45
+ grad_h, grad_w = ctx.saved_tensors
46
+ return grad_h * g.to(grad_h.dtype), grad_w * g, None, None, None
47
+
48
+
49
+ def _chunk_multi(logits, tgt, z_loss: float, inv_n: float):
50
+ """Patch-level loss: one distribution per position scored against all k tokens of the next patch.
51
+ tgt [n, k]; loss = sum_k CE(logits, tgt[:, k]); grad = k*softmax - sum_k onehot."""
52
+ lg = logits.float()
53
+ k = tgt.shape[1]
54
+ lse = torch.logsumexp(lg, dim=-1)
55
+ loss = (k * lse - lg.gather(1, tgt).sum(1)).sum()
56
+ p = k * torch.exp(lg - lse[:, None])
57
+ if z_loss:
58
+ loss = loss + k * z_loss * (lse * lse).sum()
59
+ p = p * (1.0 + 2.0 * z_loss * lse[:, None])
60
+ cols = torch.arange(lg.shape[1], device=lg.device)[None, :]
61
+ for j in range(k): # unrolled at trace time and fused: no logits-sized one-hot buffer
62
+ p = p - (cols == tgt[:, j : j + 1]).to(p.dtype)
63
+ return loss, (p * inv_n).to(torch.bfloat16)
64
+
65
+
66
+ chunk_multi = torch.compile(_chunk_multi, dynamic=False)
67
+
68
+
69
+ class PatchLinearCE(torch.autograd.Function):
70
+ """FastLinearCE for patch-level training: h [N, d], targets [N, k]; returns the mean per-token CE."""
71
+ @staticmethod
72
+ def forward(ctx, h, w, targets, chunk: int, z_loss: float):
73
+ N, k = targets.shape
74
+ wb = w.to(h.dtype)
75
+ grad_h = torch.empty_like(h)
76
+ grad_w = torch.zeros_like(w, dtype=torch.float32)
77
+ total = torch.zeros((), device=h.device, dtype=torch.float32)
78
+ for i in range(0, N, chunk):
79
+ hc, tc = h[i : i + chunk], targets[i : i + chunk]
80
+ loss_i, pb = chunk_multi(hc @ wb.t(), tc, z_loss, 1.0 / (N * k))
81
+ total += loss_i
82
+ grad_h[i : i + chunk] = pb @ wb
83
+ grad_w.add_((pb.t() @ hc).float())
84
+ ctx.save_for_backward(grad_h, grad_w)
85
+ return total / (N * k)
86
+
87
+ @staticmethod
88
+ def backward(ctx, g):
89
+ grad_h, grad_w = ctx.saved_tensors
90
+ return grad_h * g.to(grad_h.dtype), grad_w * g, None, None, None
91
+
92
+
93
+ # Variable assistant counts and CE tails must not specialize each compiled call.
94
+ chunk_sft = torch.compile(_chunk, dynamic=True)
95
+
96
+
97
+ class SFTLinearCE(torch.autograd.Function):
98
+ @staticmethod
99
+ def forward(ctx, h, w, targets, chunk: int, z_loss: float):
100
+ N = h.shape[0]
101
+ wb = w.to(h.dtype)
102
+ grad_h = torch.empty_like(h)
103
+ grad_w = torch.zeros_like(w, dtype=torch.float32)
104
+ total = torch.zeros((), device=h.device, dtype=torch.float32)
105
+ for i in range(0, N, chunk):
106
+ hc, tc = h[i : i + chunk], targets[i : i + chunk]
107
+ loss_i, pb = chunk_sft(hc @ wb.t(), tc, z_loss, torch.tensor(1.0 / N, device=h.device))
108
+ total += loss_i
109
+ grad_h[i : i + chunk] = pb @ wb
110
+ grad_w.add_((pb.t() @ hc).float())
111
+ ctx.save_for_backward(grad_h, grad_w)
112
+ return total / N
113
+
114
+ @staticmethod
115
+ def backward(ctx, g):
116
+ grad_h, grad_w = ctx.saved_tensors
117
+ return grad_h * g.to(grad_h.dtype), grad_w * g, None, None, None
vbq/model.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VBQ transformer: Llama-style decoder (RMSNorm, RoPE, GQA, QK-norm, SwiGLU, tied embeddings)."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from dataclasses import dataclass, asdict
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ import torch.nn.functional as F
10
+
11
+
12
+ @dataclass
13
+ class VBQConfig:
14
+ vocab_size: int = 32768
15
+ d_model: int = 640
16
+ n_layers: int = 12
17
+ n_heads: int = 10
18
+ n_kv_heads: int = 5
19
+ mlp_hidden: int = 1664
20
+ max_seq_len: int = 2048
21
+ rope_theta: float = 10000.0
22
+ norm_eps: float = 1e-5
23
+ qk_norm: bool = True
24
+ tie_embeddings: bool = True
25
+ # Mixture of experts (0 = dense SwiGLU). Top-1 routing, Switch-style.
26
+ n_experts: int = 0
27
+ capacity_factor: float = 1.25
28
+
29
+ @property
30
+ def head_dim(self) -> int:
31
+ return self.d_model // self.n_heads
32
+
33
+ def n_params(self) -> int:
34
+ d, hd = self.d_model, self.head_dim
35
+ attn = d * d * 2 + d * (self.n_kv_heads * hd) * 2
36
+ mlp = 3 * d * self.mlp_hidden * max(1, self.n_experts) + (d * self.n_experts if self.n_experts else 0)
37
+ per_layer = attn + mlp + 2 * d + (2 * hd if self.qk_norm else 0)
38
+ emb = self.vocab_size * d * (1 if self.tie_embeddings else 2)
39
+ return emb + self.n_layers * per_layer + d
40
+
41
+ def to_dict(self):
42
+ return asdict(self)
43
+
44
+
45
+ class RMSNorm(nn.Module):
46
+ def __init__(self, dim: int, eps: float = 1e-5):
47
+ super().__init__()
48
+ self.eps = eps
49
+ self.weight = nn.Parameter(torch.ones(dim))
50
+
51
+ def forward(self, x):
52
+ return F.rms_norm(x, (x.shape[-1],), self.weight.to(x.dtype), self.eps)
53
+
54
+
55
+ def build_rope(seq_len: int, head_dim: int, theta: float, device=None):
56
+ inv = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
57
+ t = torch.arange(seq_len, device=device, dtype=torch.float32)
58
+ freqs = torch.outer(t, inv)
59
+ return freqs.cos(), freqs.sin() # [T, hd/2]
60
+
61
+
62
+ def apply_rope(x, cos, sin):
63
+ # x: [B, H, T, hd]; half-split rotation
64
+ x1, x2 = x.chunk(2, dim=-1)
65
+ cos = cos[None, None, : x.shape[2]].to(x.dtype)
66
+ sin = sin[None, None, : x.shape[2]].to(x.dtype)
67
+ return torch.cat((x1 * cos - x2 * sin, x2 * cos + x1 * sin), dim=-1)
68
+
69
+
70
+ class Attention(nn.Module):
71
+ def __init__(self, c: VBQConfig):
72
+ super().__init__()
73
+ self.c = c
74
+ hd = c.head_dim
75
+ self.wq = nn.Linear(c.d_model, c.n_heads * hd, bias=False)
76
+ self.wk = nn.Linear(c.d_model, c.n_kv_heads * hd, bias=False)
77
+ self.wv = nn.Linear(c.d_model, c.n_kv_heads * hd, bias=False)
78
+ self.wo = nn.Linear(c.n_heads * hd, c.d_model, bias=False)
79
+ if c.qk_norm:
80
+ self.q_norm = RMSNorm(hd, c.norm_eps)
81
+ self.k_norm = RMSNorm(hd, c.norm_eps)
82
+
83
+ def forward(self, x, cos, sin):
84
+ B, T, _ = x.shape
85
+ c, hd = self.c, self.c.head_dim
86
+ q = self.wq(x).view(B, T, c.n_heads, hd).transpose(1, 2)
87
+ k = self.wk(x).view(B, T, c.n_kv_heads, hd).transpose(1, 2)
88
+ v = self.wv(x).view(B, T, c.n_kv_heads, hd).transpose(1, 2)
89
+ if c.qk_norm:
90
+ q, k = self.q_norm(q), self.k_norm(k)
91
+ q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
92
+ if c.n_kv_heads != c.n_heads:
93
+ rep = c.n_heads // c.n_kv_heads
94
+ k = k[:, :, None].expand(B, c.n_kv_heads, rep, T, hd).reshape(B, c.n_heads, T, hd)
95
+ v = v[:, :, None].expand(B, c.n_kv_heads, rep, T, hd).reshape(B, c.n_heads, T, hd)
96
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
97
+ return self.wo(y.transpose(1, 2).reshape(B, T, c.n_heads * hd))
98
+
99
+
100
+ class MLP(nn.Module):
101
+ def __init__(self, c: VBQConfig):
102
+ super().__init__()
103
+ self.gate_up = nn.Linear(c.d_model, 2 * c.mlp_hidden, bias=False)
104
+ self.down = nn.Linear(c.mlp_hidden, c.d_model, bias=False)
105
+
106
+ def forward(self, x):
107
+ g, u = self.gate_up(x).chunk(2, dim=-1)
108
+ return self.down(F.silu(g) * u)
109
+
110
+
111
+ class MoE(nn.Module):
112
+ """Top-1 mixture of SwiGLU experts (Switch Transformer routing).
113
+
114
+ Fixed shapes (compile-friendly): every expert gets a buffer of
115
+ capacity = ceil(capacity_factor * tokens / experts) slots while training
116
+ (tokens past it skip the MLP and keep only the residual); at eval every
117
+ token fits. The chosen expert's output is scaled by p / p.detach(), which
118
+ is 1 in value, so a model upcycled from identical copies computes exactly
119
+ what the dense model did, while the router still gets gradients.
120
+ Returns (y, aux) with aux = Switch load-balancing loss (1.0 = balanced).
121
+ """
122
+
123
+ def __init__(self, c: VBQConfig):
124
+ super().__init__()
125
+ self.c = c
126
+ E, d, h = c.n_experts, c.d_model, c.mlp_hidden
127
+ self.router = nn.Linear(d, E, bias=False)
128
+ self.gate_up = nn.Parameter(torch.empty(E, 2 * h, d))
129
+ self.down = nn.Parameter(torch.empty(E, d, h))
130
+ nn.init.normal_(self.gate_up, 0.0, 0.02)
131
+ nn.init.normal_(self.down, 0.0, 0.02)
132
+
133
+ def forward(self, x):
134
+ shape = x.shape
135
+ x = x.reshape(-1, shape[-1])
136
+ N, d = x.shape
137
+ E = self.c.n_experts
138
+ probs = F.softmax(self.router(x).float(), dim=-1) # [N, E]
139
+ gate, idx = probs.max(-1) # [N]
140
+ onehot = F.one_hot(idx, E) # [N, E]
141
+ pos = ((onehot.cumsum(0) - 1) * onehot).sum(-1) # slot within its expert
142
+ C = math.ceil(self.c.capacity_factor * N / E) if self.training else N
143
+ keep = pos < C
144
+ slot = torch.where(keep, idx * C + pos, torch.full_like(pos, E * C)) # overflow -> dump slot
145
+ buf = x.new_zeros(E * C + 1, d).index_put((slot,), x)
146
+ hbuf = buf[: E * C].view(E, C, d)
147
+ g, u = torch.bmm(hbuf, self.gate_up.to(x.dtype).transpose(1, 2)).chunk(2, dim=-1)
148
+ y = torch.bmm(F.silu(g) * u, self.down.to(x.dtype).transpose(1, 2)) # [E, C, d]
149
+ y = torch.cat([y.reshape(E * C, d), y.new_zeros(1, d)])[slot]
150
+ scale = (gate / gate.detach()) * keep
151
+ y = y * scale.to(y.dtype).unsqueeze(-1)
152
+ aux = E * (onehot.float().mean(0) * probs.mean(0)).sum()
153
+ return y.reshape(shape), aux
154
+
155
+
156
+ class Block(nn.Module):
157
+ def __init__(self, c: VBQConfig):
158
+ super().__init__()
159
+ self.attn_norm = RMSNorm(c.d_model, c.norm_eps)
160
+ self.attn = Attention(c)
161
+ self.mlp_norm = RMSNorm(c.d_model, c.norm_eps)
162
+ self.mlp = MoE(c) if c.n_experts else MLP(c)
163
+
164
+ def forward(self, x, cos, sin):
165
+ x = x + self.attn(self.attn_norm(x), cos, sin)
166
+ return x + self.ffn(self.mlp_norm(x))[0]
167
+
168
+ def ffn(self, x):
169
+ """(output, load-balancing loss) for dense and MoE blocks alike."""
170
+ y = self.mlp(x)
171
+ return y if isinstance(y, tuple) else (y, x.new_zeros((), dtype=torch.float32))
172
+
173
+
174
+ class FusedLinearCE(torch.autograd.Function):
175
+ """Cross-entropy over a huge vocab without ever materialising full logits.
176
+
177
+ Gradients w.r.t. hidden states and the (tied) output matrix are computed chunk by chunk
178
+ during the forward pass, so the backward pass is a cheap scale. Saves ~4 GB at 32k vocab.
179
+ """
180
+
181
+ @staticmethod
182
+ def forward(ctx, h, w, targets, chunk: int, z_loss: float):
183
+ N = h.shape[0]
184
+ wb = w.to(h.dtype)
185
+ grad_h = torch.empty_like(h)
186
+ grad_w = torch.zeros_like(w, dtype=torch.float32)
187
+ total = torch.zeros((), device=h.device, dtype=torch.float32)
188
+ for i in range(0, N, chunk):
189
+ hc, tc = h[i : i + chunk], targets[i : i + chunk]
190
+ logits = (hc @ wb.t()).float()
191
+ lse = torch.logsumexp(logits, dim=-1)
192
+ tgt = logits.gather(1, tc[:, None]).squeeze(1)
193
+ total += (lse - tgt).sum()
194
+ p = torch.exp(logits - lse[:, None])
195
+ if z_loss:
196
+ total += z_loss * (lse * lse).sum()
197
+ p.mul_(1.0 + 2.0 * z_loss * lse[:, None])
198
+ p.scatter_add_(1, tc[:, None], torch.full_like(tc[:, None], -1, dtype=p.dtype))
199
+ p.mul_(1.0 / N)
200
+ pb = p.to(h.dtype)
201
+ grad_h[i : i + chunk] = pb @ wb
202
+ grad_w += (pb.t() @ hc).float()
203
+ ctx.save_for_backward(grad_h, grad_w)
204
+ return total / N
205
+
206
+ @staticmethod
207
+ def backward(ctx, g):
208
+ grad_h, grad_w = ctx.saved_tensors
209
+ return grad_h * g.to(grad_h.dtype), grad_w * g, None, None, None
210
+
211
+
212
+ class VBQ(nn.Module):
213
+ def __init__(self, c: VBQConfig):
214
+ super().__init__()
215
+ self.c = c
216
+ self.tok_emb = nn.Embedding(c.vocab_size, c.d_model)
217
+ self.blocks = nn.ModuleList(Block(c) for _ in range(c.n_layers))
218
+ self.final_norm = RMSNorm(c.d_model, c.norm_eps)
219
+ self.lm_head = None if c.tie_embeddings else nn.Linear(c.d_model, c.vocab_size, bias=False)
220
+ cos, sin = build_rope(c.max_seq_len, c.head_dim, c.rope_theta)
221
+ self.register_buffer("rope_cos", cos, persistent=False)
222
+ self.register_buffer("rope_sin", sin, persistent=False)
223
+ self.apply(self._init)
224
+ for n, p in self.named_parameters(): # scaled init on residual output projections
225
+ if n.endswith("wo.weight") or n.endswith("down.weight"):
226
+ nn.init.normal_(p, 0.0, 0.02 / math.sqrt(2 * c.n_layers))
227
+
228
+ @staticmethod
229
+ def _init(m):
230
+ if isinstance(m, (nn.Linear, nn.Embedding)):
231
+ nn.init.normal_(m.weight, 0.0, 0.02)
232
+
233
+ @property
234
+ def out_weight(self):
235
+ return self.tok_emb.weight if self.lm_head is None else self.lm_head.weight
236
+
237
+ def hidden(self, idx):
238
+ return self.hidden_aux(idx)[0]
239
+
240
+ def hidden_aux(self, idx):
241
+ """Final hidden states and the mean MoE load-balancing loss (0 for dense)."""
242
+ return self._blocks(self.tok_emb(idx))
243
+
244
+ def hidden_patch(self, idx, k: int):
245
+ """Patch-level training (Shao et al. 2024): idx [B, P*k]; each group of k tokens enters as the mean of its
246
+ token embeddings, so the transformer runs on P positions. Same weights as token-level training."""
247
+ B, L = idx.shape
248
+ return self._blocks(self.tok_emb(idx).view(B, L // k, k, -1).mean(2))
249
+
250
+ def _blocks(self, x):
251
+ aux = x.new_zeros((), dtype=torch.float32)
252
+ for blk in self.blocks:
253
+ x = x + blk.attn(blk.attn_norm(x), self.rope_cos, self.rope_sin)
254
+ y, a = blk.ffn(blk.mlp_norm(x))
255
+ x = x + y
256
+ aux = aux + a
257
+ return self.final_norm(x), aux / len(self.blocks)
258
+
259
+ def forward(self, idx, targets=None, z_loss: float = 0.0, ce_chunk: int = 4096):
260
+ h = self.hidden(idx)
261
+ if targets is None:
262
+ return (h @ self.out_weight.to(h.dtype).t()).float()
263
+ loss = FusedLinearCE.apply(h.reshape(-1, h.shape[-1]), self.out_weight, targets.reshape(-1), ce_chunk, z_loss)
264
+ return loss
265
+
266
+ @torch.no_grad()
267
+ def generate(self, idx, max_new_tokens=128, temperature=0.8, top_k=50, top_p=0.95, eos_id=None, repetition_penalty=1.0):
268
+ self.eval()
269
+ for _ in range(max_new_tokens):
270
+ ctx = idx[:, -self.c.max_seq_len :]
271
+ logits = self(ctx)[:, -1]
272
+ if repetition_penalty != 1.0:
273
+ for tok in set(ctx[0].tolist()):
274
+ logits[0, tok] = logits[0, tok] / repetition_penalty if logits[0, tok] > 0 else logits[0, tok] * repetition_penalty
275
+ if temperature <= 0:
276
+ nxt = logits.argmax(-1, keepdim=True)
277
+ else:
278
+ logits = logits / temperature
279
+ if top_k:
280
+ v, _ = torch.topk(logits, min(top_k, logits.shape[-1]))
281
+ logits[logits < v[:, [-1]]] = -float("inf")
282
+ probs = F.softmax(logits, dim=-1)
283
+ if top_p < 1.0:
284
+ sp, si = torch.sort(probs, descending=True)
285
+ cum = sp.cumsum(-1)
286
+ sp[cum - sp > top_p] = 0
287
+ probs = torch.zeros_like(probs).scatter_(1, si, sp)
288
+ probs = probs / probs.sum(-1, keepdim=True)
289
+ nxt = torch.multinomial(probs, 1)
290
+ idx = torch.cat([idx, nxt], dim=1)
291
+ yield int(nxt.item())
292
+ if eos_id is not None and int(nxt.item()) == eos_id:
293
+ break
294
+
295
+ @torch.no_grad()
296
+ def generate_cached(self, idx, max_new_tokens=128, temperature=0.8, top_k=50, top_p=0.95, eos_id=None, repetition_penalty=1.0):
297
+ """KV-cached sampling. Yields token ids one by one. idx: [1, T] prompt."""
298
+ self.eval()
299
+ c = self.c
300
+ idx = idx[:, -(c.max_seq_len - 1):]
301
+ stop_ids = set() if eos_id is None else ({eos_id} if isinstance(eos_id, int) else set(eos_id))
302
+ caches: list = [None] * c.n_layers
303
+ seen = set(idx[0].tolist())
304
+
305
+ def forward(ids, pos):
306
+ T = ids.shape[1]
307
+ x = self.tok_emb(ids)
308
+ cos, sin = self.rope_cos[pos : pos + T], self.rope_sin[pos : pos + T]
309
+ for i, blk in enumerate(self.blocks):
310
+ a = blk.attn
311
+ h = blk.attn_norm(x)
312
+ q = a.wq(h).view(1, T, c.n_heads, c.head_dim).transpose(1, 2)
313
+ k = a.wk(h).view(1, T, c.n_kv_heads, c.head_dim).transpose(1, 2)
314
+ v = a.wv(h).view(1, T, c.n_kv_heads, c.head_dim).transpose(1, 2)
315
+ if c.qk_norm:
316
+ q, k = a.q_norm(q), a.k_norm(k)
317
+ q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
318
+ if caches[i] is not None:
319
+ k, v = torch.cat([caches[i][0], k], 2), torch.cat([caches[i][1], v], 2)
320
+ caches[i] = (k, v)
321
+ rep = c.n_heads // c.n_kv_heads
322
+ kk = k.repeat_interleave(rep, dim=1) if rep > 1 else k
323
+ vv = v.repeat_interleave(rep, dim=1) if rep > 1 else v
324
+ y = F.scaled_dot_product_attention(q, kk, vv, is_causal=(T > 1))
325
+ x = x + a.wo(y.transpose(1, 2).reshape(1, T, c.n_heads * c.head_dim))
326
+ x = x + blk.ffn(blk.mlp_norm(x))[0]
327
+ return (self.final_norm(x[:, -1]) @ self.out_weight.to(x.dtype).t()).float()
328
+
329
+ logits = forward(idx, 0)
330
+ pos = idx.shape[1]
331
+ for _ in range(max_new_tokens):
332
+ if pos >= c.max_seq_len:
333
+ break
334
+ lg = logits[0].clone()
335
+ if repetition_penalty != 1.0:
336
+ for tok in seen:
337
+ lg[tok] = lg[tok] / repetition_penalty if lg[tok] > 0 else lg[tok] * repetition_penalty
338
+ if temperature <= 0:
339
+ nxt = int(lg.argmax())
340
+ else:
341
+ lg = lg / temperature
342
+ if top_k:
343
+ v, _ = torch.topk(lg, min(top_k, lg.numel()))
344
+ lg[lg < v[-1]] = -float("inf")
345
+ probs = F.softmax(lg, dim=-1)
346
+ if top_p < 1.0:
347
+ sp, si = torch.sort(probs, descending=True)
348
+ sp[sp.cumsum(-1) - sp > top_p] = 0
349
+ probs = torch.zeros_like(probs).scatter_(0, si, sp)
350
+ probs = probs / probs.sum()
351
+ nxt = int(torch.multinomial(probs, 1))
352
+ yield nxt
353
+ if nxt in stop_ids:
354
+ break
355
+ seen.add(nxt)
356
+ logits = forward(torch.tensor([[nxt]], device=idx.device), pos)
357
+ pos += 1
vbq/optim.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Muon (Newton-Schulz orthogonalised momentum) for 2-D hidden matrices (and 3-D stacks of MoE expert matrices); scale matched to AdamW RMS (Moonlight)."""
2
+ import torch
3
+
4
+
5
+ @torch.no_grad()
6
+ def newton_schulz(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
7
+ a, b, c = 3.4445, -4.7750, 2.0315
8
+ X = G.bfloat16()
9
+ transposed = X.shape[0] > X.shape[1]
10
+ if transposed:
11
+ X = X.T
12
+ X = X / (X.norm() + 1e-7)
13
+ for _ in range(steps):
14
+ A = X @ X.T
15
+ X = a * X + (b * A + c * (A @ A)) @ X
16
+ return X.T if transposed else X
17
+
18
+
19
+ class Muon(torch.optim.Optimizer):
20
+ def __init__(self, params, lr=1e-3, momentum=0.95, weight_decay=0.1, split=None):
21
+ # split: dict param-id -> number of equal row-chunks to orthogonalise independently (fused gate/up)
22
+ super().__init__(params, dict(lr=lr, momentum=momentum, weight_decay=weight_decay))
23
+ self.split = split or {}
24
+ self.bad = None # device counter of updates dropped for being non-finite (read at log time, no sync here)
25
+
26
+ @torch.no_grad()
27
+ def step(self):
28
+ for g in self.param_groups:
29
+ for p in g["params"]:
30
+ if p.grad is None:
31
+ continue
32
+ st = self.state[p]
33
+ if "buf" not in st:
34
+ st["buf"] = torch.zeros_like(p.grad)
35
+ buf = st["buf"]
36
+ buf.lerp_(p.grad, 1 - g["momentum"])
37
+ upd = p.grad.lerp(buf, g["momentum"]) # nesterov
38
+ # 3-D = stacked MoE experts: each expert's matrix is orthogonalised on its own
39
+ mats = list(upd.unbind(0)) if upd.ndim == 3 else [upd]
40
+ done = []
41
+ for m in mats:
42
+ outs = []
43
+ for ch in m.chunk(self.split.get(id(p), 1), dim=0):
44
+ o = newton_schulz(ch)
45
+ outs.append(o * (0.2 * max(ch.shape) ** 0.5))
46
+ done.append(torch.cat(outs, 0) if len(outs) > 1 else outs[0])
47
+ o = torch.stack(done) if upd.ndim == 3 else done[0]
48
+ # a rare non-finite Newton-Schulz result (seen on the RX 7600 with finite grads) would poison the
49
+ # weight for good: drop this parameter's update for the step instead
50
+ ok = torch.isfinite(o).all()
51
+ if self.bad is None: self.bad = torch.zeros((), device=o.device, dtype=torch.int64)
52
+ self.bad += (~ok).long()
53
+ o = torch.where(ok, o, torch.zeros((), device=o.device, dtype=o.dtype))
54
+ p.mul_(1 - g["lr"] * g["weight_decay"])
55
+ p.add_(o.to(p.dtype), alpha=-g["lr"])
vbq/sources.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streaming adapters for each pretraining source. Each yields cleaned text documents."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import json
6
+ import random
7
+ import re
8
+ from pathlib import Path
9
+
10
+ import pyarrow as pa
11
+ import pyarrow.compute as pc
12
+ import pyarrow.parquet as pq
13
+ import yaml
14
+ from huggingface_hub import HfApi, hf_hub_download
15
+
16
+ ROOT = Path(__file__).resolve().parent.parent
17
+ RAW = ROOT / "data" / "raw"
18
+ _BLANKS = re.compile(r"\n{4,}")
19
+
20
+
21
+ def load_cfg(path=None):
22
+ return yaml.safe_load(open(path or ROOT / "configs" / "data.yaml"))
23
+
24
+
25
+ def _clean(t: str) -> str:
26
+ t = t.replace("\r\n", "\n").replace("\x00", "")
27
+ return _BLANKS.sub("\n\n\n", t).strip()
28
+
29
+
30
+ class Source:
31
+ def __init__(self, name: str, cfg: dict, glob: dict):
32
+ self.name, self.cfg, self.glob = name, cfg, glob
33
+ self.repo = cfg["repo"]
34
+ self.min_chars = cfg.get("min_chars", glob.get("min_chars", 200))
35
+ self.quota = int(glob["total_tokens"] * cfg["weight"])
36
+ self.per_shard_cap = cfg.get("per_shard_cap_tokens")
37
+ self.api = HfApi()
38
+
39
+ # -- file selection (deterministic shuffle so a partial run is still a uniform sample) --
40
+ def list_shards(self) -> list[str]:
41
+ files = [f.path for f in self.api.list_repo_tree(self.repo, repo_type="dataset", path_in_repo=self.cfg["prefix"].rstrip("/"), recursive=True)
42
+ if hasattr(f, "size") and f.path.endswith((".parquet", ".jsonl.zst"))]
43
+ files.sort()
44
+ random.Random(self.glob["seed"]).shuffle(files)
45
+ return files
46
+
47
+ def download(self, path: str) -> Path:
48
+ return Path(hf_hub_download(self.repo, path, repo_type="dataset", local_dir=RAW / self.name))
49
+
50
+ def docs(self, local: Path):
51
+ raise NotImplementedError
52
+
53
+ def _emit(self, texts):
54
+ for t in texts:
55
+ if t is None:
56
+ continue
57
+ t = _clean(t)
58
+ if len(t) >= self.min_chars:
59
+ yield t
60
+
61
+
62
+ class ParquetSource(Source):
63
+ text_col = "text"
64
+ extra_cols: list[str] = []
65
+
66
+ def mask(self, tbl: pa.Table):
67
+ return None
68
+
69
+ def format(self, tbl: pa.Table):
70
+ return tbl[self.text_col].to_pylist()
71
+
72
+ def docs(self, local: Path):
73
+ pf = pq.ParquetFile(local)
74
+ cols = [self.text_col] + self.extra_cols
75
+ for batch in pf.iter_batches(batch_size=4096, columns=cols):
76
+ tbl = pa.Table.from_batches([batch])
77
+ m = self.mask(tbl)
78
+ if m is not None:
79
+ tbl = tbl.filter(m)
80
+ yield from self._emit(self.format(tbl))
81
+
82
+
83
+ class FineWebEdu(ParquetSource):
84
+ extra_cols = ["metadata"]
85
+
86
+ def mask(self, tbl):
87
+ score = pc.struct_field(tbl["metadata"], "score")
88
+ return pc.greater_equal(score, self.cfg["min_edu_score"])
89
+
90
+
91
+ class Cosmopedia(ParquetSource):
92
+ pass
93
+
94
+
95
+ class FineMath(ParquetSource):
96
+ extra_cols = ["score", "language", "language_score"]
97
+
98
+ def mask(self, tbl):
99
+ return pc.and_(pc.equal(tbl["language"], "en"), pc.greater_equal(tbl["language_score"], 0.9))
100
+
101
+
102
+ class Wikipedia(ParquetSource):
103
+ extra_cols = ["title"]
104
+
105
+ def format(self, tbl):
106
+ out = []
107
+ for title, text in zip(tbl["title"].to_pylist(), tbl["text"].to_pylist()):
108
+ if title.startswith(("List of", "Lists of", "Index of")) or len(text) < self.cfg.get("min_chars", 2000):
109
+ continue
110
+ out.append(f"# {title}\n\n{text}")
111
+ return out
112
+
113
+
114
+ class DCLM(ParquetSource):
115
+ extra_cols = ["fasttext_score", "language", "language_score"]
116
+
117
+ def list_shards(self):
118
+ base = "filtered/OH_eli5_vs_rw_v2_bigram_200k_train/fasttext_openhermes_reddit_eli5_vs_rw_v2_bigram_200k_train/processed_data"
119
+ rng = random.Random(self.glob["seed"])
120
+ out = [f"{base}/global-shard_{g:02d}_of_10/local-shard_{l}_of_10/shard_{n:08d}_processed.parquet"
121
+ for g in range(1, 11) for l in range(10) for n in range(0, 260)]
122
+ rng.shuffle(out)
123
+ return out[:self.cfg.get("max_shards", 200)] # far more than needed; builder stops at quota
124
+
125
+ def mask(self, tbl):
126
+ m = pc.and_(pc.greater_equal(tbl["fasttext_score"], self.cfg["min_fasttext"]),
127
+ pc.greater_equal(tbl["language_score"], self.cfg["min_lang_score"]))
128
+ return pc.and_(m, pc.equal(tbl["language"], "en"))
129
+
130
+
131
+ class StackExchange(ParquetSource):
132
+ text_col = "ThreadText"
133
+ extra_cols = ["Score", "AnswerCount"]
134
+
135
+ def list_shards(self):
136
+ out = []
137
+ for site in self.cfg["sites"]:
138
+ d = site if "." in site else f"{site}.stackexchange.com"
139
+ if site == "stackoverflow":
140
+ d = "stackoverflow.com"
141
+ try:
142
+ fs = [f.path for f in self.api.list_repo_tree(self.repo, repo_type="dataset", path_in_repo=d) if f.path.endswith(".parquet")]
143
+ except Exception:
144
+ continue
145
+ off, n = self.cfg.get("shard_offset", 0), self.cfg.get("shards_per_site", 1)
146
+ out.extend(sorted(fs)[off:off + n]) # few shards per site; per_shard_cap keeps the mix balanced
147
+ random.Random(self.glob["seed"]).shuffle(out)
148
+ return out
149
+
150
+ def mask(self, tbl):
151
+ return pc.and_(pc.greater_equal(tbl["Score"], self.cfg["min_score"]), pc.greater_equal(tbl["AnswerCount"], 1))
152
+
153
+
154
+ class CodePython(Source):
155
+ def docs(self, local: Path):
156
+ import zstandard
157
+ with open(local, "rb") as fh, zstandard.ZstdDecompressor().stream_reader(fh) as r:
158
+ for line in io.TextIOWrapper(r, encoding="utf-8"):
159
+ o = json.loads(line)
160
+ if o.get("score", 0) < self.cfg["min_score"] or o.get("length_bytes", 0) < self.cfg["min_bytes"]:
161
+ continue
162
+ yield from self._emit([o["text"]])
163
+
164
+
165
+ REGISTRY = {"fineweb_edu": FineWebEdu, "dclm": DCLM, "cosmopedia": Cosmopedia, "finemath": FineMath,
166
+ "wikipedia": Wikipedia, "stackexchange": StackExchange, "code_python": CodePython}
167
+
168
+
169
+ def get_source(name: str, cfg: dict | None = None) -> Source:
170
+ cfg = cfg or load_cfg()
171
+ return REGISTRY[name](name, cfg["sources"][name], cfg)