Coverage for transformer_lens/tools/training.py: 71%
77 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""Train loop for TransformerLens models.
3Utilities for training models on autoregressive language modeling tasks.
4Typed against the ``model_protocol`` surface (``__call__`` with
5``return_type="loss"`` plus standard ``nn.Module`` parameter access), so any
6conforming model — ``TransformerBridge`` foremost — works through this loop.
7"""
9import dataclasses
10from dataclasses import dataclass
11from typing import Optional, Union
13import torch
14import torch.optim as optim
15from torch.optim import Optimizer
16from torch.utils.data import DataLoader, Dataset
17from tqdm.auto import tqdm
19from transformer_lens import utilities as utils
20from transformer_lens.model_protocol import TrainableTransformerLensModel
21from transformer_lens.utilities.library_utils import is_library_available
24@dataclass
25class TrainConfig:
26 """Configuration class to store training hyperparameters for a training run.
28 Args:
29 num_epochs (int): Number of epochs to train for
30 batch_size (int): Size of batches to use for training
31 lr (float): Learning rate to use for training
32 seed (int): Random seed to use for training
33 momentum (float): Momentum to use for training
34 max_grad_norm (float, *optional*): Maximum gradient norm to use for
35 weight_decay (float, *optional*): Weight decay to use for training
36 optimizer_name (str): The name of the optimizer to use
37 device (str or torch.device, *optional*): Device to use for training
38 warmup_steps (int, *optional*): Number of warmup steps to use for training
39 save_every (int, *optional*): After how many batches should a checkpoint be saved
40 save_dir, (str, *optional*): Where to save checkpoints
41 wandb (bool): Whether to use Weights and Biases for logging
42 wandb_project (str, *optional*): Name of the Weights and Biases project to use
43 print_every (int, *optional*): Print the loss every n steps
44 max_steps (int, *optional*): Terminate the epoch after this many steps. Used for debugging.
45 """
47 num_epochs: int
48 batch_size: int
49 lr: float = 1e-3
50 seed: int = 0
51 momentum: float = 0.0
52 max_grad_norm: Optional[float] = None
53 weight_decay: Optional[float] = None
54 optimizer_name: str = "Adam"
55 device: Optional[Union[str, torch.device]] = None
56 warmup_steps: int = 0
57 save_every: Optional[int] = None
58 save_dir: Optional[str] = None
59 wandb: bool = False
60 wandb_project_name: Optional[str] = None
61 print_every: Optional[int] = 50
62 max_steps: Optional[int] = None
65def train(
66 model: TrainableTransformerLensModel,
67 config: TrainConfig,
68 dataset: Dataset,
69) -> TrainableTransformerLensModel:
70 """Train a model on an autoregressive language modeling task.
72 Args:
73 model: The model to train (TrainableTransformerLensModel: callable with
74 ``return_type="loss"`` and exposing torch parameters)
75 config: The training configuration
76 dataset: The dataset to train on - assumed set up for autoregressive language modeling.
78 Returns:
79 The trained model
80 """
82 # Work on a copy: mutating the caller's config (wandb_project_name/device
83 # defaults below) was a silent side effect the caller never asked for.
84 config = dataclasses.replace(config)
86 torch.manual_seed(config.seed)
87 model.train()
89 if config.wandb: 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 if not is_library_available("wandb"):
91 raise ImportError("Wandb is not available")
93 import wandb
95 if config.wandb_project_name is None:
96 config.wandb_project_name = "easy-transformer"
97 wandb.init(project=config.wandb_project_name, config=vars(config))
99 if config.device is None:
100 config.device = utils.get_device()
102 optimizer: Optimizer
103 if config.optimizer_name in ["Adam", "AdamW"]: 103 ↛ 116line 103 didn't jump to line 116 because the condition on line 103 was always true
104 # Weight decay in Adam is implemented badly, so use AdamW instead (see PyTorch AdamW docs)
105 if config.weight_decay is not None: 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 optimizer = optim.AdamW(
107 model.parameters(),
108 lr=config.lr,
109 weight_decay=config.weight_decay,
110 )
111 else:
112 optimizer = optim.Adam(
113 model.parameters(),
114 lr=config.lr,
115 )
116 elif config.optimizer_name == "SGD":
117 optimizer = optim.SGD(
118 model.parameters(),
119 lr=config.lr,
120 weight_decay=(config.weight_decay if config.weight_decay is not None else 0.0),
121 momentum=config.momentum,
122 )
123 else:
124 raise ValueError(f"Optimizer {config.optimizer_name} not supported")
126 scheduler = None
127 if config.warmup_steps > 0: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 scheduler = optim.lr_scheduler.LambdaLR(
129 optimizer,
130 lr_lambda=lambda step: min(1.0, step / config.warmup_steps),
131 )
133 dataloader = DataLoader(dataset, batch_size=config.batch_size, shuffle=True)
135 model.to(config.device)
137 for epoch in tqdm(range(1, config.num_epochs + 1)):
138 samples = 0
139 for step, batch in tqdm(enumerate(dataloader)):
140 tokens = batch["tokens"].to(config.device)
141 loss = model(tokens, return_type="loss")
142 loss.backward()
143 if config.max_grad_norm is not None: 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true
144 torch.nn.utils.clip_grad_norm_(model.parameters(), config.max_grad_norm)
145 optimizer.step()
146 if config.warmup_steps > 0: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 assert scheduler is not None
148 scheduler.step()
149 optimizer.zero_grad()
151 samples += tokens.shape[0]
153 if config.wandb: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 wandb.log({"train_loss": loss.item(), "samples": samples, "epoch": epoch})
156 if config.print_every is not None and step % config.print_every == 0:
157 print(f"Epoch {epoch} Samples {samples} Step {step} Loss {loss.item()}")
159 if ( 159 ↛ 164line 159 didn't jump to line 164 because the condition on line 159 was never true
160 config.save_every is not None
161 and step % config.save_every == 0
162 and config.save_dir is not None
163 ):
164 torch.save(model.state_dict(), f"{config.save_dir}/model_{step}.pt")
166 if config.max_steps is not None and step >= config.max_steps: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 break
169 return model