lhallee commited on
Commit
fd19e93
·
verified ·
1 Parent(s): b8fe65d

Update FastPLMs files

Browse files
README.md CHANGED
@@ -237,16 +237,13 @@ sequence-group IDs. `-1` marks padding. Omit `sequence_id` to use
237
 
238
  ESM++ supports hidden-state SAEs from the official
239
  [Biohub ESMC SAE collection](https://huggingface.co/collections/biohub/esmc-saes-for-hidden-states-all-layers).
240
- Select an SAE for this ESMC scale. Load only required layers. Then attach them
241
- to the model:
242
 
243
  ```python
244
  import torch
245
- from transformers import AutoModel
246
 
247
- sae = AutoModel.from_pretrained("biohub/ESMC-600M-sae-layer27-k64-codebook65536", device=model.device)
248
- sae.initialize_layers([27])
249
- model.add_sae_models([sae.layers["27"]])
250
 
251
  with torch.inference_mode():
252
  output = model(**batch, normalize_sae=True)
@@ -255,6 +252,12 @@ features = output.sae_outputs["layer27"]
255
  print(features.shape, features.layout) # (valid_token_count, codebook_dim), sparse COO
256
  ```
257
 
 
 
 
 
 
 
258
  SAEs run after you attach them. Use `compute_sae=False` to skip SAE work.
259
  Outputs are detached sparse tensors with keys such as `layer{N}`. They omit
260
  padding. The model uses `sequence_id`, then `attention_mask`, for padding.
 
237
 
238
  ESM++ supports hidden-state SAEs from the official
239
  [Biohub ESMC SAE collection](https://huggingface.co/collections/biohub/esmc-saes-for-hidden-states-all-layers).
240
+ This artifact implements the SAE contract, so no Biohub runtime code is needed.
241
+ Select an SAE for this ESMC scale, then load only the layers you need:
242
 
243
  ```python
244
  import torch
 
245
 
246
+ model.load_sae_models("biohub/ESMC-600M-sae-layer27-k64-codebook65536", [27])
 
 
247
 
248
  with torch.inference_mode():
249
  output = model(**batch, normalize_sae=True)
 
252
  print(features.shape, features.layout) # (valid_token_count, codebook_dim), sparse COO
253
  ```
254
 
255
+ `load_sae_models` reads the shared `config.json` and one
256
+ `layer_{index}.safetensors` shard per requested layer, from a Hub repository
257
+ or a local directory, and attaches the layers on the model device in the model
258
+ dtype. `add_sae_models` still accepts official Biohub `ESMCSAEModel.layers`
259
+ entries.
260
+
261
  SAEs run after you attach them. Use `compute_sae=False` to skip SAE work.
262
  Outputs are detached sparse tensors with keys such as `layer{N}`. They omit
263
  padding. The model uses `sequence_id`, then `attention_mask`, for padding.
fastplms/models/esm_plusplus/modeling_esm_plusplus.py CHANGED
@@ -5,6 +5,8 @@ from __future__ import annotations
5
  import importlib
6
  import importlib.metadata
7
  import math
 
 
8
  from contextlib import contextmanager
9
  from dataclasses import asdict, dataclass
10
  from functools import partial
@@ -25,6 +27,8 @@ from transformers.modeling_outputs import (
25
  TokenClassifierOutput,
26
  )
27
 
 
 
28
 
29
  try:
30
  from fastplms.attention import (
@@ -1115,14 +1119,48 @@ class PreTrainedESMplusplusModel(FastPLMsAttentionMixin, PreTrainedModel):
1115
  )
1116
  return self._esmc_precision_status
1117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
  def add_sae_models(self, sae_models: list[nn.Module]) -> None:
1119
- """Attach official Biohub hidden-state SAE layers to this ESM++ model."""
 
 
 
 
1120
 
1121
  for sae_model in sae_models:
1122
  if not isinstance(sae_model, nn.Module):
1123
  raise TypeError(
1124
- "Each SAE must be an nn.Module obtained from an official Biohub "
1125
- "ESMCSAEModel.layers entry."
1126
  )
1127
  layer = getattr(sae_model, "layer", None)
1128
  if isinstance(layer, bool) or not isinstance(layer, int):
@@ -1162,11 +1200,14 @@ class PreTrainedESMplusplusModel(FastPLMsAttentionMixin, PreTrainedModel):
1162
  ) -> tuple[tuple[int, ...], torch.Tensor | None]:
1163
  if not compute_sae or not self._sae_models:
1164
  return (), None
1165
- if input_ids is None:
 
 
1166
  raise ValueError(
1167
- "SAE computation requires input_ids so masked-token inputs can be rejected."
 
1168
  )
1169
- if torch.any(input_ids == self.config.mask_token_id):
1170
  raise ValueError("SAE inputs must not contain mask tokens; SAEs were trained unmasked.")
1171
  if sequence_id is not None:
1172
  token_mask = sequence_id >= 0
@@ -1183,7 +1224,15 @@ class PreTrainedESMplusplusModel(FastPLMsAttentionMixin, PreTrainedModel):
1183
  token_mask: torch.Tensor | None,
1184
  *,
1185
  normalize_sae: bool,
 
1186
  ) -> dict[str, torch.Tensor] | None:
 
 
 
 
 
 
 
1187
  if not self._sae_models:
1188
  return None
1189
  if hidden_states is None or token_mask is None:
@@ -1193,14 +1242,18 @@ class PreTrainedESMplusplusModel(FastPLMsAttentionMixin, PreTrainedModel):
1193
  layer = int(key.removeprefix("layer"))
1194
  if layer not in hidden_states:
1195
  raise RuntimeError(f"ESM++ did not collect the requested SAE layer {layer}.")
1196
- sae_output = sae_model.get_sae_output(hidden_states[layer].clone(), token_mask)
 
 
 
1197
  features = getattr(sae_output, "feature_magnitudes", None)
1198
  if not isinstance(features, torch.Tensor):
1199
  raise TypeError("SAE get_sae_output must return tensor feature_magnitudes.")
1200
- features = features.detach()
 
1201
  if normalize_sae:
1202
  features = (features / sae_model.max) * sae_model.idf
1203
- outputs[key] = features.to_sparse()
1204
  return outputs
1205
 
1206
  def _pad_fp8_inputs(
@@ -1422,6 +1475,7 @@ class ESMplusplusModel(PreTrainedESMplusplusModel, EmbeddingMixin):
1422
  return_dict: bool | None = None,
1423
  compute_sae: bool = True,
1424
  normalize_sae: bool = False,
 
1425
  ) -> TransformerOutput | tuple[torch.Tensor, ...]:
1426
  """Run ESMC inference with the pinned Biohub mask precedence.
1427
 
@@ -1475,6 +1529,7 @@ class ESMplusplusModel(PreTrainedESMplusplusModel, EmbeddingMixin):
1475
  transformer_output.sae_hidden_states,
1476
  sae_token_mask,
1477
  normalize_sae=normalize_sae,
 
1478
  )
1479
  if sae_layers
1480
  else None
@@ -1571,6 +1626,7 @@ class ESMplusplusForMaskedLM(
1571
  compute_logits: bool = True,
1572
  compute_sae: bool = True,
1573
  normalize_sae: bool = False,
 
1574
  ) -> ESMplusplusOutput | tuple[torch.Tensor, ...]:
1575
  if input_ids is None and inputs_embeds is None:
1576
  raise ValueError("You have to specify either input_ids or inputs_embeds")
@@ -1617,6 +1673,7 @@ class ESMplusplusForMaskedLM(
1617
  output.sae_hidden_states,
1618
  sae_token_mask,
1619
  normalize_sae=normalize_sae,
 
1620
  )
1621
  if sae_layers
1622
  else None
 
5
  import importlib
6
  import importlib.metadata
7
  import math
8
+ import os
9
+ from collections.abc import Sequence
10
  from contextlib import contextmanager
11
  from dataclasses import asdict, dataclass
12
  from functools import partial
 
27
  TokenClassifierOutput,
28
  )
29
 
30
+ from .modeling_esm_plusplus_sae import ESMplusplusSAELayer, load_esmc_sae_layers
31
+
32
 
33
  try:
34
  from fastplms.attention import (
 
1119
  )
1120
  return self._esmc_precision_status
1121
 
1122
+ def load_sae_models(
1123
+ self,
1124
+ repository: str | os.PathLike[str],
1125
+ layers: Sequence[int],
1126
+ *,
1127
+ revision: str | None = None,
1128
+ cache_dir: str | os.PathLike[str] | None = None,
1129
+ token: str | bool | None = None,
1130
+ local_files_only: bool = False,
1131
+ dtype: torch.dtype | None = None,
1132
+ ) -> dict[int, ESMplusplusSAELayer]:
1133
+ """Load hidden-state SAE layers from a Hub repository or local directory, then attach them.
1134
+
1135
+ The layers land on this model's device and, unless ``dtype`` says otherwise, in this
1136
+ model's parameter dtype, so they consume its hidden states without a dtype mismatch.
1137
+ """
1138
+
1139
+ sae_layers = load_esmc_sae_layers(
1140
+ repository,
1141
+ layers,
1142
+ revision=revision,
1143
+ cache_dir=cache_dir,
1144
+ token=token,
1145
+ local_files_only=local_files_only,
1146
+ device=self.device,
1147
+ dtype=self.dtype if dtype is None else dtype,
1148
+ )
1149
+ self.add_sae_models(list(sae_layers.values()))
1150
+ return sae_layers
1151
+
1152
  def add_sae_models(self, sae_models: list[nn.Module]) -> None:
1153
+ """Attach hidden-state SAE layers to this ESM++ model.
1154
+
1155
+ Accepts layers from ``load_sae_models`` and official Biohub ``ESMCSAEModel.layers``
1156
+ entries, which share one attachment contract.
1157
+ """
1158
 
1159
  for sae_model in sae_models:
1160
  if not isinstance(sae_model, nn.Module):
1161
  raise TypeError(
1162
+ "Each SAE must be an nn.Module exposing the hidden-state SAE contract, such "
1163
+ "as a load_sae_models layer or an official Biohub ESMCSAEModel.layers entry."
1164
  )
1165
  layer = getattr(sae_model, "layer", None)
1166
  if isinstance(layer, bool) or not isinstance(layer, int):
 
1200
  ) -> tuple[tuple[int, ...], torch.Tensor | None]:
1201
  if not compute_sae or not self._sae_models:
1202
  return (), None
1203
+ if input_ids is None and attention_mask is None and sequence_id is None:
1204
+ # Embedding inputs carry no token identities, so the caller must supply the mask.
1205
+ # Rejecting masked tokens is then the caller's precondition rather than ours.
1206
  raise ValueError(
1207
+ "SAE computation from inputs_embeds requires an explicit attention_mask or "
1208
+ "sequence_id, because the token mask cannot be recovered from embeddings."
1209
  )
1210
+ if input_ids is not None and torch.any(input_ids == self.config.mask_token_id):
1211
  raise ValueError("SAE inputs must not contain mask tokens; SAEs were trained unmasked.")
1212
  if sequence_id is not None:
1213
  token_mask = sequence_id >= 0
 
1224
  token_mask: torch.Tensor | None,
1225
  *,
1226
  normalize_sae: bool,
1227
+ differentiable_sae: bool = False,
1228
  ) -> dict[str, torch.Tensor] | None:
1229
+ """Encode collected hidden states with every attached SAE.
1230
+
1231
+ The default detaches and sparsifies, which is right for interpretation and matches the
1232
+ official implementation. ``differentiable_sae`` instead keeps the result attached to the
1233
+ graph and dense, so a gradient-based sequence designer can optimize an objective built on
1234
+ SAE features. The arithmetic is identical either way; only the tape and the layout differ.
1235
+ """
1236
  if not self._sae_models:
1237
  return None
1238
  if hidden_states is None or token_mask is None:
 
1242
  layer = int(key.removeprefix("layer"))
1243
  if layer not in hidden_states:
1244
  raise RuntimeError(f"ESM++ did not collect the requested SAE layer {layer}.")
1245
+ layer_states = hidden_states[layer]
1246
+ sae_output = sae_model.get_sae_output(
1247
+ layer_states if differentiable_sae else layer_states.clone(), token_mask
1248
+ )
1249
  features = getattr(sae_output, "feature_magnitudes", None)
1250
  if not isinstance(features, torch.Tensor):
1251
  raise TypeError("SAE get_sae_output must return tensor feature_magnitudes.")
1252
+ if not differentiable_sae:
1253
+ features = features.detach()
1254
  if normalize_sae:
1255
  features = (features / sae_model.max) * sae_model.idf
1256
+ outputs[key] = features if differentiable_sae else features.to_sparse()
1257
  return outputs
1258
 
1259
  def _pad_fp8_inputs(
 
1475
  return_dict: bool | None = None,
1476
  compute_sae: bool = True,
1477
  normalize_sae: bool = False,
1478
+ differentiable_sae: bool = False,
1479
  ) -> TransformerOutput | tuple[torch.Tensor, ...]:
1480
  """Run ESMC inference with the pinned Biohub mask precedence.
1481
 
 
1529
  transformer_output.sae_hidden_states,
1530
  sae_token_mask,
1531
  normalize_sae=normalize_sae,
1532
+ differentiable_sae=differentiable_sae,
1533
  )
1534
  if sae_layers
1535
  else None
 
1626
  compute_logits: bool = True,
1627
  compute_sae: bool = True,
1628
  normalize_sae: bool = False,
1629
+ differentiable_sae: bool = False,
1630
  ) -> ESMplusplusOutput | tuple[torch.Tensor, ...]:
1631
  if input_ids is None and inputs_embeds is None:
1632
  raise ValueError("You have to specify either input_ids or inputs_embeds")
 
1673
  output.sae_hidden_states,
1674
  sae_token_mask,
1675
  normalize_sae=normalize_sae,
1676
+ differentiable_sae=differentiable_sae,
1677
  )
1678
  if sae_layers
1679
  else None
fastplms/models/esm_plusplus/modeling_esm_plusplus_sae.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hidden-state sparse autoencoders for ESM++ (ESMC) models.
2
+
3
+ FastPLMs implements the published Biohub hidden-state SAE contract directly, so attaching an SAE
4
+ needs only PyTorch, Transformers, and the checkpoint itself. Biohub still owns the SAE weights:
5
+ this module reads their published repository layout, one shared ``config.json`` plus one
6
+ ``layer_{index}.safetensors`` shard per backbone layer, and never redistributes those tensors.
7
+
8
+ A layer produced here satisfies the same attachment contract as an official Biohub
9
+ ``ESMCSAEModel.layers`` entry, so ``PreTrainedESMplusplusModel.add_sae_models`` accepts either.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import torch
17
+ import torch.nn as nn
18
+ import torch.nn.functional as F
19
+
20
+ from collections.abc import Sequence
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from huggingface_hub import hf_hub_download
24
+ from safetensors.torch import load_file
25
+ from torch import Tensor
26
+
27
+
28
+ SAE_CONFIG_FILE = "config.json"
29
+ SAE_REQUIRED_CONFIG_FIELDS = ("d_model", "codebook_dim", "k")
30
+ # Trained per-feature statistics. Shards that never trained them omit them, and the ones default
31
+ # makes the (features / max) * idf normalization an identity.
32
+ SAE_OPTIONAL_STATE_NAMES = ("idf", "max")
33
+ STANDARDIZATION_EPSILON = 1e-5
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ESMplusplusSAEParams:
38
+ """Shape contract of one hidden-state SAE.
39
+
40
+ ``layer`` indexes the ESM++ hidden state the SAE reads, where ``0`` is the embedding output and
41
+ ``num_hidden_layers`` is the final normalized state.
42
+ """
43
+
44
+ d_model: int
45
+ codebook_dim: int
46
+ k: int
47
+ layer: int
48
+
49
+
50
+ @dataclass(slots=True)
51
+ class ESMplusplusSAEOutput:
52
+ """Sparse features, and the optional reconstruction error, for one batch of residues."""
53
+
54
+ feature_magnitudes: Tensor
55
+ reconstruction_loss: Tensor | None = None
56
+
57
+
58
+ def _standardize_residue_states(residue_states: Tensor) -> Tensor:
59
+ """Center and scale each residue vector the way the SAEs were trained."""
60
+
61
+ # residue_states: (n, d)
62
+ centered = residue_states - residue_states.mean(dim=-1, keepdim=True) # (n, d)
63
+ return centered / (centered.std(dim=-1, keepdim=True) + STANDARDIZATION_EPSILON) # (n, d)
64
+
65
+
66
+ class ESMplusplusSAELayer(nn.Module):
67
+ """Top-k sparse autoencoder over one ESM++ hidden state.
68
+
69
+ Encoding standardizes each residue vector, projects it into a wide codebook, and keeps only the
70
+ ``k`` largest activations. The decoder is used only to measure reconstruction error, which is
71
+ why it is opt-in: interpretation and gradient-based design read ``feature_magnitudes`` alone.
72
+ """
73
+
74
+ idf: Tensor
75
+ max: Tensor
76
+
77
+ def __init__(self, params: ESMplusplusSAEParams) -> None:
78
+ super().__init__()
79
+ self.params = params
80
+ self.W_enc = nn.Parameter(torch.empty(params.d_model, params.codebook_dim)) # (d, c)
81
+ self.W_dec = nn.Parameter(torch.empty(params.codebook_dim, params.d_model)) # (c, d)
82
+ self.b_dec = nn.Parameter(torch.zeros(params.d_model)) # (d,)
83
+ self.register_buffer("idf", torch.ones(params.codebook_dim)) # (c,)
84
+ self.register_buffer("max", torch.ones(params.codebook_dim)) # (c,)
85
+
86
+ @property
87
+ def layer(self) -> int:
88
+ """ESM++ hidden-state index this SAE was trained against."""
89
+
90
+ return self.params.layer
91
+
92
+ def forward(
93
+ self,
94
+ residue_states: Tensor,
95
+ *,
96
+ compute_reconstruction_loss: bool = False,
97
+ ) -> ESMplusplusSAEOutput:
98
+ # residue_states: (n, d) for n residues; c is the codebook width, k the retained features
99
+ standardized = _standardize_residue_states(residue_states) # (n, d)
100
+ preactivations = F.relu((standardized - self.b_dec) @ self.W_enc) # (n, c)
101
+ retained = torch.topk(preactivations, self.params.k, dim=-1) # values, indices: (n, k)
102
+ feature_magnitudes = torch.zeros_like(preactivations).scatter(
103
+ -1, retained.indices, retained.values
104
+ ) # (n, c)
105
+ if not compute_reconstruction_loss:
106
+ return ESMplusplusSAEOutput(feature_magnitudes=feature_magnitudes)
107
+
108
+ reconstructed = feature_magnitudes @ self.W_dec + self.b_dec # (n, d)
109
+ return ESMplusplusSAEOutput(
110
+ feature_magnitudes=feature_magnitudes,
111
+ reconstruction_loss=(reconstructed - standardized).pow(2).mean(dim=-1), # (n,)
112
+ )
113
+
114
+ def get_sae_output(self, layer_states: Tensor, token_mask: Tensor) -> ESMplusplusSAEOutput:
115
+ """Encode the unpadded residues of one ESM++ hidden state.
116
+
117
+ This name and signature are the attachment contract that
118
+ ``PreTrainedESMplusplusModel.add_sae_models`` validates and calls.
119
+ """
120
+
121
+ # layer_states: (b, l, d); token_mask: (b, l)
122
+ residue_states = layer_states[token_mask] # (n, d) for n valid tokens
123
+ encoded: ESMplusplusSAEOutput = self(residue_states)
124
+ return encoded
125
+
126
+
127
+ def _repository_file(
128
+ repository: str | os.PathLike[str],
129
+ filename: str,
130
+ *,
131
+ revision: str | None,
132
+ cache_dir: str | os.PathLike[str] | None,
133
+ token: str | bool | None,
134
+ local_files_only: bool,
135
+ ) -> Path:
136
+ """Resolve one repository file, from a local directory or the Hub cache."""
137
+
138
+ # A directory counts as local only when it holds the shared config, so a stale directory named
139
+ # like a Hub identifier cannot shadow the download.
140
+ local_directory = Path(repository)
141
+ if (local_directory / SAE_CONFIG_FILE).is_file():
142
+ path = local_directory / filename
143
+ if not path.is_file():
144
+ raise FileNotFoundError(f"SAE repository {local_directory} has no {filename}.")
145
+ return path
146
+
147
+ return Path(
148
+ hf_hub_download(
149
+ repo_id=str(repository),
150
+ filename=filename,
151
+ revision=revision,
152
+ cache_dir=None if cache_dir is None else str(cache_dir),
153
+ token=token,
154
+ local_files_only=local_files_only,
155
+ )
156
+ )
157
+
158
+
159
+ def _load_sae_layer(
160
+ shard_path: Path,
161
+ *,
162
+ params: ESMplusplusSAEParams,
163
+ device: torch.device,
164
+ dtype: torch.dtype | None,
165
+ ) -> ESMplusplusSAELayer:
166
+ state = load_file(str(shard_path), device=str(device))
167
+ encoder = state.get("W_enc")
168
+ if encoder is None:
169
+ raise ValueError(f"{shard_path} is not an ESMC SAE shard; it has no 'W_enc' entry.")
170
+
171
+ # Build on the meta device so the shard tensors are the only materialized copy of a codebook
172
+ # that reaches roughly one gigabyte for the widest published SAEs.
173
+ with torch.device("meta"):
174
+ sae_layer = ESMplusplusSAELayer(params)
175
+ sae_layer.to(dtype=encoder.dtype if dtype is None else dtype)
176
+ sae_layer.to_empty(device=device)
177
+ # to_empty leaves the statistics buffers uninitialized, so restore the identity defaults that
178
+ # shards without trained statistics rely on.
179
+ sae_layer.idf.fill_(1.0)
180
+ sae_layer.max.fill_(1.0)
181
+
182
+ incompatible = sae_layer.load_state_dict(state, strict=False)
183
+ missing = tuple(
184
+ name for name in incompatible.missing_keys if name not in SAE_OPTIONAL_STATE_NAMES
185
+ )
186
+ unexpected = tuple(incompatible.unexpected_keys)
187
+ if missing or unexpected:
188
+ raise ValueError(
189
+ f"{shard_path} does not match the ESMC SAE state contract; "
190
+ f"missing {list(missing)}, unexpected {list(unexpected)}."
191
+ )
192
+ return sae_layer
193
+
194
+
195
+ def load_esmc_sae_layers(
196
+ repository: str | os.PathLike[str],
197
+ layers: Sequence[int],
198
+ *,
199
+ revision: str | None = None,
200
+ cache_dir: str | os.PathLike[str] | None = None,
201
+ token: str | bool | None = None,
202
+ local_files_only: bool = False,
203
+ device: torch.device | str = "cpu",
204
+ dtype: torch.dtype | None = None,
205
+ ) -> dict[int, ESMplusplusSAELayer]:
206
+ """Load the requested hidden-state SAE layers from a Hub repository or a local directory.
207
+
208
+ Only the shared config and the requested shards are read, so a repository that publishes every
209
+ backbone layer costs one shard per requested layer. ``dtype`` defaults to the dtype stored in
210
+ the shard; pass the ESM++ model dtype when the SAE has to consume its hidden states directly.
211
+ """
212
+
213
+ requested = tuple(dict.fromkeys(int(layer) for layer in layers))
214
+ if not requested:
215
+ raise ValueError("Loading SAE layers requires at least one backbone layer index.")
216
+
217
+ config_path = _repository_file(
218
+ repository,
219
+ SAE_CONFIG_FILE,
220
+ revision=revision,
221
+ cache_dir=cache_dir,
222
+ token=token,
223
+ local_files_only=local_files_only,
224
+ )
225
+ config = json.loads(config_path.read_text(encoding="utf-8"))
226
+ missing_fields = [name for name in SAE_REQUIRED_CONFIG_FIELDS if name not in config]
227
+ if missing_fields:
228
+ raise ValueError(f"{config_path} is not an ESMC SAE config; it omits {missing_fields}.")
229
+ available = tuple(int(index) for index in config.get("available_layers", ()))
230
+
231
+ target_device = torch.device(device)
232
+ sae_layers: dict[int, ESMplusplusSAELayer] = {}
233
+ for layer in requested:
234
+ if available and layer not in available:
235
+ raise ValueError(
236
+ f"SAE repository {repository} does not publish layer {layer}; "
237
+ f"available layers are {list(available)}."
238
+ )
239
+ shard_path = _repository_file(
240
+ repository,
241
+ f"layer_{layer}.safetensors",
242
+ revision=revision,
243
+ cache_dir=cache_dir,
244
+ token=token,
245
+ local_files_only=local_files_only,
246
+ )
247
+ sae_layers[layer] = _load_sae_layer(
248
+ shard_path,
249
+ params=ESMplusplusSAEParams(
250
+ d_model=int(config["d_model"]),
251
+ codebook_dim=int(config["codebook_dim"]),
252
+ k=int(config["k"]),
253
+ layer=layer,
254
+ ),
255
+ device=target_device,
256
+ dtype=dtype,
257
+ )
258
+ return sae_layers
259
+
260
+
261
+ __all__ = [
262
+ "ESMplusplusSAELayer",
263
+ "ESMplusplusSAEOutput",
264
+ "ESMplusplusSAEParams",
265
+ "load_esmc_sae_layers",
266
+ ]
fastplms_bundle.py CHANGED
The diff for this file is too large to render. See raw diff
 
modeling_fastplms.py CHANGED
@@ -13,7 +13,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
13
 
14
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
15
 
16
- if RUNTIME_HASH != "65b4cb38129822c36a448e295f77702ee64ca00fcfb9265b8c60f1c3ef169097":
17
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
18
 
19
  _RUNTIME_TEMPORARIES = []
 
13
 
14
  from .fastplms_bundle import RUNTIME_DATA, RUNTIME_HASH
15
 
16
+ if RUNTIME_HASH != "80b9bd43aa70400483b3f35121ed5039a743154f5b44b872578b5a12bf95bdd0":
17
  raise RuntimeError("FastPLMs runtime identity differs from the bridge.")
18
 
19
  _RUNTIME_TEMPORARIES = []