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.
- glassllm-0.1.0/.gitignore +30 -0
- glassllm-0.1.0/LICENSE +21 -0
- glassllm-0.1.0/PKG-INFO +137 -0
- glassllm-0.1.0/README.md +115 -0
- glassllm-0.1.0/pyproject.toml +52 -0
- glassllm-0.1.0/scripts/build_api_inventory.py +67 -0
- glassllm-0.1.0/scripts/build_step_index.py +14 -0
- glassllm-0.1.0/scripts/prepare_corpus.py +62 -0
- glassllm-0.1.0/scripts/train_model.py +72 -0
- glassllm-0.1.0/scripts/train_tokenizer.py +30 -0
- glassllm-0.1.0/site/scripts/capture-docs.mjs +235 -0
- glassllm-0.1.0/site/scripts/check-docs.mjs +134 -0
- glassllm-0.1.0/site/scripts/clean.mjs +6 -0
- glassllm-0.1.0/site/scripts/prepare-wheel.mjs +35 -0
- glassllm-0.1.0/site/src/scripts/free-lab.ts +317 -0
- glassllm-0.1.0/site/src/scripts/network-view.ts +489 -0
- glassllm-0.1.0/src/glassllm/__init__.py +40 -0
- glassllm-0.1.0/src/glassllm/autograd.py +427 -0
- glassllm-0.1.0/src/glassllm/cli.py +228 -0
- glassllm-0.1.0/src/glassllm/config.py +31 -0
- glassllm-0.1.0/src/glassllm/model.py +125 -0
- glassllm-0.1.0/src/glassllm/nn.py +176 -0
- glassllm-0.1.0/src/glassllm/optim.py +65 -0
- glassllm-0.1.0/src/glassllm/step_index.json +102 -0
- glassllm-0.1.0/src/glassllm/steps.py +85 -0
- glassllm-0.1.0/src/glassllm/tokenizer.py +142 -0
- glassllm-0.1.0/src/glassllm/trace.py +93 -0
- glassllm-0.1.0/src/glassllm/train.py +109 -0
- glassllm-0.1.0/src/glassllm/weights/en.meta.json +11 -0
- glassllm-0.1.0/src/glassllm/weights/en.npz +0 -0
- glassllm-0.1.0/src/glassllm/weights/en.tokenizer.json +1 -0
- glassllm-0.1.0/src/glassllm/weights/pt.meta.json +11 -0
- glassllm-0.1.0/src/glassllm/weights/pt.npz +0 -0
- glassllm-0.1.0/src/glassllm/weights/pt.tokenizer.json +1 -0
- glassllm-0.1.0/tests/__init__.py +0 -0
- glassllm-0.1.0/tests/test_autograd.py +337 -0
- glassllm-0.1.0/tests/test_cli.py +128 -0
- glassllm-0.1.0/tests/test_generate.py +192 -0
- glassllm-0.1.0/tests/test_model.py +131 -0
- glassllm-0.1.0/tests/test_nn.py +180 -0
- glassllm-0.1.0/tests/test_pretrained.py +160 -0
- glassllm-0.1.0/tests/test_steps.py +153 -0
- glassllm-0.1.0/tests/test_tokenizer.py +103 -0
- glassllm-0.1.0/tests/test_train.py +255 -0
- 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.
|
glassllm-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
+
[](https://pypi.org/project/glassllm/)
|
|
31
|
+
[](https://www.python.org/)
|
|
32
|
+
[](https://numpy.org/)
|
|
33
|
+
[](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/).
|
glassllm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+

|
|
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
|
+
[](https://pypi.org/project/glassllm/)
|
|
9
|
+
[](https://www.python.org/)
|
|
10
|
+
[](https://numpy.org/)
|
|
11
|
+
[](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()
|