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

added training et new improved model structure

Browse files
Files changed (50) hide show
  1. architecture.txt +1 -1
  2. configs/dataset/large_dataset.yaml +2 -0
  3. configs/dataset/medium_dataset.yaml +2 -0
  4. configs/dataset/small_dataset.yaml +2 -0
  5. configs/generation/generation.yaml +9 -0
  6. configs/infer_mode.yaml +8 -7
  7. configs/model/large_model.yaml +14 -0
  8. configs/model/medium_model.yaml +14 -0
  9. configs/model/small_model.yaml +14 -0
  10. configs/model/test_model.yaml +8 -4
  11. configs/optim/Adamw.yaml +6 -0
  12. configs/optimizer/optimizer.yaml +0 -0
  13. configs/runtime/local.yaml +7 -0
  14. configs/runtime/runtime.yaml +0 -3
  15. configs/sched/cosine.yaml +5 -0
  16. configs/tokenizer/bpe_16k.yaml +15 -0
  17. configs/tokenizer/bpe_32k.yaml +15 -0
  18. configs/tokenizer/bpe_4k.yaml +15 -0
  19. configs/tokenizer/bpe_8k.yaml +15 -0
  20. configs/train_mode.yaml +13 -9
  21. configs/trainer/default.yaml +14 -0
  22. environment.yml +5 -0
  23. pyproject.toml +8 -1
  24. requirements.txt +0 -0
  25. scripts/inference.py +45 -22
  26. scripts/load_data.ipynb +388 -0
  27. scripts/server.py +26 -0
  28. scripts/tokenizer.ipynb +217 -0
  29. scripts/train.ipynb +987 -0
  30. scripts/train.py +0 -0
  31. src/transformer/__init__.py +1 -2
  32. src/transformer/configs.py +104 -40
  33. src/transformer/modules/attention.py +132 -105
  34. src/transformer/modules/decoder.py +171 -99
  35. src/transformer/modules/embedding.py +146 -121
  36. src/transformer/modules/encoder.py +108 -71
  37. src/transformer/modules/feedforward.py +66 -59
  38. src/transformer/modules/lm_head.py +54 -47
  39. src/transformer/transformer.py +435 -239
  40. src/transformer/utils.py +1042 -612
  41. tests/units/modules/test_attention.py +42 -23
  42. tests/units/modules/test_decoder.py +34 -18
  43. tests/units/modules/test_embedding.py +8 -8
  44. tests/units/modules/test_encoder.py +4 -3
  45. tests/units/test_transformer.py +137 -34
  46. tests/units/test_utils.py +37 -185
  47. tokenizer/bpe_16k.json +0 -0
  48. tokenizer/bpe_32k.json +0 -0
  49. tokenizer/bpe_4k.json +0 -0
  50. tokenizer/bpe_8k.json +0 -0
architecture.txt CHANGED
@@ -1,7 +1,7 @@
1
  Transformer:
2
  embedding:
3
  input_embedding (same for encoder and decoder)
4
- positional_embedding
5
  dropout
6
  encoder
7
  encoder_layer *N
 
1
  Transformer:
2
  embedding:
3
  input_embedding (same for encoder and decoder)
4
+ positional_embedding (learned or sinusoidal)
5
  dropout
6
  encoder
7
  encoder_layer *N
configs/dataset/large_dataset.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ name: large_dataset
2
+ path: ./data/processed/processed_large_dataset
configs/dataset/medium_dataset.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ name: medium_dataset
2
+ path: ./data/processed/processed_medium_dataset
configs/dataset/small_dataset.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ name: small_dataset
2
+ path: ./data/processed/processed_small_dataset
configs/generation/generation.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ max_new_tokens: 64
2
+ temperature: 1
3
+ top_k: 10
4
+ top_p: 0.6
5
+ do_sample: true
6
+ presence_penalty: 0.6
7
+ frequency_penalty: 0.2
8
+ no_repeat_ngram: 4
9
+ min_steps_before_eos: 2
configs/infer_mode.yaml CHANGED
@@ -1,10 +1,11 @@
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
 
1
  defaults:
2
  - model: test_model
3
+ - runtime: local
4
+ - tokenizer: bpe_8k
5
+ - generation: generation
6
  - _self_
7
+ input_text: "how are you today"
8
+ hydra:
9
+ run:
10
+ dir: .
11
+ output_subdir: null
 
configs/model/large_model.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: large_model
2
+ best_checkpoint_path: ./outputs/checkpoints/large_model/best.ckpt
3
+ latest_checkpoint_path: ./outputs/checkpoints/large_model/latest.ckpt
4
+ tokenizer: bpe_32k
5
+ d_model: 512
6
+ num_heads: 8
7
+ num_layers: 6
8
+ d_ff: 2048
9
+ dropout_rate: 0.1
10
+ vocab_size: 32768
11
+ max_seq_len: 256
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
configs/model/medium_model.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: medium_model
2
+ best_checkpoint_path: ./outputs/checkpoints/medium_model/best.ckpt
3
+ latest_checkpoint_path: ./outputs/checkpoints/medium_model/latest.ckpt
4
+ tokenizer: bpe_16k
5
+ d_model: 512
6
+ num_heads: 8
7
+ num_layers: 6
8
+ d_ff: 2048
9
+ dropout_rate: 0.1
10
+ vocab_size: 16384
11
+ max_seq_len: 256
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
configs/model/small_model.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: small_model
2
+ best_checkpoint_path: ./outputs/checkpoints/small_model/best.ckpt
3
+ latest_checkpoint_path: ./outputs/checkpoints/small_model/latest.ckpt
4
+ tokenizer: bpe_8k
5
+ d_model: 256
6
+ num_heads: 8
7
+ num_layers: 4
8
+ d_ff: 1024
9
+ dropout_rate: 0.1
10
+ vocab_size: 8192
11
+ max_seq_len: 128
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
configs/model/test_model.yaml CHANGED
@@ -1,9 +1,13 @@
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
 
1
+ name: test_model
2
+ best_checkpoint_path: ./outputs/checkpoints/test_model/best.pt
3
+ latest_checkpoint_path: ./outputs/checkpoints/test_model/latest.pt
4
+ tokenizer: bpe_8k
5
+ d_model: 512
6
+ num_heads: 8
7
+ num_layers: 8
8
  d_ff: 1024
9
  dropout_rate: 0.1
10
+ vocab_size: 8192
11
  max_seq_len: 128
12
  pad_id: 0
13
  bos_id: 1
configs/optim/Adamw.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ name: AdamW
2
+ lr: 8e-4
3
+ betas: [0.9, 0.98]
4
+ eps: 1e-8
5
+ weight_decay: 0.08
6
+ no_decay_on_bias_norm_embed: true
configs/optimizer/optimizer.yaml DELETED
File without changes
configs/runtime/local.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ seed: 42
2
+ device: cuda
3
+ output_dir: ./outputs/
4
+ data_dir: ./data/
5
+ tokenizer_dir: ./tokenizer/
6
+ cache_dir: ./data/raw
7
+ checkpoint_path: ./outputs/checkpoints/
configs/runtime/runtime.yaml DELETED
@@ -1,3 +0,0 @@
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/sched/cosine.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ name: cosine_warmup
2
+ warmup_ratio: 0.08
3
+ min_lr_ratio: 0.1
4
+ max_lr_ratio: 1
5
+ hold_min: true
configs/tokenizer/bpe_16k.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: bpe_16k
2
+ path: ./tokenizer/bpe_16k.json
3
+ corpus: ./data/corpus/corpus.txt
4
+ dataset: concatenated_dataset
5
+ vocab_size: 16384
6
+ max_seq_len: 128
7
+ pad_token: <pad>
8
+ bos_token: <bos>
9
+ eos_token: <eos>
10
+ unk_token: <unk>
11
+ special_tokens: ["<pad>", "<bos>", "<eos>", "<unk>"]
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ unk_id: 3
configs/tokenizer/bpe_32k.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: bpe_32k
2
+ path: ./tokenizer/bpe_32k.json
3
+ corpus: ./data/corpus/corpus.txt
4
+ dataset: concatenated_dataset
5
+ vocab_size: 32768
6
+ max_seq_len: 256
7
+ pad_token: <pad>
8
+ bos_token: <bos>
9
+ eos_token: <eos>
10
+ unk_token: <unk>
11
+ special_tokens: ["<pad>", "<bos>", "<eos>", "<unk>"]
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ unk_id: 3
configs/tokenizer/bpe_4k.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: bpe_4k
2
+ path: ./tokenizer/bpe_4k.json
3
+ corpus: ./data/corpus/corpus.txt
4
+ dataset: concatenated_dataset
5
+ vocab_size: 4096
6
+ max_seq_len: 128
7
+ pad_token: <pad>
8
+ bos_token: <bos>
9
+ eos_token: <eos>
10
+ unk_token: <unk>
11
+ special_tokens: ["<pad>", "<bos>", "<eos>", "<unk>"]
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ unk_id: 3
configs/tokenizer/bpe_8k.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: bpe_8k
2
+ path: ./tokenizer/bpe_8k.json
3
+ corpus: ./data/corpus/corpus.txt
4
+ dataset: concatenated_dataset
5
+ vocab_size: 8192
6
+ max_seq_len: 128
7
+ pad_token: <pad>
8
+ bos_token: <bos>
9
+ eos_token: <eos>
10
+ unk_token: <unk>
11
+ special_tokens: ["<pad>", "<bos>", "<eos>", "<unk>"]
12
+ pad_id: 0
13
+ bos_id: 1
14
+ eos_id: 2
15
+ unk_id: 3
configs/train_mode.yaml CHANGED
@@ -1,9 +1,13 @@
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"
 
 
 
 
 
1
+ defaults:
2
+ - model: test_model
3
+ - tokenizer: bpe_16k
4
+ - dataset: medium_dataset
5
+ - optim: Adamw
6
+ - sched: cosine
7
+ - runtime: local
8
+ - trainer: default
9
+ - _self_
10
+ hydra:
11
+ run:
12
+ dir: . # run in current directory
13
+ output_subdir: null
configs/trainer/default.yaml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ epochs: 80
2
+ batch_size: 128
3
+ shuffle: true
4
+ precision: "bf16"
5
+ gradient_accumulation: 2
6
+ clip_grad_norm: 0.5
7
+ eval_interval: 1
8
+ save_interval: 1
9
+ debug_interval: 1000
10
+ mixed_precision: true
11
+ gradient_checkpointing: true
12
+ label_smoothing: 0.05
13
+ export_best_to_model_path: false
14
+ resume: start_over
environment.yml CHANGED
@@ -10,3 +10,8 @@ dependencies:
10
  - transformers
11
  - omegaconf
12
  - hydra-core
 
 
 
 
 
 
10
  - transformers
11
  - omegaconf
12
  - hydra-core
13
+ - jupyterlab
14
+ - tokenizers
15
+ - datasets
16
+ - fastapi
17
+ - uvicorn[standard]
pyproject.toml CHANGED
@@ -5,10 +5,13 @@ 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]
@@ -20,6 +23,10 @@ dev = [
20
  "mypy",
21
  "pre-commit",
22
  ]
 
 
 
 
23
 
24
  [tool.setuptools]
25
  package-dir = {"" = "src"}
 
5
  readme = "README.md"
6
  requires-python = ">=3.10"
7
  dependencies = [
8
+ "torch>=2.6",
9
  "transformers",
10
  "omegaconf",
11
  "hydra-core",
12
+ "jupyterlab",
13
+ "tokenizers",
14
+ "datasets",
15
  ]
16
 
17
  [project.optional-dependencies]
 
23
  "mypy",
24
  "pre-commit",
25
  ]
26
+ server = [
27
+ "fastapi",
28
+ "uvicorn[standard]",
29
+ ]
30
 
31
  [tool.setuptools]
32
  package-dir = {"" = "src"}
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
scripts/inference.py CHANGED
@@ -1,30 +1,42 @@
 
 
 
 
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()
@@ -35,18 +47,29 @@ def main(cfg: DictConfig):
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__":
 
1
+ """CLI entry point for running transformer inference with deterministic sampling."""
2
+
3
+ from __future__ import annotations
4
+
5
  import hydra
6
  import torch
7
  from hydra.utils import to_absolute_path
8
  from omegaconf import DictConfig, OmegaConf
9
+ from transformers import PreTrainedTokenizerFast
10
 
11
+ from transformer import BasicEncoderDecoderTransformer
12
+ from transformer.configs import InferAppCfg, ModelCfg, TokenizerCfg
13
+ from transformer.utils import check_tokenizer_model_compatibility, set_global_seed
 
 
14
 
15
 
16
+ def inference(cfg: DictConfig):
 
17
  scfg_temp = OmegaConf.merge(OmegaConf.structured(InferAppCfg), cfg)
18
  scfg: InferAppCfg = OmegaConf.to_object(scfg_temp)
19
+ model_cfg = ModelCfg(**vars(scfg.model))
20
+ tokenizer_cfg = TokenizerCfg(**vars(scfg.tokenizer))
21
+
22
+ # Ensure reproducible sampling when requested
23
+ set_global_seed(scfg.runtime.seed)
24
+
25
+ # Compatibility checks
26
+ check_tokenizer_model_compatibility(model_cfg, tokenizer_cfg)
27
 
28
  transformer = BasicEncoderDecoderTransformer(model_cfg)
29
+ tokenizer = PreTrainedTokenizerFast(
30
+ tokenizer_file=to_absolute_path(tokenizer_cfg.path),
31
+ bos_token=cfg.tokenizer.bos_token,
32
+ eos_token=cfg.tokenizer.eos_token,
33
+ unk_token=cfg.tokenizer.unk_token,
34
+ pad_token=cfg.tokenizer.pad_token,
35
+ model_max_length=tokenizer_cfg.max_seq_len,
36
  )
37
 
38
+ if scfg.model.best_checkpoint_path:
39
+ ckpt = torch.load(to_absolute_path(scfg.model.best_checkpoint_path), map_location="cpu")
40
  state = ckpt.get("model_state_dict", ckpt)
41
  transformer.load_state_dict(state, strict=False)
42
  transformer.eval()
 
47
 
48
  encoded = tokenizer(text, padding=True, truncation=True, return_tensors="pt")
49
  src_ids = encoded["input_ids"]
50
+ src_padd_mask = encoded["attention_mask"] == 0
51
+
52
  tgt_ids = transformer.generate(
53
  src_ids,
54
  src_padd_mask,
55
+ max_new_tokens=scfg.generation.max_new_tokens,
56
+ temperature=scfg.generation.temperature,
57
+ top_k=scfg.generation.top_k,
58
+ top_p=scfg.generation.top_p,
59
+ do_sample=scfg.generation.do_sample,
60
+ presence_penalty=scfg.generation.presence_penalty,
61
+ frequency_penalty=scfg.generation.frequency_penalty,
62
+ no_repeat_ngram=scfg.generation.no_repeat_ngram,
63
+ min_steps_before_eos=scfg.generation.min_steps_before_eos,
64
+ seed=scfg.runtime.seed,
65
  )
66
+ output = tokenizer.batch_decode(tgt_ids, skip_special_tokens=False)
67
+ return output
68
+
69
+
70
+ @hydra.main(config_path="../configs", config_name="infer_mode", version_base=None)
71
+ def main(cfg: DictConfig):
72
+ print(inference(cfg))
73
 
74
 
75
  if __name__ == "__main__":
scripts/load_data.ipynb ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e2289881",
6
+ "metadata": {},
7
+ "source": [
8
+ "Downloading datasets"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "9ef193d1",
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "from datasets import load_dataset\n",
19
+ "\n",
20
+ "_ = load_dataset(\"ahazeemi/iwslt14-en-fr\", cache_dir=\"./data/raw\")\n",
21
+ "_ = load_dataset(\"Helsinki-NLP/europarl\", \"en-fr\", cache_dir=\"./data/raw\")\n",
22
+ "# _ = load_dataset(\"wmt/wmt14\", \"fr-en\", cache_dir=\"./data/raw\")"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "markdown",
27
+ "id": "cb48dbf1",
28
+ "metadata": {},
29
+ "source": [
30
+ "Loading datasets"
31
+ ]
32
+ },
33
+ {
34
+ "cell_type": "code",
35
+ "execution_count": 1,
36
+ "id": "2e0838f5",
37
+ "metadata": {},
38
+ "outputs": [
39
+ {
40
+ "name": "stderr",
41
+ "output_type": "stream",
42
+ "text": [
43
+ "/home/ala-boussoffara/miniconda3/envs/transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
44
+ " from .autonotebook import tqdm as notebook_tqdm\n",
45
+ "Generating train split: 179435 examples [00:00, 1366541.45 examples/s]\n",
46
+ "Generating validation split: 903 examples [00:00, 558869.19 examples/s]\n",
47
+ "Generating test split: 3666 examples [00:00, 876193.43 examples/s]\n",
48
+ "Generating train split: 2051014 examples [00:01, 2040864.91 examples/s]\n"
49
+ ]
50
+ }
51
+ ],
52
+ "source": [
53
+ "from datasets import load_dataset\n",
54
+ "\n",
55
+ "small_dataset = load_dataset(\"./data/raw/ahazeemi___iwslt14-en-fr\")\n",
56
+ "medium_dataset = load_dataset(\"./data/raw/Helsinki-NLP___europarl\")\n",
57
+ "# large_dataset = load_dataset(\"./data/raw/wmt___wmt14\")"
58
+ ]
59
+ },
60
+ {
61
+ "cell_type": "markdown",
62
+ "id": "cb452f91",
63
+ "metadata": {},
64
+ "source": [
65
+ "Preprocessing\n"
66
+ ]
67
+ },
68
+ {
69
+ "cell_type": "code",
70
+ "execution_count": 2,
71
+ "id": "ca4d1867",
72
+ "metadata": {},
73
+ "outputs": [
74
+ {
75
+ "name": "stderr",
76
+ "output_type": "stream",
77
+ "text": [
78
+ "Map: 100%|██████████| 179435/179435 [00:12<00:00, 14545.45 examples/s]\n",
79
+ "Map: 100%|██████████| 903/903 [00:00<00:00, 13155.18 examples/s]\n",
80
+ "Map: 100%|██████████| 3666/3666 [00:00<00:00, 14699.07 examples/s]\n",
81
+ "Filter: 100%|██████████| 179435/179435 [00:01<00:00, 163853.20 examples/s]\n",
82
+ "Filter: 100%|██████████| 885/885 [00:00<00:00, 123014.38 examples/s]\n",
83
+ "Filter: 100%|██████████| 3620/3620 [00:00<00:00, 142652.68 examples/s]\n",
84
+ "Map: 100%|██████████| 2051014/2051014 [02:27<00:00, 13892.40 examples/s]\n",
85
+ "Filter: 100%|██████████| 2028907/2028907 [00:12<00:00, 163723.53 examples/s]\n",
86
+ "Saving the dataset (1/1 shards): 100%|██████████| 179435/179435 [00:00<00:00, 186519.65 examples/s]\n",
87
+ "Saving the dataset (1/1 shards): 100%|██████████| 885/885 [00:00<00:00, 142866.56 examples/s]\n",
88
+ "Saving the dataset (1/1 shards): 100%|██████████| 3620/3620 [00:00<00:00, 170674.57 examples/s]\n",
89
+ "Saving the dataset (3/3 shards): 100%|██████████| 1826016/1826016 [00:11<00:00, 159762.08 examples/s]\n",
90
+ "Saving the dataset (1/1 shards): 100%|██████████| 101445/101445 [00:00<00:00, 193029.72 examples/s]\n",
91
+ "Saving the dataset (1/1 shards): 100%|██████████| 101446/101446 [00:00<00:00, 191733.84 examples/s]\n"
92
+ ]
93
+ }
94
+ ],
95
+ "source": [
96
+ "import re\n",
97
+ "\n",
98
+ "import yaml\n",
99
+ "from datasets import DatasetDict\n",
100
+ "\n",
101
+ "from transformer.utils import set_global_seed\n",
102
+ "\n",
103
+ "with open(\"configs/runtime/local.yaml\", encoding=\"utf-8\") as cfg_file:\n",
104
+ " runtime_cfg = yaml.safe_load(cfg_file) or {}\n",
105
+ "seed = int(runtime_cfg.get(\"seed\", 0))\n",
106
+ "set_global_seed(seed)\n",
107
+ "\n",
108
+ "\n",
109
+ "def preprocess_data(example, min_len=2, max_len=256, length_ratio=2.0):\n",
110
+ " \"\"\"\n",
111
+ " Clean and filter a parallel translation pair (English-French).\n",
112
+ "\n",
113
+ " Args:\n",
114
+ " example (dict): {\"translation\": {\"en\": \"...\", \"fr\": \"...\"}}\n",
115
+ " min_len (int): Minimum tokenized length allowed.\n",
116
+ " max_len (int): Maximum tokenized length allowed.\n",
117
+ " length_ratio (float): Max src/tgt length ratio to keep.\n",
118
+ "\n",
119
+ " Returns:\n",
120
+ " dict | None: Cleaned translation dict, or None if filtered out.\n",
121
+ " \"\"\"\n",
122
+ "\n",
123
+ " src = example[\"translation\"][\"en\"]\n",
124
+ " tgt = example[\"translation\"][\"fr\"]\n",
125
+ "\n",
126
+ " # ----------------\n",
127
+ " # Basic cleanup\n",
128
+ " # ----------------\n",
129
+ " def clean_text(text: str) -> str:\n",
130
+ " # Remove HTML tags\n",
131
+ " text = re.sub(r\"<.*?>\", \" \", text)\n",
132
+ "\n",
133
+ " # Remove URLs and emails\n",
134
+ " text = re.sub(r\"http\\S+|www\\.\\S+\", \" \", text)\n",
135
+ " text = re.sub(r\"\\S+@\\S+\", \" \", text)\n",
136
+ "\n",
137
+ " # Normalize quotes and punctuation\n",
138
+ " text = re.sub(r\"[“”]\", '\"', text)\n",
139
+ " text = re.sub(r\"[‘’]\", \"'\", text)\n",
140
+ " text = re.sub(r\"([!?.,])\\1+\", r\"\\1\", text) # collapse repeated punct\n",
141
+ "\n",
142
+ " # Remove non-alphabetic junk (keep accents, digits, basic punct)\n",
143
+ " text = re.sub(r\"[^a-zA-ZÀ-ÖØ-öø-ÿ0-9\\s.,!?']\", \" \", text)\n",
144
+ "\n",
145
+ " # Collapse whitespace\n",
146
+ " text = re.sub(r\"\\s+\", \" \", text).strip()\n",
147
+ " return text.lower()\n",
148
+ "\n",
149
+ " src = clean_text(src)\n",
150
+ " tgt = clean_text(tgt)\n",
151
+ "\n",
152
+ " # ----------------\n",
153
+ " # Length filtering\n",
154
+ " # ----------------\n",
155
+ " src_len, tgt_len = len(src.split()), len(tgt.split())\n",
156
+ " if src_len < min_len or tgt_len < min_len:\n",
157
+ " return None\n",
158
+ " if src_len > max_len or tgt_len > max_len:\n",
159
+ " return None\n",
160
+ " if not (1 / length_ratio <= src_len / tgt_len <= length_ratio):\n",
161
+ " return None\n",
162
+ "\n",
163
+ " # ----------------\n",
164
+ " # Optional sanity check: drop if empty after cleaning\n",
165
+ " # ----------------\n",
166
+ " if not src or not tgt:\n",
167
+ " return None\n",
168
+ "\n",
169
+ " return {\"en\": src, \"fr\": tgt}\n",
170
+ "\n",
171
+ "\n",
172
+ "processed_small_dataset = small_dataset.map(preprocess_data).filter(lambda x: x is not None)\n",
173
+ "processed_medium_dataset = medium_dataset.map(preprocess_data).filter(lambda x: x is not None)\n",
174
+ "# processed_large_dataset = large_dataset.map(preprocess_data).filter(lambda x: x is not None)\n",
175
+ "\n",
176
+ "splits = processed_medium_dataset[\"train\"].train_test_split(test_size=0.1, seed=seed)\n",
177
+ "train_split = splits[\"train\"]\n",
178
+ "temp_split = splits[\"test\"].train_test_split(test_size=0.5, seed=seed)\n",
179
+ "processed_medium_dataset = DatasetDict(\n",
180
+ " {\n",
181
+ " \"train\": train_split,\n",
182
+ " \"validation\": temp_split[\"train\"],\n",
183
+ " \"test\": temp_split[\"test\"],\n",
184
+ " }\n",
185
+ ")\n",
186
+ "\n",
187
+ "processed_small_dataset.save_to_disk(\"./data/processed/processed_small_dataset\")\n",
188
+ "processed_medium_dataset.save_to_disk(\"./data/processed/processed_medium_dataset\")\n",
189
+ "# processed_large_dataset.save_to_disk(\"./data/processed/processed_large_dataset\")"
190
+ ]
191
+ },
192
+ {
193
+ "cell_type": "code",
194
+ "execution_count": null,
195
+ "id": "d782d9db",
196
+ "metadata": {},
197
+ "outputs": [],
198
+ "source": [
199
+ "from collections import Counter\n",
200
+ "\n",
201
+ "import matplotlib.pyplot as plt\n",
202
+ "import numpy as np\n",
203
+ "from hydra import compose, initialize\n",
204
+ "from hydra.core.global_hydra import GlobalHydra\n",
205
+ "from omegaconf import OmegaConf\n",
206
+ "from transformers import PreTrainedTokenizerFast\n",
207
+ "\n",
208
+ "from transformer.configs import TokenizerCfg\n",
209
+ "\n",
210
+ "\n",
211
+ "def load_tokenizer(name: str) -> PreTrainedTokenizerFast:\n",
212
+ " \"\"\"Load a tokenizer defined in configs/tokenizer/<name>.yaml.\"\"\"\n",
213
+ " GlobalHydra.instance().clear()\n",
214
+ " initialize(config_path=\"./configs/tokenizer\", version_base=None)\n",
215
+ " cfg = compose(config_name=\"bpe_4k\")\n",
216
+ " scfg_temp = OmegaConf.merge(OmegaConf.structured(TokenizerCfg), cfg)\n",
217
+ " tokenizer_cfg: TokenizerCfg = OmegaConf.to_object(scfg_temp)\n",
218
+ " tokenizer_path = tokenizer_cfg.path\n",
219
+ " tokenizer = PreTrainedTokenizerFast(\n",
220
+ " tokenizer_file=str(tokenizer_path),\n",
221
+ " bos_token=cfg.get(\"bos_token\"),\n",
222
+ " eos_token=cfg.get(\"eos_token\"),\n",
223
+ " unk_token=cfg.get(\"unk_token\"),\n",
224
+ " pad_token=cfg.get(\"pad_token\"),\n",
225
+ " )\n",
226
+ " tokenizer.model_max_length = cfg.get(\"max_seq_len\", tokenizer.model_max_length)\n",
227
+ " return tokenizer\n",
228
+ "\n",
229
+ "\n",
230
+ "def _resolve_tokenizer(tokenizers, field: str) -> PreTrainedTokenizerFast:\n",
231
+ " if isinstance(tokenizers, dict):\n",
232
+ " tok = tokenizers.get(field)\n",
233
+ " if tok is None:\n",
234
+ " raise KeyError(f\"No tokenizer provided for field '{field}'\")\n",
235
+ " return tok\n",
236
+ " if tokenizers is None:\n",
237
+ " raise ValueError(\"Tokenizer or field->tokenizer mapping required\")\n",
238
+ " return tokenizers\n",
239
+ "\n",
240
+ "\n",
241
+ "def _tokenize_text(\n",
242
+ " tokenizer: PreTrainedTokenizerFast, text: str, include_special_tokens: bool\n",
243
+ ") -> list[str]:\n",
244
+ " encoded = tokenizer(\n",
245
+ " text,\n",
246
+ " add_special_tokens=include_special_tokens,\n",
247
+ " return_attention_mask=False,\n",
248
+ " return_token_type_ids=False,\n",
249
+ " )\n",
250
+ " input_ids = encoded[\"input_ids\"]\n",
251
+ " if input_ids and isinstance(input_ids[0], list):\n",
252
+ " input_ids = input_ids[0]\n",
253
+ " return tokenizer.convert_ids_to_tokens(input_ids)\n",
254
+ "\n",
255
+ "\n",
256
+ "def _subset_dataset(ds, sample_size: int | None):\n",
257
+ " if sample_size is None or sample_size >= len(ds):\n",
258
+ " return ds\n",
259
+ " return ds.select(range(sample_size))\n",
260
+ "\n",
261
+ "\n",
262
+ "def show_top_k_tokens(\n",
263
+ " dataset_dict,\n",
264
+ " tokenizers,\n",
265
+ " fields=(\"en\", \"fr\"),\n",
266
+ " k: int = 20,\n",
267
+ " sample_size: int | None = 2000,\n",
268
+ " include_special_tokens: bool = False,\n",
269
+ "):\n",
270
+ " \"\"\"Print the top-k most frequent tokens using configured tokenizer(s).\"\"\"\n",
271
+ " for split, ds in dataset_dict.items():\n",
272
+ " ds_view = _subset_dataset(ds, sample_size)\n",
273
+ " counters = {field: Counter() for field in fields}\n",
274
+ " for example in ds_view:\n",
275
+ " for field in fields:\n",
276
+ " tokenizer = _resolve_tokenizer(tokenizers, field)\n",
277
+ " tokens = _tokenize_text(tokenizer, example[field], include_special_tokens)\n",
278
+ " counters[field].update(tokens)\n",
279
+ " print(f\"\\nSplit: {split}\")\n",
280
+ " for field in fields:\n",
281
+ " print(f\" Top {k} tokens for '{field}':\")\n",
282
+ " for token, freq in counters[field].most_common(k):\n",
283
+ " print(f\" {token!r}: {freq}\")\n",
284
+ "\n",
285
+ "\n",
286
+ "def plot_token_length_barchart(\n",
287
+ " dataset_dict,\n",
288
+ " tokenizers,\n",
289
+ " fields=(\"en\", \"fr\"),\n",
290
+ " sample_size: int | None = 2000,\n",
291
+ " include_special_tokens: bool = False,\n",
292
+ "):\n",
293
+ " \"\"\"Display bar charts of token-length distributions using configured tokenizer(s).\"\"\"\n",
294
+ " for split, ds in dataset_dict.items():\n",
295
+ " ds_view = _subset_dataset(ds, sample_size)\n",
296
+ " length_counters = {field: Counter() for field in fields}\n",
297
+ " for example in ds_view:\n",
298
+ " for field in fields:\n",
299
+ " tokenizer = _resolve_tokenizer(tokenizers, field)\n",
300
+ " count = len(_tokenize_text(tokenizer, example[field], include_special_tokens))\n",
301
+ " length_counters[field][count] += 1\n",
302
+ " support = sorted({length for counter in length_counters.values() for length in counter})\n",
303
+ " if not support:\n",
304
+ " continue\n",
305
+ " bin_size = 50\n",
306
+ " bin_starts = list(range(0, support[-1] + bin_size, bin_size))\n",
307
+ " bin_labels = [f\"{start}-{start + bin_size - 1}\" for start in bin_starts]\n",
308
+ " bin_counts = {field: [0] * len(bin_starts) for field in fields}\n",
309
+ " for field in fields:\n",
310
+ " for length, freq in length_counters[field].items():\n",
311
+ " idx = min(len(bin_starts) - 1, length // bin_size)\n",
312
+ " bin_counts[field][idx] += freq\n",
313
+ " x = np.arange(len(bin_starts))\n",
314
+ " width = 0.8 / max(1, len(fields))\n",
315
+ " fig, ax = plt.subplots(figsize=(8, 4))\n",
316
+ " for idx_field, field in enumerate(fields):\n",
317
+ " counts = bin_counts[field]\n",
318
+ " ax.bar(x + idx_field * width, counts, width=width, label=field)\n",
319
+ " ax.set_xticks(x + width * (len(fields) - 1) / 2)\n",
320
+ " ax.set_xticklabels(bin_labels)\n",
321
+ " ax.set_xlabel(\"Token count per example\")\n",
322
+ " ax.set_ylabel(\"Number of examples\")\n",
323
+ " ax.set_title(f\"Example token lengths in '{split}' split\")\n",
324
+ " ax.legend()\n",
325
+ " fig.tight_layout()\n",
326
+ " plt.show()"
327
+ ]
328
+ },
329
+ {
330
+ "cell_type": "code",
331
+ "execution_count": null,
332
+ "id": "b8ed32b9",
333
+ "metadata": {},
334
+ "outputs": [],
335
+ "source": [
336
+ "tokenizers = {\n",
337
+ " \"en\": load_tokenizer(\"bpe_8k\"),\n",
338
+ " \"fr\": load_tokenizer(\"bpe_8k\"),\n",
339
+ "}"
340
+ ]
341
+ },
342
+ {
343
+ "cell_type": "code",
344
+ "execution_count": null,
345
+ "id": "04f27397",
346
+ "metadata": {},
347
+ "outputs": [],
348
+ "source": [
349
+ "# Small dataset statistics\n",
350
+ "show_top_k_tokens(processed_small_dataset, tokenizers, k=20, sample_size=2000)\n",
351
+ "plot_token_length_barchart(processed_small_dataset, tokenizers, sample_size=2000)"
352
+ ]
353
+ },
354
+ {
355
+ "cell_type": "code",
356
+ "execution_count": null,
357
+ "id": "e7819046",
358
+ "metadata": {},
359
+ "outputs": [],
360
+ "source": [
361
+ "# Medium dataset statistics\n",
362
+ "show_top_k_tokens(processed_medium_dataset, tokenizers, k=20, sample_size=2000)\n",
363
+ "plot_token_length_barchart(processed_medium_dataset, tokenizers, sample_size=2000)"
364
+ ]
365
+ }
366
+ ],
367
+ "metadata": {
368
+ "kernelspec": {
369
+ "display_name": "transformer",
370
+ "language": "python",
371
+ "name": "python3"
372
+ },
373
+ "language_info": {
374
+ "codemirror_mode": {
375
+ "name": "ipython",
376
+ "version": 3
377
+ },
378
+ "file_extension": ".py",
379
+ "mimetype": "text/x-python",
380
+ "name": "python",
381
+ "nbconvert_exporter": "python",
382
+ "pygments_lexer": "ipython3",
383
+ "version": "3.12.11"
384
+ }
385
+ },
386
+ "nbformat": 4,
387
+ "nbformat_minor": 5
388
+ }
scripts/server.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from fastapi import FastAPI
4
+ from hydra import compose, initialize
5
+ from omegaconf import DictConfig
6
+
7
+ from scripts.inference import inference as run_inference
8
+
9
+ app = FastAPI(title="Transformer Inference API")
10
+
11
+
12
+ @app.get("/healthz")
13
+ def healthz():
14
+ return {"status": "ok"}
15
+
16
+
17
+ @app.get("/generate")
18
+ def generate(text: str | None = None):
19
+ # Compose the default config exactly as inference.py expects
20
+ with initialize(config_path="../configs", version_base=None):
21
+ cfg: DictConfig = compose(config_name="infer_mode")
22
+ # If a text query parameter is provided, override the config's input_text
23
+ if text is not None and text.strip():
24
+ cfg.input_text = text
25
+ outputs = run_inference(cfg) # returns a list of strings
26
+ return {"outputs": outputs}
scripts/tokenizer.ipynb ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e360283a",
6
+ "metadata": {},
7
+ "source": [
8
+ "imports"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": 1,
14
+ "id": "47d34b96",
15
+ "metadata": {},
16
+ "outputs": [
17
+ {
18
+ "name": "stderr",
19
+ "output_type": "stream",
20
+ "text": [
21
+ "/home/ala-boussoffara/miniconda3/envs/transformer/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
22
+ " from .autonotebook import tqdm as notebook_tqdm\n"
23
+ ]
24
+ }
25
+ ],
26
+ "source": [
27
+ "from datasets import concatenate_datasets, load_from_disk\n",
28
+ "from hydra import compose, initialize\n",
29
+ "from hydra.core.global_hydra import GlobalHydra\n",
30
+ "from omegaconf import OmegaConf\n",
31
+ "from tokenizers import Tokenizer, models, pre_tokenizers, trainers\n",
32
+ "from tokenizers.processors import TemplateProcessing\n",
33
+ "\n",
34
+ "from transformer.configs import TokenizerCfg"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "markdown",
39
+ "id": "9a0c380c",
40
+ "metadata": {},
41
+ "source": [
42
+ "Choosing tokenizer config"
43
+ ]
44
+ },
45
+ {
46
+ "cell_type": "code",
47
+ "execution_count": 9,
48
+ "id": "8f13dc2b",
49
+ "metadata": {},
50
+ "outputs": [],
51
+ "source": [
52
+ "GlobalHydra.instance().clear()\n",
53
+ "initialize(config_path=\"./configs/tokenizer\", version_base=None)\n",
54
+ "cfg = compose(config_name=\"bpe_16k\")\n",
55
+ "scfg_temp = OmegaConf.merge(OmegaConf.structured(TokenizerCfg), cfg)\n",
56
+ "tokenizer_cfg: TokenizerCfg = OmegaConf.to_object(scfg_temp)"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "markdown",
61
+ "id": "d5ee3d16",
62
+ "metadata": {},
63
+ "source": [
64
+ "Loading datasets"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": 3,
70
+ "id": "d880e59e",
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "small_dataset = load_from_disk(\"./data/processed/processed_small_dataset\")\n",
75
+ "medium_dataset = load_from_disk(\"./data/processed/processed_medium_dataset\")\n",
76
+ "\n",
77
+ "concatenated_dataset = concatenate_datasets(\n",
78
+ " [\n",
79
+ " small_dataset[\"train\"],\n",
80
+ " small_dataset[\"validation\"],\n",
81
+ " small_dataset[\"test\"],\n",
82
+ " medium_dataset[\"train\"],\n",
83
+ " ]\n",
84
+ ")"
85
+ ]
86
+ },
87
+ {
88
+ "cell_type": "markdown",
89
+ "id": "c6b53e40",
90
+ "metadata": {},
91
+ "source": [
92
+ "preparing corpus"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": 4,
98
+ "id": "f60ac4b2",
99
+ "metadata": {},
100
+ "outputs": [],
101
+ "source": [
102
+ "with open(tokenizer_cfg.corpus, \"w\", encoding=\"utf-8\") as f:\n",
103
+ " for example in globals()[tokenizer_cfg.dataset]:\n",
104
+ " en = example[\"translation\"][\"en\"]\n",
105
+ " fr = example[\"translation\"][\"fr\"]\n",
106
+ " if en and fr:\n",
107
+ " f.write(en + \"\\n\")\n",
108
+ " f.write(fr + \"\\n\")"
109
+ ]
110
+ },
111
+ {
112
+ "cell_type": "markdown",
113
+ "id": "f7a50455",
114
+ "metadata": {},
115
+ "source": [
116
+ "training the tokenizer"
117
+ ]
118
+ },
119
+ {
120
+ "cell_type": "code",
121
+ "execution_count": 10,
122
+ "id": "29fc0f7e",
123
+ "metadata": {},
124
+ "outputs": [
125
+ {
126
+ "name": "stdout",
127
+ "output_type": "stream",
128
+ "text": [
129
+ "\n",
130
+ "\n",
131
+ "\n"
132
+ ]
133
+ }
134
+ ],
135
+ "source": [
136
+ "tokenizer = Tokenizer(models.BPE(unk_token=tokenizer_cfg.unk_token))\n",
137
+ "tokenizer.pre_tokenizer = pre_tokenizers.Sequence(\n",
138
+ " [pre_tokenizers.Whitespace(), pre_tokenizers.Punctuation()]\n",
139
+ ")\n",
140
+ "trainer = trainers.BpeTrainer(\n",
141
+ " vocab_size=tokenizer_cfg.vocab_size, special_tokens=tokenizer_cfg.special_tokens\n",
142
+ ")\n",
143
+ "tokenizer.train([tokenizer_cfg.corpus], trainer)\n",
144
+ "tokenizer.post_processor = TemplateProcessing(\n",
145
+ " single=f\"{tokenizer_cfg.bos_token} $A {tokenizer_cfg.eos_token}\",\n",
146
+ " special_tokens=[\n",
147
+ " (tokenizer_cfg.bos_token, tokenizer_cfg.bos_id),\n",
148
+ " (tokenizer_cfg.eos_token, tokenizer_cfg.eos_id),\n",
149
+ " ],\n",
150
+ ")\n",
151
+ "tokenizer.save(tokenizer_cfg.path)"
152
+ ]
153
+ },
154
+ {
155
+ "cell_type": "markdown",
156
+ "id": "537b9d9d",
157
+ "metadata": {},
158
+ "source": [
159
+ "Loading and testing"
160
+ ]
161
+ },
162
+ {
163
+ "cell_type": "code",
164
+ "execution_count": 6,
165
+ "id": "443ed7ee",
166
+ "metadata": {},
167
+ "outputs": [
168
+ {
169
+ "name": "stdout",
170
+ "output_type": "stream",
171
+ "text": [
172
+ "['i', 'here', 'by', 'an', 'n', 'ou', 'ce', 'you', 'as', 'the', 'next', 'presi', 'dent']\n",
173
+ "PAD: 0\n",
174
+ "BOS: 1\n",
175
+ "EOS: 2\n",
176
+ "UNK: 3\n"
177
+ ]
178
+ }
179
+ ],
180
+ "source": [
181
+ "from tokenizers import Tokenizer\n",
182
+ "\n",
183
+ "tokenizer = Tokenizer.from_file(tokenizer_cfg.path)\n",
184
+ "print(\n",
185
+ " tokenizer.encode(\"i hereby annouce you as the next president\", add_special_tokens=False).tokens\n",
186
+ ")\n",
187
+ "vocab = tokenizer.get_vocab()\n",
188
+ "\n",
189
+ "print(\"PAD:\", vocab[\"<pad>\"])\n",
190
+ "print(\"BOS:\", vocab[\"<bos>\"])\n",
191
+ "print(\"EOS:\", vocab[\"<eos>\"])\n",
192
+ "print(\"UNK:\", vocab[\"<unk>\"])"
193
+ ]
194
+ }
195
+ ],
196
+ "metadata": {
197
+ "kernelspec": {
198
+ "display_name": "transformer",
199
+ "language": "python",
200
+ "name": "python3"
201
+ },
202
+ "language_info": {
203
+ "codemirror_mode": {
204
+ "name": "ipython",
205
+ "version": 3
206
+ },
207
+ "file_extension": ".py",
208
+ "mimetype": "text/x-python",
209
+ "name": "python",
210
+ "nbconvert_exporter": "python",
211
+ "pygments_lexer": "ipython3",
212
+ "version": "3.12.11"
213
+ }
214
+ },
215
+ "nbformat": 4,
216
+ "nbformat_minor": 5
217
+ }
scripts/train.ipynb ADDED
@@ -0,0 +1,987 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "381e1824",
6
+ "metadata": {},
7
+ "source": [
8
+ "Imports"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "5a9e6fb4",
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "import contextlib\n",
19
+ "import json\n",
20
+ "import math\n",
21
+ "import os\n",
22
+ "from datetime import datetime\n",
23
+ "from pathlib import Path\n",
24
+ "from typing import Any\n",
25
+ "\n",
26
+ "import matplotlib.pyplot as plt\n",
27
+ "import torch\n",
28
+ "import torch.nn as nn\n",
29
+ "from datasets import load_from_disk\n",
30
+ "from hydra import compose, initialize\n",
31
+ "from hydra.core.global_hydra import GlobalHydra\n",
32
+ "from hydra.utils import to_absolute_path\n",
33
+ "from omegaconf import OmegaConf\n",
34
+ "from torch.nn.utils import clip_grad_norm_\n",
35
+ "from torch.utils.data import DataLoader\n",
36
+ "from tqdm import tqdm\n",
37
+ "from transformers import PreTrainedTokenizerFast\n",
38
+ "\n",
39
+ "from transformer import BasicEncoderDecoderTransformer\n",
40
+ "from transformer.configs import ModelCfg, TokenizerCfg, TrainAppCfg\n",
41
+ "from transformer.utils import (\n",
42
+ " check_tokenizer_model_compatibility,\n",
43
+ " debug_transformer_forward,\n",
44
+ " make_worker_init_fn,\n",
45
+ " set_global_seed,\n",
46
+ ")"
47
+ ]
48
+ },
49
+ {
50
+ "cell_type": "markdown",
51
+ "id": "512d7a41",
52
+ "metadata": {},
53
+ "source": [
54
+ "Modifying working directory"
55
+ ]
56
+ },
57
+ {
58
+ "cell_type": "code",
59
+ "execution_count": null,
60
+ "id": "d1c043be",
61
+ "metadata": {},
62
+ "outputs": [],
63
+ "source": [
64
+ "project_root = Path(__file__).resolve().parent if \"__file__\" in globals() else Path.cwd()\n",
65
+ "os.chdir(project_root)\n",
66
+ "print(f\"Working directory set to: {Path.cwd()}\")"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "markdown",
71
+ "id": "9b3f89d1",
72
+ "metadata": {},
73
+ "source": [
74
+ "Initiliazation"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "code",
79
+ "execution_count": null,
80
+ "id": "2bd6dd82",
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "GlobalHydra.instance().clear()\n",
85
+ "initialize(config_path=\"./configs\", version_base=None)\n",
86
+ "cfg = compose(\n",
87
+ " config_name=\"train_mode\",\n",
88
+ " overrides=[\n",
89
+ " \"model=test_model\",\n",
90
+ " \"tokenizer=bpe_8k\",\n",
91
+ " \"dataset=medium_dataset\",\n",
92
+ " \"trainer.batch_size=128\",\n",
93
+ " \"trainer.gradient_accumulation=2\",\n",
94
+ " \"trainer.epochs=80\",\n",
95
+ " ],\n",
96
+ ")\n",
97
+ "scfg_temp = OmegaConf.merge(OmegaConf.structured(TrainAppCfg), cfg)\n",
98
+ "scfg: TrainAppCfg = OmegaConf.to_object(scfg_temp)\n",
99
+ "set_global_seed(scfg.runtime.seed)\n",
100
+ "model_cfg = ModelCfg(**vars(scfg.model))\n",
101
+ "tokenizer_cfg = TokenizerCfg(**vars(scfg.tokenizer))\n",
102
+ "\n",
103
+ "# --- Compatibility check ---\n",
104
+ "check_tokenizer_model_compatibility(model_cfg, tokenizer_cfg)"
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "markdown",
109
+ "id": "24caf2de",
110
+ "metadata": {},
111
+ "source": [
112
+ "Preparing data"
113
+ ]
114
+ },
115
+ {
116
+ "cell_type": "code",
117
+ "execution_count": null,
118
+ "id": "8d60e085",
119
+ "metadata": {},
120
+ "outputs": [],
121
+ "source": [
122
+ "dataset = load_from_disk(cfg.dataset.path)\n",
123
+ "\n",
124
+ "train_dataset = dataset[\"train\"].select(range(200000))\n",
125
+ "validation_dataset = dataset[\"validation\"]\n",
126
+ "test_dataset = dataset[\"test\"]\n",
127
+ "\n",
128
+ "\n",
129
+ "def tuple_collate_fn(batch):\n",
130
+ " # batch is a list of dicts with 'en' and 'fr' keys\n",
131
+ " src = [item[\"en\"] for item in batch]\n",
132
+ " tgt = [item[\"fr\"] for item in batch]\n",
133
+ " return src, tgt\n",
134
+ "\n",
135
+ "\n",
136
+ "dataloader_generator = torch.Generator().manual_seed(scfg.runtime.seed)\n",
137
+ "worker_init = make_worker_init_fn(scfg.runtime.seed)\n",
138
+ "\n",
139
+ "train_dataloader = DataLoader(\n",
140
+ " train_dataset,\n",
141
+ " batch_size=scfg.trainer.batch_size,\n",
142
+ " shuffle=scfg.trainer.shuffle,\n",
143
+ " collate_fn=tuple_collate_fn,\n",
144
+ " generator=dataloader_generator,\n",
145
+ " worker_init_fn=worker_init,\n",
146
+ ")\n",
147
+ "validation_dataloader = DataLoader(\n",
148
+ " validation_dataset,\n",
149
+ " batch_size=scfg.trainer.batch_size,\n",
150
+ " shuffle=False,\n",
151
+ " collate_fn=tuple_collate_fn,\n",
152
+ " generator=dataloader_generator,\n",
153
+ " worker_init_fn=worker_init,\n",
154
+ ")\n",
155
+ "test_dataloader = DataLoader(\n",
156
+ " test_dataset,\n",
157
+ " batch_size=scfg.trainer.batch_size,\n",
158
+ " shuffle=False,\n",
159
+ " collate_fn=tuple_collate_fn,\n",
160
+ " generator=dataloader_generator,\n",
161
+ " worker_init_fn=worker_init,\n",
162
+ ")\n",
163
+ "\n",
164
+ "print(\"Number of training samples:\", len(train_dataset))\n",
165
+ "print(\"Number of training batches:\", len(train_dataloader))"
166
+ ]
167
+ },
168
+ {
169
+ "cell_type": "markdown",
170
+ "id": "96c84adf",
171
+ "metadata": {},
172
+ "source": [
173
+ "Model and tokenizer loading"
174
+ ]
175
+ },
176
+ {
177
+ "cell_type": "code",
178
+ "execution_count": null,
179
+ "id": "f9da0dab",
180
+ "metadata": {},
181
+ "outputs": [],
182
+ "source": [
183
+ "device = torch.device(scfg.runtime.device)\n",
184
+ "model = BasicEncoderDecoderTransformer(model_cfg).to(device)\n",
185
+ "\n",
186
+ "\n",
187
+ "# xavier initialization\n",
188
+ "def init_weights(m):\n",
189
+ " if isinstance(m, nn.Linear | nn.Embedding):\n",
190
+ " torch.nn.init.xavier_uniform_(m.weight)\n",
191
+ " elif isinstance(m, nn.LayerNorm):\n",
192
+ " m.bias.data.fill_(0)\n",
193
+ " m.weight.data.fill_(1.0)\n",
194
+ " if isinstance(m, nn.Linear) and m.bias is not None:\n",
195
+ " m.bias.data.fill_(0)\n",
196
+ "\n",
197
+ "\n",
198
+ "model.apply(init_weights)\n",
199
+ "\n",
200
+ "tokenizer = PreTrainedTokenizerFast(\n",
201
+ " tokenizer_file=tokenizer_cfg.path,\n",
202
+ " bos_token=tokenizer_cfg.bos_token,\n",
203
+ " eos_token=tokenizer_cfg.eos_token,\n",
204
+ " unk_token=tokenizer_cfg.unk_token,\n",
205
+ " pad_token=tokenizer_cfg.pad_token,\n",
206
+ " model_max_length=tokenizer_cfg.max_seq_len,\n",
207
+ ")\n",
208
+ "\n",
209
+ "criterion = nn.CrossEntropyLoss(\n",
210
+ " ignore_index=tokenizer_cfg.pad_id, label_smoothing=scfg.trainer.label_smoothing\n",
211
+ ")\n",
212
+ "\n",
213
+ "# print total number of parameters\n",
214
+ "total_params = sum(p.numel() for p in model.parameters())\n",
215
+ "print(f\"Total number of parameters: {total_params}\")"
216
+ ]
217
+ },
218
+ {
219
+ "cell_type": "markdown",
220
+ "id": "608a205a",
221
+ "metadata": {},
222
+ "source": [
223
+ "Optimizer setup"
224
+ ]
225
+ },
226
+ {
227
+ "cell_type": "code",
228
+ "execution_count": null,
229
+ "id": "372dea7e",
230
+ "metadata": {},
231
+ "outputs": [],
232
+ "source": [
233
+ "def setup_optimizer(model: nn.Module, cfg: Any) -> torch.optim.Optimizer:\n",
234
+ " \"\"\"Create an AdamW optimizer with decoupled weight decay groups.\"\"\"\n",
235
+ "\n",
236
+ " decay_params: set[str] = set()\n",
237
+ " no_decay_params: set[str] = set()\n",
238
+ "\n",
239
+ " blacklist_weight_modules = (nn.LayerNorm,)\n",
240
+ "\n",
241
+ " for module_name, module in model.named_modules():\n",
242
+ " for param_name, param in module.named_parameters(recurse=False):\n",
243
+ " if not param.requires_grad:\n",
244
+ " continue\n",
245
+ "\n",
246
+ " full_name = f\"{module_name}.{param_name}\" if module_name else param_name\n",
247
+ "\n",
248
+ " if param_name.endswith(\"bias\"):\n",
249
+ " no_decay_params.add(full_name)\n",
250
+ " elif isinstance(module, blacklist_weight_modules):\n",
251
+ " no_decay_params.add(full_name)\n",
252
+ " elif param.ndim == 1:\n",
253
+ " no_decay_params.add(full_name)\n",
254
+ " else:\n",
255
+ " decay_params.add(full_name)\n",
256
+ "\n",
257
+ " for name, param in model.named_parameters():\n",
258
+ " if not param.requires_grad:\n",
259
+ " continue\n",
260
+ " lowered = name.lower()\n",
261
+ " if \"embedding\" in lowered or (\"pos\" in lowered and \"embed\" in lowered):\n",
262
+ " decay_params.discard(name)\n",
263
+ " no_decay_params.add(name)\n",
264
+ "\n",
265
+ " param_dict = {name: param for name, param in model.named_parameters() if param.requires_grad}\n",
266
+ " decay_group = [param_dict[name] for name in sorted(decay_params) if name in param_dict]\n",
267
+ " no_decay_group = [param_dict[name] for name in sorted(no_decay_params) if name in param_dict]\n",
268
+ "\n",
269
+ " optim_cfg = cfg.optim\n",
270
+ " param_groups = []\n",
271
+ " if decay_group:\n",
272
+ " param_groups.append({\"params\": decay_group, \"weight_decay\": optim_cfg.weight_decay})\n",
273
+ " if no_decay_group:\n",
274
+ " param_groups.append({\"params\": no_decay_group, \"weight_decay\": 0.0})\n",
275
+ "\n",
276
+ " if not param_groups:\n",
277
+ " raise ValueError(\"No trainable parameters found when setting up the optimizer.\")\n",
278
+ "\n",
279
+ " return torch.optim.AdamW(\n",
280
+ " model.parameters(),\n",
281
+ " lr=optim_cfg.lr,\n",
282
+ " betas=optim_cfg.betas,\n",
283
+ " eps=optim_cfg.eps,\n",
284
+ " )\n",
285
+ "\n",
286
+ "\n",
287
+ "optimizer = setup_optimizer(model, scfg)"
288
+ ]
289
+ },
290
+ {
291
+ "cell_type": "markdown",
292
+ "id": "f64a8b38",
293
+ "metadata": {},
294
+ "source": [
295
+ "AMP and scaler setup"
296
+ ]
297
+ },
298
+ {
299
+ "cell_type": "code",
300
+ "execution_count": null,
301
+ "id": "4ad52841",
302
+ "metadata": {},
303
+ "outputs": [],
304
+ "source": [
305
+ "# 1) Decide whether AMP is on and which dtype to use\n",
306
+ "use_amp = scfg.trainer.mixed_precision and (scfg.trainer.precision in [\"fp16\", \"bf16\"])\n",
307
+ "\n",
308
+ "# 2) Pick the autocast dtype from precision\n",
309
+ "amp_dtype = torch.float16 if scfg.trainer.precision == \"fp16\" else torch.bfloat16\n",
310
+ "\n",
311
+ "# 3) Build the context manager:\n",
312
+ "# - If AMP is on → use autocast(dtype=...)\n",
313
+ "# - If AMP is off → use a nullcontext (does nothing)\n",
314
+ "autocast_ctx = (\n",
315
+ " torch.amp.autocast(dtype=amp_dtype, device_type=scfg.runtime.device)\n",
316
+ " if use_amp\n",
317
+ " else contextlib.nullcontext()\n",
318
+ ")\n",
319
+ "\n",
320
+ "# 4) Create the GradScaler:\n",
321
+ "# - enabled=True only for fp16 (needs loss scaling)\n",
322
+ "# - for bf16/fp32 it becomes a no-op automatically\n",
323
+ "scaler = torch.amp.GradScaler(\n",
324
+ " enabled=(scfg.trainer.mixed_precision and scfg.trainer.precision == \"fp16\")\n",
325
+ ")"
326
+ ]
327
+ },
328
+ {
329
+ "cell_type": "markdown",
330
+ "id": "34e29854",
331
+ "metadata": {},
332
+ "source": [
333
+ "Scheduler setup"
334
+ ]
335
+ },
336
+ {
337
+ "cell_type": "code",
338
+ "execution_count": null,
339
+ "id": "479390a8",
340
+ "metadata": {},
341
+ "outputs": [],
342
+ "source": [
343
+ "def setup_scheduler(\n",
344
+ " optimizer: torch.optim.Optimizer, trainer_cfg: Any, sched_cfg: Any, steps_per_epoch: int\n",
345
+ ") -> torch.optim.lr_scheduler.LambdaLR:\n",
346
+ " \"\"\"Create a cosine LR scheduler driven by warmup/min LR ratios.\"\"\"\n",
347
+ " total_steps = max(1, int(trainer_cfg.epochs) * max(1, int(steps_per_epoch)))\n",
348
+ " warmup_ratio = max(0.0, float(getattr(sched_cfg, \"warmup_ratio\", 0.0)))\n",
349
+ " min_lr_ratio = max(0.0, float(getattr(sched_cfg, \"min_lr_ratio\", 0.0)))\n",
350
+ " hold_min = bool(getattr(sched_cfg, \"hold_min\", True))\n",
351
+ " max_lr_ratio = max(0.0, float(getattr(sched_cfg, \"max_lr_ratio\", 1.0)))\n",
352
+ " warmup_steps = int(round(total_steps * warmup_ratio))\n",
353
+ " warmup_steps = max(0, min(warmup_steps, max(0, total_steps - 1)))\n",
354
+ " decay_steps = max(1, total_steps - warmup_steps)\n",
355
+ " for group in optimizer.param_groups:\n",
356
+ " base_lr = float(group.get(\"initial_lr\", group[\"lr\"]))\n",
357
+ " group[\"initial_lr\"] = base_lr\n",
358
+ " group[\"min_lr\"] = base_lr * min_lr_ratio\n",
359
+ " group[\"max_lr\"] = base_lr * max_lr_ratio\n",
360
+ "\n",
361
+ " def lr_lambda(step: int) -> float:\n",
362
+ " if warmup_steps > 0 and step < warmup_steps:\n",
363
+ " warmup_progress = step / max(1, warmup_steps)\n",
364
+ " warmup_progress = max(0.0, min(1.0, warmup_progress))\n",
365
+ " return min_lr_ratio + (1.0 - min_lr_ratio) * warmup_progress\n",
366
+ " if decay_steps <= 0:\n",
367
+ " return 1.0\n",
368
+ " progress = (step - warmup_steps) / decay_steps\n",
369
+ " progress = max(0.0, min(1.0, progress))\n",
370
+ " cosine = 0.5 * (1.0 + math.cos(math.pi * progress))\n",
371
+ " factor = min_lr_ratio + (1.0 - min_lr_ratio) * cosine\n",
372
+ " if progress >= 1.0 and not hold_min:\n",
373
+ " return 0.0\n",
374
+ " return factor\n",
375
+ "\n",
376
+ " return torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)\n",
377
+ "\n",
378
+ "\n",
379
+ "steps_per_epoch = math.ceil(len(train_dataloader) / max(1, scfg.trainer.gradient_accumulation))\n",
380
+ "scheduler = setup_scheduler(optimizer, scfg.trainer, scfg.sched, steps_per_epoch=steps_per_epoch)"
381
+ ]
382
+ },
383
+ {
384
+ "cell_type": "markdown",
385
+ "id": "db51c02a",
386
+ "metadata": {},
387
+ "source": [
388
+ "Loading checkpoints"
389
+ ]
390
+ },
391
+ {
392
+ "cell_type": "code",
393
+ "execution_count": null,
394
+ "id": "a9843809",
395
+ "metadata": {},
396
+ "outputs": [],
397
+ "source": [
398
+ "output_root = Path(to_absolute_path(scfg.runtime.output_dir))\n",
399
+ "output_root.mkdir(parents=True, exist_ok=True)\n",
400
+ "\n",
401
+ "timestamp = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n",
402
+ "run_dir = output_root / f\"{model_cfg.name}_{timestamp}\"\n",
403
+ "suffix = 1\n",
404
+ "while run_dir.exists():\n",
405
+ " run_dir = output_root / f\"{model_cfg.name}_{timestamp}_{suffix}\"\n",
406
+ " suffix += 1\n",
407
+ "run_dir.mkdir(parents=True, exist_ok=True)\n",
408
+ "\n",
409
+ "checkpoints_root = run_dir / \"checkpoints\"\n",
410
+ "latest_dir = checkpoints_root / \"latest\"\n",
411
+ "best_dir = checkpoints_root / \"best\"\n",
412
+ "latest_dir.mkdir(parents=True, exist_ok=True)\n",
413
+ "best_dir.mkdir(parents=True, exist_ok=True)\n",
414
+ "\n",
415
+ "resume_latest_source = getattr(model_cfg, \"latest_checkpoint_path\", \"\")\n",
416
+ "resume_best_source = getattr(model_cfg, \"best_checkpoint_path\", \"\")\n",
417
+ "best_checkpoint_export_target = (\n",
418
+ " Path(to_absolute_path(resume_best_source)) if resume_best_source else None\n",
419
+ ")\n",
420
+ "\n",
421
+ "latest_checkpoint_path = latest_dir / \"checkpoint.pt\"\n",
422
+ "best_checkpoint_path = best_dir / \"checkpoint.pt\"\n",
423
+ "\n",
424
+ "\n",
425
+ "config_snapshot_path = run_dir / \"config.yaml\"\n",
426
+ "OmegaConf.save(config=cfg, f=str(config_snapshot_path))\n",
427
+ "\n",
428
+ "manifest_path = run_dir / \"manifest.json\"\n",
429
+ "\n",
430
+ "resume_mode = getattr(scfg.trainer, \"resume\", \"start_over\")\n",
431
+ "resume_mode = resume_mode.replace(\" \", \"_\").lower()\n",
432
+ "if resume_mode not in {\"start_over\", \"best\", \"latest\"}:\n",
433
+ " print(f\"Unknown resume mode '{resume_mode}', defaulting to 'start_over'.\")\n",
434
+ " resume_mode = \"start_over\"\n",
435
+ "\n",
436
+ "resume_checkpoint_path = None\n",
437
+ "if resume_mode == \"latest\" and resume_latest_source:\n",
438
+ " resume_checkpoint_path = Path(to_absolute_path(resume_latest_source))\n",
439
+ "elif resume_mode == \"best\" and resume_best_source:\n",
440
+ " resume_checkpoint_path = Path(to_absolute_path(resume_best_source))\n",
441
+ "\n",
442
+ "manifest_data = {\n",
443
+ " \"run_dir\": str(run_dir),\n",
444
+ " \"model\": model_cfg.name,\n",
445
+ " \"resume\": {\n",
446
+ " \"mode\": resume_mode,\n",
447
+ " \"source\": str(resume_checkpoint_path) if resume_checkpoint_path else None,\n",
448
+ " \"status\": \"pending\",\n",
449
+ " },\n",
450
+ " \"latest\": None,\n",
451
+ " \"best\": None,\n",
452
+ "}\n",
453
+ "manifest_path.write_text(json.dumps(manifest_data, indent=2))\n",
454
+ "\n",
455
+ "start_epoch = 0\n",
456
+ "completed_optimizer_steps = 0\n",
457
+ "train_losses: list[float] = []\n",
458
+ "val_losses: list[float] = []\n",
459
+ "learning_rates: list[float] = []\n",
460
+ "resume_batch_idx = 0\n",
461
+ "resume_running_loss = 0.0\n",
462
+ "best_val_loss = float(\"inf\")\n",
463
+ "best_val_epoch = -1\n",
464
+ "\n",
465
+ "loaded_checkpoint = None\n",
466
+ "if resume_checkpoint_path and resume_checkpoint_path.is_file():\n",
467
+ " print(f\"Loading checkpoint from {resume_checkpoint_path}\")\n",
468
+ " loaded_checkpoint = torch.load(resume_checkpoint_path, map_location=\"cpu\", weights_only=False)\n",
469
+ " model_state = loaded_checkpoint.get(\"model_state_dict\")\n",
470
+ " if model_state is not None:\n",
471
+ " model.load_state_dict(model_state)\n",
472
+ " optim_state = loaded_checkpoint.get(\"optimizer_state_dict\")\n",
473
+ " if optim_state is not None:\n",
474
+ " optimizer.load_state_dict(optim_state)\n",
475
+ " sched_state = loaded_checkpoint.get(\"scheduler_state_dict\")\n",
476
+ " if sched_state is not None:\n",
477
+ " scheduler.load_state_dict(sched_state)\n",
478
+ " scaler_state = loaded_checkpoint.get(\"scaler_state_dict\")\n",
479
+ " if scaler_state is not None:\n",
480
+ " scaler.load_state_dict(scaler_state)\n",
481
+ " train_losses = list(loaded_checkpoint.get(\"train_losses\", train_losses))\n",
482
+ " val_losses = list(loaded_checkpoint.get(\"val_losses\", val_losses))\n",
483
+ " learning_rates = list(loaded_checkpoint.get(\"learning_rates\", learning_rates))\n",
484
+ " start_epoch = int(loaded_checkpoint.get(\"epoch\", start_epoch))\n",
485
+ " completed_optimizer_steps = int(\n",
486
+ " loaded_checkpoint.get(\"optimizer_step\", completed_optimizer_steps)\n",
487
+ " )\n",
488
+ " resume_batch_idx = int(loaded_checkpoint.get(\"batch_idx\", resume_batch_idx))\n",
489
+ " resume_running_loss = float(loaded_checkpoint.get(\"running_loss\", resume_running_loss))\n",
490
+ " best_val_loss = float(\n",
491
+ " loaded_checkpoint.get(\n",
492
+ " \"best_val_loss\",\n",
493
+ " loaded_checkpoint.get(\"best_train_loss\", best_val_loss),\n",
494
+ " )\n",
495
+ " )\n",
496
+ " best_val_epoch = int(\n",
497
+ " loaded_checkpoint.get(\n",
498
+ " \"best_val_epoch\",\n",
499
+ " loaded_checkpoint.get(\"best_train_epoch\", best_val_epoch),\n",
500
+ " )\n",
501
+ " )\n",
502
+ " for state in optimizer.state.values():\n",
503
+ " for key, value in state.items():\n",
504
+ " if isinstance(value, torch.Tensor):\n",
505
+ " state[key] = value.to(device)\n",
506
+ " model.to(device)\n",
507
+ "else:\n",
508
+ " if resume_checkpoint_path:\n",
509
+ " print(f\"Checkpoint '{resume_checkpoint_path}' not found. Starting fresh.\")\n",
510
+ " else:\n",
511
+ " print(\"Starting fresh (no resume checkpoint provided).\")\n",
512
+ "\n",
513
+ "if loaded_checkpoint is not None:\n",
514
+ " manifest_data[\"resume\"][\"status\"] = \"loaded\"\n",
515
+ "elif resume_checkpoint_path:\n",
516
+ " manifest_data[\"resume\"][\"status\"] = \"missing\"\n",
517
+ "else:\n",
518
+ " manifest_data[\"resume\"][\"status\"] = \"fresh\"\n",
519
+ "manifest_path.write_text(json.dumps(manifest_data, indent=2))\n",
520
+ "\n",
521
+ "\n",
522
+ "def _update_manifest(section: str, payload: dict) -> None:\n",
523
+ " manifest_data[section] = payload\n",
524
+ " manifest_path.write_text(json.dumps(manifest_data, indent=2))\n",
525
+ "\n",
526
+ "\n",
527
+ "if loaded_checkpoint is not None:\n",
528
+ " torch.save(loaded_checkpoint, latest_checkpoint_path)\n",
529
+ " _update_manifest(\n",
530
+ " \"latest\",\n",
531
+ " {\n",
532
+ " \"path\": str(latest_checkpoint_path),\n",
533
+ " \"epoch\": start_epoch,\n",
534
+ " \"optimizer_step\": completed_optimizer_steps,\n",
535
+ " \"batch_idx\": resume_batch_idx,\n",
536
+ " \"running_loss\": resume_running_loss,\n",
537
+ " \"timestamp\": datetime.now().isoformat(),\n",
538
+ " },\n",
539
+ " )\n",
540
+ " if resume_mode == \"best\":\n",
541
+ " torch.save(loaded_checkpoint, best_checkpoint_path)\n",
542
+ " _update_manifest(\n",
543
+ " \"best\",\n",
544
+ " {\n",
545
+ " \"path\": str(best_checkpoint_path),\n",
546
+ " \"epoch\": start_epoch,\n",
547
+ " \"optimizer_step\": completed_optimizer_steps,\n",
548
+ " \"val_loss\": loaded_checkpoint.get(\n",
549
+ " \"best_val_loss\", loaded_checkpoint.get(\"best_train_loss\")\n",
550
+ " ),\n",
551
+ " \"train_loss\": loaded_checkpoint.get(\"metadata\", {}).get(\"train_loss\"),\n",
552
+ " \"timestamp\": datetime.now().isoformat(),\n",
553
+ " },\n",
554
+ " )\n",
555
+ "\n",
556
+ "\n",
557
+ "def save_checkpoint(\n",
558
+ " epoch_to_resume: int,\n",
559
+ " optimizer_step: int,\n",
560
+ " *,\n",
561
+ " batch_idx: int,\n",
562
+ " running_loss_value: float,\n",
563
+ " metadata: dict[str, Any] | None = None,\n",
564
+ ") -> dict:\n",
565
+ " checkpoint = {\n",
566
+ " \"epoch\": epoch_to_resume,\n",
567
+ " \"optimizer_step\": optimizer_step,\n",
568
+ " \"batch_idx\": batch_idx,\n",
569
+ " \"running_loss\": running_loss_value,\n",
570
+ " \"model_state_dict\": model.state_dict(),\n",
571
+ " \"optimizer_state_dict\": optimizer.state_dict(),\n",
572
+ " \"scheduler_state_dict\": scheduler.state_dict(),\n",
573
+ " \"scaler_state_dict\": scaler.state_dict(),\n",
574
+ " \"train_losses\": train_losses,\n",
575
+ " \"val_losses\": val_losses,\n",
576
+ " \"learning_rates\": learning_rates,\n",
577
+ " \"best_val_loss\": best_val_loss,\n",
578
+ " \"best_val_epoch\": best_val_epoch,\n",
579
+ " \"metadata\": metadata or {},\n",
580
+ " }\n",
581
+ " torch.save(checkpoint, latest_checkpoint_path)\n",
582
+ " _update_manifest(\n",
583
+ " \"latest\",\n",
584
+ " {\n",
585
+ " \"path\": str(latest_checkpoint_path),\n",
586
+ " \"epoch\": epoch_to_resume,\n",
587
+ " \"optimizer_step\": optimizer_step,\n",
588
+ " \"batch_idx\": batch_idx,\n",
589
+ " \"running_loss\": running_loss_value,\n",
590
+ " \"metadata\": metadata or {},\n",
591
+ " \"timestamp\": datetime.now().isoformat(),\n",
592
+ " },\n",
593
+ " )\n",
594
+ " return checkpoint"
595
+ ]
596
+ },
597
+ {
598
+ "cell_type": "code",
599
+ "execution_count": null,
600
+ "id": "ab23bf3c",
601
+ "metadata": {},
602
+ "outputs": [],
603
+ "source": [
604
+ "# src,tgt=next(iter(train_dataloader))\n",
605
+ "src = [\"it can be a very complicated thing, the ocean\"]\n",
606
+ "tgt = [\"ca peut être très compliqué, l'océan\"]\n",
607
+ "encoded_src = tokenizer(\n",
608
+ " src, padding=True, truncation=True, return_tensors=\"pt\", add_special_tokens=False\n",
609
+ ")\n",
610
+ "encoded_tgt = tokenizer(tgt, padding=True, truncation=True, return_tensors=\"pt\")\n",
611
+ "src_ids = encoded_src[\"input_ids\"].to(device)\n",
612
+ "tgt_ids = encoded_tgt[\"input_ids\"].to(device)\n",
613
+ "src_padd_mask = (src_ids == scfg.tokenizer.pad_id).to(device, dtype=torch.bool)\n",
614
+ "tgt_padd_mask = (tgt_ids == scfg.tokenizer.pad_id).to(device, dtype=torch.bool)\n",
615
+ "# initilaize src_ids and tgt_ids randomly with big lengths\n",
616
+ "\"\"\"src_ids = torch.randint(0, scfg.tokenizer.vocab_size, (2, 50)).to(device, dtype=torch.long)\n",
617
+ "tgt_ids = torch.randint(0, scfg.tokenizer.vocab_size, (2, 50)).to(device, dtype=torch.long)\n",
618
+ "src_padd_mask = (src_ids == scfg.tokenizer.pad_id).to(device, dtype=torch.bool)\n",
619
+ "tgt_padd_mask = (tgt_ids == scfg.tokenizer.pad_id).to(device, dtype=torch.bool)\"\"\""
620
+ ]
621
+ },
622
+ {
623
+ "cell_type": "code",
624
+ "execution_count": null,
625
+ "id": "689fe371",
626
+ "metadata": {},
627
+ "outputs": [],
628
+ "source": [
629
+ "# Optional transformer debugging / attention capture\n",
630
+ "debug_forward_options = {\n",
631
+ " \"enabled\": True, # flip to True to run debug_transformer_forward\n",
632
+ " \"show_attention\": False, # show attention plots interactively\n",
633
+ " \"save_attention\": False, # persist attention artefacts\n",
634
+ " \"attention_layers\": None, # e.g. [0, 1] to restrict encoder/decoder layers\n",
635
+ " \"attention_heads\": None, # e.g. [0, 3] to filter heads\n",
636
+ " \"attention_types\": (\"enc_self\", \"dec_self\", \"dec_cross\"),\n",
637
+ " \"average_heads\": False, # average selected heads into a single map\n",
638
+ " \"sample_index\": 0, # which row within the batch to inspect\n",
639
+ " \"attention_figsize\": (2.4, 2.4),\n",
640
+ " \"skip_special_tokens\": False,\n",
641
+ " \"save_dir\": None, # override directory; defaults to run_dir/debug/attention\n",
642
+ " \"return_maps\": False, # set True to receive raw attention tensors\n",
643
+ "}\n",
644
+ "\n",
645
+ "if debug_forward_options[\"save_dir\"]:\n",
646
+ " debug_default_attention_dir = Path(debug_forward_options[\"save_dir\"])\n",
647
+ " if not debug_default_attention_dir.is_absolute():\n",
648
+ " debug_default_attention_dir = run_dir / debug_default_attention_dir\n",
649
+ "else:\n",
650
+ " debug_default_attention_dir = run_dir / \"debug\" / \"attention\"\n",
651
+ "debug_default_attention_dir.mkdir(parents=True, exist_ok=True)"
652
+ ]
653
+ },
654
+ {
655
+ "cell_type": "code",
656
+ "execution_count": null,
657
+ "id": "af2e9c6a",
658
+ "metadata": {},
659
+ "outputs": [],
660
+ "source": [
661
+ "if debug_forward_options.get(\"enabled\", False):\n",
662
+ " sample_index = int(debug_forward_options.get(\"sample_index\", 0))\n",
663
+ " sample_index = max(0, min(sample_index, src_ids.size(0) - 1))\n",
664
+ " debug_result = debug_transformer_forward(\n",
665
+ " model=model,\n",
666
+ " tokenizer=tokenizer,\n",
667
+ " src_ids=src_ids,\n",
668
+ " tgt_ids=tgt_ids,\n",
669
+ " pad_id=scfg.tokenizer.pad_id,\n",
670
+ " device=device,\n",
671
+ " batch_index=0,\n",
672
+ " sample_index=sample_index,\n",
673
+ " logits=None,\n",
674
+ " show_attention=debug_forward_options.get(\"show_attention\", False),\n",
675
+ " attention_layers=debug_forward_options.get(\"attention_layers\"),\n",
676
+ " attention_heads=debug_forward_options.get(\"attention_heads\"),\n",
677
+ " attention_types=debug_forward_options.get(\n",
678
+ " \"attention_types\", (\"enc_self\", \"dec_self\", \"dec_cross\")\n",
679
+ " ),\n",
680
+ " average_heads=debug_forward_options.get(\"average_heads\", False),\n",
681
+ " save_attention=debug_forward_options.get(\"save_attention\", False),\n",
682
+ " save_dir=debug_default_attention_dir,\n",
683
+ " run_dir=run_dir,\n",
684
+ " attention_figsize=debug_forward_options.get(\"attention_figsize\", (2.4, 2.4)),\n",
685
+ " skip_special_tokens=debug_forward_options.get(\"skip_special_tokens\", False),\n",
686
+ " return_maps=debug_forward_options.get(\"return_maps\", False),\n",
687
+ " )\n",
688
+ " print(\"[debug] transformer forward summary saved to\", debug_result.get(\"attention\", {}))\n",
689
+ "else:\n",
690
+ " debug_result = None"
691
+ ]
692
+ },
693
+ {
694
+ "cell_type": "code",
695
+ "execution_count": null,
696
+ "id": "439d4094",
697
+ "metadata": {},
698
+ "outputs": [],
699
+ "source": [
700
+ "model.to(device)\n",
701
+ "model.enable_gradient_checkpointing(cfg.trainer.gradient_checkpointing)\n",
702
+ "\n",
703
+ "optimizer.zero_grad(set_to_none=True)\n",
704
+ "\n",
705
+ "optimizer_step = completed_optimizer_steps\n",
706
+ "current_resume_batch = resume_batch_idx\n",
707
+ "current_resume_running_loss = resume_running_loss\n",
708
+ "\n",
709
+ "for epoch in range(start_epoch, scfg.trainer.epochs):\n",
710
+ " model.train()\n",
711
+ " running_loss = current_resume_running_loss if epoch == start_epoch else 0.0\n",
712
+ " batches_processed = 0\n",
713
+ " last_batch_idx = -1\n",
714
+ " for batch_idx, (src, tgt) in enumerate(\n",
715
+ " tqdm(train_dataloader, desc=f\"Epoch {epoch + 1} [train]\")\n",
716
+ " ):\n",
717
+ " if epoch == start_epoch and batch_idx < current_resume_batch:\n",
718
+ " continue\n",
719
+ " if epoch == start_epoch and current_resume_batch and batch_idx == current_resume_batch:\n",
720
+ " current_resume_batch = 0\n",
721
+ " current_resume_running_loss = 0.0\n",
722
+ "\n",
723
+ " batches_processed += 1\n",
724
+ " last_batch_idx = batch_idx\n",
725
+ "\n",
726
+ " encoded_src = tokenizer(\n",
727
+ " src,\n",
728
+ " padding=True,\n",
729
+ " truncation=True,\n",
730
+ " return_tensors=\"pt\",\n",
731
+ " add_special_tokens=False,\n",
732
+ " )\n",
733
+ " encoded_tgt = tokenizer(\n",
734
+ " tgt,\n",
735
+ " padding=True,\n",
736
+ " truncation=True,\n",
737
+ " return_tensors=\"pt\",\n",
738
+ " )\n",
739
+ " tgt_ids = encoded_tgt[\"input_ids\"].to(device)\n",
740
+ " decoder_in = tgt_ids[:, :-1]\n",
741
+ " labels = tgt_ids[:, 1:]\n",
742
+ "\n",
743
+ " src_ids = encoded_src[\"input_ids\"].to(device)\n",
744
+ " src_padd_mask = (encoded_src[\"attention_mask\"] == 0).to(device)\n",
745
+ " tgt_padd_mask = decoder_in.eq(scfg.tokenizer.pad_id).to(device)\n",
746
+ "\n",
747
+ " with autocast_ctx:\n",
748
+ " logits = model(src_ids, decoder_in, src_padd_mask, tgt_padd_mask)\n",
749
+ " loss = criterion(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))\n",
750
+ "\n",
751
+ " running_loss += loss.item()\n",
752
+ " loss = loss / scfg.trainer.gradient_accumulation\n",
753
+ " scaled_loss = scaler.scale(loss)\n",
754
+ " scaled_loss.backward()\n",
755
+ "\n",
756
+ " if (batch_idx + 1) % scfg.trainer.gradient_accumulation == 0:\n",
757
+ " scaler.unscale_(optimizer)\n",
758
+ " clip_grad_norm_(model.parameters(), cfg.trainer.clip_grad_norm)\n",
759
+ " scaler.step(optimizer)\n",
760
+ " scaler.update()\n",
761
+ " optimizer.zero_grad(set_to_none=True)\n",
762
+ " scheduler.step()\n",
763
+ " optimizer_step += 1\n",
764
+ " save_checkpoint(\n",
765
+ " epoch,\n",
766
+ " optimizer_step,\n",
767
+ " batch_idx=batch_idx + 1,\n",
768
+ " running_loss_value=running_loss,\n",
769
+ " )\n",
770
+ "\n",
771
+ " if batches_processed and (batches_processed % scfg.trainer.gradient_accumulation != 0):\n",
772
+ " scaler.unscale_(optimizer)\n",
773
+ " clip_grad_norm_(model.parameters(), cfg.trainer.clip_grad_norm)\n",
774
+ " scaler.step(optimizer)\n",
775
+ " scaler.update()\n",
776
+ " optimizer.zero_grad(set_to_none=True)\n",
777
+ " scheduler.step()\n",
778
+ " optimizer_step += 1\n",
779
+ " save_checkpoint(\n",
780
+ " epoch,\n",
781
+ " optimizer_step,\n",
782
+ " batch_idx=last_batch_idx + 1,\n",
783
+ " running_loss_value=running_loss,\n",
784
+ " )\n",
785
+ "\n",
786
+ " avg_train_loss = running_loss / len(train_dataloader)\n",
787
+ " print(f\"Epoch [{epoch + 1}/{scfg.trainer.epochs}] Train Loss: {avg_train_loss:.4f}\")\n",
788
+ "\n",
789
+ " train_losses.append(avg_train_loss)\n",
790
+ " last_lr = scheduler.get_last_lr()\n",
791
+ " learning_rates.append(float(last_lr[0]) if last_lr else optimizer.param_groups[0][\"lr\"])\n",
792
+ " latest_val_loss: float | None = None\n",
793
+ " if epoch % scfg.trainer.eval_interval == 0:\n",
794
+ " model.eval()\n",
795
+ " val_loss = 0.0\n",
796
+ " with torch.no_grad(), autocast_ctx:\n",
797
+ " for src, tgt in tqdm(validation_dataloader, desc=f\"Epoch {epoch + 1} [val]\"):\n",
798
+ " encoded_src = tokenizer(\n",
799
+ " src,\n",
800
+ " padding=True,\n",
801
+ " truncation=True,\n",
802
+ " return_tensors=\"pt\",\n",
803
+ " add_special_tokens=False,\n",
804
+ " )\n",
805
+ " encoded_tgt = tokenizer(\n",
806
+ " tgt,\n",
807
+ " padding=True,\n",
808
+ " truncation=True,\n",
809
+ " return_tensors=\"pt\",\n",
810
+ " )\n",
811
+ "\n",
812
+ " src_ids = encoded_src[\"input_ids\"].to(device)\n",
813
+ " src_padd_mask = (encoded_src[\"attention_mask\"] == 0).to(device)\n",
814
+ " tgt_ids = encoded_tgt[\"input_ids\"].to(device)\n",
815
+ " decoder_in = tgt_ids[:, :-1]\n",
816
+ " labels = tgt_ids[:, 1:]\n",
817
+ " tgt_padd_mask = decoder_in.eq(scfg.tokenizer.pad_id)\n",
818
+ "\n",
819
+ " logits = model(src_ids, decoder_in, src_padd_mask, tgt_padd_mask)\n",
820
+ " loss = criterion(logits.reshape(-1, logits.size(-1)), labels.reshape(-1))\n",
821
+ " val_loss += loss.item()\n",
822
+ "\n",
823
+ " avg_val_loss = val_loss / max(1, len(validation_dataloader))\n",
824
+ " print(f\"Epoch [{epoch + 1}/{scfg.trainer.epochs}] Val Loss: {avg_val_loss:.4f}\")\n",
825
+ " val_losses.append(avg_val_loss)\n",
826
+ " latest_val_loss = avg_val_loss\n",
827
+ "\n",
828
+ " model.train()\n",
829
+ "\n",
830
+ " is_new_best = latest_val_loss is not None and latest_val_loss < best_val_loss\n",
831
+ " if is_new_best:\n",
832
+ " best_val_loss = latest_val_loss\n",
833
+ " best_val_epoch = epoch + 1\n",
834
+ "\n",
835
+ " epoch_metadata = {\"train_loss\": avg_train_loss}\n",
836
+ " if latest_val_loss is not None:\n",
837
+ " epoch_metadata[\"val_loss\"] = latest_val_loss\n",
838
+ "\n",
839
+ " latest_payload = save_checkpoint(\n",
840
+ " epoch + 1,\n",
841
+ " optimizer_step,\n",
842
+ " batch_idx=0,\n",
843
+ " running_loss_value=0.0,\n",
844
+ " metadata=epoch_metadata,\n",
845
+ " )\n",
846
+ "\n",
847
+ " if is_new_best and best_checkpoint_path is not None:\n",
848
+ " torch.save(latest_payload, best_checkpoint_path)\n",
849
+ " _update_manifest(\n",
850
+ " \"best\",\n",
851
+ " {\n",
852
+ " \"path\": str(best_checkpoint_path),\n",
853
+ " \"epoch\": best_val_epoch,\n",
854
+ " \"optimizer_step\": optimizer_step,\n",
855
+ " \"train_loss\": avg_train_loss,\n",
856
+ " \"val_loss\": best_val_loss,\n",
857
+ " \"metadata\": latest_payload.get(\"metadata\", {}),\n",
858
+ " \"timestamp\": datetime.now().isoformat(),\n",
859
+ " },\n",
860
+ " )\n",
861
+ " print(\n",
862
+ " f\"New best checkpoint saved at {best_checkpoint_path} \"\n",
863
+ " f\"(epoch {best_val_epoch}, val {best_val_loss:.4f}, train {avg_train_loss:.4f})\"\n",
864
+ " )\n",
865
+ "\n",
866
+ " current_resume_batch = 0\n",
867
+ " current_resume_running_loss = 0.0"
868
+ ]
869
+ },
870
+ {
871
+ "cell_type": "markdown",
872
+ "id": "d55a574f",
873
+ "metadata": {},
874
+ "source": [
875
+ "Exporting"
876
+ ]
877
+ },
878
+ {
879
+ "cell_type": "code",
880
+ "execution_count": null,
881
+ "id": "ad75105a",
882
+ "metadata": {},
883
+ "outputs": [],
884
+ "source": [
885
+ "\"\"\"if scfg.trainer.export_best_to_model_path and best_checkpoint_export_target:\n",
886
+ " if best_checkpoint_path.exists():\n",
887
+ " best_checkpoint_export_target.parent.mkdir(parents=True, exist_ok=True)\n",
888
+ " shutil.copy2(best_checkpoint_path, best_checkpoint_export_target)\n",
889
+ " print(f\"[export] Best checkpoint copied to {best_checkpoint_export_target}\")\n",
890
+ " else:\n",
891
+ " print(f\"[warn] No best checkpoint found at {best_checkpoint_path} to export.\")\"\"\""
892
+ ]
893
+ },
894
+ {
895
+ "cell_type": "markdown",
896
+ "id": "4b21a8a3",
897
+ "metadata": {},
898
+ "source": [
899
+ "Plotting"
900
+ ]
901
+ },
902
+ {
903
+ "cell_type": "code",
904
+ "execution_count": null,
905
+ "id": "6875ddbe",
906
+ "metadata": {},
907
+ "outputs": [],
908
+ "source": [
909
+ "plots_dir = run_dir / \"plots\"\n",
910
+ "plots_dir.mkdir(parents=True, exist_ok=True)\n",
911
+ "\n",
912
+ "if train_losses:\n",
913
+ " epochs = list(range(1, len(train_losses) + 1))\n",
914
+ " fig, ax = plt.subplots(figsize=(8, 5))\n",
915
+ " ax.plot(epochs, train_losses, label=\"Training Loss\")\n",
916
+ " if val_losses:\n",
917
+ " val_epochs = list(range(1, len(val_losses) + 1))\n",
918
+ " ax.plot(val_epochs, val_losses, label=\"Validation Loss\")\n",
919
+ " ax.set_xlabel(\"Epochs\")\n",
920
+ " ax.set_ylabel(\"Loss\")\n",
921
+ " ax.set_title(\"Training, Validation Loss & Learning Rate\")\n",
922
+ " lines = ax.get_lines()\n",
923
+ " labels = [line.get_label() for line in lines]\n",
924
+ " if learning_rates:\n",
925
+ " lr_epochs = epochs[: len(learning_rates)]\n",
926
+ " ax2 = ax.twinx()\n",
927
+ " ax2.plot(\n",
928
+ " lr_epochs, learning_rates, label=\"Learning Rate\", color=\"tab:green\", linestyle=\"--\"\n",
929
+ " )\n",
930
+ " ax2.set_ylabel(\"Learning Rate\")\n",
931
+ " lr_lines = ax2.get_lines()\n",
932
+ " lines = list(lines) + list(lr_lines)\n",
933
+ " labels += [line.get_label() for line in lr_lines]\n",
934
+ " ax.legend(lines, labels, loc=\"upper right\")\n",
935
+ " plot_path = plots_dir / \"loss_curves.png\"\n",
936
+ " fig.savefig(plot_path, dpi=150, bbox_inches=\"tight\")\n",
937
+ " print(f\"[saved] Loss curves -> {plot_path}\")\n",
938
+ " plt.show()\n",
939
+ "else:\n",
940
+ " print(\"No training history available to plot.\")"
941
+ ]
942
+ },
943
+ {
944
+ "cell_type": "markdown",
945
+ "id": "f8652b1c",
946
+ "metadata": {},
947
+ "source": [
948
+ "Inference"
949
+ ]
950
+ },
951
+ {
952
+ "cell_type": "code",
953
+ "execution_count": null,
954
+ "id": "45a55eaf",
955
+ "metadata": {},
956
+ "outputs": [],
957
+ "source": [
958
+ "\"\"\"from scripts.inference import inference\n",
959
+ "GlobalHydra.instance().clear()\n",
960
+ "initialize(config_path=\"./configs\", version_base=None)\n",
961
+ "cfg = compose(config_name=\"infer_mode\")\n",
962
+ "print(inference(cfg))\"\"\""
963
+ ]
964
+ }
965
+ ],
966
+ "metadata": {
967
+ "kernelspec": {
968
+ "display_name": "transformer",
969
+ "language": "python",
970
+ "name": "python3"
971
+ },
972
+ "language_info": {
973
+ "codemirror_mode": {
974
+ "name": "ipython",
975
+ "version": 3
976
+ },
977
+ "file_extension": ".py",
978
+ "mimetype": "text/x-python",
979
+ "name": "python",
980
+ "nbconvert_exporter": "python",
981
+ "pygments_lexer": "ipython3",
982
+ "version": "3.12.11"
983
+ }
984
+ },
985
+ "nbformat": 4,
986
+ "nbformat_minor": 5
987
+ }
scripts/train.py DELETED
File without changes
src/transformer/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
- from .configs import BasicEncDecCfg, InferAppCfg, TrainAppCfg
2
  from .transformer import BasicEncoderDecoderTransformer
3
 
4
- __all__ = ["BasicEncoderDecoderTransformer", "BasicEncDecCfg", "TrainAppCfg", "InferAppCfg"]
 
 
1
  from .transformer import BasicEncoderDecoderTransformer
2
 
3
+ __all__ = ["BasicEncoderDecoderTransformer"]
src/transformer/configs.py CHANGED
@@ -1,78 +1,142 @@
 
 
 
 
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
 
1
+ """Typed dataclass containers mirroring the YAML configuration structure."""
2
+
3
+ from __future__ import annotations
4
+
5
  from dataclasses import dataclass
6
 
7
+ __all__ = [
8
+ "ModelCfg",
9
+ "DatasetCfg",
10
+ "OptimCfg",
11
+ "SchedCfg",
12
+ "TokenizerCfg",
13
+ "TrainerCfg",
14
+ "RuntimeCfg",
15
+ "GenerationCfg",
16
+ "TrainAppCfg",
17
+ "InferAppCfg",
18
+ ]
19
+
20
 
21
  @dataclass
22
+ class ModelCfg:
23
+ name: str
24
+ best_checkpoint_path: str
25
+ latest_checkpoint_path: str
26
+ tokenizer: str
27
  d_model: int
28
+ num_layers: int
29
  num_heads: int
30
  d_ff: int
31
+ dropout_rate: float
32
+ max_seq_len: int
33
+ vocab_size: int
34
+ pad_id: int
35
+ bos_id: int
36
+ eos_id: int
37
 
38
 
39
  @dataclass
40
+ class DatasetCfg:
41
+ name: str
42
+ path: str
 
 
 
43
 
44
 
45
  @dataclass
46
  class OptimCfg:
47
+ name: str
48
+ lr: float
49
+ betas: tuple[float, float]
50
+ weight_decay: float
51
+ eps: float
52
+ no_decay_on_bias_norm_embed: bool
53
 
54
 
55
  @dataclass
56
  class SchedCfg:
57
+ name: str
58
+ warmup_ratio: float
59
+ min_lr_ratio: float
60
+ max_lr_ratio: float
61
+ hold_min: bool
62
+
63
+
64
+ @dataclass
65
+ class TokenizerCfg:
66
+ name: str
67
+ path: str
68
+ corpus: str
69
+ dataset: str
70
+ vocab_size: int
71
+ max_seq_len: int
72
+ pad_token: str
73
+ bos_token: str
74
+ eos_token: str
75
+ unk_token: str
76
+ special_tokens: list[str]
77
+ pad_id: int
78
+ bos_id: int
79
+ eos_id: int
80
+ unk_id: int
81
 
82
 
83
  @dataclass
84
  class TrainerCfg:
85
+ epochs: int
86
+ batch_size: int
87
+ shuffle: bool
88
+ precision: str
89
+ gradient_accumulation: int
90
+ clip_grad_norm: float
91
+ eval_interval: int
92
+ save_interval: int
93
+ debug_interval: int
94
+ mixed_precision: bool
95
+ gradient_checkpointing: bool
96
+ label_smoothing: float
97
+ export_best_to_model_path: bool
98
+ resume: str
99
 
100
 
101
  @dataclass
102
  class RuntimeCfg:
103
+ seed: int
104
+ device: str
105
+ output_dir: str
106
+ data_dir: str
107
+ cache_dir: str
108
+ tokenizer_dir: str
109
+ checkpoint_path: str
110
+
111
+
112
+ @dataclass
113
+ class GenerationCfg:
114
+ max_new_tokens: int
115
+ temperature: float
116
+ top_k: int | None # None disables top-k
117
+ top_p: float | None
118
+ do_sample: bool
119
+ presence_penalty: float
120
+ frequency_penalty: float
121
+ no_repeat_ngram: int | None
122
+ min_steps_before_eos: int
123
 
124
 
125
  @dataclass
126
  class TrainAppCfg:
127
+ model: ModelCfg
128
+ dataset: DatasetCfg
 
129
  optim: OptimCfg
130
+ sched: SchedCfg
131
  trainer: TrainerCfg
132
  runtime: RuntimeCfg
133
+ tokenizer: TokenizerCfg
134
 
135
 
136
  @dataclass
137
  class InferAppCfg:
138
+ model: ModelCfg
139
  runtime: RuntimeCfg
140
+ tokenizer: TokenizerCfg
141
+ generation: GenerationCfg
142
+ input_text: str
 
 
 
src/transformer/modules/attention.py CHANGED
@@ -1,105 +1,132 @@
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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Multi-head attention building block with explicit validation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch import Tensor
10
+
11
+ from transformer.utils import (
12
+ calculate_attention,
13
+ combine_masks,
14
+ create_qk_padding_mask,
15
+ join_heads,
16
+ split_heads,
17
+ )
18
+
19
+ __all__ = ["MultiHeadAttention"]
20
+
21
+
22
+ class MultiHeadAttention(nn.Module):
23
+ """
24
+ Multi-head attention: linear projections -> split heads -> scaled dot-product
25
+ attention (via utils) -> merge heads -> output projection (+ dropout).
26
+
27
+ Args:
28
+ d_model (int): Model dimension (>0). Must be divisible by num_heads.
29
+ num_heads (int): Number of attention heads (>0).
30
+ dropout_rate (float): Dropout probability in (0,1).
31
+
32
+ Inputs:
33
+ query, key, value: (B, S, D) with D == d_model
34
+ mask (optional): Boolean tensor broadcastable to (B, H, S_q, S_k) where True entries are masked.
35
+
36
+ Returns:
37
+ Tensor: (B, S_q, D)
38
+ """
39
+
40
+ def __init__(self, d_model: int, num_heads: int, dropout_rate: float):
41
+ super().__init__()
42
+ # ---- type checks
43
+ if not isinstance(num_heads, int):
44
+ raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
45
+ if not isinstance(d_model, int):
46
+ raise TypeError(f"d_model must be an int, got {type(d_model)}")
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 num_heads <= 0:
52
+ raise ValueError(f"num_heads must be strictly greater than 0, got {num_heads}")
53
+ if d_model <= 0:
54
+ raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
55
+ if d_model % num_heads != 0:
56
+ raise ValueError("d_model must be divisible by num_heads")
57
+ if not (0 <= dropout_rate < 1):
58
+ raise ValueError(f"dropout_rate must be between 0 and 1 excluded, got {dropout_rate}")
59
+
60
+ self.d_model = d_model
61
+ self.num_heads = num_heads
62
+ self.d_head = d_model // num_heads
63
+ self.dropout_rate = dropout_rate
64
+
65
+ self.query_linear = nn.Linear(d_model, d_model, bias=False)
66
+ self.key_linear = nn.Linear(d_model, d_model, bias=False)
67
+ self.value_linear = nn.Linear(d_model, d_model, bias=False)
68
+ self.output_linear = nn.Linear(d_model, d_model, bias=True)
69
+
70
+ def forward(
71
+ self,
72
+ query: Tensor,
73
+ key: Tensor,
74
+ value: Tensor,
75
+ q_mask: Tensor,
76
+ k_mask: Tensor,
77
+ causal_mask: Tensor | None = None,
78
+ ) -> Tensor:
79
+ """Run multi-head attention with boolean padding and optional causal masks."""
80
+
81
+ # ---- basic type checks
82
+ if not isinstance(query, torch.Tensor):
83
+ raise TypeError(f"query must be a torch.Tensor, got {type(query)}")
84
+ if not isinstance(key, torch.Tensor):
85
+ raise TypeError(f"key must be a torch.Tensor, got {type(key)}")
86
+ if not isinstance(value, torch.Tensor):
87
+ raise TypeError(f"value must be a torch.Tensor, got {type(value)}")
88
+ if not isinstance(q_mask, torch.Tensor):
89
+ raise TypeError(f"q_mask must be a torch.Tensor, got {type(q_mask)}")
90
+ if k_mask is None or not isinstance(k_mask, torch.Tensor):
91
+ raise TypeError(f"k_mask must be a torch.Tensor, got {type(k_mask)}")
92
+
93
+ if query.dim() != 3 or key.dim() != 3 or value.dim() != 3:
94
+ raise ValueError(
95
+ "query/key/value must be 3D tensors of shape (B, S, D); "
96
+ f"got q={tuple(query.shape)}, k={tuple(key.shape)}, v={tuple(value.shape)}"
97
+ )
98
+ Bq, Sq, Dq = query.shape
99
+ Bk, Sk, Dk = key.shape
100
+ Bv, Sv, Dv = value.shape
101
+
102
+ if not (Dq == Dk == Dv == self.d_model):
103
+ raise ValueError(
104
+ f"Last dimension must equal d_model={self.d_model}; got Dq={Dq}, Dk={Dk}, Dv={Dv}"
105
+ )
106
+ if not (Bq == Bk == Bv):
107
+ raise ValueError(f"Batch size mismatch: q={Bq}, k={Bk}, v={Bv}")
108
+ if Sk != Sv:
109
+ raise ValueError(f"Key/Value seq length mismatch: Sk={Sk} vs Sv={Sv}")
110
+
111
+ # ---- padding mask validation ----
112
+ if q_mask.dtype != torch.bool:
113
+ raise TypeError(f"q_mask must be boolean, got {q_mask.dtype}")
114
+ if k_mask.dtype != torch.bool:
115
+ raise TypeError(f"k_mask must be boolean, got {k_mask.dtype}")
116
+ if q_mask.dim() != 4 or k_mask.dim() != 4:
117
+ raise ValueError(
118
+ "q_mask and k_mask must be 4D tensors shaped (B, H, 1, S); "
119
+ f"got {tuple(q_mask.shape)} and {tuple(k_mask.shape)}"
120
+ )
121
+
122
+ # ---- project and split into heads -> (B, H, S, Dh)
123
+ q = split_heads(self.query_linear(query), self.num_heads)
124
+ k = split_heads(self.key_linear(key), self.num_heads)
125
+ v = split_heads(self.value_linear(value), self.num_heads)
126
+ pad_mask = create_qk_padding_mask(q_mask, k_mask)
127
+ combined_mask = combine_masks(pad_mask, causal_mask)
128
+
129
+ p = self.dropout_rate if self.training else 0.0
130
+ attn = cast(Tensor, calculate_attention(q, k, v, combined_mask, dropout_p=p))
131
+ out = join_heads(attn) # (B, Sq, D)
132
+ return self.output_linear(out)
src/transformer/modules/decoder.py CHANGED
@@ -1,99 +1,171 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Decoder stack used by the encoder-decoder transformer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.utils.checkpoint as ckpt
8
+ from torch import Tensor
9
+
10
+ from transformer.modules.attention import MultiHeadAttention
11
+ from transformer.modules.feedforward import FeedForwardLayer
12
+
13
+ __all__ = ["DecoderLayer", "TransformerDecoder"]
14
+
15
+
16
+ class DecoderLayer(nn.Module):
17
+ """Single decoder block with self-attention, cross-attention, and feed-forward."""
18
+
19
+ def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float) -> None:
20
+ super().__init__()
21
+
22
+ if not isinstance(d_model, int):
23
+ raise TypeError(f"d_model must be an int, got {type(d_model)}")
24
+ if not isinstance(num_heads, int):
25
+ raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
26
+ if not isinstance(d_ff, int):
27
+ raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
28
+ if not isinstance(dropout_rate, float):
29
+ raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
30
+
31
+ if d_model <= 0:
32
+ raise ValueError("d_model must be strictly greater than 0")
33
+ if num_heads <= 0:
34
+ raise ValueError("num_heads must be strictly greater than 0")
35
+ if d_ff <= 0:
36
+ raise ValueError("d_ff must be strictly greater than 0")
37
+ if not 0.0 <= dropout_rate < 1.0:
38
+ raise ValueError("dropout_rate must be in [0, 1)")
39
+
40
+ self.self_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
41
+ self.cross_attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
42
+ self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
43
+
44
+ self.norm1 = nn.LayerNorm(d_model)
45
+ self.dropout1 = nn.Dropout(dropout_rate)
46
+ self.norm2 = nn.LayerNorm(d_model)
47
+ self.dropout2 = nn.Dropout(dropout_rate)
48
+ self.norm3 = nn.LayerNorm(d_model)
49
+ self.dropout3 = nn.Dropout(dropout_rate)
50
+
51
+ def forward(
52
+ self,
53
+ x: Tensor,
54
+ y: Tensor,
55
+ src_padding_mask: Tensor,
56
+ tgt_padding_mask: Tensor,
57
+ tgt_causal_mask: Tensor | None,
58
+ ) -> Tensor:
59
+ """Run one decoder layer using post-layer-normalisation formulation."""
60
+
61
+ if not isinstance(x, torch.Tensor):
62
+ raise TypeError("x must be a torch.Tensor")
63
+ if x.dim() != 3:
64
+ raise ValueError(
65
+ f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
66
+ )
67
+ if not isinstance(y, torch.Tensor):
68
+ raise TypeError("y must be a torch.Tensor")
69
+ if y.dim() != 3:
70
+ raise ValueError(
71
+ f"y must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(y.shape)}"
72
+ )
73
+ if x.shape[0] != y.shape[0] or x.shape[-1] != y.shape[-1]:
74
+ raise ValueError("Encoder memory and decoder input must match in batch and d_model")
75
+
76
+ for mask_name, mask in (
77
+ ("src_padding_mask", src_padding_mask),
78
+ ("tgt_padding_mask", tgt_padding_mask),
79
+ ):
80
+ if not isinstance(mask, torch.Tensor):
81
+ raise TypeError(f"{mask_name} must be a torch.Tensor")
82
+ if mask.dtype != torch.bool or mask.dim() != 4:
83
+ raise TypeError(
84
+ f"{mask_name} must be boolean with shape (B, H, 1, S);"
85
+ f" got dtype {mask.dtype} and shape {tuple(mask.shape)}"
86
+ )
87
+
88
+ # Self-attention
89
+ self_attn_out = self.self_attention_layer(
90
+ y,
91
+ y,
92
+ y,
93
+ tgt_padding_mask,
94
+ tgt_padding_mask,
95
+ tgt_causal_mask,
96
+ )
97
+ y = self.norm1(y + self.dropout1(self_attn_out))
98
+
99
+ # Cross-attention (encoder memory as keys/values)
100
+ cross_attn_out = self.cross_attention_layer(
101
+ y,
102
+ x,
103
+ x,
104
+ tgt_padding_mask,
105
+ src_padding_mask,
106
+ )
107
+ y = self.norm2(y + self.dropout2(cross_attn_out))
108
+
109
+ # Feed-forward block
110
+ ff_out = self.feed_forward(y)
111
+ return self.norm3(y + self.dropout3(ff_out))
112
+
113
+
114
+ class TransformerDecoder(nn.Module):
115
+ """Stack of decoder layers with optional activation checkpointing."""
116
+
117
+ def __init__(
118
+ self,
119
+ d_model: int,
120
+ num_heads: int,
121
+ d_ff: int,
122
+ num_layers: int,
123
+ dropout_rate: float,
124
+ ) -> None:
125
+ super().__init__()
126
+
127
+ if not isinstance(num_layers, int):
128
+ raise TypeError(f"num_layers must be an int, got {type(num_layers)}")
129
+ if num_layers <= 0:
130
+ raise ValueError("num_layers must be strictly greater than 0")
131
+
132
+ self.layers = nn.ModuleList(
133
+ [DecoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
134
+ )
135
+ self.use_ckpt = False
136
+
137
+ def forward(
138
+ self,
139
+ x: Tensor,
140
+ y: Tensor,
141
+ src_padding_mask: Tensor,
142
+ tgt_padding_mask: Tensor,
143
+ tgt_causal_mask: Tensor | None,
144
+ ) -> Tensor:
145
+ """Run the decoder stack for all time steps."""
146
+
147
+ if not isinstance(x, torch.Tensor):
148
+ raise TypeError("x must be a torch.Tensor")
149
+ if x.dim() != 3:
150
+ raise ValueError(
151
+ f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
152
+ )
153
+ if not isinstance(y, torch.Tensor):
154
+ raise TypeError("y must be a torch.Tensor")
155
+ if y.dim() != 3:
156
+ raise ValueError(
157
+ f"y must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(y.shape)}"
158
+ )
159
+ if x.shape[0] != y.shape[0] or x.shape[-1] != y.shape[-1]:
160
+ raise ValueError("Encoder memory and decoder input must match in batch and d_model")
161
+
162
+ for layer in self.layers:
163
+ if self.use_ckpt:
164
+
165
+ def _fn(y_, *, _layer=layer):
166
+ return _layer(x, y_, src_padding_mask, tgt_padding_mask, tgt_causal_mask)
167
+
168
+ y = ckpt.checkpoint(_fn, y, use_reentrant=False)
169
+ else:
170
+ y = layer(x, y, src_padding_mask, tgt_padding_mask, tgt_causal_mask)
171
+ return y
src/transformer/modules/embedding.py CHANGED
@@ -1,121 +1,146 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Embedding modules used by the transformer architecture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch import Tensor
10
+
11
+ from transformer.utils import sinusoidal_positional_encoding
12
+
13
+ __all__ = ["InputEmbedding", "PositionalEmbedding", "LearnedPositionalEmbedding"]
14
+
15
+
16
+ class InputEmbedding(nn.Module):
17
+ """Token and positional embedding with optional dropout."""
18
+
19
+ def __init__(
20
+ self,
21
+ vocab_size: int,
22
+ d_model: int,
23
+ max_seq_len: int,
24
+ pad_id: int,
25
+ dropout_rate: float = 0.0,
26
+ ) -> None:
27
+ super().__init__()
28
+
29
+ if not isinstance(vocab_size, int):
30
+ raise TypeError(f"vocab_size must be int, got {type(vocab_size)}")
31
+ if not isinstance(d_model, int):
32
+ raise TypeError(f"d_model must be int, got {type(d_model)}")
33
+ if not isinstance(max_seq_len, int):
34
+ raise TypeError(f"max_seq_len must be int, got {type(max_seq_len)}")
35
+ if not isinstance(pad_id, int):
36
+ raise TypeError(f"pad_id must be int, got {type(pad_id)}")
37
+ if not isinstance(dropout_rate, float):
38
+ raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
39
+
40
+ if vocab_size <= 0:
41
+ raise ValueError("vocab_size must be > 0")
42
+ if d_model <= 0:
43
+ raise ValueError("d_model must be > 0")
44
+ if max_seq_len <= 0:
45
+ raise ValueError("max_seq_len must be > 0")
46
+ if not (0 <= pad_id < vocab_size):
47
+ raise ValueError(f"pad_id must be in [0, {vocab_size - 1}]")
48
+ if not 0.0 <= dropout_rate < 1.0:
49
+ raise ValueError("dropout_rate must be in [0, 1)")
50
+
51
+ self.vocab_size = vocab_size
52
+ self.d_model = d_model
53
+ self.max_seq_len = max_seq_len
54
+ self.pad_id = pad_id
55
+
56
+ self.token_embed = nn.Embedding(vocab_size, d_model, padding_idx=pad_id)
57
+ self.pos_embed = PositionalEmbedding(max_seq_len, d_model)
58
+ self.dropout = nn.Dropout(dropout_rate)
59
+
60
+ def forward(self, x: Tensor) -> Tensor:
61
+ if not isinstance(x, torch.Tensor):
62
+ raise TypeError(f"x must be Tensor, got {type(x)}")
63
+ if x.dtype != torch.long:
64
+ raise TypeError(f"x must be torch.long, got {x.dtype}")
65
+ if x.dim() != 2:
66
+ raise ValueError(f"x must be 2D (B, S), got shape {tuple(x.shape)}")
67
+ if x.size(1) > self.max_seq_len:
68
+ raise ValueError(f"Sequence length {x.size(1)} exceeds max_seq_len {self.max_seq_len}")
69
+
70
+ tokens = self.token_embed(x) * (self.d_model**0.5)
71
+ tokens = self.pos_embed(tokens)
72
+ return self.dropout(tokens)
73
+
74
+
75
+ class PositionalEmbedding(nn.Module):
76
+ """Fixed sinusoidal positional encodings."""
77
+
78
+ def __init__(self, max_seq_len: int, d_model: int) -> None:
79
+ super().__init__()
80
+
81
+ if not isinstance(max_seq_len, int):
82
+ raise TypeError(f"max_seq_len must be int, got {type(max_seq_len)}")
83
+ if not isinstance(d_model, int):
84
+ raise TypeError(f"d_model must be int, got {type(d_model)}")
85
+ if max_seq_len < 0:
86
+ raise ValueError("max_seq_len must be >= 0")
87
+ if d_model <= 0:
88
+ raise ValueError("d_model must be > 0")
89
+
90
+ self.max_seq_len = max_seq_len
91
+ self.d_model = d_model
92
+
93
+ pe = sinusoidal_positional_encoding(max_seq_len, d_model)
94
+ self.register_buffer("pe", pe.unsqueeze(0))
95
+
96
+ def forward(self, x: Tensor) -> Tensor:
97
+ if not isinstance(x, torch.Tensor):
98
+ raise TypeError(f"x must be Tensor, got {type(x)}")
99
+ if x.dim() != 3:
100
+ raise ValueError(f"x must be 3D (B, S, D), got shape {tuple(x.shape)}")
101
+
102
+ _, seq_len, dim = x.shape
103
+ if dim != self.d_model:
104
+ raise ValueError(f"d_model mismatch: got {dim}, expected {self.d_model}")
105
+ if seq_len > self.max_seq_len:
106
+ raise ValueError(f"seq_len {seq_len} exceeds max_seq_len {self.max_seq_len}")
107
+
108
+ pe_buffer = cast(Tensor, self.pe)
109
+ return x + pe_buffer[:, :seq_len].to(dtype=x.dtype, device=x.device)
110
+
111
+
112
+ class LearnedPositionalEmbedding(nn.Module):
113
+ """Learned positional embeddings compatible with :class:`InputEmbedding`."""
114
+
115
+ def __init__(self, max_seq_len: int, d_model: int) -> None:
116
+ super().__init__()
117
+
118
+ if not isinstance(max_seq_len, int):
119
+ raise TypeError(f"max_seq_len must be int, got {type(max_seq_len)}")
120
+ if not isinstance(d_model, int):
121
+ raise TypeError(f"d_model must be int, got {type(d_model)}")
122
+ if max_seq_len < 0:
123
+ raise ValueError("max_seq_len must be >= 0")
124
+ if d_model <= 0:
125
+ raise ValueError("d_model must be > 0")
126
+
127
+ self.max_seq_len = max_seq_len
128
+ self.d_model = d_model
129
+ self.pos_table = nn.Embedding(max_seq_len, d_model)
130
+ nn.init.normal_(self.pos_table.weight, mean=0.0, std=0.02)
131
+
132
+ def forward(self, x: Tensor) -> Tensor:
133
+ if not isinstance(x, torch.Tensor):
134
+ raise TypeError(f"x must be Tensor, got {type(x)}")
135
+ if x.dim() != 3:
136
+ raise ValueError(f"x must be 3D (B, S, D), got {tuple(x.shape)}")
137
+
138
+ batch, seq_len, dim = x.shape
139
+ if dim != self.d_model:
140
+ raise ValueError(f"d_model mismatch: got {dim}, expected {self.d_model}")
141
+ if seq_len > self.max_seq_len:
142
+ raise ValueError(f"seq_len {seq_len} exceeds max_seq_len {self.max_seq_len}")
143
+
144
+ positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch, seq_len)
145
+ pos_emb = self.pos_table(positions)
146
+ return x + pos_emb
src/transformer/modules/encoder.py CHANGED
@@ -1,71 +1,108 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Encoder stack used by the transformer architecture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.utils.checkpoint as ckpt
8
+ from torch import Tensor
9
+
10
+ from transformer.modules.attention import MultiHeadAttention
11
+ from transformer.modules.feedforward import FeedForwardLayer
12
+
13
+ __all__ = ["EncoderLayer", "TransformerEncoder"]
14
+
15
+
16
+ class EncoderLayer(nn.Module):
17
+ """Self-attention + feed-forward block with post-layer-normalisation."""
18
+
19
+ def __init__(self, d_model: int, num_heads: int, d_ff: int, dropout_rate: float) -> None:
20
+ super().__init__()
21
+
22
+ if not isinstance(d_model, int):
23
+ raise TypeError(f"d_model must be an int, got {type(d_model)}")
24
+ if not isinstance(num_heads, int):
25
+ raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
26
+ if not isinstance(d_ff, int):
27
+ raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
28
+ if not isinstance(dropout_rate, float):
29
+ raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
30
+
31
+ if d_model <= 0:
32
+ raise ValueError("d_model must be strictly greater than 0")
33
+ if num_heads <= 0:
34
+ raise ValueError("num_heads must be strictly greater than 0")
35
+ if d_ff <= 0:
36
+ raise ValueError("d_ff must be strictly greater than 0")
37
+ if not 0.0 <= dropout_rate < 1.0:
38
+ raise ValueError("dropout_rate must be in [0, 1)")
39
+
40
+ self.attention_layer = MultiHeadAttention(d_model, num_heads, dropout_rate)
41
+ self.feed_forward = FeedForwardLayer(d_model, d_ff, dropout_rate)
42
+ self.norm1 = nn.LayerNorm(d_model)
43
+ self.dropout1 = nn.Dropout(dropout_rate)
44
+ self.norm2 = nn.LayerNorm(d_model)
45
+ self.dropout2 = nn.Dropout(dropout_rate)
46
+
47
+ def forward(self, x: Tensor, src_padding_mask: Tensor) -> Tensor:
48
+ if not isinstance(x, torch.Tensor):
49
+ raise TypeError("x must be a torch.Tensor")
50
+ if x.dim() != 3:
51
+ raise ValueError(
52
+ f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
53
+ )
54
+ if not isinstance(src_padding_mask, torch.Tensor):
55
+ raise TypeError("src_padding_mask must be a torch.Tensor")
56
+ if src_padding_mask.dtype != torch.bool or src_padding_mask.dim() != 4:
57
+ raise TypeError(
58
+ f"src_padding_mask must be boolean with shape (B, H, 1, S);"
59
+ f" got dtype {src_padding_mask.dtype} and shape {tuple(src_padding_mask.shape)}"
60
+ )
61
+
62
+ attn_out = self.attention_layer(x, x, x, src_padding_mask, src_padding_mask)
63
+ x = self.norm1(x + self.dropout1(attn_out))
64
+ ff_out = self.feed_forward(x)
65
+ return self.norm2(x + self.dropout2(ff_out))
66
+
67
+
68
+ class TransformerEncoder(nn.Module):
69
+ """Stack of encoder layers with optional activation checkpointing."""
70
+
71
+ def __init__(
72
+ self,
73
+ d_model: int,
74
+ num_heads: int,
75
+ d_ff: int,
76
+ num_layers: int,
77
+ dropout_rate: float,
78
+ ) -> None:
79
+ super().__init__()
80
+
81
+ if not isinstance(num_layers, int):
82
+ raise TypeError(f"num_layers must be an int, got {type(num_layers)}")
83
+ if num_layers <= 0:
84
+ raise ValueError("num_layers must be strictly greater than 0")
85
+
86
+ self.layers = nn.ModuleList(
87
+ [EncoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
88
+ )
89
+ self.use_ckpt = False
90
+
91
+ def forward(self, x: Tensor, src_padding_mask: Tensor) -> Tensor:
92
+ if not isinstance(x, torch.Tensor):
93
+ raise TypeError("x must be a torch.Tensor")
94
+ if x.dim() != 3:
95
+ raise ValueError(
96
+ f"x must be a 3D tensor of shape (B, S, D); got shape {tuple(x.shape)}"
97
+ )
98
+
99
+ for layer in self.layers:
100
+ if self.use_ckpt:
101
+
102
+ def _fn(x_, *, _layer=layer):
103
+ return _layer(x_, src_padding_mask)
104
+
105
+ x = ckpt.checkpoint(_fn, x, use_reentrant=False)
106
+ else:
107
+ x = layer(x, src_padding_mask)
108
+ return x
src/transformer/modules/feedforward.py CHANGED
@@ -1,59 +1,66 @@
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))))
 
 
 
 
 
 
 
 
1
+ """Position-wise feed-forward sub-layer used in transformer blocks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch import Tensor
8
+
9
+ __all__ = ["FeedForwardLayer"]
10
+
11
+
12
+ class FeedForwardLayer(nn.Module):
13
+ """
14
+ Position-wise feed-forward layer used in Transformer blocks.
15
+
16
+ Architecture:
17
+ fc1: Linear(d_model -> d_ff)
18
+ activation: ReLU
19
+ dropout: nn.Dropout(dropout_rate)
20
+ fc2: Linear(d_ff -> d_model)
21
+
22
+ Args:
23
+ d_model (int): Dimensionality of model embeddings.
24
+ d_ff (int): Hidden dimensionality of feed-forward layer.
25
+ dropout_rate (float): Dropout probability between 0 and 1 (exclusive).
26
+
27
+ Shape:
28
+ Input: (B, S, D) where D == d_model
29
+ Output: (B, S, D)
30
+ """
31
+
32
+ def __init__(self, d_model: int, d_ff: int, dropout_rate: float = 0.1):
33
+ super().__init__()
34
+
35
+ if not isinstance(d_ff, int):
36
+ raise TypeError(f"d_ff must be an int, got {type(d_ff)}")
37
+ if not isinstance(d_model, int):
38
+ raise TypeError(f"d_model must be an int, got {type(d_model)}")
39
+ if not isinstance(dropout_rate, float):
40
+ raise TypeError(f"dropout_rate must be a float, got {type(dropout_rate)}")
41
+
42
+ if d_ff <= 0:
43
+ raise ValueError(f"d_ff must be strictly greater than 0, got {d_ff}")
44
+ if d_model <= 0:
45
+ raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
46
+ if not (0.0 <= dropout_rate < 1.0):
47
+ raise ValueError(f"dropout_rate must be in [0,1), got {dropout_rate}")
48
+
49
+ self.d_model = d_model
50
+ self.d_ff = d_ff
51
+ self.dropout_rate = dropout_rate
52
+
53
+ self.fc1 = nn.Linear(d_model, d_ff)
54
+ self.relu = nn.ReLU()
55
+ self.dropout = nn.Dropout(dropout_rate)
56
+ self.fc2 = nn.Linear(d_ff, d_model)
57
+
58
+ def forward(self, x: Tensor) -> Tensor:
59
+ if not isinstance(x, torch.Tensor):
60
+ raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
61
+ if x.ndim != 3:
62
+ raise ValueError(f"x must be 3D of shape (B,S,D); got shape {tuple(x.shape)}")
63
+ if x.shape[-1] != self.d_model:
64
+ raise ValueError(f"Last dim {x.shape[-1]} must match d_model {self.d_model}")
65
+
66
+ return self.fc2(self.dropout(self.relu(self.fc1(x))))
src/transformer/modules/lm_head.py CHANGED
@@ -1,47 +1,54 @@
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)
 
 
 
 
 
 
 
 
1
+ """Lightweight language-model head projecting hidden states to vocab logits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ from torch import Tensor
8
+
9
+ __all__ = ["LMHead"]
10
+
11
+
12
+ class LMHead(nn.Module):
13
+ """
14
+ Language modeling head: projects hidden states to vocabulary logits.
15
+
16
+ Args:
17
+ d_model (int): Model hidden dimension (>0).
18
+ vocab_size (int): Vocabulary size (>0).
19
+
20
+ Input:
21
+ x (Tensor): shape (B, S, D) with D == d_model.
22
+
23
+ Output:
24
+ logits (Tensor): shape (B, S, V) with V == vocab_size.
25
+ """
26
+
27
+ def __init__(self, d_model: int, vocab_size: int):
28
+ super().__init__()
29
+ if not isinstance(vocab_size, int):
30
+ raise TypeError(f"vocab_size must be an int, got {type(vocab_size)}")
31
+ if not isinstance(d_model, int):
32
+ raise TypeError(f"d_model must be an int, got {type(d_model)}")
33
+ if vocab_size <= 0:
34
+ raise ValueError(f"vocab_size must be strictly greater than 0, got {vocab_size}")
35
+ if d_model <= 0:
36
+ raise ValueError(f"d_model must be strictly greater than 0, got {d_model}")
37
+
38
+ self.d_model = d_model
39
+ self.vocab_size = vocab_size
40
+ self.fc = nn.Linear(d_model, vocab_size, bias=False)
41
+
42
+ def forward(self, x: Tensor) -> Tensor:
43
+ if not isinstance(x, torch.Tensor):
44
+ raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
45
+ if x.dim() != 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
+
50
+ B, S, D = x.shape
51
+ if D != self.d_model:
52
+ raise ValueError(f"Last dim {D} must match d_model {self.d_model}")
53
+
54
+ return self.fc(x) # (B, S, V)
src/transformer/transformer.py CHANGED
@@ -1,239 +1,435 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Implementation of the encoder-decoder transformer used across the project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import cast
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ from torch import Tensor
11
+
12
+ from transformer import modules
13
+ from transformer.configs import ModelCfg
14
+ from transformer.utils import broadcast_padding_mask, create_causal_mask, sample_from_logits
15
+
16
+ __all__ = ["BasicEncoderDecoderTransformer"]
17
+
18
+
19
+ class BasicEncoderDecoderTransformer(nn.Module):
20
+ """Full encoder-decoder model with tied input/output embeddings."""
21
+
22
+ def __init__(self, cfg: ModelCfg) -> None:
23
+ super().__init__()
24
+
25
+ if not isinstance(cfg, ModelCfg):
26
+ raise TypeError(f"cfg must be a ModelCfg, got {type(cfg)}")
27
+
28
+ if not isinstance(cfg.vocab_size, int):
29
+ raise TypeError(f"vocab_size must be an int, got {type(cfg.vocab_size)}")
30
+ if not cfg.vocab_size > 0:
31
+ raise ValueError(f"vocab_size must be strictly greater than 0, got {cfg.vocab_size}")
32
+
33
+ if not isinstance(cfg.d_model, int):
34
+ raise TypeError(f"d_model must be an int, got {type(cfg.d_model)}")
35
+ if not cfg.d_model > 0:
36
+ raise ValueError(f"d_model must be strictly greater than 0, got {cfg.d_model}")
37
+
38
+ if not isinstance(cfg.max_seq_len, int):
39
+ raise TypeError(f"max_seq_len must be an int, got {type(cfg.max_seq_len)}")
40
+ if not cfg.max_seq_len > 0:
41
+ raise ValueError(f"max_seq_len must be greater than 0, got {cfg.max_seq_len}")
42
+ if not 0 <= cfg.dropout_rate < 1:
43
+ raise ValueError(f"dropout_rate must be in [0,1[, got {cfg.dropout_rate}")
44
+
45
+ if not isinstance(cfg.pad_id, int):
46
+ raise TypeError(f"pad_id must be an int, got {type(cfg.pad_id)}")
47
+ if not (0 <= cfg.pad_id < cfg.vocab_size):
48
+ raise ValueError(f"pad_id must be in [0, {cfg.vocab_size - 1}], got {cfg.pad_id}")
49
+ if not isinstance(cfg.bos_id, int):
50
+ raise TypeError(f"bos_id must be an int, got {type(cfg.bos_id)}")
51
+ if not (0 <= cfg.bos_id < cfg.vocab_size):
52
+ raise ValueError(f"bos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.bos_id}")
53
+ if not isinstance(cfg.eos_id, int):
54
+ raise TypeError(f"eos_id must be an int, got {type(cfg.eos_id)}")
55
+ if not (0 <= cfg.eos_id < cfg.vocab_size):
56
+ raise ValueError(f"eos_id must be in [0, {cfg.vocab_size - 1}], got {cfg.eos_id}")
57
+
58
+ self.cfg = cfg
59
+
60
+ self.embed = modules.InputEmbedding(
61
+ cfg.vocab_size, cfg.d_model, cfg.max_seq_len, cfg.pad_id, cfg.dropout_rate
62
+ )
63
+ self.encoder = modules.TransformerEncoder(
64
+ cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
65
+ )
66
+ self.decoder = modules.TransformerDecoder(
67
+ cfg.d_model, cfg.num_heads, cfg.d_ff, cfg.num_layers, cfg.dropout_rate
68
+ )
69
+
70
+ self.lm_head = modules.LMHead(cfg.d_model, cfg.vocab_size)
71
+
72
+ # tie weights
73
+ self.lm_head.fc.weight = self.embed.token_embed.weight
74
+
75
+ self.max_seq_len = cfg.max_seq_len
76
+ self.pad_id = cfg.pad_id
77
+ self.bos_id = cfg.bos_id
78
+ self.eos_id = cfg.eos_id
79
+
80
+ self.gradient_checkpointing = False
81
+
82
+ def enable_gradient_checkpointing(self, enabled: bool = True) -> None:
83
+ self.gradient_checkpointing = enabled
84
+ self.encoder.use_ckpt = enabled
85
+ self.decoder.use_ckpt = enabled
86
+
87
+ def forward(
88
+ self,
89
+ src_ids: Tensor,
90
+ tgt_ids: Tensor,
91
+ src_padding_mask: Tensor,
92
+ tgt_padding_mask: Tensor,
93
+ ) -> Tensor:
94
+ """Compute logits given input/output token ids and boolean padding masks."""
95
+
96
+ if not isinstance(src_ids, Tensor):
97
+ raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
98
+ if src_ids.dim() != 2:
99
+ raise ValueError(
100
+ f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
101
+ )
102
+ if src_ids.dtype != torch.long:
103
+ raise TypeError(f"src_idsmust be torch.long (int64), got {src_ids.dtype}")
104
+
105
+ if not isinstance(tgt_ids, Tensor):
106
+ raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}")
107
+ if tgt_ids.dim() != 2:
108
+ raise ValueError(
109
+ f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}"
110
+ )
111
+ if tgt_ids.dtype != torch.long:
112
+ raise TypeError(f"tgt_ids must be torch.long (int64), got {tgt_ids.dtype}")
113
+
114
+ if (
115
+ not isinstance(src_padding_mask, Tensor)
116
+ or src_padding_mask.dtype != torch.bool
117
+ or src_padding_mask.dim() != 2
118
+ ):
119
+ raise TypeError("src_padding_mask must be a boolean tensor shaped (B, S)")
120
+
121
+ if (
122
+ not isinstance(tgt_padding_mask, Tensor)
123
+ or tgt_padding_mask.dtype != torch.bool
124
+ or tgt_padding_mask.dim() != 2
125
+ ):
126
+ raise TypeError("tgt_padding_mask must be a boolean tensor shaped (B, S)")
127
+ src_padding_mask = broadcast_padding_mask(src_padding_mask, self.cfg.num_heads)
128
+ tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads)
129
+ hidden_states = self.encode(src_ids, src_padding_mask)
130
+ dec_hidden_states = self.decode(hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask)
131
+ logits = self.lm_head(dec_hidden_states)
132
+ return logits # (B, T_tgt, V)
133
+
134
+ def encode(self, src_ids: Tensor, src_padding_mask: Tensor) -> Tensor:
135
+ """Encode source tokens into memory representations."""
136
+
137
+ if not isinstance(src_ids, torch.Tensor):
138
+ raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
139
+ if src_ids.dim() != 2:
140
+ raise ValueError(
141
+ f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
142
+ )
143
+ if src_ids.dtype != torch.long:
144
+ raise TypeError("src_ids must be torch.long (int64)")
145
+
146
+ if (
147
+ not isinstance(src_padding_mask, Tensor)
148
+ or src_padding_mask.dtype != torch.bool
149
+ or src_padding_mask.dim() != 4
150
+ ):
151
+ raise TypeError("src_padding_mask must be a boolean tensor shaped (B, H, 1, S)")
152
+
153
+ x = self.embed(src_ids) # (B, Sx, D)
154
+ hidden_states = self.encoder(x, src_padding_mask)
155
+ return hidden_states # (B, Sx, D)
156
+
157
+ def decode(
158
+ self,
159
+ hidden_states: Tensor,
160
+ tgt_ids: Tensor,
161
+ src_padding_mask: Tensor,
162
+ tgt_padding_mask: Tensor,
163
+ ) -> Tensor:
164
+ if not isinstance(hidden_states, torch.Tensor):
165
+ raise TypeError(f"hidden_states must be a torch.Tensor, got {type(hidden_states)}")
166
+ if hidden_states.dim() != 3:
167
+ raise ValueError(
168
+ f"hidden_states must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(hidden_states.shape)}"
169
+ )
170
+
171
+ if not isinstance(tgt_ids, torch.Tensor):
172
+ raise TypeError(f"tgt_ids must be a torch.Tensor, got {type(tgt_ids)}")
173
+ if tgt_ids.dim() != 2:
174
+ raise ValueError(
175
+ f"tgt_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(tgt_ids.shape)}"
176
+ )
177
+ if tgt_ids.dtype != torch.long:
178
+ raise TypeError("tgt_ids must be torch.long (int64)")
179
+
180
+ if (
181
+ not isinstance(src_padding_mask, Tensor)
182
+ or src_padding_mask.dtype != torch.bool
183
+ or src_padding_mask.dim() != 4
184
+ ):
185
+ raise TypeError("src_padding_mask must be a boolean tensor shaped (B, H, 1, S)")
186
+
187
+ if (
188
+ not isinstance(tgt_padding_mask, Tensor)
189
+ or tgt_padding_mask.dtype != torch.bool
190
+ or tgt_padding_mask.dim() != 4
191
+ ):
192
+ raise TypeError("tgt_padding_mask must be a boolean tensor shaped (B, H, 1, S)")
193
+
194
+ tgt_causal_mask = create_causal_mask(tgt_ids, self.cfg.num_heads)
195
+
196
+ y = self.embed(tgt_ids) # (B, Sy, D)
197
+ out = self.decoder(hidden_states, y, src_padding_mask, tgt_padding_mask, tgt_causal_mask)
198
+ return out # (B, Sy, D)
199
+
200
+ def generate(
201
+ self,
202
+ src_ids: Tensor,
203
+ src_padding_mask: Tensor,
204
+ max_new_tokens: int = 20,
205
+ temperature: float = 1.0,
206
+ top_k: int | None = None,
207
+ top_p: float | None = None,
208
+ do_sample: bool = False,
209
+ presence_penalty: float = 0.0,
210
+ frequency_penalty: float = 0.0,
211
+ no_repeat_ngram: int | None = None,
212
+ min_steps_before_eos: int = 0,
213
+ *,
214
+ seed: int | None = None,
215
+ generator: torch.Generator | None = None,
216
+ ) -> Tensor:
217
+ if not isinstance(src_ids, torch.Tensor):
218
+ raise TypeError(f"src_ids must be a torch.Tensor, got {type(src_ids)}")
219
+ if src_ids.dim() != 2:
220
+ raise ValueError(
221
+ f"src_ids must be a 2D torch.Tensor of shape (B, S), got shape {tuple(src_ids.shape)}"
222
+ )
223
+ if src_ids.dtype != torch.long:
224
+ raise TypeError("src_ids must be torch.long (int64)")
225
+
226
+ if not isinstance(src_padding_mask, torch.Tensor):
227
+ raise TypeError(
228
+ f"src_padding_mask must be a torch.Tensor, got {type(src_padding_mask)}"
229
+ )
230
+
231
+ if not isinstance(max_new_tokens, int):
232
+ raise TypeError(f"max_new_tokens must be an int, got {type(max_new_tokens)}")
233
+ if max_new_tokens < 0:
234
+ raise ValueError(
235
+ f"max_new_tokens must be greater than or equal to 0, got {max_new_tokens}"
236
+ )
237
+ if not isinstance(temperature, float):
238
+ raise TypeError(f"temperature must be a float, got {type(temperature)}")
239
+ if not (temperature > 0.0):
240
+ raise ValueError(f"temperature must be strictly greater than 0, got {temperature}")
241
+ if top_k is not None and not isinstance(top_k, int):
242
+ raise TypeError(f"top_k must be an int or None, got {type(top_k)}")
243
+ if top_k is not None and top_k < 0:
244
+ raise ValueError(f"top_k must be >= 0, got {top_k}")
245
+ if top_p is not None and not isinstance(top_p, int | float):
246
+ raise TypeError(f"top_p must be a number or None, got {type(top_p)}")
247
+ if top_p is not None:
248
+ top_p = float(top_p)
249
+ if not (0.0 < top_p <= 1.0):
250
+ raise ValueError(f"top_p must be in (0, 1], got {top_p}")
251
+ if not isinstance(do_sample, bool):
252
+ raise TypeError(f"do_sample must be a bool, got {type(do_sample)}")
253
+ if not isinstance(presence_penalty, int | float) or not math.isfinite(
254
+ float(presence_penalty)
255
+ ):
256
+ raise ValueError(
257
+ f"presence_penalty must be a finite number >= 0, got {presence_penalty!r}."
258
+ )
259
+ presence_penalty = float(presence_penalty)
260
+ if presence_penalty < 0.0:
261
+ raise ValueError(f"presence_penalty must be >= 0, got {presence_penalty}.")
262
+ if not isinstance(frequency_penalty, int | float) or not math.isfinite(
263
+ float(frequency_penalty)
264
+ ):
265
+ raise ValueError(
266
+ f"frequency_penalty must be a finite number >= 0, got {frequency_penalty!r}."
267
+ )
268
+ frequency_penalty = float(frequency_penalty)
269
+ if frequency_penalty < 0.0:
270
+ raise ValueError(f"frequency_penalty must be >= 0, got {frequency_penalty}.")
271
+ if no_repeat_ngram is not None:
272
+ if not isinstance(no_repeat_ngram, int):
273
+ raise ValueError(f"no_repeat_ngram must be int or None, got {no_repeat_ngram!r}.")
274
+ if no_repeat_ngram < 2:
275
+ no_repeat_ngram = None # size <2 is meaningless; treat as disabled
276
+ if not isinstance(min_steps_before_eos, int) or min_steps_before_eos < 0:
277
+ raise ValueError(
278
+ f"min_steps_before_eos must be int >= 0, got {min_steps_before_eos!r}."
279
+ )
280
+ if seed is not None and not isinstance(seed, int):
281
+ raise TypeError(f"seed must be int or None, got {type(seed)}")
282
+ if generator is not None and not isinstance(generator, torch.Generator):
283
+ raise TypeError(f"generator must be a torch.Generator or None, got {type(generator)}")
284
+
285
+ self.eval()
286
+ with torch.no_grad():
287
+ batch_size, _ = src_ids.shape
288
+ device = src_ids.device
289
+ tgt_ids = torch.full((batch_size, 1), self.bos_id, device=device, dtype=torch.long)
290
+ allowed_new = max(0, self.max_seq_len - tgt_ids.size(1))
291
+ max_new_tokens = min(max_new_tokens, allowed_new)
292
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
293
+ src_padding_mask = broadcast_padding_mask(src_padding_mask, self.cfg.num_heads)
294
+ hidden_states = self.encode(src_ids, src_padding_mask)
295
+
296
+ def _make_generator() -> torch.Generator:
297
+ if device.type == "cpu":
298
+ return torch.Generator()
299
+ return torch.Generator(device=device)
300
+
301
+ rng = generator
302
+ if seed is not None:
303
+ rng = rng if rng is not None else _make_generator()
304
+ rng.manual_seed(seed)
305
+ elif rng is None and do_sample:
306
+ rng = _make_generator()
307
+ for step in range(max_new_tokens):
308
+ # decode on current tgt_ids; take last-step logits
309
+ tgt_padding_mask = tgt_ids != self.pad_id
310
+ tgt_padding_mask = broadcast_padding_mask(tgt_padding_mask, self.cfg.num_heads)
311
+ step_hidden = self.decode(
312
+ hidden_states, tgt_ids, src_padding_mask, tgt_padding_mask
313
+ ) # (B, T, D)
314
+ logits = self.lm_head(step_hidden)[:, -1, :] # (B, V)
315
+ disallowed = [self.pad_id] + ([self.eos_id] if step < min_steps_before_eos else [])
316
+ next_ids = cast(
317
+ Tensor,
318
+ sample_from_logits(
319
+ logits,
320
+ temperature=temperature,
321
+ top_k=top_k,
322
+ top_p=top_p,
323
+ do_sample=do_sample,
324
+ disallowed_tokens=disallowed,
325
+ repetition_ctx=tgt_ids,
326
+ presence_penalty=presence_penalty,
327
+ frequency_penalty=frequency_penalty,
328
+ no_repeat_ngram_size=no_repeat_ngram,
329
+ rng=rng,
330
+ ),
331
+ ) # (B,)
332
+ next_ids = torch.where(finished, torch.full_like(next_ids, self.eos_id), next_ids)
333
+ tgt_ids = torch.cat([tgt_ids, next_ids.unsqueeze(1)], dim=-1)
334
+ finished |= next_ids == self.eos_id
335
+ if finished.all():
336
+ return tgt_ids
337
+ return tgt_ids
338
+
339
+ def debug_generate(
340
+ self,
341
+ src_ids: Tensor,
342
+ src_padding_mask: Tensor | None,
343
+ tokenizer,
344
+ max_new_tokens: int = 20,
345
+ temperature: float = 1.0,
346
+ top_k: int | None = None,
347
+ top_p: float | None = None,
348
+ do_sample: bool = False,
349
+ *,
350
+ seed: int | None = None,
351
+ generator: torch.Generator | None = None,
352
+ ) -> Tensor:
353
+ """
354
+ Like generate(), but prints debug info at each step: input tokens, ids, output ids, tokens, logits, etc.
355
+ """
356
+ self.eval()
357
+ with torch.no_grad():
358
+ batch_size, _ = src_ids.shape
359
+ device = src_ids.device
360
+ tgt_ids = torch.full((batch_size, 1), self.bos_id, device=device, dtype=torch.long)
361
+ allowed_new = max(0, self.max_seq_len - tgt_ids.size(1))
362
+ max_new_tokens = min(max_new_tokens, allowed_new)
363
+ finished = torch.zeros(batch_size, dtype=torch.bool, device=device)
364
+ if src_padding_mask is not None:
365
+ if (
366
+ src_padding_mask.dtype != torch.bool
367
+ or src_padding_mask.dim() != 2
368
+ or src_padding_mask.size(0) != batch_size
369
+ ):
370
+ raise TypeError(
371
+ "src_padding_mask must be boolean tensor shaped (B, S) when provided"
372
+ )
373
+ src_mask_2d = src_padding_mask
374
+ else:
375
+ src_mask_2d = torch.ones_like(src_ids, dtype=torch.bool, device=device)
376
+ src_mask_4d = broadcast_padding_mask(src_mask_2d, self.cfg.num_heads)
377
+ hidden_states = self.encode(src_ids, src_mask_4d)
378
+
379
+ def _make_generator() -> torch.Generator:
380
+ if device.type == "cpu":
381
+ return torch.Generator()
382
+ return torch.Generator(device=device)
383
+
384
+ rng = generator
385
+ if seed is not None:
386
+ rng = rng if rng is not None else _make_generator()
387
+ rng.manual_seed(seed)
388
+ elif rng is None and do_sample:
389
+ rng = _make_generator()
390
+ # print("[DEBUG] Input ids:", src_ids)
391
+ print(
392
+ "[DEBUG] Input tokens:", tokenizer.batch_decode(src_ids, skip_special_tokens=False)
393
+ )
394
+ for step in range(max_new_tokens):
395
+ print(f"[DEBUG] Step {step + 1}")
396
+ # print(" Output ids so far:", tgt_ids)
397
+ print(
398
+ " Output tokens so far:",
399
+ tokenizer.batch_decode(tgt_ids, skip_special_tokens=False),
400
+ )
401
+ tgt_padding_mask_2d = tgt_ids != self.pad_id
402
+ tgt_padding_mask_4d = broadcast_padding_mask(
403
+ tgt_padding_mask_2d, self.cfg.num_heads
404
+ )
405
+ step_hidden = self.decode(
406
+ hidden_states, tgt_ids, src_mask_4d, tgt_padding_mask_4d
407
+ ) # (B, T, D)
408
+ logits = self.lm_head(step_hidden)[:, -1, :] # (B, V)
409
+ next_ids = cast(
410
+ Tensor,
411
+ sample_from_logits(
412
+ logits,
413
+ temperature=temperature,
414
+ top_k=top_k,
415
+ top_p=top_p,
416
+ do_sample=do_sample,
417
+ disallowed_tokens=[self.pad_id],
418
+ rng=rng,
419
+ ),
420
+ ) # (B,)
421
+ next_ids = torch.where(finished, torch.full_like(next_ids, self.eos_id), next_ids)
422
+ tgt_ids = torch.cat([tgt_ids, next_ids.unsqueeze(1)], dim=-1)
423
+ finished |= next_ids == self.eos_id
424
+
425
+ # print(" Next token ids:", next_ids)
426
+ print(
427
+ " Next tokens:",
428
+ tokenizer.batch_decode(next_ids.unsqueeze(1), skip_special_tokens=False),
429
+ )
430
+ # print(" Logits (first 5):", logits[0, :5].cpu().numpy() if logits.shape[0] > 0 else None)
431
+ if finished.all():
432
+ print("[DEBUG] All sequences finished.")
433
+ return tgt_ids
434
+ print("[DEBUG] Max steps reached.")
435
+ return tgt_ids
src/transformer/utils.py CHANGED
@@ -1,612 +1,1042 @@
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import random
5
+ from collections.abc import Callable, Iterable, Sequence
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import matplotlib.pyplot as plt
10
+ import numpy as np
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from torch import Tensor
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Global seeding utilities
17
+ # -----------------------------------------------------------------------------
18
+
19
+
20
+ def set_global_seed(seed: int, *, deterministic: bool = True) -> None:
21
+ """Seed Python, NumPy, and PyTorch (CPU and CUDA) for reproducible runs."""
22
+
23
+ random.seed(seed)
24
+ np.random.seed(seed)
25
+ torch.manual_seed(seed)
26
+ torch.cuda.manual_seed_all(seed)
27
+
28
+ if deterministic:
29
+ torch.backends.cudnn.deterministic = True
30
+ torch.backends.cudnn.benchmark = False
31
+
32
+
33
+ def make_worker_init_fn(seed: int) -> Callable[[int], None]:
34
+ """Return a DataLoader worker init function that derives unique seeds."""
35
+
36
+ def _init_fn(worker_id: int) -> None:
37
+ worker_seed = seed + worker_id
38
+ random.seed(worker_seed)
39
+ np.random.seed(worker_seed)
40
+ torch.manual_seed(worker_seed)
41
+
42
+ return _init_fn
43
+
44
+
45
+ # -----------------------------------------------------------------------------
46
+ # Attention building blocks
47
+ # -----------------------------------------------------------------------------
48
+
49
+
50
+ def split_heads(x: torch.Tensor, num_heads: int) -> torch.Tensor:
51
+ """Split the last dimension into ``(num_heads, d_head)`` and permute to (B, H, S, d_head)."""
52
+
53
+ if not isinstance(x, torch.Tensor):
54
+ raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
55
+ if not isinstance(num_heads, int):
56
+ raise TypeError(f"num_heads must be an int, got {type(num_heads)}")
57
+ if x.ndim != 3:
58
+ raise ValueError(
59
+ f"x must be a 3D torch.Tensor of shape (B, S, D); got shape {tuple(x.shape)}"
60
+ )
61
+ if num_heads <= 0:
62
+ raise ValueError(f"num_heads must be > 0; got {num_heads}")
63
+
64
+ batch_size, seq_length, d_model = x.shape
65
+ if d_model % num_heads != 0:
66
+ raise ValueError(
67
+ f"d_model ({d_model}) must be divisible by num_heads ({num_heads});"
68
+ f" got remainder {d_model % num_heads}"
69
+ )
70
+
71
+ d_head = d_model // num_heads
72
+ return x.reshape(batch_size, seq_length, num_heads, d_head).permute(0, 2, 1, 3)
73
+
74
+
75
+ def join_heads(x: torch.Tensor) -> torch.Tensor:
76
+ """Merge ``(num_heads, d_head)`` back into the model dimension."""
77
+
78
+ if not isinstance(x, torch.Tensor):
79
+ raise TypeError(f"Expected torch.Tensor, got {type(x)}")
80
+ if x.ndim != 4:
81
+ raise ValueError(
82
+ f"Expected 4D torch.Tensor (batch, num_heads, seq_len, d_head), got {tuple(x.shape)}"
83
+ )
84
+
85
+ batch_size, num_heads, seq_length, d_head = x.shape
86
+ return x.permute(0, 2, 1, 3).contiguous().view(batch_size, seq_length, num_heads * d_head)
87
+
88
+
89
+ def calculate_attention(
90
+ query: torch.Tensor,
91
+ key: torch.Tensor,
92
+ value: torch.Tensor,
93
+ mask: torch.Tensor | None,
94
+ *,
95
+ dropout_p: float = 0.0,
96
+ return_probs: bool = False,
97
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
98
+ """Scaled dot-product attention with optional dropout and probability return.
99
+
100
+ Args:
101
+ query, key, value: ``[B, H, S, Dh]`` tensors on the same device/dtype.
102
+ mask: Optional boolean tensor broadcastable to ``[B, H, Sq, Sk]`` where
103
+ ``True`` entries are masked. ``None`` disables masking.
104
+ dropout_p: Dropout probability applied to the attention weights. Callers
105
+ should pass ``0.0`` when not training.
106
+ return_probs: When ``True`` returns ``(context, probs)``; otherwise only
107
+ the context tensor is returned.
108
+ """
109
+
110
+ if not (query.dim() == key.dim() == value.dim() == 4):
111
+ raise ValueError(
112
+ "query, key, value must be 4D tensors shaped (B, H, S, Dh);"
113
+ f" got q={tuple(query.shape)}, k={tuple(key.shape)}, v={tuple(value.shape)}"
114
+ )
115
+ if query.device != key.device or query.device != value.device:
116
+ raise RuntimeError("query, key, value must be on the same device")
117
+
118
+ if mask is not None:
119
+ if mask.dtype != torch.bool:
120
+ raise TypeError("mask must be boolean when provided")
121
+ if mask.device != query.device:
122
+ mask = mask.to(query.device)
123
+ target_shape = (query.size(0), query.size(1), query.size(2), key.size(2))
124
+ if mask.shape != target_shape:
125
+ try:
126
+ mask = mask.expand(target_shape)
127
+ except RuntimeError as exc:
128
+ raise ValueError(
129
+ f"mask with shape {tuple(mask.shape)} not broadcastable to {target_shape}"
130
+ ) from exc
131
+
132
+ p = float(dropout_p)
133
+ if p < 0 or p >= 1:
134
+ raise ValueError(f"dropout_p must be in [0, 1), got {dropout_p}")
135
+
136
+ return _attention_with_probs(query, key, value, mask, p, return_probs)
137
+
138
+
139
+ def _attention_with_probs(
140
+ query: torch.Tensor,
141
+ key: torch.Tensor,
142
+ value: torch.Tensor,
143
+ mask: torch.Tensor | None,
144
+ dropout_p: float,
145
+ return_probs: bool,
146
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
147
+ """Manual attention path supporting optional probability return."""
148
+
149
+ head_dim = query.size(-1)
150
+ work_dtype = torch.float32 if query.dtype in (torch.float16, torch.bfloat16) else query.dtype
151
+
152
+ q = query.to(work_dtype)
153
+ k = key.to(work_dtype)
154
+ scores = torch.matmul(q, k.transpose(-2, -1))
155
+ scores.mul_(1.0 / math.sqrt(head_dim))
156
+
157
+ full_mask_rows = None
158
+ if mask is not None:
159
+ mask = mask.to(scores.device)
160
+ scores = scores.masked_fill(mask, torch.finfo(work_dtype).min)
161
+ full_mask_rows = mask.all(dim=-1, keepdim=True)
162
+ if full_mask_rows.any():
163
+ scores = scores.masked_fill(full_mask_rows, 0.0)
164
+
165
+ row_max = scores.max(dim=-1, keepdim=True).values
166
+ row_max = torch.where(torch.isfinite(row_max), row_max, torch.zeros_like(row_max))
167
+ logits = scores - row_max
168
+ probs = torch.softmax(logits, dim=-1)
169
+ if full_mask_rows is not None:
170
+ probs = torch.where(full_mask_rows, torch.zeros_like(probs), probs)
171
+
172
+ if dropout_p > 0.0:
173
+ probs = F.dropout(probs, p=dropout_p, training=True)
174
+
175
+ context = torch.matmul(probs.to(value.dtype), value)
176
+ if return_probs:
177
+ probs_out = probs.to(work_dtype)
178
+ return context, probs_out
179
+ return context
180
+
181
+
182
+ # -----------------------------------------------------------------------------
183
+ # Positional encodings
184
+ # -----------------------------------------------------------------------------
185
+
186
+
187
+ def sinusoidal_positional_encoding(S: int, D: int) -> torch.Tensor:
188
+ """Return the classic sinusoidal positional encoding table (shape ``[S, D]``)."""
189
+
190
+ if S <= 0 or D <= 0:
191
+ raise ValueError(f"S and D must be > 0, got S={S}, D={D}")
192
+
193
+ positions = torch.arange(S, dtype=torch.float32).unsqueeze(1)
194
+ div_terms = torch.exp(torch.arange(0, D, 2, dtype=torch.float32) * -(math.log(10000.0) / D))
195
+
196
+ pe = torch.zeros(S, D, dtype=torch.float32)
197
+ pe[:, 0::2] = torch.sin(positions * div_terms)
198
+ pe[:, 1::2] = torch.cos(positions * div_terms[: D // 2])
199
+ return pe
200
+
201
+
202
+ # -----------------------------------------------------------------------------
203
+ # Sampling utilities
204
+ # -----------------------------------------------------------------------------
205
+
206
+
207
+ def _ensure_2d_logits(logits: torch.Tensor) -> torch.Tensor:
208
+ """Reshape logits so that the last dimension is vocabulary sized and the rest flatten."""
209
+
210
+ if logits.dim() == 2:
211
+ return logits
212
+ if logits.dim() >= 3:
213
+ V = logits.size(-1)
214
+ return logits.reshape(-1, V)
215
+ raise ValueError(f"sample_from_logits: logits must be at least 2D, got {tuple(logits.shape)}")
216
+
217
+
218
+ def _apply_allow_deny_mask(
219
+ logits: torch.Tensor,
220
+ *,
221
+ allowed_tokens: Iterable[int] | None,
222
+ disallowed_tokens: Iterable[int] | None,
223
+ filter_value: float,
224
+ ) -> torch.Tensor:
225
+ if allowed_tokens is not None:
226
+ mask = torch.zeros_like(logits, dtype=torch.bool)
227
+ idx = torch.tensor(list(allowed_tokens), device=logits.device)
228
+ idx = idx[(idx >= 0) & (idx < logits.size(-1))]
229
+ if idx.numel() > 0:
230
+ mask.index_fill_(-1, idx, True)
231
+ logits = torch.where(mask, logits, torch.full_like(logits, filter_value))
232
+ if disallowed_tokens is not None:
233
+ idx = torch.tensor(list(disallowed_tokens), device=logits.device)
234
+ idx = idx[(idx >= 0) & (idx < logits.size(-1))]
235
+ if idx.numel() > 0:
236
+ logits.index_fill_(-1, idx, filter_value)
237
+ return logits
238
+
239
+
240
+ def _top_k_filtering(
241
+ logits: torch.Tensor,
242
+ top_k: int | None,
243
+ *,
244
+ min_tokens_to_keep: int,
245
+ filter_value: float,
246
+ ) -> torch.Tensor:
247
+ if top_k is None or top_k <= 0:
248
+ return logits
249
+ k = min(max(top_k, min_tokens_to_keep), logits.size(-1))
250
+ values, _ = torch.topk(logits, k, dim=-1)
251
+ threshold = values[..., -1, None]
252
+ return torch.where(logits < threshold, torch.full_like(logits, filter_value), logits)
253
+
254
+
255
+ def _top_p_filtering(
256
+ logits_scaled: torch.Tensor,
257
+ probs: torch.Tensor,
258
+ top_p: float,
259
+ *,
260
+ min_tokens_to_keep: int,
261
+ filter_value: float,
262
+ ) -> torch.Tensor:
263
+ if top_p is None or not (0.0 < top_p < 1.0):
264
+ return logits_scaled
265
+
266
+ sorted_probs, sorted_idx = torch.sort(probs, dim=-1, descending=True)
267
+ cumulative = torch.cumsum(sorted_probs, dim=-1)
268
+ to_remove = cumulative > top_p
269
+ to_remove[..., :min_tokens_to_keep] = False
270
+ scatter_mask = torch.zeros_like(to_remove, dtype=torch.bool).scatter(-1, sorted_idx, to_remove)
271
+ return torch.where(scatter_mask, torch.full_like(logits_scaled, filter_value), logits_scaled)
272
+
273
+
274
+ def sample_from_logits(
275
+ logits: torch.Tensor,
276
+ *,
277
+ do_sample: bool = False,
278
+ temperature: float = 1.0,
279
+ top_k: int | None = None,
280
+ top_p: float | None = None,
281
+ min_tokens_to_keep: int = 1,
282
+ allowed_tokens: Iterable[int] | None = None,
283
+ disallowed_tokens: Iterable[int] | None = None,
284
+ repetition_ctx: torch.Tensor | None = None,
285
+ presence_penalty: float = 0.0,
286
+ frequency_penalty: float = 0.0,
287
+ no_repeat_ngram_size: int | None = None,
288
+ filter_value: float = -float("inf"),
289
+ rng: torch.Generator | None = None,
290
+ return_probs: bool = False,
291
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
292
+ """Warp logits (temperature, top-k, nucleus, penalties) and produce next-token ids."""
293
+
294
+ if temperature <= 0:
295
+ raise ValueError("temperature must be > 0")
296
+
297
+ logits2d = _ensure_2d_logits(logits).to(dtype=torch.float32)
298
+ batch, vocab = logits2d.shape
299
+
300
+ logits2d = _apply_allow_deny_mask(
301
+ logits2d,
302
+ allowed_tokens=allowed_tokens,
303
+ disallowed_tokens=disallowed_tokens,
304
+ filter_value=filter_value,
305
+ )
306
+
307
+ if repetition_ctx is not None and (presence_penalty > 0.0 or frequency_penalty > 0.0):
308
+ if repetition_ctx.dim() != 2 or repetition_ctx.size(0) != batch:
309
+ raise ValueError(
310
+ f"repetition_ctx must be [B, T]; got {tuple(repetition_ctx.shape)} with B={batch}"
311
+ )
312
+ counts = torch.zeros((batch, vocab), device=logits2d.device, dtype=torch.float32)
313
+ ctx = repetition_ctx
314
+ valid = (ctx >= 0) & (ctx < vocab)
315
+ if valid.any():
316
+ ids = ctx.masked_select(valid)
317
+ bidx = (
318
+ torch.arange(batch, device=logits2d.device)
319
+ .unsqueeze(1)
320
+ .expand_as(ctx)
321
+ .masked_select(valid)
322
+ )
323
+ counts.index_put_(
324
+ (bidx, ids), torch.ones_like(ids, dtype=torch.float32), accumulate=True
325
+ )
326
+ if presence_penalty > 0.0:
327
+ logits2d = logits2d - presence_penalty * (counts > 0).to(logits2d.dtype)
328
+ if frequency_penalty > 0.0:
329
+ logits2d = logits2d - frequency_penalty * counts
330
+
331
+ if (
332
+ no_repeat_ngram_size is not None
333
+ and no_repeat_ngram_size >= 2
334
+ and repetition_ctx is not None
335
+ ):
336
+ ctx_cpu = repetition_ctx.detach().to("cpu")
337
+ for b, seq in enumerate(ctx_cpu.tolist()):
338
+ tokens = [t for t in seq if 0 <= t < vocab]
339
+ if len(tokens) < no_repeat_ngram_size:
340
+ continue
341
+ history: dict[tuple[int, ...], set[int]] = {}
342
+ n = int(no_repeat_ngram_size)
343
+ for i in range(len(tokens) - (n - 1)):
344
+ key = tuple(tokens[i : i + n - 1])
345
+ nxt = tokens[i + n - 1]
346
+ history.setdefault(key, set()).add(nxt)
347
+ key = tuple(tokens[-(n - 1) :])
348
+ banned = history.get(key)
349
+ if banned:
350
+ idx = torch.tensor(list(banned), device=logits2d.device, dtype=torch.long)
351
+ logits2d[b].index_fill_(0, idx, filter_value)
352
+
353
+ logits_scaled = logits2d / float(temperature)
354
+ logits_scaled = _top_k_filtering(
355
+ logits_scaled, top_k=top_k, min_tokens_to_keep=min_tokens_to_keep, filter_value=filter_value
356
+ )
357
+ probs = F.softmax(logits_scaled, dim=-1)
358
+ if top_p is not None and 0.0 < top_p < 1.0:
359
+ logits_scaled = _top_p_filtering(
360
+ logits_scaled,
361
+ probs,
362
+ top_p=top_p,
363
+ min_tokens_to_keep=min_tokens_to_keep,
364
+ filter_value=filter_value,
365
+ )
366
+ probs = F.softmax(logits_scaled, dim=-1)
367
+
368
+ if do_sample:
369
+ next_ids = torch.multinomial(probs, num_samples=1, replacement=True, generator=rng).squeeze(
370
+ -1
371
+ )
372
+ else:
373
+ next_ids = torch.argmax(probs, dim=-1)
374
+
375
+ next_ids = next_ids.to(dtype=torch.long, device=logits.device)
376
+ if return_probs:
377
+ return next_ids, probs.to(device=logits.device, dtype=probs.dtype)
378
+ return next_ids
379
+
380
+
381
+ # -----------------------------------------------------------------------------
382
+ # Attention mask helpers
383
+ # -----------------------------------------------------------------------------
384
+
385
+
386
+ def create_causal_mask(x: torch.Tensor, num_heads: int) -> torch.Tensor:
387
+ """Return a boolean causal mask ``[B, num_heads, S, S]`` for decoder self-attention."""
388
+
389
+ if x.ndim != 2:
390
+ raise ValueError(f"Expected input of shape (B, S), got {x.shape}")
391
+ if num_heads <= 0:
392
+ raise ValueError(f"num_heads must be > 0, got {num_heads}")
393
+
394
+ batch, seq_len = x.shape
395
+ if seq_len <= 0:
396
+ raise ValueError(f"Sequence length must be > 0, got {seq_len}")
397
+
398
+ base = torch.triu(torch.ones(seq_len, seq_len, dtype=torch.bool, device=x.device), diagonal=1)
399
+ return base.view(1, 1, seq_len, seq_len).expand(batch, num_heads, seq_len, seq_len).contiguous()
400
+
401
+
402
+ def create_qk_padding_mask(
403
+ query_attention_mask: torch.Tensor, key_attention_mask: torch.Tensor
404
+ ) -> torch.Tensor:
405
+ """Combine query and key padding masks (boolean) into a ``[B, H, Sq, Sk]`` mask."""
406
+
407
+ Bq, Hq, _, Sq = query_attention_mask.shape
408
+ Bk, Hk, _, Sk = key_attention_mask.shape
409
+ if Bq != Bk or Hq != Hk:
410
+ raise ValueError(
411
+ f"Padding mask batch/head mismatch: query {query_attention_mask.shape} vs key {key_attention_mask.shape}"
412
+ )
413
+
414
+ q_mask = query_attention_mask.to(torch.bool).view(Bq, Hq, Sq, 1)
415
+ k_mask = key_attention_mask.to(torch.bool).view(Bk, Hk, 1, Sk)
416
+ return (q_mask | k_mask).expand(Bq, Hq, Sq, Sk)
417
+
418
+
419
+ def broadcast_padding_mask(mask: torch.Tensor, num_heads: int) -> torch.Tensor:
420
+ """Expand a ``[B, S]`` padding mask to ``[B, H, 1, S]``."""
421
+
422
+ if not isinstance(mask, torch.Tensor):
423
+ raise TypeError(f"mask must be a torch.Tensor, got {type(mask)}")
424
+ if mask.dim() != 2:
425
+ raise ValueError(f"mask must be [B, S], got shape {tuple(mask.shape)}")
426
+ if not isinstance(num_heads, int) or num_heads <= 0:
427
+ raise ValueError(f"num_heads must be a positive int, got {num_heads}")
428
+
429
+ batch, seq_len = mask.shape
430
+ return mask.unsqueeze(1).unsqueeze(2).expand(batch, num_heads, 1, seq_len)
431
+
432
+
433
+ def combine_masks(
434
+ causal_mask: torch.Tensor | None, padding_mask: torch.Tensor | None
435
+ ) -> torch.Tensor | None:
436
+ """Combine causal and padding masks (boolean OR) while handling ``None`` values."""
437
+
438
+ if padding_mask is None:
439
+ return causal_mask.to(torch.bool) if causal_mask is not None else None
440
+ if causal_mask is None:
441
+ return padding_mask.to(torch.bool)
442
+
443
+ m1 = causal_mask.to(torch.bool)
444
+ m2 = padding_mask.to(torch.bool).to(device=m1.device)
445
+ try:
446
+ return m1 | m2
447
+ except RuntimeError as exc:
448
+ raise ValueError(
449
+ f"Masks not broadcastable: causal {tuple(m1.shape)} vs padding {tuple(m2.shape)}"
450
+ ) from exc
451
+
452
+
453
+ # -----------------------------------------------------------------------------
454
+ # Attention introspection / plotting
455
+ # -----------------------------------------------------------------------------
456
+
457
+
458
+ def extract_all_attention_maps(
459
+ model,
460
+ src_ids: torch.Tensor,
461
+ tgt_ids: torch.Tensor,
462
+ src_padding_2d: torch.Tensor,
463
+ tgt_padding_2d: torch.Tensor,
464
+ ):
465
+ """Collect attention probability tensors for encoder only/decoder self/cross attention."""
466
+
467
+ was_training = model.training
468
+ model.eval()
469
+ device = src_ids.device
470
+ num_heads = model.cfg.num_heads
471
+
472
+ src_pad_b = broadcast_padding_mask(src_padding_2d.to(device), num_heads)
473
+ tgt_pad_b = broadcast_padding_mask(tgt_padding_2d.to(device), num_heads)
474
+ tgt_causal = create_causal_mask(tgt_ids.to(device), num_heads)
475
+
476
+ x = model.embed(src_ids.to(device))
477
+ y = model.embed(tgt_ids.to(device))
478
+
479
+ enc_layers = model.encoder.layers
480
+ dec_layers = model.decoder.layers
481
+
482
+ enc_self_maps: list[Tensor] = []
483
+ dec_self_maps: list[Tensor] = []
484
+ dec_cross_maps: list[Tensor] = []
485
+
486
+ cur = x
487
+ for layer in enc_layers:
488
+ mha = layer.attention_layer
489
+ q = split_heads(mha.query_linear(cur), mha.num_heads)
490
+ k = split_heads(mha.key_linear(cur), mha.num_heads)
491
+ v = split_heads(mha.value_linear(cur), mha.num_heads)
492
+ mask = create_qk_padding_mask(src_pad_b, src_pad_b)
493
+ _, probs = calculate_attention(q, k, v, mask, dropout_p=0.0, return_probs=True)
494
+ enc_self_maps.append(probs)
495
+ cur = layer(cur, src_pad_b)
496
+
497
+ mem_full = cur
498
+
499
+ y_in = y
500
+ for layer in dec_layers:
501
+ self_mha = layer.self_attention_layer
502
+ q = split_heads(self_mha.query_linear(y_in), self_mha.num_heads)
503
+ k = split_heads(self_mha.key_linear(y_in), self_mha.num_heads)
504
+ v = split_heads(self_mha.value_linear(y_in), self_mha.num_heads)
505
+ pad = create_qk_padding_mask(tgt_pad_b, tgt_pad_b)
506
+ combined_mask = combine_masks(pad, tgt_causal)
507
+ self_ctx, self_probs = calculate_attention(
508
+ q, k, v, combined_mask, dropout_p=0.0, return_probs=True
509
+ )
510
+ dec_self_maps.append(self_probs)
511
+
512
+ self_out = self_mha(y_in, y_in, y_in, tgt_pad_b, tgt_pad_b, tgt_causal)
513
+ y_mid = y_in + layer.dropout1(layer.norm1(self_out))
514
+
515
+ cross_mha = layer.cross_attention_layer
516
+ cq = split_heads(cross_mha.query_linear(y_mid), cross_mha.num_heads)
517
+ ck = split_heads(cross_mha.key_linear(mem_full), cross_mha.num_heads)
518
+ cv = split_heads(cross_mha.value_linear(mem_full), cross_mha.num_heads)
519
+ cmask = create_qk_padding_mask(tgt_pad_b, src_pad_b)
520
+ _, cross_probs = calculate_attention(cq, ck, cv, cmask, dropout_p=0.0, return_probs=True)
521
+ dec_cross_maps.append(cross_probs)
522
+
523
+ y_in = layer(mem_full, y_in, src_pad_b, tgt_pad_b, tgt_causal)
524
+
525
+ maps = {"enc_self": enc_self_maps, "dec_self": dec_self_maps, "dec_cross": dec_cross_maps}
526
+ model.train(was_training)
527
+ return maps
528
+
529
+
530
+ def attach_tokens(maps_dict, tokenizer, src_ids, tgt_ids):
531
+ """Attach decoded token strings to a maps dictionary produced by ``extract_all_attention_maps``."""
532
+
533
+ src_tok = [
534
+ tokenizer.convert_ids_to_tokens(row, skip_special_tokens=False) for row in src_ids.tolist()
535
+ ]
536
+ tgt_tok = [
537
+ tokenizer.convert_ids_to_tokens(row, skip_special_tokens=False) for row in tgt_ids.tolist()
538
+ ]
539
+ maps_dict["src_tokens"] = src_tok
540
+ maps_dict["tgt_tokens"] = tgt_tok
541
+ return maps_dict
542
+
543
+
544
+ def _imshow(ax, array2d, title="", vmin=0.0, vmax=1.0):
545
+ image = ax.imshow(array2d, aspect="auto", vmin=vmin, vmax=vmax)
546
+ ax.set_title(title, fontsize=9)
547
+ ax.set_xticks([])
548
+ ax.set_yticks([])
549
+ return image
550
+
551
+
552
+ def plot_layer_heads_grid(
553
+ maps_dict: dict,
554
+ layer_idx: int,
555
+ batch: int = 0,
556
+ heads: list[int] | None = None,
557
+ figsize_per_cell: tuple[float, float] = (2.2, 2.2),
558
+ show_colorbar: bool = True,
559
+ vmin: float = 0.0,
560
+ vmax: float = 1.0,
561
+ *,
562
+ show: bool = True,
563
+ save_path: str | Path | None = None,
564
+ ):
565
+ """Plot a single layer as a 3-row (enc self / dec self / dec cross) grid."""
566
+
567
+ enc_layers = maps_dict["enc_self"]
568
+ dec_self_layers = maps_dict["dec_self"]
569
+ dec_cross_layers = maps_dict["dec_cross"]
570
+
571
+ enc_idx = min(layer_idx, len(enc_layers) - 1)
572
+ dec_idx = min(layer_idx, len(dec_self_layers) - 1)
573
+
574
+ enc = enc_layers[enc_idx][batch]
575
+ dec_self = dec_self_layers[dec_idx][batch]
576
+ dec_cross = dec_cross_layers[dec_idx][batch]
577
+
578
+ num_heads = enc.size(0)
579
+ heads = list(range(num_heads)) if heads is None else heads
580
+
581
+ rows = 3
582
+ cols = len(heads)
583
+ fig_width = max(1, int(round(figsize_per_cell[0] * cols)))
584
+ fig_height = max(1, int(round(figsize_per_cell[1] * rows)))
585
+ fig, axes = plt.subplots(
586
+ rows, cols, figsize=(fig_width, fig_height), squeeze=False, constrained_layout=True
587
+ )
588
+
589
+ images = []
590
+ for column, head in enumerate(heads):
591
+ images.append(
592
+ _imshow(axes[0, column], enc[head].cpu().float().numpy(), f"Enc h{head}", vmin, vmax)
593
+ )
594
+ _imshow(
595
+ axes[1, column], dec_self[head].cpu().float().numpy(), f"Dec self h{head}", vmin, vmax
596
+ )
597
+ _imshow(
598
+ axes[2, column], dec_cross[head].cpu().float().numpy(), f"Dec cross h{head}", vmin, vmax
599
+ )
600
+
601
+ if show_colorbar and images:
602
+ fig.colorbar(images[0], ax=axes, fraction=0.02, pad=0.01)
603
+ fig.suptitle(f"Layer {layer_idx}", fontsize=12)
604
+
605
+ if save_path is not None:
606
+ save_path = Path(save_path)
607
+ save_path.parent.mkdir(parents=True, exist_ok=True)
608
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
609
+
610
+ if show:
611
+ plt.show()
612
+ else:
613
+ plt.close(fig)
614
+ return fig
615
+
616
+
617
+ def plot_all_layers_all_heads(
618
+ maps_dict: dict,
619
+ batch: int = 0,
620
+ max_layers: int | None = None,
621
+ heads: list[int] | None = None,
622
+ figsize_per_cell: tuple[float, float] = (2.0, 2.0),
623
+ vmin: float = 0.0,
624
+ vmax: float = 1.0,
625
+ save_pdf_path: str | None = None,
626
+ *,
627
+ show: bool = True,
628
+ ):
629
+ """Render attention grids for every layer (optionally saving a multi-page PDF)."""
630
+
631
+ from matplotlib.backends.backend_pdf import PdfPages
632
+
633
+ enc_layers = len(maps_dict["enc_self"])
634
+ dec_layers = len(maps_dict["dec_self"])
635
+ total_layers = max(enc_layers, dec_layers)
636
+ if max_layers is not None:
637
+ total_layers = min(total_layers, max_layers)
638
+
639
+ def _render_layers(record_page):
640
+ for layer in range(total_layers):
641
+ fig = plot_layer_heads_grid(
642
+ maps_dict,
643
+ layer_idx=layer,
644
+ batch=batch,
645
+ heads=heads,
646
+ figsize_per_cell=figsize_per_cell,
647
+ show_colorbar=True,
648
+ vmin=vmin,
649
+ vmax=vmax,
650
+ show=show,
651
+ )
652
+ if record_page is not None:
653
+ record_page(fig)
654
+ plt.close(fig)
655
+
656
+ if save_pdf_path:
657
+ with PdfPages(save_pdf_path) as pdf:
658
+ _render_layers(pdf.savefig)
659
+ print(f"[saved] multi-page attention viewer -> {save_pdf_path}")
660
+ else:
661
+ _render_layers(None)
662
+
663
+
664
+ def _format_token_sequence(tokens: list[str]) -> str:
665
+ return " | ".join(tokens)
666
+
667
+
668
+ def _resolve_attention_layers(available: int, requested: Sequence[int] | None) -> list[int]:
669
+ if requested is None:
670
+ return list(range(available))
671
+ return sorted({int(idx) for idx in requested if 0 <= int(idx) < available})
672
+
673
+
674
+ def _resolve_attention_heads(available: int, requested: Sequence[int] | None) -> list[int]:
675
+ if requested is None:
676
+ return list(range(available))
677
+ return sorted({int(idx) for idx in requested if 0 <= int(idx) < available})
678
+
679
+
680
+ # -----------------------------------------------------------------------------
681
+ # Debug helper
682
+ # -----------------------------------------------------------------------------
683
+
684
+
685
+ def debug_transformer_forward(
686
+ model,
687
+ tokenizer,
688
+ src_ids: torch.Tensor,
689
+ tgt_ids: torch.Tensor,
690
+ *,
691
+ logits: torch.Tensor | None = None,
692
+ pad_id: int,
693
+ device: str | torch.device = "cpu",
694
+ batch_index: int = 0,
695
+ sample_index: int = 0,
696
+ show_attention: bool = False,
697
+ save_attention: bool = False,
698
+ average_heads: bool = False,
699
+ attention_types: Sequence[str] = ("enc_self", "dec_self", "dec_cross"),
700
+ attention_layers: Sequence[int] | None = None,
701
+ attention_heads: Sequence[int] | None = None,
702
+ attention_figsize: tuple[float, float] = (4.0, 4.0),
703
+ skip_special_tokens: bool = False,
704
+ log_fn: Callable[[str], None] | None = print,
705
+ return_maps: bool = False,
706
+ save_dir: str | Path | None = None,
707
+ run_dir: str | Path | None = None,
708
+ ) -> dict[str, Any]:
709
+ """Run a forward pass, compute diagnostics, and optionally render attention heatmaps."""
710
+
711
+ if log_fn is None:
712
+
713
+ def log_fn(_: str) -> None: # type: ignore[redefinition]
714
+ return
715
+
716
+ device = torch.device(device)
717
+
718
+ if src_ids.dim() != 2 or tgt_ids.dim() != 2:
719
+ raise ValueError("src_ids and tgt_ids must be rank-2 tensors")
720
+
721
+ batch_size = src_ids.size(0)
722
+ if not (0 <= sample_index < batch_size):
723
+ raise IndexError(f"sample_index {sample_index} out of range for batch size {batch_size}")
724
+
725
+ src_ids = src_ids.to(device)
726
+ tgt_ids = tgt_ids.to(device)
727
+
728
+ if tgt_ids.size(1) < 2:
729
+ raise ValueError("tgt_ids must contain at least BOS and one target token")
730
+
731
+ decoder_in = tgt_ids[:, :-1]
732
+ labels = tgt_ids[:, 1:]
733
+
734
+ src_pad_mask = src_ids.eq(pad_id)
735
+ tgt_pad_mask = decoder_in.eq(pad_id)
736
+ tgt_pad_full = tgt_ids.eq(pad_id)
737
+
738
+ was_training = model.training
739
+ attention_requested = show_attention or save_attention or return_maps
740
+
741
+ if logits is None:
742
+ if not callable(model):
743
+ raise TypeError("model must be callable")
744
+
745
+ model.eval()
746
+ with torch.no_grad():
747
+ logits = model(src_ids, decoder_in, src_pad_mask, tgt_pad_mask)
748
+ model.train(was_training)
749
+ else:
750
+ logits = logits.to(device)
751
+
752
+ if logits.dim() != 3 or logits.size(0) != batch_size:
753
+ raise ValueError("logits must be of shape [batch, seq_len, vocab]")
754
+
755
+ vocab_size = logits.size(-1)
756
+ mask = labels.ne(pad_id)
757
+
758
+ with torch.no_grad():
759
+ log_probs = F.log_softmax(logits, dim=-1)
760
+ nll = F.nll_loss(
761
+ log_probs.reshape(-1, vocab_size),
762
+ labels.reshape(-1),
763
+ reduction="sum",
764
+ ignore_index=pad_id,
765
+ )
766
+
767
+ tokens_total = int(mask.sum().item())
768
+ avg_nll = (nll / max(tokens_total, 1)).item()
769
+ perplexity = math.exp(avg_nll) if tokens_total > 0 else float("nan")
770
+
771
+ pred_ids = logits.argmax(dim=-1)
772
+ correct_mask = pred_ids.eq(labels) & mask
773
+ correct_tokens = int(correct_mask.sum().item())
774
+ token_accuracy = correct_tokens / max(tokens_total, 1)
775
+
776
+ sample_labels = labels[sample_index]
777
+ sample_preds = pred_ids[sample_index]
778
+ sample_mask = mask[sample_index]
779
+
780
+ sample_tokens_total = int(sample_mask.sum().item())
781
+ sample_correct_tokens = int((sample_preds.eq(sample_labels) & sample_mask).sum().item())
782
+ sample_token_accuracy = sample_correct_tokens / max(sample_tokens_total, 1)
783
+ sample_exact_match = bool(torch.equal(sample_preds[sample_mask], sample_labels[sample_mask]))
784
+
785
+ sample_decoder_in = decoder_in[sample_index]
786
+ sample_pred_sequence = torch.cat([sample_decoder_in[:1], sample_preds], dim=0)
787
+
788
+ with torch.no_grad():
789
+ sample_log_probs = F.log_softmax(logits[sample_index], dim=-1)
790
+ sample_nll = F.nll_loss(
791
+ sample_log_probs,
792
+ sample_labels,
793
+ reduction="sum",
794
+ ignore_index=pad_id,
795
+ )
796
+
797
+ sample_avg_nll = (sample_nll / max(sample_tokens_total, 1)).item()
798
+ sample_perplexity = math.exp(sample_avg_nll) if sample_tokens_total > 0 else float("nan")
799
+
800
+ sample_src_ids = src_ids[sample_index]
801
+ sample_tgt_ids = tgt_ids[sample_index]
802
+
803
+ sample_src_tokens = tokenizer.convert_ids_to_tokens(
804
+ sample_src_ids.tolist(),
805
+ skip_special_tokens=skip_special_tokens,
806
+ )
807
+ sample_tgt_tokens = tokenizer.convert_ids_to_tokens(
808
+ sample_tgt_ids.tolist(),
809
+ skip_special_tokens=skip_special_tokens,
810
+ )
811
+ sample_pred_tokens = tokenizer.convert_ids_to_tokens(
812
+ sample_pred_sequence.tolist(),
813
+ skip_special_tokens=skip_special_tokens,
814
+ )
815
+
816
+ sample_src_text = tokenizer.decode(sample_src_ids.tolist(), skip_special_tokens=True)
817
+ sample_tgt_text = tokenizer.decode(sample_tgt_ids.tolist(), skip_special_tokens=True)
818
+ sample_pred_text = tokenizer.decode(sample_pred_sequence.tolist(), skip_special_tokens=True)
819
+
820
+ log_fn("\n--- DEBUG TRANSFORMER FORWARD ---")
821
+ log_fn(f"Batch index : {batch_index}")
822
+ log_fn(f"Sample index: {sample_index}")
823
+ log_fn(f"Batch token accuracy: {token_accuracy * 100:.2f}% ({correct_tokens}/{tokens_total})")
824
+ log_fn(
825
+ f"Batch NLL / ppl : {avg_nll:.4f} / {perplexity:.4f}"
826
+ if tokens_total > 0
827
+ else "Batch NLL / ppl : n/a"
828
+ )
829
+ log_fn(
830
+ f"Sample token accuracy: {sample_token_accuracy * 100:.2f}% ({sample_correct_tokens}/{sample_tokens_total})"
831
+ )
832
+ log_fn(
833
+ f"Sample NLL / ppl : {sample_avg_nll:.4f} / {sample_perplexity:.4f}"
834
+ if sample_tokens_total > 0
835
+ else "Sample NLL / ppl : n/a"
836
+ )
837
+ log_fn(f"Sample exact match : {sample_exact_match}")
838
+
839
+ log_fn("-- Source sequence --")
840
+ log_fn(f"IDs : {sample_src_ids.tolist()}")
841
+ log_fn(f"Tokens: {_format_token_sequence(sample_src_tokens)}")
842
+ log_fn(f"Text : {sample_src_text}")
843
+
844
+ log_fn("-- Target sequence --")
845
+ log_fn(f"IDs : {sample_tgt_ids.tolist()}")
846
+ log_fn(f"Tokens: {_format_token_sequence(sample_tgt_tokens)}")
847
+ log_fn(f"Text : {sample_tgt_text}")
848
+
849
+ log_fn("-- Predicted sequence --")
850
+ log_fn(f"IDs : {sample_pred_sequence.tolist()}")
851
+ log_fn(f"Tokens: {_format_token_sequence(sample_pred_tokens)}")
852
+ log_fn(f"Text : {sample_pred_text}")
853
+
854
+ result: dict[str, Any] = {
855
+ "batch_index": batch_index,
856
+ "token_accuracy": token_accuracy,
857
+ "avg_negative_log_likelihood": avg_nll,
858
+ "perplexity": perplexity,
859
+ "tokens_total": tokens_total,
860
+ "correct_tokens": correct_tokens,
861
+ "sample": {
862
+ "index": sample_index,
863
+ "token_accuracy": sample_token_accuracy,
864
+ "exact_match": sample_exact_match,
865
+ "avg_negative_log_likelihood": sample_avg_nll,
866
+ "perplexity": sample_perplexity,
867
+ "source_ids": sample_src_ids.tolist(),
868
+ "target_ids": sample_tgt_ids.tolist(),
869
+ "predicted_ids": sample_pred_sequence.tolist(),
870
+ "source_tokens": sample_src_tokens,
871
+ "target_tokens": sample_tgt_tokens,
872
+ "predicted_tokens": sample_pred_tokens,
873
+ "source_text": sample_src_text,
874
+ "target_text": sample_tgt_text,
875
+ "predicted_text": sample_pred_text,
876
+ },
877
+ }
878
+
879
+ if attention_requested:
880
+ if not hasattr(model, "embed"):
881
+ raise AttributeError("model must expose an embed() method for debugging")
882
+ with torch.no_grad():
883
+ attention_maps = extract_all_attention_maps(
884
+ model,
885
+ src_ids,
886
+ tgt_ids,
887
+ src_pad_mask,
888
+ tgt_pad_full,
889
+ )
890
+
891
+ target_types = [t for t in attention_types if t in attention_maps]
892
+ if not target_types:
893
+ target_types = [k for k in ("enc_self", "dec_self", "dec_cross") if k in attention_maps]
894
+
895
+ target_dir: Path | None = None
896
+ if save_attention:
897
+ if save_dir is not None:
898
+ target_dir = Path(save_dir)
899
+ elif run_dir is not None:
900
+ target_dir = Path(run_dir) / "debug" / "attention"
901
+ else:
902
+ log_fn(
903
+ "[warn] save_attention requested but no save_dir/run_dir provided; skipping save."
904
+ )
905
+ if target_dir is not None:
906
+ target_dir.mkdir(parents=True, exist_ok=True)
907
+
908
+ sample_label = f"batch{batch_index}_sample{sample_index}"
909
+ figure_records: list[dict[str, Any]] = []
910
+
911
+ for att_type in target_types:
912
+ layer_maps = attention_maps.get(att_type, [])
913
+ if not layer_maps:
914
+ continue
915
+
916
+ layers_to_plot = _resolve_attention_layers(len(layer_maps), attention_layers)
917
+ for layer_idx in layers_to_plot:
918
+ layer_tensor = layer_maps[layer_idx][sample_index]
919
+ head_candidates = _resolve_attention_heads(layer_tensor.size(0), attention_heads)
920
+ if not head_candidates:
921
+ continue
922
+
923
+ if average_heads:
924
+ averaged = layer_tensor[head_candidates].mean(dim=0).cpu().float().numpy()
925
+ fig, ax = plt.subplots(figsize=attention_figsize)
926
+ im = ax.imshow(averaged, aspect="auto", vmin=0.0, vmax=1.0)
927
+ ax.set_title(f"{att_type} L{layer_idx} (avg heads)")
928
+ ax.set_xlabel("Key index")
929
+ ax.set_ylabel("Query index")
930
+ fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
931
+
932
+ save_path = None
933
+ if target_dir is not None:
934
+ save_path = target_dir / f"{sample_label}_{att_type}_L{layer_idx}_avg.png"
935
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
936
+ if show_attention:
937
+ plt.show()
938
+ else:
939
+ plt.close(fig)
940
+
941
+ figure_records.append(
942
+ {
943
+ "type": att_type,
944
+ "layer": layer_idx,
945
+ "heads": "average",
946
+ "path": str(save_path) if save_path else None,
947
+ }
948
+ )
949
+ else:
950
+ cols = len(head_candidates)
951
+ fig, axes = plt.subplots(
952
+ 1,
953
+ cols,
954
+ figsize=(attention_figsize[0] * cols, attention_figsize[1]),
955
+ squeeze=False,
956
+ constrained_layout=True,
957
+ )
958
+ for col, head_idx in enumerate(head_candidates):
959
+ ax = axes[0, col]
960
+ head_map = layer_tensor[head_idx].cpu().float().numpy()
961
+ im = ax.imshow(head_map, aspect="auto", vmin=0.0, vmax=1.0)
962
+ ax.set_title(f"{att_type} L{layer_idx} H{head_idx}")
963
+ ax.set_xlabel("Key index")
964
+ if col == 0:
965
+ ax.set_ylabel("Query index")
966
+ fig.colorbar(im, ax=axes.ravel().tolist(), fraction=0.046, pad=0.04)
967
+
968
+ save_path = None
969
+ if target_dir is not None:
970
+ head_tag = "-".join(str(h) for h in head_candidates)
971
+ save_path = (
972
+ target_dir / f"{sample_label}_{att_type}_L{layer_idx}_H{head_tag}.png"
973
+ )
974
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
975
+ if show_attention:
976
+ plt.show()
977
+ else:
978
+ plt.close(fig)
979
+
980
+ figure_records.append(
981
+ {
982
+ "type": att_type,
983
+ "layer": layer_idx,
984
+ "heads": head_candidates,
985
+ "path": str(save_path) if save_path else None,
986
+ }
987
+ )
988
+
989
+ if target_dir is not None:
990
+ raw_path = target_dir / f"{sample_label}_attention.pt"
991
+ torch.save(
992
+ {k: [v.cpu() for v in tensors] for k, tensors in attention_maps.items()}, raw_path
993
+ )
994
+ result.setdefault("attention", {})["raw_path"] = str(raw_path)
995
+
996
+ summary_path = target_dir / f"{sample_label}_attention_all_layers.pdf"
997
+ plot_all_layers_all_heads(
998
+ attention_maps,
999
+ batch=sample_index,
1000
+ figsize_per_cell=attention_figsize,
1001
+ save_pdf_path=str(summary_path),
1002
+ show=False,
1003
+ )
1004
+ result.setdefault("attention", {})["summary_path"] = str(summary_path)
1005
+
1006
+ if figure_records:
1007
+ result.setdefault("attention", {})["figures"] = figure_records
1008
+ if return_maps:
1009
+ result["attention_maps"] = attention_maps
1010
+
1011
+ log_fn("--- END DEBUG ---\n")
1012
+ return result
1013
+
1014
+
1015
+ # -----------------------------------------------------------------------------
1016
+ # Config compatibility checker
1017
+ # -----------------------------------------------------------------------------
1018
+
1019
+
1020
+ def check_tokenizer_model_compatibility(model_cfg, tokenizer_cfg):
1021
+ """Ensure tokenizer and model configuration agree on core vocabulary settings."""
1022
+
1023
+ if model_cfg.vocab_size != tokenizer_cfg.vocab_size:
1024
+ raise ValueError(
1025
+ f"Vocab size mismatch: model={model_cfg.vocab_size}, tokenizer={tokenizer_cfg.vocab_size}"
1026
+ )
1027
+ if model_cfg.max_seq_len != tokenizer_cfg.max_seq_len:
1028
+ raise ValueError(
1029
+ f"Max sequence length mismatch: model={model_cfg.max_seq_len}, tokenizer={tokenizer_cfg.max_seq_len}"
1030
+ )
1031
+ if model_cfg.pad_id != tokenizer_cfg.pad_id:
1032
+ raise ValueError(
1033
+ f"pad_id mismatch: model={model_cfg.pad_id}, tokenizer={tokenizer_cfg.pad_id}"
1034
+ )
1035
+ if model_cfg.bos_id != tokenizer_cfg.bos_id:
1036
+ raise ValueError(
1037
+ f"bos_id mismatch: model={model_cfg.bos_id}, tokenizer={tokenizer_cfg.bos_id}"
1038
+ )
1039
+ if model_cfg.eos_id != tokenizer_cfg.eos_id:
1040
+ raise ValueError(
1041
+ f"eos_id mismatch: model={model_cfg.eos_id}, tokenizer={tokenizer_cfg.eos_id}"
1042
+ )
tests/units/modules/test_attention.py CHANGED
@@ -11,6 +11,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
  # =======================
@@ -47,15 +51,18 @@ def test_ctor_happy_path():
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():
@@ -63,17 +70,20 @@ def test_forward_rank_and_lastdim_checks():
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():
@@ -81,11 +91,15 @@ def test_forward_batch_and_seq_mismatch_checks():
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
  # =======================
@@ -93,7 +107,7 @@ def test_forward_batch_and_seq_mismatch_checks():
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()}")
@@ -104,8 +118,9 @@ def test_forward_shapes_and_device_dtype(B, S, D, H):
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
@@ -118,22 +133,25 @@ def test_boolean_mask_blocks_positions():
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
  # =======================
@@ -167,8 +185,9 @@ def test_gradients_flow():
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
 
 
11
  return torch.randn(B, S, D, device=device, dtype=dtype)
12
 
13
 
14
+ def _pad_mask(batch: int, heads: int, seq_len: int, device) -> torch.Tensor:
15
+ return torch.zeros(batch, heads, 1, seq_len, dtype=torch.bool, device=device)
16
+
17
+
18
  # =======================
19
  # Constructor checks
20
  # =======================
 
51
  def test_forward_requires_tensors_and_mask_type():
52
  mha = MultiHeadAttention(32, 4, 0.1)
53
  q, k, v = _rand(2, 5, 32), _rand(2, 5, 32), _rand(2, 5, 32)
54
+ mask = _pad_mask(2, mha.num_heads, 5, q.device)
55
 
56
  with pytest.raises(TypeError):
57
+ mha("q", k, v, mask, mask)
58
+ with pytest.raises(TypeError):
59
+ mha(q, "k", v, mask, mask)
60
  with pytest.raises(TypeError):
61
+ mha(q, k, "v", mask, mask)
62
  with pytest.raises(TypeError):
63
+ mha(q, k, v, "bad", mask)
64
  with pytest.raises(TypeError):
65
+ mha(q, k, v, mask, "bad")
66
 
67
 
68
  def test_forward_rank_and_lastdim_checks():
 
70
  q = torch.randn(2, 5, 32)
71
  k = torch.randn(2, 5, 32)
72
  v = torch.randn(2, 5, 32)
73
+ mask = _pad_mask(2, mha.num_heads, 5, q.device)
74
 
75
  with pytest.raises(ValueError):
76
+ bad_q = q.unsqueeze(0)
77
+ bad_q_mask = _pad_mask(bad_q.shape[0], mha.num_heads, bad_q.shape[1], bad_q.device)
78
+ mha(bad_q, k, v, bad_q_mask, mask)
79
  with pytest.raises(ValueError):
80
+ mha(q, k.view(10, 32), v, mask, mask)
81
  with pytest.raises(ValueError):
82
+ mha(q[..., :16], k, v, mask, mask)
83
  with pytest.raises(ValueError):
84
+ mha(q, k[..., :16], v, mask, mask)
85
  with pytest.raises(ValueError):
86
+ mha(q, k, v[..., :16], mask, mask)
87
 
88
 
89
  def test_forward_batch_and_seq_mismatch_checks():
 
91
  q = torch.randn(2, 5, 24)
92
  k = torch.randn(3, 5, 24) # batch mismatch
93
  v = torch.randn(2, 5, 24) # seq mismatch with k
94
+ q_mask = _pad_mask(q.shape[0], mha.num_heads, q.shape[1], q.device)
95
+ k_mask = _pad_mask(k.shape[0], mha.num_heads, k.shape[1], k.device)
96
 
97
  with pytest.raises(ValueError):
98
+ mha(q, k, torch.randn(3, 5, 24), q_mask, k_mask)
99
  with pytest.raises(ValueError):
100
+ bad_k = torch.randn(2, 6, 24)
101
+ bad_k_mask = _pad_mask(bad_k.shape[0], mha.num_heads, bad_k.shape[1], bad_k.device)
102
+ mha(q, bad_k, v, q_mask, bad_k_mask)
103
 
104
 
105
  # =======================
 
107
  # =======================
108
 
109
 
110
+ @pytest.mark.parametrize("B,S,D,H", [(1, 1, 16, 4), (2, 5, 32, 4)])
111
  def test_forward_shapes_and_device_dtype(B, S, D, H):
112
  device = (
113
  torch.device(f"cuda:{torch.cuda.current_device()}")
 
118
  q = _rand(B, S, D, device=device)
119
  k = _rand(B, S, D, device=device)
120
  v = _rand(B, S, D, device=device)
121
+ mask = _pad_mask(B, H, S, device)
122
 
123
+ out = mha(q, k, v, mask, mask)
124
  assert out.shape == (B, S, D)
125
  assert out.device == device
126
  assert out.dtype == q.dtype
 
133
  k = _rand(B, S, D)
134
  v = _rand(B, S, D)
135
 
136
+ q_mask = _pad_mask(B, H, S, q.device)
137
+ k_mask = _pad_mask(B, H, S, q.device)
138
+ k_mask[..., -2:] = True
139
 
140
+ out1 = mha(q, k, v, q_mask, q_mask)
141
+ out2 = mha(q, k, v, q_mask, k_mask)
142
  assert not torch.allclose(out1, out2)
143
 
144
 
145
+ def test_causal_mask_blocks_future_positions():
146
  B, S, D, H = 2, 5, 32, 4
147
  mha = MultiHeadAttention(D, H, 0.0)
148
  q, k, v = _rand(B, S, D), _rand(B, S, D), _rand(B, S, D)
149
+ base_mask = _pad_mask(B, H, S, q.device)
150
+ causal = torch.ones(B, H, S, S, dtype=torch.bool, device=q.device).triu(1)
151
+
152
+ out_free = mha(q, k, v, base_mask, base_mask, None)
153
+ out_causal = mha(q, k, v, base_mask, base_mask, causal)
154
+ assert not torch.allclose(out_free, out_causal)
155
 
156
 
157
  # =======================
 
185
  q.requires_grad_(True)
186
  k.requires_grad_(True)
187
  v.requires_grad_(True)
188
+ mask = _pad_mask(2, 4, 5, q.device)
189
 
190
+ out = mha(q, k, v, mask, mask)
191
  loss = out.pow(2).mean()
192
  loss.backward()
193
 
tests/units/modules/test_decoder.py CHANGED
@@ -45,17 +45,19 @@ def test_transformer_decoder_layers_count_checks():
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)
@@ -64,31 +66,45 @@ def test_decoder_forward_happy_path_and_zero_len(B, Sx, Sy, D, H, FF, L):
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
- )
 
45
  # -------- forward path --------
46
 
47
 
48
+ @pytest.mark.parametrize("B,Sx,Sy,D,H,FF,L", [(2, 5, 6, 24, 3, 48, 2)])
49
+ def test_decoder_forward_happy_path(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
+ heads = dec.layers[0].self_attention_layer.num_heads
56
+ src_pad = torch.zeros(B, heads, 1, Sx, dtype=torch.bool, device=device)
57
+ tgt_pad = torch.zeros(B, heads, 1, Sy, dtype=torch.bool, device=device)
58
+ causal = (
59
+ torch.ones(B, heads, Sy, Sy, dtype=torch.bool, device=device).triu(1) if Sy > 0 else None
60
+ )
61
 
62
  out = dec(x, y, src_pad, tgt_pad, causal)
63
  assert out.shape == (B, Sy, D)
 
66
 
67
  def test_decoder_forward_input_checks_and_message_format():
68
  layer = DecoderLayer(24, 3, 48, 0.1)
69
+ heads = layer.self_attention_layer.num_heads
70
+ base_src_mask = torch.zeros(2, heads, 1, 3, dtype=torch.bool)
71
+ base_tgt_mask = torch.zeros(2, heads, 1, 3, dtype=torch.bool)
72
 
73
  with pytest.raises(TypeError):
74
+ layer("not a tensor", torch.randn(2, 3, 24), base_src_mask, base_tgt_mask, None)
75
  with pytest.raises(TypeError):
76
+ layer(torch.randn(2, 3, 24), "not a tensor", base_src_mask, base_tgt_mask, None)
77
 
78
  with pytest.raises(ValueError) as e1:
79
+ layer(
80
+ torch.randn(2, 3, 24, 5),
81
+ torch.randn(2, 3, 24),
82
+ base_src_mask,
83
+ base_tgt_mask,
84
+ None,
85
+ ) # x rank 4
86
  assert "x must be a 3D torch.Tensor of shape (B, S, D)" in str(e1.value)
87
 
88
  with pytest.raises(ValueError) as e2:
89
+ layer(
90
+ torch.randn(2, 3, 24),
91
+ torch.randn(2, 3, 24, 5),
92
+ base_src_mask,
93
+ base_tgt_mask,
94
+ None,
95
+ ) # y rank 4
96
  assert "y must be a 3D torch.Tensor of shape (B, S, D)" in str(e2.value)
97
 
98
  x = torch.randn(2, 5, 24)
99
  y = torch.randn(3, 6, 24) # batch mismatch
100
+ src_mask_x = torch.zeros(2, heads, 1, 5, dtype=torch.bool)
101
+ tgt_mask_y = torch.zeros(3, heads, 1, 6, dtype=torch.bool)
102
  with pytest.raises(ValueError) as e3:
103
+ layer(x, y, src_mask_x, tgt_mask_y, None)
104
+ assert "Encoder memory and decoder input must match in batch and d_model" in str(e3.value)
 
 
105
 
106
  y2 = torch.randn(2, 6, 16) # d_model mismatch
107
+ tgt_mask_y2 = torch.zeros(2, heads, 1, 6, dtype=torch.bool)
108
  with pytest.raises(ValueError) as e4:
109
+ layer(x, y2, src_mask_x, tgt_mask_y2, None)
110
+ assert "Encoder memory and decoder input must match in batch and d_model" in str(e4.value)
 
 
tests/units/modules/test_embedding.py CHANGED
@@ -42,9 +42,9 @@ def test_ctor_value_checks_basic():
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():
@@ -114,7 +114,7 @@ def test_forward_happy_path_shape_dtype_device_and_zero_len():
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
 
@@ -126,7 +126,7 @@ def test_forward_respects_sequence_length_limit():
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
  # =======================
@@ -141,11 +141,11 @@ def test_positional_ctor_value_checks():
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
 
 
42
  InputEmbedding(10, 8, 16, -1)
43
 
44
 
45
+ def test_ctor_rejects_zero_length_max_seq():
46
+ with pytest.raises(ValueError):
47
+ InputEmbedding(11, 8, 0, 0)
48
 
49
 
50
  def test_padding_row_zero_and_stays_zero_after_step():
 
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) * (m.d_model**0.5)
118
  out = m(x)
119
  assert not torch.allclose(out, tok_only)
120
 
 
126
  x_bad = _rand_ids(B=2, S=6, vocab=16)
127
  with pytest.raises(ValueError) as ei:
128
  _ = m(x_bad)
129
+ assert "Sequence length" in str(ei.value) and "exceeds max_seq_len" in str(ei.value)
130
 
131
 
132
  # =======================
 
141
  PositionalEmbedding(16, 0) # d_model <= 0
142
 
143
 
144
+ def test_positional_buffer_registered_and_constant_shape():
145
+ pe = PositionalEmbedding(4, 10)
146
  assert hasattr(pe, "pe")
147
  assert isinstance(pe.pe, torch.Tensor)
148
+ assert pe.pe.shape == (1, 4, 10)
149
  assert pe.pe.requires_grad is False
150
 
151
 
tests/units/modules/test_encoder.py CHANGED
@@ -45,13 +45,14 @@ def test_transformer_encoder_layers_count_checks():
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)
 
45
  # -------- forward path --------
46
 
47
 
48
+ @pytest.mark.parametrize("B,S,D,H,FF,L", [(2, 5, 24, 3, 48, 2)])
49
+ def test_encoder_forward_happy_path(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
+ heads = enc.layers[0].attention_layer.num_heads
55
+ src_pad = torch.zeros(B, heads, 1, S, dtype=torch.bool, device=device)
56
 
57
  out = enc(x, src_pad)
58
  assert out.shape == (B, S, D)
tests/units/test_transformer.py CHANGED
@@ -1,8 +1,9 @@
1
  import pytest
2
  import torch
3
 
4
- from transformer.configs import BasicEncDecCfg
5
  from transformer.transformer import BasicEncoderDecoderTransformer
 
6
 
7
 
8
  def _dev():
@@ -16,6 +17,10 @@ def _dev():
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,
@@ -28,7 +33,15 @@ def _tiny_cfg(**over):
28
  dropout_rate=0.1,
29
  )
30
  base.update(over)
31
- return BasicEncDecCfg(**base)
 
 
 
 
 
 
 
 
32
 
33
 
34
  # -----------------------
@@ -64,7 +77,7 @@ def test_ctor_cfg_type_and_values():
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)
@@ -74,16 +87,19 @@ def test_forward_pipeline_shapes_and_types(Sx, Sy):
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)
@@ -97,23 +113,28 @@ def test_forward_dtype_checks_and_shape_errors():
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():
@@ -121,19 +142,21 @@ def test_encode_decode_errors_and_messages():
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():
@@ -141,8 +164,9 @@ def test_exceeding_max_seq_len_raises():
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
  # -----------------------
@@ -158,8 +182,8 @@ def test_backward_through_full_forward_and_tied_weights():
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
@@ -184,7 +208,7 @@ def test_generate_arg_checks_and_output_shapes():
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):
@@ -205,14 +229,16 @@ def test_generate_arg_checks_and_output_shapes():
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)
@@ -231,12 +257,83 @@ def test_generate_zero_new_tokens_returns_only_bos():
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
 
@@ -249,8 +346,8 @@ def test_forward_accepts_boolean_padding_masks():
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)
@@ -273,7 +370,9 @@ def test_weight_tying_and_optimizer_step_changes_weights_once():
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()
@@ -310,7 +409,9 @@ def test_decode_invariance_to_future_tokens():
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)
@@ -319,8 +420,10 @@ def test_decode_invariance_to_future_tokens():
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)
 
1
  import pytest
2
  import torch
3
 
4
+ from transformer.configs import ModelCfg
5
  from transformer.transformer import BasicEncoderDecoderTransformer
6
+ from transformer.utils import broadcast_padding_mask
7
 
8
 
9
  def _dev():
 
17
  def _tiny_cfg(**over):
18
  # Small, fast config for tests; zero-length safe via max_seq_len >= 0
19
  base = dict(
20
+ name="test",
21
+ best_checkpoint_path="/tmp/best.ckpt",
22
+ latest_checkpoint_path="/tmp/latest.ckpt",
23
+ tokenizer="bpe_8k",
24
  vocab_size=32,
25
  d_model=16,
26
  d_ff=32,
 
33
  dropout_rate=0.1,
34
  )
35
  base.update(over)
36
+ return ModelCfg(**base)
37
+
38
+
39
+ def _zeros_mask(batch: int, seq_len: int, device) -> torch.Tensor:
40
+ return torch.zeros(batch, seq_len, dtype=torch.bool, device=device)
41
+
42
+
43
+ def _broadcast_mask(mask_2d: torch.Tensor, num_heads: int) -> torch.Tensor:
44
+ return broadcast_padding_mask(mask_2d, num_heads)
45
 
46
 
47
  # -----------------------
 
77
  # -----------------------
78
 
79
 
80
+ @pytest.mark.parametrize("Sx,Sy", [(5, 6)])
81
  def test_forward_pipeline_shapes_and_types(Sx, Sy):
82
  device = _dev()
83
  cfg = _tiny_cfg(max_seq_len=16)
 
87
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
88
  tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
89
 
90
+ # padding masks (boolean) shaped [B, S]; zero-length handled via empty tensors
91
+ src_mask_2d = _zeros_mask(B, Sx, device)
92
+ tgt_mask_2d = _zeros_mask(B, Sy, device)
93
 
94
+ logits = model(src, tgt, src_mask_2d, tgt_mask_2d)
95
  assert logits.shape == (B, Sy, cfg.vocab_size)
96
  assert logits.device == device
97
  assert logits.dtype == torch.get_default_dtype()
98
 
99
  # encode / decode individually
100
+ src_mask = _broadcast_mask(src_mask_2d, cfg.num_heads)
101
+ tgt_mask = _broadcast_mask(tgt_mask_2d, cfg.num_heads)
102
+
103
  mem = model.encode(src, src_mask)
104
  assert mem.shape == (B, Sx, cfg.d_model)
105
  out = model.decode(mem, tgt, src_mask, tgt_mask)
 
113
  B, Sx, Sy = 2, 4, 5
114
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long)
115
  tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long)
116
+ src_mask = _zeros_mask(B, Sx, src.device)
117
+ tgt_mask = _zeros_mask(B, Sy, tgt.device)
118
 
119
  with pytest.raises(TypeError):
120
+ model("not a tensor", tgt, src_mask, tgt_mask)
121
  with pytest.raises(TypeError):
122
+ model(src, "not a tensor", src_mask, tgt_mask)
123
  with pytest.raises(ValueError):
124
+ bad_src = src.unsqueeze(0)
125
+ bad_src_mask = _zeros_mask(bad_src.shape[0], bad_src.shape[1], bad_src.device)
126
+ model(bad_src, tgt, bad_src_mask, tgt_mask)
127
  with pytest.raises(ValueError):
128
+ bad_tgt = tgt.unsqueeze(-1)
129
+ model(src, bad_tgt, src_mask, tgt_mask)
130
  with pytest.raises(TypeError):
131
+ model(src.float(), tgt, src_mask, tgt_mask) # wrong dtype
132
  with pytest.raises(TypeError):
133
+ model(src, tgt.int(), src_mask, tgt_mask) # wrong dtype
134
  with pytest.raises(TypeError):
135
+ model(src, tgt, src_padding_mask="not a tensor", tgt_padding_mask=tgt_mask)
136
  with pytest.raises(TypeError):
137
+ model(src, tgt, src_mask, tgt_padding_mask="not a tensor")
138
 
139
 
140
  def test_encode_decode_errors_and_messages():
 
142
  model = BasicEncoderDecoderTransformer(cfg)
143
 
144
  src = torch.randint(0, cfg.vocab_size, (2, 4))
145
+ src_mask = _broadcast_mask(_zeros_mask(src.shape[0], src.shape[1], src.device), cfg.num_heads)
146
  with pytest.raises(ValueError):
147
+ model.encode(src.unsqueeze(-1), src_mask) # rank 3 not allowed
148
  with pytest.raises(TypeError):
149
+ model.encode(src.float(), src_mask) # dtype must be long
150
 
151
  mem = torch.randn(2, 4, cfg.d_model)
152
  tgt = torch.randint(0, cfg.vocab_size, (2, 5))
153
+ tgt_mask = _broadcast_mask(_zeros_mask(tgt.shape[0], tgt.shape[1], tgt.device), cfg.num_heads)
154
  with pytest.raises(ValueError):
155
+ model.decode(mem.unsqueeze(0), tgt, src_mask, tgt_mask) # mem rank 4
156
  with pytest.raises(ValueError):
157
+ model.decode(mem, tgt.unsqueeze(-1), src_mask, tgt_mask) # tgt rank 3
158
  with pytest.raises(TypeError):
159
+ model.decode(mem, tgt.float(), src_mask, tgt_mask) # tgt dtype must be long
160
 
161
 
162
  def test_exceeding_max_seq_len_raises():
 
164
  model = BasicEncoderDecoderTransformer(cfg)
165
  # src longer than max -> PositionalEmbedding should raise
166
  src = torch.randint(0, cfg.vocab_size, (2, 5), dtype=torch.long)
167
+ src_mask = _broadcast_mask(_zeros_mask(src.shape[0], src.shape[1], src.device), cfg.num_heads)
168
  with pytest.raises(ValueError):
169
+ model.encode(src, src_mask)
170
 
171
 
172
  # -----------------------
 
182
  B, Sx, Sy = 2, 6, 5
183
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
184
  tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
185
+ src_mask = _zeros_mask(B, Sx, device)
186
+ tgt_mask = _zeros_mask(B, Sy, device)
187
 
188
  logits = model(src, tgt, src_mask, tgt_mask) # (B, Sy, V)
189
  # Dummy labels (language modeling): predict tgt itself; ignore pads via mask
 
208
 
209
  B, Sx = 2, 4
210
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
211
+ src_mask = _zeros_mask(B, Sx, device)
212
 
213
  # type/value errors
214
  with pytest.raises(TypeError):
 
229
  model.generate(src, src_mask, temperature=0.0)
230
  with pytest.raises(TypeError):
231
  model.generate(src, src_mask, top_k="5")
 
 
232
  with pytest.raises(TypeError):
233
  model.generate(src, src_mask, top_p="0.9")
234
  with pytest.raises(ValueError):
235
  model.generate(src, src_mask, top_p=0.0)
236
  with pytest.raises(ValueError):
237
  model.generate(src, src_mask, top_p=1.1)
238
+ with pytest.raises(TypeError):
239
+ model.generate(src, src_mask, seed="123")
240
+ with pytest.raises(TypeError):
241
+ model.generate(src, src_mask, generator="not a generator")
242
 
243
  # happy path: shapes & dtypes; content is stochastic so we don't assert exact ids
244
  out = model.generate(src, src_mask, max_new_tokens=7, temperature=1.0, top_k=None, top_p=None)
 
257
  model = BasicEncoderDecoderTransformer(cfg).to(device)
258
 
259
  src = torch.randint(0, cfg.vocab_size, (2, 3), dtype=torch.long, device=device)
260
+ src_mask = _zeros_mask(src.shape[0], src.shape[1], device)
261
+ out = model.generate(src, src_mask, max_new_tokens=0)
262
  assert out.shape == (2, 1) # just the BOS token
263
 
264
 
265
+ def test_generate_sampling_seed_reproducible():
266
+ device = _dev()
267
+ cfg = _tiny_cfg()
268
+ model = BasicEncoderDecoderTransformer(cfg).to(device).eval()
269
+
270
+ B, Sx = 2, 4
271
+ src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
272
+ src_mask = _zeros_mask(B, Sx, device)
273
+
274
+ out_seed_a = model.generate(
275
+ src,
276
+ src_mask,
277
+ max_new_tokens=5,
278
+ do_sample=True,
279
+ temperature=1.0,
280
+ top_k=None,
281
+ top_p=None,
282
+ seed=123,
283
+ )
284
+ out_seed_b = model.generate(
285
+ src,
286
+ src_mask,
287
+ max_new_tokens=5,
288
+ do_sample=True,
289
+ temperature=1.0,
290
+ top_k=None,
291
+ top_p=None,
292
+ seed=123,
293
+ )
294
+ assert torch.equal(out_seed_a, out_seed_b)
295
+
296
+ out_seed_c = model.generate(
297
+ src,
298
+ src_mask,
299
+ max_new_tokens=5,
300
+ do_sample=True,
301
+ temperature=1.0,
302
+ top_k=None,
303
+ top_p=None,
304
+ seed=124,
305
+ )
306
+ assert not torch.equal(out_seed_a, out_seed_c)
307
+
308
+ gen1 = torch.Generator(device=device)
309
+ gen1.manual_seed(999)
310
+ gen2 = torch.Generator(device=device)
311
+ gen2.manual_seed(999)
312
+ out_gen_a = model.generate(
313
+ src,
314
+ src_mask,
315
+ max_new_tokens=5,
316
+ do_sample=True,
317
+ temperature=1.0,
318
+ top_k=None,
319
+ top_p=None,
320
+ generator=gen1,
321
+ )
322
+ out_gen_b = model.generate(
323
+ src,
324
+ src_mask,
325
+ max_new_tokens=5,
326
+ do_sample=True,
327
+ temperature=1.0,
328
+ top_k=None,
329
+ top_p=None,
330
+ generator=gen2,
331
+ )
332
+ assert torch.equal(out_gen_a, out_gen_b)
333
+
334
+
335
  # -----------------------
336
+ # Mask broadcast sanity (boolean masks handled in utils; we just pass through)
337
  # -----------------------
338
 
339
 
 
346
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
347
  tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
348
 
349
+ src_pad_bool = _zeros_mask(B, Sx, device)
350
+ tgt_pad_bool = _zeros_mask(B, Sy, device)
351
 
352
  logits = model(src, tgt, src_pad_bool, tgt_pad_bool)
353
  assert logits.shape == (B, Sy, cfg.vocab_size)
 
370
  B, Sx, Sy = 2, 5, 4
371
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
372
  tgt = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
373
+ src_mask = _zeros_mask(B, Sx, device)
374
+ tgt_mask = _zeros_mask(B, Sy, device)
375
+ logits = model(src, tgt, src_mask, tgt_mask) # (B, Sy, V)
376
 
377
  # Simple loss to get gradients flowing
378
  loss = logits.pow(2).mean()
 
409
 
410
  B, Sx, Sy = 1, 4, 5
411
  src = torch.randint(0, cfg.vocab_size, (B, Sx), dtype=torch.long, device=device)
412
+ src_mask_2d = _zeros_mask(B, Sx, device)
413
+ src_mask = _broadcast_mask(src_mask_2d, cfg.num_heads)
414
+ mem = model.encode(src, src_mask)
415
 
416
  # Construct y1 and y2 identical up to t=3, different afterwards
417
  y1 = torch.randint(0, cfg.vocab_size, (B, Sy), dtype=torch.long, device=device)
 
420
  if Sy > t + 1:
421
  y2[:, t + 1 :] = (y1[:, t + 1 :] + 1) % cfg.vocab_size
422
 
423
+ tgt_mask_2d = _zeros_mask(B, Sy, device)
424
+ tgt_mask = _broadcast_mask(tgt_mask_2d, cfg.num_heads)
425
+ out1 = model.decode(mem, y1, src_mask, tgt_mask) # (B, Sy, D)
426
+ out2 = model.decode(mem, y2, src_mask, tgt_mask)
427
 
428
  # Compare hidden states up to and including position t
429
  assert torch.allclose(out1[:, : t + 1], out2[:, : t + 1], atol=1e-5, rtol=1e-5)
tests/units/test_utils.py CHANGED
@@ -10,7 +10,6 @@ from transformer.utils import (
10
  create_causal_mask,
11
  join_heads,
12
  sample_from_logits,
13
- shift_right,
14
  sinusoidal_positional_encoding,
15
  split_heads,
16
  )
@@ -230,19 +229,6 @@ def test_half_precision_close_to_fp32_or_skip():
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)
@@ -266,13 +252,11 @@ def test_gradients_flow_no_inplace_breakage():
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
 
@@ -344,6 +328,7 @@ def test_qkv_device_mismatch_raises_clear_error():
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)
@@ -369,23 +354,6 @@ def test_boolean_mask_broadcast_variants_equivalent():
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)
@@ -432,90 +400,45 @@ def test_join_heads_roundtrip():
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
 
@@ -560,72 +483,6 @@ def test_dtype_conversion():
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
 
@@ -706,19 +563,18 @@ def test_handles_higher_rank_logits_flattening():
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],
@@ -727,23 +583,19 @@ def test_mask_upper_triangle():
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)
 
10
  create_causal_mask,
11
  join_heads,
12
  sample_from_logits,
 
13
  sinusoidal_positional_encoding,
14
  split_heads,
15
  )
 
229
  # ----------------------------
230
  # D. Edge cases
231
  # ----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  def test_singleton_softmax_is_one():
233
  q = torch.randn(1, 1, 1, 1)
234
  k = torch.randn(1, 1, 1, 1)
 
252
  assert t.grad is not None and torch.isfinite(t.grad).all()
253
 
254
 
255
+ def test_repeated_calls_consistent_without_dropout():
256
  B, H, Sq, Sk, D = 2, 3, 4, 5, 8
257
  q, k, v = _rand(B, H, Sq, Sk, D)
258
+ a1 = calculate_attention(q, k, v, mask=None)
259
+ a2 = calculate_attention(q, k, v, mask=None)
 
 
260
  assert torch.allclose(a1, a2)
261
 
262
 
 
328
  assert (
329
  "q/k/v must be on the same device" in msg # our explicit check
330
  or "Expected all tensors to be on the same device" in msg # PyTorch matmul
331
+ or "query, key, value must be on the same device" in msg # updated error wording
332
  )
333
  finally:
334
  torch.use_deterministic_algorithms(prev)
 
354
  assert torch.allclose(p2, p_exact, atol=1e-6, rtol=1e-5)
355
 
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  def test_non_broadcastable_mask_raises_value_error():
358
  B, H, Sq, Sk, D = 2, 3, 4, 5, 8
359
  q, k, v = _rand(B, H, Sq, Sk, D)
 
400
  ###___sinusoidal_positional_encoding___###
401
 
402
 
403
+ @pytest.mark.parametrize("seq_len,dim", [(1, 1), (1, 2), (7, 4), (17, 33)])
404
+ def test_sinusoidal_shapes_and_dtype(seq_len, dim):
405
+ pe = sinusoidal_positional_encoding(seq_len, dim)
406
+ assert pe.shape == (seq_len, dim)
407
+ assert pe.dtype == torch.float32
408
+ assert pe.requires_grad is False
 
 
 
 
 
 
 
 
409
 
410
 
411
+ def test_sinusoidal_matches_reference_small_case():
 
412
  seq_len, dim = 4, 6
413
+ pe = sinusoidal_positional_encoding(seq_len, dim)
 
414
 
415
  ref = torch.zeros_like(pe)
416
+ base = 10_000.0
417
  for pos in range(seq_len):
418
  for i in range(0, dim // 2):
419
  denom = base ** (2 * i / dim)
420
  ref[pos, 2 * i] = math.sin(pos / denom)
421
  ref[pos, 2 * i + 1] = math.cos(pos / denom)
422
 
 
423
  paired = (dim // 2) * 2
424
  assert torch.allclose(pe[:, :paired], ref[:, :paired], atol=1e-6, rtol=1e-6)
425
 
426
 
427
+ def test_sinusoidal_deterministic():
428
+ a = sinusoidal_positional_encoding(8, 16)
429
+ b = sinusoidal_positional_encoding(8, 16)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  assert torch.allclose(a, b)
431
 
432
 
433
+ def test_sinusoidal_invalid_inputs():
434
+ with pytest.raises(ValueError):
435
+ sinusoidal_positional_encoding(0, 8)
436
  with pytest.raises(ValueError):
437
  sinusoidal_positional_encoding(-1, 8)
438
  with pytest.raises(ValueError):
439
  sinusoidal_positional_encoding(1, 0)
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  ###___combine_masks___###
443
 
444
 
 
483
  assert torch.equal(out, expected)
484
 
485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  ###___sample_from_logits___###
487
 
488
 
 
563
  ###___create_causal_mask___###
564
 
565
 
566
+ def test_causal_mask_shape_and_dtype_cpu():
567
+ x = torch.zeros(2, 5, dtype=torch.long)
568
+ mask = create_causal_mask(x, num_heads=3)
569
+ assert mask.shape == (2, 3, 5, 5)
570
  assert mask.dtype == torch.bool
571
+ assert mask.device == x.device
572
 
573
 
574
+ def test_causal_mask_is_upper_triangular():
575
+ x = torch.zeros(1, 4, dtype=torch.long)
576
+ mask = create_causal_mask(x, num_heads=2)
577
+ tri = mask[0, 0].to(torch.int)
 
578
  expected = torch.tensor(
579
  [
580
  [0, 1, 1, 1],
 
583
  [0, 0, 0, 0],
584
  ]
585
  )
586
+ assert torch.equal(tri, expected)
 
 
 
 
 
 
 
587
 
588
 
589
  @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
590
+ def test_causal_mask_tracks_device():
591
+ x = torch.zeros(1, 3, dtype=torch.long, device="cuda")
592
+ mask = create_causal_mask(x, num_heads=4)
593
  assert mask.device.type == "cuda"
594
 
595
 
596
+ def test_causal_mask_rejects_invalid_inputs():
597
+ with pytest.raises(ValueError):
598
+ create_causal_mask(torch.zeros(2, 0, dtype=torch.long), num_heads=2)
599
+
600
  with pytest.raises(ValueError):
601
+ create_causal_mask(torch.zeros(2, 3, dtype=torch.long), num_heads=0)
tokenizer/bpe_16k.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer/bpe_32k.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer/bpe_4k.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer/bpe_8k.json ADDED
The diff for this file is too large to render. See raw diff