amachine.am_transformers.am_set_trainable
Sets requires_grad on a HuggingFace transformer model based on three independent axes — decoder layers, input embeddings, output layer (lm_head + final norm) — and prints a readable summary of what ended up trainable.
Depends on model_introspection.py for all module-finding logic; this file is purely about applying requires_grad and reporting the result.
1""" 2Sets requires_grad on a HuggingFace transformer model based on three 3independent axes — decoder layers, input embeddings, output layer (lm_head + 4final norm) — and prints a readable summary of what ended up trainable. 5 6Depends on model_introspection.py for all module-finding logic; this file is 7purely about applying requires_grad and reporting the result. 8""" 9 10from __future__ import annotations 11 12from typing import Literal 13 14import torch 15 16from .am_model_introspection import ( 17 ResolvedModel, 18 TiedEmbeddingConflictError, 19 resolve_model, 20) 21 22def set_trainable_parameters( 23 model: torch.nn.Module, 24 layers_to_train: Literal["all"] | list[int], 25 train_input_embeddings: bool, 26 train_output_layer: bool, 27 *, 28 strict_tie_check: bool = False, 29 verbose: bool = True, 30) -> list[torch.nn.Parameter]: 31 """Freeze the whole model, then selectively unfreeze: 32 - the requested decoder layers (their own norm/attention/mlp submodules 33 come along for free, since we unfreeze at the layer level) 34 - the input embedding matrix, if requested 35 - the lm_head + final pre-lm_head norm, if requested 36 37 Raises TiedEmbeddingConflictError if input/output embeddings are tied 38 (share the same weight tensor) and train_input_embeddings != train_output_layer, 39 since the architecture cannot satisfy training one without the other. 40 41 Args: 42 layers_to_train: "all", or a list of decoder layer indices (negative 43 indices allowed, e.g. [-1, -2] for the last two layers). 44 train_input_embeddings: unfreeze the input embedding matrix. 45 train_output_layer: unfreeze lm_head + the final pre-lm_head norm. 46 strict_tie_check: passed through to detect_tied_embeddings — raise 47 instead of warn on config/identity disagreement. Set True when you 48 control model construction directly. 49 verbose: print a per-component trainable/frozen summary. 50 51 Returns: 52 List of parameters with requires_grad=True, suitable for passing 53 directly to an optimizer. 54 """ 55 resolved = resolve_model(model, strict_tie_check=strict_tie_check) 56 57 if resolved.embeddings_tied and train_input_embeddings != train_output_layer: 58 raise TiedEmbeddingConflictError( 59 "Input embeddings and lm_head are tied (share the same weight tensor " 60 f"via config.tie_word_embeddings={resolved.tie_word_embeddings_config}), " 61 f"but train_input_embeddings={train_input_embeddings} and " 62 f"train_output_layer={train_output_layer} disagree. These cannot be " 63 "set independently on this model — either set both to the same " 64 "value, or untie the weights first, e.g.:\n" 65 " out = model.get_output_embeddings()\n" 66 " out.weight = torch.nn.Parameter(out.weight.clone())" 67 ) 68 69 # 1. Freeze everything. 70 for p in model.parameters(): 71 p.requires_grad = False 72 73 # 2. Unfreeze requested decoder layers. 74 num_layers = len(resolved.decoder_layers) 75 layer_indices = ( 76 list(range(num_layers)) if layers_to_train == "all" else list(layers_to_train) 77 ) 78 # Normalize negative indices for reporting purposes. 79 normalized_indices = {i % num_layers for i in layer_indices} 80 for idx in layer_indices: 81 for p in resolved.decoder_layers[idx].parameters(): 82 p.requires_grad = True 83 84 # 3. Input embeddings. 85 if train_input_embeddings and resolved.input_embeddings is not None: 86 for p in resolved.input_embeddings.parameters(): 87 p.requires_grad = True 88 89 # 4. Output layer: lm_head + final norm. 90 if train_output_layer and resolved.output_embeddings is not None: 91 for p in resolved.output_embeddings.parameters(): 92 p.requires_grad = True 93 94 if train_output_layer and resolved.final_norm is not None: 95 for p in resolved.final_norm.parameters(): 96 p.requires_grad = True 97 98 trainable_parameters = [p for p in model.parameters() if p.requires_grad] 99 if not trainable_parameters: 100 raise RuntimeError("No trainable parameters selected — check your training config.") 101 102 if verbose: 103 _print_summary( 104 model=model, 105 resolved=resolved, 106 num_layers=num_layers, 107 normalized_indices=normalized_indices, 108 train_input_embeddings=train_input_embeddings, 109 train_output_layer=train_output_layer, 110 ) 111 112 return trainable_parameters 113 114 115# --------------------------------------------------------------------------- 116# Reporting 117# --------------------------------------------------------------------------- 118 119def _count_params(module: torch.nn.Module | None) -> tuple[int, int]: 120 """Returns (trainable_count, total_count) for a module's own parameters.""" 121 if module is None: 122 return 0, 0 123 total = 0 124 trainable = 0 125 for p in module.parameters(): 126 n = p.numel() 127 total += n 128 if p.requires_grad: 129 trainable += n 130 return trainable, total 131 132 133def _fmt(n: int) -> str: 134 if n >= 1_000_000: 135 return f"{n / 1_000_000:.2f}M" 136 if n >= 1_000: 137 return f"{n / 1_000:.1f}K" 138 return str(n) 139 140 141def _print_summary( 142 model: torch.nn.Module, 143 resolved: ResolvedModel, 144 num_layers: int, 145 normalized_indices: set[int], 146 train_input_embeddings: bool, 147 train_output_layer: bool, 148) -> None: 149 lines: list[str] = [] 150 lines.append("=" * 72) 151 lines.append("Trainable parameter summary") 152 lines.append("=" * 72) 153 154 # Decoder layers, one line each so nothing is silently missed. 155 lines.append(f"\nDecoder layers ({num_layers} total):") 156 for idx in range(num_layers): 157 trainable_n, total_n = _count_params(resolved.decoder_layers[idx]) 158 status = "TRAIN" if idx in normalized_indices else "frozen" 159 marker = "x" if idx in normalized_indices else " " 160 lines.append( 161 f" [{marker}] layer {idx:>3} {status:<6} " 162 f"{_fmt(trainable_n):>8} / {_fmt(total_n):<8} trainable" 163 ) 164 165 # Input embeddings. 166 in_trainable, in_total = _count_params(resolved.input_embeddings) 167 lines.append("\nInput embeddings:") 168 if resolved.input_embeddings is None: 169 lines.append(" not found on this model") 170 else: 171 status = "TRAIN" if train_input_embeddings else "frozen" 172 lines.append( 173 f" [{'x' if train_input_embeddings else ' '}] {status:<6} " 174 f"{_fmt(in_trainable):>8} / {_fmt(in_total):<8} trainable " 175 f"({type(resolved.input_embeddings).__name__})" 176 ) 177 178 # Output layer: lm_head + final norm. 179 out_trainable, out_total = _count_params(resolved.output_embeddings) 180 norm_trainable, norm_total = _count_params(resolved.final_norm) 181 lines.append("\nOutput layer:") 182 if resolved.output_embeddings is None: 183 lines.append(" lm_head: not found on this model") 184 else: 185 status = "TRAIN" if train_output_layer else "frozen" 186 lines.append( 187 f" [{'x' if train_output_layer else ' '}] lm_head {status:<6} " 188 f"{_fmt(out_trainable):>8} / {_fmt(out_total):<8} trainable " 189 f"({type(resolved.output_embeddings).__name__})" 190 ) 191 if resolved.final_norm is None: 192 lines.append(" final norm: not found on this model (skipped, non-fatal)") 193 else: 194 status = "TRAIN" if train_output_layer else "frozen" 195 lines.append( 196 f" [{'x' if train_output_layer else ' '}] final norm {status:<6} " 197 f"{_fmt(norm_trainable):>8} / {_fmt(norm_total):<8} trainable " 198 f"({type(resolved.final_norm).__name__})" 199 ) 200 201 # Tied-embedding note. 202 lines.append("\nTied embeddings:") 203 lines.append( 204 f" {resolved.embeddings_tied} " 205 f"(config.tie_word_embeddings={resolved.tie_word_embeddings_config})" 206 ) 207 208 # Anything outside decoder layers / embeddings / lm_head / final norm 209 # that ended up trainable (e.g. unexpected top-level modules) or that 210 # exists but wasn't touched at all — catches architectures with extra 211 # top-level submodules (adapters, pooler heads, etc.) so nothing is silently 212 # missed either way. 213 accounted_ids = {id(p) for p in resolved.decoder_layers.parameters()} 214 if resolved.input_embeddings is not None: 215 accounted_ids |= {id(p) for p in resolved.input_embeddings.parameters()} 216 if resolved.output_embeddings is not None: 217 accounted_ids |= {id(p) for p in resolved.output_embeddings.parameters()} 218 if resolved.final_norm is not None: 219 accounted_ids |= {id(p) for p in resolved.final_norm.parameters()} 220 221 other_trainable = 0 222 other_total = 0 223 other_names: list[str] = [] 224 for name, p in model.named_parameters(): 225 if id(p) in accounted_ids: 226 continue 227 other_total += p.numel() 228 if p.requires_grad: 229 other_trainable += p.numel() 230 other_names.append(name) 231 232 if other_total > 0: 233 lines.append("\nOther top-level parameters (outside layers/embeddings/lm_head/norm):") 234 lines.append( 235 f" {_fmt(other_trainable)} / {_fmt(other_total)} trainable" 236 ) 237 if other_names: 238 lines.append(f" trainable: {', '.join(other_names[:10])}" 239 + (" ..." if len(other_names) > 10 else "")) 240 241 # Grand total. 242 total_trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) 243 total_all = sum(p.numel() for p in model.parameters()) 244 pct = 100 * total_trainable / total_all if total_all else 0.0 245 lines.append("\n" + "-" * 72) 246 lines.append( 247 f"TOTAL: {_fmt(total_trainable)} / {_fmt(total_all)} trainable ({pct:.2f}%)" 248 ) 249 lines.append("=" * 72) 250 251 print("\n".join(lines))
def
set_trainable_parameters( model: torch.nn.modules.module.Module, layers_to_train: Union[Literal['all'], list[int]], train_input_embeddings: bool, train_output_layer: bool, *, strict_tie_check: bool = False, verbose: bool = True) -> list[torch.nn.parameter.Parameter]:
23def set_trainable_parameters( 24 model: torch.nn.Module, 25 layers_to_train: Literal["all"] | list[int], 26 train_input_embeddings: bool, 27 train_output_layer: bool, 28 *, 29 strict_tie_check: bool = False, 30 verbose: bool = True, 31) -> list[torch.nn.Parameter]: 32 """Freeze the whole model, then selectively unfreeze: 33 - the requested decoder layers (their own norm/attention/mlp submodules 34 come along for free, since we unfreeze at the layer level) 35 - the input embedding matrix, if requested 36 - the lm_head + final pre-lm_head norm, if requested 37 38 Raises TiedEmbeddingConflictError if input/output embeddings are tied 39 (share the same weight tensor) and train_input_embeddings != train_output_layer, 40 since the architecture cannot satisfy training one without the other. 41 42 Args: 43 layers_to_train: "all", or a list of decoder layer indices (negative 44 indices allowed, e.g. [-1, -2] for the last two layers). 45 train_input_embeddings: unfreeze the input embedding matrix. 46 train_output_layer: unfreeze lm_head + the final pre-lm_head norm. 47 strict_tie_check: passed through to detect_tied_embeddings — raise 48 instead of warn on config/identity disagreement. Set True when you 49 control model construction directly. 50 verbose: print a per-component trainable/frozen summary. 51 52 Returns: 53 List of parameters with requires_grad=True, suitable for passing 54 directly to an optimizer. 55 """ 56 resolved = resolve_model(model, strict_tie_check=strict_tie_check) 57 58 if resolved.embeddings_tied and train_input_embeddings != train_output_layer: 59 raise TiedEmbeddingConflictError( 60 "Input embeddings and lm_head are tied (share the same weight tensor " 61 f"via config.tie_word_embeddings={resolved.tie_word_embeddings_config}), " 62 f"but train_input_embeddings={train_input_embeddings} and " 63 f"train_output_layer={train_output_layer} disagree. These cannot be " 64 "set independently on this model — either set both to the same " 65 "value, or untie the weights first, e.g.:\n" 66 " out = model.get_output_embeddings()\n" 67 " out.weight = torch.nn.Parameter(out.weight.clone())" 68 ) 69 70 # 1. Freeze everything. 71 for p in model.parameters(): 72 p.requires_grad = False 73 74 # 2. Unfreeze requested decoder layers. 75 num_layers = len(resolved.decoder_layers) 76 layer_indices = ( 77 list(range(num_layers)) if layers_to_train == "all" else list(layers_to_train) 78 ) 79 # Normalize negative indices for reporting purposes. 80 normalized_indices = {i % num_layers for i in layer_indices} 81 for idx in layer_indices: 82 for p in resolved.decoder_layers[idx].parameters(): 83 p.requires_grad = True 84 85 # 3. Input embeddings. 86 if train_input_embeddings and resolved.input_embeddings is not None: 87 for p in resolved.input_embeddings.parameters(): 88 p.requires_grad = True 89 90 # 4. Output layer: lm_head + final norm. 91 if train_output_layer and resolved.output_embeddings is not None: 92 for p in resolved.output_embeddings.parameters(): 93 p.requires_grad = True 94 95 if train_output_layer and resolved.final_norm is not None: 96 for p in resolved.final_norm.parameters(): 97 p.requires_grad = True 98 99 trainable_parameters = [p for p in model.parameters() if p.requires_grad] 100 if not trainable_parameters: 101 raise RuntimeError("No trainable parameters selected — check your training config.") 102 103 if verbose: 104 _print_summary( 105 model=model, 106 resolved=resolved, 107 num_layers=num_layers, 108 normalized_indices=normalized_indices, 109 train_input_embeddings=train_input_embeddings, 110 train_output_layer=train_output_layer, 111 ) 112 113 return trainable_parameters
Freeze the whole model, then selectively unfreeze:
- the requested decoder layers (their own norm/attention/mlp submodules come along for free, since we unfreeze at the layer level)
- the input embedding matrix, if requested
- the lm_head + final pre-lm_head norm, if requested
Raises TiedEmbeddingConflictError if input/output embeddings are tied (share the same weight tensor) and train_input_embeddings != train_output_layer, since the architecture cannot satisfy training one without the other.
Arguments:
- layers_to_train: "all", or a list of decoder layer indices (negative indices allowed, e.g. [-1, -2] for the last two layers).
- train_input_embeddings: unfreeze the input embedding matrix.
- train_output_layer: unfreeze lm_head + the final pre-lm_head norm.
- strict_tie_check: passed through to detect_tied_embeddings — raise instead of warn on config/identity disagreement. Set True when you control model construction directly.
- verbose: print a per-component trainable/frozen summary.
Returns:
List of parameters with requires_grad=True, suitable for passing directly to an optimizer.