glassllm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. glassllm-0.1.0/.gitignore +30 -0
  2. glassllm-0.1.0/LICENSE +21 -0
  3. glassllm-0.1.0/PKG-INFO +137 -0
  4. glassllm-0.1.0/README.md +115 -0
  5. glassllm-0.1.0/pyproject.toml +52 -0
  6. glassllm-0.1.0/scripts/build_api_inventory.py +67 -0
  7. glassllm-0.1.0/scripts/build_step_index.py +14 -0
  8. glassllm-0.1.0/scripts/prepare_corpus.py +62 -0
  9. glassllm-0.1.0/scripts/train_model.py +72 -0
  10. glassllm-0.1.0/scripts/train_tokenizer.py +30 -0
  11. glassllm-0.1.0/site/scripts/capture-docs.mjs +235 -0
  12. glassllm-0.1.0/site/scripts/check-docs.mjs +134 -0
  13. glassllm-0.1.0/site/scripts/clean.mjs +6 -0
  14. glassllm-0.1.0/site/scripts/prepare-wheel.mjs +35 -0
  15. glassllm-0.1.0/site/src/scripts/free-lab.ts +317 -0
  16. glassllm-0.1.0/site/src/scripts/network-view.ts +489 -0
  17. glassllm-0.1.0/src/glassllm/__init__.py +40 -0
  18. glassllm-0.1.0/src/glassllm/autograd.py +427 -0
  19. glassllm-0.1.0/src/glassllm/cli.py +228 -0
  20. glassllm-0.1.0/src/glassllm/config.py +31 -0
  21. glassllm-0.1.0/src/glassllm/model.py +125 -0
  22. glassllm-0.1.0/src/glassllm/nn.py +176 -0
  23. glassllm-0.1.0/src/glassllm/optim.py +65 -0
  24. glassllm-0.1.0/src/glassllm/step_index.json +102 -0
  25. glassllm-0.1.0/src/glassllm/steps.py +85 -0
  26. glassllm-0.1.0/src/glassllm/tokenizer.py +142 -0
  27. glassllm-0.1.0/src/glassllm/trace.py +93 -0
  28. glassllm-0.1.0/src/glassllm/train.py +109 -0
  29. glassllm-0.1.0/src/glassllm/weights/en.meta.json +11 -0
  30. glassllm-0.1.0/src/glassllm/weights/en.npz +0 -0
  31. glassllm-0.1.0/src/glassllm/weights/en.tokenizer.json +1 -0
  32. glassllm-0.1.0/src/glassllm/weights/pt.meta.json +11 -0
  33. glassllm-0.1.0/src/glassllm/weights/pt.npz +0 -0
  34. glassllm-0.1.0/src/glassllm/weights/pt.tokenizer.json +1 -0
  35. glassllm-0.1.0/tests/__init__.py +0 -0
  36. glassllm-0.1.0/tests/test_autograd.py +337 -0
  37. glassllm-0.1.0/tests/test_cli.py +128 -0
  38. glassllm-0.1.0/tests/test_generate.py +192 -0
  39. glassllm-0.1.0/tests/test_model.py +131 -0
  40. glassllm-0.1.0/tests/test_nn.py +180 -0
  41. glassllm-0.1.0/tests/test_pretrained.py +160 -0
  42. glassllm-0.1.0/tests/test_steps.py +153 -0
  43. glassllm-0.1.0/tests/test_tokenizer.py +103 -0
  44. glassllm-0.1.0/tests/test_train.py +255 -0
  45. glassllm-0.1.0/tests/torch_reference.py +67 -0
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Node / Astro
15
+ node_modules/
16
+ site/dist/
17
+ site/.astro/
18
+ site/src/generated/
19
+ site/public/py/
20
+
21
+ # Notebooks
22
+ .ipynb_checkpoints/
23
+
24
+ # Editors and OS
25
+ .vscode/
26
+ .idea/
27
+ .DS_Store
28
+
29
+ # Corpus (regenerate with scripts/prepare_corpus.py)
30
+ data/
glassllm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oséias Dias de Farias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: glassllm
3
+ Version: 0.1.0
4
+ Summary: A small Transformer written from scratch in NumPy, built to be looked at.
5
+ Project-URL: Homepage, https://oseiasdfarias.github.io/glassllm/
6
+ Project-URL: Documentation, https://oseiasdfarias.github.io/glassllm/en/docs/
7
+ Project-URL: Source, https://github.com/Oseiasdfarias/glassllm
8
+ Project-URL: Issues, https://github.com/Oseiasdfarias/glassllm/issues
9
+ Author: Oséias Dias de Farias
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: attention,autograd,education,llm,numpy,transformer
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: numpy>=1.26
21
+ Description-Content-Type: text/markdown
22
+
23
+ ![glassllm — see every step of a small language model](https://raw.githubusercontent.com/Oseiasdfarias/glassllm/main/docs/assets/banner-light.svg)
24
+
25
+ # glassllm
26
+
27
+ A small Transformer written from scratch in NumPy, built to be looked at. Follow a token through
28
+ the model and inspect the equation, source code and values produced at each step.
29
+
30
+ [![PyPI version](https://img.shields.io/pypi/v/glassllm)](https://pypi.org/project/glassllm/)
31
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
32
+ [![NumPy only](https://img.shields.io/badge/runtime-NumPy%20only-4fb3c2)](https://numpy.org/)
33
+ [![MIT license](https://img.shields.io/badge/license-MIT-4fb3c2)](https://github.com/Oseiasdfarias/glassllm/blob/main/LICENSE)
34
+
35
+ **English** · [Português](https://github.com/Oseiasdfarias/glassllm/blob/main/README.pt-BR.md) ·
36
+ [Documentation](https://oseiasdfarias.github.io/glassllm/)
37
+
38
+ glassllm is an educational language model and Python library. Its decoder-only Transformer,
39
+ tokenizer, automatic differentiation engine, training code and tracing tools are implemented
40
+ with NumPy. Pretrained Portuguese and English models are included in the package.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ python -m pip install glassllm
46
+ ```
47
+
48
+ The library supports Python 3.10 and later. NumPy is its only runtime dependency. The pretrained
49
+ weights and tokenizers are included, so inference works immediately after installation.
50
+
51
+ ## Generate text
52
+
53
+ The `glassllm` command uses the included English model by default. Select `pt` for Portuguese.
54
+
55
+ ```bash
56
+ glassllm --lang en generate "Alice was beginning to" --n 40
57
+ glassllm --lang pt generate "Capitu olhou para" --n 40
58
+ ```
59
+
60
+ Use greedy decoding for repeatable output, or inspect the available model operations:
61
+
62
+ ```bash
63
+ glassllm --lang en generate "Alice was beginning to" --n 40 --greedy
64
+ glassllm steps
65
+ glassllm trace "Alice was beginning to" --step attn.softmax --layer 0 --head 1
66
+ ```
67
+
68
+ ## Inspect a calculation
69
+
70
+ Pass a `Tracer` to the model to record its actual intermediate values. Each record includes the
71
+ equation, tensor shape and source location for a tagged operation.
72
+
73
+ ```python
74
+ from glassllm import Tracer, load_pretrained
75
+
76
+ model, tokenizer = load_pretrained("en")
77
+ token_ids = tokenizer.encode("Alice was beginning to")
78
+
79
+ tracer = Tracer()
80
+ model.forward(token_ids, trace=tracer)
81
+
82
+ step = tracer.get("attn.softmax", layer=0, head=1)
83
+ print(step.equation)
84
+ print(step.shape)
85
+ print(step.value)
86
+ ```
87
+
88
+ List all traceable operations with `glassllm steps`. The same tagged calculations are explained
89
+ in the [library documentation](https://oseiasdfarias.github.io/glassllm/en/docs/biblioteca/api-publica/).
90
+
91
+ ## What happens inside the model
92
+
93
+ Each prediction follows the same sequence of real calculations. The website can replay all 77
94
+ recorded operations for a prediction and lets you inspect their equations, tensors and source.
95
+
96
+ ```text
97
+ text → BPE tokens → embeddings → 4 Transformer blocks → logits → probabilities → next token
98
+ attention + SwiGLU
99
+ ```
100
+
101
+ The model uses causal self-attention, RoPE, grouped-query attention, an attention KV cache,
102
+ RMSNorm, SwiGLU and tied input/output embeddings. It has 852,608 parameters and runs on CPU.
103
+ It is deliberately small for learning; it is not intended for production text generation.
104
+
105
+ ## Interactive site and documentation
106
+
107
+ Try the [live bilingual lab](https://oseiasdfarias.github.io/glassllm/). It runs the same Python
108
+ wheel in a Pyodide worker in your browser. The network view shows the complete model architecture,
109
+ and slow motion steps through each recorded calculation at a readable pace.
110
+
111
+ The documentation includes 35 pages in each language, with interface guides, inference and
112
+ training flowcharts, mathematical foundations, API reference, examples and research references:
113
+
114
+ - [Documentation in English](https://oseiasdfarias.github.io/glassllm/en/docs/)
115
+ - [Documentação em português](https://oseiasdfarias.github.io/glassllm/pt/docs/)
116
+
117
+ ## Training and data
118
+
119
+ The included models were trained on public-domain books by Machado de Assis (Portuguese) and
120
+ Lewis Carroll (English), sourced from [Project Gutenberg](https://www.gutenberg.org/). Training
121
+ scripts and dataset preparation live in the repository. PyTorch is used only for development
122
+ and parity checks; it is not installed when users install the library.
123
+
124
+ ## Contributing
125
+
126
+ The source code, tests, design notes and issue tracker are on
127
+ [GitHub](https://github.com/Oseiasdfarias/glassllm). See the
128
+ [contribution guide](https://github.com/Oseiasdfarias/glassllm/blob/main/CONTRIBUTING.md) before
129
+ opening a pull request.
130
+
131
+ The interactive lab and complete library documentation are available now. The guided chapter
132
+ experience is planned for a later phase.
133
+
134
+ ## License
135
+
136
+ The Python library is licensed under the [MIT License](https://github.com/Oseiasdfarias/glassllm/blob/main/LICENSE).
137
+ Documentation text is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
@@ -0,0 +1,115 @@
1
+ ![glassllm — see every step of a small language model](https://raw.githubusercontent.com/Oseiasdfarias/glassllm/main/docs/assets/banner-light.svg)
2
+
3
+ # glassllm
4
+
5
+ A small Transformer written from scratch in NumPy, built to be looked at. Follow a token through
6
+ the model and inspect the equation, source code and values produced at each step.
7
+
8
+ [![PyPI version](https://img.shields.io/pypi/v/glassllm)](https://pypi.org/project/glassllm/)
9
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
10
+ [![NumPy only](https://img.shields.io/badge/runtime-NumPy%20only-4fb3c2)](https://numpy.org/)
11
+ [![MIT license](https://img.shields.io/badge/license-MIT-4fb3c2)](https://github.com/Oseiasdfarias/glassllm/blob/main/LICENSE)
12
+
13
+ **English** · [Português](https://github.com/Oseiasdfarias/glassllm/blob/main/README.pt-BR.md) ·
14
+ [Documentation](https://oseiasdfarias.github.io/glassllm/)
15
+
16
+ glassllm is an educational language model and Python library. Its decoder-only Transformer,
17
+ tokenizer, automatic differentiation engine, training code and tracing tools are implemented
18
+ with NumPy. Pretrained Portuguese and English models are included in the package.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ python -m pip install glassllm
24
+ ```
25
+
26
+ The library supports Python 3.10 and later. NumPy is its only runtime dependency. The pretrained
27
+ weights and tokenizers are included, so inference works immediately after installation.
28
+
29
+ ## Generate text
30
+
31
+ The `glassllm` command uses the included English model by default. Select `pt` for Portuguese.
32
+
33
+ ```bash
34
+ glassllm --lang en generate "Alice was beginning to" --n 40
35
+ glassllm --lang pt generate "Capitu olhou para" --n 40
36
+ ```
37
+
38
+ Use greedy decoding for repeatable output, or inspect the available model operations:
39
+
40
+ ```bash
41
+ glassllm --lang en generate "Alice was beginning to" --n 40 --greedy
42
+ glassllm steps
43
+ glassllm trace "Alice was beginning to" --step attn.softmax --layer 0 --head 1
44
+ ```
45
+
46
+ ## Inspect a calculation
47
+
48
+ Pass a `Tracer` to the model to record its actual intermediate values. Each record includes the
49
+ equation, tensor shape and source location for a tagged operation.
50
+
51
+ ```python
52
+ from glassllm import Tracer, load_pretrained
53
+
54
+ model, tokenizer = load_pretrained("en")
55
+ token_ids = tokenizer.encode("Alice was beginning to")
56
+
57
+ tracer = Tracer()
58
+ model.forward(token_ids, trace=tracer)
59
+
60
+ step = tracer.get("attn.softmax", layer=0, head=1)
61
+ print(step.equation)
62
+ print(step.shape)
63
+ print(step.value)
64
+ ```
65
+
66
+ List all traceable operations with `glassllm steps`. The same tagged calculations are explained
67
+ in the [library documentation](https://oseiasdfarias.github.io/glassllm/en/docs/biblioteca/api-publica/).
68
+
69
+ ## What happens inside the model
70
+
71
+ Each prediction follows the same sequence of real calculations. The website can replay all 77
72
+ recorded operations for a prediction and lets you inspect their equations, tensors and source.
73
+
74
+ ```text
75
+ text → BPE tokens → embeddings → 4 Transformer blocks → logits → probabilities → next token
76
+ attention + SwiGLU
77
+ ```
78
+
79
+ The model uses causal self-attention, RoPE, grouped-query attention, an attention KV cache,
80
+ RMSNorm, SwiGLU and tied input/output embeddings. It has 852,608 parameters and runs on CPU.
81
+ It is deliberately small for learning; it is not intended for production text generation.
82
+
83
+ ## Interactive site and documentation
84
+
85
+ Try the [live bilingual lab](https://oseiasdfarias.github.io/glassllm/). It runs the same Python
86
+ wheel in a Pyodide worker in your browser. The network view shows the complete model architecture,
87
+ and slow motion steps through each recorded calculation at a readable pace.
88
+
89
+ The documentation includes 35 pages in each language, with interface guides, inference and
90
+ training flowcharts, mathematical foundations, API reference, examples and research references:
91
+
92
+ - [Documentation in English](https://oseiasdfarias.github.io/glassllm/en/docs/)
93
+ - [Documentação em português](https://oseiasdfarias.github.io/glassllm/pt/docs/)
94
+
95
+ ## Training and data
96
+
97
+ The included models were trained on public-domain books by Machado de Assis (Portuguese) and
98
+ Lewis Carroll (English), sourced from [Project Gutenberg](https://www.gutenberg.org/). Training
99
+ scripts and dataset preparation live in the repository. PyTorch is used only for development
100
+ and parity checks; it is not installed when users install the library.
101
+
102
+ ## Contributing
103
+
104
+ The source code, tests, design notes and issue tracker are on
105
+ [GitHub](https://github.com/Oseiasdfarias/glassllm). See the
106
+ [contribution guide](https://github.com/Oseiasdfarias/glassllm/blob/main/CONTRIBUTING.md) before
107
+ opening a pull request.
108
+
109
+ The interactive lab and complete library documentation are available now. The guided chapter
110
+ experience is planned for a later phase.
111
+
112
+ ## License
113
+
114
+ The Python library is licensed under the [MIT License](https://github.com/Oseiasdfarias/glassllm/blob/main/LICENSE).
115
+ Documentation text is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "glassllm"
3
+ version = "0.1.0"
4
+ description = "A small Transformer written from scratch in NumPy, built to be looked at."
5
+ readme = { file = "README.md", content-type = "text/markdown" }
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Oséias Dias de Farias" }]
9
+ requires-python = ">=3.10"
10
+ dependencies = ["numpy>=1.26"]
11
+ keywords = ["llm", "transformer", "education", "numpy", "attention", "autograd"]
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Education",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://oseiasdfarias.github.io/glassllm/"
23
+ Documentation = "https://oseiasdfarias.github.io/glassllm/en/docs/"
24
+ Source = "https://github.com/Oseiasdfarias/glassllm"
25
+ Issues = "https://github.com/Oseiasdfarias/glassllm/issues"
26
+
27
+ [project.scripts]
28
+ glassllm = "glassllm.cli:main"
29
+
30
+ [dependency-groups]
31
+ dev = ["pytest>=8", "torch>=2.2"]
32
+
33
+ [build-system]
34
+ requires = ["hatchling>=1.26"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/glassllm"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src/glassllm", "tests", "scripts", "README.md", "LICENSE", "pyproject.toml"]
42
+
43
+ [tool.uv.sources]
44
+ torch = { index = "pytorch-cpu" }
45
+
46
+ [[tool.uv.index]]
47
+ name = "pytorch-cpu"
48
+ url = "https://download.pytorch.org/whl/cpu"
49
+ explicit = true
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
@@ -0,0 +1,67 @@
1
+ """Snapshot actual declarations for reviewing the documentation's API coverage.
2
+
3
+ This uses AST only: it does not import NumPy, load models or execute library code.
4
+ Regenerate with `python scripts/build_api_inventory.py` after an API change.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import ast
9
+ import json
10
+ from pathlib import Path
11
+
12
+ ROOT = Path(__file__).resolve().parents[1]
13
+ PACKAGE = ROOT / "src/glassllm"
14
+ OUTPUT = ROOT / "site/src/data/api-inventory.json"
15
+
16
+
17
+ def visible(name: str) -> bool:
18
+ return not name.startswith("_") or (name.startswith("__") and name.endswith("__") and name not in {"__repr__", "__post_init__"})
19
+
20
+
21
+ def function(node: ast.FunctionDef) -> dict:
22
+ decorators = {ast.unparse(item) for item in node.decorator_list}
23
+ returns = f" -> {ast.unparse(node.returns)}" if node.returns else ""
24
+ return {"name": node.name, "kind": "property" if "property" in decorators else "function",
25
+ "signature": f"{node.name}({ast.unparse(node.args)}){returns}", "line": node.lineno}
26
+
27
+
28
+ def members(node: ast.ClassDef) -> list[dict]:
29
+ result = []
30
+ for item in node.body:
31
+ if isinstance(item, ast.FunctionDef) and visible(item.name):
32
+ result.append(function(item))
33
+ elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name) and visible(item.target.id):
34
+ result.append({"name": item.target.id, "kind": "field", "signature": f"{item.target.id}: {ast.unparse(item.annotation)}", "line": item.lineno})
35
+ elif isinstance(item, ast.Assign):
36
+ for target in item.targets:
37
+ if isinstance(target, ast.Name) and visible(target.id):
38
+ result.append({"name": target.id, "kind": "alias", "signature": f"{target.id} = {ast.unparse(item.value)}", "line": item.lineno})
39
+ return result
40
+
41
+
42
+ def main() -> None:
43
+ modules = []
44
+ root_exports = []
45
+ for path in sorted(PACKAGE.glob("*.py")):
46
+ tree = ast.parse(path.read_text(encoding="utf-8"))
47
+ symbols = []
48
+ for node in tree.body:
49
+ if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
50
+ symbols.append(function(node))
51
+ elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
52
+ symbols.append({"name": node.name, "kind": "class", "signature": node.name,
53
+ "line": node.lineno, "members": members(node)})
54
+ elif path.name == "__init__.py" and isinstance(node, ast.Assign):
55
+ for target in node.targets:
56
+ if isinstance(target, ast.Name) and target.id == "__all__":
57
+ root_exports = ast.literal_eval(node.value)
58
+ modules.append({"module": "glassllm" if path.stem == "__init__" else f"glassllm.{path.stem}",
59
+ "file": path.relative_to(ROOT).as_posix(), "symbols": symbols})
60
+ OUTPUT.parent.mkdir(parents=True, exist_ok=True)
61
+ OUTPUT.write_text(json.dumps({"rootExports": root_exports, "modules": modules}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
62
+ count = sum(len(module["symbols"]) for module in modules)
63
+ print(f"API inventory: {len(root_exports)} root exports; {count} declarations in {len(modules)} modules")
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,14 @@
1
+ """Write src/glassllm/step_index.json from source comment markers."""
2
+ import json
3
+
4
+ from glassllm.steps import INDEX_PATH, scan_markers
5
+
6
+
7
+ def main() -> None:
8
+ index = scan_markers()
9
+ INDEX_PATH.write_text(json.dumps(index, indent=2, sort_keys=True) + "\n", encoding="utf-8")
10
+ print(f"{len(index)} steps -> {INDEX_PATH}")
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()
@@ -0,0 +1,62 @@
1
+ """Download eight public domain books and split each book's final 5% for validation."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import re
6
+ import unicodedata
7
+ import urllib.request
8
+ from pathlib import Path
9
+
10
+
11
+ BOOKS = {
12
+ "pt": { # Machado de Assis
13
+ 55752: "Dom Casmurro",
14
+ 54829: "Memorias Posthumas de Braz Cubas",
15
+ 55682: "Quincas Borba",
16
+ 56737: "Esau e Jacob",
17
+ 55797: "Memorial de Ayres",
18
+ },
19
+ "en": { # Lewis Carroll
20
+ 11: "Alice's Adventures in Wonderland",
21
+ 12: "Through the Looking-Glass",
22
+ 620: "Sylvie and Bruno",
23
+ },
24
+ }
25
+ URL = "https://www.gutenberg.org/cache/epub/{id}/pg{id}.txt"
26
+ START = re.compile(r"\*\*\* ?START OF (THE|THIS) PROJECT GUTENBERG EBOOK.*?\*\*\*", re.S)
27
+ END = re.compile(r"\*\*\* ?END OF (THE|THIS) PROJECT GUTENBERG EBOOK")
28
+
29
+
30
+ def clean(raw: str) -> str:
31
+ """Remove Gutenberg delimiters and normalize whitespace and Unicode to NFC."""
32
+ text = raw.replace("\r\n", "\n")
33
+ start, end = START.search(text), END.search(text)
34
+ if start is None or end is None or start.end() >= end.start():
35
+ raise ValueError("missing or invalid Project Gutenberg header/footer markers")
36
+ text = unicodedata.normalize("NFC", text[start.end():end.start()])
37
+ text = re.sub(r"[ \t]+", " ", text)
38
+ text = re.sub(r"\n{3,}", "\n\n", text)
39
+ return text.strip() + "\n"
40
+
41
+
42
+ def main(argv: list[str] | None = None) -> None:
43
+ parser = argparse.ArgumentParser(description=__doc__)
44
+ parser.add_argument("--out", default="data")
45
+ args = parser.parse_args(argv)
46
+ out = Path(args.out)
47
+ out.mkdir(parents=True, exist_ok=True)
48
+ for lang, books in BOOKS.items():
49
+ train_parts, val_parts = [], []
50
+ for book_id, title in books.items():
51
+ with urllib.request.urlopen(URL.format(id=book_id), timeout=30) as response:
52
+ text = clean(response.read().decode("utf-8-sig"))
53
+ cut = text.index("\n", int(len(text) * 0.95)) + 1
54
+ train_parts.append(text[:cut])
55
+ val_parts.append(text[cut:])
56
+ print(f"{lang}: {title}: {len(text):,} characters")
57
+ (out / f"{lang}_train.txt").write_text("\n".join(train_parts), encoding="utf-8")
58
+ (out / f"{lang}_val.txt").write_text("\n".join(val_parts), encoding="utf-8")
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -0,0 +1,72 @@
1
+ """Train one language with glassllm, restore its best checkpoint, and validate fp16 export."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ import time
8
+
9
+ import numpy as np
10
+
11
+ from glassllm import Config, GlassLM, LANGUAGES, Tokenizer
12
+ from glassllm.train import evaluate, train
13
+
14
+
15
+ MAX_SECONDS = 2 * 60 * 60
16
+ EVAL_SEED = 1234
17
+ EVAL_N_BATCHES = 10
18
+
19
+
20
+ def main(argv: list[str] | None = None) -> None:
21
+ parser = argparse.ArgumentParser(description=__doc__)
22
+ parser.add_argument("--lang", required=True, choices=LANGUAGES)
23
+ parser.add_argument("--steps", type=int, default=3000)
24
+ parser.add_argument("--batch-size", type=int, default=16)
25
+ parser.add_argument("--lr", type=float, default=3e-3)
26
+ parser.add_argument("--data", default="data")
27
+ parser.add_argument("--out", default="src/glassllm/weights")
28
+ args = parser.parse_args(argv)
29
+ deadline = time.monotonic() + MAX_SECONDS
30
+
31
+ def check_deadline() -> None:
32
+ if time.monotonic() >= deadline:
33
+ raise TimeoutError("pretrained model run exceeded the two-hour deadline")
34
+
35
+ out = Path(args.out)
36
+ out.mkdir(parents=True, exist_ok=True)
37
+ tok = Tokenizer.load(out / f"{args.lang}.tokenizer.json")
38
+
39
+ def read(split: str) -> np.ndarray:
40
+ text = (Path(args.data) / f"{args.lang}_{split}.txt").read_text(encoding="utf-8")
41
+ return np.array(tok.encode(text), dtype=np.int64)
42
+
43
+ train_ids, val_ids = read("train"), read("val")
44
+ cfg = Config(vocab_size=tok.vocab_size)
45
+ model = GlassLM(cfg, seed=0)
46
+ check_deadline()
47
+ train(model, train_ids, steps=args.steps, batch_size=args.batch_size, seq_len=cfg.max_len,
48
+ lr=args.lr, warmup=100, seed=0, val_data=val_ids, eval_every=100, keep_best=True)
49
+
50
+ # Measure the restored checkpoint and its fp16 export on identical windows.
51
+ settings = {"batch_size": args.batch_size, "seq_len": cfg.max_len,
52
+ "n_batches": EVAL_N_BATCHES, "seed": EVAL_SEED}
53
+ check_deadline()
54
+ fp32_loss = evaluate(model, val_ids, **settings)
55
+ check_deadline()
56
+ path = out / f"{args.lang}.npz"
57
+ model.save(path, fp16=True)
58
+ check_deadline()
59
+ fp16_loss = evaluate(GlassLM.load(path), val_ids, **settings)
60
+ check_deadline()
61
+ if not np.isfinite(fp32_loss) or not np.isfinite(fp16_loss) or abs(fp16_loss - fp32_loss) > 0.01:
62
+ raise ValueError(f"fp16 validation loss {fp16_loss} differs from fp32 {fp32_loss} by more than 0.01")
63
+ meta = {"steps": args.steps, "val_loss_fp32": fp32_loss, "val_loss": fp16_loss,
64
+ "train_tokens": len(train_ids), "val_tokens": len(val_ids),
65
+ "eval_batch_size": args.batch_size, "eval_seq_len": cfg.max_len,
66
+ "eval_n_batches": EVAL_N_BATCHES, "eval_seed": EVAL_SEED}
67
+ (out / f"{args.lang}.meta.json").write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
68
+ print(f"saved {path}: val loss {fp16_loss:.3f} (fp16; fp32 {fp32_loss:.3f})")
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,30 @@
1
+ """Train a language's BPE tokenizer and save it next to the model weights."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ from glassllm import LANGUAGES, Tokenizer
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> None:
11
+ parser = argparse.ArgumentParser(description=__doc__)
12
+ parser.add_argument("--lang", required=True, choices=LANGUAGES)
13
+ parser.add_argument("--vocab-size", type=int, default=1024)
14
+ parser.add_argument("--data", default="data")
15
+ parser.add_argument("--out", default="src/glassllm/weights")
16
+ args = parser.parse_args(argv)
17
+ text = (Path(args.data) / f"{args.lang}_train.txt").read_text(encoding="utf-8")
18
+ tok = Tokenizer.train(text, args.vocab_size)
19
+ out = Path(args.out)
20
+ out.mkdir(parents=True, exist_ok=True)
21
+ path = out / f"{args.lang}.tokenizer.json"
22
+ tok.save(path)
23
+ ids = tok.encode(text)
24
+ ratio = len(text) / len(ids) if ids else 0.0
25
+ print(f"{args.lang}: vocab {tok.vocab_size}, {len(ids):,} tokens, "
26
+ f"{ratio:.2f} characters per token -> {path}")
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()