Spaces:
Sleeping
Sleeping
Commit ·
dc8d52b
0
Parent(s):
initial commit with working prototype
Browse files- .dockerignore +67 -0
- .gitignore +61 -0
- .pre-commit-config.yaml +17 -0
- Dockerfile.dev +38 -0
- Makefile +42 -0
- README.md +8 -0
- architecture.txt +28 -0
- configs/infer_mode.yaml +10 -0
- configs/model/test_model.yaml +10 -0
- configs/optimizer/optimizer.yaml +0 -0
- configs/runtime/runtime.yaml +3 -0
- configs/train_mode.yaml +9 -0
- docker-compose.yml +14 -0
- environment.yml +12 -0
- pyproject.toml +58 -0
- requirements.txt +0 -0
- scripts/inference.py +53 -0
- scripts/train.py +0 -0
- src/transformer/__init__.py +4 -0
- src/transformer/configs.py +78 -0
- src/transformer/modules/__init__.py +6 -0
- src/transformer/modules/attention.py +105 -0
- src/transformer/modules/decoder.py +99 -0
- src/transformer/modules/embedding.py +121 -0
- src/transformer/modules/encoder.py +71 -0
- src/transformer/modules/feedforward.py +59 -0
- src/transformer/modules/lm_head.py +47 -0
- src/transformer/transformer.py +239 -0
- src/transformer/utils.py +612 -0
- tests/units/modules/test_attention.py +177 -0
- tests/units/modules/test_decoder.py +94 -0
- tests/units/modules/test_embedding.py +222 -0
- tests/units/modules/test_encoder.py +69 -0
- tests/units/modules/test_feedforward.py +131 -0
- tests/units/modules/test_lm_head.py +73 -0
- tests/units/test_transformer.py +326 -0
- tests/units/test_utils.py +749 -0
.dockerignore
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =========================================
|
| 2 |
+
# 1. Python cache & build artifacts
|
| 3 |
+
# =========================================
|
| 4 |
+
__pycache__/
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.mypy_cache/
|
| 7 |
+
.ruff_cache/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
*.so
|
| 12 |
+
build/
|
| 13 |
+
dist/
|
| 14 |
+
*.egg-info/
|
| 15 |
+
|
| 16 |
+
# =========================================
|
| 17 |
+
# 2. Virtual environments
|
| 18 |
+
# =========================================
|
| 19 |
+
env/
|
| 20 |
+
.venv/
|
| 21 |
+
venv/
|
| 22 |
+
condaenv.*.requirements.txt
|
| 23 |
+
|
| 24 |
+
# =========================================
|
| 25 |
+
# 3. Jupyter notebooks
|
| 26 |
+
# =========================================
|
| 27 |
+
.ipynb_checkpoints/
|
| 28 |
+
*.nbconvert.ipynb
|
| 29 |
+
|
| 30 |
+
# =========================================
|
| 31 |
+
# 4. Logs & temp files
|
| 32 |
+
# =========================================
|
| 33 |
+
*.log
|
| 34 |
+
*.tmp
|
| 35 |
+
*.swp
|
| 36 |
+
*.swo
|
| 37 |
+
*.bak
|
| 38 |
+
.DS_Store
|
| 39 |
+
Thumbs.db
|
| 40 |
+
|
| 41 |
+
# =========================================
|
| 42 |
+
# 5. IDE / Editor configs
|
| 43 |
+
# =========================================
|
| 44 |
+
.vscode/
|
| 45 |
+
.idea/
|
| 46 |
+
*.sublime-*
|
| 47 |
+
*.iml
|
| 48 |
+
|
| 49 |
+
# =========================================
|
| 50 |
+
# 6. Machine Learning artifacts
|
| 51 |
+
# =========================================
|
| 52 |
+
data/
|
| 53 |
+
outputs/
|
| 54 |
+
runs/
|
| 55 |
+
artifacts/
|
| 56 |
+
*.pt
|
| 57 |
+
*.pth
|
| 58 |
+
*.ckpt
|
| 59 |
+
*.h5
|
| 60 |
+
*.onnx
|
| 61 |
+
|
| 62 |
+
# =========================================
|
| 63 |
+
# 7. Version control
|
| 64 |
+
# =========================================
|
| 65 |
+
.git
|
| 66 |
+
.gitignore
|
| 67 |
+
.pre-commit-config.yaml
|
.gitignore
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =========================================
|
| 2 |
+
# Python cache & build artifacts
|
| 3 |
+
# =========================================
|
| 4 |
+
__pycache__/
|
| 5 |
+
.pytest_cache/
|
| 6 |
+
.mypy_cache/
|
| 7 |
+
.ruff_cache/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
*.so
|
| 12 |
+
build/
|
| 13 |
+
dist/
|
| 14 |
+
*.egg-info/
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# =========================================
|
| 18 |
+
# Virtual / Conda environments
|
| 19 |
+
# =========================================
|
| 20 |
+
env/
|
| 21 |
+
.venv/
|
| 22 |
+
venv/
|
| 23 |
+
condaenv.*.requirements.txt
|
| 24 |
+
|
| 25 |
+
# =========================================
|
| 26 |
+
# Jupyter / Notebooks
|
| 27 |
+
# =========================================
|
| 28 |
+
.ipynb_checkpoints/
|
| 29 |
+
*.nbconvert.ipynb
|
| 30 |
+
|
| 31 |
+
# =========================================
|
| 32 |
+
# Logs / Temporary files
|
| 33 |
+
# =========================================
|
| 34 |
+
*.log
|
| 35 |
+
*.tmp
|
| 36 |
+
*.swp
|
| 37 |
+
*.swo
|
| 38 |
+
*.bak
|
| 39 |
+
.DS_Store
|
| 40 |
+
Thumbs.db
|
| 41 |
+
|
| 42 |
+
# =========================================
|
| 43 |
+
# IDE / Editor settings
|
| 44 |
+
# =========================================
|
| 45 |
+
.vscode/
|
| 46 |
+
.idea/
|
| 47 |
+
*.sublime-*
|
| 48 |
+
*.iml
|
| 49 |
+
|
| 50 |
+
# =========================================
|
| 51 |
+
# Machine Learning artifacts
|
| 52 |
+
# =========================================
|
| 53 |
+
data/
|
| 54 |
+
outputs/
|
| 55 |
+
runs/
|
| 56 |
+
artifacts/
|
| 57 |
+
*.pt
|
| 58 |
+
*.pth
|
| 59 |
+
*.ckpt
|
| 60 |
+
*.h5
|
| 61 |
+
*.onnx
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 3 |
+
rev: v0.12.12
|
| 4 |
+
hooks:
|
| 5 |
+
- id: ruff # lints
|
| 6 |
+
- id: ruff-format # formats (ruff's formatter is black-compatible)
|
| 7 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 8 |
+
rev: v6.0.0
|
| 9 |
+
hooks:
|
| 10 |
+
- id: end-of-file-fixer
|
| 11 |
+
- id: trailing-whitespace
|
| 12 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 13 |
+
rev: v1.17.1
|
| 14 |
+
hooks:
|
| 15 |
+
- id: mypy
|
| 16 |
+
args: ["--config-file=pyproject.toml"]
|
| 17 |
+
additional_dependencies: [pydantic>=2]
|
Dockerfile.dev
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
# FROM = choose a base OS + language runtime (Debian slim with Python 3.12)
|
| 4 |
+
|
| 5 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
| 6 |
+
PIP_NO_CACHE_DIR=1 \
|
| 7 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 8 |
+
PYTHONUNBUFFERED=1
|
| 9 |
+
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
+
git build-essential && \
|
| 12 |
+
rm -rf /var/lib/apt/lists/*
|
| 13 |
+
# RUN = execute at build time
|
| 14 |
+
# apt-get update -> refresh package index
|
| 15 |
+
# apt-get install -> install tools you might need (git, compilers)
|
| 16 |
+
# --no-install-recommends -> fewer extra packages
|
| 17 |
+
# rm -rf ... -> clean apt cache to shrink the layer
|
| 18 |
+
|
| 19 |
+
WORKDIR /app
|
| 20 |
+
# WORKDIR = default directory for the following Dockerfile steps and 'docker run' processes
|
| 21 |
+
|
| 22 |
+
COPY requirements.txt pyproject.toml ./
|
| 23 |
+
# COPY = bring files from host into the image
|
| 24 |
+
# Only copying dependency metadata first lets Docker cache layer if code changes later
|
| 25 |
+
|
| 26 |
+
RUN pip install -U pip && pip install -r requirements.txt
|
| 27 |
+
# pip install -U pip -> upgrade pip in the image
|
| 28 |
+
# pip install -r ... -> install your Python runtime dependencies
|
| 29 |
+
|
| 30 |
+
COPY . .
|
| 31 |
+
# Now copy your actual code (done AFTER deps for better caching)
|
| 32 |
+
|
| 33 |
+
RUN pip install -e .[dev]
|
| 34 |
+
# -e . = editable install (your src is importable as a package without rebuilding)
|
| 35 |
+
|
| 36 |
+
CMD ["pytest"]
|
| 37 |
+
# CMD = default command when a container starts from this image
|
| 38 |
+
# Here: run unit tests quietly (-q) in tests/units
|
Makefile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: help conda-setup precommit lint fmt type test clean docker-build docker-run
|
| 2 |
+
|
| 3 |
+
help:
|
| 4 |
+
@echo "Common Makefile commands:"
|
| 5 |
+
@echo " conda-setup - Create and activate conda env, install all dependencies, setup pre-commit"
|
| 6 |
+
@echo " precommit - Run all pre-commit hooks (lint, fmt, type)"
|
| 7 |
+
@echo " lint - Run ruff linter"
|
| 8 |
+
@echo " fmt - Run ruff formatter and black"
|
| 9 |
+
@echo " type - Run mypy type checks"
|
| 10 |
+
@echo " test - Run all tests with pytest and coverage"
|
| 11 |
+
@echo " clean - Remove build, cache, and venv artifacts"
|
| 12 |
+
@echo " docker-build - Build the Docker dev image"
|
| 13 |
+
@echo " docker-run - Run the Docker dev container (interactive)"
|
| 14 |
+
|
| 15 |
+
conda-setup:
|
| 16 |
+
conda env create -n transformer -f environment.yml|| conda env update -n transformer -f environment.yml
|
| 17 |
+
conda run -n transformer pip install -e .[dev]
|
| 18 |
+
conda run -n transformer pre-commit install
|
| 19 |
+
|
| 20 |
+
precommit:
|
| 21 |
+
conda run -n transformer pre-commit run --all-files
|
| 22 |
+
|
| 23 |
+
lint:
|
| 24 |
+
conda run -n transformer ruff check .
|
| 25 |
+
|
| 26 |
+
fmt:
|
| 27 |
+
conda run -n transformer ruff format .
|
| 28 |
+
conda run -n transformer black src tests
|
| 29 |
+
|
| 30 |
+
type:
|
| 31 |
+
conda run -n transformer mypy src
|
| 32 |
+
|
| 33 |
+
test:
|
| 34 |
+
conda run -n transformer pytest
|
| 35 |
+
clean:
|
| 36 |
+
rm -rf .venv .pytest_cache .mypy_cache dist build *.egg-info
|
| 37 |
+
|
| 38 |
+
docker-build:
|
| 39 |
+
docker compose build dev --no-cache
|
| 40 |
+
|
| 41 |
+
docker-run:
|
| 42 |
+
docker compose run --rm dev bash
|
README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# transformer
|
| 2 |
+
|
| 3 |
+
Minimal encoder–decoder Transformer.
|
| 4 |
+
|
| 5 |
+
## Quickstart
|
| 6 |
+
```bash
|
| 7 |
+
make conda-setup
|
| 8 |
+
make test
|
architecture.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Transformer:
|
| 2 |
+
embedding:
|
| 3 |
+
input_embedding (same for encoder and decoder)
|
| 4 |
+
positional_embedding
|
| 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
|
configs/infer_mode.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defaults:
|
| 2 |
+
- model: test_model
|
| 3 |
+
- runtime: runtime
|
| 4 |
+
- _self_
|
| 5 |
+
input_text: ""
|
| 6 |
+
max_new_tokens: 64
|
| 7 |
+
temperature: 1.0
|
| 8 |
+
top_k: 4
|
| 9 |
+
top_p: 0.7
|
| 10 |
+
do_sample: True
|
configs/model/test_model.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
d_model: 256
|
| 2 |
+
num_heads: 4
|
| 3 |
+
num_layers: 4
|
| 4 |
+
d_ff: 1024
|
| 5 |
+
dropout_rate: 0.1
|
| 6 |
+
vocab_size: 32100
|
| 7 |
+
max_seq_len: 128
|
| 8 |
+
pad_id: 0
|
| 9 |
+
bos_id: 1
|
| 10 |
+
eos_id: 2
|
configs/optimizer/optimizer.yaml
ADDED
|
File without changes
|
configs/runtime/runtime.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tokenizer_name: t5-small
|
| 2 |
+
weights_path: artifacts/checkpoints/initial_model_weights.pth
|
| 3 |
+
artifact_dir: outputs/${now:%Y-%m-%d}/${now:%H-%M-%S}
|
configs/train_mode.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
seed: 42
|
| 2 |
+
batch_size: 16
|
| 3 |
+
epochs: 3
|
| 4 |
+
lr: 3e-4
|
| 5 |
+
weight_decay: 0.01
|
| 6 |
+
grad_clip: 1.0
|
| 7 |
+
precision: "fp32" # one of: fp32, fp16, bf16
|
| 8 |
+
checkpoint_dir: "artifacts/checkpoints"
|
| 9 |
+
log_dir: "artifacts/mlruns"
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
dev:
|
| 3 |
+
build:
|
| 4 |
+
context: .
|
| 5 |
+
dockerfile: Dockerfile.dev
|
| 6 |
+
image: transformer:dev
|
| 7 |
+
container_name: transformer_dev
|
| 8 |
+
working_dir: /app
|
| 9 |
+
volumes:
|
| 10 |
+
- ./artifacts:/app/artifacts
|
| 11 |
+
stdin_open: true
|
| 12 |
+
tty: true
|
| 13 |
+
gpus: all
|
| 14 |
+
command: bash
|
environment.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: transformer
|
| 2 |
+
channels:
|
| 3 |
+
- defaults
|
| 4 |
+
dependencies:
|
| 5 |
+
- python=3.12
|
| 6 |
+
- pip
|
| 7 |
+
- pip:
|
| 8 |
+
- --extra-index-url https://download.pytorch.org/whl/cu129
|
| 9 |
+
- torch==2.8.0+cu129
|
| 10 |
+
- transformers
|
| 11 |
+
- omegaconf
|
| 12 |
+
- hydra-core
|
pyproject.toml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "transformer"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "A transformer implementation"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.10"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"torch==2.8",
|
| 9 |
+
"transformers",
|
| 10 |
+
"omegaconf",
|
| 11 |
+
"hydra-core",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[project.optional-dependencies]
|
| 15 |
+
dev = [
|
| 16 |
+
"pytest",
|
| 17 |
+
"pytest-cov",
|
| 18 |
+
"ruff",
|
| 19 |
+
"black",
|
| 20 |
+
"mypy",
|
| 21 |
+
"pre-commit",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[tool.setuptools]
|
| 25 |
+
package-dir = {"" = "src"}
|
| 26 |
+
|
| 27 |
+
[build-system]
|
| 28 |
+
requires = ["setuptools>=42", "wheel"]
|
| 29 |
+
build-backend = "setuptools.build_meta"
|
| 30 |
+
|
| 31 |
+
[tool.setuptools.packages.find]
|
| 32 |
+
where = ["src"]
|
| 33 |
+
|
| 34 |
+
[tool.pytest.ini_options]
|
| 35 |
+
minversion = "7.0"
|
| 36 |
+
addopts = "-ra -q"
|
| 37 |
+
testpaths = ["tests"]
|
| 38 |
+
|
| 39 |
+
[tool.black]
|
| 40 |
+
line-length = 100
|
| 41 |
+
target-version = ["py310"]
|
| 42 |
+
|
| 43 |
+
[tool.ruff]
|
| 44 |
+
line-length = 100
|
| 45 |
+
|
| 46 |
+
[tool.ruff.lint]
|
| 47 |
+
select = ["E","F","I","B","UP"]
|
| 48 |
+
ignore = ["E501"]
|
| 49 |
+
|
| 50 |
+
[tool.mypy]
|
| 51 |
+
python_version = "3.11"
|
| 52 |
+
strict = false
|
| 53 |
+
ignore_missing_imports = true
|
| 54 |
+
|
| 55 |
+
# Make mypy understand the src/ layout
|
| 56 |
+
files = ["src"]
|
| 57 |
+
mypy_path = ["src"]
|
| 58 |
+
explicit_package_bases = true
|
requirements.txt
ADDED
|
Binary file (233 Bytes). View file
|
|
|
scripts/inference.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hydra
|
| 2 |
+
import torch
|
| 3 |
+
from hydra.utils import to_absolute_path
|
| 4 |
+
from omegaconf import DictConfig, OmegaConf
|
| 5 |
+
from transformers import AutoTokenizer
|
| 6 |
+
|
| 7 |
+
from transformer import (
|
| 8 |
+
BasicEncDecCfg,
|
| 9 |
+
BasicEncoderDecoderTransformer,
|
| 10 |
+
InferAppCfg,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@hydra.main(config_path="../configs", config_name="infer_mode", version_base=None)
|
| 15 |
+
def main(cfg: DictConfig):
|
| 16 |
+
scfg_temp = OmegaConf.merge(OmegaConf.structured(InferAppCfg), cfg)
|
| 17 |
+
scfg: InferAppCfg = OmegaConf.to_object(scfg_temp)
|
| 18 |
+
model_cfg = BasicEncDecCfg(**vars(scfg.model))
|
| 19 |
+
|
| 20 |
+
transformer = BasicEncoderDecoderTransformer(model_cfg)
|
| 21 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 22 |
+
scfg.runtime.tokenizer_name,
|
| 23 |
+
use_fast=True,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
if scfg.runtime.weights_path:
|
| 27 |
+
ckpt = torch.load(to_absolute_path(scfg.runtime.weights_path), map_location="cpu")
|
| 28 |
+
state = ckpt.get("model_state_dict", ckpt)
|
| 29 |
+
transformer.load_state_dict(state, strict=False)
|
| 30 |
+
transformer.eval()
|
| 31 |
+
|
| 32 |
+
text = scfg.input_text
|
| 33 |
+
if not text:
|
| 34 |
+
raise SystemExit('Pass text like: input_text="hello world"')
|
| 35 |
+
|
| 36 |
+
encoded = tokenizer(text, padding=True, truncation=True, return_tensors="pt")
|
| 37 |
+
src_ids = encoded["input_ids"]
|
| 38 |
+
src_padd_mask = (encoded["attention_mask"] == 0).unsqueeze(1).unsqueeze(2)
|
| 39 |
+
tgt_ids = transformer.generate(
|
| 40 |
+
src_ids,
|
| 41 |
+
src_padd_mask,
|
| 42 |
+
max_new_tokens=scfg.max_new_tokens,
|
| 43 |
+
temperature=scfg.temperature,
|
| 44 |
+
top_k=scfg.top_k,
|
| 45 |
+
top_p=scfg.top_p,
|
| 46 |
+
do_sample=scfg.do_sample,
|
| 47 |
+
)
|
| 48 |
+
output = tokenizer.batch_decode(tgt_ids, skip_special_tokens=True)
|
| 49 |
+
print(output)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|
scripts/train.py
ADDED
|
File without changes
|
src/transformer/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .configs import BasicEncDecCfg, InferAppCfg, TrainAppCfg
|
| 2 |
+
from .transformer import BasicEncoderDecoderTransformer
|
| 3 |
+
|
| 4 |
+
__all__ = ["BasicEncoderDecoderTransformer", "BasicEncDecCfg", "TrainAppCfg", "InferAppCfg"]
|
src/transformer/configs.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class BasicEncDecCfg:
|
| 6 |
+
vocab_size: int
|
| 7 |
+
d_model: int
|
| 8 |
+
max_seq_len: int
|
| 9 |
+
num_heads: int
|
| 10 |
+
d_ff: int
|
| 11 |
+
num_layers: int
|
| 12 |
+
dropout_rate: float = 0.1
|
| 13 |
+
pad_id: int = 0
|
| 14 |
+
bos_id: int = 2
|
| 15 |
+
eos_id: int = 1
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class DataCfg:
|
| 20 |
+
train_path: str
|
| 21 |
+
val_path: str
|
| 22 |
+
num_workers: int = 4
|
| 23 |
+
batch_size: int = 32
|
| 24 |
+
max_length: int = 512
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class OptimCfg:
|
| 29 |
+
lr: float = 3e-4
|
| 30 |
+
betas: tuple[float, float] = (0.9, 0.95)
|
| 31 |
+
weight_decay: float = 0.1
|
| 32 |
+
eps: float = 1e-8
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class SchedCfg:
|
| 37 |
+
type: str = "cosine"
|
| 38 |
+
warmup_steps: int = 2000
|
| 39 |
+
total_steps: int = 200000
|
| 40 |
+
min_lr: float = 1e-5
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclass
|
| 44 |
+
class TrainerCfg:
|
| 45 |
+
epochs: int = 10
|
| 46 |
+
gradient_accumulation: int = 1
|
| 47 |
+
eval_interval: int = 1000
|
| 48 |
+
save_dir: str = "outputs/run"
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class RuntimeCfg:
|
| 53 |
+
tokenizer_name: str = "t5-small"
|
| 54 |
+
weights_path: str = ""
|
| 55 |
+
artifact_dir: str = "outputs/run"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class TrainAppCfg:
|
| 60 |
+
seed: int
|
| 61 |
+
model: BasicEncDecCfg
|
| 62 |
+
data: DataCfg
|
| 63 |
+
optim: OptimCfg
|
| 64 |
+
scheduler: SchedCfg
|
| 65 |
+
trainer: TrainerCfg
|
| 66 |
+
runtime: RuntimeCfg
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@dataclass
|
| 70 |
+
class InferAppCfg:
|
| 71 |
+
model: BasicEncDecCfg
|
| 72 |
+
runtime: RuntimeCfg
|
| 73 |
+
input_text: str = ""
|
| 74 |
+
max_new_tokens: int = 64
|
| 75 |
+
temperature: float = 1.0
|
| 76 |
+
top_k: int = 0
|
| 77 |
+
top_p: float = 1.0
|
| 78 |
+
do_sample: bool = False
|
src/transformer/modules/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"]
|
src/transformer/modules/attention.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
from transformer.utils import calculate_attention, join_heads, split_heads
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class MultiHeadAttention(nn.Module):
|
| 9 |
+
"""
|
| 10 |
+
Multi-head attention: linear projections -> split heads -> scaled dot-product
|
| 11 |
+
attention (via utils) -> merge heads -> output projection (+ dropout).
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
d_model (int): Model dimension (>0). Must be divisible by num_heads.
|
| 15 |
+
num_heads (int): Number of attention heads (>0).
|
| 16 |
+
dropout_rate (float): Dropout probability in (0,1).
|
| 17 |
+
|
| 18 |
+
Inputs:
|
| 19 |
+
query, key, value: (B, S, D) with D == d_model
|
| 20 |
+
mask (optional): Tensor broadcastable to (B, H, S_q, S_k), either boolean
|
| 21 |
+
(True = masked) or additive float mask.
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
Tensor: (B, S_q, D)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(self, d_model: int, num_heads: int, dropout_rate: float):
|
| 28 |
+
super().__init__()
|
| 29 |
+
# ---- type checks
|
| 30 |
+
if not isinstance(num_heads, int):
|
| 31 |
+
raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
|
| 32 |
+
if not isinstance(d_model, int):
|
| 33 |
+
raise TypeError(f"d_model must be an int, got {type(d_model)}")
|
| 34 |
+
if not isinstance(dropout_rate, float):
|
| 35 |
+
raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
|
| 36 |
+
|
| 37 |
+
# ---- value checks
|
| 38 |
+
if num_heads <= 0:
|
| 39 |
+
raise ValueError(f"num_heads must be strictly greater than 0, got {num_heads}")
|
| 40 |
+
if d_model <= 0:
|
| 41 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
|
| 42 |
+
if d_model % num_heads != 0:
|
| 43 |
+
raise ValueError("d_model must be divisible by num_heads")
|
| 44 |
+
if not (0 <= dropout_rate <= 1):
|
| 45 |
+
raise ValueError(f"dropout_rate must be between 0 and 1 included, got {dropout_rate}")
|
| 46 |
+
|
| 47 |
+
self.d_model = d_model
|
| 48 |
+
self.num_heads = num_heads
|
| 49 |
+
self.d_head = d_model // num_heads
|
| 50 |
+
self.dropout_rate = dropout_rate
|
| 51 |
+
|
| 52 |
+
self.query_linear = nn.Linear(d_model, d_model, bias=False)
|
| 53 |
+
self.key_linear = nn.Linear(d_model, d_model, bias=False)
|
| 54 |
+
self.value_linear = nn.Linear(d_model, d_model, bias=False)
|
| 55 |
+
self.output_linear = nn.Linear(d_model, d_model, bias=True)
|
| 56 |
+
self.dropout = nn.Dropout(dropout_rate)
|
| 57 |
+
|
| 58 |
+
def forward(
|
| 59 |
+
self,
|
| 60 |
+
query: Tensor,
|
| 61 |
+
key: Tensor,
|
| 62 |
+
value: Tensor,
|
| 63 |
+
mask: Tensor | None,
|
| 64 |
+
) -> Tensor:
|
| 65 |
+
# ---- basic type checks
|
| 66 |
+
if not isinstance(query, torch.Tensor):
|
| 67 |
+
raise TypeError(f"query must be a torch.Tensor, got {type(query)}")
|
| 68 |
+
if not isinstance(key, torch.Tensor):
|
| 69 |
+
raise TypeError(f"key must be a torch.Tensor, got {type(key)}")
|
| 70 |
+
if not isinstance(value, torch.Tensor):
|
| 71 |
+
raise TypeError(f"value must be a torch.Tensor, got {type(value)}")
|
| 72 |
+
if mask is not None and not isinstance(mask, torch.Tensor):
|
| 73 |
+
raise TypeError(f"mask must be a torch.Tensor or None, got {type(mask)}")
|
| 74 |
+
|
| 75 |
+
# ---- shape checks for q/k/v (3D [B,S,D] and D == d_model)
|
| 76 |
+
if query.dim() != 3 or key.dim() != 3 or value.dim() != 3:
|
| 77 |
+
raise ValueError(
|
| 78 |
+
"query/key/value must be 3D tensors of shape (B, S, D); "
|
| 79 |
+
f"got q={tuple(query.shape)}, k={tuple(key.shape)}, v={tuple(value.shape)}"
|
| 80 |
+
)
|
| 81 |
+
Bq, Sq, Dq = query.shape
|
| 82 |
+
Bk, Sk, Dk = key.shape
|
| 83 |
+
Bv, Sv, Dv = value.shape
|
| 84 |
+
|
| 85 |
+
if not (Dq == Dk == Dv == self.d_model):
|
| 86 |
+
raise ValueError(
|
| 87 |
+
f"Last dimension must equal d_model={self.d_model}; got Dq={Dq}, Dk={Dk}, Dv={Dv}"
|
| 88 |
+
)
|
| 89 |
+
if not (Bq == Bk == Bv):
|
| 90 |
+
raise ValueError(f"Batch size mismatch: q={Bq}, k={Bk}, v={Bv}")
|
| 91 |
+
if Sk != Sv:
|
| 92 |
+
raise ValueError(f"Key/Value seq length mismatch: Sk={Sk} vs Sv={Sv}")
|
| 93 |
+
|
| 94 |
+
# ---- project and split into heads -> (B, H, S, Dh)
|
| 95 |
+
q = split_heads(self.query_linear(query), self.num_heads)
|
| 96 |
+
k = split_heads(self.key_linear(key), self.num_heads)
|
| 97 |
+
v = split_heads(self.value_linear(value), self.num_heads)
|
| 98 |
+
|
| 99 |
+
# ---- attention (utils handles mask broadcasting/device and numeric stability)
|
| 100 |
+
p = self.dropout_rate if self.training else 0.0
|
| 101 |
+
attn = calculate_attention(q, k, v, mask, attn_dropout_p=p) # (B, H, Sq, Dh)
|
| 102 |
+
|
| 103 |
+
# ---- merge heads, project out, dropout
|
| 104 |
+
out = join_heads(attn) # (B, Sq, D)
|
| 105 |
+
return self.dropout(self.output_linear(out))
|
src/transformer/modules/decoder.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
from transformer.modules.attention import MultiHeadAttention
|
| 6 |
+
from transformer.modules.feedforward import FeedForwardLayer
|
| 7 |
+
from transformer.utils import combine_masks
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DecoderLayer(nn.Module):
|
| 11 |
+
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float):
|
| 12 |
+
super().__init__()
|
| 13 |
+
|
| 14 |
+
if not isinstance(d_model, int):
|
| 15 |
+
raise TypeError(f"d_model must be an int, got {type(d_model)}")
|
| 16 |
+
if not isinstance(num_heads, int):
|
| 17 |
+
raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
|
| 18 |
+
if not isinstance(d_ff, int):
|
| 19 |
+
raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
|
| 20 |
+
if not isinstance(dropout_rate, float):
|
| 21 |
+
raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
|
| 22 |
+
|
| 23 |
+
if not d_model > 0:
|
| 24 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
|
| 25 |
+
if not num_heads > 0:
|
| 26 |
+
raise ValueError(f"num_heads must be strictly greater than 0, got {num_heads}")
|
| 27 |
+
if not d_ff > 0:
|
| 28 |
+
raise ValueError(f"d_ff must be strictly greater than 0, got {d_ff}")
|
| 29 |
+
if not (0.0 <= dropout_rate < 1.0):
|
| 30 |
+
raise ValueError(f"dropout_rate must be between 0 and 1 excluded, got {dropout_rate}")
|
| 31 |
+
|
| 32 |
+
self.self_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
|
| 33 |
+
self.feed_forward = FeedForwardLayer(d_model, d_ff)
|
| 34 |
+
self.cross_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
|
| 35 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 36 |
+
self.dropout1 = nn.Dropout(dropout_rate)
|
| 37 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 38 |
+
self.dropout2 = nn.Dropout(dropout_rate)
|
| 39 |
+
self.norm3 = nn.LayerNorm(d_model)
|
| 40 |
+
self.dropout3 = nn.Dropout(dropout_rate)
|
| 41 |
+
|
| 42 |
+
def forward(
|
| 43 |
+
self,
|
| 44 |
+
x: Tensor,
|
| 45 |
+
y: Tensor,
|
| 46 |
+
src_padding_mask: Tensor | None,
|
| 47 |
+
tgt_padding_mask: Tensor | None,
|
| 48 |
+
tgt_causal_mask: Tensor | None,
|
| 49 |
+
) -> Tensor:
|
| 50 |
+
if not isinstance(x, torch.Tensor):
|
| 51 |
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
| 52 |
+
if not isinstance(y, torch.Tensor):
|
| 53 |
+
raise TypeError(f"y must be a torch.Tensor, got {type(y)}")
|
| 54 |
+
if not (x.dim() == 3):
|
| 55 |
+
raise ValueError(
|
| 56 |
+
f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
|
| 57 |
+
)
|
| 58 |
+
if not (y.dim() == 3):
|
| 59 |
+
raise ValueError(
|
| 60 |
+
f"y must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(y.shape)}"
|
| 61 |
+
)
|
| 62 |
+
if not (x.shape[0] == y.shape[0] and x.shape[-1] == y.shape[-1]):
|
| 63 |
+
raise ValueError(
|
| 64 |
+
"Batch size or d_model mismatch between encoder memory and decoder input"
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
tgt_mask = combine_masks(tgt_padding_mask, tgt_causal_mask)
|
| 68 |
+
y = self.norm1(y + self.dropout1(self.self_attention_layer(y, y, y, tgt_mask)))
|
| 69 |
+
y = self.norm2(y + self.dropout2(self.cross_attention_layer(y, x, x, src_padding_mask)))
|
| 70 |
+
y = self.norm3(y + self.dropout3(self.feed_forward(y)))
|
| 71 |
+
return y
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class TransformerDecoder(nn.Module):
|
| 75 |
+
def __init__(
|
| 76 |
+
self, d_model: int, num_heads: int, d_ff: int, num_layers: int, dropout_rate: float
|
| 77 |
+
):
|
| 78 |
+
super().__init__()
|
| 79 |
+
|
| 80 |
+
if not isinstance(num_layers, int):
|
| 81 |
+
raise TypeError(f"num_layers must be an int, got {type(num_layers)}")
|
| 82 |
+
if not num_layers > 0:
|
| 83 |
+
raise ValueError(f"num_layers must be strictly greater than 0, got {num_layers}")
|
| 84 |
+
|
| 85 |
+
self.layers = nn.ModuleList(
|
| 86 |
+
[DecoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
def forward(
|
| 90 |
+
self,
|
| 91 |
+
x: Tensor,
|
| 92 |
+
y: Tensor,
|
| 93 |
+
src_padding_mask: Tensor | None,
|
| 94 |
+
tgt_padding_mask: Tensor | None,
|
| 95 |
+
tgt_causal_mask: Tensor | None,
|
| 96 |
+
) -> Tensor:
|
| 97 |
+
for layer in self.layers:
|
| 98 |
+
y = layer(x, y, src_padding_mask, tgt_padding_mask, tgt_causal_mask)
|
| 99 |
+
return y
|
src/transformer/modules/embedding.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
from transformer.utils import sinusoidal_positional_encoding
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class InputEmbedding(nn.Module):
|
| 9 |
+
"""
|
| 10 |
+
Token + positional embedding module.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
vocab_size (int): Size of vocabulary (>0).
|
| 14 |
+
d_model (int): Embedding dimension (>0).
|
| 15 |
+
sequence_length (int): Maximum sequence length (≥0).
|
| 16 |
+
pad_id (int): Padding token ID (0 <= pad_id < vocab_size).
|
| 17 |
+
|
| 18 |
+
Input:
|
| 19 |
+
x (LongTensor): shape (batch_size, seq_len), token IDs.
|
| 20 |
+
|
| 21 |
+
Output:
|
| 22 |
+
Tensor: shape (batch_size, seq_len, d_model), embeddings.
|
| 23 |
+
|
| 24 |
+
Notes:
|
| 25 |
+
- Zero-length sequences are allowed (returns [B, 0, D]).
|
| 26 |
+
- Positional encodings are sinusoidal and added to token embeddings.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
vocab_size: int,
|
| 32 |
+
d_model: int,
|
| 33 |
+
sequence_length: int,
|
| 34 |
+
pad_id: int,
|
| 35 |
+
dropout_rate: float = 0.0,
|
| 36 |
+
):
|
| 37 |
+
super().__init__()
|
| 38 |
+
# --- type checks
|
| 39 |
+
if not isinstance(vocab_size, int):
|
| 40 |
+
raise TypeError(f"vocab_size must be int, got {type(vocab_size)}")
|
| 41 |
+
if not isinstance(d_model, int):
|
| 42 |
+
raise TypeError(f"d_model must be int, got {type(d_model)}")
|
| 43 |
+
if not isinstance(sequence_length, int):
|
| 44 |
+
raise TypeError(f"sequence_length must be int, got {type(sequence_length)}")
|
| 45 |
+
if not isinstance(pad_id, int):
|
| 46 |
+
raise TypeError(f"pad_id must be int, got {type(pad_id)}")
|
| 47 |
+
if not isinstance(dropout_rate, float):
|
| 48 |
+
raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
|
| 49 |
+
|
| 50 |
+
# --- value checks
|
| 51 |
+
if vocab_size <= 0:
|
| 52 |
+
raise ValueError(f"vocab_size must be > 0, got {vocab_size}")
|
| 53 |
+
if d_model <= 0:
|
| 54 |
+
raise ValueError(f"d_model must be > 0, got {d_model}")
|
| 55 |
+
if sequence_length < 0:
|
| 56 |
+
raise ValueError(f"sequence_length must be >= 0, got {sequence_length}")
|
| 57 |
+
if not (0 <= pad_id < vocab_size):
|
| 58 |
+
raise ValueError(f"pad_id must be in [0, {vocab_size - 1}], got {pad_id}")
|
| 59 |
+
if not (0 <= dropout_rate <= 1):
|
| 60 |
+
raise ValueError(f"dropout_rate must be in [0,1], got {dropout_rate}")
|
| 61 |
+
self.vocab_size = vocab_size
|
| 62 |
+
self.d_model = d_model
|
| 63 |
+
self.sequence_length = sequence_length
|
| 64 |
+
self.pad_id = pad_id
|
| 65 |
+
self.dropout_rate = dropout_rate
|
| 66 |
+
|
| 67 |
+
self.token_embed = nn.Embedding(vocab_size, d_model, padding_idx=pad_id)
|
| 68 |
+
self.pos_embed = PositionalEmbedding(sequence_length, d_model)
|
| 69 |
+
self.dropout = nn.Dropout(dropout_rate)
|
| 70 |
+
|
| 71 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 72 |
+
if not isinstance(x, torch.Tensor):
|
| 73 |
+
raise TypeError(f"x must be Tensor, got {type(x)}")
|
| 74 |
+
if x.dtype != torch.long:
|
| 75 |
+
raise TypeError(f"x must be torch.long, got {x.dtype}")
|
| 76 |
+
if x.dim() != 2:
|
| 77 |
+
raise ValueError(f"x must be 2D (B, S), got shape {tuple(x.shape)}")
|
| 78 |
+
|
| 79 |
+
tok = self.token_embed(x) # (B, S, D) — S can be 0
|
| 80 |
+
return self.dropout(self.pos_embed(tok)) # (B, S, D)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class PositionalEmbedding(nn.Module):
|
| 84 |
+
"""
|
| 85 |
+
Adds sinusoidal positional encodings.
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
sequence_length (int): Maximum sequence length (≥0).
|
| 89 |
+
d_model (int): Embedding dimension (>0).
|
| 90 |
+
|
| 91 |
+
Notes:
|
| 92 |
+
- Zero-length tables are supported (shape [1, 0, D]).
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
def __init__(self, sequence_length: int, d_model: int):
|
| 96 |
+
super().__init__()
|
| 97 |
+
|
| 98 |
+
if sequence_length < 0:
|
| 99 |
+
raise ValueError(f"sequence_length must be >= 0, got {sequence_length}")
|
| 100 |
+
if d_model <= 0:
|
| 101 |
+
raise ValueError(f"d_model must be > 0, got {d_model}")
|
| 102 |
+
|
| 103 |
+
self.sequence_length = sequence_length
|
| 104 |
+
self.d_model = d_model
|
| 105 |
+
|
| 106 |
+
pe = sinusoidal_positional_encoding(sequence_length, d_model) # [S, D]
|
| 107 |
+
self.register_buffer("pe", pe.unsqueeze(0)) # [1, S, D]
|
| 108 |
+
|
| 109 |
+
def forward(self, x: Tensor) -> Tensor:
|
| 110 |
+
if not isinstance(x, torch.Tensor):
|
| 111 |
+
raise TypeError(f"x must be Tensor, got {type(x)}")
|
| 112 |
+
if x.dim() != 3:
|
| 113 |
+
raise ValueError(f"x must be 3D (B, S, D), got shape {tuple(x.shape)}")
|
| 114 |
+
|
| 115 |
+
_, S, D = x.shape
|
| 116 |
+
if D != self.d_model:
|
| 117 |
+
raise ValueError(f"d_model mismatch: got {D}, expected {self.d_model}")
|
| 118 |
+
if S > self.sequence_length:
|
| 119 |
+
raise ValueError(f"seq_len {S} exceeds max_seq_len {self.sequence_length}")
|
| 120 |
+
|
| 121 |
+
return x + self.pe[:, :S].to(dtype=x.dtype, device=x.device)
|
src/transformer/modules/encoder.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
from transformer.modules.attention import MultiHeadAttention
|
| 6 |
+
from transformer.modules.feedforward import FeedForwardLayer
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class EncoderLayer(nn.Module):
|
| 10 |
+
def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float):
|
| 11 |
+
super().__init__()
|
| 12 |
+
|
| 13 |
+
# --- type checks (match project convention)
|
| 14 |
+
if not isinstance(d_model, int):
|
| 15 |
+
raise TypeError(f"d_model must be an int, got {type(d_model)}")
|
| 16 |
+
if not isinstance(num_heads, int):
|
| 17 |
+
raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
|
| 18 |
+
if not isinstance(d_ff, int):
|
| 19 |
+
raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
|
| 20 |
+
if not isinstance(dropout_rate, float):
|
| 21 |
+
raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
|
| 22 |
+
|
| 23 |
+
# --- value checks (match wording)
|
| 24 |
+
if not d_model > 0:
|
| 25 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
|
| 26 |
+
if not num_heads > 0:
|
| 27 |
+
raise ValueError(f"num_heads must be strictly greater than 0, got {num_heads}")
|
| 28 |
+
if not d_ff > 0:
|
| 29 |
+
raise ValueError(f"d_ff must be strictly greater than 0, got {d_ff}")
|
| 30 |
+
if not (0.0 <= dropout_rate < 1.0):
|
| 31 |
+
raise ValueError(f"dropout_rate must be between 0 and 1 excluded, got {dropout_rate}")
|
| 32 |
+
|
| 33 |
+
self.attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
|
| 34 |
+
self.feed_forward = FeedForwardLayer(d_model, d_ff)
|
| 35 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 36 |
+
self.dropout1 = nn.Dropout(dropout_rate)
|
| 37 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 38 |
+
self.dropout2 = nn.Dropout(dropout_rate)
|
| 39 |
+
|
| 40 |
+
def forward(self, x: Tensor, src_padding_mask: Tensor | None) -> Tensor:
|
| 41 |
+
if not isinstance(x, torch.Tensor):
|
| 42 |
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
| 43 |
+
if not (x.dim() == 3):
|
| 44 |
+
raise ValueError(
|
| 45 |
+
f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
x = self.norm1(x + self.dropout1(self.attention_layer(x, x, x, src_padding_mask)))
|
| 49 |
+
x = self.norm2(x + self.dropout2(self.feed_forward(x)))
|
| 50 |
+
return x
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class TransformerEncoder(nn.Module):
|
| 54 |
+
def __init__(
|
| 55 |
+
self, d_model: int, num_heads: int, d_ff: int, num_layers: int, dropout_rate: float
|
| 56 |
+
):
|
| 57 |
+
super().__init__()
|
| 58 |
+
|
| 59 |
+
if not isinstance(num_layers, int):
|
| 60 |
+
raise TypeError(f"num_layers must be an int, got {type(num_layers)}")
|
| 61 |
+
if not num_layers > 0:
|
| 62 |
+
raise ValueError(f"num_layers must be strictly greater than 0, got {num_layers}")
|
| 63 |
+
|
| 64 |
+
self.layers = nn.ModuleList(
|
| 65 |
+
[EncoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def forward(self, x: Tensor, src_padding_mask: Tensor | None) -> Tensor:
|
| 69 |
+
for layer in self.layers:
|
| 70 |
+
x = layer(x, src_padding_mask)
|
| 71 |
+
return x
|
src/transformer/modules/feedforward.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class FeedForwardLayer(nn.Module):
|
| 6 |
+
"""
|
| 7 |
+
Position-wise feed-forward layer used in Transformer blocks.
|
| 8 |
+
|
| 9 |
+
Architecture:
|
| 10 |
+
fc1: Linear(d_model -> d_ff)
|
| 11 |
+
activation: ReLU
|
| 12 |
+
dropout: nn.Dropout(dropout_rate)
|
| 13 |
+
fc2: Linear(d_ff -> d_model)
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
d_model (int): Dimensionality of model embeddings.
|
| 17 |
+
d_ff (int): Hidden dimensionality of feed-forward layer.
|
| 18 |
+
dropout_rate (float): Dropout probability between 0 and 1 (exclusive).
|
| 19 |
+
|
| 20 |
+
Shape:
|
| 21 |
+
Input: (B, S, D) where D == d_model
|
| 22 |
+
Output: (B, S, D)
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, d_model: int, d_ff: int, dropout_rate: float = 0.1):
|
| 26 |
+
super().__init__()
|
| 27 |
+
|
| 28 |
+
if not isinstance(d_ff, int):
|
| 29 |
+
raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
|
| 30 |
+
if not isinstance(d_model, int):
|
| 31 |
+
raise TypeError(f"d_model must be an int, got {type(d_model)}")
|
| 32 |
+
if not isinstance(dropout_rate, float):
|
| 33 |
+
raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
|
| 34 |
+
|
| 35 |
+
if d_ff <= 0:
|
| 36 |
+
raise ValueError(f"d_ff must be strictly greater than 0, got {d_ff}")
|
| 37 |
+
if d_model <= 0:
|
| 38 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
|
| 39 |
+
if not (0.0 <= dropout_rate < 1.0):
|
| 40 |
+
raise ValueError(f"dropout_rate must be in [0,1), got {dropout_rate}")
|
| 41 |
+
|
| 42 |
+
self.d_model = d_model
|
| 43 |
+
self.d_ff = d_ff
|
| 44 |
+
self.dropout_rate = dropout_rate
|
| 45 |
+
|
| 46 |
+
self.fc1 = nn.Linear(d_model, d_ff)
|
| 47 |
+
self.relu = nn.ReLU()
|
| 48 |
+
self.dropout = nn.Dropout(dropout_rate)
|
| 49 |
+
self.fc2 = nn.Linear(d_ff, d_model)
|
| 50 |
+
|
| 51 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 52 |
+
if not isinstance(x, torch.Tensor):
|
| 53 |
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
| 54 |
+
if x.ndim != 3:
|
| 55 |
+
raise ValueError(f"x must be 3D of shape (B,S,D); got shape {tuple(x.shape)}")
|
| 56 |
+
if x.shape[-1] != self.d_model:
|
| 57 |
+
raise ValueError(f"Last dim {x.shape[-1]} must match d_model {self.d_model}")
|
| 58 |
+
|
| 59 |
+
return self.fc2(self.dropout(self.relu(self.fc1(x))))
|
src/transformer/modules/lm_head.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class LMHead(nn.Module):
|
| 6 |
+
"""
|
| 7 |
+
Language modeling head: projects hidden states to vocabulary logits.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
d_model (int): Model hidden dimension (>0).
|
| 11 |
+
vocab_size (int): Vocabulary size (>0).
|
| 12 |
+
|
| 13 |
+
Input:
|
| 14 |
+
x (Tensor): shape (B, S, D) with D == d_model. S may be 0.
|
| 15 |
+
|
| 16 |
+
Output:
|
| 17 |
+
logits (Tensor): shape (B, S, V) with V == vocab_size.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self, d_model: int, vocab_size: int):
|
| 21 |
+
super().__init__()
|
| 22 |
+
if not isinstance(vocab_size, int):
|
| 23 |
+
raise TypeError(f"vocab_size must be an int, got {type(vocab_size)}")
|
| 24 |
+
if not isinstance(d_model, int):
|
| 25 |
+
raise TypeError(f"d_model must be an int, got {type(d_model)}")
|
| 26 |
+
if vocab_size <= 0:
|
| 27 |
+
raise ValueError(f"vocab_size must be strictly greater than 0, got {vocab_size}")
|
| 28 |
+
if d_model <= 0:
|
| 29 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
|
| 30 |
+
|
| 31 |
+
self.d_model = d_model
|
| 32 |
+
self.vocab_size = vocab_size
|
| 33 |
+
self.fc = nn.Linear(d_model, vocab_size, bias=False)
|
| 34 |
+
|
| 35 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 36 |
+
if not isinstance(x, torch.Tensor):
|
| 37 |
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
| 38 |
+
if x.dim() != 3:
|
| 39 |
+
raise ValueError(
|
| 40 |
+
f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
B, S, D = x.shape
|
| 44 |
+
if D != self.d_model:
|
| 45 |
+
raise ValueError(f"Last dim {D} must match d_model {self.d_model}")
|
| 46 |
+
|
| 47 |
+
return self.fc(x)
|
src/transformer/transformer.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
from transformer import modules
|
| 6 |
+
from transformer.configs import BasicEncDecCfg
|
| 7 |
+
from transformer.utils import create_causal_mask, sample_from_logits, shift_right
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class BasicEncoderDecoderTransformer(nn.Module):
|
| 11 |
+
def __init__(self, cfg: BasicEncDecCfg):
|
| 12 |
+
super().__init__()
|
| 13 |
+
|
| 14 |
+
# Basic type/value guarding for critical cfg fields (mirror project style)
|
| 15 |
+
if not isinstance(cfg, BasicEncDecCfg):
|
| 16 |
+
raise TypeError(f"cfg must be a BasicEncDecCfg, got {type(cfg)}")
|
| 17 |
+
if not isinstance(cfg.vocab_size, int):
|
| 18 |
+
raise TypeError(f"vocab_size must be an int, got {type(cfg.vocab_size)}")
|
| 19 |
+
if not cfg.vocab_size > 0:
|
| 20 |
+
raise ValueError(f"vocab_size must be strictly greater than 0, got {cfg.vocab_size}")
|
| 21 |
+
if not isinstance(cfg.d_model, int):
|
| 22 |
+
raise TypeError(f"d_model must be an int, got {type(cfg.d_model)}")
|
| 23 |
+
if not cfg.d_model > 0:
|
| 24 |
+
raise ValueError(f"d_model must be strictly greater than 0, got {cfg.d_model}")
|
| 25 |
+
if not isinstance(cfg.max_seq_len, int):
|
| 26 |
+
raise TypeError(f"max_seq_len must be an int, got {type(cfg.max_seq_len)}")
|
| 27 |
+
if not cfg.max_seq_len >= 0:
|
| 28 |
+
raise ValueError(
|
| 29 |
+
f"max_seq_len must be greater than or equal to 0, got {cfg.max_seq_len}"
|
| 30 |
+
)
|
| 31 |
+
if not isinstance(cfg.pad_id, int):
|
| 32 |
+
raise TypeError(f"pad_id must be an int, got {type(cfg.pad_id)}")
|
| 33 |
+
if not (0 <= cfg.pad_id < cfg.vocab_size):
|
| 34 |
+
raise ValueError(f"pad_id must be in [0, {cfg.vocab_size - 1}], got {cfg.pad_id}")
|
| 35 |
+
if not isinstance(cfg.bos_id, int):
|
| 36 |
+
raise TypeError(f"bos_id must be an int, got {type(cfg.bos_id)}")
|
| 37 |
+
if not (0 <= cfg.bos_id < cfg.vocab_size):
|
| 38 |
+
raise ValueError(f"bos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.bos_id}")
|
| 39 |
+
if not isinstance(cfg.eos_id, int):
|
| 40 |
+
raise TypeError(f"eos_id must be an int, got {type(cfg.eos_id)}")
|
| 41 |
+
if not (0 <= cfg.eos_id < cfg.vocab_size):
|
| 42 |
+
raise ValueError(f"eos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.eos_id}")
|
| 43 |
+
|
| 44 |
+
self.cfg = cfg
|
| 45 |
+
|
| 46 |
+
self.embed = modules.InputEmbedding(
|
| 47 |
+
cfg.vocab_size, cfg.d_model, cfg.max_seq_len, cfg.pad_id, cfg.dropout_rate
|
| 48 |
+
)
|
| 49 |
+
self.encoder = modules.TransformerEncoder(
|
| 50 |
+
cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
|
| 51 |
+
)
|
| 52 |
+
self.decoder = modules.TransformerDecoder(
|
| 53 |
+
cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
|
| 54 |
+
)
|
| 55 |
+
self.lm_head = modules.LMHead(cfg.d_model, cfg.vocab_size)
|
| 56 |
+
|
| 57 |
+
# tie weights
|
| 58 |
+
self.embed.token_embed.weight = self.lm_head.fc.weight
|
| 59 |
+
|
| 60 |
+
# expose a few attrs for convenience
|
| 61 |
+
self.max_seq_len = cfg.max_seq_len
|
| 62 |
+
self.pad_id = cfg.pad_id
|
| 63 |
+
self.bos_id = cfg.bos_id
|
| 64 |
+
self.eos_id = cfg.eos_id
|
| 65 |
+
|
| 66 |
+
def forward(
|
| 67 |
+
self,
|
| 68 |
+
src_ids: Tensor,
|
| 69 |
+
tgt_ids: Tensor,
|
| 70 |
+
src_padding_mask: Tensor | None,
|
| 71 |
+
tgt_padding_mask: Tensor | None,
|
| 72 |
+
) -> Tensor:
|
| 73 |
+
# Validate inputs (match project style)
|
| 74 |
+
if not isinstance(src_ids, torch.Tensor):
|
| 75 |
+
raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
|
| 76 |
+
if not isinstance(tgt_ids, torch.Tensor):
|
| 77 |
+
raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}")
|
| 78 |
+
if src_ids.dim() != 2:
|
| 79 |
+
raise ValueError(
|
| 80 |
+
f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
|
| 81 |
+
)
|
| 82 |
+
if tgt_ids.dim() != 2:
|
| 83 |
+
raise ValueError(
|
| 84 |
+
f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}"
|
| 85 |
+
)
|
| 86 |
+
if src_ids.dtype != torch.long or tgt_ids.dtype != torch.long:
|
| 87 |
+
raise TypeError("src_ids and tgt_ids must be torch.long (int64)")
|
| 88 |
+
if src_padding_mask is not None and not isinstance(src_padding_mask, torch.Tensor):
|
| 89 |
+
raise TypeError(
|
| 90 |
+
f"src_padding_mask must be a torch.Tensor or None, got {type(src_padding_mask)}"
|
| 91 |
+
)
|
| 92 |
+
if tgt_padding_mask is not None and not isinstance(tgt_padding_mask, torch.Tensor):
|
| 93 |
+
raise TypeError(
|
| 94 |
+
f"tgt_padding_mask must be a torch.Tensor or None, got {type(tgt_padding_mask)}"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
hidden_states = self.encode(src_ids, src_padding_mask)
|
| 98 |
+
# teacher forcing: shift right with BOS, keep PAD for padding
|
| 99 |
+
shifted_tgt = shift_right(tgt_ids, self.bos_id, self.pad_id)
|
| 100 |
+
shifted_tgt_mask = None
|
| 101 |
+
if tgt_padding_mask is not None:
|
| 102 |
+
m = tgt_padding_mask.to(dtype=torch.bool, device=shifted_tgt.device)
|
| 103 |
+
|
| 104 |
+
# Handle common mask shapes by shifting along the last (time) dimension
|
| 105 |
+
if m.dim() == 4: # [B, 1, 1, T]
|
| 106 |
+
pad_col = torch.zeros_like(m[..., :1]) # [B,1,1,1] False
|
| 107 |
+
shifted_tgt_mask = torch.cat([pad_col, m[..., :-1]], dim=-1)
|
| 108 |
+
elif m.dim() == 3: # [B, 1, T] or [B, H, T]
|
| 109 |
+
pad_col = torch.zeros_like(m[..., :1]) # [B,*,1] False
|
| 110 |
+
shifted_tgt_mask = torch.cat([pad_col, m[..., :-1]], dim=-1)
|
| 111 |
+
elif m.dim() == 2: # [B, T]
|
| 112 |
+
pad_col = torch.zeros_like(m[:, :1]) # [B,1] False
|
| 113 |
+
shifted_tgt_mask = torch.cat([pad_col, m[:, :-1]], dim=-1)
|
| 114 |
+
else:
|
| 115 |
+
raise ValueError(f"tgt_padding_mask must have 2, 3, or 4 dims; got {m.dim()}")
|
| 116 |
+
|
| 117 |
+
dec_hidden_states = self.decode(
|
| 118 |
+
hidden_states, shifted_tgt, src_padding_mask, shifted_tgt_mask
|
| 119 |
+
)
|
| 120 |
+
logits = self.lm_head(dec_hidden_states)
|
| 121 |
+
return logits # (B, T_tgt, V)
|
| 122 |
+
|
| 123 |
+
def encode(self, src_ids: Tensor, src_padding_mask: Tensor | None) -> Tensor:
|
| 124 |
+
if not isinstance(src_ids, torch.Tensor):
|
| 125 |
+
raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
|
| 126 |
+
if src_ids.dim() != 2:
|
| 127 |
+
raise ValueError(
|
| 128 |
+
f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
|
| 129 |
+
)
|
| 130 |
+
if src_ids.dtype != torch.long:
|
| 131 |
+
raise TypeError("src_ids must be torch.long (int64)")
|
| 132 |
+
if src_padding_mask is not None and not isinstance(src_padding_mask, torch.Tensor):
|
| 133 |
+
raise TypeError(
|
| 134 |
+
f"src_padding_mask must be a torch.Tensor or None, got {type(src_padding_mask)}"
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
x = self.embed(src_ids) # (B, Sx, D)
|
| 138 |
+
hidden_states = self.encoder(x, src_padding_mask)
|
| 139 |
+
return hidden_states # (B, Sx, D)
|
| 140 |
+
|
| 141 |
+
def decode(
|
| 142 |
+
self,
|
| 143 |
+
hidden_states: Tensor,
|
| 144 |
+
tgt_ids: Tensor,
|
| 145 |
+
src_padding_mask: Tensor | None,
|
| 146 |
+
tgt_padding_mask: Tensor | None = None,
|
| 147 |
+
) -> Tensor:
|
| 148 |
+
if not isinstance(hidden_states, torch.Tensor):
|
| 149 |
+
raise TypeError(f"hidden_states must be a torch.Tensor, got {type(hidden_states)}")
|
| 150 |
+
if hidden_states.dim() != 3:
|
| 151 |
+
raise ValueError(
|
| 152 |
+
f"hidden_states must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(hidden_states.shape)}"
|
| 153 |
+
)
|
| 154 |
+
if not isinstance(tgt_ids, torch.Tensor):
|
| 155 |
+
raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}")
|
| 156 |
+
if tgt_ids.dim() != 2:
|
| 157 |
+
raise ValueError(
|
| 158 |
+
f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}"
|
| 159 |
+
)
|
| 160 |
+
if tgt_ids.dtype != torch.long:
|
| 161 |
+
raise TypeError("tgt_ids must be torch.long (int64)")
|
| 162 |
+
if src_padding_mask is not None and not isinstance(src_padding_mask, torch.Tensor):
|
| 163 |
+
raise TypeError(
|
| 164 |
+
f"src_padding_mask must be a torch.Tensor or None, got {type(src_padding_mask)}"
|
| 165 |
+
)
|
| 166 |
+
if tgt_padding_mask is not None and not isinstance(tgt_padding_mask, torch.Tensor):
|
| 167 |
+
raise TypeError(
|
| 168 |
+
f"tgt_padding_mask must be a torch.Tensor or None, got {type(tgt_padding_mask)}"
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# causal mask per sequence length (utils returns broadcastable boolean mask)
|
| 172 |
+
tgt_causal_mask = create_causal_mask(tgt_ids.size(1)).to(tgt_ids.device)
|
| 173 |
+
y = self.embed(tgt_ids) # (B, Sy, D)
|
| 174 |
+
out = self.decoder(hidden_states, y, src_padding_mask, tgt_padding_mask, tgt_causal_mask)
|
| 175 |
+
return out # (B, Sy, D)
|
| 176 |
+
|
| 177 |
+
def generate(
|
| 178 |
+
self,
|
| 179 |
+
src_ids: Tensor,
|
| 180 |
+
src_padding_mask: Tensor | None,
|
| 181 |
+
max_new_tokens: int = 20,
|
| 182 |
+
temperature: float = 1.0,
|
| 183 |
+
top_k: int | None = None,
|
| 184 |
+
top_p: float | None = None,
|
| 185 |
+
do_sample: bool = False,
|
| 186 |
+
) -> Tensor:
|
| 187 |
+
# validate args
|
| 188 |
+
if not isinstance(src_ids, torch.Tensor):
|
| 189 |
+
raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
|
| 190 |
+
if src_ids.dim() != 2:
|
| 191 |
+
raise ValueError(
|
| 192 |
+
f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
|
| 193 |
+
)
|
| 194 |
+
if src_ids.dtype != torch.long:
|
| 195 |
+
raise TypeError("src_ids must be torch.long (int64)")
|
| 196 |
+
if src_padding_mask is not None and not isinstance(src_padding_mask, torch.Tensor):
|
| 197 |
+
raise TypeError(
|
| 198 |
+
f"src_padding_mask must be a torch.Tensor or None, got {type(src_padding_mask)}"
|
| 199 |
+
)
|
| 200 |
+
if not isinstance(max_new_tokens, int):
|
| 201 |
+
raise TypeError(f"max_new_tokens must be an int, got {type(max_new_tokens)}")
|
| 202 |
+
if max_new_tokens < 0:
|
| 203 |
+
raise ValueError(
|
| 204 |
+
f"max_new_tokens must be greater than or equal to 0, got {max_new_tokens}"
|
| 205 |
+
)
|
| 206 |
+
if not isinstance(temperature, float):
|
| 207 |
+
raise TypeError(f"temperature must be a float, got {type(temperature)}")
|
| 208 |
+
if not (temperature > 0.0):
|
| 209 |
+
raise ValueError(f"temperature must be strictly greater than 0, got {temperature}")
|
| 210 |
+
if top_k is not None and (not isinstance(top_k, int)):
|
| 211 |
+
raise TypeError(f"top_k must be an int or None, got {type(top_k)}")
|
| 212 |
+
if top_k is not None and top_k < 0:
|
| 213 |
+
raise ValueError(f"top_k must be strictly greater than 0, got {top_k}")
|
| 214 |
+
if top_p is not None and (not isinstance(top_p, float)):
|
| 215 |
+
raise TypeError(f"top_p must be a float or None, got {type(top_p)}")
|
| 216 |
+
if top_p is not None and not (0.0 < top_p <= 1.0):
|
| 217 |
+
raise ValueError(f"top_p must be in (0, 1], got {top_p}")
|
| 218 |
+
if not isinstance(do_sample, bool):
|
| 219 |
+
raise TypeError(f"do_sample must be a bool, got {type(do_sample)}")
|
| 220 |
+
self.eval()
|
| 221 |
+
with torch.no_grad():
|
| 222 |
+
batch_size, _ = src_ids.shape
|
| 223 |
+
device = src_ids.device
|
| 224 |
+
tgt_ids = torch.full((batch_size, 1), self.bos_id, device=device, dtype=torch.long)
|
| 225 |
+
finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
|
| 226 |
+
hidden_states = self.encode(src_ids, src_padding_mask)
|
| 227 |
+
for _ in range(max_new_tokens):
|
| 228 |
+
# decode on current tgt_ids; take last-step logits
|
| 229 |
+
step_hidden = self.decode(hidden_states, tgt_ids, src_padding_mask) # (B, T, D)
|
| 230 |
+
logits = self.lm_head(step_hidden)[:, -1, :] # (B, V)
|
| 231 |
+
next_ids = sample_from_logits(
|
| 232 |
+
logits, temperature=temperature, top_k=top_k, top_p=top_p, do_sample=do_sample
|
| 233 |
+
) # (B,)
|
| 234 |
+
next_ids = torch.where(finished, torch.full_like(next_ids, self.eos_id), next_ids)
|
| 235 |
+
tgt_ids = torch.cat([tgt_ids, next_ids.unsqueeze(1)], dim=-1)
|
| 236 |
+
finished |= next_ids == self.eos_id
|
| 237 |
+
if finished.all():
|
| 238 |
+
return tgt_ids
|
| 239 |
+
return tgt_ids
|
src/transformer/utils.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from collections.abc import Iterable
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def split_heads(x: torch.Tensor, num_heads: int) -> torch.Tensor:
|
| 11 |
+
"""
|
| 12 |
+
Split the last (model) dimension of a 3D torch.Tensor into (num_heads, d_head) and permute to
|
| 13 |
+
(batch_size, num_heads, seq_length, d_head).
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
x (torch.Tensor): Input torch.Tensor of shape (batch_size, seq_length, d_model).
|
| 17 |
+
num_heads (int): Number of attention heads to split into. Must be a positive integer
|
| 18 |
+
that divides d_model exactly.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
torch.Tensor: torch.Tensor of shape (batch_size, num_heads, seq_length, d_head), where
|
| 22 |
+
d_head = d_model // num_heads. The output torch.Tensor retains the same dtype and
|
| 23 |
+
device as the input.
|
| 24 |
+
|
| 25 |
+
Invariants:
|
| 26 |
+
- Output device == x.device
|
| 27 |
+
- Output dtype == x.dtype
|
| 28 |
+
- batch_size and seq_length are preserved from the input shape.
|
| 29 |
+
|
| 30 |
+
Notes:
|
| 31 |
+
- Zero-length sequences (seq_length == 0) are supported and will return a torch.Tensor
|
| 32 |
+
with shape (batch_size, num_heads, 0, d_head).
|
| 33 |
+
|
| 34 |
+
Raises:
|
| 35 |
+
TypeError: If x is not a torch.Tensor or num_heads is not an int.
|
| 36 |
+
ValueError: If x is not 3D, if num_heads <= 0, or if d_model is not divisible by num_heads.
|
| 37 |
+
"""
|
| 38 |
+
# Type checks
|
| 39 |
+
if not isinstance(x, torch.Tensor):
|
| 40 |
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
| 41 |
+
if not isinstance(num_heads, int):
|
| 42 |
+
raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
|
| 43 |
+
|
| 44 |
+
# Shape checks
|
| 45 |
+
if x.ndim != 3:
|
| 46 |
+
raise ValueError(
|
| 47 |
+
f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
|
| 48 |
+
)
|
| 49 |
+
if num_heads <= 0:
|
| 50 |
+
raise ValueError(f"num_heads must be > 0; got {num_heads}")
|
| 51 |
+
|
| 52 |
+
batch_size, seq_length, d_model = x.shape
|
| 53 |
+
|
| 54 |
+
# Divisibility check
|
| 55 |
+
if d_model % num_heads != 0:
|
| 56 |
+
raise ValueError(
|
| 57 |
+
f"d_model ({d_model}) must be divisible by num_heads ({num_heads}); "
|
| 58 |
+
f"got remainder {d_model % num_heads}"
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
d_head = d_model // num_heads
|
| 62 |
+
|
| 63 |
+
# Reshape and permute: (B, S, D) -> (B, S, H, Dh) -> (B, H, S, Dh)
|
| 64 |
+
x = x.reshape(batch_size, seq_length, num_heads, d_head)
|
| 65 |
+
x = x.permute(0, 2, 1, 3)
|
| 66 |
+
|
| 67 |
+
return x
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def calculate_attention(
|
| 71 |
+
query: torch.Tensor,
|
| 72 |
+
key: torch.Tensor,
|
| 73 |
+
value: torch.Tensor,
|
| 74 |
+
mask: torch.Tensor | None,
|
| 75 |
+
*,
|
| 76 |
+
deterministic: bool = False,
|
| 77 |
+
return_probs: bool = False,
|
| 78 |
+
attn_dropout_p: float = 0.0,
|
| 79 |
+
):
|
| 80 |
+
"""
|
| 81 |
+
Scaled dot-product attention with:
|
| 82 |
+
• device-safe masking (mask auto-moved to query.device),
|
| 83 |
+
• explicit broadcastability checks for mask,
|
| 84 |
+
• numerically stable softmax (row-wise max subtraction),
|
| 85 |
+
• fully-masked rows -> zero probabilities and zero outputs,
|
| 86 |
+
• fp16/bf16-safe compute via fp32 upcast, output downcast.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
query: (B, H, S_q, D)
|
| 90 |
+
key: (B, H, S_kv, D)
|
| 91 |
+
value: (B, H, S_kv, D)
|
| 92 |
+
mask: broadcastable to (B, H, S_q, S_kv)
|
| 93 |
+
- bool: True = masked/ignored position
|
| 94 |
+
- float: additive bias (e.g., large negative for masked, ALiBi, etc.)
|
| 95 |
+
deterministic: best-effort reproducibility toggle
|
| 96 |
+
return_probs: if False, only return attention output
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
attention: (B, H, S_q, D)
|
| 100 |
+
probs (optional): (B, H, S_q, S_kv)
|
| 101 |
+
"""
|
| 102 |
+
# ---- Basic shape checks
|
| 103 |
+
if query.dim() != 4 or key.dim() != 4 or value.dim() != 4:
|
| 104 |
+
raise ValueError(
|
| 105 |
+
f"q/k/v must be 4D (B,H,S,D). Got "
|
| 106 |
+
f"q={tuple(query.shape)}, k={tuple(key.shape)}, v={tuple(value.shape)}"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
Bq, Hq, Sq, Dq = query.shape
|
| 110 |
+
Bk, Hk, Sk, Dk = key.shape
|
| 111 |
+
Bv, Hv, Sv, Dv = value.shape
|
| 112 |
+
|
| 113 |
+
if not (Bq == Bk == Bv):
|
| 114 |
+
raise ValueError(f"Batch mismatch: q={Bq}, k={Bk}, v={Bv}")
|
| 115 |
+
if not (Hq == Hk == Hv):
|
| 116 |
+
raise ValueError(f"Heads mismatch: q={Hq}, k={Hk}, v={Hv}")
|
| 117 |
+
if Sk != Sv:
|
| 118 |
+
raise ValueError(f"Key/Value seq length mismatch: Sk={Sk} vs Sv={Sv}")
|
| 119 |
+
if not (Dq == Dk == Dv):
|
| 120 |
+
raise ValueError(f"Head dimension mismatch: Dq={Dq}, Dk={Dk}, Dv={Dv}")
|
| 121 |
+
|
| 122 |
+
target_shape = (Bq, Hq, Sq, Sk)
|
| 123 |
+
|
| 124 |
+
# ---- Device checks: q/k/v must be on the same device (fail fast); mask will be auto-moved
|
| 125 |
+
if not (query.device == key.device == value.device):
|
| 126 |
+
raise RuntimeError(
|
| 127 |
+
f"q/k/v must be on the same device, got "
|
| 128 |
+
f"query={query.device}, key={key.device}, value={value.device}"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# ---- Early exit for empty sequences
|
| 132 |
+
if Sq == 0:
|
| 133 |
+
empty_attn = query.new_zeros(Bq, Hq, 0, Dq)
|
| 134 |
+
empty_probs = query.new_zeros(Bq, Hq, 0, Sk)
|
| 135 |
+
return (empty_attn, empty_probs) if return_probs else empty_attn
|
| 136 |
+
if Sk == 0:
|
| 137 |
+
zero_attn = query.new_zeros(Bq, Hq, Sq, Dq)
|
| 138 |
+
zero_probs = query.new_zeros(Bq, Hq, Sq, 0)
|
| 139 |
+
return (zero_attn, zero_probs) if return_probs else zero_attn
|
| 140 |
+
|
| 141 |
+
# ---- Compute dtype policy: upcast to fp32 for stability if needed
|
| 142 |
+
input_dtype = query.dtype
|
| 143 |
+
compute_dtype = torch.float32 if input_dtype in (torch.float16, torch.bfloat16) else input_dtype
|
| 144 |
+
q = query.to(compute_dtype)
|
| 145 |
+
k = key.to(compute_dtype)
|
| 146 |
+
v = value.to(compute_dtype)
|
| 147 |
+
|
| 148 |
+
# ---- Determinism (best effort)
|
| 149 |
+
if deterministic:
|
| 150 |
+
try:
|
| 151 |
+
torch.use_deterministic_algorithms(True)
|
| 152 |
+
except Exception:
|
| 153 |
+
pass
|
| 154 |
+
|
| 155 |
+
# ---- Scores
|
| 156 |
+
d_head = Dq
|
| 157 |
+
scores = torch.matmul(q, k.transpose(-1, -2)) / (d_head**0.5) # (B,H,Sq,Sk)
|
| 158 |
+
|
| 159 |
+
# ---- Mask handling (device + broadcastability + semantics)
|
| 160 |
+
full_row_mask = None # (B,H,Sq,1) True where the entire key row is invalid
|
| 161 |
+
if mask is not None:
|
| 162 |
+
# Move mask to the same device
|
| 163 |
+
if mask.device != q.device:
|
| 164 |
+
mask = mask.to(q.device)
|
| 165 |
+
|
| 166 |
+
# Check broadcastability to target shape, then expand (without copy)
|
| 167 |
+
def _expand_or_error(x: torch.Tensor, name: str) -> torch.Tensor:
|
| 168 |
+
# Try expanding to target_shape; torch.Tensor.expand raises if not broadcastable
|
| 169 |
+
try:
|
| 170 |
+
return x.expand(target_shape)
|
| 171 |
+
except RuntimeError as e:
|
| 172 |
+
raise ValueError(
|
| 173 |
+
f"{name} with shape {tuple(x.shape)} is not broadcastable to {target_shape}. "
|
| 174 |
+
f"Broadcasting rules require dimensions to be equal or 1 in the source."
|
| 175 |
+
) from e
|
| 176 |
+
|
| 177 |
+
if mask.dtype == torch.bool:
|
| 178 |
+
mask = _expand_or_error(mask, "Boolean mask")
|
| 179 |
+
# Use a large finite negative to avoid (-inf) - (-inf) during stabilization
|
| 180 |
+
neg_large = torch.finfo(compute_dtype).min / 4
|
| 181 |
+
scores = scores.masked_fill(mask, neg_large)
|
| 182 |
+
full_row_mask = mask.all(dim=-1, keepdim=True) # (B,H,Sq,1)
|
| 183 |
+
else:
|
| 184 |
+
# Additive bias mask
|
| 185 |
+
mask = _expand_or_error(mask.to(compute_dtype), "Additive mask")
|
| 186 |
+
scores = scores + mask
|
| 187 |
+
# If caller used -inf as additive, detect fully-masked rows after addition
|
| 188 |
+
full_row_mask = torch.isneginf(scores).all(dim=-1, keepdim=True)
|
| 189 |
+
|
| 190 |
+
# ---- Numerically stable softmax: subtract row-wise max
|
| 191 |
+
row_max = torch.amax(scores, dim=-1, keepdim=True)
|
| 192 |
+
if full_row_mask is None:
|
| 193 |
+
# If any rows are -inf (rare with finite negatives), neutralize their max to 0
|
| 194 |
+
row_max = torch.where(torch.isneginf(row_max), torch.zeros_like(row_max), row_max)
|
| 195 |
+
else:
|
| 196 |
+
# For rows marked fully masked, set max to 0 to avoid -inf - (-inf)
|
| 197 |
+
row_max = torch.where(full_row_mask, torch.zeros_like(row_max), row_max)
|
| 198 |
+
|
| 199 |
+
stable_scores = scores - row_max
|
| 200 |
+
probs = F.softmax(stable_scores, dim=-1)
|
| 201 |
+
|
| 202 |
+
# ---- Zero-out fully masked rows (probabilities and thus outputs)
|
| 203 |
+
if full_row_mask is not None:
|
| 204 |
+
probs = probs * (~full_row_mask).to(probs.dtype)
|
| 205 |
+
if attn_dropout_p and attn_dropout_p > 0.0:
|
| 206 |
+
probs = F.dropout(probs, p=attn_dropout_p, training=True)
|
| 207 |
+
# ---- Weighted sum with values; cast back to input dtype
|
| 208 |
+
attention = torch.matmul(probs, v).to(input_dtype)
|
| 209 |
+
|
| 210 |
+
if return_probs:
|
| 211 |
+
out_probs_dtype = (
|
| 212 |
+
input_dtype if input_dtype in (torch.float16, torch.bfloat16) else probs.dtype
|
| 213 |
+
)
|
| 214 |
+
return attention, probs.to(out_probs_dtype)
|
| 215 |
+
else:
|
| 216 |
+
return attention
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def join_heads(x: torch.Tensor) -> torch.Tensor:
|
| 220 |
+
"""
|
| 221 |
+
Merge multi-head attention outputs into model dimension.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
x (torch.Tensor): shape (batch_size, num_heads, seq_length, d_head)
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
torch.Tensor: shape (batch_size, seq_length, d_model) where d_model = num_heads * d_head
|
| 228 |
+
"""
|
| 229 |
+
# --- Type checks ---
|
| 230 |
+
if not isinstance(x, torch.Tensor):
|
| 231 |
+
raise TypeError(f"Expected torch.Tensor, got {type(x)}")
|
| 232 |
+
|
| 233 |
+
if x.ndim != 4:
|
| 234 |
+
raise ValueError(
|
| 235 |
+
f"Expected 4D torch.Tensor (batch, num_heads, seq_len, d_head), "
|
| 236 |
+
f"got shape {tuple(x.shape)} with ndim={x.ndim}"
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
batch_size, num_heads, seq_length, d_head = x.shape
|
| 240 |
+
|
| 241 |
+
if not all(
|
| 242 |
+
isinstance(dim, int) and dim >= 0 for dim in (batch_size, num_heads, seq_length, d_head)
|
| 243 |
+
):
|
| 244 |
+
raise ValueError(f"Invalid shape values: {x.shape}")
|
| 245 |
+
|
| 246 |
+
d_model = num_heads * d_head
|
| 247 |
+
|
| 248 |
+
# --- Safe reshape ---
|
| 249 |
+
x = x.permute(0, 2, 1, 3).contiguous().view(batch_size, seq_length, d_model)
|
| 250 |
+
|
| 251 |
+
return x
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def sinusoidal_positional_encoding(
|
| 255 |
+
seq_len: int,
|
| 256 |
+
dim: int,
|
| 257 |
+
*,
|
| 258 |
+
base: float = 10_000.0,
|
| 259 |
+
dtype: torch.dtype | None = None,
|
| 260 |
+
device: torch.device | None = None,
|
| 261 |
+
offset: int = 0,
|
| 262 |
+
return_positions: bool = False,
|
| 263 |
+
) -> torch.Tensor:
|
| 264 |
+
"""
|
| 265 |
+
Build a [seq_len, dim] sinusoidal (trig) positional encoding table:
|
| 266 |
+
PE[pos, 2i] = sin(pos / (base^(2i/dim)))
|
| 267 |
+
PE[pos, 2i+1] = cos(pos / (base^(2i/dim)))
|
| 268 |
+
|
| 269 |
+
Design goals:
|
| 270 |
+
- Numerically stable: uses exp with -log(base) to avoid huge powers.
|
| 271 |
+
- Dtype/device safe: computes frequencies in float32 and casts at the end;
|
| 272 |
+
supports fp16/bf16 by upcasting, then downcasting.
|
| 273 |
+
- Odd dimensions handled: the last column is zero-padded when dim is odd.
|
| 274 |
+
- Supports an integer `offset` so you can continue encodings for cached KV.
|
| 275 |
+
- No autograd needed: returns a tensor with requires_grad=False.
|
| 276 |
+
|
| 277 |
+
Args:
|
| 278 |
+
seq_len: length of sequence (>=0).
|
| 279 |
+
dim: embedding dimension (>=1).
|
| 280 |
+
base: frequency base (default 10k like Vaswani et al.).
|
| 281 |
+
dtype: desired dtype of the returned table (defaults to torch.get_default_dtype()).
|
| 282 |
+
device: desired device of the returned table.
|
| 283 |
+
offset: position offset to start from (useful for incremental decoding).
|
| 284 |
+
return_positions: if True, also returns the [seq_len] position indices.
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
pe: [seq_len, dim] tensor on `device` with `dtype`.
|
| 288 |
+
(pos): optional [seq_len] tensor of positions (if return_positions=True).
|
| 289 |
+
|
| 290 |
+
Raises:
|
| 291 |
+
ValueError if seq_len<0 or dim<=0.
|
| 292 |
+
"""
|
| 293 |
+
if seq_len < 0:
|
| 294 |
+
raise ValueError(f"seq_len must be >= 0, got {seq_len}")
|
| 295 |
+
if dim <= 0:
|
| 296 |
+
raise ValueError(f"dim must be > 0, got {dim}")
|
| 297 |
+
if dtype is None:
|
| 298 |
+
dtype = torch.get_default_dtype()
|
| 299 |
+
|
| 300 |
+
# Work in float32 for stability regardless of target dtype.
|
| 301 |
+
work_dtype = torch.float32
|
| 302 |
+
|
| 303 |
+
# Positions [offset .. offset+seq_len-1]
|
| 304 |
+
# Note: torch.arange is dtype-agnostic; compute in float32 later.
|
| 305 |
+
pos = torch.arange(offset, offset + seq_len, device=device)
|
| 306 |
+
|
| 307 |
+
# Frequencies for even indices: exp(-(log(base) * (2i)/dim))
|
| 308 |
+
# Avoid base**(2i/dim) directly to reduce overflow/underflow risk.
|
| 309 |
+
half_dim = dim // 2 # number of sin/cos pairs
|
| 310 |
+
if half_dim > 0:
|
| 311 |
+
# [half_dim]
|
| 312 |
+
exponent = torch.arange(0, half_dim, device=device, dtype=work_dtype)
|
| 313 |
+
inv_freq = torch.exp(-math.log(base) * (2.0 * exponent) / float(dim)) # [half_dim]
|
| 314 |
+
|
| 315 |
+
# Outer product: [seq_len, half_dim]
|
| 316 |
+
# Compute phase = pos[:, None] * inv_freq[None, :]
|
| 317 |
+
phase = pos.to(work_dtype).unsqueeze(1) * inv_freq.unsqueeze(0)
|
| 318 |
+
|
| 319 |
+
sin_part = torch.sin(phase)
|
| 320 |
+
cos_part = torch.cos(phase)
|
| 321 |
+
|
| 322 |
+
# Interleave sin and cos along the last dim
|
| 323 |
+
pe_even_odd = torch.stack((sin_part, cos_part), dim=-1).reshape(seq_len, 2 * half_dim)
|
| 324 |
+
if dim % 2 == 1:
|
| 325 |
+
# Pad the last column with zeros if dim is odd
|
| 326 |
+
pad = torch.zeros(seq_len, 1, device=device, dtype=work_dtype)
|
| 327 |
+
pe_work = torch.cat([pe_even_odd, pad], dim=1)
|
| 328 |
+
else:
|
| 329 |
+
pe_work = pe_even_odd
|
| 330 |
+
else:
|
| 331 |
+
# dim == 1 case → just a zero column (consistent with odd-dim padding above)
|
| 332 |
+
pe_work = torch.zeros(seq_len, 1, device=device, dtype=work_dtype)
|
| 333 |
+
|
| 334 |
+
# If somehow dim==0 was allowed earlier, we'd have raised.
|
| 335 |
+
|
| 336 |
+
# Cast once at the end to the requested dtype.
|
| 337 |
+
pe = pe_work.to(dtype=dtype)
|
| 338 |
+
|
| 339 |
+
# Ensure no gradient tracking (users usually register this as a buffer).
|
| 340 |
+
pe.requires_grad_(False)
|
| 341 |
+
|
| 342 |
+
return (pe, pos.to(device)) if return_positions else pe
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def combine_masks(mask1, mask2):
|
| 346 |
+
"""
|
| 347 |
+
Combine two boolean masks with logical OR.
|
| 348 |
+
|
| 349 |
+
Args:
|
| 350 |
+
mask1 (torch.Tensor or None): first mask
|
| 351 |
+
mask2 (torch.Tensor or None): second mask
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
torch.Tensor or None: combined mask or None if both are None
|
| 355 |
+
|
| 356 |
+
Notes:
|
| 357 |
+
- Both masks must be broadcastable to the same shape.
|
| 358 |
+
- Returns a boolean tensor if inputs are boolean.
|
| 359 |
+
- If one mask is None, returns the other unchanged.
|
| 360 |
+
"""
|
| 361 |
+
if mask1 is not None and mask2 is not None:
|
| 362 |
+
m1 = mask1.to(dtype=torch.bool)
|
| 363 |
+
m2 = mask2.to(dtype=torch.bool, device=m1.device)
|
| 364 |
+
if m1.shape != m2.shape:
|
| 365 |
+
try:
|
| 366 |
+
return m1 | m2 # allow broadcasting, but safe
|
| 367 |
+
except RuntimeError as e:
|
| 368 |
+
raise ValueError(
|
| 369 |
+
f"combine_masks: masks not broadcastable: {m1.shape} vs {m2.shape}"
|
| 370 |
+
) from e
|
| 371 |
+
return m1 | m2
|
| 372 |
+
elif mask1 is not None:
|
| 373 |
+
return mask1
|
| 374 |
+
elif mask2 is not None:
|
| 375 |
+
return mask2
|
| 376 |
+
else:
|
| 377 |
+
return None
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def shift_right(labels: torch.Tensor, bos_id: int = 1, pad_id: int = 0) -> torch.Tensor:
|
| 381 |
+
"""
|
| 382 |
+
Build decoder input IDs by shifting target labels to the right and
|
| 383 |
+
prepending a BOS token. Any labels marked -100 (ignore_index) are
|
| 384 |
+
converted to PAD *after* shifting so they never appear as inputs.
|
| 385 |
+
|
| 386 |
+
Args:
|
| 387 |
+
labels: Long tensor of shape [B, T] or [T].
|
| 388 |
+
bos_id: Token id to place at position 0.
|
| 389 |
+
pad_id: Token id used where labels == -100 after shifting.
|
| 390 |
+
|
| 391 |
+
Returns:
|
| 392 |
+
decoder_input_ids: Long tensor of shape [B, T] (same device as labels).
|
| 393 |
+
|
| 394 |
+
"""
|
| 395 |
+
if labels.dim() == 1:
|
| 396 |
+
labels = labels.unsqueeze(0) # [1, T]
|
| 397 |
+
if labels.dim() != 2:
|
| 398 |
+
raise ValueError(f"shift_right expects 1D or 2D tensor, got shape {tuple(labels.shape)}")
|
| 399 |
+
|
| 400 |
+
B, T = labels.shape
|
| 401 |
+
if T == 0:
|
| 402 |
+
raise ValueError("shift_right: sequence length must be > 0")
|
| 403 |
+
|
| 404 |
+
# Work in long dtype on the same device; do NOT modify `labels` in-place.
|
| 405 |
+
labels = labels.to(torch.long)
|
| 406 |
+
|
| 407 |
+
# Start with PADs everywhere, then copy shifted labels, and set BOS.
|
| 408 |
+
decoder_input_ids = labels.new_full((B, T), pad_id)
|
| 409 |
+
decoder_input_ids[:, 1:] = labels[:, :-1]
|
| 410 |
+
decoder_input_ids[:, 0] = bos_id
|
| 411 |
+
|
| 412 |
+
# Any positions that inherited -100 from labels should be PAD instead.
|
| 413 |
+
# (labels contains -100 for "ignore_index" targets; these should never be inputs)
|
| 414 |
+
mask_ignore = decoder_input_ids.eq(-100)
|
| 415 |
+
if mask_ignore.any():
|
| 416 |
+
decoder_input_ids.masked_fill_(mask_ignore, pad_id)
|
| 417 |
+
|
| 418 |
+
return decoder_input_ids
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def _ensure_2d_logits(logits: torch.Tensor) -> torch.Tensor:
|
| 422 |
+
"""
|
| 423 |
+
Accepts [B, V] or [..., V]. If rank > 2, flattens leading dims into batch.
|
| 424 |
+
Returns 2D [B', V] along with a flag and original shape for potential future use.
|
| 425 |
+
"""
|
| 426 |
+
if logits.dim() == 2:
|
| 427 |
+
return logits
|
| 428 |
+
if logits.dim() >= 3:
|
| 429 |
+
V = logits.size(-1)
|
| 430 |
+
return logits.reshape(-1, V)
|
| 431 |
+
raise ValueError(
|
| 432 |
+
f"sample_from_logits: logits must be at least 2D, got shape {tuple(logits.shape)}"
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def _apply_allow_deny_mask(
|
| 437 |
+
logits: torch.Tensor,
|
| 438 |
+
*,
|
| 439 |
+
allowed_tokens: Iterable[int] | None,
|
| 440 |
+
disallowed_tokens: Iterable[int] | None,
|
| 441 |
+
filter_value: float,
|
| 442 |
+
) -> torch.Tensor:
|
| 443 |
+
if allowed_tokens is not None:
|
| 444 |
+
mask = torch.zeros_like(logits, dtype=torch.bool)
|
| 445 |
+
idx = torch.tensor(list(allowed_tokens), device=logits.device)
|
| 446 |
+
idx = idx[(idx >= 0) & (idx < logits.size(-1))]
|
| 447 |
+
if idx.numel() > 0:
|
| 448 |
+
mask.index_fill_(-1, idx, True)
|
| 449 |
+
logits = torch.where(mask, logits, torch.full_like(logits, filter_value))
|
| 450 |
+
if disallowed_tokens is not None:
|
| 451 |
+
idx = torch.tensor(list(disallowed_tokens), device=logits.device)
|
| 452 |
+
idx = idx[(idx >= 0) & (idx < logits.size(-1))]
|
| 453 |
+
if idx.numel() > 0:
|
| 454 |
+
logits.index_fill_(-1, idx, filter_value)
|
| 455 |
+
return logits
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def _top_k_filtering(
|
| 459 |
+
logits: torch.Tensor,
|
| 460 |
+
top_k: int | None,
|
| 461 |
+
*,
|
| 462 |
+
min_tokens_to_keep: int,
|
| 463 |
+
filter_value: float,
|
| 464 |
+
) -> torch.Tensor:
|
| 465 |
+
if top_k is None or top_k <= 0:
|
| 466 |
+
return logits
|
| 467 |
+
k = min(max(top_k, min_tokens_to_keep), logits.size(-1))
|
| 468 |
+
# threshold per row
|
| 469 |
+
values, _ = torch.topk(logits, k, dim=-1)
|
| 470 |
+
thresh = values[..., -1, None]
|
| 471 |
+
return torch.where(logits < thresh, torch.full_like(logits, filter_value), logits)
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def _top_p_filtering(
|
| 475 |
+
logits_scaled: torch.Tensor,
|
| 476 |
+
probs: torch.Tensor,
|
| 477 |
+
top_p: float,
|
| 478 |
+
*,
|
| 479 |
+
min_tokens_to_keep: int,
|
| 480 |
+
filter_value: float,
|
| 481 |
+
) -> torch.Tensor:
|
| 482 |
+
if top_p is None or not (0.0 < top_p < 1.0):
|
| 483 |
+
return logits_scaled
|
| 484 |
+
# sort by probability
|
| 485 |
+
sorted_probs, sorted_idx = torch.sort(probs, dim=-1, descending=True)
|
| 486 |
+
cum = torch.cumsum(sorted_probs, dim=-1)
|
| 487 |
+
# tokens to remove: everything after first point where cum > top_p
|
| 488 |
+
to_remove = cum > top_p
|
| 489 |
+
# always keep at least min_tokens_to_keep highest-prob tokens
|
| 490 |
+
to_remove[..., :min_tokens_to_keep] = False
|
| 491 |
+
# scatter back to original indices
|
| 492 |
+
scatter_mask = torch.zeros_like(to_remove, dtype=torch.bool).scatter(-1, sorted_idx, to_remove)
|
| 493 |
+
return torch.where(scatter_mask, torch.full_like(logits_scaled, filter_value), logits_scaled)
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def sample_from_logits(
|
| 497 |
+
logits: torch.Tensor,
|
| 498 |
+
*,
|
| 499 |
+
do_sample: bool = False,
|
| 500 |
+
temperature: float = 1.0,
|
| 501 |
+
top_k: int | None = None,
|
| 502 |
+
top_p: float | None = None,
|
| 503 |
+
min_tokens_to_keep: int = 1,
|
| 504 |
+
allowed_tokens: Iterable[int] | None = None,
|
| 505 |
+
disallowed_tokens: Iterable[int] | None = None,
|
| 506 |
+
filter_value: float = -float("inf"),
|
| 507 |
+
rng: torch.Generator | None = None,
|
| 508 |
+
return_probs: bool = False,
|
| 509 |
+
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
| 510 |
+
"""
|
| 511 |
+
Select token IDs from logits with optional sampling & filtering.
|
| 512 |
+
|
| 513 |
+
Inputs:
|
| 514 |
+
logits: [..., V] or [B, V]; will be treated as 2D [B', V] internally.
|
| 515 |
+
do_sample: False -> greedy argmax; True -> multinomial sampling.
|
| 516 |
+
temperature: >0; values <1 sharpen, >1 flatten.
|
| 517 |
+
top_k: keep only top-k logits (after temperature); None/<=0 disables.
|
| 518 |
+
top_p: nucleus (0<p<1): keep smallest set with prob mass >= p (after temperature).
|
| 519 |
+
min_tokens_to_keep: safety floor so we never filter out *everything*.
|
| 520 |
+
allowed_tokens: iterable of token IDs to allow (others masked out).
|
| 521 |
+
disallowed_tokens: iterable of token IDs to ban (masked out).
|
| 522 |
+
filter_value: value used for filtered logits (-inf by default).
|
| 523 |
+
rng: optional torch.Generator for reproducible sampling.
|
| 524 |
+
return_probs: if True, also returns the final probabilities used.
|
| 525 |
+
|
| 526 |
+
Returns:
|
| 527 |
+
token_ids: [B'] int64 (and optionally probs [B', V] if return_probs=True).
|
| 528 |
+
"""
|
| 529 |
+
if temperature <= 0:
|
| 530 |
+
raise ValueError("temperature must be > 0")
|
| 531 |
+
|
| 532 |
+
# Work on a stable float32 copy; keep device and shape
|
| 533 |
+
logits2d = _ensure_2d_logits(logits).to(dtype=torch.float32)
|
| 534 |
+
|
| 535 |
+
# Apply allow/deny lists first (hard mask)
|
| 536 |
+
logits2d = _apply_allow_deny_mask(
|
| 537 |
+
logits2d,
|
| 538 |
+
allowed_tokens=allowed_tokens,
|
| 539 |
+
disallowed_tokens=disallowed_tokens,
|
| 540 |
+
filter_value=filter_value,
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
# Scale by temperature before top-k/top-p
|
| 544 |
+
logits_scaled = logits2d / float(temperature)
|
| 545 |
+
|
| 546 |
+
# Top-k first (logit space)
|
| 547 |
+
logits_scaled = _top_k_filtering(
|
| 548 |
+
logits_scaled, top_k=top_k, min_tokens_to_keep=min_tokens_to_keep, filter_value=filter_value
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
# Compute probs (needed for top-p and sampling)
|
| 552 |
+
probs = F.softmax(logits_scaled, dim=-1)
|
| 553 |
+
|
| 554 |
+
# Top-p (probability space) — re-mask logits_scaled accordingly
|
| 555 |
+
if top_p is not None and 0.0 < top_p < 1.0:
|
| 556 |
+
logits_scaled = _top_p_filtering(
|
| 557 |
+
logits_scaled,
|
| 558 |
+
probs,
|
| 559 |
+
top_p=top_p,
|
| 560 |
+
min_tokens_to_keep=min_tokens_to_keep,
|
| 561 |
+
filter_value=filter_value,
|
| 562 |
+
)
|
| 563 |
+
probs = F.softmax(logits_scaled, dim=-1) # recompute after top-p masking
|
| 564 |
+
|
| 565 |
+
if do_sample:
|
| 566 |
+
# multinomial sampling with optional RNG for reproducibility
|
| 567 |
+
next_ids = torch.multinomial(probs, num_samples=1, replacement=True, generator=rng).squeeze(
|
| 568 |
+
-1
|
| 569 |
+
)
|
| 570 |
+
else:
|
| 571 |
+
# greedy
|
| 572 |
+
next_ids = torch.argmax(probs, dim=-1)
|
| 573 |
+
|
| 574 |
+
next_ids = next_ids.to(dtype=torch.long, device=logits.device) # back to original device
|
| 575 |
+
if return_probs:
|
| 576 |
+
return next_ids, probs.to(device=logits.device, dtype=probs.dtype)
|
| 577 |
+
return next_ids
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def create_causal_mask(
|
| 581 |
+
max_seq_len: int,
|
| 582 |
+
*,
|
| 583 |
+
device: torch.device | None = None,
|
| 584 |
+
dtype: torch.dtype = torch.bool,
|
| 585 |
+
) -> torch.Tensor:
|
| 586 |
+
"""
|
| 587 |
+
Build a standard causal mask to prevent attending to future positions.
|
| 588 |
+
|
| 589 |
+
Shape: [1, 1, max_seq_len, max_seq_len]
|
| 590 |
+
|
| 591 |
+
mask[i, j] = True means position j should be masked when predicting i.
|
| 592 |
+
|
| 593 |
+
Args:
|
| 594 |
+
max_seq_len: length of the target sequence (must be > 0).
|
| 595 |
+
device: torch device to place the mask on (defaults to cpu).
|
| 596 |
+
dtype: dtype of mask (default: torch.bool).
|
| 597 |
+
Usually boolean, but some fused kernels want float with -inf/0.
|
| 598 |
+
|
| 599 |
+
Returns:
|
| 600 |
+
Tensor of shape [1, 1, max_seq_len, max_seq_len].
|
| 601 |
+
"""
|
| 602 |
+
if max_seq_len <= 0:
|
| 603 |
+
raise ValueError(f"max_seq_len must be > 0, got {max_seq_len}")
|
| 604 |
+
|
| 605 |
+
# upper-triangular (excluding main diagonal)
|
| 606 |
+
mask = torch.triu(
|
| 607 |
+
torch.ones((1, 1, max_seq_len, max_seq_len), device=device, dtype=torch.bool),
|
| 608 |
+
diagonal=1,
|
| 609 |
+
)
|
| 610 |
+
if dtype != torch.bool:
|
| 611 |
+
mask = mask.to(dtype)
|
| 612 |
+
return mask
|
tests/units/modules/test_attention.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.modules.attention import MultiHeadAttention
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# -----------------------
|
| 8 |
+
# Helpers
|
| 9 |
+
# -----------------------
|
| 10 |
+
def _rand(B=2, S=5, D=12, device="cpu", dtype=torch.float32):
|
| 11 |
+
return torch.randn(B, S, D, device=device, dtype=dtype)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# =======================
|
| 15 |
+
# Constructor checks
|
| 16 |
+
# =======================
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_ctor_type_checks():
|
| 20 |
+
with pytest.raises(TypeError):
|
| 21 |
+
MultiHeadAttention(d_model="64", num_heads=4, dropout_rate=0.1)
|
| 22 |
+
with pytest.raises(TypeError):
|
| 23 |
+
MultiHeadAttention(d_model=64, num_heads=4.0, dropout_rate=0.1)
|
| 24 |
+
with pytest.raises(TypeError):
|
| 25 |
+
MultiHeadAttention(d_model=64, num_heads=4, dropout_rate="0.1")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_ctor_value_checks():
|
| 29 |
+
with pytest.raises(ValueError):
|
| 30 |
+
MultiHeadAttention(d_model=0, num_heads=4, dropout_rate=0.1)
|
| 31 |
+
with pytest.raises(ValueError):
|
| 32 |
+
MultiHeadAttention(d_model=64, num_heads=0, dropout_rate=0.1)
|
| 33 |
+
with pytest.raises(ValueError):
|
| 34 |
+
MultiHeadAttention(d_model=63, num_heads=4, dropout_rate=0.1) # not divisible
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_ctor_happy_path():
|
| 38 |
+
mha = MultiHeadAttention(64, 8, 0.1)
|
| 39 |
+
assert mha.d_model == 64 and mha.num_heads == 8 and mha.d_head == 8
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# =======================
|
| 43 |
+
# Forward: type/shape checks
|
| 44 |
+
# =======================
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_forward_requires_tensors_and_mask_type():
|
| 48 |
+
mha = MultiHeadAttention(32, 4, 0.1)
|
| 49 |
+
q, k, v = _rand(2, 5, 32), _rand(2, 5, 32), _rand(2, 5, 32)
|
| 50 |
+
|
| 51 |
+
with pytest.raises(TypeError):
|
| 52 |
+
mha("q", k, v, None)
|
| 53 |
+
with pytest.raises(TypeError):
|
| 54 |
+
mha(q, "k", v, None)
|
| 55 |
+
with pytest.raises(TypeError):
|
| 56 |
+
mha(q, k, "v", None)
|
| 57 |
+
with pytest.raises(TypeError):
|
| 58 |
+
mha(q, k, v, mask="bad")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_forward_rank_and_lastdim_checks():
|
| 62 |
+
mha = MultiHeadAttention(32, 4, 0.1)
|
| 63 |
+
q = torch.randn(2, 5, 32)
|
| 64 |
+
k = torch.randn(2, 5, 32)
|
| 65 |
+
v = torch.randn(2, 5, 32)
|
| 66 |
+
|
| 67 |
+
with pytest.raises(ValueError):
|
| 68 |
+
mha(q.unsqueeze(0), k, v, None) # rank 4 for q
|
| 69 |
+
with pytest.raises(ValueError):
|
| 70 |
+
mha(q, k.view(10, 32), v, None) # rank 2 for k
|
| 71 |
+
with pytest.raises(ValueError):
|
| 72 |
+
mha(q[..., :16], k, v, None) # wrong Dq
|
| 73 |
+
with pytest.raises(ValueError):
|
| 74 |
+
mha(q, k[..., :16], v, None) # wrong Dk
|
| 75 |
+
with pytest.raises(ValueError):
|
| 76 |
+
mha(q, k, v[..., :16], None) # wrong Dv
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_forward_batch_and_seq_mismatch_checks():
|
| 80 |
+
mha = MultiHeadAttention(24, 3, 0.1)
|
| 81 |
+
q = torch.randn(2, 5, 24)
|
| 82 |
+
k = torch.randn(3, 5, 24) # batch mismatch
|
| 83 |
+
v = torch.randn(2, 5, 24) # seq mismatch with k
|
| 84 |
+
|
| 85 |
+
with pytest.raises(ValueError):
|
| 86 |
+
mha(q, k, torch.randn(3, 5, 24), None) # batch mismatch q vs k/v
|
| 87 |
+
with pytest.raises(ValueError):
|
| 88 |
+
mha(q, torch.randn(2, 6, 24), v, None) # Sk != Sv
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
# =======================
|
| 92 |
+
# Forward: happy path + masks + zero-length
|
| 93 |
+
# =======================
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
@pytest.mark.parametrize("B,S,D,H", [(1, 1, 16, 4), (2, 5, 32, 4), (3, 0, 24, 3)])
|
| 97 |
+
def test_forward_shapes_and_device_dtype(B, S, D, H):
|
| 98 |
+
device = (
|
| 99 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 100 |
+
if torch.cuda.is_available()
|
| 101 |
+
else torch.device("cpu")
|
| 102 |
+
)
|
| 103 |
+
mha = MultiHeadAttention(D, H, 0.1).to(device)
|
| 104 |
+
q = _rand(B, S, D, device=device)
|
| 105 |
+
k = _rand(B, S, D, device=device)
|
| 106 |
+
v = _rand(B, S, D, device=device)
|
| 107 |
+
|
| 108 |
+
out = mha(q, k, v, mask=None)
|
| 109 |
+
assert out.shape == (B, S, D)
|
| 110 |
+
assert out.device == device
|
| 111 |
+
assert out.dtype == q.dtype
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_boolean_mask_blocks_positions():
|
| 115 |
+
B, S, D, H = 2, 6, 24, 3
|
| 116 |
+
mha = MultiHeadAttention(D, H, 0.0) # no dropout for determinism
|
| 117 |
+
q = _rand(B, S, D)
|
| 118 |
+
k = _rand(B, S, D)
|
| 119 |
+
v = _rand(B, S, D)
|
| 120 |
+
|
| 121 |
+
# mask last two keys for all heads/queries
|
| 122 |
+
mask = torch.zeros(B, 1, 1, S, dtype=torch.bool)
|
| 123 |
+
mask[..., -2:] = True
|
| 124 |
+
|
| 125 |
+
out1 = mha(q, k, v, mask=None)
|
| 126 |
+
out2 = mha(q, k, v, mask=mask)
|
| 127 |
+
assert not torch.allclose(out1, out2)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_additive_mask_supported():
|
| 131 |
+
B, S, D, H = 2, 5, 32, 4
|
| 132 |
+
mha = MultiHeadAttention(D, H, 0.0)
|
| 133 |
+
q, k, v = _rand(B, S, D), _rand(B, S, D), _rand(B, S, D)
|
| 134 |
+
add = torch.zeros(1, 1, 1, S)
|
| 135 |
+
out = mha(q, k, v, add)
|
| 136 |
+
assert out.shape == (B, S, D)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# =======================
|
| 140 |
+
# Gradients smoke
|
| 141 |
+
# =======================
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_gradients_flow():
|
| 145 |
+
mha = MultiHeadAttention(32, 4, 0.1)
|
| 146 |
+
q = _rand(
|
| 147 |
+
2,
|
| 148 |
+
5,
|
| 149 |
+
32,
|
| 150 |
+
dtype=torch.float32,
|
| 151 |
+
device="cpu",
|
| 152 |
+
)
|
| 153 |
+
k = _rand(
|
| 154 |
+
2,
|
| 155 |
+
5,
|
| 156 |
+
32,
|
| 157 |
+
dtype=torch.float32,
|
| 158 |
+
device="cpu",
|
| 159 |
+
)
|
| 160 |
+
v = _rand(
|
| 161 |
+
2,
|
| 162 |
+
5,
|
| 163 |
+
32,
|
| 164 |
+
dtype=torch.float32,
|
| 165 |
+
device="cpu",
|
| 166 |
+
)
|
| 167 |
+
q.requires_grad_(True)
|
| 168 |
+
k.requires_grad_(True)
|
| 169 |
+
v.requires_grad_(True)
|
| 170 |
+
|
| 171 |
+
out = mha(q, k, v, mask=None)
|
| 172 |
+
loss = out.pow(2).mean()
|
| 173 |
+
loss.backward()
|
| 174 |
+
|
| 175 |
+
for t in (q, k, v):
|
| 176 |
+
assert t.grad is not None
|
| 177 |
+
assert torch.isfinite(t.grad).all()
|
tests/units/modules/test_decoder.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.modules.decoder import DecoderLayer, TransformerDecoder
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _dev():
|
| 8 |
+
return (
|
| 9 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 10 |
+
if torch.cuda.is_available()
|
| 11 |
+
else torch.device("cpu")
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# -------- ctor validations --------
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_decoder_layer_param_checks():
|
| 19 |
+
with pytest.raises(TypeError):
|
| 20 |
+
DecoderLayer("32", 4, 64, 0.1)
|
| 21 |
+
with pytest.raises(TypeError):
|
| 22 |
+
DecoderLayer(32, "4", 64, 0.1)
|
| 23 |
+
with pytest.raises(TypeError):
|
| 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)
|
| 30 |
+
with pytest.raises(ValueError):
|
| 31 |
+
DecoderLayer(32, 0, 64, 0.1)
|
| 32 |
+
with pytest.raises(ValueError):
|
| 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():
|
| 39 |
+
with pytest.raises(TypeError):
|
| 40 |
+
TransformerDecoder(32, 4, 64, "2", 0.1)
|
| 41 |
+
with pytest.raises(ValueError):
|
| 42 |
+
TransformerDecoder(32, 4, 64, 0, 0.1)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# -------- forward path --------
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@pytest.mark.parametrize("B,Sx,Sy,D,H,FF,L", [(2, 5, 6, 24, 3, 48, 2), (1, 0, 0, 16, 1, 32, 1)])
|
| 49 |
+
def test_decoder_forward_happy_path_and_zero_len(B, Sx, Sy, D, H, FF, L):
|
| 50 |
+
device = _dev()
|
| 51 |
+
dec = TransformerDecoder(D, H, FF, L, 0.1).to(device)
|
| 52 |
+
|
| 53 |
+
x = torch.randn(B, Sx, D, device=device)
|
| 54 |
+
y = torch.randn(B, Sy, D, device=device)
|
| 55 |
+
|
| 56 |
+
src_pad = torch.zeros(B, 1, 1, Sx, dtype=torch.bool, device=device) if Sx > 0 else None
|
| 57 |
+
tgt_pad = torch.zeros(B, 1, 1, Sy, dtype=torch.bool, device=device) if Sy > 0 else None
|
| 58 |
+
causal = torch.ones(1, 1, Sy, Sy, dtype=torch.bool, device=device).triu(1) if Sy > 0 else None
|
| 59 |
+
|
| 60 |
+
out = dec(x, y, src_pad, tgt_pad, causal)
|
| 61 |
+
assert out.shape == (B, Sy, D)
|
| 62 |
+
assert out.device == device
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_decoder_forward_input_checks_and_message_format():
|
| 66 |
+
layer = DecoderLayer(24, 3, 48, 0.1)
|
| 67 |
+
|
| 68 |
+
with pytest.raises(TypeError):
|
| 69 |
+
layer("not a tensor", torch.randn(2, 3, 24), None, None, None)
|
| 70 |
+
with pytest.raises(TypeError):
|
| 71 |
+
layer(torch.randn(2, 3, 24), "not a tensor", None, None, None)
|
| 72 |
+
|
| 73 |
+
with pytest.raises(ValueError) as e1:
|
| 74 |
+
layer(torch.randn(2, 3, 24, 5), torch.randn(2, 3, 24), None, None, None) # x rank 4
|
| 75 |
+
assert "x must be a 3D torch.Tensor of shape (B, S, D)" in str(e1.value)
|
| 76 |
+
|
| 77 |
+
with pytest.raises(ValueError) as e2:
|
| 78 |
+
layer(torch.randn(2, 3, 24), torch.randn(2, 3, 24, 5), None, None, None) # y rank 4
|
| 79 |
+
assert "y must be a 3D torch.Tensor of shape (B, S, D)" in str(e2.value)
|
| 80 |
+
|
| 81 |
+
x = torch.randn(2, 5, 24)
|
| 82 |
+
y = torch.randn(3, 6, 24) # batch mismatch
|
| 83 |
+
with pytest.raises(ValueError) as e3:
|
| 84 |
+
layer(x, y, None, None, None)
|
| 85 |
+
assert "Batch size or d_model mismatch between encoder memory and decoder input" in str(
|
| 86 |
+
e3.value
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
y2 = torch.randn(2, 6, 16) # d_model mismatch
|
| 90 |
+
with pytest.raises(ValueError) as e4:
|
| 91 |
+
layer(x, y2, None, None, None)
|
| 92 |
+
assert "Batch size or d_model mismatch between encoder memory and decoder input" in str(
|
| 93 |
+
e4.value
|
| 94 |
+
)
|
tests/units/modules/test_embedding.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from transformer.modules.embedding import InputEmbedding, PositionalEmbedding
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# -----------------------
|
| 10 |
+
# Helpers
|
| 11 |
+
# -----------------------
|
| 12 |
+
def _rand_ids(B=2, S=5, vocab=11, device="cpu"):
|
| 13 |
+
return torch.randint(0, vocab, (B, S), dtype=torch.long, device=device)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# =======================
|
| 17 |
+
# InputEmbedding — ctor
|
| 18 |
+
# =======================
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_ctor_type_checks():
|
| 22 |
+
with pytest.raises(TypeError):
|
| 23 |
+
InputEmbedding("10", 8, 16, 0) # vocab_size
|
| 24 |
+
with pytest.raises(TypeError):
|
| 25 |
+
InputEmbedding(10, "8", 16, 0) # d_model
|
| 26 |
+
with pytest.raises(TypeError):
|
| 27 |
+
InputEmbedding(10, 8, "16", 0) # sequence_length
|
| 28 |
+
with pytest.raises(TypeError):
|
| 29 |
+
InputEmbedding(10, 8, 16, "0") # pad_id
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_ctor_value_checks_basic():
|
| 33 |
+
with pytest.raises(ValueError): # vocab_size <= 0
|
| 34 |
+
InputEmbedding(0, 8, 16, 0)
|
| 35 |
+
with pytest.raises(ValueError): # d_model <= 0
|
| 36 |
+
InputEmbedding(10, 0, 16, 0)
|
| 37 |
+
with pytest.raises(ValueError): # sequence_length < 0 (now allowed to be 0)
|
| 38 |
+
InputEmbedding(10, 8, -1, 0)
|
| 39 |
+
with pytest.raises(ValueError): # pad_id out of range
|
| 40 |
+
InputEmbedding(10, 8, 16, 10)
|
| 41 |
+
with pytest.raises(ValueError):
|
| 42 |
+
InputEmbedding(10, 8, 16, -1)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_ctor_allows_zero_length_and_sets_attrs():
|
| 46 |
+
m = InputEmbedding(11, 8, 0, 0)
|
| 47 |
+
assert m.sequence_length == 0
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def test_padding_row_zero_and_stays_zero_after_step():
|
| 51 |
+
m = InputEmbedding(13, 6, 10, pad_id=3)
|
| 52 |
+
with torch.no_grad():
|
| 53 |
+
assert torch.allclose(
|
| 54 |
+
m.token_embed.weight[3], torch.zeros(6, dtype=m.token_embed.weight.dtype)
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
ids = torch.tensor([[3, 4, 5, 6]], dtype=torch.long) # includes pad token 3
|
| 58 |
+
out = m(ids).sum()
|
| 59 |
+
out.backward()
|
| 60 |
+
opt = torch.optim.SGD(m.parameters(), lr=0.1)
|
| 61 |
+
opt.step()
|
| 62 |
+
with torch.no_grad():
|
| 63 |
+
assert torch.allclose(
|
| 64 |
+
m.token_embed.weight[3], torch.zeros(6, dtype=m.token_embed.weight.dtype)
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# =======================
|
| 69 |
+
# InputEmbedding — forward
|
| 70 |
+
# =======================
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_forward_type_and_shape_checks():
|
| 74 |
+
m = InputEmbedding(10, 8, 16, 0)
|
| 75 |
+
|
| 76 |
+
with pytest.raises(TypeError):
|
| 77 |
+
m("not a tensor") # wrong type
|
| 78 |
+
|
| 79 |
+
with pytest.raises(TypeError):
|
| 80 |
+
m(torch.ones(2, 5, dtype=torch.float32)) # wrong dtype, must be long
|
| 81 |
+
|
| 82 |
+
with pytest.raises(ValueError):
|
| 83 |
+
m(torch.ones(2, 5, 1, dtype=torch.long)) # rank != 2
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_forward_out_of_range_ids_raise_index_error():
|
| 87 |
+
m = InputEmbedding(10, 8, 16, 0)
|
| 88 |
+
x = torch.tensor([[0, 9, 10]], dtype=torch.long) # 10 is out of range for vocab_size=10
|
| 89 |
+
with pytest.raises((IndexError, RuntimeError)): # PyTorch may raise either
|
| 90 |
+
m(x)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_forward_happy_path_shape_dtype_device_and_zero_len():
|
| 94 |
+
device = (
|
| 95 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 96 |
+
if torch.cuda.is_available()
|
| 97 |
+
else torch.device("cpu")
|
| 98 |
+
)
|
| 99 |
+
m = InputEmbedding(32, 24, 64, 0).to(device)
|
| 100 |
+
|
| 101 |
+
# non-empty
|
| 102 |
+
x = _rand_ids(B=3, S=7, vocab=32, device=device)
|
| 103 |
+
out = m(x)
|
| 104 |
+
assert out.shape == (3, 7, 24)
|
| 105 |
+
assert out.device == device
|
| 106 |
+
assert out.dtype == torch.get_default_dtype()
|
| 107 |
+
|
| 108 |
+
# zero-length sequence allowed
|
| 109 |
+
x0 = _rand_ids(B=2, S=0, vocab=32, device=device)
|
| 110 |
+
out0 = m(x0)
|
| 111 |
+
assert out0.shape == (2, 0, 24)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def test_forward_adds_positions_not_just_tokens():
|
| 115 |
+
m = InputEmbedding(20, 12, 32, 0)
|
| 116 |
+
x = _rand_ids(B=2, S=5, vocab=20)
|
| 117 |
+
tok_only = m.token_embed(x)
|
| 118 |
+
out = m(x)
|
| 119 |
+
assert not torch.allclose(out, tok_only)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def test_forward_respects_sequence_length_limit():
|
| 123 |
+
m = InputEmbedding(16, 8, 5, 0)
|
| 124 |
+
x_ok = _rand_ids(B=2, S=5, vocab=16)
|
| 125 |
+
_ = m(x_ok) # should not raise
|
| 126 |
+
x_bad = _rand_ids(B=2, S=6, vocab=16)
|
| 127 |
+
with pytest.raises(ValueError) as ei:
|
| 128 |
+
_ = m(x_bad)
|
| 129 |
+
assert "exceeds max_seq_len" in str(ei.value)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# =======================
|
| 133 |
+
# PositionalEmbedding — ctor
|
| 134 |
+
# =======================
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def test_positional_ctor_value_checks():
|
| 138 |
+
with pytest.raises(ValueError):
|
| 139 |
+
PositionalEmbedding(-1, 8) # sequence_length < 0
|
| 140 |
+
with pytest.raises(ValueError):
|
| 141 |
+
PositionalEmbedding(16, 0) # d_model <= 0
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_positional_buffer_registered_and_constant_zero_ok():
|
| 145 |
+
pe = PositionalEmbedding(0, 10) # zero supported
|
| 146 |
+
assert hasattr(pe, "pe")
|
| 147 |
+
assert isinstance(pe.pe, torch.Tensor)
|
| 148 |
+
assert pe.pe.shape == (1, 0, 10)
|
| 149 |
+
assert pe.pe.requires_grad is False
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# =======================
|
| 153 |
+
# PositionalEmbedding — forward
|
| 154 |
+
# =======================
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def test_positional_forward_type_and_shape_checks():
|
| 158 |
+
pe = PositionalEmbedding(16, 8)
|
| 159 |
+
|
| 160 |
+
with pytest.raises(TypeError):
|
| 161 |
+
pe("not a tensor")
|
| 162 |
+
with pytest.raises(ValueError):
|
| 163 |
+
pe(torch.zeros(2, 5)) # rank != 3
|
| 164 |
+
with pytest.raises(ValueError):
|
| 165 |
+
pe(torch.zeros(2, 4, 6)) # d_model mismatch
|
| 166 |
+
with pytest.raises(ValueError):
|
| 167 |
+
pe(torch.zeros(2, 17, 8)) # seq_len exceeds max
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def test_positional_forward_adds_nonzero_positions_and_preserves_shape():
|
| 171 |
+
pe = PositionalEmbedding(32, 12)
|
| 172 |
+
x = torch.zeros(2, 7, 12)
|
| 173 |
+
y = pe(x)
|
| 174 |
+
assert y.shape == x.shape
|
| 175 |
+
assert not torch.allclose(y, x)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def test_positional_forward_device_and_dtype_follow_input_and_zero_len():
|
| 179 |
+
pe = PositionalEmbedding(64, 16)
|
| 180 |
+
|
| 181 |
+
# CPU float32
|
| 182 |
+
x = torch.zeros(1, 5, 16, dtype=torch.float32, device="cpu")
|
| 183 |
+
y = pe(x)
|
| 184 |
+
assert y.device.type == "cpu" and y.dtype == torch.float32
|
| 185 |
+
|
| 186 |
+
# zero length
|
| 187 |
+
x0 = torch.zeros(1, 0, 16)
|
| 188 |
+
y0 = pe(x0)
|
| 189 |
+
assert y0.shape == (1, 0, 16)
|
| 190 |
+
|
| 191 |
+
# bfloat16 on CPU (if supported)
|
| 192 |
+
try:
|
| 193 |
+
x_bf = torch.zeros(1, 5, 16, dtype=torch.bfloat16)
|
| 194 |
+
y_bf = pe(x_bf)
|
| 195 |
+
assert y_bf.dtype == torch.bfloat16
|
| 196 |
+
except Exception:
|
| 197 |
+
pass
|
| 198 |
+
|
| 199 |
+
# CUDA & half if available
|
| 200 |
+
if torch.cuda.is_available():
|
| 201 |
+
xh = torch.zeros(1, 5, 16, dtype=torch.float16, device="cuda")
|
| 202 |
+
yh = pe(xh)
|
| 203 |
+
assert yh.device.type == "cuda" and yh.dtype == torch.float16
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_positional_matches_known_small_reference():
|
| 207 |
+
# Cross-check first few values with textbook sin/cos
|
| 208 |
+
S, D = 4, 6
|
| 209 |
+
pe = PositionalEmbedding(S, D)
|
| 210 |
+
x = torch.zeros(1, S, D)
|
| 211 |
+
y = pe(x)
|
| 212 |
+
added = y[0] # [S, D]
|
| 213 |
+
|
| 214 |
+
ref = torch.zeros_like(added)
|
| 215 |
+
base = 10_000.0
|
| 216 |
+
for pos in range(S):
|
| 217 |
+
for i in range(D // 2):
|
| 218 |
+
denom = base ** (2 * i / D)
|
| 219 |
+
ref[pos, 2 * i] = math.sin(pos / denom)
|
| 220 |
+
ref[pos, 2 * i + 1] = math.cos(pos / denom)
|
| 221 |
+
|
| 222 |
+
assert torch.allclose(added, ref, atol=1e-6, rtol=1e-6)
|
tests/units/modules/test_encoder.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.modules.encoder import EncoderLayer, TransformerEncoder
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _dev():
|
| 8 |
+
return (
|
| 9 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 10 |
+
if torch.cuda.is_available()
|
| 11 |
+
else torch.device("cpu")
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# -------- ctor validations --------
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_encoder_layer_param_checks():
|
| 19 |
+
with pytest.raises(TypeError):
|
| 20 |
+
EncoderLayer("32", 4, 64, 0.1)
|
| 21 |
+
with pytest.raises(TypeError):
|
| 22 |
+
EncoderLayer(32, "4", 64, 0.1)
|
| 23 |
+
with pytest.raises(TypeError):
|
| 24 |
+
EncoderLayer(32, 4, "64", 0.1)
|
| 25 |
+
with pytest.raises(TypeError):
|
| 26 |
+
EncoderLayer(32, 4, 64, "0.1")
|
| 27 |
+
|
| 28 |
+
with pytest.raises(ValueError):
|
| 29 |
+
EncoderLayer(0, 4, 64, 0.1)
|
| 30 |
+
with pytest.raises(ValueError):
|
| 31 |
+
EncoderLayer(32, 0, 64, 0.1)
|
| 32 |
+
with pytest.raises(ValueError):
|
| 33 |
+
EncoderLayer(32, 4, 0, 0.1)
|
| 34 |
+
with pytest.raises(ValueError):
|
| 35 |
+
EncoderLayer(32, 4, 64, 1.0) # upper bound excluded
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_transformer_encoder_layers_count_checks():
|
| 39 |
+
with pytest.raises(TypeError):
|
| 40 |
+
TransformerEncoder(32, 4, 64, "2", 0.1)
|
| 41 |
+
with pytest.raises(ValueError):
|
| 42 |
+
TransformerEncoder(32, 4, 64, 0, 0.1)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# -------- forward path --------
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@pytest.mark.parametrize("B,S,D,H,FF,L", [(2, 5, 24, 3, 48, 2), (1, 0, 16, 1, 32, 1)])
|
| 49 |
+
def test_encoder_forward_happy_path_and_zero_len(B, S, D, H, FF, L):
|
| 50 |
+
device = _dev()
|
| 51 |
+
enc = TransformerEncoder(D, H, FF, L, 0.1).to(device)
|
| 52 |
+
|
| 53 |
+
x = torch.randn(B, S, D, device=device)
|
| 54 |
+
src_pad = torch.zeros(B, 1, 1, S, dtype=torch.bool, device=device) if S > 0 else None
|
| 55 |
+
|
| 56 |
+
out = enc(x, src_pad)
|
| 57 |
+
assert out.shape == (B, S, D)
|
| 58 |
+
assert out.device == device
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_encoder_forward_input_checks_and_message_format():
|
| 62 |
+
layer = EncoderLayer(24, 3, 48, 0.1)
|
| 63 |
+
|
| 64 |
+
with pytest.raises(TypeError):
|
| 65 |
+
layer("not a tensor", None)
|
| 66 |
+
|
| 67 |
+
with pytest.raises(ValueError) as ei:
|
| 68 |
+
layer(torch.randn(2, 3, 4, 5), None) # rank 4
|
| 69 |
+
assert "x must be a 3D torch.Tensor of shape (B, S, D)" in str(ei.value)
|
tests/units/modules/test_feedforward.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.modules.feedforward import FeedForwardLayer
|
| 5 |
+
|
| 6 |
+
# =======================
|
| 7 |
+
# Constructor checks
|
| 8 |
+
# =======================
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_ctor_type_checks():
|
| 12 |
+
with pytest.raises(TypeError):
|
| 13 |
+
FeedForwardLayer("64", 256, 0.1) # d_model
|
| 14 |
+
with pytest.raises(TypeError):
|
| 15 |
+
FeedForwardLayer(64, "256", 0.1) # d_ff
|
| 16 |
+
with pytest.raises(TypeError):
|
| 17 |
+
FeedForwardLayer(64, 256, "0.1") # dropout_rate
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_ctor_value_checks():
|
| 21 |
+
with pytest.raises(ValueError):
|
| 22 |
+
FeedForwardLayer(0, 256, 0.1)
|
| 23 |
+
with pytest.raises(ValueError):
|
| 24 |
+
FeedForwardLayer(64, 0, 0.1)
|
| 25 |
+
with pytest.raises(ValueError):
|
| 26 |
+
FeedForwardLayer(64, 256, -1.0)
|
| 27 |
+
with pytest.raises(ValueError):
|
| 28 |
+
FeedForwardLayer(64, 256, 1.0) # upper bound excluded
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_ctor_happy_path_defaults_and_attrs():
|
| 32 |
+
ffn = FeedForwardLayer(64, 256) # default dropout=0.1
|
| 33 |
+
assert ffn.d_model == 64 and ffn.d_ff == 256
|
| 34 |
+
assert isinstance(ffn.dropout, torch.nn.Dropout)
|
| 35 |
+
assert abs(ffn.dropout.p - 0.1) < 1e-9
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# =======================
|
| 39 |
+
# Forward: input validation
|
| 40 |
+
# =======================
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_forward_type_and_rank_checks():
|
| 44 |
+
ffn = FeedForwardLayer(32, 64, 0.1)
|
| 45 |
+
with pytest.raises(TypeError):
|
| 46 |
+
ffn("not a tensor")
|
| 47 |
+
with pytest.raises(ValueError):
|
| 48 |
+
ffn(torch.randn(5, 32)) # rank 2
|
| 49 |
+
with pytest.raises(ValueError):
|
| 50 |
+
ffn(torch.randn(2, 3, 16)) # D mismatch (expects 32)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# =======================
|
| 54 |
+
# Forward: shapes, device, dtype, zero-length
|
| 55 |
+
# =======================
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@pytest.mark.parametrize("B,S,D", [(1, 1, 16), (2, 7, 32), (3, 0, 24)])
|
| 59 |
+
def test_forward_shapes_device_dtype_and_zero_len(B, S, D):
|
| 60 |
+
device = (
|
| 61 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 62 |
+
if torch.cuda.is_available()
|
| 63 |
+
else torch.device("cpu")
|
| 64 |
+
)
|
| 65 |
+
ffn = FeedForwardLayer(D, D * 2, 0.2).to(device)
|
| 66 |
+
|
| 67 |
+
x = torch.randn(B, S, D, device=device, dtype=torch.float32)
|
| 68 |
+
y = ffn(x)
|
| 69 |
+
assert y.shape == (B, S, D)
|
| 70 |
+
assert y.device == device
|
| 71 |
+
assert y.dtype == x.dtype
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# =======================
|
| 75 |
+
# Forward: gradient flow
|
| 76 |
+
# =======================
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_gradients_flow():
|
| 80 |
+
D, H = 32, 64
|
| 81 |
+
ffn = FeedForwardLayer(D, H, 0.1)
|
| 82 |
+
x = torch.randn(2, 5, D, requires_grad=True)
|
| 83 |
+
y = ffn(x)
|
| 84 |
+
loss = y.pow(2).mean()
|
| 85 |
+
loss.backward()
|
| 86 |
+
assert x.grad is not None and torch.isfinite(x.grad).all()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
# =======================
|
| 90 |
+
# Dropout behavior
|
| 91 |
+
# =======================
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_dropout_zero_equals_no_dropout_path():
|
| 95 |
+
D, H = 16, 32
|
| 96 |
+
# Build two modules: one with p=0.0, one with p>0 but switched to eval()
|
| 97 |
+
ffn0 = FeedForwardLayer(D, H, 0.0)
|
| 98 |
+
ffnx = FeedForwardLayer(D, H, 0.5).eval() # eval disables dropout
|
| 99 |
+
|
| 100 |
+
ffnx.load_state_dict(ffn0.state_dict(), strict=False)
|
| 101 |
+
|
| 102 |
+
x = torch.randn(2, 4, D)
|
| 103 |
+
y0 = ffn0(x)
|
| 104 |
+
yx = ffnx(x)
|
| 105 |
+
# With dropout disabled both should be equal (same weights distrib not identical, but same computation tree)
|
| 106 |
+
assert torch.allclose(y0, yx, atol=1e-6, rtol=1e-6)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_dropout_training_changes_output_vs_eval():
|
| 110 |
+
D, H = 32, 64
|
| 111 |
+
ffn = FeedForwardLayer(D, H, 0.5)
|
| 112 |
+
x = torch.randn(3, 6, D)
|
| 113 |
+
|
| 114 |
+
torch.manual_seed(123)
|
| 115 |
+
ffn.train()
|
| 116 |
+
y_train = ffn(x)
|
| 117 |
+
|
| 118 |
+
torch.manual_seed(123)
|
| 119 |
+
ffn.eval()
|
| 120 |
+
y_eval = ffn(x)
|
| 121 |
+
|
| 122 |
+
# Same seed but eval disables dropout -> outputs should differ
|
| 123 |
+
assert not torch.allclose(y_train, y_eval)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def test_dropout_is_noop_on_zero_length():
|
| 127 |
+
D, H = 32, 64
|
| 128 |
+
ffn = FeedForwardLayer(D, H, 0.7).train()
|
| 129 |
+
x = torch.randn(2, 0, D) # zero-length sequence
|
| 130 |
+
y = ffn(x)
|
| 131 |
+
assert y.shape == (2, 0, D) # no crash; shape preserved
|
tests/units/modules/test_lm_head.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.modules.lm_head import LMHead
|
| 5 |
+
|
| 6 |
+
# -----------------------
|
| 7 |
+
# Constructor checks
|
| 8 |
+
# -----------------------
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_ctor_type_and_value_checks():
|
| 12 |
+
with pytest.raises(TypeError):
|
| 13 |
+
LMHead("64", 10)
|
| 14 |
+
with pytest.raises(TypeError):
|
| 15 |
+
LMHead(64, "10")
|
| 16 |
+
with pytest.raises(ValueError):
|
| 17 |
+
LMHead(0, 10)
|
| 18 |
+
with pytest.raises(ValueError):
|
| 19 |
+
LMHead(64, 0)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_ctor_happy_path():
|
| 23 |
+
lm = LMHead(32, 1000)
|
| 24 |
+
assert lm.d_model == 32
|
| 25 |
+
assert lm.vocab_size == 1000
|
| 26 |
+
assert lm.fc.weight.shape == (1000, 32)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# -----------------------
|
| 30 |
+
# Forward checks (3D only)
|
| 31 |
+
# -----------------------
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_forward_type_and_shape_errors():
|
| 35 |
+
lm = LMHead(16, 50)
|
| 36 |
+
with pytest.raises(TypeError):
|
| 37 |
+
lm("not a tensor")
|
| 38 |
+
with pytest.raises(ValueError):
|
| 39 |
+
lm(torch.randn(2, 3, 4, 5)) # rank 4 not supported
|
| 40 |
+
with pytest.raises(ValueError):
|
| 41 |
+
lm(torch.randn(5, 16)) # rank 2 not supported
|
| 42 |
+
with pytest.raises(ValueError):
|
| 43 |
+
lm(torch.randn(2, 8, 15)) # last dim != d_model
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_forward_happy_path_and_zero_len():
|
| 47 |
+
device = (
|
| 48 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 49 |
+
if torch.cuda.is_available()
|
| 50 |
+
else torch.device("cpu")
|
| 51 |
+
)
|
| 52 |
+
lm = LMHead(24, 101).to(device)
|
| 53 |
+
|
| 54 |
+
# Non-empty
|
| 55 |
+
x = torch.randn(3, 7, 24, device=device, dtype=torch.float32)
|
| 56 |
+
y = lm(x)
|
| 57 |
+
assert y.shape == (3, 7, 101)
|
| 58 |
+
assert y.device == device and y.dtype == torch.float32
|
| 59 |
+
|
| 60 |
+
# Zero-length sequence allowed
|
| 61 |
+
x0 = torch.randn(2, 0, 24, device=device, dtype=torch.float32)
|
| 62 |
+
y0 = lm(x0)
|
| 63 |
+
assert y0.shape == (2, 0, 101)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_gradients_flow():
|
| 67 |
+
lm = LMHead(8, 40)
|
| 68 |
+
x = torch.randn(3, 6, 8, requires_grad=True)
|
| 69 |
+
y = lm(x)
|
| 70 |
+
loss = y.pow(2).mean()
|
| 71 |
+
loss.backward()
|
| 72 |
+
assert x.grad is not None
|
| 73 |
+
assert torch.isfinite(x.grad).all()
|
tests/units/test_transformer.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from transformer.configs import BasicEncDecCfg
|
| 5 |
+
from transformer.transformer import BasicEncoderDecoderTransformer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _dev():
|
| 9 |
+
return (
|
| 10 |
+
torch.device(f"cuda:{torch.cuda.current_device()}")
|
| 11 |
+
if torch.cuda.is_available()
|
| 12 |
+
else torch.device("cpu")
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _tiny_cfg(**over):
|
| 17 |
+
# Small, fast config for tests; zero-length safe via max_seq_len >= 0
|
| 18 |
+
base = dict(
|
| 19 |
+
vocab_size=32,
|
| 20 |
+
d_model=16,
|
| 21 |
+
d_ff=32,
|
| 22 |
+
num_heads=4,
|
| 23 |
+
num_layers=2,
|
| 24 |
+
max_seq_len=32,
|
| 25 |
+
pad_id=0,
|
| 26 |
+
bos_id=1,
|
| 27 |
+
eos_id=2,
|
| 28 |
+
dropout_rate=0.1,
|
| 29 |
+
)
|
| 30 |
+
base.update(over)
|
| 31 |
+
return BasicEncDecCfg(**base)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# -----------------------
|
| 35 |
+
# Constructor & config validation
|
| 36 |
+
# -----------------------
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_ctor_cfg_type_and_values():
|
| 40 |
+
with pytest.raises(TypeError):
|
| 41 |
+
BasicEncoderDecoderTransformer(cfg="not a cfg")
|
| 42 |
+
# invalid vocab / dims / indices
|
| 43 |
+
bads = [
|
| 44 |
+
dict(vocab_size=0),
|
| 45 |
+
dict(d_model=0),
|
| 46 |
+
dict(max_seq_len=-1),
|
| 47 |
+
dict(pad_id=-1),
|
| 48 |
+
dict(pad_id=100), # out of range
|
| 49 |
+
dict(bos_id=999),
|
| 50 |
+
dict(eos_id=999),
|
| 51 |
+
]
|
| 52 |
+
for upd in bads:
|
| 53 |
+
with pytest.raises((TypeError, ValueError)):
|
| 54 |
+
BasicEncoderDecoderTransformer(_tiny_cfg(**upd))
|
| 55 |
+
|
| 56 |
+
# happy path
|
| 57 |
+
model = BasicEncoderDecoderTransformer(_tiny_cfg())
|
| 58 |
+
# weight tying sanity: same storage
|
| 59 |
+
assert model.embed.token_embed.weight.data_ptr() == model.lm_head.fc.weight.data_ptr()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# -----------------------
|
| 63 |
+
# Encode/Decode/Forward — shapes, dtypes, devices, zero-length
|
| 64 |
+
# -----------------------
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@pytest.mark.parametrize("Sx,Sy", [(5, 6), (0, 1)])
|
| 68 |
+
def test_forward_pipeline_shapes_and_types(Sx, Sy):
|
| 69 |
+
device = _dev()
|
| 70 |
+
cfg = _tiny_cfg(max_seq_len=16)
|
| 71 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 72 |
+
|
| 73 |
+
B = 2
|
| 74 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 75 |
+
tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
|
| 76 |
+
|
| 77 |
+
# padding masks (boolean), broadcastable; allow None when S == 0
|
| 78 |
+
src_mask = torch.zeros(B, 1, 1, Sx, dtype=torch.bool, device=device) if Sx > 0 else None
|
| 79 |
+
tgt_mask = torch.zeros(B, 1, 1, Sy, dtype=torch.bool, device=device) if Sy > 0 else None
|
| 80 |
+
|
| 81 |
+
logits = model(src, tgt, src_mask, tgt_mask)
|
| 82 |
+
assert logits.shape == (B, Sy, cfg.vocab_size)
|
| 83 |
+
assert logits.device == device
|
| 84 |
+
assert logits.dtype == torch.get_default_dtype()
|
| 85 |
+
|
| 86 |
+
# encode / decode individually
|
| 87 |
+
mem = model.encode(src, src_mask)
|
| 88 |
+
assert mem.shape == (B, Sx, cfg.d_model)
|
| 89 |
+
out = model.decode(mem, tgt, src_mask, tgt_mask)
|
| 90 |
+
assert out.shape == (B, Sy, cfg.d_model)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_forward_dtype_checks_and_shape_errors():
|
| 94 |
+
cfg = _tiny_cfg()
|
| 95 |
+
model = BasicEncoderDecoderTransformer(cfg)
|
| 96 |
+
|
| 97 |
+
B, Sx, Sy = 2, 4, 5
|
| 98 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long)
|
| 99 |
+
tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long)
|
| 100 |
+
|
| 101 |
+
with pytest.raises(TypeError):
|
| 102 |
+
model("not a tensor", tgt, None, None)
|
| 103 |
+
with pytest.raises(TypeError):
|
| 104 |
+
model(src, "not a tensor", None, None)
|
| 105 |
+
with pytest.raises(ValueError):
|
| 106 |
+
model(src.unsqueeze(0), tgt, None, None) # rank 3 src_ids
|
| 107 |
+
with pytest.raises(ValueError):
|
| 108 |
+
model(src, tgt.unsqueeze(-1), None, None) # rank 3 tgt_ids
|
| 109 |
+
with pytest.raises(TypeError):
|
| 110 |
+
model(src.float(), tgt, None, None) # wrong dtype
|
| 111 |
+
with pytest.raises(TypeError):
|
| 112 |
+
model(src, tgt.int(), None, None) # wrong dtype
|
| 113 |
+
with pytest.raises(TypeError):
|
| 114 |
+
model(src, tgt, src_padding_mask="not a tensor", tgt_padding_mask=None)
|
| 115 |
+
with pytest.raises(TypeError):
|
| 116 |
+
model(src, tgt, src_padding_mask=None, tgt_padding_mask="not a tensor")
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_encode_decode_errors_and_messages():
|
| 120 |
+
cfg = _tiny_cfg()
|
| 121 |
+
model = BasicEncoderDecoderTransformer(cfg)
|
| 122 |
+
|
| 123 |
+
src = torch.randint(0, cfg.vocab_size, (2, 4))
|
| 124 |
+
with pytest.raises(ValueError):
|
| 125 |
+
model.encode(src.unsqueeze(-1), None) # rank 3 not allowed
|
| 126 |
+
with pytest.raises(TypeError):
|
| 127 |
+
model.encode(src.float(), None) # dtype must be long
|
| 128 |
+
|
| 129 |
+
mem = torch.randn(2, 4, cfg.d_model)
|
| 130 |
+
tgt = torch.randint(0, cfg.vocab_size, (2, 5))
|
| 131 |
+
with pytest.raises(ValueError):
|
| 132 |
+
model.decode(mem.unsqueeze(0), tgt, None, None) # mem rank 4
|
| 133 |
+
with pytest.raises(ValueError):
|
| 134 |
+
model.decode(mem, tgt.unsqueeze(-1), None, None) # tgt rank 3
|
| 135 |
+
with pytest.raises(TypeError):
|
| 136 |
+
model.decode(mem, tgt.float(), None, None) # tgt dtype must be long
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_exceeding_max_seq_len_raises():
|
| 140 |
+
cfg = _tiny_cfg(max_seq_len=4)
|
| 141 |
+
model = BasicEncoderDecoderTransformer(cfg)
|
| 142 |
+
# src longer than max -> PositionalEmbedding should raise
|
| 143 |
+
src = torch.randint(0, cfg.vocab_size, (2, 5), dtype=torch.long)
|
| 144 |
+
with pytest.raises(ValueError):
|
| 145 |
+
model.encode(src, None)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# -----------------------
|
| 149 |
+
# Gradients & weight tying
|
| 150 |
+
# -----------------------
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def test_backward_through_full_forward_and_tied_weights():
|
| 154 |
+
device = _dev()
|
| 155 |
+
cfg = _tiny_cfg()
|
| 156 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 157 |
+
|
| 158 |
+
B, Sx, Sy = 2, 6, 5
|
| 159 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 160 |
+
tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
|
| 161 |
+
src_mask = torch.zeros(B, 1, 1, Sx, dtype=torch.bool, device=device)
|
| 162 |
+
tgt_mask = torch.zeros(B, 1, 1, Sy, dtype=torch.bool, device=device)
|
| 163 |
+
|
| 164 |
+
logits = model(src, tgt, src_mask, tgt_mask) # (B, Sy, V)
|
| 165 |
+
# Dummy labels (language modeling): predict tgt itself; ignore pads via mask
|
| 166 |
+
loss = logits.pow(2).mean()
|
| 167 |
+
loss.backward()
|
| 168 |
+
|
| 169 |
+
# Tied weights should have gradient (one shared tensor)
|
| 170 |
+
tied_weight = model.lm_head.fc.weight
|
| 171 |
+
assert tied_weight.grad is not None
|
| 172 |
+
assert torch.isfinite(tied_weight.grad).all()
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# -----------------------
|
| 176 |
+
# Generate API
|
| 177 |
+
# -----------------------
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def test_generate_arg_checks_and_output_shapes():
|
| 181 |
+
device = _dev()
|
| 182 |
+
cfg = _tiny_cfg()
|
| 183 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 184 |
+
|
| 185 |
+
B, Sx = 2, 4
|
| 186 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 187 |
+
src_mask = torch.zeros(B, 1, 1, Sx, dtype=torch.bool, device=device)
|
| 188 |
+
|
| 189 |
+
# type/value errors
|
| 190 |
+
with pytest.raises(TypeError):
|
| 191 |
+
model.generate("not", src_mask)
|
| 192 |
+
with pytest.raises(ValueError):
|
| 193 |
+
model.generate(src.unsqueeze(-1), src_mask)
|
| 194 |
+
with pytest.raises(TypeError):
|
| 195 |
+
model.generate(src.float(), src_mask)
|
| 196 |
+
with pytest.raises(TypeError):
|
| 197 |
+
model.generate(src, "not a tensor")
|
| 198 |
+
with pytest.raises(TypeError):
|
| 199 |
+
model.generate(src, src_mask, max_new_tokens="10")
|
| 200 |
+
with pytest.raises(ValueError):
|
| 201 |
+
model.generate(src, src_mask, max_new_tokens=-1)
|
| 202 |
+
with pytest.raises(TypeError):
|
| 203 |
+
model.generate(src, src_mask, temperature="1.0")
|
| 204 |
+
with pytest.raises(ValueError):
|
| 205 |
+
model.generate(src, src_mask, temperature=0.0)
|
| 206 |
+
with pytest.raises(TypeError):
|
| 207 |
+
model.generate(src, src_mask, top_k="5")
|
| 208 |
+
with pytest.raises(ValueError):
|
| 209 |
+
model.generate(src, src_mask, top_k=0)
|
| 210 |
+
with pytest.raises(TypeError):
|
| 211 |
+
model.generate(src, src_mask, top_p="0.9")
|
| 212 |
+
with pytest.raises(ValueError):
|
| 213 |
+
model.generate(src, src_mask, top_p=0.0)
|
| 214 |
+
with pytest.raises(ValueError):
|
| 215 |
+
model.generate(src, src_mask, top_p=1.1)
|
| 216 |
+
|
| 217 |
+
# happy path: shapes & dtypes; content is stochastic so we don't assert exact ids
|
| 218 |
+
out = model.generate(src, src_mask, max_new_tokens=7, temperature=1.0, top_k=None, top_p=None)
|
| 219 |
+
# Should start with BOS and append new tokens; total length = 1 + new tokens (or earlier if EOS)
|
| 220 |
+
assert out.shape[0] == B and out.dim() == 2
|
| 221 |
+
assert out.dtype == torch.long and out.device == device
|
| 222 |
+
assert out.size(1) >= 1 and out.size(1) <= 1 + 7 # early stop on EOS allowed
|
| 223 |
+
|
| 224 |
+
# Ensure BOS is at position 0 for every sequence
|
| 225 |
+
assert torch.all(out[:, 0] == cfg.bos_id)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def test_generate_zero_new_tokens_returns_only_bos():
|
| 229 |
+
device = _dev()
|
| 230 |
+
cfg = _tiny_cfg()
|
| 231 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 232 |
+
|
| 233 |
+
src = torch.randint(0, cfg.vocab_size, (2, 3), dtype=torch.long, device=device)
|
| 234 |
+
out = model.generate(src, None, max_new_tokens=0)
|
| 235 |
+
assert out.shape == (2, 1) # just the BOS token
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# -----------------------
|
| 239 |
+
# Mask broadcast sanity (boolean & additive are handled in utils; we just pass through)
|
| 240 |
+
# -----------------------
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def test_forward_accepts_boolean_padding_masks():
|
| 244 |
+
device = _dev()
|
| 245 |
+
cfg = _tiny_cfg()
|
| 246 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 247 |
+
|
| 248 |
+
B, Sx, Sy = 2, 5, 6
|
| 249 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 250 |
+
tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
|
| 251 |
+
|
| 252 |
+
src_pad_bool = torch.zeros(B, 1, 1, Sx, dtype=torch.bool, device=device)
|
| 253 |
+
tgt_pad_bool = torch.zeros(B, 1, 1, Sy, dtype=torch.bool, device=device)
|
| 254 |
+
|
| 255 |
+
logits = model(src, tgt, src_pad_bool, tgt_pad_bool)
|
| 256 |
+
assert logits.shape == (B, Sy, cfg.vocab_size)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# -----------------------
|
| 260 |
+
# Weight tying and optimizer step
|
| 261 |
+
# -----------------------
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def test_weight_tying_and_optimizer_step_changes_weights_once():
|
| 265 |
+
device = _dev()
|
| 266 |
+
cfg = _tiny_cfg()
|
| 267 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device)
|
| 268 |
+
|
| 269 |
+
# Same storage pointer means truly tied
|
| 270 |
+
assert model.embed.token_embed.weight.data_ptr() == model.lm_head.fc.weight.data_ptr()
|
| 271 |
+
|
| 272 |
+
opt = torch.optim.SGD(model.parameters(), lr=0.01)
|
| 273 |
+
B, Sx, Sy = 2, 5, 4
|
| 274 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 275 |
+
tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
|
| 276 |
+
logits = model(src, tgt, None, None) # (B, Sy, V)
|
| 277 |
+
|
| 278 |
+
# Simple loss to get gradients flowing
|
| 279 |
+
loss = logits.pow(2).mean()
|
| 280 |
+
loss.backward()
|
| 281 |
+
|
| 282 |
+
# The tied tensor should have a gradient and be finite
|
| 283 |
+
tied = model.lm_head.fc.weight
|
| 284 |
+
assert tied.grad is not None and torch.isfinite(tied.grad).all()
|
| 285 |
+
|
| 286 |
+
# Save copy before step and ensure it changes after step (once, not twice)
|
| 287 |
+
before = tied.detach().clone()
|
| 288 |
+
opt.step()
|
| 289 |
+
after = tied.detach().clone()
|
| 290 |
+
|
| 291 |
+
assert not torch.allclose(before, after)
|
| 292 |
+
# Still tied after step (same storage)
|
| 293 |
+
assert model.embed.token_embed.weight.data_ptr() == model.lm_head.fc.weight.data_ptr()
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# -----------------------
|
| 297 |
+
# Autoregressive invariants: causal mask honored
|
| 298 |
+
# -----------------------
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def test_decode_invariance_to_future_tokens():
|
| 302 |
+
"""
|
| 303 |
+
Sanity-check causal masking: logits at position t must not depend on tokens at > t.
|
| 304 |
+
Construct two target sequences that are identical up to t and differ after; compare
|
| 305 |
+
decoder outputs at step t.
|
| 306 |
+
"""
|
| 307 |
+
device = _dev()
|
| 308 |
+
cfg = _tiny_cfg()
|
| 309 |
+
model = BasicEncoderDecoderTransformer(cfg).to(device).eval()
|
| 310 |
+
|
| 311 |
+
B, Sx, Sy = 1, 4, 5
|
| 312 |
+
src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
|
| 313 |
+
mem = model.encode(src, None)
|
| 314 |
+
|
| 315 |
+
# Construct y1 and y2 identical up to t=3, different afterwards
|
| 316 |
+
y1 = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
|
| 317 |
+
y2 = y1.clone()
|
| 318 |
+
t = 3
|
| 319 |
+
if Sy > t + 1:
|
| 320 |
+
y2[:, t + 1 :] = (y1[:, t + 1 :] + 1) % cfg.vocab_size
|
| 321 |
+
|
| 322 |
+
out1 = model.decode(mem, y1, None, None) # (B, Sy, D)
|
| 323 |
+
out2 = model.decode(mem, y2, None, None)
|
| 324 |
+
|
| 325 |
+
# Compare hidden states up to and including position t
|
| 326 |
+
assert torch.allclose(out1[:, : t + 1], out2[:, : t + 1], atol=1e-5, rtol=1e-5)
|
tests/units/test_utils.py
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
import pytest
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
|
| 7 |
+
from transformer.utils import (
|
| 8 |
+
calculate_attention,
|
| 9 |
+
combine_masks,
|
| 10 |
+
create_causal_mask,
|
| 11 |
+
join_heads,
|
| 12 |
+
sample_from_logits,
|
| 13 |
+
shift_right,
|
| 14 |
+
sinusoidal_positional_encoding,
|
| 15 |
+
split_heads,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
###___split_heads___###
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_happy_path_shape_dtype_device_cpu():
|
| 22 |
+
x = torch.randn(2, 10, 16) # B=2, S=10, D=16
|
| 23 |
+
out = split_heads(x, num_heads=4)
|
| 24 |
+
assert out.shape == (2, 4, 10, 4)
|
| 25 |
+
assert out.dtype == x.dtype
|
| 26 |
+
assert out.device == x.device
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_smallest_valid():
|
| 30 |
+
x = torch.randn(1, 1, 4)
|
| 31 |
+
out = split_heads(x, num_heads=2)
|
| 32 |
+
assert out.shape == (1, 2, 1, 2)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_zero_length_seq_allowed():
|
| 36 |
+
x = torch.randn(2, 0, 8)
|
| 37 |
+
out = split_heads(x, num_heads=2)
|
| 38 |
+
assert out.shape == (2, 2, 0, 4)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_non_3d_input_raises_value_error():
|
| 42 |
+
x2 = torch.randn(10, 16)
|
| 43 |
+
with pytest.raises(ValueError) as ei2:
|
| 44 |
+
split_heads(x2, 4)
|
| 45 |
+
assert "(B, S, D)" in str(ei2.value)
|
| 46 |
+
|
| 47 |
+
x4 = torch.randn(2, 3, 4, 5)
|
| 48 |
+
with pytest.raises(ValueError) as ei4:
|
| 49 |
+
split_heads(x4, 4)
|
| 50 |
+
assert "(B, S, D)" in str(ei4.value)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_invalid_num_heads_type_raises_type_error():
|
| 54 |
+
x = torch.randn(2, 10, 16)
|
| 55 |
+
with pytest.raises(TypeError):
|
| 56 |
+
split_heads(x, 4.0) # float
|
| 57 |
+
with pytest.raises(TypeError):
|
| 58 |
+
split_heads(x, torch.tensor(4)) # Tensor
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_num_heads_leq_zero_raises_value_error():
|
| 62 |
+
x = torch.randn(2, 10, 16)
|
| 63 |
+
with pytest.raises(ValueError):
|
| 64 |
+
split_heads(x, 0)
|
| 65 |
+
with pytest.raises(ValueError):
|
| 66 |
+
split_heads(x, -2)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_non_divisible_d_model_raises_value_error_message_has_values():
|
| 70 |
+
x = torch.randn(2, 5, 10)
|
| 71 |
+
with pytest.raises(ValueError) as e:
|
| 72 |
+
split_heads(x, 3)
|
| 73 |
+
msg = str(e.value)
|
| 74 |
+
assert "d_model (10)" in msg and "num_heads (3)" in msg
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@pytest.mark.parametrize(
|
| 78 |
+
"B,S,D,H",
|
| 79 |
+
[
|
| 80 |
+
(1, 1, 8, 1),
|
| 81 |
+
(2, 3, 12, 3),
|
| 82 |
+
(4, 7, 32, 8),
|
| 83 |
+
(3, 0, 24, 6),
|
| 84 |
+
],
|
| 85 |
+
)
|
| 86 |
+
def test_invariants_random_valid(B, S, D, H):
|
| 87 |
+
x = torch.randn(B, S, D)
|
| 88 |
+
out = split_heads(x, H)
|
| 89 |
+
assert out.shape[0] == B
|
| 90 |
+
assert out.shape[2] == S
|
| 91 |
+
assert (H * (D // H)) == D
|
| 92 |
+
assert x.numel() == out.numel()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_grad_propagates():
|
| 96 |
+
x = torch.randn(2, 10, 16, requires_grad=True)
|
| 97 |
+
out = split_heads(x, 4)
|
| 98 |
+
# Do a simple scalar reduction to make grad non-trivial
|
| 99 |
+
loss = out.square().mean()
|
| 100 |
+
loss.backward()
|
| 101 |
+
assert x.grad is not None
|
| 102 |
+
assert x.grad.shape == x.shape
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def test_non_contiguous_input():
|
| 106 |
+
# Create a non-contiguous view by transposing and slicing
|
| 107 |
+
base = torch.randn(10, 2, 16)
|
| 108 |
+
x = base.transpose(0, 1)[:, :8, :] # shape (2, 8, 16), likely non-contiguous
|
| 109 |
+
assert not x.is_contiguous()
|
| 110 |
+
out = split_heads(x, 4)
|
| 111 |
+
assert out.shape == (2, 4, 8, 4)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
| 115 |
+
def test_device_preserved_cuda():
|
| 116 |
+
x = torch.randn(2, 10, 16, device="cuda")
|
| 117 |
+
out = split_heads(x, 4)
|
| 118 |
+
assert out.device.type == "cuda"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
| 122 |
+
def test_dtype_half_bfloat_on_gpu():
|
| 123 |
+
x_half = torch.randn(2, 10, 16, device="cuda", dtype=torch.float16)
|
| 124 |
+
out_half = split_heads(x_half, 4)
|
| 125 |
+
assert out_half.dtype == torch.float16
|
| 126 |
+
|
| 127 |
+
# bfloat16 may not be available on all GPUs; guard with try
|
| 128 |
+
try:
|
| 129 |
+
x_bf16 = torch.randn(2, 10, 16, device="cuda", dtype=torch.bfloat16)
|
| 130 |
+
out_bf16 = split_heads(x_bf16, 4)
|
| 131 |
+
assert out_bf16.dtype == torch.bfloat16
|
| 132 |
+
except RuntimeError:
|
| 133 |
+
pytest.skip("bfloat16 not supported on this device")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
###___calculate_attention___###
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# ----------------------------
|
| 140 |
+
# Helpers
|
| 141 |
+
# ----------------------------
|
| 142 |
+
def _rand(B=2, H=3, Sq=4, Sk=5, D=8, device="cpu", dtype=torch.float32):
|
| 143 |
+
q = torch.randn(B, H, Sq, D, device=device, dtype=dtype)
|
| 144 |
+
k = torch.randn(B, H, Sk, D, device=device, dtype=dtype)
|
| 145 |
+
v = torch.randn(B, H, Sk, D, device=device, dtype=dtype)
|
| 146 |
+
return q, k, v
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
# ----------------------------
|
| 150 |
+
# A. Basic correctness
|
| 151 |
+
# ----------------------------
|
| 152 |
+
def test_basic_shapes_and_row_sums():
|
| 153 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 154 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 155 |
+
attn, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 156 |
+
assert attn.shape == (B, H, Sq, D)
|
| 157 |
+
assert probs.shape == (B, H, Sq, Sk)
|
| 158 |
+
# softmax rows sum to 1
|
| 159 |
+
assert torch.allclose(probs.sum(-1), torch.ones(B, H, Sq), atol=1e-6)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def test_identity_prefers_diagonal():
|
| 163 |
+
# q == k == v as orthogonal-ish basis to encourage diagonal peak
|
| 164 |
+
Sq = Sk = D = 4
|
| 165 |
+
base = torch.eye(D).view(1, 1, Sk, D).expand(1, 1, Sk, D).contiguous()
|
| 166 |
+
q = base.clone()
|
| 167 |
+
k = base.clone()
|
| 168 |
+
v = base.clone()
|
| 169 |
+
_, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 170 |
+
assert torch.equal(probs[0, 0].argmax(-1), torch.arange(Sq))
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# ----------------------------
|
| 174 |
+
# B. Mask behavior (boolean & causal)
|
| 175 |
+
# ----------------------------
|
| 176 |
+
def test_padding_mask_boolean_last_two_keys():
|
| 177 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 178 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 179 |
+
mask = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
|
| 180 |
+
mask[:, :, :, -2:] = True # mask last two keys
|
| 181 |
+
_, probs = calculate_attention(q, k, v, mask=mask, return_probs=True)
|
| 182 |
+
assert (probs[:, :, :, -2:] < 1e-6).all()
|
| 183 |
+
# After zeroing masked entries, rows renormalize to ~1
|
| 184 |
+
assert torch.allclose(probs.masked_fill(mask, 0).sum(-1), torch.ones(B, H, Sq), atol=1e-6)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def test_causal_mask_upper_triangle_zero():
|
| 188 |
+
B, H, S, D = 2, 3, 6, 8
|
| 189 |
+
q, k, v = _rand(B, H, S, S, D)
|
| 190 |
+
causal = torch.ones(B, H, S, S, dtype=torch.bool).triu(1) # True above diagonal
|
| 191 |
+
_, probs = calculate_attention(q, k, v, mask=causal, return_probs=True)
|
| 192 |
+
assert (probs.triu(1) < 1e-6).all()
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_fully_masked_row_zero_probs_and_output():
|
| 196 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 197 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 198 |
+
mask = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
|
| 199 |
+
mask[:, :, 1, :] = True # fully mask a query row
|
| 200 |
+
attn, probs = calculate_attention(q, k, v, mask=mask, return_probs=True)
|
| 201 |
+
assert torch.allclose(probs[:, :, 1, :], torch.zeros(B, H, Sk))
|
| 202 |
+
assert torch.allclose(attn[:, :, 1, :], torch.zeros(B, H, D))
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
# ----------------------------
|
| 206 |
+
# C. Numerical stability
|
| 207 |
+
# ----------------------------
|
| 208 |
+
def test_extreme_logits_no_nans_or_infs():
|
| 209 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 210 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 211 |
+
q = q * 1000
|
| 212 |
+
k = k * 1000
|
| 213 |
+
attn, probs = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 214 |
+
assert not (torch.isnan(probs).any() or torch.isinf(probs).any())
|
| 215 |
+
assert attn.shape == (B, H, Sq, D)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def test_half_precision_close_to_fp32_or_skip():
|
| 219 |
+
if not torch.cuda.is_available():
|
| 220 |
+
pytest.skip("CUDA not available")
|
| 221 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 222 |
+
q32, k32, v32 = _rand(B, H, Sq, Sk, D, device="cuda", dtype=torch.float32)
|
| 223 |
+
q16, k16, v16 = q32.half(), k32.half(), v32.half()
|
| 224 |
+
a16 = calculate_attention(q16, k16, v16, mask=None)
|
| 225 |
+
a32 = calculate_attention(q32, k32, v32, mask=None)
|
| 226 |
+
# looser tolerance for fp16
|
| 227 |
+
assert torch.allclose(a16.float(), a32.float(), atol=5e-2, rtol=5e-2)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
# ----------------------------
|
| 231 |
+
# D. Edge cases
|
| 232 |
+
# ----------------------------
|
| 233 |
+
def test_zero_length_sequences():
|
| 234 |
+
B, H, D = 2, 3, 8
|
| 235 |
+
# Sq==0
|
| 236 |
+
q, k, v = _rand(B, H, 0, 5, D)
|
| 237 |
+
a, p = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 238 |
+
assert a.shape == (B, H, 0, D) and p.shape == (B, H, 0, 5)
|
| 239 |
+
# Sk==0
|
| 240 |
+
q, k, v = _rand(B, H, 4, 0, D)
|
| 241 |
+
a, p = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 242 |
+
assert a.shape == (B, H, 4, D) and p.shape == (B, H, 4, 0)
|
| 243 |
+
assert torch.allclose(a, torch.zeros_like(a))
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def test_singleton_softmax_is_one():
|
| 247 |
+
q = torch.randn(1, 1, 1, 1)
|
| 248 |
+
k = torch.randn(1, 1, 1, 1)
|
| 249 |
+
v = torch.randn(1, 1, 1, 1)
|
| 250 |
+
_, p = calculate_attention(q, k, v, mask=None, return_probs=True)
|
| 251 |
+
assert torch.allclose(p, torch.ones_like(p), atol=1e-6)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# ----------------------------
|
| 255 |
+
# E. Autograd & determinism
|
| 256 |
+
# ----------------------------
|
| 257 |
+
def test_gradients_flow_no_inplace_breakage():
|
| 258 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 259 |
+
q = torch.randn(B, H, Sq, D, requires_grad=True)
|
| 260 |
+
k = torch.randn(B, H, Sk, D, requires_grad=True)
|
| 261 |
+
v = torch.randn(B, H, Sk, D, requires_grad=True)
|
| 262 |
+
out = calculate_attention(q, k, v, mask=None)
|
| 263 |
+
loss = out.pow(2).sum()
|
| 264 |
+
loss.backward()
|
| 265 |
+
for t in (q, k, v):
|
| 266 |
+
assert t.grad is not None and torch.isfinite(t.grad).all()
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def test_deterministic_best_effort_same_seed():
|
| 270 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 271 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 272 |
+
torch.manual_seed(42)
|
| 273 |
+
a1 = calculate_attention(q, k, v, mask=None, deterministic=True)
|
| 274 |
+
torch.manual_seed(42)
|
| 275 |
+
a2 = calculate_attention(q, k, v, mask=None, deterministic=True)
|
| 276 |
+
assert torch.allclose(a1, a2)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# ----------------------------
|
| 280 |
+
# F. Parity with PyTorch SDPA (when available)
|
| 281 |
+
# ----------------------------
|
| 282 |
+
def test_sdpa_parity_if_available():
|
| 283 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 284 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 285 |
+
try:
|
| 286 |
+
sdpa = F.scaled_dot_product_attention(
|
| 287 |
+
q, k, v, dropout_p=0.0, attn_mask=None, is_causal=False
|
| 288 |
+
)
|
| 289 |
+
manual = calculate_attention(q, k, v, mask=None)
|
| 290 |
+
assert torch.allclose(sdpa, manual, atol=1e-5, rtol=1e-4)
|
| 291 |
+
except Exception:
|
| 292 |
+
# Older PyTorch: skip
|
| 293 |
+
pass
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ----------------------------
|
| 297 |
+
# G. Performance smoke (sanity only)
|
| 298 |
+
# ----------------------------
|
| 299 |
+
def test_perf_smoke_runs_reasonably():
|
| 300 |
+
# Not asserting timing; just ensure no OOM or pathological slowdowns
|
| 301 |
+
q, k, v = _rand(B=1, H=8, Sq=1024, Sk=1024, D=64)
|
| 302 |
+
_ = calculate_attention(q, k, v, mask=None, return_probs=False)
|
| 303 |
+
q, k, v = _rand(B=2, H=8, Sq=1536, Sk=1536, D=64)
|
| 304 |
+
_ = calculate_attention(q, k, v, mask=None, return_probs=False)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ----------------------------
|
| 308 |
+
# H. Device behavior
|
| 309 |
+
# ----------------------------
|
| 310 |
+
def test_mask_on_cpu_qkv_on_gpu_autofix_or_skip():
|
| 311 |
+
if not torch.cuda.is_available():
|
| 312 |
+
pytest.skip("CUDA not available")
|
| 313 |
+
|
| 314 |
+
prev = torch.are_deterministic_algorithms_enabled()
|
| 315 |
+
try:
|
| 316 |
+
# Disable determinism *only for this test*
|
| 317 |
+
if prev:
|
| 318 |
+
torch.use_deterministic_algorithms(False)
|
| 319 |
+
|
| 320 |
+
q, k, v = _rand(device="cuda")
|
| 321 |
+
mask = torch.zeros(q.shape[0], 1, 1, k.shape[2], dtype=torch.bool) # CPU
|
| 322 |
+
attn, probs = calculate_attention(q, k, v, mask, return_probs=True)
|
| 323 |
+
assert attn.device.type == "cuda"
|
| 324 |
+
assert probs.device.type == "cuda"
|
| 325 |
+
finally:
|
| 326 |
+
# restore whatever the global setting was
|
| 327 |
+
torch.use_deterministic_algorithms(prev)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def test_qkv_device_mismatch_raises_clear_error():
|
| 331 |
+
if not torch.cuda.is_available():
|
| 332 |
+
pytest.skip("CUDA not available")
|
| 333 |
+
|
| 334 |
+
prev = torch.are_deterministic_algorithms_enabled()
|
| 335 |
+
try:
|
| 336 |
+
if prev:
|
| 337 |
+
torch.use_deterministic_algorithms(False)
|
| 338 |
+
|
| 339 |
+
q, k, v = _rand(device="cuda")
|
| 340 |
+
k = k.cpu() # force mismatch
|
| 341 |
+
with pytest.raises(RuntimeError) as ei:
|
| 342 |
+
_ = calculate_attention(q, k, v, mask=None)
|
| 343 |
+
msg = str(ei.value)
|
| 344 |
+
assert (
|
| 345 |
+
"q/k/v must be on the same device" in msg # our explicit check
|
| 346 |
+
or "Expected all tensors to be on the same device" in msg # PyTorch matmul
|
| 347 |
+
)
|
| 348 |
+
finally:
|
| 349 |
+
torch.use_deterministic_algorithms(prev)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
# ----------------------------
|
| 353 |
+
# I. Mask broadcastability
|
| 354 |
+
# ----------------------------
|
| 355 |
+
def test_boolean_mask_broadcast_variants_equivalent():
|
| 356 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 357 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 358 |
+
m_exact = torch.zeros(B, H, Sq, Sk, dtype=torch.bool)
|
| 359 |
+
m_B1_1Sk = torch.zeros(B, 1, 1, Sk, dtype=torch.bool)
|
| 360 |
+
m_11SqSk = torch.zeros(1, 1, Sq, Sk, dtype=torch.bool)
|
| 361 |
+
|
| 362 |
+
a_exact, p_exact = calculate_attention(q, k, v, m_exact, return_probs=True)
|
| 363 |
+
a1, p1 = calculate_attention(q, k, v, m_B1_1Sk, return_probs=True)
|
| 364 |
+
a2, p2 = calculate_attention(q, k, v, m_11SqSk, return_probs=True)
|
| 365 |
+
|
| 366 |
+
assert torch.allclose(a1, a_exact, atol=1e-6, rtol=1e-5)
|
| 367 |
+
assert torch.allclose(p1, p_exact, atol=1e-6, rtol=1e-5)
|
| 368 |
+
assert torch.allclose(a2, a_exact, atol=1e-6, rtol=1e-5)
|
| 369 |
+
assert torch.allclose(p2, p_exact, atol=1e-6, rtol=1e-5)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def test_additive_mask_broadcast_variants_equivalent():
|
| 373 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 374 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 375 |
+
m_exact = torch.zeros(B, H, Sq, Sk, dtype=torch.float32)
|
| 376 |
+
m_111Sk = torch.zeros(1, 1, 1, Sk, dtype=torch.float32)
|
| 377 |
+
m_B1Sq1 = torch.zeros(B, 1, Sq, 1, dtype=torch.float32)
|
| 378 |
+
|
| 379 |
+
a_exact, p_exact = calculate_attention(q, k, v, m_exact, return_probs=True)
|
| 380 |
+
a1, p1 = calculate_attention(q, k, v, m_111Sk, return_probs=True)
|
| 381 |
+
a2, p2 = calculate_attention(q, k, v, m_B1Sq1, return_probs=True)
|
| 382 |
+
|
| 383 |
+
assert torch.allclose(a1, a_exact, atol=1e-6, rtol=1e-5)
|
| 384 |
+
assert torch.allclose(p1, p_exact, atol=1e-6, rtol=1e-5)
|
| 385 |
+
assert torch.allclose(a2, a_exact, atol=1e-6, rtol=1e-5)
|
| 386 |
+
assert torch.allclose(p2, p_exact, atol=1e-6, rtol=1e-5)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def test_non_broadcastable_mask_raises_value_error():
|
| 390 |
+
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 391 |
+
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 392 |
+
bad = torch.zeros(
|
| 393 |
+
B, H, Sq, dtype=torch.bool
|
| 394 |
+
) # missing last dim => not broadcastable to (B,H,Sq,Sk)
|
| 395 |
+
with pytest.raises(ValueError) as ei:
|
| 396 |
+
_ = calculate_attention(q, k, v, bad)
|
| 397 |
+
assert "not broadcastable" in str(ei.value)
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
###___join_heads___###
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def test_join_heads_type_error():
|
| 404 |
+
with pytest.raises(TypeError):
|
| 405 |
+
join_heads([1, 2, 3]) # not a tensor
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
def test_join_heads_dim_error():
|
| 409 |
+
with pytest.raises(ValueError):
|
| 410 |
+
join_heads(torch.randn(2, 3, 4)) # only 3D
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def test_join_heads_shape_values():
|
| 414 |
+
x = torch.randn(2, 0, 4, 5) # zero heads
|
| 415 |
+
y = join_heads(x)
|
| 416 |
+
assert y.shape == (2, 4, 0)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def test_join_heads_roundtrip():
|
| 420 |
+
B, H, T, Dh = 2, 3, 5, 7
|
| 421 |
+
x = torch.randn(B, H, T, Dh)
|
| 422 |
+
y = join_heads(x)
|
| 423 |
+
assert y.shape == (B, T, H * Dh)
|
| 424 |
+
# Check element mapping correctness
|
| 425 |
+
for b in range(B):
|
| 426 |
+
for h in range(H):
|
| 427 |
+
for t in range(T):
|
| 428 |
+
for d in range(Dh):
|
| 429 |
+
assert torch.allclose(y[b, t, h * Dh + d], x[b, h, t, d])
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
###___sinusoidal_positional_encoding___###
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@pytest.mark.parametrize("seq_len,dim", [(0, 1), (1, 1), (1, 2), (7, 4), (17, 33)])
|
| 436 |
+
def test_shapes_and_dtypes(seq_len, dim):
|
| 437 |
+
# Try a few dtypes; fp16 on CPU can be quirky, so we gate it on CUDA.
|
| 438 |
+
dtypes = [torch.float32, torch.bfloat16]
|
| 439 |
+
if torch.cuda.is_available():
|
| 440 |
+
dtypes.append(torch.float16)
|
| 441 |
+
|
| 442 |
+
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
|
| 443 |
+
for dtype in dtypes:
|
| 444 |
+
pe = sinusoidal_positional_encoding(seq_len, dim, dtype=dtype, device=device)
|
| 445 |
+
assert pe.shape == (seq_len, dim)
|
| 446 |
+
assert pe.dtype == dtype
|
| 447 |
+
# Returned tensor should be a constant table (no gradients)
|
| 448 |
+
assert pe.requires_grad is False
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def test_values_match_reference_small_case():
|
| 452 |
+
# Small deterministic comparison against the textbook formula.
|
| 453 |
+
seq_len, dim = 4, 6
|
| 454 |
+
base = 10_000.0
|
| 455 |
+
pe = sinusoidal_positional_encoding(seq_len, dim, dtype=torch.float32, device="cpu", base=base)
|
| 456 |
+
|
| 457 |
+
ref = torch.zeros_like(pe)
|
| 458 |
+
# Fill sin/cos pairs explicitly
|
| 459 |
+
for pos in range(seq_len):
|
| 460 |
+
for i in range(0, dim // 2):
|
| 461 |
+
denom = base ** (2 * i / dim)
|
| 462 |
+
ref[pos, 2 * i] = math.sin(pos / denom)
|
| 463 |
+
ref[pos, 2 * i + 1] = math.cos(pos / denom)
|
| 464 |
+
|
| 465 |
+
# Compare only the complete sin/cos pairs (even columns and their cos twins)
|
| 466 |
+
paired = (dim // 2) * 2
|
| 467 |
+
assert torch.allclose(pe[:, :paired], ref[:, :paired], atol=1e-6, rtol=1e-6)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@pytest.mark.parametrize("dim", [1, 3, 8])
|
| 471 |
+
def test_odd_dim_last_column_zero(dim):
|
| 472 |
+
seq_len = 5
|
| 473 |
+
pe = sinusoidal_positional_encoding(seq_len, dim)
|
| 474 |
+
if dim % 2 == 1:
|
| 475 |
+
assert torch.allclose(
|
| 476 |
+
pe[:, -1],
|
| 477 |
+
torch.zeros(seq_len, dtype=pe.dtype, device=pe.device),
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
def test_offset_windowing_equivalence():
|
| 482 |
+
# pe[pos] with offset should match a slice of a longer table without offset
|
| 483 |
+
seq_len, dim, offset = 8, 6, 5
|
| 484 |
+
base_long = sinusoidal_positional_encoding(seq_len + offset, dim)
|
| 485 |
+
win = sinusoidal_positional_encoding(seq_len, dim, offset=offset)
|
| 486 |
+
assert torch.allclose(win, base_long[offset : offset + seq_len])
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def test_determinism():
|
| 490 |
+
a = sinusoidal_positional_encoding(32, 64)
|
| 491 |
+
b = sinusoidal_positional_encoding(32, 64)
|
| 492 |
+
assert torch.allclose(a, b)
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
def test_invalid_inputs():
|
| 496 |
+
with pytest.raises(ValueError):
|
| 497 |
+
sinusoidal_positional_encoding(-1, 8)
|
| 498 |
+
with pytest.raises(ValueError):
|
| 499 |
+
sinusoidal_positional_encoding(1, 0)
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
@pytest.mark.parametrize("base", [1_000.0, 10_000.0, 100_000.0])
|
| 503 |
+
def test_different_bases_change_values(base):
|
| 504 |
+
seq_len, dim = 6, 8
|
| 505 |
+
a = sinusoidal_positional_encoding(seq_len, dim, base=base)
|
| 506 |
+
b = sinusoidal_positional_encoding(seq_len, dim, base=base * 10)
|
| 507 |
+
# Different bases should not produce identical tables
|
| 508 |
+
assert not torch.allclose(a, b)
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required for strict fp16 checks")
|
| 512 |
+
def test_cuda_half_precision_support():
|
| 513 |
+
seq_len, dim = 12, 16
|
| 514 |
+
pe = sinusoidal_positional_encoding(seq_len, dim, dtype=torch.float16, device="cuda")
|
| 515 |
+
assert pe.device.type == "cuda"
|
| 516 |
+
assert pe.dtype == torch.float16
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
###___combine_masks___###
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def test_both_none_returns_none():
|
| 523 |
+
assert combine_masks(None, None) is None
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def test_one_none_returns_other():
|
| 527 |
+
m = torch.tensor([[True, False]])
|
| 528 |
+
assert torch.equal(combine_masks(m, None), m)
|
| 529 |
+
assert torch.equal(combine_masks(None, m), m)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def test_or_combination():
|
| 533 |
+
m1 = torch.tensor([[True, False]])
|
| 534 |
+
m2 = torch.tensor([[False, True]])
|
| 535 |
+
expected = torch.tensor([[True, True]])
|
| 536 |
+
out = combine_masks(m1, m2)
|
| 537 |
+
assert torch.equal(out, expected)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def test_shape_mismatch_error():
|
| 541 |
+
m1 = torch.ones(2, 2, dtype=torch.bool)
|
| 542 |
+
m2 = torch.ones(3, 3, dtype=torch.bool)
|
| 543 |
+
with pytest.raises(ValueError):
|
| 544 |
+
combine_masks(m1, m2)
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def test_broadcastable_masks():
|
| 548 |
+
m1 = torch.tensor([[True, False, True]])
|
| 549 |
+
m2 = torch.tensor([[False]]) # broadcast across
|
| 550 |
+
out = combine_masks(m1, m2)
|
| 551 |
+
assert torch.equal(out, m1 | m2)
|
| 552 |
+
|
| 553 |
+
|
| 554 |
+
def test_dtype_conversion():
|
| 555 |
+
# Non-bool masks still should work if they are 0/1 ints
|
| 556 |
+
m1 = torch.tensor([[1, 0]], dtype=torch.int32)
|
| 557 |
+
m2 = torch.tensor([[0, 1]], dtype=torch.int32)
|
| 558 |
+
out = combine_masks(m1.bool(), m2.bool())
|
| 559 |
+
expected = torch.tensor([[True, True]])
|
| 560 |
+
assert torch.equal(out, expected)
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
###___shift_right___###
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def test_basic_shift_single_batch():
|
| 567 |
+
labels = torch.tensor([[5, 6, 7]], dtype=torch.long)
|
| 568 |
+
out = shift_right(labels, bos_id=1, pad_id=0)
|
| 569 |
+
assert torch.equal(out, torch.tensor([[1, 5, 6]], dtype=torch.long))
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def test_basic_shift_vector_input():
|
| 573 |
+
labels = torch.tensor([10, 11, 12], dtype=torch.long)
|
| 574 |
+
out = shift_right(labels, bos_id=2, pad_id=0)
|
| 575 |
+
assert out.shape == (1, 3)
|
| 576 |
+
assert torch.equal(out, torch.tensor([[2, 10, 11]], dtype=torch.long))
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
def test_batch_shift():
|
| 580 |
+
labels = torch.tensor([[5, 6, 7], [8, 9, 10]], dtype=torch.long)
|
| 581 |
+
out = shift_right(labels, bos_id=3, pad_id=0)
|
| 582 |
+
expected = torch.tensor([[3, 5, 6], [3, 8, 9]], dtype=torch.long)
|
| 583 |
+
assert torch.equal(out, expected)
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def test_ignores_are_padded_after_shift():
|
| 587 |
+
# -100 in labels must never appear in inputs; becomes PAD after shifting.
|
| 588 |
+
labels = torch.tensor([[5, -100, 7, -100]], dtype=torch.long)
|
| 589 |
+
out = shift_right(labels, bos_id=4, pad_id=99)
|
| 590 |
+
# positions 2 and 4 in inputs come from -100 -> PAD(99)
|
| 591 |
+
expected = torch.tensor([[4, 5, 99, 7]], dtype=torch.long)
|
| 592 |
+
assert torch.equal(out, expected)
|
| 593 |
+
assert (out == -100).sum() == 0
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
def test_custom_pad_and_bos_ids():
|
| 597 |
+
labels = torch.tensor([[42, 43, -100, 45]], dtype=torch.long)
|
| 598 |
+
out = shift_right(labels, bos_id=7, pad_id=3)
|
| 599 |
+
expected = torch.tensor([[7, 42, 43, 3]], dtype=torch.long)
|
| 600 |
+
assert torch.equal(out, expected)
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def test_zero_length_raises():
|
| 604 |
+
with pytest.raises(ValueError):
|
| 605 |
+
shift_right(torch.zeros((2, 0), dtype=torch.long))
|
| 606 |
+
|
| 607 |
+
|
| 608 |
+
def test_wrong_rank_raises():
|
| 609 |
+
with pytest.raises(ValueError):
|
| 610 |
+
shift_right(torch.zeros((2, 3, 4), dtype=torch.long))
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def test_dtype_device_preserved():
|
| 614 |
+
device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
|
| 615 |
+
labels = torch.tensor([[1, 2, 3]], device=device)
|
| 616 |
+
out = shift_right(labels, bos_id=9, pad_id=0)
|
| 617 |
+
assert out.device == device
|
| 618 |
+
assert out.dtype == torch.long
|
| 619 |
+
assert torch.equal(out, torch.tensor([[9, 1, 2]], device=device, dtype=torch.long))
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
def test_no_inplace_on_labels():
|
| 623 |
+
labels = torch.tensor([[5, 6, -100]], dtype=torch.long)
|
| 624 |
+
labels_clone = labels.clone()
|
| 625 |
+
_ = shift_right(labels, bos_id=1, pad_id=0)
|
| 626 |
+
assert torch.equal(labels, labels_clone)
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
###___sample_from_logits___###
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def test_greedy_equals_argmax():
|
| 633 |
+
torch.manual_seed(0)
|
| 634 |
+
logits = torch.randn(4, 7)
|
| 635 |
+
ids = sample_from_logits(logits) # greedy
|
| 636 |
+
ref = torch.argmax(F.softmax(logits, dim=-1), dim=-1)
|
| 637 |
+
assert torch.equal(ids, ref)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def test_temperature_effect_no_sampling():
|
| 641 |
+
# Greedy with temperature should leave argmax unchanged (monotonic transform)
|
| 642 |
+
logits = torch.tensor([[0.1, 1.2, 0.3]], dtype=torch.float32)
|
| 643 |
+
ids_t1 = sample_from_logits(logits, do_sample=False, temperature=1.0)
|
| 644 |
+
ids_t05 = sample_from_logits(logits, do_sample=False, temperature=0.5)
|
| 645 |
+
assert torch.equal(ids_t1, ids_t05) # argmax unchanged
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def test_top_k_limits_candidates():
|
| 649 |
+
logits = torch.tensor([[1.0, 0.9, 0.1, -1.0]], dtype=torch.float32)
|
| 650 |
+
# with top_k=1 greedy must select the max (index 0)
|
| 651 |
+
ids = sample_from_logits(logits, do_sample=False, top_k=1)
|
| 652 |
+
assert torch.equal(ids, torch.tensor([0]))
|
| 653 |
+
# with sampling, and top_k=1, the only candidate is index 0
|
| 654 |
+
gen = torch.Generator().manual_seed(123)
|
| 655 |
+
ids_s = sample_from_logits(logits, do_sample=True, top_k=1, rng=gen)
|
| 656 |
+
assert torch.equal(ids_s, torch.tensor([0]))
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def test_top_p_nucleus_filters_tail():
|
| 660 |
+
logits = torch.tensor([[5.0, 4.0, 3.0, -5.0]], dtype=torch.float32) # probs heavily skewed
|
| 661 |
+
ids = sample_from_logits(logits, do_sample=False, top_p=0.6) # should keep a minimal head
|
| 662 |
+
assert ids.item() in (0, 1) # greediest is 0 anyway; check no crash
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def test_min_tokens_to_keep_safety():
|
| 666 |
+
logits = torch.tensor([[0.0, 0.0, 0.0, 0.0]], dtype=torch.float32)
|
| 667 |
+
# Even with very small top_p, we keep at least 1 token
|
| 668 |
+
ids = sample_from_logits(logits, do_sample=False, top_p=0.01, min_tokens_to_keep=1)
|
| 669 |
+
assert ids.numel() == 1
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def test_allow_and_deny_lists():
|
| 673 |
+
logits = torch.tensor([[0.1, 2.0, 0.3, 3.0]], dtype=torch.float32) # best is idx=3
|
| 674 |
+
# Allow only {0,2} → best among those is 2
|
| 675 |
+
ids = sample_from_logits(logits, do_sample=False, allowed_tokens=[0, 2])
|
| 676 |
+
assert torch.equal(ids, torch.tensor([2]))
|
| 677 |
+
# Deny {3} → next best is 1
|
| 678 |
+
ids = sample_from_logits(logits, do_sample=False, disallowed_tokens=[3])
|
| 679 |
+
assert torch.equal(ids, torch.tensor([1]))
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
def test_rng_reproducibility_sampling():
|
| 683 |
+
logits = torch.tensor([[1.0, 1.0, 1.0, 1.0]], dtype=torch.float32) # uniform
|
| 684 |
+
g1 = torch.Generator().manual_seed(42)
|
| 685 |
+
g2 = torch.Generator().manual_seed(42)
|
| 686 |
+
s1 = sample_from_logits(logits, do_sample=True, rng=g1)
|
| 687 |
+
s2 = sample_from_logits(logits, do_sample=True, rng=g2)
|
| 688 |
+
assert torch.equal(s1, s2)
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def test_device_is_preserved_cpu_cuda():
|
| 692 |
+
target_device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
|
| 693 |
+
logits = torch.randn(3, 5, device=target_device)
|
| 694 |
+
out = sample_from_logits(logits) # greedy by default
|
| 695 |
+
assert out.device == target_device
|
| 696 |
+
assert out.dtype == torch.long
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
def test_handles_higher_rank_logits_flattening():
|
| 700 |
+
# [B, T, V] -> flattened internally; here we just ensure no crash and correct shape
|
| 701 |
+
logits = torch.randn(2, 3, 7)
|
| 702 |
+
out = sample_from_logits(logits) # returns [B*T]
|
| 703 |
+
assert out.shape == (2 * 3,)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
###___create_causal_mask___###
|
| 707 |
+
|
| 708 |
+
|
| 709 |
+
def test_mask_shape_and_dtype_cpu():
|
| 710 |
+
L = 5
|
| 711 |
+
mask = create_causal_mask(L)
|
| 712 |
+
assert mask.shape == (1, 1, L, L)
|
| 713 |
+
assert mask.dtype == torch.bool
|
| 714 |
+
assert mask.device.type == "cpu"
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
def test_mask_upper_triangle():
|
| 718 |
+
L = 4
|
| 719 |
+
mask = create_causal_mask(L)
|
| 720 |
+
# For causal mask: mask[i,j] == True if j > i
|
| 721 |
+
mat = mask[0, 0].to(torch.int)
|
| 722 |
+
expected = torch.tensor(
|
| 723 |
+
[
|
| 724 |
+
[0, 1, 1, 1],
|
| 725 |
+
[0, 0, 1, 1],
|
| 726 |
+
[0, 0, 0, 1],
|
| 727 |
+
[0, 0, 0, 0],
|
| 728 |
+
]
|
| 729 |
+
)
|
| 730 |
+
assert torch.equal(mat, expected)
|
| 731 |
+
|
| 732 |
+
|
| 733 |
+
@pytest.mark.parametrize("dtype", [torch.bool, torch.float32])
|
| 734 |
+
def test_mask_dtype(dtype):
|
| 735 |
+
L = 3
|
| 736 |
+
mask = create_causal_mask(L, dtype=dtype)
|
| 737 |
+
assert mask.dtype == dtype
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
|
| 741 |
+
def test_device_cuda():
|
| 742 |
+
L = 6
|
| 743 |
+
mask = create_causal_mask(L, device=torch.device("cuda"))
|
| 744 |
+
assert mask.device.type == "cuda"
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
def test_invalid_length_raises():
|
| 748 |
+
with pytest.raises(ValueError):
|
| 749 |
+
create_causal_mask(0)
|