AlaBoussoffara commited on
Commit
2d52135
·
1 Parent(s): 6edd10d

organized code and set up chainlit for demos

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +3 -0
  2. CONTRIBUTING.md +27 -0
  3. Dockerfile.dev +6 -16
  4. LICENSE +21 -0
  5. Makefile +70 -24
  6. README.md +125 -3
  7. architecture.txt +0 -28
  8. chainlit.md +14 -0
  9. configs/generation/generation.yaml +1 -1
  10. configs/model/large_model.yaml +4 -3
  11. configs/model/medium_model.yaml +5 -4
  12. configs/model/small_model.yaml +3 -2
  13. configs/model/test_model.yaml +3 -2
  14. docker-compose.yml +10 -3
  15. environment.yml +16 -5
  16. {scripts → notebooks}/load_data.ipynb +4 -4
  17. {scripts → notebooks}/tokenizer.ipynb +2 -2
  18. {scripts → notebooks}/train.ipynb +5 -5
  19. pyproject.toml +91 -65
  20. requirements.txt +0 -0
  21. scripts/inference.py +0 -76
  22. scripts/server.py +0 -26
  23. src/mini_transformer/__init__.py +14 -0
  24. src/mini_transformer/apps/__init__.py +3 -0
  25. src/mini_transformer/apps/chainlit_app.py +220 -0
  26. src/mini_transformer/apps/server.py +53 -0
  27. src/mini_transformer/cli.py +237 -0
  28. src/mini_transformer/conf/__init__.py +1 -0
  29. src/mini_transformer/conf/generation/generation.yaml +9 -0
  30. src/mini_transformer/conf/infer_mode.yaml +11 -0
  31. src/mini_transformer/conf/model/small.yaml +15 -0
  32. src/mini_transformer/conf/runtime/local.yaml +7 -0
  33. src/mini_transformer/conf/tokenizer/bpe_8k.yaml +15 -0
  34. src/{transformer → mini_transformer}/configs.py +1 -0
  35. src/mini_transformer/inference.py +176 -0
  36. src/mini_transformer/model_loader.py +172 -0
  37. src/mini_transformer/modules/__init__.py +6 -0
  38. src/{transformer → mini_transformer}/modules/attention.py +1 -1
  39. src/{transformer → mini_transformer}/modules/decoder.py +66 -6
  40. src/{transformer → mini_transformer}/modules/embedding.py +1 -1
  41. src/{transformer → mini_transformer}/modules/encoder.py +49 -5
  42. src/{transformer → mini_transformer}/modules/feedforward.py +0 -0
  43. src/{transformer → mini_transformer}/modules/lm_head.py +0 -0
  44. src/{transformer → mini_transformer}/transformer.py +33 -6
  45. src/{transformer → mini_transformer}/utils.py +14 -2
  46. src/transformer/__init__.py +0 -3
  47. src/transformer/modules/__init__.py +0 -6
  48. tests/units/modules/test_attention.py +1 -1
  49. tests/units/modules/test_decoder.py +24 -1
  50. tests/units/modules/test_embedding.py +1 -1
.gitignore CHANGED
@@ -54,6 +54,9 @@ data/
54
  outputs/
55
  runs/
56
  artifacts/
 
 
 
57
  *.pt
58
  *.pth
59
  *.ckpt
 
54
  outputs/
55
  runs/
56
  artifacts/
57
+ trained_models/
58
+ !trained_models/README.md
59
+ .chainlit/
60
  *.pt
61
  *.pth
62
  *.ckpt
CONTRIBUTING.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to mini_transformer
2
+
3
+ Thanks for your interest in contributing! Follow these steps to get started.
4
+
5
+ 1. **Create the development environment**
6
+ ```bash
7
+ make conda-setup
8
+ ```
9
+
10
+ 2. **Run quality checks** before opening a pull request:
11
+ ```bash
12
+ make fmt
13
+ make lint
14
+ make type
15
+ make test
16
+ ```
17
+
18
+ 3. **Submit changes** via a pull request. Please describe the motivation, relevant issues, and testing performed.
19
+
20
+ 4. **Code style**
21
+ - Keep imports sorted and formatted with `ruff` and `black`.
22
+ - Prefer type annotations and existing helper utilities.
23
+
24
+ 5. **Doc updates**
25
+ - Update the README and any tutorial notebooks if behaviour changes.
26
+
27
+ Feel free to open an issue for questions or proposals before starting larger pieces of work.
Dockerfile.dev CHANGED
@@ -1,6 +1,6 @@
1
  # syntax=docker/dockerfile:1
2
- FROM python:3.12-slim
3
- # FROM = choose a base OS + language runtime (Debian slim with Python 3.12)
4
 
5
  ENV DEBIAN_FRONTEND=noninteractive \
6
  PIP_NO_CACHE_DIR=1 \
@@ -17,22 +17,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
17
  # rm -rf ... -> clean apt cache to shrink the layer
18
 
19
  WORKDIR /app
20
- # WORKDIR = default directory for the following Dockerfile steps and 'docker run' processes
21
 
22
- COPY requirements.txt pyproject.toml ./
23
- # COPY = bring files from host into the image
24
- # Only copying dependency metadata first lets Docker cache layer if code changes later
25
 
26
- RUN pip install -U pip && pip install -r requirements.txt
27
- # pip install -U pip -> upgrade pip in the image
28
- # pip install -r ... -> install your Python runtime dependencies
29
 
30
  COPY . .
31
- # Now copy your actual code (done AFTER deps for better caching)
32
 
33
- RUN pip install -e .[dev]
34
- # -e . = editable install (your src is importable as a package without rebuilding)
35
-
36
- CMD ["pytest"]
37
- # CMD = default command when a container starts from this image
38
- # Here: run unit tests quietly (-q) in tests/units
 
1
  # syntax=docker/dockerfile:1
2
+ FROM python:3.11-slim
3
+ # FROM = choose a base OS + language runtime (Debian slim with Python 3.11)
4
 
5
  ENV DEBIAN_FRONTEND=noninteractive \
6
  PIP_NO_CACHE_DIR=1 \
 
17
  # rm -rf ... -> clean apt cache to shrink the layer
18
 
19
  WORKDIR /app
 
20
 
21
+ COPY pyproject.toml README.md LICENSE ./
22
+ COPY src ./src
 
23
 
24
+ RUN pip install -U pip && pip install -e .[dev,server,viz,notebook]
 
 
25
 
26
  COPY . .
 
27
 
28
+ CMD ["bash"]
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ala Boussoffara
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.
Makefile CHANGED
@@ -1,39 +1,85 @@
1
- .PHONY: help conda-setup precommit lint fmt type test clean docker-build docker-run
 
 
 
 
2
 
3
  help:
4
- @echo "Common Makefile commands:"
5
- @echo " conda-setup - Create and activate conda env, install all dependencies, setup pre-commit"
6
- @echo " precommit - Run all pre-commit hooks (lint, fmt, type)"
7
- @echo " lint - Run ruff linter"
8
- @echo " fmt - Run ruff formatter and black"
9
- @echo " type - Run mypy type checks"
10
- @echo " test - Run all tests with pytest and coverage"
11
- @echo " clean - Remove build, cache, and venv artifacts"
12
- @echo " docker-build - Build the Docker dev image"
13
- @echo " docker-run - Run the Docker dev container (interactive)"
14
-
15
- conda-setup:
16
- conda env create -n transformer -f environment.yml|| conda env update -n transformer -f environment.yml
17
- conda run -n transformer pip install -e .[dev]
18
- conda run -n transformer pre-commit install
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  precommit:
21
- conda run -n transformer pre-commit run --all-files
 
 
 
22
 
23
- lint:
24
- conda run -n transformer ruff check .
25
 
26
  fmt:
27
- conda run -n transformer ruff format .
28
- conda run -n transformer black src tests
 
 
 
 
29
 
30
  type:
31
- conda run -n transformer mypy src
32
 
33
  test:
34
- conda run -n transformer pytest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  clean:
36
- rm -rf .venv .pytest_cache .mypy_cache dist build *.egg-info
 
 
 
37
 
38
  docker-build:
39
  docker compose build dev --no-cache
 
1
+ ENV?=mini-transformer
2
+ CONDA_RUN=conda run -n $(ENV)
3
+ PYTHON?=python
4
+
5
+ .PHONY: help create-env install precommit lint lint-check fmt fmt-check type test cov fetch-test fetch-small infer serve ui clean clean-all docker-build docker-run
6
 
7
  help:
8
+ @echo "Available targets:"
9
+ @echo " create-env - Create or update the $(ENV) conda environment"
10
+ @echo " install - Install project with dev/notebook/viz extras into the active environment"
11
+ @echo " precommit - Run all pre-commit hooks"
12
+ @echo " lint - Run ruff lint (auto-fixes with fmt beforehand)"
13
+ @echo " lint-check - Run ruff in check-only mode"
14
+ @echo " fmt - Format code with ruff + black"
15
+ @echo " fmt-check - Check formatting without modifying files"
16
+ @echo " type - Run mypy type checks"
17
+ @echo " test - Run unit tests"
18
+ @echo " cov - Run unit tests with coverage report"
19
+ @echo " fetch-test - Download the AlaBoussoffara/transformer_test model"
20
+ @echo " fetch-small - Download the AlaBoussoffara/transformer_small model"
21
+ @echo " infer - Quick inference using the demo model"
22
+ @echo " serve - Start the FastAPI server (reload mode)"
23
+ @echo " ui - Launch the Chainlit UI"
24
+ @echo " clean - Remove build/test caches"
25
+ @echo " clean-all - Clean caches plus notebooks outputs"
26
+ @echo " docker-build - Build the development Docker image"
27
+ @echo " docker-run - Drop into the development container"
28
+
29
+ create-env:
30
+ conda env create -n $(ENV) -f environment.yml || conda env update -n $(ENV) -f environment.yml
31
+ $(CONDA_RUN) pip install -e .[dev,notebook,viz]
32
+ $(CONDA_RUN) pre-commit install
33
+
34
+ install:
35
+ $(PYTHON) -m pip install -e .[dev,notebook,viz]
36
 
37
  precommit:
38
+ $(CONDA_RUN) pre-commit run --all-files
39
+
40
+ lint: fmt
41
+ $(CONDA_RUN) ruff check .
42
 
43
+ lint-check:
44
+ $(CONDA_RUN) ruff check .
45
 
46
  fmt:
47
+ $(CONDA_RUN) ruff format .
48
+ $(CONDA_RUN) black src tests
49
+
50
+ fmt-check:
51
+ $(CONDA_RUN) ruff format . --check
52
+ $(CONDA_RUN) black src tests --check
53
 
54
  type:
55
+ $(CONDA_RUN) mypy src
56
 
57
  test:
58
+ $(CONDA_RUN) pytest
59
+
60
+ cov:
61
+ $(CONDA_RUN) pytest --cov=mini_transformer --cov-report=term-missing
62
+
63
+ fetch-test:
64
+ $(CONDA_RUN) mini-transformer-fetch AlaBoussoffara/transformer_test --force
65
+
66
+ fetch-small:
67
+ $(CONDA_RUN) mini-transformer-fetch AlaBoussoffara/transformer_small --force
68
+
69
+ infer:
70
+ $(CONDA_RUN) mini-transformer-infer --model small_model_v1 -t "translate this sentence."
71
+
72
+ serve:
73
+ $(CONDA_RUN) mini-transformer-serve --model small_model_v1 --reload
74
+
75
+ ui:
76
+ $(CONDA_RUN) mini-transformer-ui --model small_model_v1 --host 0.0.0.0 --port 8000
77
+
78
  clean:
79
+ rm -rf .pytest_cache .mypy_cache .ruff_cache dist build *.egg-info
80
+
81
+ clean-all: clean
82
+ rm -rf notebooks/.ipynb_checkpoints outputs/*
83
 
84
  docker-build:
85
  docker compose build dev --no-cache
README.md CHANGED
@@ -1,8 +1,130 @@
1
- # transformer
2
 
3
- Minimal encoder–decoder Transformer.
4
 
5
- ## Quickstart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  ```bash
7
  make conda-setup
 
 
8
  make test
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mini-transformer
2
 
3
+ A compact, encoder–decoder Transformer packaged so it can be used both as a Python library and as a ready-to-run demo. The repository mirrors the installable package layout so cloning the repo or `pip install mini-transformer` gives the same structure and tooling.
4
 
5
+ ## Highlights
6
+ - Typed, unit-tested implementation of an encoder–decoder Transformer with reusable building blocks (`mini_transformer/modules`).
7
+ - Hydra configuration system with both packaged defaults (`mini_transformer/conf`) and editable configs in the repo.
8
+ - Ready-made UIs: FastAPI REST server and Chainlit chat interface.
9
+ - CLI convenience commands for inference, serving, UI launch, and Hugging Face model downloads.
10
+ - Makefile shortcuts, notebooks, and Docker dev environment for day-to-day work.
11
+
12
+ ## Installation
13
+ ```bash
14
+ # inside the repository (editable install with optional extras)
15
+ pip install -e .[server,viz]
16
+ ```
17
+ Or install the published package:
18
+ ```bash
19
+ pip install "mini-transformer[server,viz]"
20
+ ```
21
+
22
+ ## Repository Layout
23
+ - `src/mini_transformer/` – installable package (model code, CLI, apps, packaged Hydra configs).
24
+ - `configs/` – editable Hydra configs for local experiments.
25
+ - `trained_models/` – place downloaded or exported checkpoints here (see below).
26
+ - `notebooks/` – exploratory notebooks (`train.ipynb`, `tokenizer.ipynb`, etc.).
27
+ - `tests/` – unit and smoke tests.
28
+ - Supporting files: `pyproject.toml`, `Makefile`, `Dockerfile.dev`, `environment.yml`, etc.
29
+
30
+ ## Preparing Models
31
+ Download the demo models hosted on Hugging Face:
32
+ ```bash
33
+ mini-transformer-fetch --model-id AlaBoussoffara/transformer_test
34
+ mini-transformer-fetch --model-id AlaBoussoffara/transformer_small
35
+ ```
36
+ Use `--name` to customise the local directory and `--force` to refresh an existing download.
37
+ The local folder name defaults to `<author>__<repo>` (e.g. `AlaBoussoffara__transformer_small`). The Hugging Face repo is downloaded as a whole, so if the model files live inside a subfolder (for example `transformer_small/small_model_v1/`), extract or move those contents up into the root of `trained_models/<local-name>/` so the structure contains `configs/`, `checkpoints/`, and `tokenizer/` directly.
38
+ In other words, after fetching `AlaBoussoffara/transformer_small`, move the `transformer_small/small_model_v1/` directory into `trained_models/AlaBoussoffara__transformer_small/`.
39
+
40
+ You can also place your own model manually under `trained_models/<model-name>/`:
41
+ ```
42
+ trained_models/
43
+ my-model/
44
+ configs/
45
+ config_inference.yaml
46
+ checkpoints/
47
+ best.pt
48
+ tokenizer/
49
+ tokenizer.json
50
+ ```
51
+ Relative paths in `config_inference.yaml` should stay inside the model folder. Set `MINI_TRANSFORMER_MODELS=/path/to/trained_models` if you store models elsewhere.
52
+
53
+ ## CLI Usage
54
+ Install the optional extras (see Installation) and use the commands below.
55
+
56
+ ```bash
57
+ mini-transformer-infer --model AlaBoussoffara__transformer_small -t "Once upon a time"
58
+ mini-transformer-serve --model AlaBoussoffara__transformer_small --reload
59
+ mini-transformer-ui --model AlaBoussoffara__transformer_small --host 0.0.0.0 --port 8000
60
+ ```
61
+ If you see “Tokenizer file not found”, update the model config or set `MINI_TRANSFORMER_TOKENIZER_PATH` to the correct JSON file.
62
+
63
+ ## Chainlit Demo
64
+ Follow these steps to spin up the bundled Chainlit chat UI for a local inference demo.
65
+
66
+ 1. Install the project with the server extras (or run `make create-env`):
67
+ ```bash
68
+ pip install -e .[server]
69
+ ```
70
+ 2. Download a demo checkpoint (skip if you already have one under `trained_models/`):
71
+ ```bash
72
+ mini-transformer-fetch --model-id AlaBoussoffara/transformer_small
73
+ ```
74
+ 3. Launch the Chainlit UI, pointing to the model folder you want to use (defaults to the first available model):
75
+ ```bash
76
+ mini-transformer-ui --model AlaBoussoffara__transformer_small --host 0.0.0.0 --port 8000
77
+ ```
78
+ 4. Open http://localhost:8000 in your browser, send a prompt, and use `/model` in chat to switch between downloaded checkpoints.
79
+
80
+ ### Environment Variables
81
+ - `MINI_TRANSFORMER_MODELS` – override the models root directory.
82
+ - `MINI_TRANSFORMER_MODEL_NAME` – preselect a model for the server/UI.
83
+ - `MINI_TRANSFORMER_CONFIG_DIR` / `MINI_TRANSFORMER_CONFIG_NAME` – point to custom Hydra configs.
84
+ - `MINI_TRANSFORMER_UI_HOST` / `MINI_TRANSFORMER_UI_PORT` – defaults for Chainlit binding.
85
+ - Optional overrides: `MINI_TRANSFORMER_CHECKPOINT_BEST`, `MINI_TRANSFORMER_TOKENIZER_PATH`, `MINI_TRANSFORMER_OUTPUT_DIR`, etc.
86
+
87
+ ## Programmatic Inference
88
+ ```python
89
+ from mini_transformer.model_loader import compose_model_config
90
+ from mini_transformer.inference import run_inference
91
+
92
+ cfg = compose_model_config("AlaBoussoffara__transformer_small")
93
+ cfg.input_text = "Hello world"
94
+ print(run_inference(cfg)[0])
95
+ ```
96
+
97
+ ## Development Quickstart
98
  ```bash
99
  make conda-setup
100
+ make lint
101
+ make type
102
  make test
103
+ ```
104
+
105
+ ## Makefile Shortcuts
106
+ ```bash
107
+ make help # list common tasks
108
+ make create-env # create/update the conda env and install extras
109
+ make lint # format + lint (ruff + black)
110
+ make lint-check # lint without auto-fixes
111
+ make fmt # format code
112
+ make type # mypy
113
+ make test # pytest
114
+ make cov # pytest with coverage
115
+ make fetch-test # download AlaBoussoffara/transformer_test
116
+ make fetch-small # download AlaBoussoffara/transformer_small
117
+ make infer # quick demo inference
118
+ make serve # run FastAPI server (reload mode)
119
+ make ui # launch Chainlit UI
120
+ make docker-build # build the dev container
121
+ make docker-run # open an interactive shell in the container
122
+ ```
123
+
124
+ ## Full Workflow At A Glance
125
+ 1. Fetch a model: `mini-transformer-fetch --model-id AlaBoussoffara/transformer_small`
126
+ 2. Run inference: `mini-transformer-infer --model AlaBoussoffara__transformer_small -t "Once upon a time"`
127
+ 3. Launch FastAPI (optional): `mini-transformer-serve --model AlaBoussoffara__transformer_small --reload`
128
+ 4. Start Chainlit UI (optional): `mini-transformer-ui --model AlaBoussoffara__transformer_small --host 0.0.0.0 --port 8000`
129
+
130
+ Environment variables (`MINI_TRANSFORMER_MODELS`, `MINI_TRANSFORMER_MODEL_NAME`, etc.) let you tailor the workflow to your setup.
architecture.txt DELETED
@@ -1,28 +0,0 @@
1
- Transformer:
2
- embedding:
3
- input_embedding (same for encoder and decoder)
4
- positional_embedding (learned or sinusoidal)
5
- dropout
6
- encoder
7
- encoder_layer *N
8
- multihead_self_attention
9
- dropout+norm
10
- feedforward:
11
- linear
12
- relu
13
- linear
14
- dropout+norm
15
- decoder
16
- decoder_layer*N
17
- masked_multihead_self_attention
18
- dropout+norm
19
- multihead_cross_attention
20
- dropout+norm
21
- feedforward:
22
- linear
23
- relu
24
- linear
25
- dropout+norm
26
- lm_head:
27
- linear (same parameters as the embedding)
28
- softmax
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chainlit.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Welcome to Chainlit! 🚀🤖
2
+
3
+ Hi there, Developer! 👋 We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
4
+
5
+ ## Useful Links 🔗
6
+
7
+ - **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) 📚
8
+ - **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! 💬
9
+
10
+ We can't wait to see what you create with Chainlit! Happy coding! 💻😊
11
+
12
+ ## Welcome screen
13
+
14
+ To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
configs/generation/generation.yaml CHANGED
@@ -1,4 +1,4 @@
1
- max_new_tokens: 64
2
  temperature: 1
3
  top_k: 10
4
  top_p: 0.6
 
1
+ max_new_tokens: 128
2
  temperature: 1
3
  top_k: 10
4
  top_p: 0.6
configs/model/large_model.yaml CHANGED
@@ -2,9 +2,9 @@ name: large_model
2
  best_checkpoint_path: ./outputs/checkpoints/large_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/large_model/latest.ckpt
4
  tokenizer: bpe_32k
5
- d_model: 512
6
- num_heads: 8
7
- num_layers: 6
8
  d_ff: 2048
9
  dropout_rate: 0.1
10
  vocab_size: 32768
@@ -12,3 +12,4 @@ max_seq_len: 256
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
 
 
2
  best_checkpoint_path: ./outputs/checkpoints/large_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/large_model/latest.ckpt
4
  tokenizer: bpe_32k
5
+ d_model: 2048
6
+ num_heads: 32
7
+ num_layers: 16
8
  d_ff: 2048
9
  dropout_rate: 0.1
10
  vocab_size: 32768
 
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
15
+ layer_norm_style: pre
configs/model/medium_model.yaml CHANGED
@@ -2,13 +2,14 @@ name: medium_model
2
  best_checkpoint_path: ./outputs/checkpoints/medium_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/medium_model/latest.ckpt
4
  tokenizer: bpe_16k
5
- d_model: 512
6
- num_heads: 8
7
- num_layers: 6
8
  d_ff: 2048
9
  dropout_rate: 0.1
10
  vocab_size: 16384
11
- max_seq_len: 256
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
 
 
2
  best_checkpoint_path: ./outputs/checkpoints/medium_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/medium_model/latest.ckpt
4
  tokenizer: bpe_16k
5
+ d_model: 1024
6
+ num_heads: 16
7
+ num_layers: 8
8
  d_ff: 2048
9
  dropout_rate: 0.1
10
  vocab_size: 16384
11
+ max_seq_len: 128
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
15
+ layer_norm_style: pre
configs/model/small_model.yaml CHANGED
@@ -2,8 +2,8 @@ name: small_model
2
  best_checkpoint_path: ./outputs/checkpoints/small_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/small_model/latest.ckpt
4
  tokenizer: bpe_8k
5
- d_model: 256
6
- num_heads: 8
7
  num_layers: 4
8
  d_ff: 1024
9
  dropout_rate: 0.1
@@ -12,3 +12,4 @@ max_seq_len: 128
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
 
 
2
  best_checkpoint_path: ./outputs/checkpoints/small_model/best.ckpt
3
  latest_checkpoint_path: ./outputs/checkpoints/small_model/latest.ckpt
4
  tokenizer: bpe_8k
5
+ d_model: 512
6
+ num_heads: 4
7
  num_layers: 4
8
  d_ff: 1024
9
  dropout_rate: 0.1
 
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
15
+ layer_norm_style: post
configs/model/test_model.yaml CHANGED
@@ -3,8 +3,8 @@ best_checkpoint_path: ./outputs/checkpoints/test_model/best.pt
3
  latest_checkpoint_path: ./outputs/checkpoints/test_model/latest.pt
4
  tokenizer: bpe_8k
5
  d_model: 512
6
- num_heads: 8
7
- num_layers: 8
8
  d_ff: 1024
9
  dropout_rate: 0.1
10
  vocab_size: 8192
@@ -12,3 +12,4 @@ max_seq_len: 128
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
 
 
3
  latest_checkpoint_path: ./outputs/checkpoints/test_model/latest.pt
4
  tokenizer: bpe_8k
5
  d_model: 512
6
+ num_heads: 4
7
+ num_layers: 4
8
  d_ff: 1024
9
  dropout_rate: 0.1
10
  vocab_size: 8192
 
12
  pad_id: 0
13
  bos_id: 1
14
  eos_id: 2
15
+ layer_norm_style: post
docker-compose.yml CHANGED
@@ -3,12 +3,19 @@ services:
3
  build:
4
  context: .
5
  dockerfile: Dockerfile.dev
6
- image: transformer:dev
7
- container_name: transformer_dev
8
  working_dir: /app
9
  volumes:
10
- - ./artifacts:/app/artifacts
 
 
 
 
 
11
  stdin_open: true
12
  tty: true
13
  gpus: all
14
  command: bash
 
 
 
3
  build:
4
  context: .
5
  dockerfile: Dockerfile.dev
6
+ image: mini-transformer:dev
7
+ container_name: mini_transformer_dev
8
  working_dir: /app
9
  volumes:
10
+ - ./outputs:/app/outputs
11
+ - ./trained_models:/app/trained_models
12
+ environment:
13
+ MINI_TRANSFORMER_MODELS: /app/trained_models
14
+ MINI_TRANSFORMER_UI_HOST: 0.0.0.0
15
+ MINI_TRANSFORMER_UI_PORT: 8000
16
  stdin_open: true
17
  tty: true
18
  gpus: all
19
  command: bash
20
+ ports:
21
+ - "8000:8000"
environment.yml CHANGED
@@ -1,17 +1,28 @@
1
- name: transformer
2
  channels:
3
  - defaults
4
  dependencies:
5
- - python=3.12
6
  - pip
7
  - pip:
8
- - --extra-index-url https://download.pytorch.org/whl/cu129
9
- - torch==2.8.0+cu129
 
10
  - transformers
11
  - omegaconf
12
  - hydra-core
13
- - jupyterlab
14
  - tokenizers
15
  - datasets
 
 
 
16
  - fastapi
17
  - uvicorn[standard]
 
 
 
 
 
 
 
 
 
1
+ name: mini-transformer
2
  channels:
3
  - defaults
4
  dependencies:
5
+ - python=3.11
6
  - pip
7
  - pip:
8
+ - --extra-index-url https://download.pytorch.org/whl/cu121
9
+ - torch==2.3.1
10
+ - numpy
11
  - transformers
12
  - omegaconf
13
  - hydra-core
 
14
  - tokenizers
15
  - datasets
16
+ - huggingface_hub
17
+ - matplotlib
18
+ - jupyterlab
19
  - fastapi
20
  - uvicorn[standard]
21
+ - chainlit
22
+ - pytest
23
+ - pytest-cov
24
+ - ruff
25
+ - black
26
+ - mypy
27
+ - pre-commit
28
+ - httpx
{scripts → notebooks}/load_data.ipynb RENAMED
@@ -40,7 +40,7 @@
40
  "name": "stderr",
41
  "output_type": "stream",
42
  "text": [
43
- "/home/ala-boussoffara/miniconda3/envs/transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
44
  " from .autonotebook import tqdm as notebook_tqdm\n",
45
  "Generating train split: 179435 examples [00:00, 1366541.45 examples/s]\n",
46
  "Generating validation split: 903 examples [00:00, 558869.19 examples/s]\n",
@@ -98,7 +98,7 @@
98
  "import yaml\n",
99
  "from datasets import DatasetDict\n",
100
  "\n",
101
- "from transformer.utils import set_global_seed\n",
102
  "\n",
103
  "with open(\"configs/runtime/local.yaml\", encoding=\"utf-8\") as cfg_file:\n",
104
  " runtime_cfg = yaml.safe_load(cfg_file) or {}\n",
@@ -202,10 +202,10 @@
202
  "import numpy as np\n",
203
  "from hydra import compose, initialize\n",
204
  "from hydra.core.global_hydra import GlobalHydra\n",
 
205
  "from omegaconf import OmegaConf\n",
206
- "from transformers import PreTrainedTokenizerFast\n",
207
  "\n",
208
- "from transformer.configs import TokenizerCfg\n",
209
  "\n",
210
  "\n",
211
  "def load_tokenizer(name: str) -> PreTrainedTokenizerFast:\n",
 
40
  "name": "stderr",
41
  "output_type": "stream",
42
  "text": [
43
+ "/home/ala-boussoffara/miniconda3/envs/mini-transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
44
  " from .autonotebook import tqdm as notebook_tqdm\n",
45
  "Generating train split: 179435 examples [00:00, 1366541.45 examples/s]\n",
46
  "Generating validation split: 903 examples [00:00, 558869.19 examples/s]\n",
 
98
  "import yaml\n",
99
  "from datasets import DatasetDict\n",
100
  "\n",
101
+ "from mini_transformer.utils import set_global_seed\n",
102
  "\n",
103
  "with open(\"configs/runtime/local.yaml\", encoding=\"utf-8\") as cfg_file:\n",
104
  " runtime_cfg = yaml.safe_load(cfg_file) or {}\n",
 
202
  "import numpy as np\n",
203
  "from hydra import compose, initialize\n",
204
  "from hydra.core.global_hydra import GlobalHydra\n",
205
+ "from mini_transformers import PreTrainedTokenizerFast\n",
206
  "from omegaconf import OmegaConf\n",
 
207
  "\n",
208
+ "from mini_transformer.configs import TokenizerCfg\n",
209
  "\n",
210
  "\n",
211
  "def load_tokenizer(name: str) -> PreTrainedTokenizerFast:\n",
{scripts → notebooks}/tokenizer.ipynb RENAMED
@@ -18,7 +18,7 @@
18
  "name": "stderr",
19
  "output_type": "stream",
20
  "text": [
21
- "/home/ala-boussoffara/miniconda3/envs/transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
22
  " from .autonotebook import tqdm as notebook_tqdm\n"
23
  ]
24
  }
@@ -31,7 +31,7 @@
31
  "from tokenizers import Tokenizer, models, pre_tokenizers, trainers\n",
32
  "from tokenizers.processors import TemplateProcessing\n",
33
  "\n",
34
- "from transformer.configs import TokenizerCfg"
35
  ]
36
  },
37
  {
 
18
  "name": "stderr",
19
  "output_type": "stream",
20
  "text": [
21
+ "/home/ala-boussoffara/miniconda3/envs/mini-transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
22
  " from .autonotebook import tqdm as notebook_tqdm\n"
23
  ]
24
  }
 
31
  "from tokenizers import Tokenizer, models, pre_tokenizers, trainers\n",
32
  "from tokenizers.processors import TemplateProcessing\n",
33
  "\n",
34
+ "from mini_transformer.configs import TokenizerCfg"
35
  ]
36
  },
37
  {
{scripts → notebooks}/train.ipynb RENAMED
@@ -30,15 +30,15 @@
30
  "from hydra import compose, initialize\n",
31
  "from hydra.core.global_hydra import GlobalHydra\n",
32
  "from hydra.utils import to_absolute_path\n",
 
33
  "from omegaconf import OmegaConf\n",
34
  "from torch.nn.utils import clip_grad_norm_\n",
35
  "from torch.utils.data import DataLoader\n",
36
  "from tqdm import tqdm\n",
37
- "from transformers import PreTrainedTokenizerFast\n",
38
  "\n",
39
- "from transformer import BasicEncoderDecoderTransformer\n",
40
- "from transformer.configs import ModelCfg, TokenizerCfg, TrainAppCfg\n",
41
- "from transformer.utils import (\n",
42
  " check_tokenizer_model_compatibility,\n",
43
  " debug_transformer_forward,\n",
44
  " make_worker_init_fn,\n",
@@ -277,7 +277,7 @@
277
  " raise ValueError(\"No trainable parameters found when setting up the optimizer.\")\n",
278
  "\n",
279
  " return torch.optim.AdamW(\n",
280
- " model.parameters(),\n",
281
  " lr=optim_cfg.lr,\n",
282
  " betas=optim_cfg.betas,\n",
283
  " eps=optim_cfg.eps,\n",
 
30
  "from hydra import compose, initialize\n",
31
  "from hydra.core.global_hydra import GlobalHydra\n",
32
  "from hydra.utils import to_absolute_path\n",
33
+ "from mini_transformers import PreTrainedTokenizerFast\n",
34
  "from omegaconf import OmegaConf\n",
35
  "from torch.nn.utils import clip_grad_norm_\n",
36
  "from torch.utils.data import DataLoader\n",
37
  "from tqdm import tqdm\n",
 
38
  "\n",
39
+ "from mini_transformer import BasicEncoderDecoderTransformer\n",
40
+ "from mini_transformer.configs import ModelCfg, TokenizerCfg, TrainAppCfg\n",
41
+ "from mini_transformer.utils import (\n",
42
  " check_tokenizer_model_compatibility,\n",
43
  " debug_transformer_forward,\n",
44
  " make_worker_init_fn,\n",
 
277
  " raise ValueError(\"No trainable parameters found when setting up the optimizer.\")\n",
278
  "\n",
279
  " return torch.optim.AdamW(\n",
280
+ " param_groups,\n",
281
  " lr=optim_cfg.lr,\n",
282
  " betas=optim_cfg.betas,\n",
283
  " eps=optim_cfg.eps,\n",
pyproject.toml CHANGED
@@ -1,65 +1,91 @@
1
- [project]
2
- name = "transformer"
3
- version = "0.1.0"
4
- description = "A transformer implementation"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = [
8
- "torch>=2.6",
9
- "transformers",
10
- "omegaconf",
11
- "hydra-core",
12
- "jupyterlab",
13
- "tokenizers",
14
- "datasets",
15
- ]
16
-
17
- [project.optional-dependencies]
18
- dev = [
19
- "pytest",
20
- "pytest-cov",
21
- "ruff",
22
- "black",
23
- "mypy",
24
- "pre-commit",
25
- ]
26
- server = [
27
- "fastapi",
28
- "uvicorn[standard]",
29
- ]
30
-
31
- [tool.setuptools]
32
- package-dir = {"" = "src"}
33
-
34
- [build-system]
35
- requires = ["setuptools>=42", "wheel"]
36
- build-backend = "setuptools.build_meta"
37
-
38
- [tool.setuptools.packages.find]
39
- where = ["src"]
40
-
41
- [tool.pytest.ini_options]
42
- minversion = "7.0"
43
- addopts = "-ra -q"
44
- testpaths = ["tests"]
45
-
46
- [tool.black]
47
- line-length = 100
48
- target-version = ["py310"]
49
-
50
- [tool.ruff]
51
- line-length = 100
52
-
53
- [tool.ruff.lint]
54
- select = ["E","F","I","B","UP"]
55
- ignore = ["E501"]
56
-
57
- [tool.mypy]
58
- python_version = "3.11"
59
- strict = false
60
- ignore_missing_imports = true
61
-
62
- # Make mypy understand the src/ layout
63
- files = ["src"]
64
- mypy_path = ["src"]
65
- explicit_package_bases = true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "mini-transformer"
3
+ version = "0.1.0"
4
+ description = "A mini encoder–decoder transformer implementation"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { file = "LICENSE" }
8
+ authors = [
9
+ { name = "Ala Boussoffara" }
10
+ ]
11
+ dependencies = [
12
+ "torch>=2.3",
13
+ "numpy",
14
+ "transformers",
15
+ "omegaconf",
16
+ "hydra-core",
17
+ "tokenizers",
18
+ "datasets",
19
+ "huggingface_hub",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "pytest",
25
+ "pytest-cov",
26
+ "ruff",
27
+ "black",
28
+ "mypy",
29
+ "pre-commit",
30
+ "httpx",
31
+ "fastapi",
32
+ "uvicorn[standard]",
33
+ "chainlit",
34
+ ]
35
+ server = [
36
+ "fastapi",
37
+ "uvicorn[standard]",
38
+ "chainlit",
39
+ ]
40
+ viz = [
41
+ "matplotlib",
42
+ ]
43
+ notebook = [
44
+ "jupyterlab",
45
+ ]
46
+
47
+
48
+ [project.scripts]
49
+ mini-transformer-infer = "mini_transformer.cli:infer_main"
50
+ mini-transformer-serve = "mini_transformer.cli:serve_main"
51
+ mini-transformer-ui = "mini_transformer.cli:ui_main"
52
+ mini-transformer-fetch = "mini_transformer.cli:fetch_main"
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+
57
+ [tool.setuptools.package-data]
58
+ "mini_transformer" = ["conf/**/*.yaml"]
59
+
60
+ [build-system]
61
+ requires = ["setuptools>=42", "wheel"]
62
+ build-backend = "setuptools.build_meta"
63
+
64
+ [tool.setuptools.packages.find]
65
+ where = ["src"]
66
+
67
+ [tool.pytest.ini_options]
68
+ minversion = "7.0"
69
+ addopts = "-ra -q"
70
+ testpaths = ["tests"]
71
+
72
+ [tool.black]
73
+ line-length = 100
74
+ target-version = ["py310"]
75
+
76
+ [tool.ruff]
77
+ line-length = 100
78
+
79
+ [tool.ruff.lint]
80
+ select = ["E","F","I","B","UP"]
81
+ ignore = ["E501"]
82
+
83
+ [tool.mypy]
84
+ python_version = "3.11"
85
+ strict = false
86
+ ignore_missing_imports = true
87
+
88
+ # Make mypy understand the src/ layout
89
+ files = ["src"]
90
+ mypy_path = ["src"]
91
+ explicit_package_bases = true
requirements.txt DELETED
Binary file (357 Bytes)
 
scripts/inference.py DELETED
@@ -1,76 +0,0 @@
1
- """CLI entry point for running transformer inference with deterministic sampling."""
2
-
3
- from __future__ import annotations
4
-
5
- import hydra
6
- import torch
7
- from hydra.utils import to_absolute_path
8
- from omegaconf import DictConfig, OmegaConf
9
- from transformers import PreTrainedTokenizerFast
10
-
11
- from transformer import BasicEncoderDecoderTransformer
12
- from transformer.configs import InferAppCfg, ModelCfg, TokenizerCfg
13
- from transformer.utils import check_tokenizer_model_compatibility, set_global_seed
14
-
15
-
16
- def inference(cfg: DictConfig):
17
- scfg_temp = OmegaConf.merge(OmegaConf.structured(InferAppCfg), cfg)
18
- scfg: InferAppCfg = OmegaConf.to_object(scfg_temp)
19
- model_cfg = ModelCfg(**vars(scfg.model))
20
- tokenizer_cfg = TokenizerCfg(**vars(scfg.tokenizer))
21
-
22
- # Ensure reproducible sampling when requested
23
- set_global_seed(scfg.runtime.seed)
24
-
25
- # Compatibility checks
26
- check_tokenizer_model_compatibility(model_cfg, tokenizer_cfg)
27
-
28
- transformer = BasicEncoderDecoderTransformer(model_cfg)
29
- tokenizer = PreTrainedTokenizerFast(
30
- tokenizer_file=to_absolute_path(tokenizer_cfg.path),
31
- bos_token=cfg.tokenizer.bos_token,
32
- eos_token=cfg.tokenizer.eos_token,
33
- unk_token=cfg.tokenizer.unk_token,
34
- pad_token=cfg.tokenizer.pad_token,
35
- model_max_length=tokenizer_cfg.max_seq_len,
36
- )
37
-
38
- if scfg.model.best_checkpoint_path:
39
- ckpt = torch.load(to_absolute_path(scfg.model.best_checkpoint_path), map_location="cpu")
40
- state = ckpt.get("model_state_dict", ckpt)
41
- transformer.load_state_dict(state, strict=False)
42
- transformer.eval()
43
-
44
- text = scfg.input_text
45
- if not text:
46
- raise SystemExit('Pass text like: input_text="hello world"')
47
-
48
- encoded = tokenizer(text, padding=True, truncation=True, return_tensors="pt")
49
- src_ids = encoded["input_ids"]
50
- src_padd_mask = encoded["attention_mask"] == 0
51
-
52
- tgt_ids = transformer.generate(
53
- src_ids,
54
- src_padd_mask,
55
- max_new_tokens=scfg.generation.max_new_tokens,
56
- temperature=scfg.generation.temperature,
57
- top_k=scfg.generation.top_k,
58
- top_p=scfg.generation.top_p,
59
- do_sample=scfg.generation.do_sample,
60
- presence_penalty=scfg.generation.presence_penalty,
61
- frequency_penalty=scfg.generation.frequency_penalty,
62
- no_repeat_ngram=scfg.generation.no_repeat_ngram,
63
- min_steps_before_eos=scfg.generation.min_steps_before_eos,
64
- seed=scfg.runtime.seed,
65
- )
66
- output = tokenizer.batch_decode(tgt_ids, skip_special_tokens=False)
67
- return output
68
-
69
-
70
- @hydra.main(config_path="../configs", config_name="infer_mode", version_base=None)
71
- def main(cfg: DictConfig):
72
- print(inference(cfg))
73
-
74
-
75
- if __name__ == "__main__":
76
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/server.py DELETED
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from fastapi import FastAPI
4
- from hydra import compose, initialize
5
- from omegaconf import DictConfig
6
-
7
- from scripts.inference import inference as run_inference
8
-
9
- app = FastAPI(title="Transformer Inference API")
10
-
11
-
12
- @app.get("/healthz")
13
- def healthz():
14
- return {"status": "ok"}
15
-
16
-
17
- @app.get("/generate")
18
- def generate(text: str | None = None):
19
- # Compose the default config exactly as inference.py expects
20
- with initialize(config_path="../configs", version_base=None):
21
- cfg: DictConfig = compose(config_name="infer_mode")
22
- # If a text query parameter is provided, override the config's input_text
23
- if text is not None and text.strip():
24
- cfg.input_text = text
25
- outputs = run_inference(cfg) # returns a list of strings
26
- return {"outputs": outputs}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/mini_transformer/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """mini_transformer public API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import metadata
6
+
7
+ from .transformer import BasicEncoderDecoderTransformer
8
+
9
+ try: # pragma: no cover - during editable installs metadata may be missing
10
+ __version__ = metadata.version("mini-transformer")
11
+ except metadata.PackageNotFoundError: # pragma: no cover
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = ["BasicEncoderDecoderTransformer", "__version__"]
src/mini_transformer/apps/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Interactive applications (Chainlit UI & FastAPI server)."""
2
+
3
+ __all__ = ["chainlit_app", "server"]
src/mini_transformer/apps/chainlit_app.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import chainlit as cl
7
+ from hydra.core.global_hydra import GlobalHydra
8
+ from omegaconf import DictConfig
9
+
10
+ from mini_transformer.inference import run_inference
11
+ from mini_transformer.model_loader import (
12
+ MODEL_NAME_ENV,
13
+ compose_model_config,
14
+ ensure_models_root,
15
+ list_model_names,
16
+ )
17
+
18
+ JOB_NAME = "chainlit_app"
19
+ MODEL_SESSION_KEY = "selected_model"
20
+ HAS_ACTION_PROMPT = hasattr(cl, "AskActionMessage") and hasattr(cl, "Action")
21
+
22
+
23
+ def _discover_action_fields() -> set[str]:
24
+ if not HAS_ACTION_PROMPT:
25
+ return set()
26
+ action_cls = cl.Action
27
+ for attr in ("model_fields", "__fields__"):
28
+ fields = getattr(action_cls, attr, None)
29
+ if isinstance(fields, dict):
30
+ return set(fields.keys())
31
+ return set()
32
+
33
+
34
+ ACTION_FIELDS = _discover_action_fields()
35
+
36
+
37
+ class ModelOption:
38
+ """Lightweight container describing a trained model configuration."""
39
+
40
+ def __init__(self, name: str) -> None:
41
+ self.name = name
42
+
43
+ @property
44
+ def job_name(self) -> str:
45
+ return f"{JOB_NAME}_{self.name}"
46
+
47
+
48
+ def discover_model_options() -> list[ModelOption]:
49
+ ensure_models_root()
50
+ return [ModelOption(name) for name in list_model_names()]
51
+
52
+
53
+ def restore_model_from_session() -> ModelOption | None:
54
+ data: dict[str, str] | None = cl.user_session.get(MODEL_SESSION_KEY)
55
+ if not data:
56
+ env_model = os.environ.get(MODEL_NAME_ENV)
57
+ if env_model and env_model in list_model_names():
58
+ return ModelOption(name=env_model)
59
+ return None
60
+ name = data.get("name")
61
+ if not name:
62
+ return None
63
+ if name not in list_model_names():
64
+ return None
65
+ return ModelOption(name=name)
66
+
67
+
68
+ def persist_model_selection(option: ModelOption) -> None:
69
+ cl.user_session.set(MODEL_SESSION_KEY, {"name": option.name})
70
+
71
+
72
+ def extract_action_value(result: Any) -> str | None:
73
+ if result is None:
74
+ return None
75
+ if isinstance(result, str):
76
+ return result
77
+ if isinstance(result, dict):
78
+ payload = result.get("payload")
79
+ if isinstance(payload, dict):
80
+ selected = payload.get("model") or payload.get("value") or payload.get("name")
81
+ if selected:
82
+ return str(selected)
83
+ for key in ("value", "name", "action"):
84
+ value = result.get(key)
85
+ if value:
86
+ return str(value)
87
+ return None
88
+ value = getattr(result, "value", None)
89
+ if value:
90
+ return str(value)
91
+ name = getattr(result, "name", None)
92
+ if name:
93
+ return str(name)
94
+ payload = getattr(result, "payload", None)
95
+ if isinstance(payload, dict):
96
+ selected = payload.get("model") or payload.get("value") or payload.get("name")
97
+ if selected:
98
+ return str(selected)
99
+ return None
100
+
101
+
102
+ async def prompt_model_selection(header: str | None = None) -> ModelOption | None:
103
+ options = discover_model_options()
104
+ if not options:
105
+ await cl.Message(
106
+ content=(
107
+ "No trained models found in `trained_models/`. "
108
+ "Place a model folder with configs/config_inference.yaml there and restart the chat."
109
+ )
110
+ ).send()
111
+ return None
112
+
113
+ if not HAS_ACTION_PROMPT:
114
+ chosen = options[0]
115
+ await cl.Message(
116
+ content=f"Defaulting to `{chosen.name}`. Action prompts are unavailable."
117
+ ).send()
118
+ return chosen
119
+
120
+ fields = ACTION_FIELDS or {"name", "value", "label", "payload"}
121
+ actions = []
122
+ for option in options:
123
+ kwargs: dict[str, Any] = {"name": option.name}
124
+ if "label" in fields:
125
+ kwargs["label"] = option.name
126
+ if "value" in fields:
127
+ kwargs["value"] = option.name
128
+ if not fields or "payload" in fields:
129
+ kwargs["payload"] = {"model": option.name}
130
+ actions.append(cl.Action(**kwargs))
131
+ message = header or "Select a trained model to use for inference:"
132
+ result = await cl.AskActionMessage(content=message, actions=actions).send()
133
+ selected_name = extract_action_value(result)
134
+ if not selected_name:
135
+ selected_name = options[0].name
136
+ return next((opt for opt in options if opt.name == selected_name), options[0])
137
+
138
+
139
+ def build_config(user_input: str, model: ModelOption) -> DictConfig:
140
+ if GlobalHydra.instance().is_initialized():
141
+ GlobalHydra.instance().clear()
142
+ cfg = compose_model_config(model.name, job_name=model.job_name)
143
+ cfg.input_text = user_input
144
+ return cfg
145
+
146
+
147
+ async def ensure_model_selected(header: str | None = None) -> ModelOption | None:
148
+ model = restore_model_from_session()
149
+ if model:
150
+ return model
151
+ model = await prompt_model_selection(header=header)
152
+ if model:
153
+ persist_model_selection(model)
154
+ return model
155
+
156
+
157
+ async def handle_model_command(content: str) -> None:
158
+ parts = content.split(maxsplit=1)
159
+ options = discover_model_options()
160
+ if not options:
161
+ await cl.Message(
162
+ content="No trained models available to select. Add a model under `trained_models/` and try again."
163
+ ).send()
164
+ return
165
+
166
+ if len(parts) == 1:
167
+ model = await prompt_model_selection(header="Pick a new model:")
168
+ else:
169
+ requested = parts[1].strip()
170
+ model = next((opt for opt in options if opt.name == requested), None)
171
+ if not model:
172
+ available = ", ".join(opt.name for opt in options)
173
+ await cl.Message(
174
+ content=f"Model `{requested}` not found. Available models: {available}."
175
+ ).send()
176
+ return
177
+ if model:
178
+ persist_model_selection(model)
179
+ await cl.Message(
180
+ content=f"Switched to `{model.name}`. Provide text to generate a continuation."
181
+ ).send()
182
+
183
+
184
+ @cl.on_chat_start
185
+ async def on_chat_start() -> None:
186
+ model = await ensure_model_selected()
187
+ if not model:
188
+ return
189
+ await cl.Message(
190
+ content=(
191
+ f"Using trained model `{model.name}`. Send text for generation, or type `/model` to switch models."
192
+ )
193
+ ).send()
194
+
195
+
196
+ @cl.on_message
197
+ async def on_message(message: cl.Message) -> None:
198
+ user_input = message.content.strip()
199
+
200
+ if user_input.lower().startswith("/model"):
201
+ await handle_model_command(user_input)
202
+ return
203
+
204
+ if not user_input:
205
+ await cl.Message(content="Please provide some text for the model.").send()
206
+ return
207
+
208
+ model = await ensure_model_selected(header="Select a model before requesting generations:")
209
+ if not model:
210
+ return
211
+
212
+ cfg = build_config(user_input, model)
213
+ try:
214
+ outputs = run_inference(cfg)
215
+ except Exception as exc: # pragma: no cover
216
+ await cl.Message(content=f"Inference error: {exc}").send()
217
+ return
218
+
219
+ response = outputs[0] if outputs else "No output generated."
220
+ await cl.Message(content=response).send()
src/mini_transformer/apps/server.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+ from omegaconf import DictConfig
7
+
8
+ from mini_transformer.inference import run_inference
9
+ from mini_transformer.model_loader import (
10
+ CONFIG_DIR_ENV,
11
+ CONFIG_NAME_ENV,
12
+ MODEL_NAME_ENV,
13
+ compose_config_from_dir,
14
+ compose_model_config,
15
+ ensure_models_root,
16
+ list_model_names,
17
+ )
18
+
19
+ app = FastAPI(title="Mini-Transformer Inference API")
20
+
21
+
22
+ @app.get("/healthz")
23
+ def healthz():
24
+ return {"status": "ok"}
25
+
26
+
27
+ @app.get("/generate")
28
+ def generate(text: str | None = None):
29
+ config_dir = os.environ.get(CONFIG_DIR_ENV)
30
+ config_name = os.environ.get(CONFIG_NAME_ENV, "config_inference")
31
+
32
+ if config_dir:
33
+ cfg: DictConfig = compose_config_from_dir(config_dir, config_name=config_name)
34
+ else:
35
+ ensure_models_root()
36
+ model_name = os.environ.get(MODEL_NAME_ENV)
37
+ if not model_name:
38
+ names = list_model_names()
39
+ if not names:
40
+ raise HTTPException(
41
+ status_code=503,
42
+ detail=(
43
+ "No models available under `trained_models/`. "
44
+ "Start the server with --model or place a model folder."
45
+ ),
46
+ )
47
+ model_name = names[0]
48
+ cfg = compose_model_config(model_name, config_name=config_name)
49
+
50
+ if text is not None and text.strip():
51
+ cfg.input_text = text
52
+ outputs = run_inference(cfg)
53
+ return {"outputs": outputs}
src/mini_transformer/cli.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Command-line entry points for mini_transformer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+
12
+ import uvicorn
13
+ from omegaconf import DictConfig
14
+
15
+ from .apps import server as server_module
16
+ from .inference import run_inference
17
+ from .model_loader import (
18
+ CONFIG_DIR_ENV,
19
+ CONFIG_NAME_ENV,
20
+ MODEL_NAME_ENV,
21
+ MODELS_ENV,
22
+ compose_config_from_dir,
23
+ compose_model_config,
24
+ ensure_models_root,
25
+ list_model_names,
26
+ snapshot_to_local,
27
+ )
28
+
29
+ DEFAULT_CONFIG_NAME = "config_inference"
30
+
31
+ __all__ = ["infer_main", "serve_main", "ui_main", "fetch_main"]
32
+
33
+
34
+ def _select_model(model: str | None) -> str:
35
+ ensure_models_root()
36
+ names = list_model_names()
37
+ if not names:
38
+ raise SystemExit(
39
+ "No models found in `trained_models/`. Place a model folder with a configs/config_inference.yaml file there first."
40
+ )
41
+ if model and model in names:
42
+ return model
43
+ if model:
44
+ raise SystemExit(f"Model '{model}' not found. Available models: {', '.join(names)}.")
45
+ return names[0]
46
+
47
+
48
+ def infer_main(argv: Sequence[str] | None = None) -> int:
49
+ parser = argparse.ArgumentParser(description="Run mini_transformer inference")
50
+ parser.add_argument("-t", "--input-text", help="Prompt to feed the model")
51
+ parser.add_argument("-m", "--model", help="Model folder under trained_models/")
52
+ parser.add_argument("--model-root", help="Override path to trained_models root")
53
+ parser.add_argument(
54
+ "-c", "--config-dir", help="Use Hydra configs from this directory instead of a model folder"
55
+ )
56
+ parser.add_argument(
57
+ "--config-name",
58
+ default=os.environ.get(CONFIG_NAME_ENV, DEFAULT_CONFIG_NAME),
59
+ help="Hydra config name to compose (default: config_inference)",
60
+ )
61
+ parser.add_argument(
62
+ "-o",
63
+ "--override",
64
+ action="append",
65
+ default=[],
66
+ help="Hydra override in dotlist form (e.g. generation.temperature=0.7)",
67
+ )
68
+ args = parser.parse_args(argv)
69
+
70
+ overrides = args.override or []
71
+ if args.model_root:
72
+ os.environ[MODELS_ENV] = args.model_root
73
+
74
+ if args.config_dir or os.environ.get(CONFIG_DIR_ENV):
75
+ config_dir = args.config_dir or os.environ.get(CONFIG_DIR_ENV)
76
+ if config_dir is None:
77
+ raise SystemExit("Config directory not provided")
78
+ cfg: DictConfig = compose_config_from_dir(
79
+ config_dir,
80
+ config_name=args.config_name,
81
+ overrides=overrides,
82
+ )
83
+ else:
84
+ model_name = _select_model(args.model)
85
+ cfg = compose_model_config(
86
+ model_name,
87
+ config_name=args.config_name,
88
+ overrides=overrides,
89
+ )
90
+ if args.input_text:
91
+ cfg.input_text = args.input_text
92
+
93
+ outputs = run_inference(cfg)
94
+ print("\n".join(outputs))
95
+ return 0
96
+
97
+
98
+ def serve_main(argv: Sequence[str] | None = None) -> int:
99
+ parser = argparse.ArgumentParser(description="Launch the FastAPI inference server")
100
+ parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
101
+ parser.add_argument("--port", type=int, default=8000, help="Port for uvicorn (default: 8000)")
102
+ parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
103
+ parser.add_argument("-m", "--model", help="Model folder under trained_models/")
104
+ parser.add_argument("--model-root", help="Override path to trained_models root")
105
+ parser.add_argument(
106
+ "-c", "--config-dir", help="Use configs from this directory instead of a model folder"
107
+ )
108
+ parser.add_argument(
109
+ "--config-name",
110
+ default=os.environ.get(CONFIG_NAME_ENV, DEFAULT_CONFIG_NAME),
111
+ help="Hydra config name to compose",
112
+ )
113
+ args = parser.parse_args(argv)
114
+
115
+ if args.model_root:
116
+ os.environ[MODELS_ENV] = args.model_root
117
+
118
+ if args.config_dir:
119
+ os.environ[CONFIG_DIR_ENV] = args.config_dir
120
+ os.environ.pop(MODEL_NAME_ENV, None)
121
+ else:
122
+ model_name = _select_model(args.model)
123
+ os.environ[MODEL_NAME_ENV] = model_name
124
+ os.environ.pop(CONFIG_DIR_ENV, None)
125
+
126
+ os.environ[CONFIG_NAME_ENV] = args.config_name
127
+
128
+ uvicorn.run(
129
+ "mini_transformer.apps.server:app",
130
+ host=args.host,
131
+ port=args.port,
132
+ reload=args.reload,
133
+ )
134
+ return 0
135
+
136
+
137
+ def ui_main(argv: Sequence[str] | None = None) -> int:
138
+ parser = argparse.ArgumentParser(description="Launch the Chainlit chat UI")
139
+ parser.add_argument("--model-root", help="Override path to trained_models root")
140
+ parser.add_argument("-m", "--model", help="Pre-select a model when the UI starts")
141
+ parser.add_argument(
142
+ "-c", "--config-dir", help="Use configs from this directory instead of trained_models"
143
+ )
144
+ parser.add_argument(
145
+ "--host",
146
+ default=os.environ.get("MINI_TRANSFORMER_UI_HOST", "127.0.0.1"),
147
+ help="Host to bind Chainlit to (default: 127.0.0.1)",
148
+ )
149
+ parser.add_argument(
150
+ "--port",
151
+ type=int,
152
+ default=int(os.environ.get("MINI_TRANSFORMER_UI_PORT", 8000)),
153
+ help="Port for Chainlit (default: 8000)",
154
+ )
155
+ parser.add_argument(
156
+ "--config-name",
157
+ default=os.environ.get(CONFIG_NAME_ENV, DEFAULT_CONFIG_NAME),
158
+ help="Hydra config name to compose",
159
+ )
160
+ args = parser.parse_args(argv)
161
+
162
+ env = os.environ.copy()
163
+ if args.model_root:
164
+ env[MODELS_ENV] = args.model_root
165
+ if args.config_dir:
166
+ env[CONFIG_DIR_ENV] = args.config_dir
167
+ if args.model:
168
+ env[MODEL_NAME_ENV] = args.model
169
+ env[CONFIG_NAME_ENV] = args.config_name
170
+ env.setdefault("MINI_TRANSFORMER_UI_HOST", args.host)
171
+ env.setdefault("MINI_TRANSFORMER_UI_PORT", str(args.port))
172
+
173
+ app_path = Path(server_module.__file__).with_name("chainlit_app.py")
174
+ cmd = [
175
+ "chainlit",
176
+ "run",
177
+ str(app_path),
178
+ "--host",
179
+ args.host,
180
+ "--port",
181
+ str(args.port),
182
+ ]
183
+ try:
184
+ proc = subprocess.run(cmd, env=env)
185
+ except FileNotFoundError as exc: # pragma: no cover
186
+ raise SystemExit(
187
+ "The 'chainlit' executable is missing. Install mini-transformer[server] to run the UI."
188
+ ) from exc
189
+ return proc.returncode
190
+
191
+
192
+ def fetch_main(argv: Sequence[str] | None = None) -> int:
193
+ parser = argparse.ArgumentParser(description="Download a model from the Hugging Face Hub")
194
+ parser.add_argument(
195
+ "model_id", help="Repo ID on Hugging Face Hub (e.g. AlaBoussoffara/transformer_small)"
196
+ )
197
+ parser.add_argument(
198
+ "-n",
199
+ "--name",
200
+ help="Local directory name under trained_models/ (default derived from model id)",
201
+ )
202
+ parser.add_argument("--revision", help="Optional revision or branch to download")
203
+ parser.add_argument("--repo-type", default="model", help="Hub repo type (default: model)")
204
+ parser.add_argument("--token", help="Authentication token for private repos")
205
+ parser.add_argument("--cache-dir", help="Custom Hugging Face cache directory")
206
+ parser.add_argument("--model-root", help="Override path to trained_models root")
207
+ parser.add_argument("--force", action="store_true", help="Overwrite existing local directory")
208
+ args = parser.parse_args(argv)
209
+
210
+ if args.model_root:
211
+ os.environ[MODELS_ENV] = args.model_root
212
+
213
+ try:
214
+ destination = snapshot_to_local(
215
+ args.model_id,
216
+ local_name=args.name,
217
+ revision=args.revision,
218
+ repo_type=args.repo_type,
219
+ token=args.token,
220
+ cache_dir=args.cache_dir,
221
+ force=args.force,
222
+ )
223
+ except FileExistsError as exc: # pragma: no cover
224
+ raise SystemExit(str(exc)) from exc
225
+
226
+ print(f"Downloaded {args.model_id} → {destination}")
227
+ config_path = destination / "configs" / f"{DEFAULT_CONFIG_NAME}.yaml"
228
+ if not config_path.exists():
229
+ print(
230
+ f"[warning] Expected config file {config_path} not found. The inference CLI requires this file.",
231
+ file=sys.stderr,
232
+ )
233
+ return 0
234
+
235
+
236
+ if __name__ == "__main__": # pragma: no cover
237
+ sys.exit(infer_main())
src/mini_transformer/conf/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Hydra configuration package included with mini_transformer."""
src/mini_transformer/conf/generation/generation.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ max_new_tokens: 128
2
+ temperature: 1.0
3
+ top_k: null
4
+ top_p: null
5
+ do_sample: false
6
+ presence_penalty: 0.0
7
+ frequency_penalty: 0.0
8
+ no_repeat_ngram: null
9
+ min_steps_before_eos: 0
src/mini_transformer/conf/infer_mode.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - model: small
3
+ - runtime: local
4
+ - tokenizer: bpe_8k
5
+ - generation: generation
6
+ - _self_
7
+ input_text: ""
8
+ hydra:
9
+ run:
10
+ dir: .
11
+ output_subdir: null
src/mini_transformer/conf/model/small.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: small
2
+ best_checkpoint_path: ${oc.env:MINI_TRANSFORMER_CHECKPOINT_BEST, ""}
3
+ latest_checkpoint_path: ${oc.env:MINI_TRANSFORMER_CHECKPOINT_LATEST, ""}
4
+ tokenizer: bpe_8k
5
+ d_model: 512
6
+ num_layers: 4
7
+ num_heads: 8
8
+ d_ff: 1024
9
+ dropout_rate: 0.1
10
+ vocab_size: 8192
11
+ max_seq_len: 128
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ layer_norm_style: post
src/mini_transformer/conf/runtime/local.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ seed: ${oc.env:MINI_TRANSFORMER_SEED, 42}
2
+ device: ${oc.env:MINI_TRANSFORMER_DEVICE, "cpu"}
3
+ output_dir: ${oc.env:MINI_TRANSFORMER_OUTPUT_DIR, "./outputs"}
4
+ data_dir: ${oc.env:MINI_TRANSFORMER_DATA_DIR, "./data"}
5
+ cache_dir: ${oc.env:MINI_TRANSFORMER_CACHE_DIR, "./data/cache"}
6
+ tokenizer_dir: ${oc.env:MINI_TRANSFORMER_TOKENIZER_DIR, "./tokenizer"}
7
+ checkpoint_path: ${oc.env:MINI_TRANSFORMER_CHECKPOINT_DIR, "./outputs/checkpoints"}
src/mini_transformer/conf/tokenizer/bpe_8k.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: bpe_8k
2
+ path: ${oc.env:MINI_TRANSFORMER_TOKENIZER_PATH, ""}
3
+ corpus: ${oc.env:MINI_TRANSFORMER_CORPUS_PATH, ""}
4
+ dataset: ${oc.env:MINI_TRANSFORMER_DATASET_NAME, ""}
5
+ vocab_size: 8192
6
+ max_seq_len: 128
7
+ pad_token: <pad>
8
+ bos_token: <bos>
9
+ eos_token: <eos>
10
+ unk_token: <unk>
11
+ special_tokens: ["<pad>", "<bos>", "<eos>", "<unk>"]
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ unk_id: 3
src/{transformer → mini_transformer}/configs.py RENAMED
@@ -34,6 +34,7 @@ class ModelCfg:
34
  pad_id: int
35
  bos_id: int
36
  eos_id: int
 
37
 
38
 
39
  @dataclass
 
34
  pad_id: int
35
  bos_id: int
36
  eos_id: int
37
+ layer_norm_style: str | None = None
38
 
39
 
40
  @dataclass
src/mini_transformer/inference.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Programmatic inference helper for mini_transformer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from pathlib import Path
7
+
8
+ import torch
9
+ from hydra.utils import to_absolute_path
10
+ from omegaconf import DictConfig, OmegaConf
11
+ from transformers import PreTrainedTokenizerFast
12
+
13
+ from .configs import InferAppCfg, ModelCfg, TokenizerCfg
14
+ from .transformer import BasicEncoderDecoderTransformer
15
+ from .utils import check_tokenizer_model_compatibility, set_global_seed
16
+
17
+
18
+ def _resolve_device(device_str: str | None) -> torch.device:
19
+ """Pick a device honoring configs while falling back to CPU if unavailable."""
20
+
21
+ if not device_str:
22
+ return torch.device("cpu")
23
+ try:
24
+ device = torch.device(device_str)
25
+ except (TypeError, ValueError) as exc:
26
+ raise ValueError(f"Invalid runtime.device value: {device_str!r}") from exc
27
+
28
+ if device.type == "cuda":
29
+ if not torch.cuda.is_available():
30
+ warnings.warn(
31
+ "CUDA device requested but torch.cuda.is_available() is False; falling back to CPU.",
32
+ RuntimeWarning,
33
+ stacklevel=2,
34
+ )
35
+ return torch.device("cpu")
36
+ if device.index is not None and device.index >= torch.cuda.device_count():
37
+ warnings.warn(
38
+ f"CUDA device index {device.index} out of range; using cuda:0 instead.",
39
+ RuntimeWarning,
40
+ stacklevel=2,
41
+ )
42
+ return torch.device("cuda:0")
43
+ return device
44
+
45
+
46
+ _SESSION_CACHE: dict[
47
+ tuple[str, ...], tuple[BasicEncoderDecoderTransformer, PreTrainedTokenizerFast]
48
+ ] = {}
49
+
50
+
51
+ def _session_cache_key(
52
+ model_cfg: ModelCfg,
53
+ tokenizer_cfg: TokenizerCfg,
54
+ tokenizer_path: Path,
55
+ checkpoint_path: Path | None,
56
+ device: torch.device,
57
+ ) -> tuple[str, ...]:
58
+ special_tokens = ",".join(tokenizer_cfg.special_tokens) if tokenizer_cfg.special_tokens else ""
59
+ return (
60
+ model_cfg.name,
61
+ str(tokenizer_path),
62
+ str(checkpoint_path) if checkpoint_path else "",
63
+ str(device),
64
+ str(model_cfg.vocab_size),
65
+ str(model_cfg.d_model),
66
+ str(model_cfg.num_heads),
67
+ str(model_cfg.num_layers),
68
+ str(model_cfg.d_ff),
69
+ f"{model_cfg.dropout_rate}",
70
+ str(model_cfg.max_seq_len),
71
+ str(model_cfg.pad_id),
72
+ str(model_cfg.bos_id),
73
+ str(model_cfg.eos_id),
74
+ tokenizer_cfg.name,
75
+ str(tokenizer_cfg.vocab_size),
76
+ str(tokenizer_cfg.max_seq_len),
77
+ tokenizer_cfg.pad_token or "",
78
+ tokenizer_cfg.bos_token or "",
79
+ tokenizer_cfg.eos_token or "",
80
+ tokenizer_cfg.unk_token or "",
81
+ special_tokens,
82
+ )
83
+
84
+
85
+ def _get_or_create_session(
86
+ *,
87
+ model_cfg: ModelCfg,
88
+ tokenizer_cfg: TokenizerCfg,
89
+ tokenizer_path: Path,
90
+ checkpoint_path: Path | None,
91
+ device: torch.device,
92
+ ) -> tuple[BasicEncoderDecoderTransformer, PreTrainedTokenizerFast]:
93
+ key = _session_cache_key(model_cfg, tokenizer_cfg, tokenizer_path, checkpoint_path, device)
94
+ if key in _SESSION_CACHE:
95
+ return _SESSION_CACHE[key]
96
+
97
+ transformer = BasicEncoderDecoderTransformer(model_cfg)
98
+ if checkpoint_path:
99
+ ckpt = torch.load(checkpoint_path, map_location=device)
100
+ state = ckpt.get("model_state_dict", ckpt)
101
+ transformer.load_state_dict(state, strict=True)
102
+ transformer.to(device)
103
+ transformer.eval()
104
+
105
+ tokenizer = PreTrainedTokenizerFast(
106
+ tokenizer_file=str(tokenizer_path),
107
+ bos_token=tokenizer_cfg.bos_token,
108
+ eos_token=tokenizer_cfg.eos_token,
109
+ unk_token=tokenizer_cfg.unk_token,
110
+ pad_token=tokenizer_cfg.pad_token,
111
+ model_max_length=tokenizer_cfg.max_seq_len,
112
+ )
113
+
114
+ _SESSION_CACHE[key] = (transformer, tokenizer)
115
+ return transformer, tokenizer
116
+
117
+
118
+ def run_inference(cfg: DictConfig) -> list[str]:
119
+ """Run inference using a composed Hydra configuration."""
120
+
121
+ scfg_temp = OmegaConf.merge(OmegaConf.structured(InferAppCfg), cfg)
122
+ scfg: InferAppCfg = OmegaConf.to_object(scfg_temp)
123
+ model_cfg = ModelCfg(**vars(scfg.model))
124
+ tokenizer_cfg = TokenizerCfg(**vars(scfg.tokenizer))
125
+
126
+ set_global_seed(scfg.runtime.seed)
127
+ check_tokenizer_model_compatibility(model_cfg, tokenizer_cfg)
128
+
129
+ device = _resolve_device(getattr(scfg.runtime, "device", None))
130
+
131
+ if not tokenizer_cfg.path:
132
+ raise FileNotFoundError(
133
+ "Tokenizer path is empty. Update your config or set MINI_TRANSFORMER_TOKENIZER_PATH."
134
+ )
135
+
136
+ tokenizer_path = Path(to_absolute_path(tokenizer_cfg.path))
137
+ if not tokenizer_path.is_file():
138
+ raise FileNotFoundError(
139
+ f"Tokenizer file not found: {tokenizer_path}. Check your model's config or environment variables."
140
+ )
141
+
142
+ checkpoint_path: Path | None = None
143
+ if scfg.model.best_checkpoint_path:
144
+ checkpoint_path = Path(to_absolute_path(scfg.model.best_checkpoint_path))
145
+
146
+ transformer, tokenizer = _get_or_create_session(
147
+ model_cfg=model_cfg,
148
+ tokenizer_cfg=tokenizer_cfg,
149
+ tokenizer_path=tokenizer_path,
150
+ checkpoint_path=checkpoint_path,
151
+ device=device,
152
+ )
153
+
154
+ text_input = scfg.input_text
155
+ if not text_input:
156
+ raise SystemExit('Pass text like: input_text="hello world"')
157
+
158
+ encoded = tokenizer(text_input, padding=True, truncation=True, return_tensors="pt")
159
+ src_ids = encoded["input_ids"].to(device)
160
+ src_padd_mask = (encoded["attention_mask"] == 0).to(device)
161
+
162
+ tgt_ids = transformer.generate(
163
+ src_ids,
164
+ src_padd_mask,
165
+ max_new_tokens=scfg.generation.max_new_tokens,
166
+ temperature=scfg.generation.temperature,
167
+ top_k=scfg.generation.top_k,
168
+ top_p=scfg.generation.top_p,
169
+ do_sample=scfg.generation.do_sample,
170
+ presence_penalty=scfg.generation.presence_penalty,
171
+ frequency_penalty=scfg.generation.frequency_penalty,
172
+ no_repeat_ngram=scfg.generation.no_repeat_ngram,
173
+ min_steps_before_eos=scfg.generation.min_steps_before_eos,
174
+ seed=scfg.runtime.seed,
175
+ )
176
+ return tokenizer.batch_decode(tgt_ids.cpu(), skip_special_tokens=True)
src/mini_transformer/model_loader.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for loading model configurations from local trained_models folders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+
10
+ from huggingface_hub import snapshot_download
11
+ from hydra import compose, initialize_config_dir
12
+ from hydra.core.global_hydra import GlobalHydra
13
+ from omegaconf import DictConfig
14
+
15
+ MODELS_ENV = "MINI_TRANSFORMER_MODELS"
16
+ MODEL_NAME_ENV = "MINI_TRANSFORMER_MODEL_NAME"
17
+ CONFIG_DIR_ENV = "MINI_TRANSFORMER_CONFIG_DIR"
18
+ CONFIG_NAME_ENV = "MINI_TRANSFORMER_CONFIG_NAME"
19
+ DEFAULT_CONFIG_NAME = "config_inference"
20
+ CONFIG_FILENAME = f"{DEFAULT_CONFIG_NAME}.yaml"
21
+
22
+
23
+ def get_models_root() -> Path:
24
+ """Return the directory where local models are stored."""
25
+
26
+ env_root = os.environ.get(MODELS_ENV)
27
+ if env_root:
28
+ return Path(env_root).expanduser().resolve()
29
+ return (Path.cwd() / "trained_models").resolve()
30
+
31
+
32
+ def ensure_models_root() -> Path:
33
+ root = get_models_root()
34
+ root.mkdir(parents=True, exist_ok=True)
35
+ return root
36
+
37
+
38
+ def list_model_names() -> list[str]:
39
+ root = ensure_models_root()
40
+ names: list[str] = []
41
+ for entry in sorted(root.iterdir()):
42
+ if entry.is_dir() and (entry / "configs" / CONFIG_FILENAME).exists():
43
+ names.append(entry.name)
44
+ return names
45
+
46
+
47
+ def _resolve_relative_paths(cfg: DictConfig, base_dir: Path) -> None:
48
+ def _resolve(value: str | None) -> str | None:
49
+ if not value:
50
+ return value
51
+ path = Path(value)
52
+ if path.is_absolute():
53
+ return str(path)
54
+ parts = path.parts
55
+ if len(parts) >= 2 and parts[0] == "trained_models" and parts[1] == base_dir.name:
56
+ path = Path(*parts[2:])
57
+ elif parts and parts[0] == base_dir.name:
58
+ path = Path(*parts[1:])
59
+ return str((base_dir / path).resolve())
60
+
61
+ if "model" in cfg:
62
+ for key in ("best_checkpoint_path", "latest_checkpoint_path"):
63
+ if key in cfg.model:
64
+ cfg.model[key] = _resolve(cfg.model[key])
65
+ if "tokenizer" in cfg and "path" in cfg.tokenizer:
66
+ cfg.tokenizer["path"] = _resolve(cfg.tokenizer["path"])
67
+ if "runtime" in cfg:
68
+ for key in ("output_dir", "data_dir", "cache_dir", "tokenizer_dir", "checkpoint_path"):
69
+ if key in cfg.runtime:
70
+ cfg.runtime[key] = _resolve(cfg.runtime[key])
71
+
72
+
73
+ def compose_model_config(
74
+ model_name: str,
75
+ *,
76
+ config_name: str = DEFAULT_CONFIG_NAME,
77
+ overrides: Sequence[str] | None = None,
78
+ job_name: str | None = None,
79
+ ) -> DictConfig:
80
+ """Compose the Hydra config for a given model folder."""
81
+
82
+ models_root = ensure_models_root()
83
+ model_dir = models_root / model_name
84
+ config_dir = model_dir / "configs"
85
+ if not config_dir.is_dir():
86
+ raise FileNotFoundError(
87
+ f"Config directory not found for model '{model_name}' at {config_dir}."
88
+ )
89
+
90
+ overrides = list(overrides or [])
91
+ job = job_name or f"mini_transformer_{model_name}"
92
+ if GlobalHydra.instance().is_initialized():
93
+ GlobalHydra.instance().clear()
94
+ with initialize_config_dir(config_dir=str(config_dir), job_name=job, version_base=None):
95
+ cfg = compose(config_name=config_name, overrides=overrides)
96
+ _resolve_relative_paths(cfg, model_dir)
97
+ return cfg
98
+
99
+
100
+ def compose_config_from_dir(
101
+ config_dir: str,
102
+ *,
103
+ config_name: str = DEFAULT_CONFIG_NAME,
104
+ overrides: Sequence[str] | None = None,
105
+ job_name: str = "mini_transformer_cli",
106
+ ) -> DictConfig:
107
+ overrides = list(overrides or [])
108
+ config_path = Path(config_dir).resolve()
109
+ base_dir = config_path.parent if config_path.name == "configs" else config_path
110
+ if GlobalHydra.instance().is_initialized():
111
+ GlobalHydra.instance().clear()
112
+ with initialize_config_dir(config_dir=str(config_path), job_name=job_name, version_base=None):
113
+ cfg = compose(config_name=config_name, overrides=overrides)
114
+ _resolve_relative_paths(cfg, base_dir)
115
+ return cfg
116
+
117
+
118
+ __all__ = [
119
+ "CONFIG_FILENAME",
120
+ "DEFAULT_CONFIG_NAME",
121
+ "MODELS_ENV",
122
+ "MODEL_NAME_ENV",
123
+ "CONFIG_DIR_ENV",
124
+ "CONFIG_NAME_ENV",
125
+ "compose_model_config",
126
+ "compose_config_from_dir",
127
+ "ensure_models_root",
128
+ "get_models_root",
129
+ "list_model_names",
130
+ "snapshot_to_local",
131
+ ]
132
+
133
+
134
+ def snapshot_to_local(
135
+ model_id: str,
136
+ *,
137
+ local_name: str | None = None,
138
+ revision: str | None = None,
139
+ repo_type: str = "model",
140
+ token: str | None = None,
141
+ cache_dir: str | None = None,
142
+ force: bool = False,
143
+ ) -> Path:
144
+ """Download a repository from the Hugging Face Hub into `trained_models/`."""
145
+
146
+ root = ensure_models_root()
147
+ name = local_name or model_id.replace("/", "__")
148
+ destination = root / name
149
+
150
+ if destination.exists() and any(destination.iterdir()):
151
+ if not force:
152
+ raise FileExistsError(
153
+ f"Destination {destination} already exists. Use --force to overwrite."
154
+ )
155
+ for child in destination.iterdir():
156
+ if child.is_dir():
157
+ shutil.rmtree(child)
158
+ else:
159
+ child.unlink()
160
+
161
+ destination.mkdir(parents=True, exist_ok=True)
162
+
163
+ snapshot_download(
164
+ repo_id=model_id,
165
+ repo_type=repo_type,
166
+ local_dir=str(destination),
167
+ local_dir_use_symlinks=False,
168
+ revision=revision,
169
+ token=token,
170
+ cache_dir=cache_dir,
171
+ )
172
+ return destination
src/mini_transformer/modules/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .decoder import TransformerDecoder
2
+ from .embedding import InputEmbedding
3
+ from .encoder import TransformerEncoder
4
+ from .lm_head import LMHead
5
+
6
+ __all__ = ["TransformerEncoder", "TransformerDecoder", "InputEmbedding", "LMHead"]
src/{transformer → mini_transformer}/modules/attention.py RENAMED
@@ -8,7 +8,7 @@ import torch
8
  import torch.nn as nn
9
  from torch import Tensor
10
 
11
- from transformer.utils import (
12
  calculate_attention,
13
  combine_masks,
14
  create_qk_padding_mask,
 
8
  import torch.nn as nn
9
  from torch import Tensor
10
 
11
+ from ..utils import (
12
  calculate_attention,
13
  combine_masks,
14
  create_qk_padding_mask,
src/{transformer → mini_transformer}/modules/decoder.py RENAMED
@@ -7,8 +7,8 @@ import torch.nn as nn
7
  import torch.utils.checkpoint as ckpt
8
  from torch import Tensor
9
 
10
- from transformer.modules.attention import MultiHeadAttention
11
- from transformer.modules.feedforward import FeedForwardLayer
12
 
13
  __all__ = ["DecoderLayer", "TransformerDecoder"]
14
 
@@ -16,7 +16,15 @@ __all__ = ["DecoderLayer", "TransformerDecoder"]
16
  class DecoderLayer(nn.Module):
17
  """Single decoder block with self-attention, cross-attention, and feed-forward."""
18
 
19
- def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float) -> None:
 
 
 
 
 
 
 
 
20
  super().__init__()
21
 
22
  if not isinstance(d_model, int):
@@ -27,6 +35,8 @@ class DecoderLayer(nn.Module):
27
  raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
28
  if not isinstance(dropout_rate, float):
29
  raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
 
 
30
 
31
  if d_model <= 0:
32
  raise ValueError("d_model must be strictly greater than 0")
@@ -37,6 +47,12 @@ class DecoderLayer(nn.Module):
37
  if not 0.0 <= dropout_rate < 1.0:
38
  raise ValueError("dropout_rate must be in [0, 1)")
39
 
 
 
 
 
 
 
40
  self.self_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
41
  self.cross_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
42
  self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
@@ -56,7 +72,7 @@ class DecoderLayer(nn.Module):
56
  tgt_padding_mask: Tensor,
57
  tgt_causal_mask: Tensor | None,
58
  ) -> Tensor:
59
- """Run one decoder layer using post-layer-normalisation formulation."""
60
 
61
  if not isinstance(x, torch.Tensor):
62
  raise TypeError("x must be a torch.Tensor")
@@ -85,7 +101,33 @@ class DecoderLayer(nn.Module):
85
  f" got dtype {mask.dtype} and shape {tuple(mask.shape)}"
86
  )
87
 
88
- # Self-attention
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  self_attn_out = self.self_attention_layer(
90
  y,
91
  y,
@@ -121,6 +163,8 @@ class TransformerDecoder(nn.Module):
121
  d_ff: int,
122
  num_layers: int,
123
  dropout_rate: float,
 
 
124
  ) -> None:
125
  super().__init__()
126
 
@@ -129,8 +173,24 @@ class TransformerDecoder(nn.Module):
129
  if num_layers <= 0:
130
  raise ValueError("num_layers must be strictly greater than 0")
131
 
 
 
 
 
 
 
 
132
  self.layers = nn.ModuleList(
133
- [DecoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
 
 
 
 
 
 
 
 
 
134
  )
135
  self.use_ckpt = False
136
 
 
7
  import torch.utils.checkpoint as ckpt
8
  from torch import Tensor
9
 
10
+ from .attention import MultiHeadAttention
11
+ from .feedforward import FeedForwardLayer
12
 
13
  __all__ = ["DecoderLayer", "TransformerDecoder"]
14
 
 
16
  class DecoderLayer(nn.Module):
17
  """Single decoder block with self-attention, cross-attention, and feed-forward."""
18
 
19
+ def __init__(
20
+ self,
21
+ d_model: int,
22
+ num_heads: int,
23
+ d_ff: int,
24
+ dropout_rate: float,
25
+ *,
26
+ layer_norm_style: str = "post",
27
+ ) -> None:
28
  super().__init__()
29
 
30
  if not isinstance(d_model, int):
 
35
  raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
36
  if not isinstance(dropout_rate, float):
37
  raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
38
+ if not isinstance(layer_norm_style, str):
39
+ raise TypeError(f"layer_norm_style must be a string, got {type(layer_norm_style)}")
40
 
41
  if d_model <= 0:
42
  raise ValueError("d_model must be strictly greater than 0")
 
47
  if not 0.0 <= dropout_rate < 1.0:
48
  raise ValueError("dropout_rate must be in [0, 1)")
49
 
50
+ style = layer_norm_style.lower()
51
+ if style not in {"pre", "post"}:
52
+ raise ValueError("layer_norm_style must be either 'pre' or 'post' (case-insensitive)")
53
+ self.layer_norm_style = style
54
+ self.pre_norm = style == "pre"
55
+
56
  self.self_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
57
  self.cross_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
58
  self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
 
72
  tgt_padding_mask: Tensor,
73
  tgt_causal_mask: Tensor | None,
74
  ) -> Tensor:
75
+ """Run one decoder layer using the configured layer-normalisation style."""
76
 
77
  if not isinstance(x, torch.Tensor):
78
  raise TypeError("x must be a torch.Tensor")
 
101
  f" got dtype {mask.dtype} and shape {tuple(mask.shape)}"
102
  )
103
 
104
+ if self.pre_norm:
105
+ normed = self.norm1(y)
106
+ self_attn_out = self.self_attention_layer(
107
+ normed,
108
+ normed,
109
+ normed,
110
+ tgt_padding_mask,
111
+ tgt_padding_mask,
112
+ tgt_causal_mask,
113
+ )
114
+ y = y + self.dropout1(self_attn_out)
115
+
116
+ normed_y = self.norm2(y)
117
+ cross_attn_out = self.cross_attention_layer(
118
+ normed_y,
119
+ x,
120
+ x,
121
+ tgt_padding_mask,
122
+ src_padding_mask,
123
+ )
124
+ y = y + self.dropout2(cross_attn_out)
125
+
126
+ ff_out = self.feed_forward(self.norm3(y))
127
+ y = y + self.dropout3(ff_out)
128
+ return y
129
+
130
+ # Self-attention (post-LN)
131
  self_attn_out = self.self_attention_layer(
132
  y,
133
  y,
 
163
  d_ff: int,
164
  num_layers: int,
165
  dropout_rate: float,
166
+ *,
167
+ layer_norm_style: str = "post",
168
  ) -> None:
169
  super().__init__()
170
 
 
173
  if num_layers <= 0:
174
  raise ValueError("num_layers must be strictly greater than 0")
175
 
176
+ if not isinstance(layer_norm_style, str):
177
+ raise TypeError(f"layer_norm_style must be a string, got {type(layer_norm_style)}")
178
+ style = layer_norm_style.lower()
179
+ if style not in {"pre", "post"}:
180
+ raise ValueError("layer_norm_style must be either 'pre' or 'post' (case-insensitive)")
181
+
182
+ self.layer_norm_style = style
183
  self.layers = nn.ModuleList(
184
+ [
185
+ DecoderLayer(
186
+ d_model,
187
+ num_heads,
188
+ d_ff,
189
+ dropout_rate,
190
+ layer_norm_style=style,
191
+ )
192
+ for _ in range(num_layers)
193
+ ]
194
  )
195
  self.use_ckpt = False
196
 
src/{transformer → mini_transformer}/modules/embedding.py RENAMED
@@ -8,7 +8,7 @@ import torch
8
  import torch.nn as nn
9
  from torch import Tensor
10
 
11
- from transformer.utils import sinusoidal_positional_encoding
12
 
13
  __all__ = ["InputEmbedding", "PositionalEmbedding", "LearnedPositionalEmbedding"]
14
 
 
8
  import torch.nn as nn
9
  from torch import Tensor
10
 
11
+ from ..utils import sinusoidal_positional_encoding
12
 
13
  __all__ = ["InputEmbedding", "PositionalEmbedding", "LearnedPositionalEmbedding"]
14
 
src/{transformer → mini_transformer}/modules/encoder.py RENAMED
@@ -7,16 +7,24 @@ import torch.nn as nn
7
  import torch.utils.checkpoint as ckpt
8
  from torch import Tensor
9
 
10
- from transformer.modules.attention import MultiHeadAttention
11
- from transformer.modules.feedforward import FeedForwardLayer
12
 
13
  __all__ = ["EncoderLayer", "TransformerEncoder"]
14
 
15
 
16
  class EncoderLayer(nn.Module):
17
- """Self-attention + feed-forward block with post-layer-normalisation."""
18
 
19
- def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float) -> None:
 
 
 
 
 
 
 
 
20
  super().__init__()
21
 
22
  if not isinstance(d_model, int):
@@ -27,6 +35,8 @@ class EncoderLayer(nn.Module):
27
  raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
28
  if not isinstance(dropout_rate, float):
29
  raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
 
 
30
 
31
  if d_model <= 0:
32
  raise ValueError("d_model must be strictly greater than 0")
@@ -37,6 +47,12 @@ class EncoderLayer(nn.Module):
37
  if not 0.0 <= dropout_rate < 1.0:
38
  raise ValueError("dropout_rate must be in [0, 1)")
39
 
 
 
 
 
 
 
40
  self.attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
41
  self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
42
  self.norm1 = nn.LayerNorm(d_model)
@@ -59,6 +75,16 @@ class EncoderLayer(nn.Module):
59
  f" got dtype {src_padding_mask.dtype} and shape {tuple(src_padding_mask.shape)}"
60
  )
61
 
 
 
 
 
 
 
 
 
 
 
62
  attn_out = self.attention_layer(x, x, x, src_padding_mask, src_padding_mask)
63
  x = self.norm1(x + self.dropout1(attn_out))
64
  ff_out = self.feed_forward(x)
@@ -75,6 +101,8 @@ class TransformerEncoder(nn.Module):
75
  d_ff: int,
76
  num_layers: int,
77
  dropout_rate: float,
 
 
78
  ) -> None:
79
  super().__init__()
80
 
@@ -83,8 +111,24 @@ class TransformerEncoder(nn.Module):
83
  if num_layers <= 0:
84
  raise ValueError("num_layers must be strictly greater than 0")
85
 
 
 
 
 
 
 
 
86
  self.layers = nn.ModuleList(
87
- [EncoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
 
 
 
 
 
 
 
 
 
88
  )
89
  self.use_ckpt = False
90
 
 
7
  import torch.utils.checkpoint as ckpt
8
  from torch import Tensor
9
 
10
+ from .attention import MultiHeadAttention
11
+ from .feedforward import FeedForwardLayer
12
 
13
  __all__ = ["EncoderLayer", "TransformerEncoder"]
14
 
15
 
16
  class EncoderLayer(nn.Module):
17
+ """Self-attention + feed-forward block supporting pre/post layer normalisation."""
18
 
19
+ def __init__(
20
+ self,
21
+ d_model: int,
22
+ num_heads: int,
23
+ d_ff: int,
24
+ dropout_rate: float,
25
+ *,
26
+ layer_norm_style: str = "post",
27
+ ) -> None:
28
  super().__init__()
29
 
30
  if not isinstance(d_model, int):
 
35
  raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
36
  if not isinstance(dropout_rate, float):
37
  raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
38
+ if not isinstance(layer_norm_style, str):
39
+ raise TypeError(f"layer_norm_style must be a string, got {type(layer_norm_style)}")
40
 
41
  if d_model <= 0:
42
  raise ValueError("d_model must be strictly greater than 0")
 
47
  if not 0.0 <= dropout_rate < 1.0:
48
  raise ValueError("dropout_rate must be in [0, 1)")
49
 
50
+ style = layer_norm_style.lower()
51
+ if style not in {"pre", "post"}:
52
+ raise ValueError("layer_norm_style must be either 'pre' or 'post' (case-insensitive)")
53
+ self.layer_norm_style = style
54
+ self.pre_norm = style == "pre"
55
+
56
  self.attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
57
  self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
58
  self.norm1 = nn.LayerNorm(d_model)
 
75
  f" got dtype {src_padding_mask.dtype} and shape {tuple(src_padding_mask.shape)}"
76
  )
77
 
78
+ if self.pre_norm:
79
+ normed = self.norm1(x)
80
+ attn_out = self.attention_layer(
81
+ normed, normed, normed, src_padding_mask, src_padding_mask
82
+ )
83
+ x = x + self.dropout1(attn_out)
84
+ ff_out = self.feed_forward(self.norm2(x))
85
+ x = x + self.dropout2(ff_out)
86
+ return x
87
+
88
  attn_out = self.attention_layer(x, x, x, src_padding_mask, src_padding_mask)
89
  x = self.norm1(x + self.dropout1(attn_out))
90
  ff_out = self.feed_forward(x)
 
101
  d_ff: int,
102
  num_layers: int,
103
  dropout_rate: float,
104
+ *,
105
+ layer_norm_style: str = "post",
106
  ) -> None:
107
  super().__init__()
108
 
 
111
  if num_layers <= 0:
112
  raise ValueError("num_layers must be strictly greater than 0")
113
 
114
+ if not isinstance(layer_norm_style, str):
115
+ raise TypeError(f"layer_norm_style must be a string, got {type(layer_norm_style)}")
116
+ style = layer_norm_style.lower()
117
+ if style not in {"pre", "post"}:
118
+ raise ValueError("layer_norm_style must be either 'pre' or 'post' (case-insensitive)")
119
+
120
+ self.layer_norm_style = style
121
  self.layers = nn.ModuleList(
122
+ [
123
+ EncoderLayer(
124
+ d_model,
125
+ num_heads,
126
+ d_ff,
127
+ dropout_rate,
128
+ layer_norm_style=style,
129
+ )
130
+ for _ in range(num_layers)
131
+ ]
132
  )
133
  self.use_ckpt = False
134
 
src/{transformer → mini_transformer}/modules/feedforward.py RENAMED
File without changes
src/{transformer → mini_transformer}/modules/lm_head.py RENAMED
File without changes
src/{transformer → mini_transformer}/transformer.py RENAMED
@@ -9,9 +9,9 @@ import torch
9
  import torch.nn as nn
10
  from torch import Tensor
11
 
12
- from transformer import modules
13
- from transformer.configs import ModelCfg
14
- from transformer.utils import broadcast_padding_mask, create_causal_mask, sample_from_logits
15
 
16
  __all__ = ["BasicEncoderDecoderTransformer"]
17
 
@@ -57,14 +57,41 @@ class BasicEncoderDecoderTransformer(nn.Module):
57
 
58
  self.cfg = cfg
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  self.embed = modules.InputEmbedding(
61
  cfg.vocab_size, cfg.d_model, cfg.max_seq_len, cfg.pad_id, cfg.dropout_rate
62
  )
63
  self.encoder = modules.TransformerEncoder(
64
- cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
 
 
 
 
 
65
  )
66
  self.decoder = modules.TransformerDecoder(
67
- cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
 
 
 
 
 
68
  )
69
 
70
  self.lm_head = modules.LMHead(cfg.d_model, cfg.vocab_size)
@@ -306,7 +333,7 @@ class BasicEncoderDecoderTransformer(nn.Module):
306
  rng = _make_generator()
307
  for step in range(max_new_tokens):
308
  # decode on current tgt_ids; take last-step logits
309
- tgt_padding_mask = tgt_ids != self.pad_id
310
  tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads)
311
  step_hidden = self.decode(
312
  hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask
 
9
  import torch.nn as nn
10
  from torch import Tensor
11
 
12
+ from . import modules
13
+ from .configs import ModelCfg
14
+ from .utils import broadcast_padding_mask, create_causal_mask, sample_from_logits
15
 
16
  __all__ = ["BasicEncoderDecoderTransformer"]
17
 
 
57
 
58
  self.cfg = cfg
59
 
60
+ style_raw = getattr(cfg, "layer_norm_style", None)
61
+ if style_raw is None:
62
+ style = "post" if cfg.num_layers <= 4 else "pre"
63
+ elif not isinstance(style_raw, str):
64
+ raise TypeError(
65
+ f"layer_norm_style must be a string when provided, got {type(style_raw)}"
66
+ )
67
+ else:
68
+ style = style_raw.lower()
69
+ if style not in {"pre", "post"}:
70
+ raise ValueError(
71
+ "layer_norm_style must be either 'pre' or 'post'"
72
+ f" (case-insensitive); got {style_raw!r}"
73
+ )
74
+ self.layer_norm_style = style
75
+ cfg.layer_norm_style = style
76
+
77
  self.embed = modules.InputEmbedding(
78
  cfg.vocab_size, cfg.d_model, cfg.max_seq_len, cfg.pad_id, cfg.dropout_rate
79
  )
80
  self.encoder = modules.TransformerEncoder(
81
+ cfg.d_model,
82
+ cfg.num_heads,
83
+ cfg.d_ff,
84
+ cfg.num_layers,
85
+ cfg.dropout_rate,
86
+ layer_norm_style=style,
87
  )
88
  self.decoder = modules.TransformerDecoder(
89
+ cfg.d_model,
90
+ cfg.num_heads,
91
+ cfg.d_ff,
92
+ cfg.num_layers,
93
+ cfg.dropout_rate,
94
+ layer_norm_style=style,
95
  )
96
 
97
  self.lm_head = modules.LMHead(cfg.d_model, cfg.vocab_size)
 
333
  rng = _make_generator()
334
  for step in range(max_new_tokens):
335
  # decode on current tgt_ids; take last-step logits
336
+ tgt_padding_mask = tgt_ids == self.pad_id
337
  tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads)
338
  step_hidden = self.decode(
339
  hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask
src/{transformer → mini_transformer}/utils.py RENAMED
@@ -6,12 +6,20 @@ from collections.abc import Callable, Iterable, Sequence
6
  from pathlib import Path
7
  from typing import Any
8
 
9
- import matplotlib.pyplot as plt
10
  import numpy as np
11
  import torch
12
  import torch.nn.functional as F
13
  from torch import Tensor
14
 
 
 
 
 
 
 
 
 
 
15
  # -----------------------------------------------------------------------------
16
  # Global seeding utilities
17
  # -----------------------------------------------------------------------------
@@ -564,6 +572,8 @@ def plot_layer_heads_grid(
564
  ):
565
  """Plot a single layer as a 3-row (enc self / dec self / dec cross) grid."""
566
 
 
 
567
  enc_layers = maps_dict["enc_self"]
568
  dec_self_layers = maps_dict["dec_self"]
569
  dec_cross_layers = maps_dict["dec_cross"]
@@ -628,7 +638,8 @@ def plot_all_layers_all_heads(
628
  ):
629
  """Render attention grids for every layer (optionally saving a multi-page PDF)."""
630
 
631
- from matplotlib.backends.backend_pdf import PdfPages
 
632
 
633
  enc_layers = len(maps_dict["enc_self"])
634
  dec_layers = len(maps_dict["dec_self"])
@@ -877,6 +888,7 @@ def debug_transformer_forward(
877
  }
878
 
879
  if attention_requested:
 
880
  if not hasattr(model, "embed"):
881
  raise AttributeError("model must expose an embed() method for debugging")
882
  with torch.no_grad():
 
6
  from pathlib import Path
7
  from typing import Any
8
 
 
9
  import numpy as np
10
  import torch
11
  import torch.nn.functional as F
12
  from torch import Tensor
13
 
14
+
15
+ def _require_matplotlib() -> Any:
16
+ try:
17
+ import matplotlib.pyplot as plt # type: ignore
18
+ except ImportError as exc: # pragma: no cover - optional dependency
19
+ raise ImportError("Install mini-transformer[viz] to enable plotting utilities.") from exc
20
+ return plt
21
+
22
+
23
  # -----------------------------------------------------------------------------
24
  # Global seeding utilities
25
  # -----------------------------------------------------------------------------
 
572
  ):
573
  """Plot a single layer as a 3-row (enc self / dec self / dec cross) grid."""
574
 
575
+ plt = _require_matplotlib()
576
+
577
  enc_layers = maps_dict["enc_self"]
578
  dec_self_layers = maps_dict["dec_self"]
579
  dec_cross_layers = maps_dict["dec_cross"]
 
638
  ):
639
  """Render attention grids for every layer (optionally saving a multi-page PDF)."""
640
 
641
+ plt = _require_matplotlib()
642
+ from matplotlib.backends.backend_pdf import PdfPages # type: ignore
643
 
644
  enc_layers = len(maps_dict["enc_self"])
645
  dec_layers = len(maps_dict["dec_self"])
 
888
  }
889
 
890
  if attention_requested:
891
+ plt = _require_matplotlib()
892
  if not hasattr(model, "embed"):
893
  raise AttributeError("model must expose an embed() method for debugging")
894
  with torch.no_grad():
src/transformer/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- from .transformer import BasicEncoderDecoderTransformer
2
-
3
- __all__ = ["BasicEncoderDecoderTransformer"]
 
 
 
 
src/transformer/modules/__init__.py DELETED
@@ -1,6 +0,0 @@
1
- from transformer.modules.decoder import TransformerDecoder
2
- from transformer.modules.embedding import InputEmbedding
3
- from transformer.modules.encoder import TransformerEncoder
4
- from transformer.modules.lm_head import LMHead
5
-
6
- __all__ = ["TransformerEncoder", "TransformerDecoder", "InputEmbedding", "LMHead"]
 
 
 
 
 
 
 
tests/units/modules/test_attention.py CHANGED
@@ -1,7 +1,7 @@
1
  import pytest
2
  import torch
3
 
4
- from transformer.modules.attention import MultiHeadAttention
5
 
6
 
7
  # -----------------------
 
1
  import pytest
2
  import torch
3
 
4
+ from mini_transformer.modules.attention import MultiHeadAttention
5
 
6
 
7
  # -----------------------
tests/units/modules/test_decoder.py CHANGED
@@ -1,7 +1,7 @@
1
  import pytest
2
  import torch
3
 
4
- from transformer.modules.decoder import DecoderLayer, TransformerDecoder
5
 
6
 
7
  def _dev():
@@ -24,6 +24,8 @@ def test_decoder_layer_param_checks():
24
  DecoderLayer(32, 4, "64", 0.1)
25
  with pytest.raises(TypeError):
26
  DecoderLayer(32, 4, 64, "0.1")
 
 
27
 
28
  with pytest.raises(ValueError):
29
  DecoderLayer(0, 4, 64, 0.1)
@@ -33,6 +35,8 @@ def test_decoder_layer_param_checks():
33
  DecoderLayer(32, 4, 0, 0.1)
34
  with pytest.raises(ValueError):
35
  DecoderLayer(32, 4, 64, 1.0) # upper bound excluded
 
 
36
 
37
 
38
  def test_transformer_decoder_layers_count_checks():
@@ -64,6 +68,25 @@ def test_decoder_forward_happy_path(B, Sx, Sy, D, H, FF, L):
64
  assert out.device == device
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def test_decoder_forward_input_checks_and_message_format():
68
  layer = DecoderLayer(24, 3, 48, 0.1)
69
  heads = layer.self_attention_layer.num_heads
 
1
  import pytest
2
  import torch
3
 
4
+ from mini_transformer.modules.decoder import DecoderLayer, TransformerDecoder
5
 
6
 
7
  def _dev():
 
24
  DecoderLayer(32, 4, "64", 0.1)
25
  with pytest.raises(TypeError):
26
  DecoderLayer(32, 4, 64, "0.1")
27
+ with pytest.raises(TypeError):
28
+ DecoderLayer(32, 4, 64, 0.1, layer_norm_style=123)
29
 
30
  with pytest.raises(ValueError):
31
  DecoderLayer(0, 4, 64, 0.1)
 
35
  DecoderLayer(32, 4, 0, 0.1)
36
  with pytest.raises(ValueError):
37
  DecoderLayer(32, 4, 64, 1.0) # upper bound excluded
38
+ with pytest.raises(ValueError):
39
+ DecoderLayer(32, 4, 64, 0.1, layer_norm_style="weird")
40
 
41
 
42
  def test_transformer_decoder_layers_count_checks():
 
68
  assert out.device == device
69
 
70
 
71
+ def test_transformer_decoder_pre_norm_layers_flag():
72
+ dec = TransformerDecoder(24, 3, 48, 2, 0.1, layer_norm_style="pre")
73
+ assert all(layer.pre_norm for layer in dec.layers)
74
+
75
+
76
+ def test_decoder_layer_pre_norm_forward_shapes():
77
+ device = _dev()
78
+ layer = DecoderLayer(24, 3, 48, 0.1, layer_norm_style="pre").to(device)
79
+ x = torch.randn(2, 5, 24, device=device)
80
+ y = torch.randn(2, 6, 24, device=device)
81
+ heads = layer.self_attention_layer.num_heads
82
+ src_mask = torch.zeros(2, heads, 1, 5, dtype=torch.bool, device=device)
83
+ tgt_mask = torch.zeros(2, heads, 1, 6, dtype=torch.bool, device=device)
84
+ causal = torch.ones(2, heads, 6, 6, dtype=torch.bool, device=device).triu(1)
85
+ out = layer(x, y, src_mask, tgt_mask, causal)
86
+ assert out.shape == y.shape
87
+ assert layer.pre_norm is True
88
+
89
+
90
  def test_decoder_forward_input_checks_and_message_format():
91
  layer = DecoderLayer(24, 3, 48, 0.1)
92
  heads = layer.self_attention_layer.num_heads
tests/units/modules/test_embedding.py CHANGED
@@ -3,7 +3,7 @@ import math
3
  import pytest
4
  import torch
5
 
6
- from transformer.modules.embedding import InputEmbedding, PositionalEmbedding
7
 
8
 
9
  # -----------------------
 
3
  import pytest
4
  import torch
5
 
6
+ from mini_transformer.modules.embedding import InputEmbedding, PositionalEmbedding
7
 
8
 
9
  # -----------------------