Add MP-01 (9500) 100M base: weights, config, tokenizer, model code, model card and eval results
Browse files- README.md +108 -0
- config.json +24 -0
- eval_results.json +99 -0
- model.safetensors +3 -0
- tokenizer.json +0 -0
- vbq/__init__.py +7 -0
- vbq/chat.py +65 -0
- vbq/data.py +92 -0
- vbq/evals.py +88 -0
- vbq/fastce.py +117 -0
- vbq/model.py +357 -0
- vbq/optim.py +55 -0
- 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)
|