CoolFace
Modelpublic

Synthyra/ESMFold2

sourceHugging Facemitupdated 1d agoView on Hugging Face
0likes505downloads
modeling_esmc_sae.py364 linesDownload Raw Back to root
1# Copyright 2026 Biohub. All rights reserved.2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7#     http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14"""PyTorch ESMC SAE (Sparse Autoencoder) model.15 16* :class:`ESMCSAEModel` — the published HF container, one repo per17  ``(backbone, codebook_dim, k)`` group. Each backbone layer ships as a18  ``layer_{i}.safetensors`` shard; ``from_pretrained`` downloads the whole19  snapshot but loads no weights — callers materialize the layers they need20  via :meth:`initialize_layers`. Single-layer repos auto-load so bare21  ``forward(x)`` works.22* :class:`_ESMCSAELayer` — internal ``nn.Module`` that holds the weights for23  one ``(backbone, codebook_dim, k, layer)`` SAE. Not a published HF artifact;24  obtained only via ``model.layers["<idx>"]``.25"""26 27from __future__ import annotations28 29import os30from dataclasses import dataclass31from pathlib import Path32from typing import Optional33 34import torch35import torch.nn as nn36import torch.nn.functional as F37from safetensors.torch import load_file, save_file38 39from transformers.modeling_outputs import ModelOutput40from transformers.modeling_utils import PreTrainedModel41from transformers.utils import auto_docstring42from .configuration_esmc_sae import ESMCSAEConfig, ESMCSAEParams43 44 45@dataclass46@auto_docstring(47    custom_intro="""48    Output type of [`ESMCSAEModel`].49    """50)51class ESMCSAEOutput(ModelOutput):52    feature_magnitudes: torch.Tensor53    reconstruction_loss: Optional[torch.Tensor] = None54 55    def to_sparse(self) -> None:56        self.feature_magnitudes = self.feature_magnitudes.to_sparse()57 58 59class _ESMCSAELayer(nn.Module):60    """One backbone layer's SAE — internal building block of :class:`ESMCSAEModel`.61 62    Not exposed via ``AutoModel`` and not loadable on its own. Obtain one63    via ``model.layers["<layer_idx>"]`` after calling ``initialize_layers``.64    """65 66    def __init__(self, params: ESMCSAEParams):67        super().__init__()68        self.params = params69 70        self.W_enc = nn.Parameter(torch.empty(params.d_model, params.codebook_dim))71        self.W_dec = nn.Parameter(torch.empty(params.codebook_dim, params.d_model))72        self.b_dec = nn.Parameter(torch.zeros(params.d_model))73        # Per-feature normalization stats. Trained alongside the SAE for some74        # variants; for variants that don't ship them, leaving these as ones75        # makes ``_get_sae_outputs``'s ``features / max * idf`` a no-op.76        self.register_buffer("idf", torch.ones(params.codebook_dim))77        self.register_buffer("max", torch.ones(params.codebook_dim))78 79    @property80    def layer(self) -> int:81        """Backbone-layer index this SAE is trained against."""82        return self.params.layer83 84    def forward(self, x: torch.Tensor, **_kwargs: object) -> ESMCSAEOutput:85        del _kwargs86        x = self._zscore_normalize_representation(x)87 88        x_with_pre_encoder_bias = x - self.b_dec89        preactivations = F.relu(x_with_pre_encoder_bias @ self.W_enc)90 91        topk = torch.topk(preactivations, self.params.k, dim=-1)92        feature_magnitudes = torch.zeros_like(preactivations).scatter(93            -1, topk.indices, topk.values94        )95 96        reconstructed = feature_magnitudes @ self.W_dec + self.b_dec97 98        reconstruction_loss = (reconstructed - x).pow(2).mean(dim=-1)99 100        return ESMCSAEOutput(101            feature_magnitudes=feature_magnitudes,102            reconstruction_loss=reconstruction_loss,103        )104 105    def get_sae_output(106        self, layer_states: torch.Tensor, token_mask: torch.Tensor107    ) -> ESMCSAEOutput:108        _, _, v_len = layer_states.shape109        nonpad_states = layer_states[token_mask].view(-1, v_len)110        return self(nonpad_states)111 112    def _zscore_normalize_representation(self, x: torch.Tensor) -> torch.Tensor:113        x_mean = x.mean(dim=-1, keepdim=True)114        x = x - x_mean115        x_std = x.std(dim=-1, keepdim=True)116        return x / (x_std + 1e-5)117 118 119@auto_docstring120class ESMCSAEPreTrainedModel(PreTrainedModel):121    config_class = ESMCSAEConfig122    base_model_prefix = "esmc_sae"123 124 125@auto_docstring(126    custom_intro="""127    HF container holding one SAE per backbone layer, all sharing the same128    ``(d_model, codebook_dim, k)``.129 130    ``from_pretrained`` downloads the entire repo (every ``layer_{i}.safetensors``)131    into the local HF cache but does **not** load any weights into memory.132    Callers materialize the layers they actually need by calling133    :meth:`initialize_layers`. The full set is available on disk after the134    first call, so subsequent layer switches read from the local cache without135    re-downloading.136 137    Examples::138 139        model = ESMCSAEModel.from_pretrained(140            "biohub/esmc-6b-2024-12-sae-k64-codebook16384"141        )142        model.initialize_layers([60])                  # ~2.5 GB into memory143        out = model(layer_states, layer=60)            # forward through layer 60144        model.initialize_layers([45])                  # add layer 45 (cached locally)145        model.release_layer(60)                        # free layer 60146    """147)148class ESMCSAEModel(ESMCSAEPreTrainedModel):149    def __init__(self, config: ESMCSAEConfig):150        super().__init__(config)151        # Layers are populated lazily by ``initialize_layers``; the container152        # starts empty so ``from_pretrained`` doesn't materialize hundreds of153        # GB of unused parameters.154        self.layers = nn.ModuleDict()155        # Zero-element buffer that rides along with ``.to(device/dtype)``.156        # ``initialize_layers`` reads its current device/dtype so SAEs added157        # after ``model.to("cuda")`` land on CUDA without re-passing ``device=``.158        self.register_buffer("_device_marker", torch.empty(0), persistent=False)159        self._snapshot_dir: Optional[str] = None160        self.post_init()161 162    @classmethod163    def from_pretrained(  # type: ignore[override]164        cls, pretrained_model_name_or_path: str | os.PathLike, *model_args, **kwargs165    ) -> "ESMCSAEModel":166        """Download (or reuse cached) the full repo and return the model.167 168        By default no weights are read into memory and the caller must invoke169        :meth:`initialize_layers` before running :meth:`forward`. The single170        exception is when the repo ships exactly one layer: that layer is171        auto-loaded (honoring ``torch_dtype`` / ``device`` if passed) so the172        bare ``forward(x)`` call just works.173 174        Honored kwargs: ``revision``, ``cache_dir``, ``token``,175        ``allow_patterns``, ``local_files_only``, ``force_download`` (forwarded176        to ``snapshot_download``); ``torch_dtype`` and ``device`` (used by the177        single-layer auto-load path; otherwise pass them to178        :meth:`initialize_layers`). Behavioral kwargs that imply work we do179        not perform (``device_map``, ``low_cpu_mem_usage``,180        ``quantization_config``, ``attn_implementation``) raise so the user181        isn't silently misled. Other HF housekeeping kwargs (``config``,182        ``trust_remote_code``, ``adapter_kwargs``, …) are accepted and183        ignored — they only matter for the standard loader, which we bypass.184        """185        del model_args186        torch_dtype = kwargs.pop("torch_dtype", None)187        device = kwargs.pop("device", None)188        local_dir = _resolve_snapshot_dir(pretrained_model_name_or_path, kwargs)189        unsupported = {190            "device_map",191            "low_cpu_mem_usage",192            "quantization_config",193            "attn_implementation",194            "max_memory",195            "offload_folder",196            "offload_state_dict",197        } & kwargs.keys()198        if unsupported:199            raise TypeError(200                f"Unsupported kwargs to ESMCSAEModel.from_pretrained: "201                f"{sorted(unsupported)}. The standard HF loader is bypassed —"202                " call initialize_layers(..., device=, dtype=) instead."203            )204        config = ESMCSAEConfig.from_pretrained(local_dir)205        model = cls(config)206        model._snapshot_dir = str(local_dir)207        if device is not None:208            model.to(device)209        if torch_dtype is not None:210            model.to(torch_dtype)211        if len(config.available_layers) == 1:212            model.initialize_layers(list(config.available_layers))213        return model214 215    def initialize_layers(216        self,217        layers: list[int],218        *,219        device: torch.device | str | None = None,220        dtype: torch.dtype | None = None,221    ) -> None:222        """Load the requested layers from the local snapshot into memory.223 224        Layers already present in :attr:`self.layers` are skipped — calling225        ``initialize_layers([23])`` twice is idempotent. ``device`` / ``dtype``226        default to wherever the model itself lives (via the ``_device_marker``227        buffer that moves with ``.to(...)``), so the common pattern of228        ``model.to("cuda"); model.initialize_layers([7])`` Just Works.229        """230        assert self._snapshot_dir is not None, (231            "ESMCSAEModel has no snapshot directory — call "232            "from_pretrained first, or set _snapshot_dir manually."233        )234        if device is None:235            device = self._device_marker.device236        if dtype is None:237            dtype = self._device_marker.dtype238        snapshot_dir = Path(self._snapshot_dir)239        available = set(self.config.available_layers)240        for layer_idx in layers:241            key = str(layer_idx)242            if key in self.layers:243                continue244            if layer_idx not in available:245                raise KeyError(246                    f"Layer {layer_idx} is not in this repo. "247                    f"available_layers={sorted(available)}"248                )249            shard = snapshot_dir / f"layer_{layer_idx}.safetensors"250            if not shard.exists():251                raise FileNotFoundError(252                    f"Missing layer file {shard} — config lists layer "253                    f"{layer_idx} as available but the shard is not on disk."254                )255            params = ESMCSAEParams(256                d_model=self.config.d_model,257                codebook_dim=self.config.codebook_dim,258                k=self.config.k,259                layer=layer_idx,260            )261            # Build on the meta device so we don't allocate weights that262            # ``load_state_dict`` would immediately overwrite.263            with torch.device("meta"):264                layer = _ESMCSAELayer(params)265            layer.to_empty(device=device)266            layer.load_state_dict(load_file(str(shard)))267            layer.to(dtype=dtype)268            self.layers[key] = layer269 270    def release_layer(self, layer: int) -> None:271        """Drop the named layer from memory. No-op if not loaded."""272        key = str(layer)273        if key in self.layers:274            del self.layers[key]275 276    def loaded_layers(self) -> list[int]:277        """Sorted list of layer indices currently materialized in memory."""278        return sorted(int(k) for k in self.layers.keys())279 280    def forward(281        self, x: torch.Tensor, layer: int | None = None, **kwargs: object282    ) -> ESMCSAEOutput:283        if layer is None:284            if len(self.layers) == 1:285                # Unambiguous: exactly one layer loaded → use it.286                ((_only_key, only_layer),) = self.layers.items()287                return only_layer(x, **kwargs)288            if len(self.layers) == 0:289                raise RuntimeError(290                    "No layers loaded — call "291                    f"initialize_layers([...]) first. "292                    f"available_layers={self.config.available_layers}"293                )294            raise RuntimeError(295                "Multiple layers are loaded — please select one via "296                f"forward(x, layer=<idx>). Loaded layers: {self.loaded_layers()}"297            )298        key = str(layer)299        if key not in self.layers:300            raise KeyError(301                f"Layer {layer} is not loaded. Call "302                f"initialize_layers([{layer}]) first. Loaded layers: "303                f"{self.loaded_layers()}"304            )305        return self.layers[key](x, **kwargs)306 307    def save_pretrained(  # type: ignore[override]308        self, save_directory: str | os.PathLike, *args, **kwargs309    ) -> None:310        """Write ``config.json`` plus one ``layer_{i}.safetensors`` per loaded layer.311 312        Only layers currently in :attr:`self.layers` are written.313        ``available_layers`` in the saved config is synced to what's actually314        on disk so a ``release_layer`` + ``save_pretrained`` round-trip never315        advertises a layer whose shard is missing.316        """317        del args, kwargs318        save_directory = Path(save_directory)319        save_directory.mkdir(parents=True, exist_ok=True)320        # Sync available_layers to what we're about to write — never advertise321        # a layer that isn't on disk in this repo.322        self.config.available_layers = self.loaded_layers()323        self.config.save_pretrained(str(save_directory))324        for key, layer in self.layers.items():325            shard = save_directory / f"layer_{key}.safetensors"326            save_file(327                {328                    k: v.detach().cpu().contiguous()329                    for k, v in layer.state_dict().items()330                },331                str(shard),332            )333 334 335def _resolve_snapshot_dir(336    pretrained_model_name_or_path: str | os.PathLike, kwargs: dict337) -> str:338    """Local dir → return as-is; hub id → ``snapshot_download`` it.339 340    A directory only counts as "local" if it actually contains ``config.json``,341    so a stale subdir named like a hub id (``./biohub/esmc-...``)342    doesn't accidentally shadow the hub fetch.343 344    Pops the standard ``snapshot_download`` keyword args from ``kwargs`` so345    callers can forward them via ``from_pretrained``.346    """347    path = Path(pretrained_model_name_or_path)348    if path.is_dir() and (path / "config.json").exists():349        return str(path)350    from huggingface_hub import snapshot_download351 352    return snapshot_download(353        repo_id=str(pretrained_model_name_or_path),354        revision=kwargs.pop("revision", None),355        cache_dir=kwargs.pop("cache_dir", None),356        token=kwargs.pop("token", None),357        allow_patterns=kwargs.pop("allow_patterns", None),358        local_files_only=kwargs.pop("local_files_only", False),359        force_download=kwargs.pop("force_download", False),360    )361 362 363__all__ = ["ESMCSAEModel", "ESMCSAEOutput", "ESMCSAEPreTrainedModel"]364