Spaces:
Sleeping
Sleeping
Commit ·
6edd10d
1
Parent(s): dc8d52b
added training et new improved model structure
Browse files- architecture.txt +1 -1
- configs/dataset/large_dataset.yaml +2 -0
- configs/dataset/medium_dataset.yaml +2 -0
- configs/dataset/small_dataset.yaml +2 -0
- configs/generation/generation.yaml +9 -0
- configs/infer_mode.yaml +8 -7
- configs/model/large_model.yaml +14 -0
- configs/model/medium_model.yaml +14 -0
- configs/model/small_model.yaml +14 -0
- configs/model/test_model.yaml +8 -4
- configs/optim/Adamw.yaml +6 -0
- configs/optimizer/optimizer.yaml +0 -0
- configs/runtime/local.yaml +7 -0
- configs/runtime/runtime.yaml +0 -3
- configs/sched/cosine.yaml +5 -0
- configs/tokenizer/bpe_16k.yaml +15 -0
- configs/tokenizer/bpe_32k.yaml +15 -0
- configs/tokenizer/bpe_4k.yaml +15 -0
- configs/tokenizer/bpe_8k.yaml +15 -0
- configs/train_mode.yaml +13 -9
- configs/trainer/default.yaml +14 -0
- environment.yml +5 -0
- pyproject.toml +8 -1
- requirements.txt +0 -0
- scripts/inference.py +45 -22
- scripts/load_data.ipynb +388 -0
- scripts/server.py +26 -0
- scripts/tokenizer.ipynb +217 -0
- scripts/train.ipynb +987 -0
- scripts/train.py +0 -0
- src/transformer/__init__.py +1 -2
- src/transformer/configs.py +104 -40
- src/transformer/modules/attention.py +132 -105
- src/transformer/modules/decoder.py +171 -99
- src/transformer/modules/embedding.py +146 -121
- src/transformer/modules/encoder.py +108 -71
- src/transformer/modules/feedforward.py +66 -59
- src/transformer/modules/lm_head.py +54 -47
- src/transformer/transformer.py +435 -239
- src/transformer/utils.py +1042 -612
- tests/units/modules/test_attention.py +42 -23
- tests/units/modules/test_decoder.py +34 -18
- tests/units/modules/test_embedding.py +8 -8
- tests/units/modules/test_encoder.py +4 -3
- tests/units/test_transformer.py +137 -34
- tests/units/test_utils.py +37 -185
- tokenizer/bpe_16k.json +0 -0
- tokenizer/bpe_32k.json +0 -0
- tokenizer/bpe_4k.json +0 -0
- 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:
|
|
|
|
|
|
|
| 4 |
- _self_
|
| 5 |
-
input_text: ""
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
d_ff: 1024
|
| 5 |
dropout_rate: 0.1
|
| 6 |
-
vocab_size:
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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
|
| 6 |
|
| 7 |
-
from transformer import
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
InferAppCfg,
|
| 11 |
-
)
|
| 12 |
|
| 13 |
|
| 14 |
-
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
transformer = BasicEncoderDecoderTransformer(model_cfg)
|
| 21 |
-
tokenizer =
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
-
if scfg.
|
| 27 |
-
ckpt = torch.load(to_absolute_path(scfg.
|
| 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 =
|
|
|
|
| 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=
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
|
|
|
| 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
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
d_model: int
|
| 8 |
-
|
| 9 |
num_heads: int
|
| 10 |
d_ff: int
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
@dataclass
|
| 19 |
-
class
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
num_workers: int = 4
|
| 23 |
-
batch_size: int = 32
|
| 24 |
-
max_length: int = 512
|
| 25 |
|
| 26 |
|
| 27 |
@dataclass
|
| 28 |
class OptimCfg:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
@dataclass
|
| 36 |
class SchedCfg:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
@dataclass
|
| 44 |
class TrainerCfg:
|
| 45 |
-
epochs: int
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
@dataclass
|
| 52 |
class RuntimeCfg:
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
|
| 58 |
@dataclass
|
| 59 |
class TrainAppCfg:
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
data: DataCfg
|
| 63 |
optim: OptimCfg
|
| 64 |
-
|
| 65 |
trainer: TrainerCfg
|
| 66 |
runtime: RuntimeCfg
|
|
|
|
| 67 |
|
| 68 |
|
| 69 |
@dataclass
|
| 70 |
class InferAppCfg:
|
| 71 |
-
model:
|
| 72 |
runtime: RuntimeCfg
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
self.
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
if
|
| 119 |
-
raise
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
if not
|
| 25 |
-
raise
|
| 26 |
-
if not
|
| 27 |
-
raise
|
| 28 |
-
if not
|
| 29 |
-
raise
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
if d_ff
|
| 36 |
-
raise
|
| 37 |
-
if d_model
|
| 38 |
-
raise
|
| 39 |
-
if not (
|
| 40 |
-
raise
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
self.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
from
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
if not isinstance(cfg
|
| 26 |
-
raise TypeError(f"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
if not (
|
| 34 |
-
raise
|
| 35 |
-
if not
|
| 36 |
-
raise
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
self.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
self.
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
raise
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
)
|
| 166 |
-
if
|
| 167 |
-
raise
|
| 168 |
-
f"
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
import
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
if
|
| 56 |
-
raise
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
if
|
| 116 |
-
raise
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
if
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
)
|
| 214 |
-
return
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
"""
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
return
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
if
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
if
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
def
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
)
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
)
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
if
|
| 611 |
-
|
| 612 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 53 |
with pytest.raises(TypeError):
|
| 54 |
-
mha(q,
|
| 55 |
with pytest.raises(TypeError):
|
| 56 |
-
mha(q, k,
|
| 57 |
with pytest.raises(TypeError):
|
| 58 |
-
mha(q, k, v, mask
|
| 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 |
-
|
|
|
|
|
|
|
| 69 |
with pytest.raises(ValueError):
|
| 70 |
-
mha(q, k.view(10, 32), v,
|
| 71 |
with pytest.raises(ValueError):
|
| 72 |
-
mha(q[..., :16], k, v,
|
| 73 |
with pytest.raises(ValueError):
|
| 74 |
-
mha(q, k[..., :16], v,
|
| 75 |
with pytest.raises(ValueError):
|
| 76 |
-
mha(q, k, v[..., :16],
|
| 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),
|
| 87 |
with pytest.raises(ValueError):
|
| 88 |
-
|
|
|
|
|
|
|
| 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)
|
| 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
|
| 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 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
|
| 125 |
-
out1 = mha(q, k, v,
|
| 126 |
-
out2 = mha(q, k, v,
|
| 127 |
assert not torch.allclose(out1, out2)
|
| 128 |
|
| 129 |
|
| 130 |
-
def
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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)
|
| 49 |
-
def
|
| 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,
|
| 57 |
-
tgt_pad = torch.zeros(B,
|
| 58 |
-
causal =
|
|
|
|
|
|
|
| 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),
|
| 70 |
with pytest.raises(TypeError):
|
| 71 |
-
layer(torch.randn(2, 3, 24), "not a tensor",
|
| 72 |
|
| 73 |
with pytest.raises(ValueError) as e1:
|
| 74 |
-
layer(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 85 |
-
assert "
|
| 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,
|
| 92 |
-
assert "
|
| 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
|
| 46 |
-
|
| 47 |
-
|
| 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
|
| 145 |
-
pe = PositionalEmbedding(
|
| 146 |
assert hasattr(pe, "pe")
|
| 147 |
assert isinstance(pe.pe, torch.Tensor)
|
| 148 |
-
assert pe.pe.shape == (1,
|
| 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)
|
| 49 |
-
def
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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),
|
| 78 |
-
|
| 79 |
-
|
| 80 |
|
| 81 |
-
logits = model(src, tgt,
|
| 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,
|
| 103 |
with pytest.raises(TypeError):
|
| 104 |
-
model(src, "not a tensor",
|
| 105 |
with pytest.raises(ValueError):
|
| 106 |
-
|
|
|
|
|
|
|
| 107 |
with pytest.raises(ValueError):
|
| 108 |
-
|
|
|
|
| 109 |
with pytest.raises(TypeError):
|
| 110 |
-
model(src.float(), tgt,
|
| 111 |
with pytest.raises(TypeError):
|
| 112 |
-
model(src, tgt.int(),
|
| 113 |
with pytest.raises(TypeError):
|
| 114 |
-
model(src, tgt, src_padding_mask="not a tensor", tgt_padding_mask=
|
| 115 |
with pytest.raises(TypeError):
|
| 116 |
-
model(src, tgt,
|
| 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),
|
| 126 |
with pytest.raises(TypeError):
|
| 127 |
-
model.encode(src.float(),
|
| 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,
|
| 133 |
with pytest.raises(ValueError):
|
| 134 |
-
model.decode(mem, tgt.unsqueeze(-1),
|
| 135 |
with pytest.raises(TypeError):
|
| 136 |
-
model.decode(mem, tgt.float(),
|
| 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,
|
| 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 =
|
| 162 |
-
tgt_mask =
|
| 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 =
|
| 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 |
-
|
|
|
|
| 235 |
assert out.shape == (2, 1) # just the BOS token
|
| 236 |
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
# -----------------------
|
| 239 |
-
# Mask broadcast sanity (boolean
|
| 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 =
|
| 253 |
-
tgt_pad_bool =
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 323 |
-
|
|
|
|
|
|
|
| 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
|
| 270 |
B, H, Sq, Sk, D = 2, 3, 4, 5, 8
|
| 271 |
q, k, v = _rand(B, H, Sq, Sk, D)
|
| 272 |
-
|
| 273 |
-
|
| 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", [(
|
| 436 |
-
def
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 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
|
| 452 |
-
# Small deterministic comparison against the textbook formula.
|
| 453 |
seq_len, dim = 4, 6
|
| 454 |
-
|
| 455 |
-
pe = sinusoidal_positional_encoding(seq_len, dim, dtype=torch.float32, device="cpu", base=base)
|
| 456 |
|
| 457 |
ref = torch.zeros_like(pe)
|
| 458 |
-
|
| 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 |
-
|
| 471 |
-
|
| 472 |
-
|
| 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
|
|
|
|
|
|
|
| 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
|
| 710 |
-
|
| 711 |
-
mask = create_causal_mask(
|
| 712 |
-
assert mask.shape == (
|
| 713 |
assert mask.dtype == torch.bool
|
| 714 |
-
assert mask.device
|
| 715 |
|
| 716 |
|
| 717 |
-
def
|
| 718 |
-
|
| 719 |
-
mask = create_causal_mask(
|
| 720 |
-
|
| 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(
|
| 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
|
| 742 |
-
|
| 743 |
-
mask = create_causal_mask(
|
| 744 |
assert mask.device.type == "cuda"
|
| 745 |
|
| 746 |
|
| 747 |
-
def
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|