Spaces:
Sleeping
Sleeping
Commit ·
2d52135
1
Parent(s): 6edd10d
organized code and set up chainlit for demos
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +3 -0
- CONTRIBUTING.md +27 -0
- Dockerfile.dev +6 -16
- LICENSE +21 -0
- Makefile +70 -24
- README.md +125 -3
- architecture.txt +0 -28
- chainlit.md +14 -0
- configs/generation/generation.yaml +1 -1
- configs/model/large_model.yaml +4 -3
- configs/model/medium_model.yaml +5 -4
- configs/model/small_model.yaml +3 -2
- configs/model/test_model.yaml +3 -2
- docker-compose.yml +10 -3
- environment.yml +16 -5
- {scripts → notebooks}/load_data.ipynb +4 -4
- {scripts → notebooks}/tokenizer.ipynb +2 -2
- {scripts → notebooks}/train.ipynb +5 -5
- pyproject.toml +91 -65
- requirements.txt +0 -0
- scripts/inference.py +0 -76
- scripts/server.py +0 -26
- src/mini_transformer/__init__.py +14 -0
- src/mini_transformer/apps/__init__.py +3 -0
- src/mini_transformer/apps/chainlit_app.py +220 -0
- src/mini_transformer/apps/server.py +53 -0
- src/mini_transformer/cli.py +237 -0
- src/mini_transformer/conf/__init__.py +1 -0
- src/mini_transformer/conf/generation/generation.yaml +9 -0
- src/mini_transformer/conf/infer_mode.yaml +11 -0
- src/mini_transformer/conf/model/small.yaml +15 -0
- src/mini_transformer/conf/runtime/local.yaml +7 -0
- src/mini_transformer/conf/tokenizer/bpe_8k.yaml +15 -0
- src/{transformer → mini_transformer}/configs.py +1 -0
- src/mini_transformer/inference.py +176 -0
- src/mini_transformer/model_loader.py +172 -0
- src/mini_transformer/modules/__init__.py +6 -0
- src/{transformer → mini_transformer}/modules/attention.py +1 -1
- src/{transformer → mini_transformer}/modules/decoder.py +66 -6
- src/{transformer → mini_transformer}/modules/embedding.py +1 -1
- src/{transformer → mini_transformer}/modules/encoder.py +49 -5
- src/{transformer → mini_transformer}/modules/feedforward.py +0 -0
- src/{transformer → mini_transformer}/modules/lm_head.py +0 -0
- src/{transformer → mini_transformer}/transformer.py +33 -6
- src/{transformer → mini_transformer}/utils.py +14 -2
- src/transformer/__init__.py +0 -3
- src/transformer/modules/__init__.py +0 -6
- tests/units/modules/test_attention.py +1 -1
- tests/units/modules/test_decoder.py +24 -1
- 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.
|
| 3 |
-
# FROM = choose a base OS + language runtime (Debian slim with Python 3.
|
| 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
|
| 23 |
-
|
| 24 |
-
# Only copying dependency metadata first lets Docker cache layer if code changes later
|
| 25 |
|
| 26 |
-
RUN pip install -U pip && pip install -
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
help:
|
| 4 |
-
@echo "
|
| 5 |
-
@echo "
|
| 6 |
-
@echo "
|
| 7 |
-
@echo "
|
| 8 |
-
@echo "
|
| 9 |
-
@echo "
|
| 10 |
-
@echo "
|
| 11 |
-
@echo "
|
| 12 |
-
@echo "
|
| 13 |
-
@echo "
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
precommit:
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
lint:
|
| 24 |
-
|
| 25 |
|
| 26 |
fmt:
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
type:
|
| 31 |
-
|
| 32 |
|
| 33 |
test:
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
clean:
|
| 36 |
-
rm -rf .
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
|
| 5 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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:
|
| 6 |
-
num_heads:
|
| 7 |
-
num_layers:
|
| 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:
|
| 6 |
-
num_heads:
|
| 7 |
-
num_layers:
|
| 8 |
d_ff: 2048
|
| 9 |
dropout_rate: 0.1
|
| 10 |
vocab_size: 16384
|
| 11 |
-
max_seq_len:
|
| 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:
|
| 6 |
-
num_heads:
|
| 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:
|
| 7 |
-
num_layers:
|
| 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:
|
| 8 |
working_dir: /app
|
| 9 |
volumes:
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 6 |
- pip
|
| 7 |
- pip:
|
| 8 |
-
- --extra-index-url https://download.pytorch.org/whl/
|
| 9 |
-
- torch==2.
|
|
|
|
| 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
|
| 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
|
| 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
|
| 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
|
| 40 |
-
"from
|
| 41 |
-
"from
|
| 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 |
-
"
|
| 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 |
-
|
| 8 |
-
|
| 9 |
-
"
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
"
|
| 13 |
-
"
|
| 14 |
-
"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
"
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
[tool.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 11 |
-
from
|
| 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__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 11 |
-
from
|
| 12 |
|
| 13 |
__all__ = ["EncoderLayer", "TransformerEncoder"]
|
| 14 |
|
| 15 |
|
| 16 |
class EncoderLayer(nn.Module):
|
| 17 |
-
"""Self-attention + feed-forward block
|
| 18 |
|
| 19 |
-
def __init__(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 13 |
-
from
|
| 14 |
-
from
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
)
|
| 66 |
self.decoder = modules.TransformerDecoder(
|
| 67 |
-
cfg.d_model,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
| 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
|
| 7 |
|
| 8 |
|
| 9 |
# -----------------------
|
|
|
|
| 3 |
import pytest
|
| 4 |
import torch
|
| 5 |
|
| 6 |
+
from mini_transformer.modules.embedding import InputEmbedding, PositionalEmbedding
|
| 7 |
|
| 8 |
|
| 9 |
# -----------------------
|